Files
cardverse-be/internal/modules/auth/service.go

349 lines
8.3 KiB
Go
Raw Normal View History

2026-07-30 09:51:25 +07:00
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)
}