Replace token-based admin auth with JWT session authentication
CI / test (push) Failing after 12m45s
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:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user