Changeset 0.6.0 (#36)
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -91,8 +93,12 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
_ = database.DB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&userCount)
|
||||
isFirstUser := userCount == 0
|
||||
|
||||
// If not first user, check if registration is enabled
|
||||
if !isFirstUser {
|
||||
// First-user-becomes-admin only when no env admin is configured
|
||||
envAdminSet := os.Getenv("SWITCHBOARD_ADMIN_USERNAME") != ""
|
||||
promoteFirst := isFirstUser && !envAdminSet
|
||||
|
||||
// If not first user (or env admin handles bootstrap), check registration
|
||||
if !promoteFirst {
|
||||
if !IsRegistrationEnabled() {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "registration is disabled"})
|
||||
return
|
||||
@@ -106,19 +112,25 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// First user gets admin role
|
||||
// Determine role and active state
|
||||
role := "user"
|
||||
if isFirstUser {
|
||||
isActive := true
|
||||
if promoteFirst {
|
||||
role = "admin"
|
||||
} else {
|
||||
// Apply registration default state
|
||||
if GetRegistrationDefaultState() == "pending" {
|
||||
isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
// Insert user
|
||||
var user userResponse
|
||||
err = database.DB.QueryRow(`
|
||||
INSERT INTO users (username, email, password_hash, role)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
INSERT INTO users (username, email, password_hash, role, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, username, email, display_name, role
|
||||
`, req.Username, req.Email, string(hash), role).Scan(
|
||||
`, req.Username, req.Email, string(hash), role, isActive).Scan(
|
||||
&user.ID, &user.Username, &user.Email, &user.DisplayName, &user.Role,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -134,6 +146,15 @@ func (h *AuthHandler) Register(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// If account is pending, don't generate tokens
|
||||
if !isActive {
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "Account created and pending admin approval",
|
||||
"pending": true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
resp, err := h.generateTokenPair(user)
|
||||
if err != nil {
|
||||
@@ -152,8 +173,8 @@ func IsRegistrationEnabled() bool {
|
||||
}
|
||||
var enabled bool
|
||||
err := database.DB.QueryRow(`
|
||||
SELECT COALESCE((value->>'enabled')::boolean, true)
|
||||
FROM global_settings WHERE key = 'registration'
|
||||
SELECT COALESCE((value->>'value')::boolean, true)
|
||||
FROM global_settings WHERE key = 'registration_enabled'
|
||||
`).Scan(&enabled)
|
||||
if err != nil {
|
||||
return true // Default to open if setting missing
|
||||
@@ -161,6 +182,75 @@ func IsRegistrationEnabled() bool {
|
||||
return enabled
|
||||
}
|
||||
|
||||
// GetRegistrationDefaultState returns "active" or "pending".
|
||||
func GetRegistrationDefaultState() string {
|
||||
if database.DB == nil {
|
||||
return "active"
|
||||
}
|
||||
var state string
|
||||
err := database.DB.QueryRow(`
|
||||
SELECT COALESCE(value->>'value', 'active')
|
||||
FROM global_settings WHERE key = 'registration_default_state'
|
||||
`).Scan(&state)
|
||||
if err != nil || state == "" {
|
||||
return "active"
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
// BootstrapAdmin creates or updates the admin user from environment variables.
|
||||
// This runs on every startup, so changing the K8s secret + restarting resets the password.
|
||||
// Handles both username and email conflicts (e.g. admin username changed between deploys).
|
||||
func BootstrapAdmin(cfg *config.Config) {
|
||||
if cfg.AdminUsername == "" || cfg.AdminPassword == "" {
|
||||
return
|
||||
}
|
||||
if database.DB == nil {
|
||||
return
|
||||
}
|
||||
|
||||
email := cfg.AdminEmail
|
||||
if email == "" {
|
||||
email = cfg.AdminUsername + "@localhost"
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.AdminPassword), bcryptCost)
|
||||
if err != nil {
|
||||
log.Printf("⚠ Failed to hash admin password: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Try upsert by username (common case: same username, new password)
|
||||
_, err = database.DB.Exec(`
|
||||
INSERT INTO users (username, email, password_hash, role, is_active)
|
||||
VALUES ($1, $2, $3, 'admin', true)
|
||||
ON CONFLICT (username) DO UPDATE SET
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
email = EXCLUDED.email,
|
||||
role = 'admin',
|
||||
is_active = true
|
||||
`, cfg.AdminUsername, email, string(hash))
|
||||
|
||||
if err != nil && strings.Contains(err.Error(), "duplicate key") {
|
||||
// Email conflict — admin username was changed in config but email
|
||||
// already belongs to old admin row. Update that row instead.
|
||||
_, err = database.DB.Exec(`
|
||||
UPDATE users SET
|
||||
username = $1,
|
||||
password_hash = $3,
|
||||
role = 'admin',
|
||||
is_active = true
|
||||
WHERE email = $2
|
||||
`, cfg.AdminUsername, email, string(hash))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("⚠ Admin bootstrap failed: %v", err)
|
||||
} else {
|
||||
log.Printf("✅ Admin user '%s' bootstrapped from environment", cfg.AdminUsername)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Login ───────────────────────────────────
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
@@ -195,7 +285,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
}
|
||||
|
||||
if !isActive {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account disabled"})
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is pending admin approval"})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user