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:
Ho Ngoc Hai
2025-12-27 01:37:59 +07:00
parent 4da46b5b8e
commit 526f376c5f
24 changed files with 883 additions and 89 deletions

View File

@@ -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',
},
};

View File

@@ -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',
},
};

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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'],
};

View File

@@ -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');
};

View File

@@ -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'],
};

View File

@@ -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');
};

View File

@@ -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!');
}

View File

@@ -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);
});

View File

@@ -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(),
});

View File

@@ -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(),
};

View File

@@ -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,

View File

@@ -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(),
});

View File

@@ -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';

View File

@@ -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 });
}
}

View File

@@ -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,

View File

@@ -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());