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 }