first commit
This commit is contained in:
240
internal/services/auth_service.go
Normal file
240
internal/services/auth_service.go
Normal file
@@ -0,0 +1,240 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user