feat(admin): add session-based portal login
This commit is contained in:
@@ -11,12 +11,17 @@ const (
|
||||
EnvListenAddr = "SUB2API_CRM_LISTEN_ADDR"
|
||||
EnvSQLiteDSN = "SUB2API_CRM_SQLITE_DSN"
|
||||
EnvAdminToken = "SUB2API_CRM_ADMIN_TOKEN"
|
||||
EnvAdminUsername = "SUB2API_CRM_ADMIN_USERNAME"
|
||||
EnvAdminPassword = "SUB2API_CRM_ADMIN_PASSWORD"
|
||||
EnvAdminSessionTTL = "SUB2API_CRM_ADMIN_SESSION_TTL"
|
||||
EnvRepoRoot = "SUB2API_CRM_REPO_ROOT"
|
||||
EnvReconcileWorkerEnabled = "SUB2API_CRM_RECONCILE_WORKER_ENABLED"
|
||||
EnvReconcilePollInterval = "SUB2API_CRM_RECONCILE_POLL_INTERVAL"
|
||||
|
||||
DefaultListenAddr = ":8080"
|
||||
DefaultSQLiteDSN = "file:sub2api-cn-relay-manager.db?_foreign_keys=on&_busy_timeout=5000"
|
||||
DefaultAdminUsername = "admin"
|
||||
DefaultAdminSessionTTL = 12 * time.Hour
|
||||
DefaultReconcilePollInterval = 10 * time.Minute
|
||||
)
|
||||
|
||||
@@ -44,6 +49,12 @@ type StartupConfig struct {
|
||||
Reconcile ReconcileConfig
|
||||
}
|
||||
|
||||
type AdminSessionConfig struct {
|
||||
Username string
|
||||
Password string
|
||||
SessionTTL time.Duration
|
||||
}
|
||||
|
||||
func LoadStartupFromEnv() (StartupConfig, error) {
|
||||
return loadStartupFromLookupEnv(os.LookupEnv)
|
||||
}
|
||||
@@ -76,6 +87,10 @@ func LoadAdminTokenFromEnv() (string, error) {
|
||||
return loadAdminTokenFromLookupEnv(os.LookupEnv)
|
||||
}
|
||||
|
||||
func LoadAdminSessionFromEnv() (AdminSessionConfig, error) {
|
||||
return loadAdminSessionFromLookupEnv(os.LookupEnv)
|
||||
}
|
||||
|
||||
func loadAdminTokenFromLookupEnv(lookup func(string) (string, bool)) (string, error) {
|
||||
token := strings.TrimSpace(readRequiredEnv(lookup, EnvAdminToken))
|
||||
if token == "" {
|
||||
@@ -85,6 +100,18 @@ func loadAdminTokenFromLookupEnv(lookup func(string) (string, bool)) (string, er
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func loadAdminSessionFromLookupEnv(lookup func(string) (string, bool)) (AdminSessionConfig, error) {
|
||||
ttl, err := readOptionalDurationEnv(lookup, EnvAdminSessionTTL, DefaultAdminSessionTTL)
|
||||
if err != nil {
|
||||
return AdminSessionConfig{}, err
|
||||
}
|
||||
return AdminSessionConfig{
|
||||
Username: readOptionalEnv(lookup, EnvAdminUsername, DefaultAdminUsername),
|
||||
Password: strings.TrimSpace(readOptionalEnv(lookup, EnvAdminPassword, "")),
|
||||
SessionTTL: ttl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readOptionalEnv(lookup func(string) (string, bool), key string, defaultValue string) string {
|
||||
value, ok := lookup(key)
|
||||
if !ok {
|
||||
|
||||
@@ -163,6 +163,65 @@ func TestLoadAdminTokenFromLookupEnv(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadAdminSessionFromLookupEnv(t *testing.T) {
|
||||
t.Run("uses defaults", func(t *testing.T) {
|
||||
cfg, err := loadAdminSessionFromLookupEnv(func(string) (string, bool) {
|
||||
return "", false
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Username != DefaultAdminUsername {
|
||||
t.Fatalf("Username = %q, want %q", cfg.Username, DefaultAdminUsername)
|
||||
}
|
||||
if cfg.Password != "" {
|
||||
t.Fatalf("Password = %q, want empty", cfg.Password)
|
||||
}
|
||||
if cfg.SessionTTL != DefaultAdminSessionTTL {
|
||||
t.Fatalf("SessionTTL = %s, want %s", cfg.SessionTTL, DefaultAdminSessionTTL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("loads custom values", func(t *testing.T) {
|
||||
cfg, err := loadAdminSessionFromLookupEnv(func(key string) (string, bool) {
|
||||
switch key {
|
||||
case EnvAdminUsername:
|
||||
return " portal-admin ", true
|
||||
case EnvAdminPassword:
|
||||
return " super-secret ", true
|
||||
case EnvAdminSessionTTL:
|
||||
return "4h", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Username != "portal-admin" {
|
||||
t.Fatalf("Username = %q, want portal-admin", cfg.Username)
|
||||
}
|
||||
if cfg.Password != "super-secret" {
|
||||
t.Fatalf("Password = %q, want super-secret", cfg.Password)
|
||||
}
|
||||
if cfg.SessionTTL != 4*time.Hour {
|
||||
t.Fatalf("SessionTTL = %s, want 4h", cfg.SessionTTL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("rejects invalid ttl", func(t *testing.T) {
|
||||
_, err := loadAdminSessionFromLookupEnv(func(key string) (string, bool) {
|
||||
if key == EnvAdminSessionTTL {
|
||||
return "bad", true
|
||||
}
|
||||
return "", false
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid session ttl")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Verify exported wrappers call the lookup versions.
|
||||
// We can't easily test LoadStartupFromEnv / LoadAdminTokenFromEnv
|
||||
// since they depend on os.LookupEnv, but we verify they compile and don't panic.
|
||||
|
||||
Reference in New Issue
Block a user