diff --git a/apps/web-admin/next.config.js b/apps/web-admin/next.config.js index 6869a1ad..aa118188 100644 --- a/apps/web-admin/next.config.js +++ b/apps/web-admin/next.config.js @@ -1,8 +1,18 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + // EN: Enable React strict mode for development warnings + // VI: Bật React strict mode để hiển thị warnings trong development reactStrictMode: true, + + // EN: Output standalone build for container deployment + // VI: Output build standalone để deploy trong container output: 'standalone', + + // EN: Environment variables exposed to the browser + // VI: Biến môi trường được expose cho browser env: { + // EN: Public API URL for client-side API calls + // VI: URL API public để gọi API từ client-side NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1', }, }; diff --git a/apps/web-client/next.config.js b/apps/web-client/next.config.js index 6869a1ad..aa118188 100644 --- a/apps/web-client/next.config.js +++ b/apps/web-client/next.config.js @@ -1,8 +1,18 @@ /** @type {import('next').NextConfig} */ const nextConfig = { + // EN: Enable React strict mode for development warnings + // VI: Bật React strict mode để hiển thị warnings trong development reactStrictMode: true, + + // EN: Output standalone build for container deployment + // VI: Output build standalone để deploy trong container output: 'standalone', + + // EN: Environment variables exposed to the browser + // VI: Biến môi trường được expose cho browser env: { + // EN: Public API URL for client-side API calls + // VI: URL API public để gọi API từ client-side NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost/api/v1', }, }; diff --git a/packages/auth-sdk/src/index.ts b/packages/auth-sdk/src/index.ts index 5c0c404d..a6f61352 100644 --- a/packages/auth-sdk/src/index.ts +++ b/packages/auth-sdk/src/index.ts @@ -2,55 +2,114 @@ import * as jwt from 'jsonwebtoken'; import type { SignOptions } from 'jsonwebtoken'; import { TokenPayload } from '@goodgo/types'; +/** + * EN: Options for JWT token verification + * VI: Tùy chọn cho việc xác minh JWT token + */ export interface VerifyTokenOptions { + /** EN: Secret key used to verify the token / VI: Khóa bí mật dùng để xác minh token */ secret: string; + /** EN: Whether to ignore token expiration / VI: Có bỏ qua việc hết hạn token không */ ignoreExpiration?: boolean; } +/** + * EN: Verify and decode a JWT token + * VI: Xác minh và giải mã JWT token + * + * @param token - JWT token string to verify / Chuỗi JWT token cần xác minh + * @param options - Verification options / Tùy chọn xác minh + * @returns Decoded token payload / Payload đã giải mã + * @throws Error if token is invalid or expired / Lỗi nếu token không hợp lệ hoặc hết hạn + */ export const verifyToken = (token: string, options: VerifyTokenOptions): TokenPayload => { try { + // EN: Verify token signature and decode payload + // VI: Xác minh chữ ký token và giải mã payload const decoded = jwt.verify(token, options.secret, { ignoreExpiration: options.ignoreExpiration || false, }) as TokenPayload; return decoded; } catch (error) { - throw new Error('Invalid or expired token'); + throw new Error('Invalid or expired token / Token không hợp lệ hoặc hết hạn'); } }; +/** + * EN: Decode JWT token without verification (for inspection only) + * VI: Giải mã JWT token mà không xác minh (chỉ để kiểm tra) + * + * @param token - JWT token string to decode / Chuỗi JWT token cần giải mã + * @returns Decoded token payload or null if invalid / Payload đã giải mã hoặc null nếu không hợp lệ + */ export const decodeToken = (token: string): TokenPayload | null => { try { + // EN: Decode token without signature verification + // VI: Giải mã token mà không xác minh chữ ký return jwt.decode(token) as TokenPayload; } catch (error) { return null; } }; +/** + * EN: Create a new JWT token with payload and expiration + * VI: Tạo JWT token mới với payload và thời gian hết hạn + * + * @param payload - Token payload data (without iat/exp) / Dữ liệu payload token (không có iat/exp) + * @param secret - Secret key for signing / Khóa bí mật để ký + * @param expiresIn - Token expiration time (default: 15m) / Thời gian hết hạn token (mặc định: 15m) + * @returns Signed JWT token string / Chuỗi JWT token đã ký + */ export const createToken = ( payload: Omit, secret: string, expiresIn: string = '15m' ): string => { + // EN: Sign payload with secret and expiration + // VI: Ký payload với secret và thời gian hết hạn return jwt.sign(payload, secret, { expiresIn } as SignOptions); }; +/** + * EN: Check if JWT token has expired + * VI: Kiểm tra JWT token đã hết hạn chưa + * + * @param token - JWT token string to check / Chuỗi JWT token cần kiểm tra + * @returns True if token is expired or invalid / True nếu token đã hết hạn hoặc không hợp lệ + */ export const isTokenExpired = (token: string): boolean => { try { + // EN: Decode token and check expiration timestamp + // VI: Giải mã token và kiểm tra timestamp hết hạn const decoded = decodeToken(token); if (!decoded || !decoded.exp) { return true; } + // EN: Compare expiration time with current time + // VI: So sánh thời gian hết hạn với thời gian hiện tại return decoded.exp * 1000 < Date.now(); } catch { return true; } }; +/** + * EN: Extract JWT token from Authorization header (Bearer format) + * VI: Trích xuất JWT token từ header Authorization (định dạng Bearer) + * + * @param authHeader - Authorization header value / Giá trị header Authorization + * @returns JWT token string or null if invalid format / Chuỗi JWT token hoặc null nếu định dạng không hợp lệ + */ export const extractTokenFromHeader = (authHeader: string | undefined): string | null => { + // EN: Return null if header is missing + // VI: Trả về null nếu header bị thiếu if (!authHeader) { return null; } + // EN: Split header into parts (expected: "Bearer ") + // VI: Chia header thành các phần (mong đợi: "Bearer ") const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') { return null; @@ -59,6 +118,8 @@ export const extractTokenFromHeader = (authHeader: string | undefined): string | return parts[1]; }; +// EN: Default export with all authentication utilities +// VI: Export default với tất cả các utility xác thực export default { verifyToken, decodeToken, diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts index 0e4df474..7b3def17 100644 --- a/packages/http-client/src/index.ts +++ b/packages/http-client/src/index.ts @@ -1,16 +1,36 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; import { ApiResponse } from '@goodgo/types'; +/** + * EN: Configuration interface for HTTP client setup + * VI: Interface cấu hình cho việc thiết lập HTTP client + */ export interface HttpClientConfig extends AxiosRequestConfig { + /** EN: Base URL for all API requests / VI: Base URL cho tất cả API requests */ baseURL: string; + /** EN: Request timeout in milliseconds / VI: Timeout request tính bằng milliseconds */ timeout?: number; + /** EN: Default headers for all requests / VI: Headers mặc định cho tất cả requests */ headers?: Record; } +/** + * EN: HTTP client wrapper around Axios with authentication and error handling + * VI: HTTP client wrapper xung quanh Axios với authentication và xử lý lỗi + */ export class HttpClient { + /** EN: Axios instance for making HTTP requests / VI: Axios instance để thực hiện HTTP requests */ private client: AxiosInstance; + /** + * EN: Initialize HTTP client with configuration + * VI: Khởi tạo HTTP client với cấu hình + * + * @param config - HTTP client configuration / Cấu hình HTTP client + */ constructor(config: HttpClientConfig) { + // EN: Create Axios instance with base configuration + // VI: Tạo Axios instance với cấu hình cơ bản this.client = axios.create({ baseURL: config.baseURL, timeout: config.timeout || 30000, @@ -20,14 +40,22 @@ export class HttpClient { }, }); + // EN: Setup request/response interceptors + // VI: Thiết lập interceptors cho request/response this.setupInterceptors(); } + /** + * EN: Setup Axios interceptors for authentication and error handling + * VI: Thiết lập Axios interceptors cho authentication và xử lý lỗi + */ private setupInterceptors() { - // Request interceptor + // EN: Request interceptor to add authentication token + // VI: Request interceptor để thêm authentication token this.client.interceptors.request.use( (config) => { - // Add auth token if available + // EN: Add auth token if available in localStorage + // VI: Thêm auth token nếu có trong localStorage const token = this.getAuthToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; @@ -37,15 +65,18 @@ export class HttpClient { (error) => Promise.reject(error) ); - // Response interceptor + // EN: Response interceptor for error handling + // VI: Response interceptor để xử lý lỗi this.client.interceptors.response.use( (response: AxiosResponse) => { return response; }, (error: AxiosError) => { - // Handle common errors + // EN: Handle common HTTP errors + // VI: Xử lý các lỗi HTTP phổ biến if (error.response?.status === 401) { - // Handle unauthorized - clear token, redirect to login + // EN: Handle unauthorized - clear tokens and redirect to login + // VI: Xử lý unauthorized - xóa tokens và chuyển hướng đến login this.clearAuthToken(); } return Promise.reject(error); @@ -53,58 +84,136 @@ export class HttpClient { ); } + /** + * EN: Get authentication token from localStorage + * VI: Lấy authentication token từ localStorage + * + * @returns Access token or null if not found / Access token hoặc null nếu không tìm thấy + */ private getAuthToken(): string | null { + // EN: Check if running in browser environment + // VI: Kiểm tra có đang chạy trong môi trường browser không if (typeof window !== 'undefined') { return localStorage.getItem('accessToken'); } return null; } + /** + * EN: Clear authentication tokens from localStorage + * VI: Xóa authentication tokens khỏi localStorage + */ private clearAuthToken(): void { + // EN: Remove both access and refresh tokens + // VI: Xóa cả access và refresh tokens if (typeof window !== 'undefined') { localStorage.removeItem('accessToken'); localStorage.removeItem('refreshToken'); } } + /** + * EN: Make GET request to API endpoint + * VI: Thực hiện GET request tới API endpoint + * + * @param url - API endpoint URL / URL endpoint API + * @param config - Additional Axios config / Cấu hình Axios bổ sung + * @returns API response data / Dữ liệu phản hồi API + */ async get(url: string, config?: AxiosRequestConfig): Promise> { const response = await this.client.get>(url, config); return response.data; } + /** + * EN: Make POST request to API endpoint + * VI: Thực hiện POST request tới API endpoint + * + * @param url - API endpoint URL / URL endpoint API + * @param data - Request payload / Payload request + * @param config - Additional Axios config / Cấu hình Axios bổ sung + * @returns API response data / Dữ liệu phản hồi API + */ async post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { const response = await this.client.post>(url, data, config); return response.data; } + /** + * EN: Make PUT request to API endpoint + * VI: Thực hiện PUT request tới API endpoint + * + * @param url - API endpoint URL / URL endpoint API + * @param data - Request payload / Payload request + * @param config - Additional Axios config / Cấu hình Axios bổ sung + * @returns API response data / Dữ liệu phản hồi API + */ async put(url: string, data?: any, config?: AxiosRequestConfig): Promise> { const response = await this.client.put>(url, data, config); return response.data; } + /** + * EN: Make PATCH request to API endpoint + * VI: Thực hiện PATCH request tới API endpoint + * + * @param url - API endpoint URL / URL endpoint API + * @param data - Request payload / Payload request + * @param config - Additional Axios config / Cấu hình Axios bổ sung + * @returns API response data / Dữ liệu phản hồi API + */ async patch(url: string, data?: any, config?: AxiosRequestConfig): Promise> { const response = await this.client.patch>(url, data, config); return response.data; } + /** + * EN: Make DELETE request to API endpoint + * VI: Thực hiện DELETE request tới API endpoint + * + * @param url - API endpoint URL / URL endpoint API + * @param config - Additional Axios config / Cấu hình Axios bổ sung + * @returns API response data / Dữ liệu phản hồi API + */ async delete(url: string, config?: AxiosRequestConfig): Promise> { const response = await this.client.delete>(url, config); return response.data; } + /** + * EN: Set authentication token in localStorage + * VI: Đặt authentication token trong localStorage + * + * @param token - JWT access token / JWT access token + */ setAuthToken(token: string): void { + // EN: Store token in browser localStorage + // VI: Lưu token trong localStorage của browser if (typeof window !== 'undefined') { localStorage.setItem('accessToken', token); } } + /** + * EN: Remove authentication token from localStorage + * VI: Xóa authentication token khỏi localStorage + */ removeAuthToken(): void { this.clearAuthToken(); } } +/** + * EN: Factory function to create HttpClient instance + * VI: Hàm factory để tạo HttpClient instance + * + * @param config - HTTP client configuration / Cấu hình HTTP client + * @returns HttpClient instance / Instance HttpClient + */ export const createHttpClient = (config: HttpClientConfig): HttpClient => { return new HttpClient(config); }; +// EN: Default export for the HttpClient class +// VI: Export default cho HttpClient class export default HttpClient; diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index a8313f34..2553b8bd 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -1,13 +1,26 @@ import winston from 'winston'; +/** + * EN: Configuration interface for logger setup + * VI: Interface cấu hình cho việc thiết lập logger + */ export interface LoggerConfig { + /** EN: Logging level (error, warn, info, debug) / VI: Mức độ ghi log (error, warn, info, debug) */ level?: string; + /** EN: Service name for log identification / VI: Tên service để xác định log */ serviceName?: string; + /** EN: Enable console output / VI: Bật output console */ enableConsole?: boolean; + /** EN: Enable file output / VI: Bật output file */ enableFile?: boolean; + /** EN: Directory for log files / VI: Thư mục cho các file log */ logDir?: string; } +/** + * EN: Default logger configuration with environment variable fallbacks + * VI: Cấu hình logger mặc định với fallback từ biến môi trường + */ const defaultConfig: Required = { level: process.env.LOG_LEVEL || 'info', serviceName: process.env.SERVICE_NAME || 'microservice', @@ -16,11 +29,24 @@ const defaultConfig: Required = { logDir: './logs', }; +/** + * EN: Create a Winston logger instance with custom configuration + * VI: Tạo instance Winston logger với cấu hình tùy chỉnh + * + * @param config - Logger configuration options / Tùy chọn cấu hình logger + * @returns Configured Winston logger instance / Instance Winston logger đã cấu hình + */ export const createLogger = (config: LoggerConfig = {}) => { + // EN: Merge user config with defaults + // VI: Hợp nhất config người dùng với mặc định const finalConfig = { ...defaultConfig, ...config }; + // EN: Array to hold Winston transport instances + // VI: Mảng chứa các instance transport của Winston const transports: winston.transport[] = []; + // EN: Add console transport if enabled + // VI: Thêm console transport nếu được bật if (finalConfig.enableConsole) { transports.push( new winston.transports.Console({ @@ -28,6 +54,8 @@ export const createLogger = (config: LoggerConfig = {}) => { winston.format.timestamp(), winston.format.colorize(), winston.format.printf(({ timestamp, level, message, service, ...meta }) => { + // EN: Format log message with timestamp, level, service tag, and metadata + // VI: Định dạng thông điệp log với timestamp, level, service tag, và metadata const serviceTag = service ? `[${service}]` : ''; const metaStr = Object.keys(meta).length ? JSON.stringify(meta) : ''; return `${timestamp} ${level} ${serviceTag} ${message} ${metaStr}`; @@ -37,6 +65,8 @@ export const createLogger = (config: LoggerConfig = {}) => { ); } + // EN: Add file transports if enabled + // VI: Thêm file transports nếu được bật if (finalConfig.enableFile) { transports.push( new winston.transports.File({ @@ -57,6 +87,8 @@ export const createLogger = (config: LoggerConfig = {}) => { ); } + // EN: Create and return Winston logger instance + // VI: Tạo và trả về instance Winston logger return winston.createLogger({ level: finalConfig.level, defaultMeta: { @@ -66,6 +98,12 @@ export const createLogger = (config: LoggerConfig = {}) => { }); }; +/** + * EN: Default logger instance using default configuration + * VI: Instance logger mặc định sử dụng cấu hình mặc định + */ export const logger = createLogger(); +// EN: Default export for convenience +// VI: Export default để thuận tiện export default logger; diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index cf071f73..36ab5576 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -4,23 +4,43 @@ import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; import { Resource } from '@opentelemetry/resources'; import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; +/** + * EN: Configuration interface for distributed tracing setup + * VI: Interface cấu hình cho việc thiết lập distributed tracing + */ export interface TracingConfig { + /** EN: Name of the service for trace identification / VI: Tên service để xác định trace */ serviceName: string; + /** EN: Jaeger collector endpoint URL / VI: URL endpoint Jaeger collector */ jaegerEndpoint?: string; + /** EN: Enable/disable tracing (default: true) / VI: Bật/tắt tracing (mặc định: true) */ enabled?: boolean; } +/** + * EN: Initialize OpenTelemetry distributed tracing with Jaeger exporter + * VI: Khởi tạo OpenTelemetry distributed tracing với Jaeger exporter + * + * @param config - Tracing configuration / Cấu hình tracing + * @returns NodeSDK instance or null if tracing is disabled / Instance NodeSDK hoặc null nếu tracing bị tắt + */ export const initTracing = (config: TracingConfig): NodeSDK | null => { + // EN: Return null if tracing is explicitly disabled + // VI: Trả về null nếu tracing bị tắt rõ ràng if (config.enabled === false) { return null; } + // EN: Create Jaeger exporter if endpoint is provided + // VI: Tạo Jaeger exporter nếu endpoint được cung cấp const jaegerExporter = config.jaegerEndpoint ? new JaegerExporter({ endpoint: config.jaegerEndpoint, }) : undefined; + // EN: Initialize OpenTelemetry NodeSDK with auto-instrumentations + // VI: Khởi tạo OpenTelemetry NodeSDK với auto-instrumentations const sdk = new NodeSDK({ resource: new Resource({ [SemanticResourceAttributes.SERVICE_NAME]: config.serviceName, @@ -29,9 +49,13 @@ export const initTracing = (config: TracingConfig): NodeSDK | null => { instrumentations: [getNodeAutoInstrumentations()], }); + // EN: Start the tracing SDK + // VI: Khởi động tracing SDK sdk.start(); return sdk; }; +// EN: Default export for the initTracing function +// VI: Export default cho hàm initTracing export default initTracing; diff --git a/packages/types/src/auth.types.ts b/packages/types/src/auth.types.ts index 70ffcb8c..b51e5831 100644 --- a/packages/types/src/auth.types.ts +++ b/packages/types/src/auth.types.ts @@ -1,44 +1,95 @@ +/** + * EN: Data transfer object for user login + * VI: Đối tượng truyền dữ liệu cho đăng nhập người dùng + */ export interface LoginDto { + /** EN: User email address / VI: Địa chỉ email người dùng */ email: string; + /** EN: User password / VI: Mật khẩu người dùng */ password: string; } +/** + * EN: Data transfer object for user registration + * VI: Đối tượng truyền dữ liệu cho đăng ký người dùng + */ export interface RegisterDto { + /** EN: User email address / VI: Địa chỉ email người dùng */ email: string; + /** EN: User password / VI: Mật khẩu người dùng */ password: string; + /** EN: Password confirmation / VI: Xác nhận mật khẩu */ confirmPassword: string; } import { UserResponse } from './user.types'; +/** + * EN: Response object for authentication operations + * VI: Đối tượng phản hồi cho các thao tác xác thực + */ export interface AuthResponse { + /** EN: JWT access token for API authentication / VI: JWT access token để xác thực API */ accessToken: string; + /** EN: JWT refresh token for obtaining new access tokens / VI: JWT refresh token để lấy access token mới */ refreshToken: string; + /** EN: Authenticated user information / VI: Thông tin người dùng đã xác thực */ user: UserResponse; } +/** + * EN: Data transfer object for token refresh + * VI: Đối tượng truyền dữ liệu để làm mới token + */ export interface RefreshTokenDto { + /** EN: Valid refresh token / VI: Refresh token hợp lệ */ refreshToken: string; } +/** + * EN: JWT token payload structure + * VI: Cấu trúc payload JWT token + */ export interface TokenPayload { + /** EN: Unique user identifier / VI: Mã định danh duy nhất người dùng */ userId: string; + /** EN: User email address / VI: Địa chỉ email người dùng */ email: string; + /** EN: User role for authorization / VI: Vai trò người dùng để phân quyền */ role: string; + /** EN: Token issued at timestamp / VI: Timestamp token được phát hành */ iat?: number; + /** EN: Token expiration timestamp / VI: Timestamp token hết hạn */ exp?: number; } +/** + * EN: Data transfer object for password change + * VI: Đối tượng truyền dữ liệu để đổi mật khẩu + */ export interface ChangePasswordDto { + /** EN: Current password for verification / VI: Mật khẩu hiện tại để xác minh */ currentPassword: string; + /** EN: New password / VI: Mật khẩu mới */ newPassword: string; } +/** + * EN: Data transfer object for forgot password + * VI: Đối tượng truyền dữ liệu cho quên mật khẩu + */ export interface ForgotPasswordDto { + /** EN: User email for password reset / VI: Email người dùng để reset mật khẩu */ email: string; } +/** + * EN: Data transfer object for password reset + * VI: Đối tượng truyền dữ liệu để reset mật khẩu + */ export interface ResetPasswordDto { + /** EN: Password reset token / VI: Token reset mật khẩu */ token: string; + /** EN: New password / VI: Mật khẩu mới */ newPassword: string; } diff --git a/packages/types/src/user.types.ts b/packages/types/src/user.types.ts index 13ef4852..9b6870aa 100644 --- a/packages/types/src/user.types.ts +++ b/packages/types/src/user.types.ts @@ -1,35 +1,76 @@ +/** + * EN: User role enumeration for authorization levels + * VI: Enumeration vai trò người dùng cho các cấp độ phân quyền + */ export enum Role { + /** EN: Standard user role / VI: Vai trò người dùng tiêu chuẩn */ USER = 'USER', + /** EN: Administrator role with elevated permissions / VI: Vai trò quản trị viên với quyền cao hơn */ ADMIN = 'ADMIN', + /** EN: Super administrator with full system access / VI: Siêu quản trị viên với quyền truy cập đầy đủ hệ thống */ SUPER_ADMIN = 'SUPER_ADMIN', } +/** + * EN: User entity interface for database operations + * VI: Interface thực thể người dùng cho các thao tác database + */ export interface User { + /** EN: Unique user identifier / VI: Mã định danh duy nhất người dùng */ id: string; + /** EN: User email address (unique) / VI: Địa chỉ email người dùng (duy nhất) */ email: string; + /** EN: User role for authorization / VI: Vai trò người dùng để phân quyền */ role: Role; + /** EN: Account active status / VI: Trạng thái hoạt động tài khoản */ isActive: boolean; + /** EN: Account creation timestamp / VI: Timestamp tạo tài khoản */ createdAt: Date; + /** EN: Last update timestamp / VI: Timestamp cập nhật cuối */ updatedAt: Date; } +/** + * EN: Data transfer object for creating new users + * VI: Đối tượng truyền dữ liệu để tạo người dùng mới + */ export interface CreateUserDto { + /** EN: User email address / VI: Địa chỉ email người dùng */ email: string; + /** EN: User password / VI: Mật khẩu người dùng */ password: string; + /** EN: User role (optional, defaults to USER) / VI: Vai trò người dùng (tùy chọn, mặc định là USER) */ role?: Role; } +/** + * EN: Data transfer object for updating user information + * VI: Đối tượng truyền dữ liệu để cập nhật thông tin người dùng + */ export interface UpdateUserDto { + /** EN: New email address (optional) / VI: Địa chỉ email mới (tùy chọn) */ email?: string; + /** EN: New user role (optional) / VI: Vai trò người dùng mới (tùy chọn) */ role?: Role; + /** EN: Account active status (optional) / VI: Trạng thái hoạt động tài khoản (tùy chọn) */ isActive?: boolean; } +/** + * EN: User response interface for API responses (with string timestamps) + * VI: Interface phản hồi người dùng cho API responses (với string timestamps) + */ export interface UserResponse { + /** EN: Unique user identifier / VI: Mã định danh duy nhất người dùng */ id: string; + /** EN: User email address / VI: Địa chỉ email người dùng */ email: string; + /** EN: User role for authorization / VI: Vai trò người dùng để phân quyền */ role: Role; + /** EN: Account active status / VI: Trạng thái hoạt động tài khoản */ isActive: boolean; + /** EN: Account creation timestamp (ISO string) / VI: Timestamp tạo tài khoản (ISO string) */ createdAt: string; + /** EN: Last update timestamp (ISO string) / VI: Timestamp cập nhật cuối (ISO string) */ updatedAt: string; } diff --git a/scripts/db/migrate.sh b/scripts/db/migrate.sh index 5929b339..789d2c87 100755 --- a/scripts/db/migrate.sh +++ b/scripts/db/migrate.sh @@ -1,39 +1,49 @@ #!/bin/bash +# EN: Database migration script for individual services +# VI: Script migration database cho từng service riêng lẻ set -e SERVICE=$1 +# EN: Validate service name parameter +# VI: Xác thực tham số tên service if [ -z "$SERVICE" ]; then - echo "Usage: $0 [dev|deploy]" - echo "Example: $0 auth-service dev" - echo "Example: $0 auth-service deploy" + echo "Usage: $0 [dev|deploy] / Cách dùng: $0 [dev|deploy]" + echo "Example: $0 auth-service dev / Ví dụ: $0 auth-service dev" + echo "Example: $0 auth-service deploy / Ví dụ: $0 auth-service deploy" exit 1 fi +# EN: Check if service directory exists +# VI: Kiểm tra thư mục service có tồn tại không if [ ! -d "services/$SERVICE" ]; then - echo "❌ Service $SERVICE not found" + echo "❌ Service $SERVICE not found / Không tìm thấy service $SERVICE" exit 1 fi -echo "🔄 Running migrations for $SERVICE..." +echo "🔄 Running migrations for $SERVICE... / Chạy migrations cho $SERVICE..." cd "services/$SERVICE" -# Load environment variables (hybrid pattern) -# 1. Load shared env (JWT secrets, Redis config) +# EN: Load environment variables (hybrid pattern) +# VI: Load biến môi trường (hybrid pattern) +# EN: 1. Load shared env (JWT secrets, Redis config) +# VI: 1. Load shared env (JWT secrets, Redis config) if [ -f "../../deployments/local/.env.local" ]; then export $(grep -v '^#' ../../deployments/local/.env.local | xargs) fi -# 2. Load service-specific env (DATABASE_URL, PORT, etc.) +# EN: 2. Load service-specific env (DATABASE_URL, PORT, etc.) +# VI: 2. Load service-specific env (DATABASE_URL, PORT, etc.) if [ -f ".env.local" ]; then export $(grep -v '^#' .env.local | xargs) fi -# Check if DATABASE_URL is set +# EN: Check if DATABASE_URL is set +# VI: Kiểm tra DATABASE_URL có được thiết lập không if [ -z "$DATABASE_URL" ]; then - echo "⚠️ DATABASE_URL not set. Please check your .env.local files:" + echo "⚠️ DATABASE_URL not set. Please check your .env.local files: / DATABASE_URL chưa được thiết lập. Vui lòng kiểm tra file .env.local:" echo " - Shared config: ../../deployments/local/.env.local" echo " - Service config: .env.local" echo " For Neon: postgresql://user:pass@ep-xxx.region.neon.tech/dbname?sslmode=require&pgbouncer=true" diff --git a/scripts/dev/start-all.sh b/scripts/dev/start-all.sh index 06731c26..4ffbd20f 100755 --- a/scripts/dev/start-all.sh +++ b/scripts/dev/start-all.sh @@ -1,41 +1,52 @@ #!/bin/bash +# EN: Script to start all services in development environment +# VI: Script để khởi động tất cả services trong môi trường development set -e -echo "🚀 Starting all services..." +echo "🚀 Starting all services... / Khởi động tất cả services..." -# Check if Docker is running +# EN: Verify Docker daemon is running +# VI: Xác minh Docker daemon đang chạy if ! docker info &> /dev/null; then - echo "❌ Docker is not running. Please start Docker first." + echo "❌ Docker is not running. Please start Docker first. / Docker không chạy. Vui lòng khởi động Docker trước." exit 1 fi -# Check if Neon DATABASE_URL is set +# EN: Load environment variables from shared config +# VI: Load biến môi trường từ shared config if [ -f "deployments/local/.env.local" ]; then export $(grep -v '^#' deployments/local/.env.local | xargs) fi +# EN: Check if DATABASE_URL is configured (required for services) +# VI: Kiểm tra DATABASE_URL có được cấu hình không (bắt buộc cho services) if [ -z "$DATABASE_URL" ]; then - echo "⚠️ WARNING: DATABASE_URL not set!" + echo "⚠️ WARNING: DATABASE_URL not set! / CẢNH BÁO: DATABASE_URL chưa được thiết lập!" echo " Please setup Neon database: ./scripts/db/setup-neon.sh" echo " Or set DATABASE_URL in deployments/local/.env.local" + echo " / Vui lòng thiết lập Neon database: ./scripts/db/setup-neon.sh" + echo " Hoặc đặt DATABASE_URL trong deployments/local/.env.local" echo "" - read -p "Continue anyway? (y/n): " continue + read -p "Continue anyway? (y/n): / Tiếp tục? (y/n): " continue if [ "$continue" != "y" ]; then exit 1 fi fi -# Start infrastructure (Redis, Traefik - no PostgreSQL needed) -echo "📦 Starting infrastructure (Redis, Traefik)..." +# EN: Start infrastructure services (Redis for caching, Traefik for routing) +# VI: Khởi động infrastructure services (Redis cho caching, Traefik cho routing) +echo "📦 Starting infrastructure (Redis, Traefik)... / Khởi động infrastructure (Redis, Traefik)..." cd deployments/local docker-compose up -d cd ../.. -# Wait for Redis to be ready -echo "⏳ Waiting for Redis to be ready..." +# EN: Give Redis time to fully start before starting services +# VI: Cho Redis thời gian để khởi động đầy đủ trước khi khởi động services +echo "⏳ Waiting for Redis to be ready... / Đang chờ Redis sẵn sàng..." sleep 3 -# Start services -echo "🚀 Starting services..." +# EN: Start all microservices using pnpm dev +# VI: Khởi động tất cả microservices sử dụng pnpm dev +echo "🚀 Starting services... / Khởi động services..." pnpm dev diff --git a/services/_template/src/config/app.config.ts b/services/_template/src/config/app.config.ts index f2cd3a12..a2b231bd 100644 --- a/services/_template/src/config/app.config.ts +++ b/services/_template/src/config/app.config.ts @@ -1,6 +1,14 @@ +/** + * EN: Application configuration object + * VI: Đối tượng cấu hình ứng dụng + */ export const appConfig = { + /** EN: Port number for the HTTP server / VI: Số port cho HTTP server */ port: parseInt(process.env.PORT || '5000', 10), + /** EN: Node.js environment (development/production) / VI: Môi trường Node.js (development/production) */ nodeEnv: process.env.NODE_ENV || 'development', + /** EN: API version prefix / VI: Tiền tố phiên bản API */ apiVersion: process.env.API_VERSION || 'v1', + /** EN: Allowed CORS origins / VI: Các origin được phép CORS */ corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], }; diff --git a/services/_template/src/config/database.config.ts b/services/_template/src/config/database.config.ts index 6c5d23ee..bca0bd1d 100644 --- a/services/_template/src/config/database.config.ts +++ b/services/_template/src/config/database.config.ts @@ -1,21 +1,39 @@ import { PrismaClient } from '@prisma/client'; import { logger } from '@goodgo/logger'; +/** + * EN: Prisma client instance configured for the application + * VI: Instance Prisma client được cấu hình cho ứng dụng + */ export const prisma = new PrismaClient({ + // EN: Enable detailed logging in development, minimal in production + // VI: Bật ghi log chi tiết trong development, tối thiểu trong production log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }); +/** + * EN: Establish database connection on application startup + * VI: Thiết lập kết nối database khi khởi động ứng dụng + */ export const connectDatabase = async (): Promise => { try { + // EN: Connect to database using Prisma + // VI: Kết nối tới database sử dụng Prisma await prisma.$connect(); - logger.info('Database connected successfully'); + logger.info('Database connected successfully / Kết nối database thành công'); } catch (error) { - logger.error('Database connection failed', { error }); + // EN: Log error and exit if database connection fails + // VI: Ghi log lỗi và thoát nếu kết nối database thất bại + logger.error('Database connection failed / Kết nối database thất bại', { error }); process.exit(1); } }; +/** + * EN: Close database connection on application shutdown + * VI: Đóng kết nối database khi tắt ứng dụng + */ export const disconnectDatabase = async (): Promise => { await prisma.$disconnect(); - logger.info('Database disconnected'); + logger.info('Database disconnected / Đã ngắt kết nối database'); }; diff --git a/services/auth-service/src/config/app.config.ts b/services/auth-service/src/config/app.config.ts index a88bbc17..2e0cacbc 100644 --- a/services/auth-service/src/config/app.config.ts +++ b/services/auth-service/src/config/app.config.ts @@ -1,6 +1,14 @@ +/** + * EN: Application configuration object + * VI: Đối tượng cấu hình ứng dụng + */ export const appConfig = { + /** EN: Port number for the HTTP server / VI: Số port cho HTTP server */ port: parseInt(process.env.PORT || '5001', 10), + /** EN: Node.js environment (development/production) / VI: Môi trường Node.js (development/production) */ nodeEnv: process.env.NODE_ENV || 'development', + /** EN: API version prefix / VI: Tiền tố phiên bản API */ apiVersion: process.env.API_VERSION || 'v1', + /** EN: Allowed CORS origins / VI: Các origin được phép CORS */ corsOrigin: process.env.CORS_ORIGIN?.split(',') || ['http://localhost:3000'], }; diff --git a/services/auth-service/src/config/database.config.ts b/services/auth-service/src/config/database.config.ts index 6c5d23ee..bca0bd1d 100644 --- a/services/auth-service/src/config/database.config.ts +++ b/services/auth-service/src/config/database.config.ts @@ -1,21 +1,39 @@ import { PrismaClient } from '@prisma/client'; import { logger } from '@goodgo/logger'; +/** + * EN: Prisma client instance configured for the application + * VI: Instance Prisma client được cấu hình cho ứng dụng + */ export const prisma = new PrismaClient({ + // EN: Enable detailed logging in development, minimal in production + // VI: Bật ghi log chi tiết trong development, tối thiểu trong production log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }); +/** + * EN: Establish database connection on application startup + * VI: Thiết lập kết nối database khi khởi động ứng dụng + */ export const connectDatabase = async (): Promise => { try { + // EN: Connect to database using Prisma + // VI: Kết nối tới database sử dụng Prisma await prisma.$connect(); - logger.info('Database connected successfully'); + logger.info('Database connected successfully / Kết nối database thành công'); } catch (error) { - logger.error('Database connection failed', { error }); + // EN: Log error and exit if database connection fails + // VI: Ghi log lỗi và thoát nếu kết nối database thất bại + logger.error('Database connection failed / Kết nối database thất bại', { error }); process.exit(1); } }; +/** + * EN: Close database connection on application shutdown + * VI: Đóng kết nối database khi tắt ứng dụng + */ export const disconnectDatabase = async (): Promise => { await prisma.$disconnect(); - logger.info('Database disconnected'); + logger.info('Database disconnected / Đã ngắt kết nối database'); }; diff --git a/services/auth-service/src/config/jwt.config.ts b/services/auth-service/src/config/jwt.config.ts index fb057306..5966bd37 100644 --- a/services/auth-service/src/config/jwt.config.ts +++ b/services/auth-service/src/config/jwt.config.ts @@ -1,10 +1,20 @@ +/** + * EN: JWT configuration object with secrets and expiration times + * VI: Đối tượng cấu hình JWT với secrets và thời gian hết hạn + */ export const jwtConfig = { + /** EN: Secret key for signing access tokens / VI: Khóa bí mật để ký access tokens */ secret: process.env.JWT_SECRET || 'default-secret-change-in-production', + /** EN: Access token expiration time / VI: Thời gian hết hạn của access token */ expiresIn: process.env.JWT_EXPIRES_IN || '15m', + /** EN: Secret key for signing refresh tokens / VI: Khóa bí mật để ký refresh tokens */ refreshSecret: process.env.JWT_REFRESH_SECRET || 'default-refresh-secret-change-in-production', + /** EN: Refresh token expiration time / VI: Thời gian hết hạn của refresh token */ refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d', }; +// EN: Warn about default JWT secret in development +// VI: Cảnh báo về JWT secret mặc định trong môi trường phát triển if (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'default-secret-change-in-production') { - console.warn('⚠️ WARNING: Using default JWT_SECRET. Change it in production!'); + console.warn('⚠️ WARNING: Using default JWT_SECRET. Change it in production! / ⚠️ CẢNH BÁO: Đang sử dụng JWT_SECRET mặc định. Hãy thay đổi trong production!'); } diff --git a/services/auth-service/src/main.ts b/services/auth-service/src/main.ts index 2b39aeef..06aa6028 100644 --- a/services/auth-service/src/main.ts +++ b/services/auth-service/src/main.ts @@ -10,7 +10,8 @@ import { errorHandler, notFoundHandler } from './middlewares/error.middleware'; import { logger } from '@goodgo/logger'; import { initTracing } from '@goodgo/tracing'; -// Initialize tracing +// EN: Initialize distributed tracing if enabled +// VI: Khởi tạo distributed tracing nếu được bật if (process.env.TRACING_ENABLED === 'true') { initTracing({ serviceName: process.env.SERVICE_NAME || 'auth-service', @@ -19,10 +20,16 @@ if (process.env.TRACING_ENABLED === 'true') { }); } +// EN: Create Express application instance +// VI: Tạo instance ứng dụng Express const app = express(); -// Security middleware +// EN: Security middleware - helmet for security headers +// VI: Middleware bảo mật - helmet cho security headers app.use(helmet()); + +// EN: CORS configuration for cross-origin requests +// VI: Cấu hình CORS cho cross-origin requests app.use( cors({ origin: appConfig.corsOrigin, @@ -30,52 +37,70 @@ app.use( }) ); -// Rate limiting +// EN: Rate limiting to prevent abuse (100 requests per 15 minutes per IP) +// VI: Giới hạn tốc độ để ngăn chặn lạm dụng (100 requests mỗi 15 phút cho mỗi IP) const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs }); app.use('/api', limiter); -// Body parsing +// EN: Body parsing middleware for JSON and URL-encoded data +// VI: Middleware phân tích body cho dữ liệu JSON và URL-encoded app.use(express.json()); app.use(express.urlencoded({ extended: true })); -// Request logging +// EN: Request logging middleware +// VI: Middleware ghi log requests app.use(requestLogger); -// Routes +// EN: Mount API routes +// VI: Mount các routes API app.use(createRouter()); -// Error handling +// EN: Error handling middleware (must be last) +// VI: Middleware xử lý lỗi (phải là cuối cùng) app.use(notFoundHandler); app.use(errorHandler); +/** + * EN: Start the HTTP server after connecting to database + * VI: Khởi động HTTP server sau khi kết nối tới database + */ const startServer = async () => { try { + // EN: Establish database connection before starting server + // VI: Thiết lập kết nối database trước khi khởi động server await connectDatabase(); + // EN: Start HTTP server on configured port + // VI: Khởi động HTTP server trên port đã cấu hình app.listen(appConfig.port, () => { - logger.info(`Auth Service started on port ${appConfig.port}`, { + logger.info(`Auth Service started on port ${appConfig.port} / Auth Service đã khởi động trên port ${appConfig.port}`, { port: appConfig.port, nodeEnv: appConfig.nodeEnv, }); }); } catch (error) { - logger.error('Failed to start server', { error }); + // EN: Exit process if server startup fails + // VI: Thoát process nếu khởi động server thất bại + logger.error('Failed to start server / Khởi động server thất bại', { error }); process.exit(1); } }; +// EN: Start the application +// VI: Khởi động ứng dụng startServer(); -// Graceful shutdown +// EN: Graceful shutdown handlers for SIGTERM and SIGINT signals +// VI: Handlers tắt máy gracefully cho tín hiệu SIGTERM và SIGINT process.on('SIGTERM', async () => { - logger.info('SIGTERM received, shutting down gracefully'); + logger.info('SIGTERM received, shutting down gracefully / Nhận SIGTERM, tắt máy gracefully'); process.exit(0); }); process.on('SIGINT', async () => { - logger.info('SIGINT received, shutting down gracefully'); + logger.info('SIGINT received, shutting down gracefully / Nhận SIGINT, tắt máy gracefully'); process.exit(0); }); diff --git a/services/auth-service/src/middlewares/auth.middleware.ts b/services/auth-service/src/middlewares/auth.middleware.ts index 40b447fa..ba5b000d 100644 --- a/services/auth-service/src/middlewares/auth.middleware.ts +++ b/services/auth-service/src/middlewares/auth.middleware.ts @@ -3,38 +3,62 @@ import { verifyToken, extractTokenFromHeader } from '@goodgo/auth-sdk'; import { jwtConfig } from '../config/jwt.config'; import { logger } from '@goodgo/logger'; +/** + * EN: Extended Express Request interface with user authentication data + * VI: Interface Request mở rộng của Express với dữ liệu xác thực người dùng + */ export interface AuthRequest extends Request { + /** EN: Authenticated user information / VI: Thông tin người dùng đã xác thực */ user?: { + /** EN: Unique user identifier / VI: Mã định danh duy nhất người dùng */ userId: string; + /** EN: User email address / VI: Địa chỉ email người dùng */ email: string; + /** EN: User role for authorization / VI: Vai trò người dùng để phân quyền */ role: string; }; } +/** + * EN: Express middleware to authenticate JWT tokens + * VI: Middleware Express để xác thực JWT tokens + * + * @param req - Express request with potential user data / Request Express với dữ liệu người dùng tiềm năng + * @param res - Express response object / Đối tượng response của Express + * @param next - Express next function / Hàm next của Express + */ export const authenticate = async ( req: AuthRequest, res: Response, next: NextFunction ): Promise => { try { + // EN: Extract JWT token from Authorization header + // VI: Trích xuất JWT token từ header Authorization const token = extractTokenFromHeader(req.headers.authorization); + // EN: Return 401 if no token provided + // VI: Trả về 401 nếu không cung cấp token if (!token) { res.status(401).json({ success: false, error: { code: 'AUTH_001', - message: 'Authentication token required', + message: 'Authentication token required / Yêu cầu token xác thực', }, timestamp: new Date().toISOString(), }); return; } + // EN: Verify token and extract payload + // VI: Xác minh token và trích xuất payload const payload = verifyToken(token, { secret: jwtConfig.secret, }); + // EN: Attach user information to request object + // VI: Gắn thông tin người dùng vào đối tượng request req.user = { userId: payload.userId, email: payload.email, @@ -43,38 +67,51 @@ export const authenticate = async ( next(); } catch (error) { - logger.error('Authentication failed', { error }); + // EN: Log authentication failure and return 401 + // VI: Ghi log thất bại xác thực và trả về 401 + logger.error('Authentication failed / Xác thực thất bại', { error }); res.status(401).json({ success: false, error: { code: 'AUTH_002', - message: 'Invalid or expired token', + message: 'Invalid or expired token / Token không hợp lệ hoặc hết hạn', }, timestamp: new Date().toISOString(), }); } }; +/** + * EN: Higher-order middleware to authorize users based on roles + * VI: Middleware higher-order để phân quyền người dùng dựa trên vai trò + * + * @param roles - Array of allowed roles / Mảng các vai trò được phép + * @returns Express middleware function / Hàm middleware Express + */ export const authorize = (...roles: string[]) => { return (req: AuthRequest, res: Response, next: NextFunction): void => { + // EN: Check if user is authenticated + // VI: Kiểm tra người dùng đã xác thực chưa if (!req.user) { res.status(401).json({ success: false, error: { code: 'AUTH_003', - message: 'Authentication required', + message: 'Authentication required / Yêu cầu xác thực', }, timestamp: new Date().toISOString(), }); return; } + // EN: Check if user has required role + // VI: Kiểm tra người dùng có vai trò yêu cầu không if (!roles.includes(req.user.role)) { res.status(403).json({ success: false, error: { code: 'AUTH_004', - message: 'Insufficient permissions', + message: 'Insufficient permissions / Quyền không đủ', }, timestamp: new Date().toISOString(), }); diff --git a/services/auth-service/src/middlewares/error.middleware.ts b/services/auth-service/src/middlewares/error.middleware.ts index ef0a6549..821f9f0f 100644 --- a/services/auth-service/src/middlewares/error.middleware.ts +++ b/services/auth-service/src/middlewares/error.middleware.ts @@ -2,24 +2,41 @@ import { Request, Response, NextFunction } from 'express'; import { logger } from '@goodgo/logger'; import { ApiResponse } from '@goodgo/types'; +/** + * EN: Express error handling middleware + * VI: Middleware xử lý lỗi của Express + * + * @param err - Error object / Đối tượng lỗi + * @param req - Express request object / Đối tượng request của Express + * @param res - Express response object / Đối tượng response của Express + * @param _next - Express next function (unused) / Hàm next của Express (không sử dụng) + */ export const errorHandler = ( err: Error, req: Request, res: Response, _next: NextFunction ): void => { - logger.error('Error occurred', { + // EN: Log error details for debugging + // VI: Ghi log chi tiết lỗi để debug + logger.error('Error occurred / Đã xảy ra lỗi', { error: err.message, stack: err.stack, path: req.path, method: req.method, }); + // EN: Create standardized error response + // VI: Tạo response lỗi chuẩn hóa const response: ApiResponse = { success: false, error: { code: 'INTERNAL_ERROR', - message: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message, + // EN: Hide sensitive error details in production + // VI: Ẩn chi tiết lỗi nhạy cảm trong production + message: process.env.NODE_ENV === 'production' ? 'Internal server error / Lỗi máy chủ nội bộ' : err.message, + // EN: Include stack trace only in development + // VI: Chỉ include stack trace trong development details: process.env.NODE_ENV === 'development' ? { stack: err.stack } : undefined, }, timestamp: new Date().toISOString(), @@ -28,12 +45,21 @@ export const errorHandler = ( res.status(500).json(response); }; +/** + * EN: 404 Not Found handler for undefined routes + * VI: Handler 404 Not Found cho các routes không tồn tại + * + * @param req - Express request object / Đối tượng request của Express + * @param res - Express response object / Đối tượng response của Express + */ export const notFoundHandler = (req: Request, res: Response): void => { + // EN: Create 404 error response for unmatched routes + // VI: Tạo response lỗi 404 cho routes không khớp const response: ApiResponse = { success: false, error: { code: 'NOT_FOUND', - message: `Route ${req.path} not found`, + message: `Route ${req.path} not found / Không tìm thấy route ${req.path}`, }, timestamp: new Date().toISOString(), }; diff --git a/services/auth-service/src/middlewares/logger.middleware.ts b/services/auth-service/src/middlewares/logger.middleware.ts index d7dd0c20..0995ca89 100644 --- a/services/auth-service/src/middlewares/logger.middleware.ts +++ b/services/auth-service/src/middlewares/logger.middleware.ts @@ -1,12 +1,26 @@ import { Request, Response, NextFunction } from 'express'; import { logger } from '@goodgo/logger'; +/** + * EN: Express middleware for logging HTTP requests + * VI: Middleware Express để ghi log HTTP requests + * + * @param req - Express request object / Đối tượng request của Express + * @param res - Express response object / Đối tượng response của Express + * @param next - Express next function / Hàm next của Express + */ export const requestLogger = (req: Request, res: Response, next: NextFunction): void => { + // EN: Record request start time + // VI: Ghi lại thời gian bắt đầu request const start = Date.now(); + // EN: Log request details when response finishes + // VI: Ghi log chi tiết request khi response hoàn thành res.on('finish', () => { + // EN: Calculate request duration + // VI: Tính thời gian xử lý request const duration = Date.now() - start; - logger.info('HTTP Request', { + logger.info('HTTP Request / HTTP Request', { method: req.method, path: req.path, statusCode: res.statusCode, diff --git a/services/auth-service/src/modules/auth/auth.controller.ts b/services/auth-service/src/modules/auth/auth.controller.ts index 93cc4887..69d6a794 100644 --- a/services/auth-service/src/modules/auth/auth.controller.ts +++ b/services/auth-service/src/modules/auth/auth.controller.ts @@ -10,130 +10,224 @@ import { AuthRequest } from '../../middlewares/auth.middleware'; import { ApiResponse } from '@goodgo/types'; import { AuthResponse } from './auth.dto'; +/** + * EN: Authentication controller handling HTTP requests for user auth operations + * VI: Controller xác thực xử lý các yêu cầu HTTP cho các thao tác xác thực người dùng + */ export class AuthController { + /** EN: Auth service instance / VI: Instance của auth service */ private authService: AuthService; + /** + * EN: Initialize auth controller with service dependency + * VI: Khởi tạo auth controller với dependency service + */ constructor() { this.authService = new AuthService(); } + /** + * EN: Handle user registration HTTP request + * VI: Xử lý yêu cầu HTTP đăng ký người dùng + * + * @param req - Express request object / Đối tượng request của Express + * @param res - Express response object / Đối tượng response của Express + * @returns Promise + */ register = async (req: Request, res: Response): Promise => { try { + // EN: Validate and parse request body using Zod schema + // VI: Xác thực và parse request body sử dụng Zod schema const data = registerDtoSchema.parse(req.body); + + // EN: Call auth service to register user + // VI: Gọi auth service để đăng ký người dùng const result = await this.authService.register(data); + // EN: Return success response with auth tokens + // VI: Trả về response thành công với auth tokens const response: ApiResponse = { success: true, data: result, - message: 'User registered successfully', + message: 'User registered successfully / Người dùng đã đăng ký thành công', timestamp: new Date().toISOString(), }; res.status(201).json(response); } catch (error: any) { + // EN: Return error response for registration failure + // VI: Trả về response lỗi cho trường hợp đăng ký thất bại res.status(400).json({ success: false, error: { code: 'AUTH_005', - message: error.message || 'Registration failed', + message: error.message || 'Registration failed / Đăng ký thất bại', }, timestamp: new Date().toISOString(), }); } }; + /** + * EN: Handle user login HTTP request + * VI: Xử lý yêu cầu HTTP đăng nhập người dùng + * + * @param req - Express request object / Đối tượng request của Express + * @param res - Express response object / Đối tượng response của Express + * @returns Promise + */ login = async (req: Request, res: Response): Promise => { try { + // EN: Validate and parse login credentials + // VI: Xác thực và parse thông tin đăng nhập const data = loginDtoSchema.parse(req.body); + + // EN: Authenticate user and generate tokens + // VI: Xác thực người dùng và tạo tokens const result = await this.authService.login(data); + // EN: Return success response with tokens + // VI: Trả về response thành công với tokens const response: ApiResponse = { success: true, data: result, - message: 'Login successful', + message: 'Login successful / Đăng nhập thành công', timestamp: new Date().toISOString(), }; res.json(response); } catch (error: any) { + // EN: Return unauthorized error for login failure + // VI: Trả về lỗi không được phép cho trường hợp đăng nhập thất bại res.status(401).json({ success: false, error: { code: 'AUTH_006', - message: error.message || 'Login failed', + message: error.message || 'Login failed / Đăng nhập thất bại', }, timestamp: new Date().toISOString(), }); } }; + /** + * EN: Handle token refresh HTTP request + * VI: Xử lý yêu cầu HTTP làm mới token + * + * @param req - Express request object / Đối tượng request của Express + * @param res - Express response object / Đối tượng response của Express + * @returns Promise + */ refresh = async (req: Request, res: Response): Promise => { try { + // EN: Validate refresh token data + // VI: Xác thực dữ liệu refresh token const data = refreshTokenDtoSchema.parse(req.body); + + // EN: Generate new access token + // VI: Tạo access token mới const result = await this.authService.refreshToken(data); + // EN: Return success response with new access token + // VI: Trả về response thành công với access token mới const response: ApiResponse<{ accessToken: string }> = { success: true, data: result, - message: 'Token refreshed successfully', + message: 'Token refreshed successfully / Token đã được làm mới thành công', timestamp: new Date().toISOString(), }; res.json(response); } catch (error: any) { + // EN: Return unauthorized error for invalid refresh token + // VI: Trả về lỗi không được phép cho refresh token không hợp lệ res.status(401).json({ success: false, error: { code: 'AUTH_007', - message: error.message || 'Token refresh failed', + message: error.message || 'Token refresh failed / Làm mới token thất bại', }, timestamp: new Date().toISOString(), }); } }; + /** + * EN: Handle user logout HTTP request (requires authentication) + * VI: Xử lý yêu cầu HTTP đăng xuất người dùng (yêu cầu xác thực) + * + * @param req - Authenticated request with user info / Request đã xác thực với thông tin người dùng + * @param res - Express response object / Đối tượng response của Express + * @returns Promise + */ logout = async (req: AuthRequest, res: Response): Promise => { try { + // EN: Get optional refresh token from request body + // VI: Lấy refresh token tùy chọn từ request body const refreshToken = req.body.refreshToken; + + // EN: Logout user and invalidate tokens + // VI: Đăng xuất người dùng và làm mất hiệu lực tokens await this.authService.logout(req.user!.userId, refreshToken); + // EN: Return success response + // VI: Trả về response thành công const response: ApiResponse = { success: true, - message: 'Logout successful', + message: 'Logout successful / Đăng xuất thành công', timestamp: new Date().toISOString(), }; res.json(response); } catch (error: any) { + // EN: Return server error for logout failure + // VI: Trả về lỗi server cho trường hợp đăng xuất thất bại res.status(500).json({ success: false, error: { code: 'AUTH_008', - message: error.message || 'Logout failed', + message: error.message || 'Logout failed / Đăng xuất thất bại', }, timestamp: new Date().toISOString(), }); } }; + /** + * EN: Handle password change HTTP request (requires authentication) + * VI: Xử lý yêu cầu HTTP đổi mật khẩu (yêu cầu xác thực) + * + * @param req - Authenticated request with user info / Request đã xác thực với thông tin người dùng + * @param res - Express response object / Đối tượng response của Express + * @returns Promise + */ changePassword = async (req: AuthRequest, res: Response): Promise => { try { + // EN: Validate password change data + // VI: Xác thực dữ liệu đổi mật khẩu const data = changePasswordDtoSchema.parse(req.body); + + // EN: Change user password + // VI: Thay đổi mật khẩu người dùng await this.authService.changePassword(req.user!.userId, data); + // EN: Return success response + // VI: Trả về response thành công const response: ApiResponse = { success: true, - message: 'Password changed successfully', + message: 'Password changed successfully / Mật khẩu đã được thay đổi thành công', timestamp: new Date().toISOString(), }; res.json(response); } catch (error: any) { + // EN: Return bad request error for password change failure + // VI: Trả về lỗi bad request cho trường hợp đổi mật khẩu thất bại res.status(400).json({ success: false, error: { code: 'AUTH_009', - message: error.message || 'Password change failed', + message: error.message || 'Password change failed / Đổi mật khẩu thất bại', }, timestamp: new Date().toISOString(), }); diff --git a/services/auth-service/src/modules/auth/auth.dto.ts b/services/auth-service/src/modules/auth/auth.dto.ts index e703cf7b..15806027 100644 --- a/services/auth-service/src/modules/auth/auth.dto.ts +++ b/services/auth-service/src/modules/auth/auth.dto.ts @@ -1,43 +1,85 @@ import { z } from 'zod'; +/** + * EN: Zod schema for user login validation + * VI: Schema Zod để xác thực đăng nhập người dùng + */ export const loginDtoSchema = z.object({ - email: z.string().email('Invalid email format'), - password: z.string().min(6, 'Password must be at least 6 characters'), + /** EN: User email address / VI: Địa chỉ email người dùng */ + email: z.string().email('Invalid email format / Định dạng email không hợp lệ'), + /** EN: User password / VI: Mật khẩu người dùng */ + password: z.string().min(6, 'Password must be at least 6 characters / Mật khẩu phải có ít nhất 6 ký tự'), }); +/** + * EN: Zod schema for user registration validation + * VI: Schema Zod để xác thực đăng ký người dùng + */ export const registerDtoSchema = z.object({ - email: z.string().email('Invalid email format'), - password: z.string().min(6, 'Password must be at least 6 characters'), + /** EN: User email address / VI: Địa chỉ email người dùng */ + email: z.string().email('Invalid email format / Định dạng email không hợp lệ'), + /** EN: User password / VI: Mật khẩu người dùng */ + password: z.string().min(6, 'Password must be at least 6 characters / Mật khẩu phải có ít nhất 6 ký tự'), + /** EN: Password confirmation / VI: Xác nhận mật khẩu */ confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { - message: "Passwords don't match", + message: "Passwords don't match / Mật khẩu không khớp", path: ['confirmPassword'], }); +/** + * EN: Zod schema for refresh token validation + * VI: Schema Zod để xác thực refresh token + */ export const refreshTokenDtoSchema = z.object({ + /** EN: JWT refresh token / VI: JWT refresh token */ refreshToken: z.string(), }); +/** + * EN: Zod schema for password change validation + * VI: Schema Zod để xác thực đổi mật khẩu + */ export const changePasswordDtoSchema = z.object({ + /** EN: Current password for verification / VI: Mật khẩu hiện tại để xác minh */ currentPassword: z.string(), - newPassword: z.string().min(6, 'Password must be at least 6 characters'), + /** EN: New password / VI: Mật khẩu mới */ + newPassword: z.string().min(6, 'Password must be at least 6 characters / Mật khẩu phải có ít nhất 6 ký tự'), }); +/** + * EN: Zod schema for forgot password validation + * VI: Schema Zod để xác thực quên mật khẩu + */ export const forgotPasswordDtoSchema = z.object({ - email: z.string().email('Invalid email format'), + /** EN: User email for password reset / VI: Email người dùng để reset mật khẩu */ + email: z.string().email('Invalid email format / Định dạng email không hợp lệ'), }); +/** + * EN: Zod schema for password reset validation + * VI: Schema Zod để xác thực reset mật khẩu + */ export const resetPasswordDtoSchema = z.object({ + /** EN: Password reset token / VI: Token reset mật khẩu */ token: z.string(), - newPassword: z.string().min(6, 'Password must be at least 6 characters'), + /** EN: New password / VI: Mật khẩu mới */ + newPassword: z.string().min(6, 'Password must be at least 6 characters / Mật khẩu phải có ít nhất 6 ký tự'), }); +/** EN: TypeScript type inferred from login schema / VI: Type TypeScript suy ra từ login schema */ export type LoginDto = z.infer; +/** EN: TypeScript type inferred from register schema / VI: Type TypeScript suy ra từ register schema */ export type RegisterDto = z.infer; +/** EN: TypeScript type inferred from refresh token schema / VI: Type TypeScript suy ra từ refresh token schema */ export type RefreshTokenDto = z.infer; +/** EN: TypeScript type inferred from change password schema / VI: Type TypeScript suy ra từ change password schema */ export type ChangePasswordDto = z.infer; +/** EN: TypeScript type inferred from forgot password schema / VI: Type TypeScript suy ra từ forgot password schema */ export type ForgotPasswordDto = z.infer; +/** EN: TypeScript type inferred from reset password schema / VI: Type TypeScript suy ra từ reset password schema */ export type ResetPasswordDto = z.infer; -// Re-export AuthResponse from @goodgo/types +// EN: Re-export AuthResponse from shared types +// VI: Re-export AuthResponse từ shared types export { AuthResponse } from '@goodgo/types'; diff --git a/services/auth-service/src/modules/auth/auth.service.ts b/services/auth-service/src/modules/auth/auth.service.ts index e6b13d19..2fff607f 100644 --- a/services/auth-service/src/modules/auth/auth.service.ts +++ b/services/auth-service/src/modules/auth/auth.service.ts @@ -11,18 +11,36 @@ import { } from './auth.dto'; import { prisma } from '../../config/database.config'; +/** + * EN: Authentication service handling user registration, login, logout, and token management + * VI: Service xác thực xử lý đăng ký, đăng nhập, đăng xuất người dùng và quản lý token + */ export class AuthService { + /** + * EN: Register a new user account with email and password + * VI: Đăng ký tài khoản người dùng mới với email và mật khẩu + * + * @param data - User registration data / Dữ liệu đăng ký người dùng + * @returns Authentication response with tokens and user info / Phản hồi xác thực với token và thông tin người dùng + * @throws Error if user already exists / Lỗi nếu người dùng đã tồn tại + */ async register(data: RegisterDto): Promise { + // EN: Check if user already exists to prevent duplicates + // VI: Kiểm tra người dùng đã tồn tại để tránh trùng lặp const existingUser = await prisma.user.findUnique({ where: { email: data.email }, }); if (existingUser) { - throw new Error('User already exists'); + throw new Error('User already exists / Người dùng đã tồn tại'); } + // EN: Hash password for security before storing + // VI: Mã hóa mật khẩu để bảo mật trước khi lưu trữ const passwordHash = await bcrypt.hash(data.password, 10); + // EN: Create new user with default USER role + // VI: Tạo người dùng mới với vai trò USER mặc định const user = await prisma.user.create({ data: { email: data.email, @@ -31,6 +49,8 @@ export class AuthService { }, }); + // EN: Generate access token (short-lived, 15 minutes) + // VI: Tạo access token (sống ngắn, 15 phút) const accessToken = createToken( { userId: user.id, @@ -41,6 +61,8 @@ export class AuthService { jwtConfig.expiresIn ); + // EN: Generate refresh token (long-lived, 7 days) + // VI: Tạo refresh token (sống dài, 7 ngày) const refreshToken = createToken( { userId: user.id, @@ -51,6 +73,8 @@ export class AuthService { jwtConfig.refreshExpiresIn ); + // EN: Store refresh token in database for later validation + // VI: Lưu refresh token trong database để xác thực sau này await prisma.refreshToken.create({ data: { userId: user.id, @@ -59,7 +83,7 @@ export class AuthService { }, }); - logger.info('User registered', { userId: user.id, email: user.email }); + logger.info('User registered / Người dùng đã đăng ký', { userId: user.id, email: user.email }); return { accessToken, @@ -75,21 +99,37 @@ export class AuthService { }; } + /** + * EN: Authenticate user with email and password + * VI: Xác thực người dùng với email và mật khẩu + * + * @param data - User login credentials / Thông tin đăng nhập của người dùng + * @returns Authentication response with tokens and user info / Phản hồi xác thực với token và thông tin người dùng + * @throws Error if credentials are invalid or user is inactive / Lỗi nếu thông tin đăng nhập không hợp lệ hoặc người dùng không hoạt động + */ async login(data: LoginDto): Promise { + // EN: Find user by email address + // VI: Tìm người dùng theo địa chỉ email const user = await prisma.user.findUnique({ where: { email: data.email }, }); + // EN: Check if user exists and is active + // VI: Kiểm tra người dùng có tồn tại và đang hoạt động if (!user || !user.isActive) { - throw new Error('Invalid credentials'); + throw new Error('Invalid credentials / Thông tin đăng nhập không hợp lệ'); } + // EN: Verify password against stored hash + // VI: Xác minh mật khẩu với hash đã lưu trữ const isValidPassword = await bcrypt.compare(data.password, user.passwordHash); if (!isValidPassword) { - throw new Error('Invalid credentials'); + throw new Error('Invalid credentials / Thông tin đăng nhập không hợp lệ'); } + // EN: Generate new access token for the authenticated user + // VI: Tạo access token mới cho người dùng đã xác thực const accessToken = createToken( { userId: user.id, @@ -100,6 +140,8 @@ export class AuthService { jwtConfig.expiresIn ); + // EN: Generate new refresh token for prolonged sessions + // VI: Tạo refresh token mới cho phiên dài hạn const refreshToken = createToken( { userId: user.id, @@ -110,6 +152,8 @@ export class AuthService { jwtConfig.refreshExpiresIn ); + // EN: Store refresh token in database for session management + // VI: Lưu refresh token trong database để quản lý phiên await prisma.refreshToken.create({ data: { userId: user.id, @@ -118,7 +162,7 @@ export class AuthService { }, }); - logger.info('User logged in', { userId: user.id, email: user.email }); + logger.info('User logged in / Người dùng đã đăng nhập', { userId: user.id, email: user.email }); return { accessToken, @@ -134,16 +178,30 @@ export class AuthService { }; } + /** + * EN: Generate new access token using valid refresh token + * VI: Tạo access token mới bằng refresh token hợp lệ + * + * @param data - Refresh token data / Dữ liệu refresh token + * @returns New access token / Access token mới + * @throws Error if refresh token is invalid or expired / Lỗi nếu refresh token không hợp lệ hoặc hết hạn + */ async refreshToken(data: RefreshTokenDto): Promise<{ accessToken: string }> { + // EN: Find refresh token record in database with user data + // VI: Tìm bản ghi refresh token trong database với dữ liệu người dùng const refreshTokenRecord = await prisma.refreshToken.findUnique({ where: { token: data.refreshToken }, include: { user: true }, }); + // EN: Validate refresh token exists and hasn't expired + // VI: Xác thực refresh token tồn tại và chưa hết hạn if (!refreshTokenRecord || refreshTokenRecord.expiresAt < new Date()) { - throw new Error('Invalid or expired refresh token'); + throw new Error('Invalid or expired refresh token / Refresh token không hợp lệ hoặc hết hạn'); } + // EN: Generate new access token with current user data + // VI: Tạo access token mới với dữ liệu người dùng hiện tại const accessToken = createToken( { userId: refreshTokenRecord.user.id, @@ -157,8 +215,19 @@ export class AuthService { return { accessToken }; } + /** + * EN: Logout user by invalidating refresh tokens + * VI: Đăng xuất người dùng bằng cách làm mất hiệu lực refresh tokens + * + * @param userId - User ID to logout / ID người dùng cần đăng xuất + * @param refreshToken - Specific refresh token to revoke (optional) / Refresh token cụ thể cần thu hồi (tùy chọn) + */ async logout(userId: string, refreshToken?: string): Promise { + // EN: Delete specific refresh token or all tokens for user + // VI: Xóa refresh token cụ thể hoặc tất cả tokens của người dùng if (refreshToken) { + // EN: Revoke specific refresh token (single device logout) + // VI: Thu hồi refresh token cụ thể (đăng xuất trên một thiết bị) await prisma.refreshToken.deleteMany({ where: { userId, @@ -166,41 +235,60 @@ export class AuthService { }, }); } else { + // EN: Revoke all refresh tokens (logout from all devices) + // VI: Thu hồi tất cả refresh tokens (đăng xuất khỏi tất cả thiết bị) await prisma.refreshToken.deleteMany({ where: { userId }, }); } - logger.info('User logged out', { userId }); + logger.info('User logged out / Người dùng đã đăng xuất', { userId }); } + /** + * EN: Change user password after verifying current password + * VI: Thay đổi mật khẩu người dùng sau khi xác minh mật khẩu hiện tại + * + * @param userId - User ID requesting password change / ID người dùng yêu cầu đổi mật khẩu + * @param data - Password change data / Dữ liệu đổi mật khẩu + * @throws Error if user not found or current password is incorrect / Lỗi nếu không tìm thấy người dùng hoặc mật khẩu hiện tại không đúng + */ async changePassword(userId: string, data: ChangePasswordDto): Promise { + // EN: Verify user exists + // VI: Xác minh người dùng tồn tại const user = await prisma.user.findUnique({ where: { id: userId }, }); if (!user) { - throw new Error('User not found'); + throw new Error('User not found / Không tìm thấy người dùng'); } + // EN: Verify current password is correct + // VI: Xác minh mật khẩu hiện tại là đúng const isValidPassword = await bcrypt.compare(data.currentPassword, user.passwordHash); if (!isValidPassword) { - throw new Error('Current password is incorrect'); + throw new Error('Current password is incorrect / Mật khẩu hiện tại không đúng'); } + // EN: Hash new password before storing + // VI: Mã hóa mật khẩu mới trước khi lưu trữ const newPasswordHash = await bcrypt.hash(data.newPassword, 10); + // EN: Update user password in database + // VI: Cập nhật mật khẩu người dùng trong database await prisma.user.update({ where: { id: userId }, data: { passwordHash: newPasswordHash }, }); - // Invalidate all refresh tokens + // EN: Invalidate all refresh tokens for security + // VI: Làm mất hiệu lực tất cả refresh tokens để bảo mật await prisma.refreshToken.deleteMany({ where: { userId }, }); - logger.info('Password changed', { userId }); + logger.info('Password changed / Mật khẩu đã được thay đổi', { userId }); } } diff --git a/services/auth-service/src/modules/health/health.controller.ts b/services/auth-service/src/modules/health/health.controller.ts index 1b08ffa7..a23f8d07 100644 --- a/services/auth-service/src/modules/health/health.controller.ts +++ b/services/auth-service/src/modules/health/health.controller.ts @@ -2,7 +2,18 @@ import { Request, Response } from 'express'; import { prisma } from '../../config/database.config'; import { ApiResponse } from '@goodgo/types'; +/** + * EN: Health check controller for monitoring service status + * VI: Controller kiểm tra sức khỏe để monitor trạng thái service + */ export class HealthController { + /** + * EN: Basic health check endpoint + * VI: Endpoint kiểm tra sức khỏe cơ bản + * + * @param _req - Express request (unused) / Request Express (không sử dụng) + * @param res - Express response / Response Express + */ health = async (_req: Request, res: Response): Promise => { const response: ApiResponse<{ status: string; timestamp: string }> = { success: true, @@ -16,8 +27,17 @@ export class HealthController { res.json(response); }; + /** + * EN: Readiness check - verifies database connectivity + * VI: Kiểm tra readiness - xác minh kết nối database + * + * @param _req - Express request (unused) / Request Express (không sử dụng) + * @param res - Express response / Response Express + */ ready = async (_req: Request, res: Response): Promise => { try { + // EN: Test database connectivity with simple query + // VI: Test kết nối database với query đơn giản await prisma.$queryRaw`SELECT 1`; res.json({ success: true, @@ -25,17 +45,26 @@ export class HealthController { timestamp: new Date().toISOString(), }); } catch (error) { + // EN: Return 503 if database is not accessible + // VI: Trả về 503 nếu database không thể truy cập res.status(503).json({ success: false, error: { code: 'HEALTH_001', - message: 'Service not ready', + message: 'Service not ready / Service chưa sẵn sàng', }, timestamp: new Date().toISOString(), }); } }; + /** + * EN: Liveness check - verifies service is running + * VI: Kiểm tra liveness - xác minh service đang chạy + * + * @param _req - Express request (unused) / Request Express (không sử dụng) + * @param res - Express response / Response Express + */ live = async (_req: Request, res: Response): Promise => { res.json({ success: true, diff --git a/services/auth-service/src/routes/index.ts b/services/auth-service/src/routes/index.ts index 519cfaa0..7b0ca51f 100644 --- a/services/auth-service/src/routes/index.ts +++ b/services/auth-service/src/routes/index.ts @@ -3,18 +3,30 @@ import { createAuthRouter } from '../modules/auth/auth.module'; import { createUserRouter } from '../modules/user/user.module'; import { HealthController } from '../modules/health/health.controller'; +/** + * EN: Create and configure main application router + * VI: Tạo và cấu hình router chính của ứng dụng + * + * @returns Configured Express router / Router Express đã cấu hình + */ export const createRouter = (): Router => { + // EN: Create new Express router instance + // VI: Tạo instance router Express mới const router = Router(); const healthController = new HealthController(); + // EN: Get API version from environment + // VI: Lấy phiên bản API từ environment const apiVersion = process.env.API_VERSION || 'v1'; - // Health checks + // EN: Health check endpoints for monitoring + // VI: Endpoints kiểm tra sức khỏe cho monitoring router.get('/health', healthController.health); router.get('/health/ready', healthController.ready); router.get('/health/live', healthController.live); - // API routes + // EN: Mount API routes with version prefix + // VI: Mount routes API với tiền tố version router.use(`/api/${apiVersion}/auth`, createAuthRouter()); router.use(`/api/${apiVersion}/users`, createUserRouter());