package auth import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "net/http" "time" "cardverse/config" "cardverse/internal/modules/user" "cardverse/internal/pkg/utils" ) var ErrInvalidCredentials = errors.New("email atau password salah") var ErrEmailAlreadyExists = errors.New("email sudah terdaftar") var ErrAccountSuspended = errors.New("akun anda sedang disuspensi") var ErrInvalidToken = errors.New("token tidak valid atau sudah kedaluwarsa") var ErrPasswordMismatch = errors.New("konfirmasi password tidak cocok") var ErrGoogleVerificationFailed = errors.New("verifikasi google id token gagal") var ErrGoogleAccountNoPassword = errors.New("akun ini mendaftar menggunakan Google, silakan login via Google atau buat password melalui fitur Lupa Password") type Service interface { Register(req RegisterRequest) (*user.User, string, error) Login(req LoginRequest) (*LoginResponse, error) GoogleLogin(req GoogleLoginRequest) (*LoginResponse, error) RefreshToken(req RefreshTokenRequest) (*TokenResponse, error) Logout(refreshToken string) error VerifyEmail(token string) error ForgotPassword(email string) (string, error) ResetPassword(req ResetPasswordRequest) error } type service struct { userRepo user.Repository authRepo Repository } func NewService(userRepo user.Repository, authRepo Repository) Service { return &service{ userRepo: userRepo, authRepo: authRepo, } } func (s *service) Register(req RegisterRequest) (*user.User, string, error) { if req.Password != req.ConfirmPassword { return nil, "", ErrPasswordMismatch } existing, _ := s.userRepo.FindByEmail(req.Email) if existing != nil { return nil, "", ErrEmailAlreadyExists } hashedPassword, err := utils.HashPassword(req.Password) if err != nil { return nil, "", err } verificationToken := generateRandomToken() newUser := &user.User{ Name: req.Name, Email: req.Email, Password: hashedPassword, Role: user.RoleUser, Status: user.StatusUnverified, IsEmailVerified: false, VerificationToken: verificationToken, } if err := s.userRepo.Create(newUser); err != nil { return nil, "", err } return newUser, verificationToken, nil } func (s *service) Login(req LoginRequest) (*LoginResponse, error) { u, err := s.userRepo.FindByEmail(req.Email) if err != nil { return nil, ErrInvalidCredentials } if u.Password == "" { return nil, ErrGoogleAccountNoPassword } if !utils.CheckPassword(u.Password, req.Password) { return nil, ErrInvalidCredentials } if u.Status == user.StatusSuspended { return nil, ErrAccountSuspended } return s.generateLoginResponse(u) } type googleTokenPayload struct { Sub string `json:"sub"` Email string `json:"email"` EmailVerified string `json:"email_verified"` Name string `json:"name"` Picture string `json:"picture"` } func (s *service) GoogleLogin(req GoogleLoginRequest) (*LoginResponse, error) { googleUser, err := verifyGoogleIDToken(req.IDToken) if err != nil || googleUser.Email == "" { return nil, ErrGoogleVerificationFailed } u, _ := s.userRepo.FindByGoogleID(googleUser.Sub) if u == nil { u, _ = s.userRepo.FindByEmail(googleUser.Email) } if u != nil { if u.Status == user.StatusSuspended { return nil, ErrAccountSuspended } updated := false if u.GoogleID == nil || *u.GoogleID == "" { u.GoogleID = &googleUser.Sub updated = true } if (u.AvatarURL == nil || *u.AvatarURL == "") && googleUser.Picture != "" { u.AvatarURL = &googleUser.Picture updated = true } if !u.IsEmailVerified { u.IsEmailVerified = true u.Status = user.StatusActive updated = true } if updated { _ = s.userRepo.Update(u) } } else { var avatar *string if googleUser.Picture != "" { avatar = &googleUser.Picture } googleID := googleUser.Sub newUser := &user.User{ Name: googleUser.Name, Email: googleUser.Email, GoogleID: &googleID, AvatarURL: avatar, Role: user.RoleUser, Status: user.StatusActive, IsEmailVerified: true, } if err := s.userRepo.Create(newUser); err != nil { return nil, err } u = newUser } return s.generateLoginResponse(u) } func (s *service) generateLoginResponse(u *user.User) (*LoginResponse, error) { accessToken, err := utils.GenerateAccessToken(u.ID, u.Email, u.Role) if err != nil { return nil, err } refreshToken, err := utils.GenerateRefreshToken(u.ID) if err != nil { return nil, err } expiresAt := time.Now().Add(time.Duration(config.Cfg.JWTRefreshExpiresHours) * time.Hour) rtRecord := &RefreshToken{ UserID: u.ID, Token: refreshToken, ExpiresAt: expiresAt, } if err := s.authRepo.ReplaceUserRefreshToken(u.ID, rtRecord); err != nil { return nil, err } resp := &LoginResponse{ AccessToken: accessToken, RefreshToken: refreshToken, } resp.User.ID = u.ID resp.User.Name = u.Name resp.User.Email = u.Email resp.User.AvatarURL = utils.FormatMediaURLPtr(u.AvatarURL) resp.User.Role = u.Role resp.User.Status = u.Status resp.User.IsEmailVerified = u.IsEmailVerified return resp, nil } func (s *service) RefreshToken(req RefreshTokenRequest) (*TokenResponse, error) { claims, err := utils.ValidateRefreshToken(req.RefreshToken) if err != nil { return nil, ErrInvalidToken } savedToken, err := s.authRepo.FindByToken(req.RefreshToken) if err != nil || savedToken == nil { return nil, ErrInvalidToken } if time.Now().After(savedToken.ExpiresAt) { _ = s.authRepo.DeleteByToken(req.RefreshToken) return nil, ErrInvalidToken } u, err := s.userRepo.FindByID(claims.UserID) if err != nil || u == nil { return nil, ErrInvalidToken } if u.Status == user.StatusSuspended { return nil, ErrAccountSuspended } newAccessToken, err := utils.GenerateAccessToken(u.ID, u.Email, u.Role) if err != nil { return nil, err } newRefreshToken, err := utils.GenerateRefreshToken(u.ID) if err != nil { return nil, err } expiresAt := time.Now().Add(time.Duration(config.Cfg.JWTRefreshExpiresHours) * time.Hour) newRtRecord := &RefreshToken{ UserID: u.ID, Token: newRefreshToken, ExpiresAt: expiresAt, } if err := s.authRepo.RotateRefreshToken(req.RefreshToken, newRtRecord); err != nil { return nil, err } return &TokenResponse{ AccessToken: newAccessToken, RefreshToken: newRefreshToken, }, nil } func (s *service) Logout(refreshToken string) error { if refreshToken == "" { return nil } return s.authRepo.DeleteByToken(refreshToken) } func (s *service) VerifyEmail(token string) error { u, err := s.userRepo.FindByVerificationToken(token) if err != nil || u == nil { return ErrInvalidToken } u.IsEmailVerified = true u.Status = user.StatusActive u.VerificationToken = "" return s.userRepo.Update(u) } func (s *service) ForgotPassword(email string) (string, error) { u, err := s.userRepo.FindByEmail(email) if err != nil || u == nil { return "", errors.New("email tidak ditemukan") } resetToken := generateRandomToken() expiresAt := time.Now().Add(1 * time.Hour) u.ResetPasswordToken = resetToken u.ResetPasswordExpiresAt = &expiresAt if err := s.userRepo.Update(u); err != nil { return "", err } return resetToken, nil } func (s *service) ResetPassword(req ResetPasswordRequest) error { if req.NewPassword != req.ConfirmNewPassword { return ErrPasswordMismatch } u, err := s.userRepo.FindByResetToken(req.Token) if err != nil || u == nil { return ErrInvalidToken } if u.ResetPasswordExpiresAt != nil && time.Now().After(*u.ResetPasswordExpiresAt) { return ErrInvalidToken } hashedPassword, err := utils.HashPassword(req.NewPassword) if err != nil { return err } u.Password = hashedPassword u.ResetPasswordToken = "" u.ResetPasswordExpiresAt = nil return s.userRepo.Update(u) } func verifyGoogleIDToken(idToken string) (*googleTokenPayload, error) { resp, err := http.Get("https://oauth2.googleapis.com/tokeninfo?id_token=" + idToken) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, errors.New("invalid google id token response status") } var payload googleTokenPayload if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { return nil, err } return &payload, nil } func generateRandomToken() string { bytes := make([]byte, 16) _, _ = rand.Read(bytes) return hex.EncodeToString(bytes) }