migration to typescript

This commit is contained in:
JakkrapartXD 2026-01-09 06:28:15 +00:00
parent 924000b084
commit 9fde77468a
41 changed files with 11952 additions and 10164 deletions

View file

@ -1,91 +0,0 @@
const { verifyToken, extractToken } = require('../utils/jwt');
const prisma = require('../config/database');
const logger = require('../config/logger');
/**
* Authentication middleware
* Verifies JWT token and attaches user to request
*/
async function authenticate(req, res, next) {
try {
const token = extractToken(req.headers.authorization);
if (!token) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Authentication required',
},
});
}
const decoded = verifyToken(token);
// Fetch user from database
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
include: { role: true },
});
if (!user || !user.is_active) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Invalid or inactive user',
},
});
}
// Attach user to request
req.user = {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
};
next();
} catch (error) {
logger.error('Authentication error:', error.message);
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Invalid or expired token',
},
});
}
}
/**
* Authorization middleware
* Checks if user has required role
* @param {string[]} allowedRoles - Array of allowed role codes
*/
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Authentication required',
},
});
}
if (!allowedRoles.includes(req.user.role.code)) {
return res.status(403).json({
error: {
code: 'FORBIDDEN',
message: 'Insufficient permissions',
},
});
}
next();
};
}
module.exports = {
authenticate,
authorize,
};

View file

@ -0,0 +1,92 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import { config } from '../config';
import { JWTPayload } from '../types';
import { logger } from '../config/logger';
export interface AuthRequest extends Request {
user?: JWTPayload;
}
export async function expressAuthentication(
request: Request,
securityName: string,
scopes?: string[]
): Promise<JWTPayload> {
if (securityName === 'jwt') {
const token = request.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new Error('No token provided');
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as JWTPayload;
// Check if user has required role
if (scopes && scopes.length > 0) {
if (!scopes.includes(decoded.roleCode)) {
throw new Error('Insufficient permissions');
}
}
return decoded;
} catch (error) {
logger.error('JWT verification failed', { error });
throw new Error('Invalid token');
}
}
throw new Error('Unknown security name');
}
export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'No token provided'
}
});
}
try {
const decoded = jwt.verify(token, config.jwt.secret) as JWTPayload;
req.user = decoded;
next();
} catch (error) {
logger.error('Authentication failed', { error });
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Invalid token'
}
});
}
}
export function authorize(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Not authenticated'
}
});
}
if (roles.length > 0 && !roles.includes(req.user.roleCode)) {
return res.status(403).json({
error: {
code: 'FORBIDDEN',
message: 'Insufficient permissions'
}
});
}
next();
};
}

View file

@ -1,85 +0,0 @@
const logger = require('../config/logger');
/**
* Global error handler middleware
*/
function errorHandler(err, req, res, next) {
logger.error('Error:', {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
});
// Prisma errors
if (err.code && err.code.startsWith('P')) {
if (err.code === 'P2002') {
return res.status(409).json({
error: {
code: 'DUPLICATE_ENTRY',
message: 'A record with this value already exists',
field: err.meta?.target?.[0],
},
});
}
if (err.code === 'P2025') {
return res.status(404).json({
error: {
code: 'NOT_FOUND',
message: 'Record not found',
},
});
}
}
// Validation errors
if (err.isJoi || err.name === 'ValidationError') {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: err.message,
details: err.details?.map((d) => ({
field: d.path.join('.'),
message: d.message,
})),
},
});
}
// JWT errors
if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Invalid or expired token',
},
});
}
// Default error
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'An unexpected error occurred',
},
});
}
/**
* 404 Not Found handler
*/
function notFoundHandler(req, res) {
res.status(404).json({
error: {
code: 'NOT_FOUND',
message: `Route ${req.method} ${req.url} not found`,
},
});
}
module.exports = {
errorHandler,
notFoundHandler,
};

View file

@ -0,0 +1,132 @@
import { Request, Response, NextFunction } from 'express';
import { ValidateError } from 'tsoa';
import { logger } from '../config/logger';
export function errorHandler(
err: any,
req: Request,
res: Response,
next: NextFunction
): Response | void {
// TSOA Validation Error
if (err instanceof ValidateError) {
logger.warn('Validation error', {
fields: err.fields,
path: req.path
});
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: err.fields
}
});
}
// JWT Errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Invalid token'
}
});
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: {
code: 'TOKEN_EXPIRED',
message: 'Token has expired'
}
});
}
// Prisma Errors
if (err.code === 'P2002') {
return res.status(409).json({
error: {
code: 'DUPLICATE_ENTRY',
message: 'A record with this value already exists',
details: err.meta
}
});
}
if (err.code === 'P2025') {
return res.status(404).json({
error: {
code: 'NOT_FOUND',
message: 'Record not found'
}
});
}
// Custom Application Errors
if (err.statusCode) {
return res.status(err.statusCode).json({
error: {
code: err.code || 'APPLICATION_ERROR',
message: err.message,
details: err.details
}
});
}
// Log unexpected errors
logger.error('Unexpected error', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
// Default error
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred'
}
});
}
export class ApplicationError extends Error {
constructor(
public statusCode: number,
public code: string,
message: string,
public details?: any
) {
super(message);
this.name = 'ApplicationError';
}
}
export class ValidationError extends ApplicationError {
constructor(message: string, details?: any) {
super(400, 'VALIDATION_ERROR', message, details);
this.name = 'ValidationError';
}
}
export class UnauthorizedError extends ApplicationError {
constructor(message = 'Unauthorized') {
super(401, 'UNAUTHORIZED', message);
this.name = 'UnauthorizedError';
}
}
export class ForbiddenError extends ApplicationError {
constructor(message = 'Forbidden') {
super(403, 'FORBIDDEN', message);
this.name = 'ForbiddenError';
}
}
export class NotFoundError extends ApplicationError {
constructor(message = 'Resource not found') {
super(404, 'NOT_FOUND', message);
this.name = 'NotFoundError';
}
}