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
+86
View File
@@ -0,0 +1,86 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt"
"github.com/llamalink/llamalink/internal/db"
)
var (
ErrInvalidAdminCredentials = errors.New("invalid username or password")
ErrAdminNotFound = errors.New("admin user not found")
ErrAdminInactive = errors.New("admin user is inactive")
)
type AdminClaims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
type AdminJWTService struct {
secret []byte
ttl time.Duration
}
func NewAdminJWTService(secret []byte, ttlSeconds int) *AdminJWTService {
return &AdminJWTService{
secret: secret,
ttl: time.Duration(ttlSeconds) * time.Second,
}
}
func (s *AdminJWTService) IssueToken(user *db.AdminUser) (string, time.Time, error) {
expiresAt := time.Now().Add(s.ttl)
claims := &AdminClaims{
Username: user.Username,
RegisteredClaims: jwt.RegisteredClaims{
Subject: user.ID.String(),
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString(s.secret)
return tokenStr, expiresAt, err
}
func (s *AdminJWTService) VerifyToken(tokenStr string) (*AdminClaims, error) {
token, err := jwt.ParseWithClaims(tokenStr, &AdminClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return s.secret, nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*AdminClaims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
func ValidateAdminPassword(user *db.AdminUser, password string) error {
if !user.IsActive {
return ErrAdminInactive
}
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return ErrInvalidAdminCredentials
}
return nil
}
func GenerateSecureToken(length int) (string, error) {
bytes := make([]byte, length)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}