diff --git a/cmd/main.go b/cmd/main.go
index 7d80cb5..f3a4b92 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -51,4 +51,4 @@ func main() {
router.SetupRoutes(app)
config.StartServer(app)
-}
+}
\ No newline at end of file
diff --git a/config/database.go b/config/database.go
index 92a4356..abcb5ae 100644
--- a/config/database.go
+++ b/config/database.go
@@ -31,4 +31,4 @@ func ConnectDatabase() {
if err := RunMigrations(DB); err != nil {
log.Fatalf("Error performing auto-migration: %v", err)
}
-}
\ No newline at end of file
+}
diff --git a/go.mod b/go.mod
index 36bd859..552a803 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,11 @@ require (
gorm.io/gorm v1.25.12
)
+require (
+ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+ gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // direct
+)
+
// require (
// golang.org/x/term v0.30.0 // indirect
// rsc.io/qr v0.2.0 // indirect
diff --git a/go.sum b/go.sum
index 58846b8..e6077af 100644
--- a/go.sum
+++ b/go.sum
@@ -109,7 +109,11 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
+gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
+gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
diff --git a/internal/authentication/authentication_dto.go b/internal/authentication/authentication_dto.go
index b29aee8..801ff71 100644
--- a/internal/authentication/authentication_dto.go
+++ b/internal/authentication/authentication_dto.go
@@ -256,6 +256,59 @@ type LoginAdminRequest struct {
DeviceID string `json:"device_id"`
}
+type VerifyAdminOTPRequest struct {
+ Email string `json:"email" validate:"required,email"`
+ OTP string `json:"otp" validate:"required,len=6,numeric"`
+ DeviceID string `json:"device_id" validate:"required"`
+}
+
+type ResendAdminOTPRequest struct {
+ Email string `json:"email" validate:"required,email"`
+}
+
+type OTPAdminResponse struct {
+ Message string `json:"message"`
+ Email string `json:"email"`
+ ExpiresIn time.Duration `json:"expires_in_seconds"`
+ RemainingTime string `json:"remaining_time"`
+ CanResend bool `json:"can_resend"`
+ MaxAttempts int `json:"max_attempts"`
+}
+
+type ForgotPasswordRequest struct {
+ Email string `json:"email" validate:"required,email"`
+}
+
+type ResetPasswordRequest struct {
+ Email string `json:"email" validate:"required,email"`
+ Token string `json:"token" validate:"required"`
+ NewPassword string `json:"new_password" validate:"required,min=6"`
+}
+
+type ResetPasswordResponse struct {
+ Message string `json:"message"`
+ Email string `json:"email"`
+ ExpiresIn time.Duration `json:"expires_in_seconds"`
+ RemainingTime string `json:"remaining_time"`
+}
+
+type VerifyEmailRequest struct {
+ Email string `json:"email" validate:"required,email"`
+ Token string `json:"token" validate:"required"`
+}
+
+type ResendVerificationRequest struct {
+ Email string `json:"email" validate:"required,email"`
+}
+
+type EmailVerificationResponse struct {
+ Message string `json:"message"`
+ Email string `json:"email"`
+ ExpiresIn time.Duration `json:"expires_in_seconds"`
+ RemainingTime string `json:"remaining_time"`
+}
+
+
func (r *LoginorRegistRequest) ValidateLoginorRegistRequest() (map[string][]string, bool) {
errors := make(map[string][]string)
diff --git a/internal/authentication/authentication_handler.go b/internal/authentication/authentication_handler.go
index 60a6a86..613a4cc 100644
--- a/internal/authentication/authentication_handler.go
+++ b/internal/authentication/authentication_handler.go
@@ -106,31 +106,144 @@ func (h *AuthenticationHandler) Login(c *fiber.Ctx) error {
}
-func (h *AuthenticationHandler) Register(c *fiber.Ctx) error {
-
+func (h *AuthenticationHandler) RegisterAdmin(c *fiber.Ctx) error {
var req RegisterAdminRequest
if err := c.BodyParser(&req); err != nil {
return utils.BadRequest(c, "Invalid request format")
}
- if errs, ok := req.ValidateRegisterAdminRequest(); !ok {
- return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
- "meta": fiber.Map{
- "status": fiber.StatusBadRequest,
- "message": "periksa lagi inputan",
- },
- "errors": errs,
- })
- }
+ // if err := h.validator.Struct(&req); err != nil {
+ // return utils.BadRequest(c, "Validation failed: "+err.Error())
+ // }
- err := h.service.RegisterAdmin(c.Context(), &req)
+ response, err := h.service.RegisterAdmin(c.Context(), &req)
if err != nil {
- return utils.InternalServerError(c, err.Error())
+ return utils.BadRequest(c, err.Error())
}
- return utils.Success(c, "Registration successful, Please login")
+ return utils.SuccessWithData(c, "Admin registered successfully", response)
}
+// POST /auth/admin/verify-email - Verify email dari registration
+func (h *AuthenticationHandler) VerifyEmail(c *fiber.Ctx) error {
+ var req VerifyEmailRequest
+ if err := c.BodyParser(&req); err != nil {
+ return utils.BadRequest(c, "Invalid request format")
+ }
+
+ // if err := h.validator.Struct(&req); err != nil {
+ // return utils.BadRequest(c, "Validation failed: "+err.Error())
+ // }
+
+ err := h.service.VerifyEmail(c.Context(), &req)
+ if err != nil {
+ return utils.BadRequest(c, err.Error())
+ }
+
+ return utils.SuccessWithData(c, "Email berhasil diverifikasi. Sekarang Anda dapat login", nil)
+}
+
+// POST /auth/admin/resend-verification - Resend verification email
+func (h *AuthenticationHandler) ResendEmailVerification(c *fiber.Ctx) error {
+ var req ResendVerificationRequest
+ if err := c.BodyParser(&req); err != nil {
+ return utils.BadRequest(c, "Invalid request format")
+ }
+
+ // if err := h.validator.Struct(&req); err != nil {
+ // return utils.BadRequest(c, "Validation failed: "+err.Error())
+ // }
+
+ response, err := h.service.ResendEmailVerification(c.Context(), &req)
+ if err != nil {
+ return utils.BadRequest(c, err.Error())
+ }
+
+ return utils.SuccessWithData(c, "Verification email resent", response)
+}
+
+
+func (h *AuthenticationHandler) VerifyAdminOTP(c *fiber.Ctx) error {
+ var req VerifyAdminOTPRequest
+ if err := c.BodyParser(&req); err != nil {
+ return utils.BadRequest(c, "Invalid request format")
+ }
+
+ // if errs, ok := req.Valida(); !ok {
+ // return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+ // "meta": fiber.Map{
+ // "status": fiber.StatusBadRequest,
+ // "message": "periksa lagi inputan",
+ // },
+ // "errors": errs,
+ // })
+ // }
+
+ response, err := h.service.VerifyAdminOTP(c.Context(), &req)
+ if err != nil {
+ return utils.BadRequest(c, err.Error())
+ }
+
+ return utils.SuccessWithData(c, "OTP resent successfully", response)
+}
+
+// POST /auth/admin/resend-otp - Resend OTP
+func (h *AuthenticationHandler) ResendAdminOTP(c *fiber.Ctx) error {
+ var req ResendAdminOTPRequest
+ if err := c.BodyParser(&req); err != nil {
+ return utils.BadRequest(c, "Invalid request format")
+ }
+
+ // if err := h.validator.Struct(&req); err != nil {
+ // return utils.BadRequest(c, "Validation failed: "+err.Error())
+ // }
+
+ response, err := h.service.ResendAdminOTP(c.Context(), &req)
+ if err != nil {
+ return utils.BadRequest(c, err.Error())
+ }
+
+ return utils.SuccessWithData(c, "OTP resent successfully", response)
+}
+
+func (h *AuthenticationHandler) ForgotPassword(c *fiber.Ctx) error {
+ var req ForgotPasswordRequest
+ if err := c.BodyParser(&req); err != nil {
+ return utils.BadRequest(c, "Invalid request format")
+ }
+
+ // if err := h.validator.Struct(&req); err != nil {
+ // return utils.BadRequest(c, "Validation failed: "+err.Error())
+ // }
+
+ response, err := h.service.ForgotPassword(c.Context(), &req)
+ if err != nil {
+ return utils.BadRequest(c, err.Error())
+ }
+
+ return utils.SuccessWithData(c, "Reset password email sent", response)
+}
+
+// POST /auth/admin/reset-password - Step 2: Reset password dengan token
+func (h *AuthenticationHandler) ResetPassword(c *fiber.Ctx) error {
+ var req ResetPasswordRequest
+ if err := c.BodyParser(&req); err != nil {
+ return utils.BadRequest(c, "Invalid request format")
+ }
+
+ // if err := h.validator.Struct(&req); err != nil {
+ // return utils.BadRequest(c, "Validation failed: "+err.Error())
+ // }
+
+ err := h.service.ResetPassword(c.Context(), &req)
+ if err != nil {
+ return utils.BadRequest(c, err.Error())
+ }
+
+ return utils.SuccessWithData(c, "Password berhasil direset", nil)
+}
+
+
func (h *AuthenticationHandler) RequestOtpHandler(c *fiber.Ctx) error {
var req LoginorRegistRequest
if err := c.BodyParser(&req); err != nil {
diff --git a/internal/authentication/authentication_route.go b/internal/authentication/authentication_route.go
index 149a019..bb5058f 100644
--- a/internal/authentication/authentication_route.go
+++ b/internal/authentication/authentication_route.go
@@ -32,7 +32,17 @@ func AuthenticationRouter(api fiber.Router) {
authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus)
authRoute.Post("/login/admin", authHandler.Login)
- authRoute.Post("/register/admin", authHandler.Register)
+ authRoute.Post("/register/admin", authHandler.RegisterAdmin)
+
+ authRoute.Post("/verify-email", authHandler.VerifyEmail)
+ authRoute.Post("/resend-verification", authHandler.ResendEmailVerification)
+
+ authRoute.Post("/verify-otp-admin", authHandler.VerifyAdminOTP)
+ authRoute.Post("/resend-otp-admin", authHandler.ResendAdminOTP)
+
+ authRoute.Post("/forgot-password", authHandler.ForgotPassword)
+ authRoute.Post("/reset-password", authHandler.ResetPassword)
+
authRoute.Post("/request-otp", authHandler.RequestOtpHandler)
authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler)
authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler)
diff --git a/internal/authentication/authentication_service.go b/internal/authentication/authentication_service.go
index d984642..980c5c8 100644
--- a/internal/authentication/authentication_service.go
+++ b/internal/authentication/authentication_service.go
@@ -14,8 +14,17 @@ import (
type AuthenticationService interface {
GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error)
- LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error)
- RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) 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)
@@ -27,29 +36,34 @@ type AuthenticationService interface {
}
type authenticationService struct {
- authRepo AuthenticationRepository
- roleRepo role.RoleRepository
+ authRepo AuthenticationRepository
+ roleRepo role.RoleRepository
+ emailService *utils.EmailService
}
func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService {
- return &authenticationService{authRepo, roleRepo}
-}
-
-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)
+ 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"`
@@ -122,31 +136,92 @@ func (s *authenticationService) GetRegistrationStatus(ctx context.Context, userI
return nil, fmt.Errorf("unsupported registration status: %s", user.RegistrationStatus)
}
-func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error) {
+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("user not found: %w", err)
+ return nil, fmt.Errorf("invalid credentials")
}
if user.Role == nil || user.Role.RoleName != "administrator" {
- return nil, fmt.Errorf("user not found: %w", err)
+ return nil, fmt.Errorf("invalid credentials")
}
if user.RegistrationStatus != "completed" {
- return nil, fmt.Errorf("user not found: %w", err)
+ return nil, fmt.Errorf("account not activated")
}
if !utils.CompareHashAndPlainText(user.Password, req.Password) {
- return nil, fmt.Errorf("user not found: %w", err)
+ return nil, fmt.Errorf("invalid credentials")
}
- token, err := utils.GenerateTokenPair(user.ID, user.Role.RoleName, req.DeviceID, user.RegistrationStatus, int(user.RegistrationProgress))
+ 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",
+ Message: "Login berhasil",
AccessToken: token.AccessToken,
RefreshToken: token.RefreshToken,
RegistrationStatus: user.RegistrationStatus,
@@ -154,42 +229,283 @@ func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminR
}, nil
}
-func (s *authenticationService) RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) error {
+func (s *authenticationService) ResendAdminOTP(ctx context.Context, req *ResendAdminOTPRequest) (*OTPAdminResponse, error) {
- existingUser, _ := s.authRepo.FindUserByEmail(ctx, req.Email)
- if existingUser != nil {
- return fmt.Errorf("email already in use")
+ user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
+ if err != nil {
+ return nil, fmt.Errorf("email not found")
}
- hashedPassword, err := utils.HashingPlainText(req.Password)
+ 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)
}
- role, err := s.roleRepo.FindRoleByName(ctx, "administrator")
- if err != nil {
- return fmt.Errorf("role name not found: %w", err)
+ user.Password = hashedPassword
+ if err := s.authRepo.UpdateUser(ctx, user); err != nil {
+ return fmt.Errorf("failed to update password: %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: "completed",
+ if err := utils.MarkResetTokenAsUsed(req.Email); err != nil {
+ log.Printf("Failed to mark reset token as used: %v", err)
}
- if err := s.authRepo.CreateUser(ctx, user); err != nil {
- return fmt.Errorf("failed to create user: %w", 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)
@@ -418,7 +734,7 @@ func (s *authenticationService) VerifyLoginOTP(ctx context.Context, req *VerifyO
var message string
if user.RegistrationStatus == utils.RegStatusComplete {
message = "verif pin"
- nextStep = "verif_pin"
+ nextStep = "verif_pin"
} else {
message = "otp berhasil diverifikasi"
}
@@ -471,19 +787,19 @@ func sendOTP(phone, otp string) error {
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 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 {
diff --git a/middleware/api_key.go b/middleware/api_key.go
deleted file mode 100644
index 1077eb0..0000000
--- a/middleware/api_key.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package middleware
-
-import (
- "os"
-
- "rijig/utils"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func APIKeyMiddleware(c *fiber.Ctx) error {
- apiKey := c.Get("x-api-key")
- if apiKey == "" {
- return utils.Unauthorized(c, "Unauthorized: API key is required")
- }
-
- validAPIKey := os.Getenv("API_KEY")
- if apiKey != validAPIKey {
- return utils.Unauthorized(c, "Unauthorized: Invalid API key")
- }
-
- return c.Next()
-}
diff --git a/middleware/middleware.go b/middleware/middleware.go
index 9f736c0..6da7a10 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -3,6 +3,7 @@ package middleware
import (
"crypto/subtle"
"fmt"
+ "os"
"rijig/utils"
"time"
@@ -122,6 +123,20 @@ func getStatusCodeForError(errorCode string) int {
}
}
+func APIKeyMiddleware(c *fiber.Ctx) error {
+ apiKey := c.Get("x-api-key")
+ if apiKey == "" {
+ return utils.Unauthorized(c, "Unauthorized: API key is required")
+ }
+
+ validAPIKey := os.Getenv("API_KEY")
+ if apiKey != validAPIKey {
+ return utils.Unauthorized(c, "Unauthorized: Invalid API key")
+ }
+
+ return c.Next()
+}
+
func AuthMiddleware(config ...AuthConfig) fiber.Handler {
cfg := AuthConfig{}
if len(config) > 0 {
diff --git a/model/user_model.go b/model/user_model.go
index faefec9..323c6ac 100644
--- a/model/user_model.go
+++ b/model/user_model.go
@@ -11,6 +11,7 @@ type User struct {
Placeofbirth string `gorm:"not null" json:"placeofbirth"`
Phone string `gorm:"not null;index" json:"phone"`
Email string `json:"email,omitempty"`
+ EmailVerified bool `gorm:"default:false" json:"emailVerified"`
PhoneVerified bool `gorm:"default:false" json:"phoneVerified"`
Password string `json:"password,omitempty"`
RoleID string `gorm:"not null" json:"roleId"`
diff --git a/utils/email_utils.go b/utils/email_utils.go
new file mode 100644
index 0000000..53f0926
--- /dev/null
+++ b/utils/email_utils.go
@@ -0,0 +1,179 @@
+package utils
+
+import (
+ "fmt"
+ "os"
+ "strconv"
+ "time"
+
+ "gopkg.in/gomail.v2"
+)
+
+type EmailService struct {
+ host string
+ port int
+ username string
+ password string
+ from string
+ fromName string
+}
+
+type OTPData struct {
+ Code string `json:"code"`
+ Email string `json:"email"`
+ ExpiresAt int64 `json:"expires_at"`
+ Attempts int `json:"attempts"`
+ CreatedAt int64 `json:"created_at"`
+}
+
+const (
+ OTP_LENGTH = 6
+ OTP_EXPIRY = 5 * time.Minute
+ MAX_OTP_ATTEMPTS = 3
+)
+
+func NewEmailService() *EmailService {
+ port, _ := strconv.Atoi("587")
+
+ return &EmailService{
+ host: "smtp.gmail.com",
+ port: port,
+ username: os.Getenv("SMTP_FROM_EMAIL"),
+ password: os.Getenv("GMAIL_APP_PASSWORD"),
+ from: os.Getenv("SMTP_FROM_EMAIL"),
+ fromName: os.Getenv("SMTP_FROM_NAME"),
+ }
+}
+
+func StoreOTP(email, otp string) error {
+ key := fmt.Sprintf("otp:admin:%s", email)
+
+ data := OTPData{
+ Code: otp,
+ Email: email,
+ ExpiresAt: time.Now().Add(OTP_EXPIRY).Unix(),
+ Attempts: 0,
+ CreatedAt: time.Now().Unix(),
+ }
+
+ return SetCache(key, data, OTP_EXPIRY)
+}
+
+func ValidateOTP(email, inputOTP string) error {
+ key := fmt.Sprintf("otp:admin:%s", email)
+
+ var data OTPData
+ err := GetCache(key, &data)
+ if err != nil {
+ return fmt.Errorf("OTP tidak ditemukan atau sudah kadaluarsa")
+ }
+
+ if time.Now().Unix() > data.ExpiresAt {
+ DeleteCache(key)
+ return fmt.Errorf("OTP sudah kadaluarsa")
+ }
+
+ if data.Attempts >= MAX_OTP_ATTEMPTS {
+ DeleteCache(key)
+ return fmt.Errorf("OTP diblokir karena terlalu banyak percobaan salah")
+ }
+
+ if data.Code != inputOTP {
+
+ data.Attempts++
+ SetCache(key, data, time.Until(time.Unix(data.ExpiresAt, 0)))
+ return fmt.Errorf("OTP tidak valid. Sisa percobaan: %d", MAX_OTP_ATTEMPTS-data.Attempts)
+ }
+
+ DeleteCache(key)
+ return nil
+}
+
+func (e *EmailService) SendOTPEmail(email, name, otp string) error {
+ m := gomail.NewMessage()
+ m.SetHeader("From", m.FormatAddress(e.from, e.fromName))
+ m.SetHeader("To", email)
+ m.SetHeader("Subject", "Kode Verifikasi Login Administrator - Rijig")
+
+ body := fmt.Sprintf(`
+
+
+
+
+
+
+
+
+
+
+
Halo %s,
+
Anda telah meminta untuk login sebagai Administrator. Gunakan kode verifikasi berikut:
+
+
%s
+
+
Penting:
+
+ - Kode ini berlaku selama 5 menit
+ - Jangan berikan kode ini kepada siapapun
+ - Maksimal 3 kali percobaan
+
+
+
⚠️ Jika Anda tidak melakukan permintaan login ini, abaikan email ini.
+
+
+
+
+
+ `, name, otp)
+
+ m.SetBody("text/html", body)
+
+ d := gomail.NewDialer(e.host, e.port, e.username, e.password)
+
+ if err := d.DialAndSend(m); err != nil {
+ return fmt.Errorf("failed to send email: %v", err)
+ }
+
+ return nil
+}
+
+func IsOTPValid(email string) bool {
+ key := fmt.Sprintf("otp:admin:%s", email)
+
+ var data OTPData
+ err := GetCache(key, &data)
+ if err != nil {
+ return false
+ }
+
+ return time.Now().Unix() <= data.ExpiresAt && data.Attempts < MAX_OTP_ATTEMPTS
+}
+
+func GetOTPRemainingTime(email string) (time.Duration, error) {
+ key := fmt.Sprintf("otp:admin:%s", email)
+
+ var data OTPData
+ err := GetCache(key, &data)
+ if err != nil {
+ return 0, err
+ }
+
+ remaining := time.Until(time.Unix(data.ExpiresAt, 0))
+ if remaining < 0 {
+ return 0, fmt.Errorf("OTP expired")
+ }
+
+ return remaining, nil
+}
diff --git a/utils/email_verification.go b/utils/email_verification.go
new file mode 100644
index 0000000..7f6dc02
--- /dev/null
+++ b/utils/email_verification.go
@@ -0,0 +1,208 @@
+package utils
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "time"
+
+ "gopkg.in/gomail.v2"
+)
+
+type EmailVerificationData struct {
+ Token string `json:"token"`
+ Email string `json:"email"`
+ UserID string `json:"user_id"`
+ ExpiresAt int64 `json:"expires_at"`
+ Used bool `json:"used"`
+ CreatedAt int64 `json:"created_at"`
+}
+
+const (
+ EMAIL_VERIFICATION_TOKEN_EXPIRY = 24 * time.Hour
+ EMAIL_VERIFICATION_TOKEN_LENGTH = 32
+)
+
+func GenerateEmailVerificationToken() (string, error) {
+ bytes := make([]byte, EMAIL_VERIFICATION_TOKEN_LENGTH)
+ _, err := rand.Read(bytes)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate email verification token: %v", err)
+ }
+ return base64.URLEncoding.EncodeToString(bytes), nil
+}
+
+func StoreEmailVerificationToken(email, userID, token string) error {
+ key := fmt.Sprintf("email_verification:%s", email)
+
+ DeleteCache(key)
+
+ data := EmailVerificationData{
+ Token: token,
+ Email: email,
+ UserID: userID,
+ ExpiresAt: time.Now().Add(EMAIL_VERIFICATION_TOKEN_EXPIRY).Unix(),
+ Used: false,
+ CreatedAt: time.Now().Unix(),
+ }
+
+ return SetCache(key, data, EMAIL_VERIFICATION_TOKEN_EXPIRY)
+}
+
+func ValidateEmailVerificationToken(email, inputToken string) (*EmailVerificationData, error) {
+ key := fmt.Sprintf("email_verification:%s", email)
+
+ var data EmailVerificationData
+ err := GetCache(key, &data)
+ if err != nil {
+ return nil, fmt.Errorf("token verifikasi tidak ditemukan atau sudah kadaluarsa")
+ }
+
+ if time.Now().Unix() > data.ExpiresAt {
+ DeleteCache(key)
+ return nil, fmt.Errorf("token verifikasi sudah kadaluarsa")
+ }
+
+ if data.Used {
+ return nil, fmt.Errorf("token verifikasi sudah digunakan")
+ }
+
+ // Validate token
+ if !ConstantTimeCompare(data.Token, inputToken) {
+ return nil, fmt.Errorf("token verifikasi tidak valid")
+ }
+
+ return &data, nil
+}
+
+// Mark email verification token as used
+func MarkEmailVerificationTokenAsUsed(email string) error {
+ key := fmt.Sprintf("email_verification:%s", email)
+
+ var data EmailVerificationData
+ err := GetCache(key, &data)
+ if err != nil {
+ return err
+ }
+
+ data.Used = true
+ remaining := time.Until(time.Unix(data.ExpiresAt, 0))
+
+ return SetCache(key, data, remaining)
+}
+
+// Check if email verification token exists and still valid
+func IsEmailVerificationTokenValid(email string) bool {
+ key := fmt.Sprintf("email_verification:%s", email)
+
+ var data EmailVerificationData
+ err := GetCache(key, &data)
+ if err != nil {
+ return false
+ }
+
+ return time.Now().Unix() <= data.ExpiresAt && !data.Used
+}
+
+// Get remaining email verification token time
+func GetEmailVerificationTokenRemainingTime(email string) (time.Duration, error) {
+ key := fmt.Sprintf("email_verification:%s", email)
+
+ var data EmailVerificationData
+ err := GetCache(key, &data)
+ if err != nil {
+ return 0, err
+ }
+
+ remaining := time.Until(time.Unix(data.ExpiresAt, 0))
+ if remaining < 0 {
+ return 0, fmt.Errorf("token expired")
+ }
+
+ return remaining, nil
+}
+
+// Send email verification email
+func (e *EmailService) SendEmailVerificationEmail(email, name, token string) error {
+ // Create verification URL - in real app this would be frontend URL
+ verificationURL := fmt.Sprintf("http://localhost:3000/verify-email?token=%s&email=%s", token, email)
+
+ m := gomail.NewMessage()
+ m.SetHeader("From", m.FormatAddress(e.from, e.fromName))
+ m.SetHeader("To", email)
+ m.SetHeader("Subject", "Verifikasi Email Administrator - Rijig")
+
+ // Email template
+ body := fmt.Sprintf(`
+
+
+
+
+
+
+
+
+
+
+
🎉
+
+
Selamat %s!
+
Akun Administrator Anda telah berhasil dibuat. Untuk mengaktifkan akun dan mulai menggunakan sistem Rijig, silakan verifikasi email Anda dengan mengklik tombol di bawah ini:
+
+
+
+
Atau copy paste link berikut ke browser Anda:
+
%s
+
+
+
Informasi Penting:
+
+ - Link verifikasi berlaku selama 24 jam
+ - Setelah verifikasi, Anda dapat login ke sistem
+ - Link hanya dapat digunakan sekali
+ - Jangan bagikan link ini kepada siapapun
+
+
+
+
Langkah selanjutnya setelah verifikasi:
+
+ - Login menggunakan email dan password
+ - Masukkan kode OTP yang dikirim ke email
+ - Mulai menggunakan sistem Rijig
+
+
+
Jika Anda tidak membuat akun ini, abaikan email ini.
+
+
+
+
+
+ `, name, verificationURL, verificationURL)
+
+ m.SetBody("text/html", body)
+
+ d := gomail.NewDialer(e.host, e.port, e.username, e.password)
+
+ if err := d.DialAndSend(m); err != nil {
+ return fmt.Errorf("failed to send email verification email: %v", err)
+ }
+
+ return nil
+}
diff --git a/utils/reset_password.go b/utils/reset_password.go
new file mode 100644
index 0000000..431987e
--- /dev/null
+++ b/utils/reset_password.go
@@ -0,0 +1,202 @@
+package utils
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "time"
+
+ "gopkg.in/gomail.v2"
+)
+
+type ResetPasswordData struct {
+ Token string `json:"token"`
+ Email string `json:"email"`
+ UserID string `json:"user_id"`
+ ExpiresAt int64 `json:"expires_at"`
+ Used bool `json:"used"`
+ CreatedAt int64 `json:"created_at"`
+}
+
+const (
+ RESET_TOKEN_EXPIRY = 30 * time.Minute
+ RESET_TOKEN_LENGTH = 32
+)
+
+// Generate secure reset token
+func GenerateResetToken() (string, error) {
+ bytes := make([]byte, RESET_TOKEN_LENGTH)
+ _, err := rand.Read(bytes)
+ if err != nil {
+ return "", fmt.Errorf("failed to generate reset token: %v", err)
+ }
+ return base64.URLEncoding.EncodeToString(bytes), nil
+}
+
+// Store reset password token di Redis
+func StoreResetToken(email, userID, token string) error {
+ key := fmt.Sprintf("reset_password:%s", email)
+
+ // Delete any existing reset token for this email
+ DeleteCache(key)
+
+ data := ResetPasswordData{
+ Token: token,
+ Email: email,
+ UserID: userID,
+ ExpiresAt: time.Now().Add(RESET_TOKEN_EXPIRY).Unix(),
+ Used: false,
+ CreatedAt: time.Now().Unix(),
+ }
+
+ return SetCache(key, data, RESET_TOKEN_EXPIRY)
+}
+
+// Validate reset password token
+func ValidateResetToken(email, inputToken string) (*ResetPasswordData, error) {
+ key := fmt.Sprintf("reset_password:%s", email)
+
+ var data ResetPasswordData
+ err := GetCache(key, &data)
+ if err != nil {
+ return nil, fmt.Errorf("token reset tidak ditemukan atau sudah kadaluarsa")
+ }
+
+ // Check if token is expired
+ if time.Now().Unix() > data.ExpiresAt {
+ DeleteCache(key)
+ return nil, fmt.Errorf("token reset sudah kadaluarsa")
+ }
+
+ // Check if token is already used
+ if data.Used {
+ return nil, fmt.Errorf("token reset sudah digunakan")
+ }
+
+ // Validate token
+ if !ConstantTimeCompare(data.Token, inputToken) {
+ return nil, fmt.Errorf("token reset tidak valid")
+ }
+
+ return &data, nil
+}
+
+// Mark reset token as used
+func MarkResetTokenAsUsed(email string) error {
+ key := fmt.Sprintf("reset_password:%s", email)
+
+ var data ResetPasswordData
+ err := GetCache(key, &data)
+ if err != nil {
+ return err
+ }
+
+ data.Used = true
+ remaining := time.Until(time.Unix(data.ExpiresAt, 0))
+
+ return SetCache(key, data, remaining)
+}
+
+// Check if reset token exists and still valid
+func IsResetTokenValid(email string) bool {
+ key := fmt.Sprintf("reset_password:%s", email)
+
+ var data ResetPasswordData
+ err := GetCache(key, &data)
+ if err != nil {
+ return false
+ }
+
+ return time.Now().Unix() <= data.ExpiresAt && !data.Used
+}
+
+// Get remaining reset token time
+func GetResetTokenRemainingTime(email string) (time.Duration, error) {
+ key := fmt.Sprintf("reset_password:%s", email)
+
+ var data ResetPasswordData
+ err := GetCache(key, &data)
+ if err != nil {
+ return 0, err
+ }
+
+ remaining := time.Until(time.Unix(data.ExpiresAt, 0))
+ if remaining < 0 {
+ return 0, fmt.Errorf("token expired")
+ }
+
+ return remaining, nil
+}
+
+// Send reset password email
+func (e *EmailService) SendResetPasswordEmail(email, name, token string) error {
+ // Create reset URL - in real app this would be frontend URL
+ resetURL := fmt.Sprintf("http://localhost:3000/reset-password?token=%s&email=%s", token, email)
+
+ m := gomail.NewMessage()
+ m.SetHeader("From", m.FormatAddress(e.from, e.fromName))
+ m.SetHeader("To", email)
+ m.SetHeader("Subject", "Reset Password Administrator - Rijig")
+
+ // Email template
+ body := fmt.Sprintf(`
+
+
+
+
+
+
+
+
+
+
+
Halo %s,
+
Kami menerima permintaan untuk reset password akun Administrator Anda.
+
+
Klik tombol di bawah ini untuk reset password:
+
+
+
Atau copy paste link berikut ke browser Anda:
+
%s
+
+
Penting:
+
+ - Link ini berlaku selama 30 menit
+ - Link hanya dapat digunakan sekali
+ - Jangan bagikan link ini kepada siapapun
+
+
+
⚠️ Jika Anda tidak melakukan permintaan reset password, abaikan email ini dan password Anda tidak akan berubah.
+
+
+
+
+
+ `, name, resetURL, resetURL)
+
+ m.SetBody("text/html", body)
+
+ d := gomail.NewDialer(e.host, e.port, e.username, e.password)
+
+ if err := d.DialAndSend(m); err != nil {
+ return fmt.Errorf("failed to send reset password email: %v", err)
+ }
+
+ return nil
+}
diff --git a/utils/response.go b/utils/response.go
deleted file mode 100644
index a36fb64..0000000
--- a/utils/response.go
+++ /dev/null
@@ -1,107 +0,0 @@
-package utils
-
-// import (
-// "github.com/gofiber/fiber/v2"
-// )
-
-// type MetaData struct {
-// Status int `json:"status"`
-// Page int `json:"page,omitempty"`
-// Limit int `json:"limit,omitempty"`
-// Total int `json:"total,omitempty"`
-// Message string `json:"message"`
-// }
-
-// type APIResponse struct {
-// Meta MetaData `json:"meta"`
-// Data interface{} `json:"data,omitempty"`
-// }
-
-// func PaginatedResponse(c *fiber.Ctx, data interface{}, page, limit, total int, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusOK,
-// Page: page,
-// Limit: limit,
-// Total: total,
-// Message: message,
-// },
-// Data: data,
-// }
-// return c.Status(fiber.StatusOK).JSON(response)
-// }
-
-// func NonPaginatedResponse(c *fiber.Ctx, data interface{}, total int, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusOK,
-// Total: total,
-// Message: message,
-// },
-// Data: data,
-// }
-// return c.Status(fiber.StatusOK).JSON(response)
-// }
-
-// func ErrorResponse(c *fiber.Ctx, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusNotFound,
-// Message: message,
-// },
-// }
-// return c.Status(fiber.StatusNotFound).JSON(response)
-// }
-
-// func ValidationErrorResponse(c *fiber.Ctx, errors map[string][]string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusBadRequest,
-// Message: "invalid user request",
-// },
-// Data: errors,
-// }
-// return c.Status(fiber.StatusBadRequest).JSON(response)
-// }
-
-// func InternalServerErrorResponse(c *fiber.Ctx, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusInternalServerError,
-// Message: message,
-// },
-// }
-// return c.Status(fiber.StatusInternalServerError).JSON(response)
-// }
-
-// func GenericResponse(c *fiber.Ctx, status int, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: status,
-// Message: message,
-// },
-// }
-// return c.Status(status).JSON(response)
-// }
-
-// func SuccessResponse(c *fiber.Ctx, data interface{}, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusOK,
-// Message: message,
-// },
-// Data: data,
-// }
-// return c.Status(fiber.StatusOK).JSON(response)
-// }
-
-// func CreateResponse(c *fiber.Ctx, data interface{}, message string) error {
-// response := APIResponse{
-// Meta: MetaData{
-// Status: fiber.StatusCreated,
-// Message: message,
-// },
-// Data: data,
-// }
-// return c.Status(fiber.StatusOK).JSON(response)
-// }
diff --git a/utils/role_permission.go b/utils/role_permission.go
deleted file mode 100644
index a805ea9..0000000
--- a/utils/role_permission.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package utils
-
-// RoleID based
-/* const (
- RoleAdministrator = "42bdecce-f2ad-44ae-b3d6-883c1fbddaf7"
- RolePengelola = "0bf86966-7042-410a-a88c-d01f70832348"
- RolePengepul = "d7245535-0e9e-4d35-ab39-baece5c10b3c"
- RoleMasyarakat = "60e5684e4-b214-4bd0-972f-3be80c4649a0"
-) */
-
-// RoleName based
-const (
- RoleAdministrator = "administrator"
- RolePengelola = "pengelola"
- RolePengepul = "pengepul"
- RoleMasyarakat = "masyarakat"
-)
diff --git a/utils/token_management.go b/utils/token_management.go
index 95ca2c8..fa1eb5c 100644
--- a/utils/token_management.go
+++ b/utils/token_management.go
@@ -15,6 +15,13 @@ import (
type TokenType string
+const (
+ RoleAdministrator = "administrator"
+ RolePengelola = "pengelola"
+ RolePengepul = "pengepul"
+ RoleMasyarakat = "masyarakat"
+)
+
const (
TokenTypePartial TokenType = "partial"
TokenTypeFull TokenType = "full"