Replace token-based admin auth with JWT session authentication
CI / test (push) Failing after 12m45s

- Add AdminUser model (bcrypt hashed passwords) and admin_users table
- Add AdminJWTService for HS256 JWT sessions (24h TTL)
- Add AdminSessionAuth middleware for /api/v1/admin/* routes
- Add admin handlers: login, logout, me, change-password, users CRUD
- Keys and model management routes now require admin JWT session
- Remove ADMIN_TOKEN, add ADMIN_USERNAME, ADMIN_PASSWORD env vars
- Update frontend: username/password login, admin_session storage, AdminUsers CRUD view
This commit is contained in:
2026-07-31 17:15:29 -04:00
parent b18d4bd146
commit 4d34c6d31a
21 changed files with 828 additions and 57 deletions
+19
View File
@@ -27,6 +27,25 @@ func (a *StringArray) Scan(value interface{}) error {
return json.Unmarshal(b, a)
}
type AdminUser struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Username string `gorm:"size:64;uniqueIndex;not null" json:"username"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
IsActive bool `gorm:"default:true" json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
}
func (AdminUser) TableName() string { return "admin_users" }
func (u *AdminUser) BeforeCreate(tx *gorm.DB) error {
if u.ID == uuid.Nil {
u.ID = uuid.New()
}
return nil
}
type ApiKey struct {
ID uuid.UUID `gorm:"type:uuid;primaryKey" json:"id"`
Name string `gorm:"size:255;not null" json:"name"`
+32 -15
View File
@@ -1,42 +1,59 @@
package db
import (
"crypto/rand"
"encoding/hex"
"log/slog"
"strings"
"golang.org/x/crypto/bcrypt"
"github.com/google/uuid"
"gorm.io/gorm"
)
func SeedAdminKey(db *gorm.DB, adminToken string) error {
var count int64
db.Model(&ApiKey{}).Count(&count)
func generateRandomPassword(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes)[:length], nil
}
func SeedAdminUser(db *gorm.DB, username, password string) error {
var count int64
db.Model(&AdminUser{}).Count(&count)
if count > 0 {
slog.Info("admin key already exists, skipping seed")
slog.Info("admin user already exists, skipping seed")
return nil
}
hash, err := bcrypt.GenerateFromPassword([]byte(adminToken), bcrypt.DefaultCost)
pwd := password
if pwd == "" {
var err error
pwd, err = generateRandomPassword(24)
if err != nil {
return err
}
slog.Warn("no ADMIN_PASSWORD set — generated random password (save this, it won't be shown again)")
}
hash, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return err
}
admin := ApiKey{
ID: uuid.New(),
Name: "admin",
KeyHash: string(hash),
KeyPrefix: "admin-",
Scopes: StringArray{"chat", "models", "usage", "admin"},
IsActive: true,
IsAdmin: true,
admin := AdminUser{
ID: uuid.New(),
Username: strings.ToLower(username),
PasswordHash: string(hash),
IsActive: true,
}
if err := db.Create(&admin).Error; err != nil {
return err
}
slog.Info("admin key seeded", "prefix", admin.KeyPrefix)
slog.Warn("ADMIN TOKEN: " + adminToken + " (save this!)")
slog.Info("admin user seeded", "username", admin.Username)
slog.Warn("ADMIN USER: username=" + username + " password=" + pwd + " (save this, it won't be shown again)")
return nil
}