Files
Charon/backend/internal/services/auth_service.go

187 lines
4.2 KiB
Go

package services
import (
"errors"
"strings"
"time"
"github.com/Wikid82/charon/backend/internal/config"
"github.com/Wikid82/charon/backend/internal/models"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"gorm.io/gorm"
)
type AuthService struct {
db *gorm.DB
config config.Config
}
func NewAuthService(db *gorm.DB, cfg config.Config) *AuthService {
return &AuthService{db: db, config: cfg}
}
type Claims struct {
UserID uint `json:"user_id"`
Role string `json:"role"`
SessionVersion uint `json:"session_version"`
jwt.RegisteredClaims
}
func (s *AuthService) Register(email, password, name string) (*models.User, error) {
email = strings.ToLower(email)
var count int64
s.db.Model(&models.User{}).Count(&count)
role := "user"
if count == 0 {
role = "admin" // First user is admin
}
user := &models.User{
UUID: uuid.New().String(),
Email: email,
Name: name,
Role: role,
APIKey: uuid.New().String(),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := user.SetPassword(password); err != nil {
return nil, err
}
if err := s.db.Create(user).Error; err != nil {
return nil, err
}
return user, nil
}
func (s *AuthService) Login(email, password string) (string, error) {
email = strings.ToLower(email)
var user models.User
if err := s.db.Where("email = ?", email).First(&user).Error; err != nil {
return "", errors.New("invalid credentials")
}
if !user.Enabled {
return "", errors.New("account disabled")
}
if user.LockedUntil != nil && user.LockedUntil.After(time.Now()) {
return "", errors.New("account locked")
}
if !user.CheckPassword(password) {
user.FailedLoginAttempts++
if user.FailedLoginAttempts >= 5 {
lockTime := time.Now().Add(15 * time.Minute)
user.LockedUntil = &lockTime
}
s.db.Save(&user)
return "", errors.New("invalid credentials")
}
// Reset failed attempts
user.FailedLoginAttempts = 0
user.LockedUntil = nil
now := time.Now()
user.LastLogin = &now
s.db.Save(&user)
return s.GenerateToken(&user)
}
func (s *AuthService) GenerateToken(user *models.User) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
UserID: user.ID,
Role: user.Role,
SessionVersion: user.SessionVersion,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
Issuer: "charon",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(s.config.JWTSecret))
}
func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
var user models.User
if err := s.db.Where("id = ?", userID).First(&user).Error; err != nil {
return errors.New("user not found")
}
if !user.CheckPassword(oldPassword) {
return errors.New("invalid current password")
}
if err := user.SetPassword(newPassword); err != nil {
return err
}
return s.db.Save(&user).Error
}
func (s *AuthService) ValidateToken(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte(s.config.JWTSecret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("invalid token")
}
return claims, nil
}
func (s *AuthService) AuthenticateToken(tokenString string) (*models.User, *Claims, error) {
claims, err := s.ValidateToken(tokenString)
if err != nil {
return nil, nil, err
}
user, err := s.GetUserByID(claims.UserID)
if err != nil || !user.Enabled {
return nil, nil, errors.New("invalid token")
}
if claims.SessionVersion != user.SessionVersion {
return nil, nil, errors.New("invalid token")
}
return user, claims, nil
}
func (s *AuthService) InvalidateSessions(userID uint) error {
result := s.db.Model(&models.User{}).
Where("id = ?", userID).
Update("session_version", gorm.Expr("session_version + 1"))
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New("user not found")
}
return nil
}
func (s *AuthService) GetUserByID(id uint) (*models.User, error) {
var user models.User
if err := s.db.Where("id = ?", id).First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}