package auth import ( "crypto/sha256" "crypto/subtle" "encoding/hex" "errors" "time" "github.com/golang-jwt/jwt/v5" "gorm.io/gorm" "onixbyte.com/pipely/internal/database" "onixbyte.com/pipely/internal/model" ) var ( ErrInvalidCredentials = errors.New("invalid username or password") ErrUserExists = errors.New("username already exists") ) type Service struct { db *gorm.DB secret []byte expiry time.Duration } func NewService(db *gorm.DB, secret []byte, expiryHrs int) *Service { return &Service{ db: db, secret: secret, expiry: time.Duration(expiryHrs) * time.Hour, } } func (s *Service) Login(username, password string) (*model.LoginResponse, error) { user, err := database.GetAdminUserByUsername(s.db, username) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrInvalidCredentials } return nil, err } hash := hashPassword(password) if subtle.ConstantTimeCompare([]byte(hash), []byte(user.PasswordHash)) != 1 { return nil, ErrInvalidCredentials } token, expiresAt, err := s.generateToken(user) if err != nil { return nil, err } return &model.LoginResponse{ Token: token, ExpiresAt: expiresAt, }, nil } func (s *Service) CreateInitialAdmin(username, password string) error { _, err := database.GetAdminUserByUsername(s.db, username) if err == nil { return nil // admin already exists } user := &model.AdminUser{ Username: username, PasswordHash: hashPassword(password), Role: "admin", } return s.db.Transaction(func(tx *gorm.DB) error { return database.CreateAdminUser(tx, user) }) } func (s *Service) ValidateToken(tokenString string) (*model.AdminUser, error) { token, err := jwt.Parse(tokenString, 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.(jwt.MapClaims) if !ok || !token.Valid { return nil, errors.New("invalid token") } username, _ := claims["sub"].(string) if username == "" { return nil, errors.New("invalid token subject") } return database.GetAdminUserByUsername(s.db, username) } func (s *Service) generateToken(user *model.AdminUser) (string, int64, error) { expiresAt := time.Now().Add(s.expiry) claims := jwt.MapClaims{ "sub": user.Username, "role": user.Role, "iat": time.Now().Unix(), "exp": expiresAt.Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) tokenStr, err := token.SignedString(s.secret) if err != nil { return "", 0, err } return tokenStr, expiresAt.Unix(), nil } func hashPassword(password string) string { h := sha256.Sum256([]byte(password)) return hex.EncodeToString(h[:]) } func (s *Service) ListUsers() ([]model.AdminUser, error) { return database.ListAdminUsers(s.db) } func (s *Service) CreateUser(username, password, role string) (*model.AdminUser, error) { _, err := database.GetAdminUserByUsername(s.db, username) if err == nil { return nil, ErrUserExists } if role == "" { role = "admin" } user := &model.AdminUser{ Username: username, PasswordHash: hashPassword(password), Role: role, } if err := database.CreateAdminUser(s.db, user); err != nil { return nil, err } return user, nil } func (s *Service) DeleteUser(id int64) error { return database.DeleteAdminUser(s.db, id) } func (s *Service) ChangePassword(id int64, newPassword string) error { return database.UpdateAdminPassword(s.db, id, hashPassword(newPassword)) }