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