187 lines
4.2 KiB
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
|
|
}
|