Enhance cursor rules and add user management functionality

- Updated cursor rules to emphasize the importance of dependency injection and logging practices.
- Introduced a new user management domain, including entities, services, handlers, and validation.
- Implemented user authentication features such as login, registration, and role-based access control.
- Added Redis integration for token management and caching.
- Enhanced API documentation with Swagger for user-related endpoints.
- Updated configuration to support new JWT settings and Redis connection parameters.
This commit is contained in:
n.nakhostin
2025-08-10 14:09:17 +03:30
parent 9119e2383e
commit 98f581ec97
28 changed files with 7782 additions and 131 deletions
BIN
View File
Binary file not shown.
+52
View File
@@ -5,8 +5,10 @@ import (
"net/http"
"time"
"tm/infra"
"tm/pkg/authorization"
"tm/pkg/logger"
"tm/pkg/mongo"
"tm/pkg/redis"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@@ -131,3 +133,53 @@ func initHTTPServer(conf infra.Config, log logger.Logger) *echo.Echo {
return e
}
// Init Redis Connection Manager
func initRedis(conf infra.RedisConfig, log logger.Logger) redis.Client {
connectionConfig := &redis.Config{
Host: conf.Host,
Port: conf.Port,
Password: conf.Password,
DB: conf.DB,
PoolSize: conf.PoolSize,
}
redisClient, err := redis.NewClient(connectionConfig, log)
if err != nil {
log.Error("Failed to initialize Redis connection", map[string]interface{}{
"error": err.Error(),
"host": conf.Host,
"port": conf.Port,
})
panic(fmt.Sprintf("Failed to initialize Redis connection: %v", err))
}
log.Info("Redis connection manager initialized successfully", map[string]interface{}{
"host": conf.Host,
"port": conf.Port,
"pool_size": conf.PoolSize,
})
return redisClient
}
// Init Authorization Service
func initAuthorizationService(conf infra.JWTConfig, redisClient redis.Client, log logger.Logger) authorization.AuthorizationService {
authConfig := &authorization.AuthorizationConfig{
AccessTokenSecret: conf.AccessSecret,
RefreshTokenSecret: conf.RefreshSecret,
AccessTokenExpiry: time.Duration(conf.AccessExpiresIn) * time.Second,
RefreshTokenExpiry: time.Duration(conf.RefreshExpiresIn) * time.Second,
Issuer: "tender-management-system",
Audience: "tender-management-users",
}
authService := authorization.NewAuthorizationService(authConfig, log, redisClient)
log.Info("Authorization service initialized successfully", map[string]interface{}{
"access_expires_in": conf.AccessExpiresIn,
"refresh_expires_in": conf.RefreshExpiresIn,
})
return authService
}
+4 -3
View File
@@ -38,9 +38,10 @@ search:
auth:
jwt:
secret: "your-super-secret-key"
access_token_duration: "24h"
refresh_token_duration: "168h" # 7 days
access_secret: "your-super-secret-access-token-key-here-make-it-long-and-secure"
refresh_secret: "your-super-secret-refresh-token-key-here-make-it-long-and-secure"
access_expires_in: 3600 # 1 hour in seconds
refresh_expires_in: 2592000 # 30 days in seconds
ai:
openai:
+1335
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+838
View File
@@ -183,6 +183,149 @@ definitions:
total:
type: integer
type: object
user.AuthResponse:
properties:
access_token:
type: string
expires_at:
type: integer
refresh_token:
type: string
user:
$ref: '#/definitions/user.UserResponse'
type: object
user.ChangePasswordForm:
properties:
new_password:
type: string
old_password:
type: string
type: object
user.CreateUserForm:
properties:
company_id:
type: string
department:
type: string
email:
type: string
full_name:
type: string
password:
type: string
phone:
type: string
position:
type: string
profile_image:
type: string
role:
type: string
username:
type: string
type: object
user.LoginForm:
properties:
email_or_username:
type: string
password:
type: string
type: object
user.RefreshTokenForm:
properties:
refresh_token:
type: string
type: object
user.ResetPasswordForm:
properties:
email:
type: string
type: object
user.UpdateUserForm:
properties:
company_id:
type: string
department:
type: string
email:
type: string
full_name:
type: string
phone:
type: string
position:
type: string
profile_image:
type: string
role:
type: string
status:
type: string
username:
type: string
type: object
user.UpdateUserRoleForm:
properties:
role:
type: string
type: object
user.UpdateUserStatusForm:
properties:
status:
type: string
type: object
user.UserListResponse:
properties:
limit:
type: integer
offset:
type: integer
total:
type: integer
total_pages:
type: integer
users:
items:
$ref: '#/definitions/user.UserResponse'
type: array
type: object
user.UserResponse:
properties:
company_id:
type: string
created_at:
type: integer
created_by:
type: string
department:
type: string
email:
type: string
full_name:
type: string
id:
type: string
is_verified:
type: boolean
last_login_at:
type: integer
phone:
type: string
position:
type: string
profile_image:
type: string
role:
type: string
status:
type: string
updated_at:
type: integer
updated_by:
type: string
username:
type: string
type: object
host: localhost:8081
info:
contact:
@@ -703,6 +846,699 @@ paths:
summary: Refresh access token
tags:
- customers
/users/admin:
get:
consumes:
- application/json
description: List users with search and filters (admin only)
parameters:
- description: Search term
in: query
name: search
type: string
- description: User status filter
in: query
name: status
type: string
- description: User role filter
in: query
name: role
type: string
- description: Company ID filter
in: query
name: company_id
type: string
- description: Limit results
in: query
name: limit
type: integer
- description: Offset results
in: query
name: offset
type: integer
- description: Sort field
in: query
name: sort_by
type: string
- description: Sort order
in: query
name: sort_order
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.UserListResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: List users
tags:
- users
post:
consumes:
- application/json
description: Create a new user (admin only)
parameters:
- description: User creation data
in: body
name: user
required: true
schema:
$ref: '#/definitions/user.CreateUserForm'
produces:
- application/json
responses:
"201":
description: Created
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.UserResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Create user
tags:
- users
/users/admin/{id}:
delete:
consumes:
- application/json
description: Delete a user (admin only)
parameters:
- description: User ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Delete user
tags:
- users
get:
consumes:
- application/json
description: Get user details by ID (admin only)
parameters:
- description: User ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.UserResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get user by ID
tags:
- users
put:
consumes:
- application/json
description: Update user information (admin only)
parameters:
- description: User ID
in: path
name: id
required: true
type: string
- description: User update data
in: body
name: user
required: true
schema:
$ref: '#/definitions/user.UpdateUserForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.UserResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Update user
tags:
- users
/users/admin/{id}/role:
put:
consumes:
- application/json
description: Update user role (admin only)
parameters:
- description: User ID
in: path
name: id
required: true
type: string
- description: Role update data
in: body
name: role
required: true
schema:
$ref: '#/definitions/user.UpdateUserRoleForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Update user role
tags:
- users
/users/admin/{id}/status:
put:
consumes:
- application/json
description: Update user account status (admin only)
parameters:
- description: User ID
in: path
name: id
required: true
type: string
- description: Status update data
in: body
name: status
required: true
schema:
$ref: '#/definitions/user.UpdateUserStatusForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Update user status
tags:
- users
/users/admin/company/{company_id}:
get:
consumes:
- application/json
description: Get users belonging to a specific company (admin only)
parameters:
- description: Company ID
in: path
name: company_id
required: true
type: string
- description: Limit results
in: query
name: limit
type: integer
- description: Offset results
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get users by company ID
tags:
- users
/users/admin/role/{role}:
get:
consumes:
- application/json
description: Get users with a specific role (admin only)
parameters:
- description: User role
in: path
name: role
required: true
type: string
- description: Limit results
in: query
name: limit
type: integer
- description: Offset results
in: query
name: offset
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"403":
description: Forbidden
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get users by role
tags:
- users
/users/change-password:
put:
consumes:
- application/json
description: Change current user password
parameters:
- description: Password change data
in: body
name: password
required: true
schema:
$ref: '#/definitions/user.ChangePasswordForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Change password
tags:
- users
/users/login:
post:
consumes:
- application/json
description: Authenticate user with email/username and password
parameters:
- description: Login credentials
in: body
name: login
required: true
schema:
$ref: '#/definitions/user.LoginForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.AuthResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Login user
tags:
- users
/users/logout:
post:
consumes:
- application/json
description: Logout current user and invalidate tokens
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Logout user
tags:
- users
/users/profile:
get:
consumes:
- application/json
description: Get current user profile information
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.UserResponse'
type: object
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Get user profile
tags:
- users
put:
consumes:
- application/json
description: Update current user profile information
parameters:
- description: Profile update data
in: body
name: profile
required: true
schema:
$ref: '#/definitions/user.UpdateUserForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.UserResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
security:
- BearerAuth: []
summary: Update user profile
tags:
- users
/users/refresh-token:
post:
consumes:
- application/json
description: Refresh access token using refresh token
parameters:
- description: Refresh token
in: body
name: refresh
required: true
schema:
$ref: '#/definitions/user.RefreshTokenForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
allOf:
- $ref: '#/definitions/response.APIResponse'
- properties:
data:
$ref: '#/definitions/user.AuthResponse'
type: object
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Refresh access token
tags:
- users
/users/reset-password:
post:
consumes:
- application/json
description: Send password reset email to user
parameters:
- description: Password reset request
in: body
name: reset
required: true
schema:
$ref: '#/definitions/user.ResetPasswordForm'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Reset password
tags:
- users
securityDefinitions:
BearerAuth:
description: Type "Bearer" followed by a space and JWT token.
@@ -713,6 +1549,8 @@ swagger: "2.0"
tags:
- description: Customer management operations
name: customers
- description: User management operations
name: users
- description: Health check operations
name: health
- description: Authentication operations
+31 -10
View File
@@ -42,6 +42,9 @@ package main
// @tag.name customers
// @tag.description Customer management operations
// @tag.name users
// @tag.description User management operations
// @tag.name health
// @tag.description Health check operations
@@ -54,6 +57,7 @@ import (
"net/http"
"time"
"tm/internal/customer"
"tm/internal/user"
_ "tm/cmd/web/docs" // This is generated by swag
)
@@ -76,33 +80,43 @@ func main() {
}
}()
logger.Info(
"Starting Tender Management API Server",
map[string]interface{}{
"version": "1.0.0",
"host": conf.Server.Host,
"port": conf.Server.Port,
})
// Initialize Redis connection manager
redisClient := initRedis(conf.Cache.Redis, logger)
defer func() {
if err := redisClient.Close(); err != nil {
logger.Error("Failed to close Redis connection", map[string]interface{}{
"error": err.Error(),
})
}
}()
// Initialize authorization service
authService := initAuthorizationService(conf.Auth.JWT, redisClient, logger)
// Initialize repositories with MongoDB connection manager
customerRepository := customer.NewCustomerRepository(mongoManager, logger)
userRepository := user.NewUserRepository(mongoManager, logger)
logger.Info("Repositories initialized successfully", map[string]interface{}{
"repositories": []string{"customer"},
"repositories": []string{"customer", "user"},
})
// Initialize validation service
userValidator := user.NewValidationService()
// Initialize services with repositories
customerService := customer.NewCustomerService(customerRepository, logger)
userService := user.NewUserService(userRepository, logger, authService, userValidator)
logger.Info("Services initialized successfully", map[string]interface{}{
"services": []string{"customer"},
"services": []string{"customer", "user"},
})
// Initialize handlers with services
customerHandler := customer.NewHandler(customerService)
userHandler := user.NewUserHandler(userService, logger, userValidator, authService)
logger.Info("Handlers initialized successfully", map[string]interface{}{
"handlers": []string{"customer"},
"handlers": []string{"customer", "user"},
})
// Initialize HTTP server
@@ -110,17 +124,24 @@ func main() {
// Register routes
customerHandler.RegisterRoutes(e)
userHandler.RegisterRoutes(e)
// Start server
serverAddr := fmt.Sprintf("%s:%d", conf.Server.Host, conf.Server.Port)
logger.Info("HTTP server starting", map[string]interface{}{
"address": serverAddr,
"version": "1.0.0",
"host": conf.Server.Host,
"port": conf.Server.Port,
})
if err := e.Start(serverAddr); err != nil && err != http.ErrServerClosed {
logger.Error("Failed to start HTTP server", map[string]interface{}{
"error": err.Error(),
"address": serverAddr,
"version": "1.0.0",
"host": conf.Server.Host,
"port": conf.Server.Port,
})
panic(fmt.Sprintf("Failed to start HTTP server: %v", err))
}