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:
@@ -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": {
|
"/api/v1/profile/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.",
|
"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": {
|
"/api/v1/tender-approvals": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"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": {
|
"customer.UpdateCustomerForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"feedback.CompanyFeedbackStatsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -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": {
|
"/api/v1/profile/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Authenticate customer with username (email) and password. Returns access token, refresh token, and customer information upon successful authentication.",
|
"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": {
|
"/api/v1/tender-approvals": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"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": {
|
"customer.UpdateCustomerForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"feedback.CompanyFeedbackStatsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -400,6 +400,39 @@ definitions:
|
|||||||
refresh_token:
|
refresh_token:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
customer.UpdateCustomerForm:
|
||||||
properties:
|
properties:
|
||||||
company_ids:
|
company_ids:
|
||||||
@@ -434,6 +467,27 @@ definitions:
|
|||||||
example: active
|
example: active
|
||||||
type: string
|
type: string
|
||||||
type: object
|
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:
|
feedback.CompanyFeedbackStatsResponse:
|
||||||
properties:
|
properties:
|
||||||
company_id:
|
company_id:
|
||||||
@@ -4075,6 +4129,45 @@ paths:
|
|||||||
summary: Get customer profile
|
summary: Get customer profile
|
||||||
tags:
|
tags:
|
||||||
- Authorization
|
- 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:
|
/api/v1/profile/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -4188,6 +4281,92 @@ paths:
|
|||||||
summary: Refresh customer access token
|
summary: Refresh customer access token
|
||||||
tags:
|
tags:
|
||||||
- Authorization
|
- 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:
|
/api/v1/tender-approvals:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
+1
-1
@@ -146,7 +146,7 @@ func main() {
|
|||||||
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator)
|
userService := user.NewUserService(userRepository, logger, userAuthService, userValidator)
|
||||||
categoryService := company_category.NewCategoryService(categoryRepository, logger)
|
categoryService := company_category.NewCategoryService(categoryRepository, logger)
|
||||||
companyService := company.NewCompanyService(companyRepository, categoryService, 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)
|
tenderService := tender.NewTenderService(tenderRepository, companyService, logger)
|
||||||
feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger)
|
feedbackService := feedback.NewFeedbackService(feedbackRepo, tenderService, logger)
|
||||||
tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger)
|
tenderApprovalService := tender_approval.NewTenderApprovalService(tenderApprovalRepository, tenderService, logger)
|
||||||
|
|||||||
@@ -137,6 +137,11 @@ func RegisterPublicRoutes(e *echo.Echo, customerHandler *customer.Handler, tende
|
|||||||
customerGP.POST("/login", customerHandler.Login)
|
customerGP.POST("/login", customerHandler.Login)
|
||||||
customerGP.POST("/refresh-token", customerHandler.RefreshToken)
|
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 := customerGP.Group("")
|
||||||
profileGP.Use(customerHandler.AuthMiddleware())
|
profileGP.Use(customerHandler.AuthMiddleware())
|
||||||
profileGP.GET("", customerHandler.GetProfile)
|
profileGP.GET("", customerHandler.GetProfile)
|
||||||
|
|||||||
@@ -70,3 +70,19 @@ func (c *Customer) GetCreatedAt() int64 {
|
|||||||
func (c *Customer) GetUpdatedAt() int64 {
|
func (c *Customer) GetUpdatedAt() int64 {
|
||||||
return c.UpdatedAt
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -120,3 +120,39 @@ type AssignCompaniesForm struct {
|
|||||||
type RemoveCompaniesForm struct {
|
type RemoveCompaniesForm struct {
|
||||||
CompanyIDs []string `json:"company_ids" valid:"required"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -364,3 +364,83 @@ func (h *Handler) Logout(c echo.Context) error {
|
|||||||
"message": "Logout successful",
|
"message": "Logout successful",
|
||||||
}, "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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,16 @@ package customer
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"tm/internal/company"
|
"tm/internal/company"
|
||||||
"tm/pkg/logger"
|
"tm/pkg/logger"
|
||||||
|
"tm/pkg/redis"
|
||||||
"tm/pkg/response"
|
"tm/pkg/response"
|
||||||
|
|
||||||
"tm/pkg/authorization"
|
"tm/pkg/authorization"
|
||||||
@@ -30,6 +35,11 @@ type Service interface {
|
|||||||
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
RefreshToken(ctx context.Context, refreshToken string) (*AuthResponse, error)
|
||||||
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
|
GetProfileWithCompanies(ctx context.Context, id string) (*CustomerResponse, error)
|
||||||
Logout(ctx context.Context, id string, accessToken string) 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
|
// customerService implements the Service interface
|
||||||
@@ -38,15 +48,17 @@ type customerService struct {
|
|||||||
logger logger.Logger
|
logger logger.Logger
|
||||||
authService authorization.AuthorizationService
|
authService authorization.AuthorizationService
|
||||||
companyService company.Service
|
companyService company.Service
|
||||||
|
redisClient redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCustomerService creates a new customer service
|
// New creates a new customer service
|
||||||
func NewCustomerService(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service) Service {
|
func New(repository Repository, logger logger.Logger, authService authorization.AuthorizationService, companyService company.Service, redisClient redis.Client) Service {
|
||||||
return &customerService{
|
return &customerService{
|
||||||
repository: repository,
|
repository: repository,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
authService: authService,
|
authService: authService,
|
||||||
companyService: companyService,
|
companyService: companyService,
|
||||||
|
redisClient: redisClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,6 +700,324 @@ func (s *customerService) Logout(ctx context.Context, customerID string, accessT
|
|||||||
return nil
|
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
|
// loadCompaniesForCustomer loads company summaries for a customer
|
||||||
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
|
func (s *customerService) loadCompaniesForCustomer(ctx context.Context, customer *Customer) ([]*CompanySummary, error) {
|
||||||
if len(customer.CompanyIDs) == 0 {
|
if len(customer.CompanyIDs) == 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user