package services import ( "errors" "time" "gauth-central/internal/database" "gauth-central/internal/models" "gauth-central/pkg/utils" "github.com/markbates/goth" "gorm.io/gorm" ) type AuthService struct { jwtService *JWTService } func NewAuthService() *AuthService { return &AuthService{ jwtService: NewJWTService(), } } func (s *AuthService) Register(username, email, password string) (*models.User, string, string, string, error) { // Check if user exists (including soft-deleted users) var count int64 database.DB.Model(&models.User{}).Unscoped().Where("email = ?", email).Count(&count) if count > 0 { return nil, "", "", "", errors.New("email already registered") } hashedPassword, err := utils.HashPassword(password) if err != nil { return nil, "", "", "", err } verifyToken, err := utils.GenerateSecureToken(32) if err != nil { return nil, "", "", "", err } // Email/password users must verify email; tokens are not issued until verified falseBool := false user := models.User{ UserName: username, Email: email, Password: hashedPassword, EmailVerified: &falseBool, // Explicitly set to false EmailVerifyToken: verifyToken, } // Create user - EmailVerified will be false by default or explicitly set if err := database.DB.Create(&user).Error; err != nil { // Fallback check for duplicate key error just in case race condition if utils.IsDuplicateKeyError(err) { return nil, "", "", "", errors.New("email already registered") } return nil, "", "", "", err } // Assign default "user" role var userRole models.Role if err := database.DB.Where("name = ?", "user").First(&userRole).Error; err == nil { database.DB.Model(&user).Association("Roles").Append(&userRole) } // Reload user with roles (no JWT until email verified) database.DB.Preload("Roles.Permissions").First(&user, user.ID) return &user, "", "", verifyToken, nil } func (s *AuthService) Login(email, password string) (*models.User, string, string, error) { var user models.User // Preload Roles and Permissions if err := database.DB.Preload("Roles.Permissions").Where("email = ?", email).First(&user).Error; err != nil { return nil, "", "", errors.New("invalid credentials") } if !utils.CheckPasswordHash(password, user.Password) { return nil, "", "", errors.New("invalid credentials") } if !user.IsEmailVerified() { return nil, "", "", errors.New("email not verified") } accessToken, refreshToken, err := s.jwtService.GenerateTokenPair(user) if err != nil { return nil, "", "", err } return &user, accessToken, refreshToken, nil } func (s *AuthService) RefreshToken(refreshToken string) (string, string, error) { claims, err := s.jwtService.ValidateToken(refreshToken) if err != nil { return "", "", err } // Here you might want to check against DB if user still exists or is banned // Also you could implement token rotation (revoke used refresh token) var user models.User // Parse UUID from claims and preload permissions if err := database.DB.Preload("Roles.Permissions").Where("id = ?", claims.UserID).First(&user).Error; err != nil { return "", "", errors.New("user not found") } return s.jwtService.GenerateTokenPair(user) } func (s *AuthService) FindOrCreateSocialUser(gothUser goth.User, provider string) (*models.User, string, string, error) { var socialAccount models.SocialAccount var user models.User // Check if social account exists err := database.DB.Where("provider = ? AND provider_id = ?", provider, gothUser.UserID).First(&socialAccount).Error if err == nil { // Social account exists, get user if err := database.DB.Preload("Roles.Permissions").First(&user, socialAccount.UserID).Error; err != nil { return nil, "", "", err } // Update avatar if changed if gothUser.AvatarURL != "" && user.Avatar != gothUser.AvatarURL { database.DB.Model(&user).Update("avatar", gothUser.AvatarURL) user.Avatar = gothUser.AvatarURL } } else if errors.Is(err, gorm.ErrRecordNotFound) { // Check if user with same email exists err := database.DB.Where("email = ?", gothUser.Email).First(&user).Error if errors.Is(err, gorm.ErrRecordNotFound) { // Create new user - use name from provider or generate from email username := gothUser.NickName if username == "" { username = gothUser.Name } if username == "" { // Generate username from email (part before @) for i, c := range gothUser.Email { if c == '@' { username = gothUser.Email[:i] break } } } // OAuth providers have already verified email; no email verification required trueBool := true user = models.User{ UserName: username, Email: gothUser.Email, EmailVerified: &trueBool, Avatar: gothUser.AvatarURL, // Save avatar from OAuth provider } if err := database.DB.Create(&user).Error; err != nil { return nil, "", "", err } } else { // Linking OAuth to existing user: treat as verified and update avatar updates := map[string]interface{}{ "email_verified": true, "email_verify_token": "", } if gothUser.AvatarURL != "" { updates["avatar"] = gothUser.AvatarURL } database.DB.Model(&user).Updates(updates) trueBool := true user.EmailVerified = &trueBool user.EmailVerifyToken = "" if gothUser.AvatarURL != "" { user.Avatar = gothUser.AvatarURL } } // Create social account with avatar and name socialAccount = models.SocialAccount{ UserID: user.ID, Provider: provider, ProviderID: gothUser.UserID, Email: gothUser.Email, Name: gothUser.Name, AvatarURL: gothUser.AvatarURL, } if err := database.DB.Create(&socialAccount).Error; err != nil { return nil, "", "", err } // Assign default "user" role var userRole models.Role if err := database.DB.Where("name = ?", "user").First(&userRole).Error; err == nil { database.DB.Model(&user).Association("Roles").Append(&userRole) } } else { return nil, "", "", err } // Make sure we have the user with all roles/permissions and social accounts loaded database.DB.Preload("Roles.Permissions").Preload("SocialAccounts").First(&user, user.ID) accessToken, refreshToken, err := s.jwtService.GenerateTokenPair(user) if err != nil { return nil, "", "", err } return &user, accessToken, refreshToken, nil } func (s *AuthService) GetUserByID(userID string) (*models.User, error) { var user models.User if err := database.DB.Preload("Roles.Permissions").Where("id = ?", userID).First(&user).Error; err != nil { return nil, errors.New("user not found") } return &user, nil } // VerifyEmail marks the user as verified when the token matches. Only for email/password registrations. func (s *AuthService) VerifyEmail(token string) error { if token == "" { return errors.New("invalid verification token") } var user models.User if err := database.DB.Where("email_verify_token = ?", token).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("invalid or expired verification token") } return err } now := time.Now() return database.DB.Model(&user).Updates(map[string]interface{}{ "email_verified": true, "email_verify_token": "", "email_verified_at": &now, }).Error }