feat(auth): implement user and device authentication
This commit is contained in:
@@ -15,10 +15,14 @@ import (
|
||||
"cmroubao/backend-api/internal/platform/assetstore"
|
||||
"cmroubao/backend-api/internal/platform/database"
|
||||
"cmroubao/backend-api/internal/platform/migration"
|
||||
"cmroubao/backend-api/internal/platform/password"
|
||||
repository "cmroubao/backend-api/internal/repository/sqlite"
|
||||
"cmroubao/backend-api/internal/transport/authcommon"
|
||||
"cmroubao/backend-api/internal/transport/httpapi"
|
||||
"cmroubao/backend-api/internal/transport/webui"
|
||||
"cmroubao/backend-api/internal/usecase"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -67,6 +71,13 @@ func run() error {
|
||||
|
||||
serverErrors := make(chan error, 1)
|
||||
go func() {
|
||||
if cfg.TLSCertificate != "" {
|
||||
serverErrors <- server.ListenAndServeTLS(
|
||||
cfg.TLSCertificate,
|
||||
cfg.TLSPrivateKey,
|
||||
)
|
||||
return
|
||||
}
|
||||
serverErrors <- server.ListenAndServe()
|
||||
}()
|
||||
|
||||
@@ -158,6 +169,20 @@ func buildRouter(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
passwords, err := password.NewBcrypt(12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth, err := usecase.NewAuthService(
|
||||
store,
|
||||
passwords,
|
||||
clock,
|
||||
ids,
|
||||
usecase.CryptoTokenGenerator{},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webService, err := webui.NewUsecaseAdapter(tasks, assets)
|
||||
if err != nil {
|
||||
@@ -171,6 +196,33 @@ func buildRouter(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authWebService, err := webui.NewAuthUsecaseAdapter(auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loginLimiter, err := authcommon.NewAttemptLimiter(
|
||||
10,
|
||||
5*time.Minute,
|
||||
4096,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authWebHandler, err := webui.NewAuthHandler(
|
||||
authWebService,
|
||||
renderer,
|
||||
loginLimiter,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registerPublicAuth, err := httpapi.NewPublicAuthRegistrar(
|
||||
auth,
|
||||
loginLimiter,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registerAdminRoutes, err := httpapi.NewAdminRouteRegistrar(
|
||||
httpapi.AdminServices{
|
||||
Assets: assets,
|
||||
@@ -181,9 +233,22 @@ func buildRouter(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registerProtectedRoutes := func(routes gin.IRoutes) error {
|
||||
if err := registerAdminRoutes(routes); err != nil {
|
||||
return err
|
||||
}
|
||||
authWebHandler.RegisterProtected(routes)
|
||||
return nil
|
||||
}
|
||||
return httpapi.NewRouter(httpapi.RouterDependencies{
|
||||
Database: db,
|
||||
RegisterAdminRoutes: registerAdminRoutes,
|
||||
Database: db,
|
||||
RegisterPublicRoutes: func(routes gin.IRoutes) error {
|
||||
webHandler.RegisterStatic(routes)
|
||||
authWebHandler.RegisterPublic(routes)
|
||||
return registerPublicAuth(routes)
|
||||
},
|
||||
RegisterAdminRoutes: registerProtectedRoutes,
|
||||
AdminSessions: auth,
|
||||
LogEvent: func(event string) {
|
||||
log.Print(event)
|
||||
},
|
||||
|
||||
@@ -5,9 +5,13 @@ import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cmroubao/backend-api/internal/config"
|
||||
"cmroubao/backend-api/internal/platform/database"
|
||||
"cmroubao/backend-api/internal/platform/migration"
|
||||
)
|
||||
|
||||
@@ -114,6 +118,45 @@ func TestRequireCurrentMigrationsHidesStatusFailure(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRouterRegistersProtectedLogoutRoute(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db, err := database.Open(
|
||||
ctx,
|
||||
filepath.Join(t.TempDir(), "api.db"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("database.Open() error = %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
runner, err := migration.New(db)
|
||||
if err != nil {
|
||||
t.Fatalf("migration.New() error = %v", err)
|
||||
}
|
||||
if _, err := runner.Up(ctx); err != nil {
|
||||
t.Fatalf("migration.Up() error = %v", err)
|
||||
}
|
||||
router, err := buildRouter(ctx, config.Config{
|
||||
AssetDirectory: filepath.Join(t.TempDir(), "assets"),
|
||||
}, db)
|
||||
if err != nil {
|
||||
t.Fatalf("buildRouter() error = %v", err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/logout", nil)
|
||||
response := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(response, request)
|
||||
|
||||
if response.Code != http.StatusSeeOther ||
|
||||
response.Header().Get("Location") !=
|
||||
"/login?next=%2Ftasks" {
|
||||
t.Fatalf(
|
||||
"status/location = %d / %q",
|
||||
response.Code,
|
||||
response.Header().Get("Location"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type stubMigrationStatusReader struct {
|
||||
statuses []migration.Status
|
||||
err error
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cmroubao/backend-api/internal/config"
|
||||
"cmroubao/backend-api/internal/domain"
|
||||
"cmroubao/backend-api/internal/platform/database"
|
||||
"cmroubao/backend-api/internal/platform/migration"
|
||||
"cmroubao/backend-api/internal/platform/password"
|
||||
repository "cmroubao/backend-api/internal/repository/sqlite"
|
||||
"cmroubao/backend-api/internal/usecase"
|
||||
)
|
||||
|
||||
const passwordEnvironment = "CMROUBAO_AUTH_PASSWORD"
|
||||
|
||||
func main() {
|
||||
if err := run(
|
||||
os.Args[1:],
|
||||
os.LookupEnv,
|
||||
os.Stdout,
|
||||
); err != nil {
|
||||
log.Printf("auth command failed: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(
|
||||
arguments []string,
|
||||
lookup config.LookupEnvironment,
|
||||
output io.Writer,
|
||||
) error {
|
||||
command, value, role, err := parseArguments(arguments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
databasePath, err := config.LoadDatabasePath(lookup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var plainPassword string
|
||||
if command == "create-user" {
|
||||
var exists bool
|
||||
plainPassword, exists = lookup(passwordEnvironment)
|
||||
if !exists || plainPassword == "" {
|
||||
return errors.New(
|
||||
passwordEnvironment + " must be set for create-user",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
db, err := database.Open(ctx, databasePath)
|
||||
if err != nil {
|
||||
return errors.New("database startup failed")
|
||||
}
|
||||
defer func() {
|
||||
if err := db.Close(); err != nil {
|
||||
log.Print("database close failed")
|
||||
}
|
||||
}()
|
||||
runner, err := migration.New(db)
|
||||
if err != nil {
|
||||
return errors.New("migration setup failed")
|
||||
}
|
||||
if err := requireCurrentMigrations(ctx, runner); err != nil {
|
||||
return err
|
||||
}
|
||||
store, err := repository.New(db)
|
||||
if err != nil {
|
||||
return errors.New("auth repository setup failed")
|
||||
}
|
||||
passwords, err := password.NewBcrypt(12)
|
||||
if err != nil {
|
||||
return errors.New("password manager setup failed")
|
||||
}
|
||||
service, err := usecase.NewAuthService(
|
||||
store,
|
||||
passwords,
|
||||
usecase.SystemClock{},
|
||||
usecase.UUIDGenerator{},
|
||||
usecase.CryptoTokenGenerator{},
|
||||
)
|
||||
if err != nil {
|
||||
return errors.New("auth service setup failed")
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "create-user":
|
||||
user, err := service.ProvisionUser(
|
||||
ctx,
|
||||
usecase.ProvisionUserCommand{
|
||||
Username: value,
|
||||
Password: plainPassword,
|
||||
Role: role,
|
||||
Active: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return publicProvisioningError(err)
|
||||
}
|
||||
_, err = fmt.Fprintf(
|
||||
output,
|
||||
"user_id=%s username=%s role=%s\n",
|
||||
user.ID,
|
||||
user.Username,
|
||||
user.Role,
|
||||
)
|
||||
return err
|
||||
case "create-device":
|
||||
result, err := service.ProvisionDevice(
|
||||
ctx,
|
||||
usecase.ProvisionDeviceCommand{
|
||||
Name: value,
|
||||
Enabled: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return publicProvisioningError(err)
|
||||
}
|
||||
_, err = fmt.Fprintf(
|
||||
output,
|
||||
"device_id=%s\ndevice_token=%s\n",
|
||||
result.Device.ID,
|
||||
result.DeviceToken,
|
||||
)
|
||||
return err
|
||||
case "enable-user", "disable-user":
|
||||
active := command == "enable-user"
|
||||
if err := service.SetUserActive(
|
||||
ctx,
|
||||
usecase.SetUserActiveCommand{
|
||||
Username: value,
|
||||
Active: active,
|
||||
},
|
||||
); err != nil {
|
||||
return publicProvisioningError(err)
|
||||
}
|
||||
_, err = fmt.Fprintf(
|
||||
output,
|
||||
"username=%s active=%t\n",
|
||||
domain.NormalizeUsername(value),
|
||||
active,
|
||||
)
|
||||
return err
|
||||
case "enable-device", "disable-device":
|
||||
enabled := command == "enable-device"
|
||||
if err := service.SetDeviceEnabled(
|
||||
ctx,
|
||||
usecase.SetDeviceEnabledCommand{
|
||||
DeviceID: value,
|
||||
Enabled: enabled,
|
||||
},
|
||||
); err != nil {
|
||||
return publicProvisioningError(err)
|
||||
}
|
||||
_, err = fmt.Fprintf(
|
||||
output,
|
||||
"device_id=%s enabled=%t\n",
|
||||
strings.TrimSpace(value),
|
||||
enabled,
|
||||
)
|
||||
return err
|
||||
default:
|
||||
return errors.New("unsupported auth command")
|
||||
}
|
||||
}
|
||||
|
||||
func parseArguments(
|
||||
arguments []string,
|
||||
) (string, string, domain.UserRole, error) {
|
||||
if len(arguments) == 2 && arguments[0] == "create-device" {
|
||||
return "create-device", arguments[1], "", nil
|
||||
}
|
||||
if len(arguments) == 2 {
|
||||
switch arguments[0] {
|
||||
case "enable-user", "disable-user",
|
||||
"enable-device", "disable-device":
|
||||
return arguments[0], arguments[1], "", nil
|
||||
}
|
||||
}
|
||||
if len(arguments) == 3 && arguments[0] == "create-user" {
|
||||
role := domain.UserRole(strings.ToUpper(arguments[1]))
|
||||
if role != domain.UserRoleAdmin && role != domain.UserRoleBuyer {
|
||||
return "", "", "", errors.New(
|
||||
"role must be ADMIN or BUYER",
|
||||
)
|
||||
}
|
||||
return "create-user", arguments[2], role, nil
|
||||
}
|
||||
return "", "", "", errors.New(
|
||||
"usage: authctl create-user <ADMIN|BUYER> <username> | " +
|
||||
"authctl create-device <name> | " +
|
||||
"authctl <enable-user|disable-user> <username> | " +
|
||||
"authctl <enable-device|disable-device> <device-id>",
|
||||
)
|
||||
}
|
||||
|
||||
type migrationStatusReader interface {
|
||||
Status(context.Context) ([]migration.Status, error)
|
||||
}
|
||||
|
||||
func requireCurrentMigrations(
|
||||
ctx context.Context,
|
||||
reader migrationStatusReader,
|
||||
) error {
|
||||
statuses, err := reader.Status(ctx)
|
||||
if err != nil {
|
||||
return errors.New("database migration status failed")
|
||||
}
|
||||
if len(statuses) == 0 {
|
||||
return errors.New("database has no known migrations")
|
||||
}
|
||||
for _, status := range statuses {
|
||||
if !status.Applied {
|
||||
return errors.New(
|
||||
"database migrations are pending; run migrate up",
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func publicProvisioningError(err error) error {
|
||||
var typed *usecase.Error
|
||||
if !errors.As(err, &typed) {
|
||||
return errors.New("authentication provisioning failed")
|
||||
}
|
||||
switch typed.Kind {
|
||||
case usecase.ErrorKindInvalid:
|
||||
return errors.New("authentication input is invalid")
|
||||
case usecase.ErrorKindConflict:
|
||||
return errors.New("authentication resource already exists")
|
||||
case usecase.ErrorKindUnavailable:
|
||||
return errors.New("authentication storage is unavailable")
|
||||
case usecase.ErrorKindNotFound:
|
||||
return errors.New("authentication resource was not found")
|
||||
default:
|
||||
return errors.New("authentication provisioning failed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cmroubao/backend-api/internal/config"
|
||||
"cmroubao/backend-api/internal/platform/database"
|
||||
"cmroubao/backend-api/internal/platform/migration"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestRunCreatesUserWithoutExposingPassword(t *testing.T) {
|
||||
databasePath := migratedDatabase(t)
|
||||
const secret = "correct-horse-password"
|
||||
lookup := testLookup(databasePath, secret)
|
||||
var output bytes.Buffer
|
||||
|
||||
err := run(
|
||||
[]string{"create-user", "ADMIN", "Admin01"},
|
||||
lookup,
|
||||
&output,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("run(create-user) error = %v", err)
|
||||
}
|
||||
if strings.Contains(output.String(), secret) ||
|
||||
!strings.Contains(output.String(), "username=admin01 role=ADMIN") {
|
||||
t.Fatalf("output = %q", output.String())
|
||||
}
|
||||
db, err := database.Open(context.Background(), databasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("database.Open() error = %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
var hash string
|
||||
if err := db.QueryRow(
|
||||
"SELECT password_hash FROM users WHERE username = 'admin01'",
|
||||
).Scan(&hash); err != nil {
|
||||
t.Fatalf("query user: %v", err)
|
||||
}
|
||||
if hash == secret ||
|
||||
bcrypt.CompareHashAndPassword([]byte(hash), []byte(secret)) != nil {
|
||||
t.Fatal("stored password is missing, plaintext, or invalid")
|
||||
}
|
||||
|
||||
output.Reset()
|
||||
err = run(
|
||||
[]string{"create-user", "ADMIN", "admin01"},
|
||||
lookup,
|
||||
&output,
|
||||
)
|
||||
if err == nil ||
|
||||
strings.Contains(err.Error(), secret) ||
|
||||
output.Len() != 0 {
|
||||
t.Fatalf("duplicate error/output = %v / %q", err, output.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunCreatesDeviceAndStoresOnlyTokenHash(t *testing.T) {
|
||||
databasePath := migratedDatabase(t)
|
||||
var output bytes.Buffer
|
||||
|
||||
err := run(
|
||||
[]string{"create-device", "buyer-phone-01"},
|
||||
testLookup(databasePath, ""),
|
||||
&output,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("run(create-device) error = %v", err)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(output.String()), "\n")
|
||||
if len(lines) != 2 ||
|
||||
!strings.HasPrefix(lines[0], "device_id=") ||
|
||||
!strings.HasPrefix(lines[1], "device_token=") {
|
||||
t.Fatalf("output = %q", output.String())
|
||||
}
|
||||
deviceID := strings.TrimPrefix(lines[0], "device_id=")
|
||||
token := strings.TrimPrefix(lines[1], "device_token=")
|
||||
db, err := database.Open(context.Background(), databasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("database.Open() error = %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
var tokenHash string
|
||||
if err := db.QueryRow(
|
||||
"SELECT token_hash FROM devices WHERE id = ?",
|
||||
deviceID,
|
||||
).Scan(&tokenHash); err != nil {
|
||||
t.Fatalf("query device: %v", err)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
if tokenHash == token ||
|
||||
tokenHash != hex.EncodeToString(sum[:]) {
|
||||
t.Fatal("stored device token is not the expected hash")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunEnablesAndDisablesUsersAndDevices(t *testing.T) {
|
||||
databasePath := migratedDatabase(t)
|
||||
lookup := testLookup(databasePath, "correct-horse-password")
|
||||
if err := run(
|
||||
[]string{"create-user", "BUYER", "buyer01"},
|
||||
lookup,
|
||||
&bytes.Buffer{},
|
||||
); err != nil {
|
||||
t.Fatalf("create user: %v", err)
|
||||
}
|
||||
var deviceOutput bytes.Buffer
|
||||
if err := run(
|
||||
[]string{"create-device", "buyer-phone-01"},
|
||||
lookup,
|
||||
&deviceOutput,
|
||||
); err != nil {
|
||||
t.Fatalf("create device: %v", err)
|
||||
}
|
||||
deviceID := strings.TrimPrefix(
|
||||
strings.Split(strings.TrimSpace(deviceOutput.String()), "\n")[0],
|
||||
"device_id=",
|
||||
)
|
||||
|
||||
var output bytes.Buffer
|
||||
if err := run(
|
||||
[]string{"disable-user", "BUYER01"},
|
||||
lookup,
|
||||
&output,
|
||||
); err != nil {
|
||||
t.Fatalf("disable user: %v", err)
|
||||
}
|
||||
if !strings.Contains(output.String(), "username=buyer01 active=false") {
|
||||
t.Fatalf("disable user output = %q", output.String())
|
||||
}
|
||||
output.Reset()
|
||||
if err := run(
|
||||
[]string{"disable-device", deviceID},
|
||||
lookup,
|
||||
&output,
|
||||
); err != nil {
|
||||
t.Fatalf("disable device: %v", err)
|
||||
}
|
||||
|
||||
db, err := database.Open(context.Background(), databasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("database.Open() error = %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
var userActive, deviceEnabled bool
|
||||
if err := db.QueryRow(
|
||||
"SELECT is_active FROM users WHERE username = 'buyer01'",
|
||||
).Scan(&userActive); err != nil {
|
||||
t.Fatalf("query user status: %v", err)
|
||||
}
|
||||
if err := db.QueryRow(
|
||||
"SELECT is_enabled FROM devices WHERE id = ?",
|
||||
deviceID,
|
||||
).Scan(&deviceEnabled); err != nil {
|
||||
t.Fatalf("query device status: %v", err)
|
||||
}
|
||||
if userActive || deviceEnabled {
|
||||
t.Fatalf(
|
||||
"user/device enabled = %v/%v",
|
||||
userActive,
|
||||
deviceEnabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRejectsWeakPasswordAndInvalidCommands(t *testing.T) {
|
||||
databasePath := migratedDatabase(t)
|
||||
err := run(
|
||||
[]string{"create-user", "BUYER", "buyer"},
|
||||
testLookup(databasePath, "too-short"),
|
||||
&bytes.Buffer{},
|
||||
)
|
||||
if err == nil || err.Error() != "authentication input is invalid" {
|
||||
t.Fatalf("weak password error = %v", err)
|
||||
}
|
||||
for _, arguments := range [][]string{
|
||||
nil,
|
||||
{"create-user", "OWNER", "user"},
|
||||
{"create-device"},
|
||||
{"unknown"},
|
||||
} {
|
||||
if err := run(
|
||||
arguments,
|
||||
testLookup(databasePath, ""),
|
||||
&bytes.Buffer{},
|
||||
); err == nil {
|
||||
t.Fatalf("run(%v) error = nil", arguments)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunRequiresCurrentMigrations(t *testing.T) {
|
||||
databasePath := filepath.Join(t.TempDir(), "pending.db")
|
||||
err := run(
|
||||
[]string{"create-device", "phone"},
|
||||
testLookup(databasePath, ""),
|
||||
&bytes.Buffer{},
|
||||
)
|
||||
if err == nil ||
|
||||
err.Error() != "database migrations are pending; run migrate up" {
|
||||
t.Fatalf("pending migration error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func migratedDatabase(t *testing.T) string {
|
||||
t.Helper()
|
||||
databasePath := filepath.Join(t.TempDir(), "authctl.db")
|
||||
db, err := database.Open(context.Background(), databasePath)
|
||||
if err != nil {
|
||||
t.Fatalf("database.Open() error = %v", err)
|
||||
}
|
||||
runner, err := migration.New(db)
|
||||
if err != nil {
|
||||
t.Fatalf("migration.New() error = %v", err)
|
||||
}
|
||||
if _, err := runner.Up(context.Background()); err != nil {
|
||||
t.Fatalf("migration.Up() error = %v", err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatalf("db.Close() error = %v", err)
|
||||
}
|
||||
return databasePath
|
||||
}
|
||||
|
||||
func testLookup(
|
||||
databasePath string,
|
||||
password string,
|
||||
) config.LookupEnvironment {
|
||||
return func(name string) (string, bool) {
|
||||
switch name {
|
||||
case config.DatabasePathEnvironment:
|
||||
return databasePath, true
|
||||
case passwordEnvironment:
|
||||
if password == "" {
|
||||
return "", false
|
||||
}
|
||||
return password, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user