Enhance configuration and authentication features across services
- Updated `next.config.js` in both web-admin and web-client to enable React strict mode, set output to standalone, and expose environment variables for API URL. - Enhanced `auth-sdk` with detailed comments for JWT verification, decoding, and token management functions. - Improved `http-client` with comprehensive documentation for HTTP client configuration and methods. - Expanded `logger` functionality with detailed configuration options and logging formats. - Enhanced `tracing` setup with detailed comments for distributed tracing configuration. - Updated `types` definitions for authentication and user data transfer objects with comprehensive comments. - Improved `auth-service` with detailed comments in controllers, services, and middlewares for better clarity and maintainability. - Added health check endpoints in `health.controller.ts` for service monitoring. - Enhanced error handling and logging throughout the application for better debugging and user feedback.
This commit is contained in:
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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<TokenPayload, 'iat' | 'exp'>,
|
||||
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 <token>")
|
||||
// VI: Chia header thành các phần (mong đợi: "Bearer <token>")
|
||||
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,
|
||||
|
||||
@@ -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<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<ApiResponse>) => {
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError<ApiResponse>) => {
|
||||
// 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<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
const response = await this.client.get<ApiResponse<T>>(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<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
const response = await this.client.post<ApiResponse<T>>(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<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
const response = await this.client.put<ApiResponse<T>>(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<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
const response = await this.client.patch<ApiResponse<T>>(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<T = any>(url: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
const response = await this.client.delete<ApiResponse<T>>(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;
|
||||
|
||||
@@ -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<LoggerConfig> = {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
serviceName: process.env.SERVICE_NAME || 'microservice',
|
||||
@@ -16,11 +29,24 @@ const defaultConfig: Required<LoggerConfig> = {
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 <service-name> [dev|deploy]"
|
||||
echo "Example: $0 auth-service dev"
|
||||
echo "Example: $0 auth-service deploy"
|
||||
echo "Usage: $0 <service-name> [dev|deploy] / Cách dùng: $0 <tên-service> [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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
await prisma.$disconnect();
|
||||
logger.info('Database disconnected');
|
||||
logger.info('Database disconnected / Đã ngắt kết nối database');
|
||||
};
|
||||
|
||||
@@ -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'],
|
||||
};
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
await prisma.$disconnect();
|
||||
logger.info('Database disconnected');
|
||||
logger.info('Database disconnected / Đã ngắt kết nối database');
|
||||
};
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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<void> => {
|
||||
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(),
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<void>
|
||||
*/
|
||||
register = async (req: Request, res: Response): Promise<void> => {
|
||||
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<AuthResponse> = {
|
||||
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<void>
|
||||
*/
|
||||
login = async (req: Request, res: Response): Promise<void> => {
|
||||
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<AuthResponse> = {
|
||||
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<void>
|
||||
*/
|
||||
refresh = async (req: Request, res: Response): Promise<void> => {
|
||||
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<void>
|
||||
*/
|
||||
logout = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
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<void>
|
||||
*/
|
||||
changePassword = async (req: AuthRequest, res: Response): Promise<void> => {
|
||||
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(),
|
||||
});
|
||||
|
||||
@@ -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<typeof loginDtoSchema>;
|
||||
/** EN: TypeScript type inferred from register schema / VI: Type TypeScript suy ra từ register schema */
|
||||
export type RegisterDto = z.infer<typeof registerDtoSchema>;
|
||||
/** EN: TypeScript type inferred from refresh token schema / VI: Type TypeScript suy ra từ refresh token schema */
|
||||
export type RefreshTokenDto = z.infer<typeof refreshTokenDtoSchema>;
|
||||
/** EN: TypeScript type inferred from change password schema / VI: Type TypeScript suy ra từ change password schema */
|
||||
export type ChangePasswordDto = z.infer<typeof changePasswordDtoSchema>;
|
||||
/** EN: TypeScript type inferred from forgot password schema / VI: Type TypeScript suy ra từ forgot password schema */
|
||||
export type ForgotPasswordDto = z.infer<typeof forgotPasswordDtoSchema>;
|
||||
/** EN: TypeScript type inferred from reset password schema / VI: Type TypeScript suy ra từ reset password schema */
|
||||
export type ResetPasswordDto = z.infer<typeof resetPasswordDtoSchema>;
|
||||
|
||||
// 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';
|
||||
|
||||
@@ -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<AuthResponse> {
|
||||
// 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<AuthResponse> {
|
||||
// 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<void> {
|
||||
// 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<void> {
|
||||
// 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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> => {
|
||||
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<void> => {
|
||||
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<void> => {
|
||||
res.json({
|
||||
success: true,
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user