update: add security for administrator authentication
This commit is contained in:
parent
8f32944c7b
commit
992b75e32b
5
go.mod
5
go.mod
|
@ -13,6 +13,11 @@ require (
|
||||||
gorm.io/gorm v1.25.12
|
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 (
|
// require (
|
||||||
// golang.org/x/term v0.30.0 // indirect
|
// golang.org/x/term v0.30.0 // indirect
|
||||||
// rsc.io/qr v0.2.0 // indirect
|
// rsc.io/qr v0.2.0 // indirect
|
||||||
|
|
4
go.sum
4
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=
|
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 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
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/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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
|
|
@ -256,6 +256,59 @@ type LoginAdminRequest struct {
|
||||||
DeviceID string `json:"device_id"`
|
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) {
|
func (r *LoginorRegistRequest) ValidateLoginorRegistRequest() (map[string][]string, bool) {
|
||||||
errors := make(map[string][]string)
|
errors := make(map[string][]string)
|
||||||
|
|
||||||
|
|
|
@ -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
|
var req RegisterAdminRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return utils.BadRequest(c, "Invalid request format")
|
return utils.BadRequest(c, "Invalid request format")
|
||||||
}
|
}
|
||||||
|
|
||||||
if errs, ok := req.ValidateRegisterAdminRequest(); !ok {
|
// if err := h.validator.Struct(&req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
// return utils.BadRequest(c, "Validation failed: "+err.Error())
|
||||||
"meta": fiber.Map{
|
// }
|
||||||
"status": fiber.StatusBadRequest,
|
|
||||||
"message": "periksa lagi inputan",
|
|
||||||
},
|
|
||||||
"errors": errs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
err := h.service.RegisterAdmin(c.Context(), &req)
|
response, err := h.service.RegisterAdmin(c.Context(), &req)
|
||||||
if err != nil {
|
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 {
|
func (h *AuthenticationHandler) RequestOtpHandler(c *fiber.Ctx) error {
|
||||||
var req LoginorRegistRequest
|
var req LoginorRegistRequest
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
|
|
@ -32,7 +32,17 @@ func AuthenticationRouter(api fiber.Router) {
|
||||||
|
|
||||||
authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus)
|
authRoute.Get("/cekapproval", middleware.AuthMiddleware(), authHandler.GetRegistrationStatus)
|
||||||
authRoute.Post("/login/admin", authHandler.Login)
|
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("/request-otp", authHandler.RequestOtpHandler)
|
||||||
authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler)
|
authRoute.Post("/verif-otp", authHandler.VerifyOtpHandler)
|
||||||
authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler)
|
authRoute.Post("/request-otp/register", authHandler.RequestOtpRegistHandler)
|
||||||
|
|
|
@ -14,8 +14,17 @@ import (
|
||||||
|
|
||||||
type AuthenticationService interface {
|
type AuthenticationService interface {
|
||||||
GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error)
|
GetRegistrationStatus(ctx context.Context, userID, deviceID string) (*AuthResponse, error)
|
||||||
LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*AuthResponse, error)
|
LoginAdmin(ctx context.Context, req *LoginAdminRequest) (*OTPAdminResponse, error)
|
||||||
RegisterAdmin(ctx context.Context, req *RegisterAdminRequest) 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)
|
SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error)
|
||||||
VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
|
VerifyRegistrationOTP(ctx context.Context, req *VerifyOtpRequest) (*AuthResponse, error)
|
||||||
|
@ -29,27 +38,32 @@ type AuthenticationService interface {
|
||||||
type authenticationService struct {
|
type authenticationService struct {
|
||||||
authRepo AuthenticationRepository
|
authRepo AuthenticationRepository
|
||||||
roleRepo role.RoleRepository
|
roleRepo role.RoleRepository
|
||||||
|
emailService *utils.EmailService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService {
|
func NewAuthenticationService(authRepo AuthenticationRepository, roleRepo role.RoleRepository) AuthenticationService {
|
||||||
return &authenticationService{authRepo, roleRepo}
|
return &authenticationService{
|
||||||
}
|
authRepo: authRepo,
|
||||||
|
roleRepo: roleRepo,
|
||||||
func normalizeRoleName(roleName string) string {
|
emailService: utils.NewEmailService(),
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
type GetRegistrationStatusResponse struct {
|
||||||
UserID string `json:"userId"`
|
UserID string `json:"userId"`
|
||||||
RegistrationStatus string `json:"registrationStatus"`
|
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)
|
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)
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
||||||
if err != nil {
|
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" {
|
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" {
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AuthResponse{
|
return &AuthResponse{
|
||||||
Message: "login berhasil",
|
Message: "Login berhasil",
|
||||||
AccessToken: token.AccessToken,
|
AccessToken: token.AccessToken,
|
||||||
RefreshToken: token.RefreshToken,
|
RefreshToken: token.RefreshToken,
|
||||||
RegistrationStatus: user.RegistrationStatus,
|
RegistrationStatus: user.RegistrationStatus,
|
||||||
|
@ -154,21 +229,235 @@ func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminR
|
||||||
}, nil
|
}, 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)
|
user, err := s.authRepo.FindUserByEmail(ctx, req.Email)
|
||||||
if existingUser != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("email already in use")
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to hash password: %w", err)
|
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")
|
role, err := s.roleRepo.FindRoleByName(ctx, "administrator")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("role name not found: %w", err)
|
return nil, fmt.Errorf("role name not found: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
user := &model.User{
|
user := &model.User{
|
||||||
|
@ -180,14 +469,41 @@ func (s *authenticationService) RegisterAdmin(ctx context.Context, req *Register
|
||||||
Placeofbirth: req.PlaceOfBirth,
|
Placeofbirth: req.PlaceOfBirth,
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
RoleID: role.ID,
|
RoleID: role.ID,
|
||||||
RegistrationStatus: "completed",
|
RegistrationStatus: "pending_email_verification",
|
||||||
|
RegistrationProgress: 1,
|
||||||
|
EmailVerified: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.authRepo.CreateUser(ctx, user); err != nil {
|
if err := s.authRepo.CreateUser(ctx, user); err != nil {
|
||||||
return fmt.Errorf("failed to create user: %w", err)
|
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return 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: "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) {
|
func (s *authenticationService) SendRegistrationOTP(ctx context.Context, req *LoginorRegistRequest) (*OTPResponse, error) {
|
||||||
|
@ -471,19 +787,19 @@ func sendOTP(phone, otp string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertUserToResponse(user *model.User) *UserResponse {
|
// func convertUserToResponse(user *model.User) *UserResponse {
|
||||||
return &UserResponse{
|
// return &UserResponse{
|
||||||
ID: user.ID,
|
// ID: user.ID,
|
||||||
Name: user.Name,
|
// Name: user.Name,
|
||||||
Phone: user.Phone,
|
// Phone: user.Phone,
|
||||||
Email: user.Email,
|
// Email: user.Email,
|
||||||
Role: user.Role.RoleName,
|
// Role: user.Role.RoleName,
|
||||||
RegistrationStatus: user.RegistrationStatus,
|
// RegistrationStatus: user.RegistrationStatus,
|
||||||
RegistrationProgress: user.RegistrationProgress,
|
// RegistrationProgress: user.RegistrationProgress,
|
||||||
PhoneVerified: user.PhoneVerified,
|
// PhoneVerified: user.PhoneVerified,
|
||||||
Avatar: user.Avatar,
|
// Avatar: user.Avatar,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
func IsRegistrationComplete(role string, progress int) bool {
|
func IsRegistrationComplete(role string, progress int) bool {
|
||||||
switch role {
|
switch role {
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ package middleware
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"rijig/utils"
|
"rijig/utils"
|
||||||
"time"
|
"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 {
|
func AuthMiddleware(config ...AuthConfig) fiber.Handler {
|
||||||
cfg := AuthConfig{}
|
cfg := AuthConfig{}
|
||||||
if len(config) > 0 {
|
if len(config) > 0 {
|
||||||
|
|
|
@ -11,6 +11,7 @@ type User struct {
|
||||||
Placeofbirth string `gorm:"not null" json:"placeofbirth"`
|
Placeofbirth string `gorm:"not null" json:"placeofbirth"`
|
||||||
Phone string `gorm:"not null;index" json:"phone"`
|
Phone string `gorm:"not null;index" json:"phone"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
EmailVerified bool `gorm:"default:false" json:"emailVerified"`
|
||||||
PhoneVerified bool `gorm:"default:false" json:"phoneVerified"`
|
PhoneVerified bool `gorm:"default:false" json:"phoneVerified"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
RoleID string `gorm:"not null" json:"roleId"`
|
RoleID string `gorm:"not null" json:"roleId"`
|
||||||
|
|
|
@ -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(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
.container { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
|
||||||
|
.header { background-color: #2E7D32; color: white; padding: 20px; text-align: center; }
|
||||||
|
.content { padding: 30px; background-color: #f9f9f9; }
|
||||||
|
.otp-code { font-size: 32px; font-weight: bold; color: #2E7D32; text-align: center; letter-spacing: 5px; margin: 20px 0; padding: 15px; background-color: white; border: 2px dashed #2E7D32; }
|
||||||
|
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
||||||
|
.warning { color: #d32f2f; font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔐 Kode Verifikasi Login</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Halo <strong>%s</strong>,</p>
|
||||||
|
<p>Anda telah meminta untuk login sebagai Administrator. Gunakan kode verifikasi berikut:</p>
|
||||||
|
|
||||||
|
<div class="otp-code">%s</div>
|
||||||
|
|
||||||
|
<p><strong>Penting:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Kode ini berlaku selama <strong>5 menit</strong></li>
|
||||||
|
<li>Jangan berikan kode ini kepada siapapun</li>
|
||||||
|
<li>Maksimal 3 kali percobaan</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="warning">⚠️ Jika Anda tidak melakukan permintaan login ini, abaikan email ini.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Email ini dikirim otomatis oleh sistem Rijig Waste Management<br>
|
||||||
|
Jangan balas email ini.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, 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
|
||||||
|
}
|
|
@ -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(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
.container { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
|
||||||
|
.header { background-color: #2E7D32; color: white; padding: 20px; text-align: center; }
|
||||||
|
.content { padding: 30px; background-color: #f9f9f9; }
|
||||||
|
.verify-button { display: inline-block; background-color: #2E7D32; color: white; padding: 15px 30px; text-decoration: none; border-radius: 5px; font-weight: bold; margin: 20px 0; }
|
||||||
|
.verify-button:hover { background-color: #1B5E20; }
|
||||||
|
.token-box { font-size: 14px; color: #666; background-color: white; padding: 15px; border-left: 4px solid #2E7D32; margin: 20px 0; word-break: break-all; }
|
||||||
|
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
||||||
|
.success-icon { font-size: 48px; text-align: center; margin: 20px 0; }
|
||||||
|
.info-box { background-color: #E8F5E8; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>✅ Verifikasi Email</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="success-icon">🎉</div>
|
||||||
|
|
||||||
|
<p>Selamat <strong>%s</strong>!</p>
|
||||||
|
<p>Akun Administrator Anda telah berhasil dibuat. Untuk mengaktifkan akun dan mulai menggunakan sistem Rijig, silakan verifikasi email Anda dengan mengklik tombol di bawah ini:</p>
|
||||||
|
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="%s" class="verify-button">Verifikasi Email Saya</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Atau copy paste link berikut ke browser Anda:</p>
|
||||||
|
<div class="token-box">%s</div>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<p><strong>Informasi Penting:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Link verifikasi berlaku selama <strong>24 jam</strong></li>
|
||||||
|
<li>Setelah verifikasi, Anda dapat login ke sistem</li>
|
||||||
|
<li>Link hanya dapat digunakan sekali</li>
|
||||||
|
<li>Jangan bagikan link ini kepada siapapun</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><strong>Langkah selanjutnya setelah verifikasi:</strong></p>
|
||||||
|
<ol>
|
||||||
|
<li>Login menggunakan email dan password</li>
|
||||||
|
<li>Masukkan kode OTP yang dikirim ke email</li>
|
||||||
|
<li>Mulai menggunakan sistem Rijig</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<p style="color: #666; font-style: italic;">Jika Anda tidak membuat akun ini, abaikan email ini.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Email ini dikirim otomatis oleh sistem Rijig Waste Management<br>
|
||||||
|
Jangan balas email ini.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, 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
|
||||||
|
}
|
|
@ -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(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
.container { max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif; }
|
||||||
|
.header { background-color: #d32f2f; color: white; padding: 20px; text-align: center; }
|
||||||
|
.content { padding: 30px; background-color: #f9f9f9; }
|
||||||
|
.reset-button { display: inline-block; background-color: #d32f2f; color: white; padding: 15px 30px; text-decoration: none; border-radius: 5px; font-weight: bold; margin: 20px 0; }
|
||||||
|
.reset-button:hover { background-color: #b71c1c; }
|
||||||
|
.token-box { font-size: 14px; color: #666; background-color: white; padding: 15px; border-left: 4px solid #d32f2f; margin: 20px 0; word-break: break-all; }
|
||||||
|
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
||||||
|
.warning { color: #d32f2f; font-weight: bold; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🔐 Reset Password</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Halo <strong>%s</strong>,</p>
|
||||||
|
<p>Kami menerima permintaan untuk reset password akun Administrator Anda.</p>
|
||||||
|
|
||||||
|
<p>Klik tombol di bawah ini untuk reset password:</p>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="%s" class="reset-button">Reset Password</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Atau copy paste link berikut ke browser Anda:</p>
|
||||||
|
<div class="token-box">%s</div>
|
||||||
|
|
||||||
|
<p><strong>Penting:</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Link ini berlaku selama <strong>30 menit</strong></li>
|
||||||
|
<li>Link hanya dapat digunakan sekali</li>
|
||||||
|
<li>Jangan bagikan link ini kepada siapapun</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p class="warning">⚠️ Jika Anda tidak melakukan permintaan reset password, abaikan email ini dan password Anda tidak akan berubah.</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Email ini dikirim otomatis oleh sistem Rijig Waste Management<br>
|
||||||
|
Jangan balas email ini.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`, 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
|
||||||
|
}
|
|
@ -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)
|
|
||||||
// }
|
|
|
@ -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"
|
|
||||||
)
|
|
|
@ -15,6 +15,13 @@ import (
|
||||||
|
|
||||||
type TokenType string
|
type TokenType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RoleAdministrator = "administrator"
|
||||||
|
RolePengelola = "pengelola"
|
||||||
|
RolePengepul = "pengepul"
|
||||||
|
RoleMasyarakat = "masyarakat"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TokenTypePartial TokenType = "partial"
|
TokenTypePartial TokenType = "partial"
|
||||||
TokenTypeFull TokenType = "full"
|
TokenTypeFull TokenType = "full"
|
||||||
|
|
Loading…
Reference in New Issue