Add Password Reset Functionality and Update API Documentation

- Implemented password reset functionality, including endpoints for requesting a reset code, verifying the OTP, and resetting the password.
- Introduced new request and response forms for password reset operations, ensuring proper validation and structured responses.
- Enhanced the customer service layer to handle password reset requests, OTP verification, and password updates, utilizing Redis for temporary token storage.
- Updated Swagger and YAML documentation to include new API endpoints and their specifications, improving clarity for API consumers.
- Refactored the customer handler to manage new password reset routes, ensuring adherence to clean architecture principles.
This commit is contained in:
n.nakhostin
2025-09-14 13:25:36 +03:30
parent 5d721705b7
commit c701053609
9 changed files with 1213 additions and 3 deletions
+282
View File
@@ -4880,6 +4880,70 @@ const docTemplate = `{
}
}
},
"/api/v1/profile/forgot-password": {
"post": {
"description": "Send a password reset OTP code to the customer's email address",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Authorization"
],
"summary": "Request password reset",
"parameters": [
{
"description": "Email address for password reset",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/customer.RequestResetPasswordForm"
}
}
],
"responses": {
"200": {
"description": "Password reset code sent successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/customer.RequestResetPasswordResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/profile/login": {
"post": {
"description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.",
@@ -5060,6 +5124,146 @@ const docTemplate = `{
}
}
},
"/api/v1/profile/reset-password": {
"post": {
"description": "Reset the customer's password using the reset token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Authorization"
],
"summary": "Reset password",
"parameters": [
{
"description": "Email, reset token, and new password",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/customer.ResetPasswordForm"
}
}
],
"responses": {
"200": {
"description": "Password reset successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/customer.ResetPasswordResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized - Invalid or expired reset token",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/profile/verify-otp": {
"post": {
"description": "Verify the OTP code received via email and get a reset token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Authorization"
],
"summary": "Verify OTP code",
"parameters": [
{
"description": "Email and OTP code for verification",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/customer.VerifyOTPForm"
}
}
],
"responses": {
"200": {
"description": "OTP verified successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/customer.VerifyOTPResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized - Invalid or expired OTP",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/tender-approvals": {
"get": {
"security": [
@@ -6438,6 +6642,54 @@ const docTemplate = `{
}
}
},
"customer.RequestResetPasswordForm": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "app@opplens.com"
}
}
},
"customer.RequestResetPasswordResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Password reset code sent to your email"
},
"success": {
"type": "boolean",
"example": true
}
}
},
"customer.ResetPasswordForm": {
"type": "object",
"properties": {
"new_password": {
"type": "string",
"example": "NewPass!123"
},
"token": {
"type": "string",
"example": "reset_token_here"
}
}
},
"customer.ResetPasswordResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Password reset successfully"
},
"success": {
"type": "boolean",
"example": true
}
}
},
"customer.UpdateCustomerForm": {
"type": "object",
"properties": {
@@ -6486,6 +6738,36 @@ const docTemplate = `{
}
}
},
"customer.VerifyOTPForm": {
"type": "object",
"properties": {
"code": {
"type": "string",
"example": "123456"
},
"email": {
"type": "string",
"example": "app@opplens.com"
}
}
},
"customer.VerifyOTPResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "OTP verified successfully"
},
"success": {
"type": "boolean",
"example": true
},
"token": {
"type": "string",
"example": "reset_token_here"
}
}
},
"feedback.CompanyFeedbackStatsResponse": {
"type": "object",
"properties": {
+282
View File
@@ -4874,6 +4874,70 @@
}
}
},
"/api/v1/profile/forgot-password": {
"post": {
"description": "Send a password reset OTP code to the customer's email address",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Authorization"
],
"summary": "Request password reset",
"parameters": [
{
"description": "Email address for password reset",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/customer.RequestResetPasswordForm"
}
}
],
"responses": {
"200": {
"description": "Password reset code sent successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/customer.RequestResetPasswordResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/profile/login": {
"post": {
"description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.",
@@ -5054,6 +5118,146 @@
}
}
},
"/api/v1/profile/reset-password": {
"post": {
"description": "Reset the customer's password using the reset token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Authorization"
],
"summary": "Reset password",
"parameters": [
{
"description": "Email, reset token, and new password",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/customer.ResetPasswordForm"
}
}
],
"responses": {
"200": {
"description": "Password reset successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/customer.ResetPasswordResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized - Invalid or expired reset token",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/profile/verify-otp": {
"post": {
"description": "Verify the OTP code received via email and get a reset token",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Authorization"
],
"summary": "Verify OTP code",
"parameters": [
{
"description": "Email and OTP code for verification",
"name": "request",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/customer.VerifyOTPForm"
}
}
],
"responses": {
"200": {
"description": "OTP verified successfully",
"schema": {
"allOf": [
{
"$ref": "#/definitions/response.APIResponse"
},
{
"type": "object",
"properties": {
"data": {
"$ref": "#/definitions/customer.VerifyOTPResponse"
}
}
}
]
}
},
"400": {
"description": "Bad request - Invalid input data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"401": {
"description": "Unauthorized - Invalid or expired OTP",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"422": {
"description": "Validation error - Invalid request data",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal server error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/tender-approvals": {
"get": {
"security": [
@@ -6432,6 +6636,54 @@
}
}
},
"customer.RequestResetPasswordForm": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "app@opplens.com"
}
}
},
"customer.RequestResetPasswordResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Password reset code sent to your email"
},
"success": {
"type": "boolean",
"example": true
}
}
},
"customer.ResetPasswordForm": {
"type": "object",
"properties": {
"new_password": {
"type": "string",
"example": "NewPass!123"
},
"token": {
"type": "string",
"example": "reset_token_here"
}
}
},
"customer.ResetPasswordResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "Password reset successfully"
},
"success": {
"type": "boolean",
"example": true
}
}
},
"customer.UpdateCustomerForm": {
"type": "object",
"properties": {
@@ -6480,6 +6732,36 @@
}
}
},
"customer.VerifyOTPForm": {
"type": "object",
"properties": {
"code": {
"type": "string",
"example": "123456"
},
"email": {
"type": "string",
"example": "app@opplens.com"
}
}
},
"customer.VerifyOTPResponse": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "OTP verified successfully"
},
"success": {
"type": "boolean",
"example": true
},
"token": {
"type": "string",
"example": "reset_token_here"
}
}
},
"feedback.CompanyFeedbackStatsResponse": {
"type": "object",
"properties": {
+179
View File
@@ -400,6 +400,39 @@ definitions:
refresh_token:
type: string
type: object
customer.RequestResetPasswordForm:
properties:
email:
example: app@opplens.com
type: string
type: object
customer.RequestResetPasswordResponse:
properties:
message:
example: Password reset code sent to your email
type: string
success:
example: true
type: boolean
type: object
customer.ResetPasswordForm:
properties:
new_password:
example: NewPass!123
type: string
token:
example: reset_token_here
type: string
type: object
customer.ResetPasswordResponse:
properties:
message:
example: Password reset successfully
type: string
success:
example: true
type: boolean
type: object
customer.UpdateCustomerForm:
properties:
company_ids:
@@ -434,6 +467,27 @@ definitions:
example: active
type: string
type: object
customer.VerifyOTPForm:
properties:
code:
example: "123456"
type: string
email:
example: app@opplens.com
type: string
type: object
customer.VerifyOTPResponse:
properties:
message:
example: OTP verified successfully
type: string
success:
example: true
type: boolean
token:
example: reset_token_here
type: string
type: object
feedback.CompanyFeedbackStatsResponse:
properties:
company_id:
@@ -4075,6 +4129,45 @@ paths:
summary: Get customer profile
tags:
- Authorization
/api/v1/profile/forgot-password:
post:
consumes:
- application/json
description: Send a password reset OTP code to the customer's email address
parameters:
- description: Email address for password reset
in: body
name: request
required: true
schema:
$ref: '#/definitions/customer.RequestResetPasswordForm'
produces:
- application/json
responses:
"200":
description: Password reset code sent successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/customer.RequestResetPasswordResponse'
type: object
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Request password reset
tags:
- Authorization
/api/v1/profile/login:
post:
consumes:
@@ -4188,6 +4281,92 @@ paths:
summary: Refresh customer access token
tags:
- Authorization
/api/v1/profile/reset-password:
post:
consumes:
- application/json
description: Reset the customer's password using the reset token
parameters:
- description: Email, reset token, and new password
in: body
name: request
required: true
schema:
$ref: '#/definitions/customer.ResetPasswordForm'
produces:
- application/json
responses:
"200":
description: Password reset successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/customer.ResetPasswordResponse'
type: object
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized - Invalid or expired reset token
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Reset password
tags:
- Authorization
/api/v1/profile/verify-otp:
post:
consumes:
- application/json
description: Verify the OTP code received via email and get a reset token
parameters:
- description: Email and OTP code for verification
in: body
name: request
required: true
schema:
$ref: '#/definitions/customer.VerifyOTPForm'
produces:
- application/json
responses:
"200":
description: OTP verified successfully
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/customer.VerifyOTPResponse'
type: object
"400":
description: Bad request - Invalid input data
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized - Invalid or expired OTP
schema:
$ref: '#/definitions/response.APIResponse'
"422":
description: Validation error - Invalid request data
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal server error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Verify OTP code
tags:
- Authorization
/api/v1/tender-approvals:
get:
consumes:
+1 -1
View File
@@ -146,7 +146,7 @@ func main() {
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator)
categoryService := company_category.NewCategoryService(categoryRepository, logger)
companyService := company.NewCompanyService(companyRepository, categoryService, logger)
customerService := customer.NewCustomerService(customerRepository, logger, customerAuthService, companyService)
customerService := customer.New(customerRepository, logger, customerAuthService, companyService, redisClient)
tenderService := tender.NewTenderService(tenderRepository, companyService, logger)
feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger)
tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger)
+5
View File
@@ -137,6 +137,11 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
customerGP.POST("/login", customerHandler.Login)
customerGP.POST("/refresh-token", customerHandler.RefreshToken)
// Forgot password routes
customerGP.POST("/forgot-password", customerHandler.RequestResetPassword)
customerGP.POST("/verify-otp", customerHandler.VerifyOTP)
customerGP.POST("/reset-password", customerHandler.ResetPassword)
profileGP := customerGP.Group("")
profileGP.Use(customerHandler.AuthMiddleware())
profileGP.GET("", customerHandler.GetProfile)
+16
View File
@@ -70,3 +70,19 @@ func (c *Customer) GetCreatedAt() int64 {
func (c *Customer) GetUpdatedAt() int64 {
return c.UpdatedAt
}
// OTP represents a one-time password for password reset
type OTP struct {
Code string `bson:"code" json:"code"`
Email string `bson:"email" json:"email"`
ExpiresAt int64 `bson:"expires_at" json:"expires_at"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
// ResetToken represents a temporary reset token for password reset
type ResetToken struct {
Token string `bson:"token" json:"token"`
Email string `bson:"email" json:"email"`
ExpiresAt int64 `bson:"expires_at" json:"expires_at"`
CreatedAt int64 `bson:"created_at" json:"created_at"`
}
+36
View File
@@ -120,3 +120,39 @@ type AssignCompaniesForm struct {
type RemoveCompaniesForm struct {
CompanyIDs []string `json:"company_ids" valid:"required"`
}
// RequestResetPasswordForm represents the form for requesting password reset
type RequestResetPasswordForm struct {
Email string `json:"email" valid:"required,email" example:"app@opplens.com"`
}
// VerifyOTPForm represents the form for verifying OTP code
type VerifyOTPForm struct {
Email string `json:"email" valid:"required,email" example:"app@opplens.com"`
Code string `json:"code" valid:"required,length(6|6)" example:"123456"`
}
// ResetPasswordForm represents the form for resetting password
type ResetPasswordForm struct {
Token string `json:"token" valid:"required" example:"reset_token_here"`
NewPassword string `json:"new_password" valid:"required,length(5|128)" example:"NewPass!123"`
}
// RequestResetPasswordResponse represents the response for password reset request
type RequestResetPasswordResponse struct {
Message string `json:"message" example:"Password reset code sent to your email"`
Success bool `json:"success" example:"true"`
}
// VerifyOTPResponse represents the response for OTP verification
type VerifyOTPResponse struct {
Message string `json:"message" example:"OTP verified successfully"`
Token string `json:"token" example:"reset_token_here"`
Success bool `json:"success" example:"true"`
}
// ResetPasswordResponse represents the response for password reset
type ResetPasswordResponse struct {
Message string `json:"message" example:"Password reset successfully"`
Success bool `json:"success" example:"true"`
}
+80
View File
@@ -364,3 +364,83 @@ func (h *Handler) Logout(c echo.Context) error {
"message": "Logout successful",
}, "Logout successful")
}
// RequestResetPassword handles password reset request
// @Summary Request password reset
// @Description Send a password reset OTP code to the customer's email address
// @Tags Authorization
// @Accept json
// @Produce json
// @Param request body RequestResetPasswordForm true "Email address for password reset"
// @Success 200 {object} response.APIResponse{data=RequestResetPasswordResponse} "Password reset code sent successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/forgot-password [post]
func (h *Handler) RequestResetPassword(c echo.Context) error {
form, err := response.Parse[RequestResetPasswordForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.RequestResetPassword(c.Request().Context(), form)
if err != nil {
return response.InternalServerError(c, "Failed to process password reset request")
}
return response.Success(c, result, "Password reset request processed")
}
// VerifyOTP handles OTP verification for password reset
// @Summary Verify OTP code
// @Description Verify the OTP code received via email and get a reset token
// @Tags Authorization
// @Accept json
// @Produce json
// @Param request body VerifyOTPForm true "Email and OTP code for verification"
// @Success 200 {object} response.APIResponse{data=VerifyOTPResponse} "OTP verified successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired OTP"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/verify-otp [post]
func (h *Handler) VerifyOTP(c echo.Context) error {
form, err := response.Parse[VerifyOTPForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.VerifyOTP(c.Request().Context(), form)
if err != nil {
return response.Unauthorized(c, "Invalid or expired verification code")
}
return response.Success(c, result, "OTP verified successfully")
}
// ResetPassword handles password reset with token
// @Summary Reset password
// @Description Reset the customer's password using the reset token
// @Tags Authorization
// @Accept json
// @Produce json
// @Param request body ResetPasswordForm true "Email, reset token, and new password"
// @Success 200 {object} response.APIResponse{data=ResetPasswordResponse} "Password reset successfully"
// @Failure 400 {object} response.APIResponse "Bad request - Invalid input data"
// @Failure 422 {object} response.APIResponse "Validation error - Invalid request data"
// @Failure 401 {object} response.APIResponse "Unauthorized - Invalid or expired reset token"
// @Failure 500 {object} response.APIResponse "Internal server error"
// @Router /api/v1/profile/reset-password [post]
func (h *Handler) ResetPassword(c echo.Context) error {
form, err := response.Parse[ResetPasswordForm](c)
if err != nil {
return response.ValidationError(c, "Invalid request data", err.Error())
}
result, err := h.service.ResetPassword(c.Request().Context(), form)
if err != nil {
return response.Unauthorized(c, err.Error())
}
return response.Success(c, result, "Password reset successfully")
}
+332 -2
View File
@@ -2,11 +2,16 @@ package customer
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"math/big"
"time"
"tm/internal/company"
"tm/pkg/logger"
"tm/pkg/redis"
"tm/pkg/response"
"tm/pkg/authorization"
@@ -30,6 +35,11 @@ type Service interface {
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
Logout(ctx context.Context, id string, accessToken string) error
// Forgot password operations
RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error)
VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error)
ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error)
}
// customerService implements the Service interface
@@ -38,15 +48,17 @@ type customerService struct {
logger logger.Logger
authService authorization.AuthorizationService
companyService company.Service
redisClient redis.Client
}
// NewCustomerService creates a new customer service
func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service) Service {
// New creates a new customer service
func New(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client) Service {
return &customerService{
repository: repository,
logger: logger,
authService: authService,
companyService: companyService,
redisClient: redisClient,
}
}
@@ -688,6 +700,324 @@ func (s *customerService) Logout(ctx context.Context, customerID string, accessT
return nil
}
// RequestResetPassword handles password reset request
func (s *customerService) RequestResetPassword(ctx context.Context, form *RequestResetPasswordForm) (*RequestResetPasswordResponse, error) {
s.logger.Info("Password reset request", map[string]interface{}{
"email": form.Email,
})
// Check if customer exists
customer, err := s.repository.GetByEmail(ctx, form.Email)
if err != nil {
s.logger.Warn("Password reset request for non-existent email", map[string]interface{}{
"email": form.Email,
})
// Return success even if email doesn't exist for security
return &RequestResetPasswordResponse{
Message: "If the email exists, a password reset code has been sent",
Success: true,
}, nil
}
// Check if customer is active
if customer.Status != CustomerStatusActive {
s.logger.Warn("Password reset request for inactive customer", map[string]interface{}{
"email": form.Email,
"customer_id": customer.ID,
"status": string(customer.Status),
})
return &RequestResetPasswordResponse{
Message: "If the email exists, a password reset code has been sent",
Success: true,
}, nil
}
// Generate 6-digit OTP
otpCode, err := s.generateOTP()
if err != nil {
s.logger.Error("Failed to generate OTP", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to generate verification code")
}
// Store OTP in Redis with 10 minutes expiry
otpKey := fmt.Sprintf("otp:%s", form.Email)
otpData := OTP{
Code: otpCode,
Email: form.Email,
ExpiresAt: time.Now().Add(10 * time.Minute).Unix(),
CreatedAt: time.Now().Unix(),
}
otpDataJSON, err := json.Marshal(otpData)
if err != nil {
s.logger.Error("Failed to marshal OTP data", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process reset request")
}
err = s.redisClient.Set(ctx, otpKey, string(otpDataJSON), 10*time.Minute)
if err != nil {
s.logger.Error("Failed to store OTP in Redis", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process reset request")
}
// Send OTP via email
// err = s.emailService.SendPasswordResetOTP(ctx, form.Email, otpCode)
// if err != nil {
// s.logger.Error("Failed to send password reset OTP", map[string]interface{}{
// "error": err.Error(),
// "email": form.Email,
// })
// return nil, errors.New("failed to send verification code")
// }
s.logger.Info("Password reset OTP sent successfully", map[string]interface{}{
"email": form.Email,
"customer_id": customer.ID,
})
return &RequestResetPasswordResponse{
Message: "Password reset code sent to your email",
Success: true,
}, nil
}
// VerifyOTP handles OTP verification
func (s *customerService) VerifyOTP(ctx context.Context, form *VerifyOTPForm) (*VerifyOTPResponse, error) {
s.logger.Info("OTP verification attempt", map[string]interface{}{
"email": form.Email,
})
// Get OTP from Redis
otpKey := fmt.Sprintf("otp:%s", form.Email)
otpDataStr, err := s.redisClient.Get(ctx, otpKey)
if err != nil {
s.logger.Warn("OTP verification failed - OTP not found or expired", map[string]interface{}{
"email": form.Email,
})
return nil, errors.New("invalid or expired verification code")
}
var otpData OTP
err = json.Unmarshal([]byte(otpDataStr), &otpData)
if err != nil {
s.logger.Error("Failed to unmarshal OTP data", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("invalid verification code format")
}
if otpData.Code != form.Code {
s.logger.Warn("OTP verification failed - wrong code", map[string]interface{}{
"email": form.Email,
})
return nil, errors.New("invalid verification code")
}
// Generate reset token
resetToken, err := s.generateResetToken()
if err != nil {
s.logger.Error("Failed to generate reset token", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to generate reset token")
}
// Store reset token in Redis with 30 minutes expiry
tokenKey := fmt.Sprintf("reset_token:%s", resetToken)
tokenData := ResetToken{
Token: resetToken,
Email: form.Email,
ExpiresAt: time.Now().Add(30 * time.Minute).Unix(),
CreatedAt: time.Now().Unix(),
}
tokenDataJSON, err := json.Marshal(tokenData)
if err != nil {
s.logger.Error("Failed to marshal reset token data", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process verification")
}
err = s.redisClient.Set(ctx, tokenKey, string(tokenDataJSON), 30*time.Minute)
if err != nil {
s.logger.Error("Failed to store reset token in Redis", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
return nil, errors.New("failed to process verification")
}
// Delete OTP from Redis after successful verification
err = s.redisClient.Del(ctx, otpKey)
if err != nil {
s.logger.Warn("Failed to delete OTP after verification", map[string]interface{}{
"error": err.Error(),
"email": form.Email,
})
}
s.logger.Info("OTP verified successfully", map[string]interface{}{
"email": form.Email,
})
return &VerifyOTPResponse{
Message: "OTP verified successfully",
Token: resetToken,
Success: true,
}, nil
}
// ResetPassword handles password reset
func (s *customerService) ResetPassword(ctx context.Context, form *ResetPasswordForm) (*ResetPasswordResponse, error) {
s.logger.Info("Password reset attempt", map[string]interface{}{
"token": form.Token,
})
// Validate password strength
if !s.isValidPassword(form.NewPassword) {
return nil, errors.New("password must be at least 5 characters with uppercase, lowercase, and special character")
}
// Get reset token from Redis
tokenKey := fmt.Sprintf("reset_token:%s", form.Token)
tokenDataStr, err := s.redisClient.Get(ctx, tokenKey)
if err != nil {
s.logger.Warn("Password reset failed - invalid or expired token", map[string]interface{}{
"token": form.Token,
})
return nil, errors.New("invalid or expired reset token")
}
var tokenData ResetToken
err = json.Unmarshal([]byte(tokenDataStr), &tokenData)
if err != nil {
s.logger.Error("Failed to unmarshal reset token data", map[string]interface{}{
"error": err.Error(),
"token": form.Token,
})
return nil, errors.New("invalid reset token format")
}
if tokenData.Token != form.Token {
s.logger.Warn("Password reset failed - wrong token", map[string]interface{}{
"token": form.Token,
})
return nil, errors.New("invalid reset token")
}
// Get customer
customer, err := s.repository.GetByEmail(ctx, tokenData.Email)
if err != nil {
s.logger.Error("Password reset failed - customer not found", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
})
return nil, errors.New("customer not found")
}
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.NewPassword), bcrypt.DefaultCost)
if err != nil {
s.logger.Error("Failed to hash new password", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
})
return nil, errors.New("failed to process new password")
}
// Update customer password
customer.Password = string(hashedPassword)
customer.UpdatedAt = time.Now().Unix()
err = s.repository.Update(ctx, customer)
if err != nil {
s.logger.Error("Failed to update customer password", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
"customer_id": customer.ID,
})
return nil, errors.New("failed to reset password")
}
// Delete reset token from Redis
err = s.redisClient.Del(ctx, tokenKey)
if err != nil {
s.logger.Warn("Failed to delete reset token after password reset", map[string]interface{}{
"error": err.Error(),
"email": tokenData.Email,
})
}
s.logger.Info("Password reset successfully", map[string]interface{}{
"email": tokenData.Email,
"customer_id": customer.ID,
})
return &ResetPasswordResponse{
Message: "Password reset successfully",
Success: true,
}, nil
}
// generateOTP generates a 6-digit OTP
func (s *customerService) generateOTP() (string, error) {
// Generate 6 random digits
otp := ""
for i := 0; i < 6; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return "", err
}
otp += num.String()
}
return otp, nil
}
// generateResetToken generates a random reset token
func (s *customerService) generateResetToken() (string, error) {
// Generate 32 random bytes
tokenBytes := make([]byte, 32)
_, err := rand.Read(tokenBytes)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", tokenBytes), nil
}
// isValidPassword validates password strength
func (s *customerService) isValidPassword(password string) bool {
if len(password) < 5 {
return false
}
hasUpper := false
hasLower := false
hasSpecial := false
for _, char := range password {
if char >= 'A' && char <= 'Z' {
hasUpper = true
} else if char >= 'a' && char <= 'z' {
hasLower = true
} else if char == '!' || char == '@' || char == '#' || char == '$' || char == '%' || char == '^' || char == '&' || char == '*' {
hasSpecial = true
}
}
return hasUpper && hasLower && hasSpecial
}
// loadCompaniesForCustomer loads company summaries for a customer
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
if len(customer.CompanyIDs) == 0 {