4d34c6d31a
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
87 lines
2.1 KiB
Go
87 lines
2.1 KiB
Go
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
|
|
}
|