815 lines
24 KiB
Go
815 lines
24 KiB
Go
package authentication
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"rijig/internal/role"
|
|
"rijig/model"
|
|
"rijig/utils"
|
|
)
|
|
|
|
type AuthenticationService interface {
|
|
GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error)
|
|
LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error)
|
|
RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) (*EmailVerificationResponse, error)
|
|
|
|
VerifyAdminOTP(ctx context.Context, req *VerifyAdminOTPRequest) (*AuthResponse, error)
|
|
ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error)
|
|
|
|
ForgotPassword(ctx context.Context, req *ForgotPasswordRequest) (*ResetPasswordResponse, error)
|
|
ResetPassword(ctx context.Context, req *ResetPasswordRequest) error
|
|
|
|
VerifyEmail(ctx context.Context, req *VerifyEmailRequest) error
|
|
ResendEmailVerification(ctx context.Context, req *ResendVerificationRequest) (*EmailVerificationResponse, error)
|
|
|
|
SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
|
|
VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
|
|
|
|
SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
|
|
VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
|
|
|
|
LogoutAuthentication(ctx context.Context, userID, deviceID string) error
|
|
}
|
|
|
|
type authenticationService struct {
|
|
authRepo AuthenticationRepository
|
|
roleRepo role.RoleRepository
|
|
emailService *utils.EmailService
|
|
}
|
|
|
|
func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService {
|
|
return &authenticationService{
|
|
authRepo: authRepo,
|
|
roleRepo: roleRepo,
|
|
emailService: utils.NewEmailService(),
|
|
}
|
|
}
|
|
|
|
// func normalizeRoleName(roleName string) string {
|
|
// switch strings.ToLower(roleName) {
|
|
// case "administrator", "admin":
|
|
// return utils.RoleAdministrator
|
|
// case "pengelola":
|
|
// return utils.RolePengelola
|
|
// case "pengepul":
|
|
// return utils.RolePengepul
|
|
// case "masyarakat":
|
|
// return utils.RoleMasyarakat
|
|
// default:
|
|
// return strings.ToLower(roleName)
|
|
// }
|
|
// }
|
|
|
|
type GetRegistrationStatusResponse struct {
|
|
UserID string `json:"userId"`
|
|
RegistrationStatus string `json:"registrationStatus"`
|
|
RegistrationProgress int8 `json:"registrationProgress"`
|
|
Name string `json:"name"`
|
|
Phone string `json:"phone"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
func (s *authenticationService) GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error) {
|
|
user, err := s.authRepo.FindUserByID(ctx, userID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found: %w", err)
|
|
}
|
|
|
|
if user.Role.RoleName == "" {
|
|
return nil, fmt.Errorf("user role not found")
|
|
}
|
|
|
|
if user.RegistrationStatus == utils.RegStatusPending {
|
|
log.Printf("⏳ User %s (%s) registration is still pending approval", user.Name, user.Phone)
|
|
|
|
return &AuthResponse{
|
|
Message: "Your registration is currently under review. Please wait for approval.",
|
|
RegistrationStatus: user.RegistrationStatus,
|
|
NextStep: "wait_for_approval",
|
|
}, nil
|
|
}
|
|
|
|
if user.RegistrationStatus == utils.RegStatusConfirmed || user.RegistrationStatus == utils.RegStatusRejected {
|
|
tokenResponse, err := utils.GenerateTokenPair(
|
|
user.ID,
|
|
user.Role.RoleName,
|
|
deviceID,
|
|
user.RegistrationStatus,
|
|
int(user.RegistrationProgress),
|
|
)
|
|
if err != nil {
|
|
log.Printf("GenerateTokenPair error: %v", err)
|
|
return nil, fmt.Errorf("failed to generate token: %v", err)
|
|
}
|
|
|
|
nextStep := utils.GetNextRegistrationStep(
|
|
user.Role.RoleName,
|
|
int(user.RegistrationProgress),
|
|
user.RegistrationStatus,
|
|
)
|
|
|
|
var message string
|
|
if user.RegistrationStatus == utils.RegStatusConfirmed {
|
|
message = "Registration approved successfully"
|
|
log.Printf("✅ User %s (%s) registration approved - generating tokens", user.Name, user.Phone)
|
|
} else if user.RegistrationStatus == utils.RegStatusRejected {
|
|
message = "Registration has been rejected"
|
|
log.Printf("❌ User %s (%s) registration rejected - generating tokens for rejection flow", user.Name, user.Phone)
|
|
}
|
|
|
|
return &AuthResponse{
|
|
Message: message,
|
|
AccessToken: tokenResponse.AccessToken,
|
|
RefreshToken: tokenResponse.RefreshToken,
|
|
TokenType: string(tokenResponse.TokenType),
|
|
ExpiresIn: tokenResponse.ExpiresIn,
|
|
RegistrationStatus: user.RegistrationStatus,
|
|
NextStep: nextStep,
|
|
SessionID: tokenResponse.SessionID,
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported registration status: %s", user.RegistrationStatus)
|
|
}
|
|
|
|
func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error) {
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid credentials")
|
|
}
|
|
|
|
if user.Role == nil || user.Role.RoleName != "administrator" {
|
|
return nil, fmt.Errorf("invalid credentials")
|
|
}
|
|
|
|
if user.RegistrationStatus != "complete" {
|
|
return nil, fmt.Errorf("account not activated")
|
|
}
|
|
|
|
if !utils.CompareHashAndPlainText(user.Password, req.Password) {
|
|
return nil, fmt.Errorf("invalid credentials")
|
|
}
|
|
|
|
if utils.IsOTPValid(req.Email) {
|
|
remaining, _ := utils.GetOTPRemainingTime(req.Email)
|
|
return &OTPAdminResponse{
|
|
Message: "OTP sudah dikirim sebelumnya",
|
|
Email: req.Email,
|
|
ExpiresIn: remaining,
|
|
RemainingTime: formatDuration(remaining),
|
|
CanResend: false,
|
|
MaxAttempts: utils.MAX_OTP_ATTEMPTS,
|
|
}, nil
|
|
}
|
|
|
|
otp, err := utils.GenerateOTP()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate OTP")
|
|
}
|
|
|
|
if err := utils.StoreOTP(req.Email, otp); err != nil {
|
|
return nil, fmt.Errorf("failed to store OTP")
|
|
}
|
|
|
|
if err := s.emailService.SendOTPEmail(req.Email, user.Name, otp); err != nil {
|
|
log.Printf("Failed to send OTP email: %v", err)
|
|
return nil, fmt.Errorf("failed to send OTP email")
|
|
}
|
|
|
|
return &OTPAdminResponse{
|
|
Message: "Kode OTP berhasil dikirim ke email Anda",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.OTP_EXPIRY,
|
|
RemainingTime: formatDuration(utils.OTP_EXPIRY),
|
|
CanResend: false,
|
|
MaxAttempts: utils.MAX_OTP_ATTEMPTS,
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) VerifyAdminOTP(ctx context.Context, req *VerifyAdminOTPRequest) (*AuthResponse, error) {
|
|
|
|
if err := utils.ValidateOTP(req.Email, req.OTP); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user not found")
|
|
}
|
|
|
|
if !user.EmailVerified {
|
|
user.EmailVerified = true
|
|
if err := s.authRepo.UpdateUser(ctx, user); err != nil {
|
|
log.Printf("Failed to update email verification status: %v", err)
|
|
}
|
|
}
|
|
|
|
token, err := utils.GenerateTokenPair(
|
|
user.ID,
|
|
user.Role.RoleName,
|
|
req.DeviceID,
|
|
user.RegistrationStatus,
|
|
int(user.RegistrationProgress),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
|
}
|
|
|
|
return &AuthResponse{
|
|
Message: "Login berhasil",
|
|
AccessToken: token.AccessToken,
|
|
RefreshToken: token.RefreshToken,
|
|
RegistrationStatus: user.RegistrationStatus,
|
|
SessionID: token.SessionID,
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error) {
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("email not found")
|
|
}
|
|
|
|
if user.Role == nil || user.Role.RoleName != "administrator" {
|
|
return nil, fmt.Errorf("not authorized")
|
|
}
|
|
|
|
if utils.IsOTPValid(req.Email) {
|
|
remaining, _ := utils.GetOTPRemainingTime(req.Email)
|
|
return nil, fmt.Errorf("OTP masih berlaku. Tunggu %s untuk kirim ulang", formatDuration(remaining))
|
|
}
|
|
|
|
otp, err := utils.GenerateOTP()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate OTP")
|
|
}
|
|
|
|
if err := utils.StoreOTP(req.Email, otp); err != nil {
|
|
return nil, fmt.Errorf("failed to store OTP")
|
|
}
|
|
|
|
if err := s.emailService.SendOTPEmail(req.Email, user.Name, otp); err != nil {
|
|
log.Printf("Failed to send OTP email: %v", err)
|
|
return nil, fmt.Errorf("failed to send OTP email")
|
|
}
|
|
|
|
return &OTPAdminResponse{
|
|
Message: "Kode OTP baru berhasil dikirim",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.OTP_EXPIRY,
|
|
RemainingTime: formatDuration(utils.OTP_EXPIRY),
|
|
CanResend: false,
|
|
MaxAttempts: utils.MAX_OTP_ATTEMPTS,
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) VerifyEmail(ctx context.Context, req *VerifyEmailRequest) error {
|
|
|
|
verificationData, err := utils.ValidateEmailVerificationToken(req.Email, req.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return fmt.Errorf("user not found")
|
|
}
|
|
|
|
if user.ID != verificationData.UserID {
|
|
return fmt.Errorf("invalid verification token")
|
|
}
|
|
|
|
if user.EmailVerified {
|
|
return fmt.Errorf("email sudah terverifikasi sebelumnya")
|
|
}
|
|
|
|
user.EmailVerified = true
|
|
user.RegistrationStatus = utils.RegStatusComplete
|
|
// user.RegistrationProgress = 3
|
|
|
|
if err := s.authRepo.UpdateUser(ctx, user); err != nil {
|
|
return fmt.Errorf("failed to update user verification status: %w", err)
|
|
}
|
|
|
|
if err := utils.MarkEmailVerificationTokenAsUsed(req.Email); err != nil {
|
|
log.Printf("Failed to mark verification token as used: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *authenticationService) ResendEmailVerification(ctx context.Context, req *ResendVerificationRequest) (*EmailVerificationResponse, error) {
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("email not found")
|
|
}
|
|
|
|
if user.Role == nil || user.Role.RoleName != "administrator" {
|
|
return nil, fmt.Errorf("not authorized")
|
|
}
|
|
|
|
if user.EmailVerified {
|
|
return nil, fmt.Errorf("email sudah terverifikasi")
|
|
}
|
|
|
|
if utils.IsEmailVerificationTokenValid(req.Email) {
|
|
remaining, _ := utils.GetEmailVerificationTokenRemainingTime(req.Email)
|
|
return &EmailVerificationResponse{
|
|
Message: "Email verifikasi sudah dikirim sebelumnya",
|
|
Email: req.Email,
|
|
ExpiresIn: remaining,
|
|
RemainingTime: formatDuration(remaining),
|
|
}, nil
|
|
}
|
|
|
|
token, err := utils.GenerateEmailVerificationToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate verification token")
|
|
}
|
|
|
|
if err := utils.StoreEmailVerificationToken(req.Email, user.ID, token); err != nil {
|
|
return nil, fmt.Errorf("failed to store verification token")
|
|
}
|
|
|
|
if err := s.emailService.SendEmailVerificationEmail(req.Email, user.Name, token); err != nil {
|
|
log.Printf("Failed to send verification email: %v", err)
|
|
return nil, fmt.Errorf("failed to send verification email")
|
|
}
|
|
|
|
return &EmailVerificationResponse{
|
|
Message: "Email verifikasi berhasil dikirim ulang",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.EMAIL_VERIFICATION_TOKEN_EXPIRY,
|
|
RemainingTime: formatDuration(utils.EMAIL_VERIFICATION_TOKEN_EXPIRY),
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) ForgotPassword(ctx context.Context, req *ForgotPasswordRequest) (*ResetPasswordResponse, error) {
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
|
|
return &ResetPasswordResponse{
|
|
Message: "Jika email terdaftar, link reset password akan dikirim",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.RESET_TOKEN_EXPIRY,
|
|
RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY),
|
|
}, nil
|
|
}
|
|
|
|
if user.Role == nil || user.Role.RoleName != "administrator" {
|
|
|
|
return &ResetPasswordResponse{
|
|
Message: "Jika email terdaftar, link reset password akan dikirim",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.RESET_TOKEN_EXPIRY,
|
|
RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY),
|
|
}, nil
|
|
}
|
|
|
|
if utils.IsResetTokenValid(req.Email) {
|
|
remaining, _ := utils.GetResetTokenRemainingTime(req.Email)
|
|
return &ResetPasswordResponse{
|
|
Message: "Link reset password sudah dikirim sebelumnya",
|
|
Email: req.Email,
|
|
ExpiresIn: remaining,
|
|
RemainingTime: formatDuration(remaining),
|
|
}, nil
|
|
}
|
|
|
|
token, err := utils.GenerateResetToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate reset token")
|
|
}
|
|
|
|
if err := utils.StoreResetToken(req.Email, user.ID, token); err != nil {
|
|
return nil, fmt.Errorf("failed to store reset token")
|
|
}
|
|
|
|
if err := s.emailService.SendResetPasswordEmail(req.Email, user.Name, token); err != nil {
|
|
log.Printf("Failed to send reset password email: %v", err)
|
|
return nil, fmt.Errorf("failed to send reset password email")
|
|
}
|
|
|
|
return &ResetPasswordResponse{
|
|
Message: "Link reset password berhasil dikirim ke email Anda",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.RESET_TOKEN_EXPIRY,
|
|
RemainingTime: formatDuration(utils.RESET_TOKEN_EXPIRY),
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) ResetPassword(ctx context.Context, req *ResetPasswordRequest) error {
|
|
|
|
resetData, err := utils.ValidateResetToken(req.Email, req.Token)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if err != nil {
|
|
return fmt.Errorf("user not found")
|
|
}
|
|
|
|
if user.ID != resetData.UserID {
|
|
return fmt.Errorf("invalid reset token")
|
|
}
|
|
|
|
hashedPassword, err := utils.HashingPlainText(req.NewPassword)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
user.Password = hashedPassword
|
|
if err := s.authRepo.UpdateUser(ctx, user); err != nil {
|
|
return fmt.Errorf("failed to update password: %w", err)
|
|
}
|
|
|
|
if err := utils.MarkResetTokenAsUsed(req.Email); err != nil {
|
|
log.Printf("Failed to mark reset token as used: %v", err)
|
|
}
|
|
|
|
if err := utils.RevokeAllRefreshTokens(user.ID); err != nil {
|
|
log.Printf("Failed to revoke refresh tokens: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) (*EmailVerificationResponse, error) {
|
|
|
|
existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email)
|
|
if existingUser != nil {
|
|
return nil, fmt.Errorf("email already in use")
|
|
}
|
|
|
|
hashedPassword, err := utils.HashingPlainText(req.Password)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to hash password: %w", err)
|
|
}
|
|
|
|
role, err := s.roleRepo.FindRoleByName(ctx, "administrator")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("role name not found: %w", err)
|
|
}
|
|
|
|
user := &model.User{
|
|
Name: req.Name,
|
|
Phone: req.Phone,
|
|
Email: req.Email,
|
|
Gender: req.Gender,
|
|
Dateofbirth: req.DateOfBirth,
|
|
Placeofbirth: req.PlaceOfBirth,
|
|
Password: hashedPassword,
|
|
RoleID: role.ID,
|
|
RegistrationStatus: "pending_email_verification",
|
|
// RegistrationProgress: 1,
|
|
EmailVerified: false,
|
|
}
|
|
|
|
if err := s.authRepo.CreateUser(ctx, user); err != nil {
|
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
|
}
|
|
|
|
token, err := utils.GenerateEmailVerificationToken()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate verification token")
|
|
}
|
|
|
|
if err := utils.StoreEmailVerificationToken(req.Email, user.ID, token); err != nil {
|
|
return nil, fmt.Errorf("failed to store verification token")
|
|
}
|
|
|
|
if err := s.emailService.SendEmailVerificationEmail(req.Email, user.Name, token); err != nil {
|
|
log.Printf("Failed to send verification email: %v", err)
|
|
return nil, fmt.Errorf("failed to send verification email")
|
|
}
|
|
|
|
return &EmailVerificationResponse{
|
|
Message: "Admin berhasil didaftarkan. Silakan cek email untuk verifikasi",
|
|
Email: req.Email,
|
|
ExpiresIn: utils.EMAIL_VERIFICATION_TOKEN_EXPIRY,
|
|
RemainingTime: formatDuration(utils.EMAIL_VERIFICATION_TOKEN_EXPIRY),
|
|
}, nil
|
|
}
|
|
|
|
func formatDuration(d time.Duration) string {
|
|
minutes := int(d.Minutes())
|
|
seconds := int(d.Seconds()) % 60
|
|
return fmt.Sprintf("%d:%02d", minutes, seconds)
|
|
}
|
|
|
|
func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
|
|
|
|
normalizedRole := strings.ToLower(req.RoleName)
|
|
|
|
existingUser, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole)
|
|
if err == nil && existingUser != nil {
|
|
return nil, fmt.Errorf("nomor telepon dengan role %s sudah terdaftar", req.RoleName)
|
|
}
|
|
|
|
roleData, err := s.roleRepo.FindRoleByName(ctx, normalizedRole)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("role tidak valid: %v", err)
|
|
}
|
|
|
|
rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone)
|
|
if isRateLimited(rateLimitKey, 3, 5*time.Minute) {
|
|
return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit")
|
|
}
|
|
|
|
otp, err := utils.GenerateOTP()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal generate OTP: %v", err)
|
|
}
|
|
|
|
otpKey := fmt.Sprintf("otp:%s:register", req.Phone)
|
|
otpData := OTPData{
|
|
Phone: req.Phone,
|
|
OTP: otp,
|
|
Role: normalizedRole,
|
|
RoleID: roleData.ID,
|
|
Type: "register",
|
|
Attempts: 0,
|
|
ExpiresAt: time.Now().Add(90 * time.Second),
|
|
}
|
|
|
|
err = utils.SetCacheWithTTL(otpKey, otpData, 90*time.Second)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal menyimpan OTP: %v", err)
|
|
}
|
|
|
|
err = sendOTP(req.Phone, otp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal mengirim OTP: %v", err)
|
|
}
|
|
|
|
return &OTPResponse{
|
|
Message: "OTP berhasil dikirim",
|
|
ExpiresIn: 90,
|
|
Phone: maskPhoneNumber(req.Phone),
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) {
|
|
|
|
otpKey := fmt.Sprintf("otp:%s:register", req.Phone)
|
|
var otpData OTPData
|
|
err := utils.GetCache(otpKey, &otpData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
|
|
}
|
|
|
|
if otpData.Attempts >= 3 {
|
|
utils.DeleteCache(otpKey)
|
|
return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru")
|
|
}
|
|
|
|
if otpData.OTP != req.Otp {
|
|
otpData.Attempts++
|
|
utils.SetCacheWithTTL(otpKey, otpData, time.Until(otpData.ExpiresAt))
|
|
return nil, fmt.Errorf("kode OTP salah")
|
|
}
|
|
|
|
if otpData.Role != req.RoleName {
|
|
return nil, fmt.Errorf("role tidak sesuai")
|
|
}
|
|
|
|
normalizedRole := strings.ToLower(req.RoleName)
|
|
|
|
user := &model.User{
|
|
Phone: req.Phone,
|
|
PhoneVerified: true,
|
|
RoleID: otpData.RoleID,
|
|
RegistrationStatus: utils.RegStatusIncomplete,
|
|
RegistrationProgress: utils.ProgressOTPVerified,
|
|
Name: "",
|
|
Gender: "",
|
|
Dateofbirth: "",
|
|
Placeofbirth: "",
|
|
}
|
|
|
|
err = s.authRepo.CreateUser(ctx, user)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal membuat user: %v", err)
|
|
}
|
|
|
|
if user.ID == "" {
|
|
return nil, fmt.Errorf("gagal mendapatkan user ID setelah registrasi")
|
|
}
|
|
|
|
utils.DeleteCache(otpKey)
|
|
|
|
tokenResponse, err := utils.GenerateTokenPair(
|
|
user.ID,
|
|
normalizedRole,
|
|
req.DeviceID,
|
|
user.RegistrationStatus,
|
|
int(user.RegistrationProgress),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal generate token: %v", err)
|
|
}
|
|
|
|
nextStep := utils.GetNextRegistrationStep(
|
|
normalizedRole,
|
|
int(user.RegistrationProgress),
|
|
user.RegistrationStatus,
|
|
)
|
|
|
|
return &AuthResponse{
|
|
Message: "Registrasi berhasil",
|
|
AccessToken: tokenResponse.AccessToken,
|
|
RefreshToken: tokenResponse.RefreshToken,
|
|
TokenType: string(tokenResponse.TokenType),
|
|
ExpiresIn: tokenResponse.ExpiresIn,
|
|
RegistrationStatus: user.RegistrationStatus,
|
|
NextStep: nextStep,
|
|
SessionID: tokenResponse.SessionID,
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) SendLoginOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
|
|
|
|
user, err := s.authRepo.FindUserByPhone(ctx, req.Phone)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("nomor telepon tidak terdaftar")
|
|
}
|
|
|
|
if !user.PhoneVerified {
|
|
return nil, fmt.Errorf("nomor telepon belum diverifikasi")
|
|
}
|
|
|
|
rateLimitKey := fmt.Sprintf("otp_limit:%s", req.Phone)
|
|
if isRateLimited(rateLimitKey, 3, 5*time.Minute) {
|
|
return nil, fmt.Errorf("terlalu banyak permintaan OTP, coba lagi dalam 5 menit")
|
|
}
|
|
|
|
otp, err := utils.GenerateOTP()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal generate OTP: %v", err)
|
|
}
|
|
|
|
otpKey := fmt.Sprintf("otp:%s:login", req.Phone)
|
|
otpData := OTPData{
|
|
Phone: req.Phone,
|
|
OTP: otp,
|
|
UserID: user.ID,
|
|
Role: user.Role.RoleName,
|
|
Type: "login",
|
|
Attempts: 0,
|
|
}
|
|
|
|
err = utils.SetCacheWithTTL(otpKey, otpData, 1*time.Minute)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal menyimpan OTP: %v", err)
|
|
}
|
|
|
|
err = sendOTP(req.Phone, otp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal mengirim OTP: %v", err)
|
|
}
|
|
|
|
return &OTPResponse{
|
|
Message: "OTP berhasil dikirim",
|
|
ExpiresIn: 300,
|
|
Phone: maskPhoneNumber(req.Phone),
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error) {
|
|
|
|
otpKey := fmt.Sprintf("otp:%s:login", req.Phone)
|
|
var otpData OTPData
|
|
err := utils.GetCache(otpKey, &otpData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
|
|
}
|
|
|
|
if otpData.Attempts >= 3 {
|
|
utils.DeleteCache(otpKey)
|
|
return nil, fmt.Errorf("terlalu banyak percobaan, silakan minta OTP baru")
|
|
}
|
|
|
|
if otpData.OTP != req.Otp {
|
|
otpData.Attempts++
|
|
utils.SetCache(otpKey, otpData, time.Until(otpData.ExpiresAt))
|
|
return nil, fmt.Errorf("kode OTP salah")
|
|
}
|
|
|
|
normalizedRole := strings.ToLower(req.RoleName)
|
|
|
|
user, err := s.authRepo.FindUserByPhoneAndRole(ctx, req.Phone, normalizedRole)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("user tidak ditemukan")
|
|
}
|
|
|
|
utils.DeleteCache(otpKey)
|
|
|
|
tokenResponse, err := utils.GenerateTokenPair(
|
|
user.ID,
|
|
normalizedRole,
|
|
req.DeviceID,
|
|
user.RegistrationStatus,
|
|
int(user.RegistrationProgress),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("gagal generate token: %v", err)
|
|
}
|
|
|
|
nextStep := utils.GetNextRegistrationStep(
|
|
normalizedRole,
|
|
int(user.RegistrationProgress),
|
|
user.RegistrationStatus,
|
|
)
|
|
|
|
var message string
|
|
if user.RegistrationStatus == utils.RegStatusComplete {
|
|
message = "verif pin"
|
|
nextStep = "verif_pin"
|
|
} else {
|
|
message = "otp berhasil diverifikasi"
|
|
}
|
|
|
|
return &AuthResponse{
|
|
Message: message,
|
|
AccessToken: tokenResponse.AccessToken,
|
|
RefreshToken: tokenResponse.RefreshToken,
|
|
TokenType: string(tokenResponse.TokenType),
|
|
ExpiresIn: tokenResponse.ExpiresIn,
|
|
RegistrationStatus: user.RegistrationStatus,
|
|
NextStep: nextStep,
|
|
SessionID: tokenResponse.SessionID,
|
|
}, nil
|
|
}
|
|
|
|
func (s *authenticationService) LogoutAuthentication(ctx context.Context, userID, deviceID string) error {
|
|
if err := utils.RevokeRefreshToken(userID, deviceID); err != nil {
|
|
return fmt.Errorf("failed to revoke token: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func maskPhoneNumber(phone string) string {
|
|
if len(phone) < 4 {
|
|
return phone
|
|
}
|
|
return phone[:4] + strings.Repeat("*", len(phone)-8) + phone[len(phone)-4:]
|
|
}
|
|
|
|
func isRateLimited(key string, maxAttempts int, duration time.Duration) bool {
|
|
var count int
|
|
err := utils.GetCache(key, &count)
|
|
if err != nil {
|
|
count = 0
|
|
}
|
|
|
|
if count >= maxAttempts {
|
|
return true
|
|
}
|
|
|
|
count++
|
|
utils.SetCache(key, count, duration)
|
|
return false
|
|
}
|
|
|
|
func sendOTP(phone, otp string) error {
|
|
|
|
fmt.Printf("Sending OTP %s to %s\n", otp, phone)
|
|
return nil
|
|
}
|
|
|
|
// func convertUserToResponse(user *model.User) *UserResponse {
|
|
// return &UserResponse{
|
|
// ID: user.ID,
|
|
// Name: user.Name,
|
|
// Phone: user.Phone,
|
|
// Email: user.Email,
|
|
// Role: user.Role.RoleName,
|
|
// RegistrationStatus: user.RegistrationStatus,
|
|
// RegistrationProgress: user.RegistrationProgress,
|
|
// PhoneVerified: user.PhoneVerified,
|
|
// Avatar: user.Avatar,
|
|
// }
|
|
// }
|
|
|
|
func IsRegistrationComplete(role string, progress int) bool {
|
|
switch role {
|
|
case "masyarakat":
|
|
return progress >= 1
|
|
case "pengepul":
|
|
return progress >= 2
|
|
case "pengelola":
|
|
return progress >= 3
|
|
}
|
|
return false
|
|
}
|