241 lines
7.2 KiB
Go
241 lines
7.2 KiB
Go
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
|
|
}
|