292 lines
9.0 KiB
TypeScript
292 lines
9.0 KiB
TypeScript
import { logger } from '@goodgo/logger';
|
|
import { ApiResponse } from '@goodgo/types';
|
|
import { Request, Response, NextFunction } from 'express';
|
|
|
|
import { jwtService } from '../modules/token/jwt.service';
|
|
import { getErrorMessage } from '../utils/error-utils';
|
|
|
|
/**
|
|
* EN: Extended Request interface with user information
|
|
* VI: Interface Request mở rộng với thông tin người dùng
|
|
*/
|
|
declare global {
|
|
|
|
namespace Express {
|
|
interface Request {
|
|
user?: {
|
|
id?: string;
|
|
userId?: string;
|
|
sub?: string;
|
|
email: string;
|
|
role?: string;
|
|
roles?: string[];
|
|
permissions?: string[];
|
|
iat?: number;
|
|
exp?: number;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* EN: Authentication middleware - verifies JWT tokens
|
|
* VI: Middleware xác thực - xác minh JWT tokens
|
|
*
|
|
* @param options - Configuration options / Tùy chọn cấu hình
|
|
*/
|
|
export const authenticate = (_options: {
|
|
secret?: string;
|
|
ignoreExpiration?: boolean;
|
|
} = {}) => {
|
|
return async (req: Request, res: Response, next: NextFunction) => {
|
|
try {
|
|
// EN: Extract token from Authorization header or cookie
|
|
// VI: Trích xuất token từ header Authorization hoặc cookie
|
|
let token: string | null = null;
|
|
|
|
// Try Authorization header first
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
token = authHeader.substring(7);
|
|
}
|
|
|
|
// Try cookie if no header token
|
|
if (!token && req.cookies) {
|
|
token = req.cookies.access_token || null;
|
|
}
|
|
|
|
if (!token) {
|
|
logger.warn('No authentication token provided / Không có token xác thực được cung cấp', {
|
|
path: req.path,
|
|
method: req.method,
|
|
});
|
|
|
|
const response: ApiResponse = {
|
|
success: false,
|
|
error: {
|
|
code: 'AUTH_001',
|
|
message: 'Authentication required / Yêu cầu xác thực',
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
res.status(401).json(response);
|
|
return;
|
|
}
|
|
|
|
// EN: Verify token using JWT service
|
|
// VI: Xác minh token sử dụng JWT service
|
|
const payload = await jwtService.verifyAccessToken(token);
|
|
|
|
// EN: Attach user information to request
|
|
// VI: Gắn thông tin người dùng vào request
|
|
req.user = {
|
|
id: payload.sub,
|
|
userId: payload.sub,
|
|
sub: payload.sub,
|
|
email: payload.email,
|
|
role: payload.roles?.[0], // Primary role
|
|
roles: payload.roles,
|
|
permissions: payload.permissions,
|
|
iat: payload.iat,
|
|
exp: payload.exp,
|
|
};
|
|
|
|
logger.debug('User authenticated successfully / Người dùng đã được xác thực thành công', {
|
|
userId: payload.sub,
|
|
email: payload.email,
|
|
roles: payload.roles,
|
|
});
|
|
|
|
next();
|
|
} catch (error: unknown) {
|
|
logger.warn('Authentication failed / Xác thực thất bại', {
|
|
error: getErrorMessage(error),
|
|
path: req.path,
|
|
method: req.method,
|
|
});
|
|
|
|
const response: ApiResponse = {
|
|
success: false,
|
|
error: {
|
|
code: 'AUTH_002',
|
|
message: 'Invalid or expired token / Token không hợp lệ hoặc hết hạn',
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
res.status(401).json(response);
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* EN: Role-based authorization middleware
|
|
* VI: Middleware phân quyền dựa trên vai trò
|
|
*
|
|
* @param allowedRoles - Array of roles that can access the resource / Mảng các vai trò được phép truy cập tài nguyên
|
|
*/
|
|
export const authorize = (...allowedRoles: string[]) => {
|
|
return (req: Request, res: Response, next: NextFunction) => {
|
|
// EN: Check if user is authenticated
|
|
// VI: Kiểm tra người dùng đã được xác thực chưa
|
|
if (!req.user) {
|
|
logger.warn('Authorization attempted without authentication / Phân quyền được thử mà không xác thực', {
|
|
path: req.path,
|
|
method: req.method,
|
|
});
|
|
|
|
const response: ApiResponse = {
|
|
success: false,
|
|
error: {
|
|
code: 'AUTH_003',
|
|
message: 'Authentication required / Yêu cầu xác thực',
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
res.status(401).json(response);
|
|
return;
|
|
}
|
|
|
|
// EN: Check if user has required role
|
|
// VI: Kiểm tra người dùng có vai trò cần thiết không
|
|
const userRoles = req.user.roles || (req.user.role ? [req.user.role] : []);
|
|
const hasRole = userRoles.some(role => allowedRoles.includes(role));
|
|
|
|
if (!hasRole) {
|
|
logger.warn('Access denied - insufficient permissions / Truy cập bị từ chối - không đủ quyền', {
|
|
userId: req.user.userId || req.user.id,
|
|
userRoles,
|
|
requiredRoles: allowedRoles,
|
|
path: req.path,
|
|
method: req.method,
|
|
});
|
|
|
|
const response: ApiResponse = {
|
|
success: false,
|
|
error: {
|
|
code: 'AUTH_004',
|
|
message: 'Insufficient permissions / Không đủ quyền',
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
};
|
|
res.status(403).json(response);
|
|
return;
|
|
}
|
|
|
|
logger.debug('Authorization successful / Phân quyền thành công', {
|
|
userId: req.user.userId || req.user.id,
|
|
userRoles,
|
|
path: req.path,
|
|
method: req.method,
|
|
});
|
|
|
|
next();
|
|
};
|
|
};
|
|
|
|
/**
|
|
* EN: Combined auth and authorization middleware
|
|
* VI: Middleware kết hợp xác thực và phân quyền
|
|
*
|
|
* @param secret - JWT secret key / Khóa bí mật JWT
|
|
* @param allowedRoles - Array of allowed roles / Mảng vai trò được phép
|
|
*/
|
|
export const requireAuth = (
|
|
secret?: string,
|
|
...allowedRoles: string[]
|
|
) => {
|
|
return [authenticate({ secret }), authorize(...allowedRoles)];
|
|
};
|
|
|
|
/**
|
|
* EN: Optional authentication middleware - doesn't fail if no token provided
|
|
* VI: Middleware xác thực tùy chọn - không thất bại nếu không có token
|
|
*
|
|
* @param options - Configuration options / Tùy chọn cấu hình
|
|
*/
|
|
export const optionalAuth = (_options: {
|
|
secret?: string;
|
|
ignoreExpiration?: boolean;
|
|
} = {}) => {
|
|
return async (req: Request, _res: Response, next: NextFunction) => {
|
|
try {
|
|
let token: string | null = null;
|
|
|
|
const authHeader = req.headers.authorization;
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
token = authHeader.substring(7);
|
|
}
|
|
|
|
if (!token && req.cookies) {
|
|
token = req.cookies.access_token || null;
|
|
}
|
|
|
|
if (token) {
|
|
const payload = await jwtService.verifyAccessToken(token);
|
|
req.user = {
|
|
id: payload.sub,
|
|
userId: payload.sub,
|
|
sub: payload.sub,
|
|
email: payload.email,
|
|
role: payload.roles?.[0],
|
|
roles: payload.roles,
|
|
permissions: payload.permissions,
|
|
iat: payload.iat,
|
|
exp: payload.exp,
|
|
};
|
|
|
|
logger.debug('Optional authentication successful / Xác thực tùy chọn thành công', {
|
|
userId: payload.sub,
|
|
});
|
|
}
|
|
|
|
next();
|
|
} catch (error: unknown) {
|
|
// EN: For optional auth, just continue without user info
|
|
// VI: Với optional auth, chỉ tiếp tục mà không có thông tin user
|
|
logger.debug('Optional authentication skipped / Xác thực tùy chọn bị bỏ qua', {
|
|
reason: getErrorMessage(error),
|
|
});
|
|
next();
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* EN: Check if user has specific role (utility function)
|
|
* VI: Kiểm tra người dùng có vai trò cụ thể (hàm tiện ích)
|
|
*
|
|
* @param user - User object from request / Đối tượng user từ request
|
|
* @param role - Role to check / Vai trò cần kiểm tra
|
|
* @returns True if user has the role / True nếu user có vai trò
|
|
*/
|
|
export const hasRole = (user: Express.Request['user'], role: string): boolean => {
|
|
if (!user) return false;
|
|
const userRoles = user.roles || (user.role ? [user.role] : []);
|
|
return userRoles.includes(role);
|
|
};
|
|
|
|
/**
|
|
* EN: Check if user has any of the specified roles (utility function)
|
|
* VI: Kiểm tra người dùng có bất kỳ vai trò nào trong danh sách (hàm tiện ích)
|
|
*
|
|
* @param user - User object from request / Đối tượng user từ request
|
|
* @param roles - Array of roles to check / Mảng vai trò cần kiểm tra
|
|
* @returns True if user has any of the roles / True nếu user có bất kỳ vai trò nào
|
|
*/
|
|
export const hasAnyRole = (user: Express.Request['user'], roles: string[]): boolean => {
|
|
if (!user) return false;
|
|
const userRoles = user.roles || (user.role ? [user.role] : []);
|
|
return userRoles.some(r => roles.includes(r));
|
|
};
|
|
|
|
/**
|
|
* EN: Check if user is authenticated (utility function)
|
|
* VI: Kiểm tra người dùng đã được xác thực (hàm tiện ích)
|
|
*
|
|
* @param user - User object from request / Đối tượng user từ request
|
|
* @returns True if user is authenticated / True nếu user đã được xác thực
|
|
*/
|
|
export const isAuthenticated = (user: Express.Request['user']): boolean => {
|
|
return !!user;
|
|
};
|