Audit table with better data
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthenticatedRequest } from '../middleware/auth.middleware';
|
import type { AuthenticatedRequest } from '../types';
|
||||||
import * as authService from '../services/auth.service';
|
import * as authService from '../services/auth.service';
|
||||||
import { LoginInput, RefreshInput } from '../validators/auth.validator';
|
import { LoginInput, RefreshInput } from '../validators/auth.validator';
|
||||||
import { createAuditLog, getIpAddress, getUserAgent } from '../services/audit.service';
|
import { createAuditLog, getIpAddress, getUserAgent } from '../services/audit.service';
|
||||||
@@ -106,7 +106,7 @@ export async function refresh(req: Request, res: Response): Promise<void> {
|
|||||||
*/
|
*/
|
||||||
export async function logout(req: AuthenticatedRequest, res: Response): Promise<void> {
|
export async function logout(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
res.status(401).json({ success: false, error: 'Authentication required' });
|
res.status(401).json({ success: false, error: 'Authentication required' });
|
||||||
@@ -121,12 +121,12 @@ export async function logout(req: AuthenticatedRequest, res: Response): Promise<
|
|||||||
|
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
createAuditLog({
|
createAuditLog({
|
||||||
userId: req.user.id,
|
userId: req.user.userId,
|
||||||
userEmail: req.user.email,
|
userEmail: req.user.email,
|
||||||
userName: req.user.role || req.user.email,
|
userName: req.user.roleName || req.user.email,
|
||||||
action: 'LOGOUT',
|
action: 'LOGOUT',
|
||||||
tableName: 'users',
|
tableName: 'users',
|
||||||
recordId: req.user.id,
|
recordId: req.user.userId,
|
||||||
description: `User logged out`,
|
description: `User logged out`,
|
||||||
ipAddress: getIpAddress(req),
|
ipAddress: getIpAddress(req),
|
||||||
userAgent: getUserAgent(req),
|
userAgent: getUserAgent(req),
|
||||||
@@ -150,7 +150,7 @@ export async function logout(req: AuthenticatedRequest, res: Response): Promise<
|
|||||||
*/
|
*/
|
||||||
export async function getMe(req: AuthenticatedRequest, res: Response): Promise<void> {
|
export async function getMe(req: AuthenticatedRequest, res: Response): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const userId = req.user?.id;
|
const userId = req.user?.userId;
|
||||||
|
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
res.status(401).json({ success: false, error: 'Authentication required' });
|
res.status(401).json({ success: false, error: 'Authentication required' });
|
||||||
|
|||||||
@@ -18,18 +18,141 @@ function methodToAction(method: string): AuditAction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTableName(path: string): string {
|
function extractSection(path: string): string {
|
||||||
const segments = path.replace(/^\/api\//, '').split('/');
|
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
|
||||||
let tableName = segments[0] || 'unknown';
|
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
const tableMapping: Record<string, string> = {
|
if (segments.length === 0) return 'general';
|
||||||
|
|
||||||
|
const sectionMapping: Record<string, string> = {
|
||||||
|
'auth': 'authentication',
|
||||||
|
'me': 'profile',
|
||||||
|
'users': 'user-management',
|
||||||
|
'roles': 'role-management',
|
||||||
|
'meters': 'meters',
|
||||||
|
'concentrators': 'concentrators',
|
||||||
|
'gateways': 'gateways',
|
||||||
|
'devices': 'devices',
|
||||||
|
'projects': 'projects',
|
||||||
|
'readings': 'readings',
|
||||||
|
'bulk-upload': 'bulk-operations',
|
||||||
|
'audit-logs': 'audit',
|
||||||
|
'webhooks': 'webhooks',
|
||||||
|
};
|
||||||
|
|
||||||
|
return sectionMapping[segments[0]] || segments[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTableName(path: string): string {
|
||||||
|
const cleanPath = path.replace(/^\/api\//, '').replace(/^\//, '');
|
||||||
|
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseTableMapping: Record<string, string> = {
|
||||||
'auth': 'users',
|
'auth': 'users',
|
||||||
|
'me': 'users',
|
||||||
|
'users': 'users',
|
||||||
|
'roles': 'roles',
|
||||||
|
'meters': 'meters',
|
||||||
|
'concentrators': 'concentrators',
|
||||||
|
'gateways': 'gateways',
|
||||||
|
'devices': 'devices',
|
||||||
|
'projects': 'projects',
|
||||||
|
'readings': 'readings',
|
||||||
'webhooks': 'webhooks',
|
'webhooks': 'webhooks',
|
||||||
'bulk-upload': 'bulk_operations',
|
'bulk-upload': 'bulk_operations',
|
||||||
'audit-logs': 'audit_logs',
|
'audit-logs': 'audit_logs',
|
||||||
};
|
};
|
||||||
|
|
||||||
return tableMapping[tableName] || tableName;
|
const operations = new Set([
|
||||||
|
'password',
|
||||||
|
'stats',
|
||||||
|
'health',
|
||||||
|
'template',
|
||||||
|
'summary',
|
||||||
|
'statistics',
|
||||||
|
'my-activity',
|
||||||
|
'record',
|
||||||
|
'refresh',
|
||||||
|
'logout',
|
||||||
|
'uplink',
|
||||||
|
'join',
|
||||||
|
'ack',
|
||||||
|
'dev-eui',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const nestedResourceMapping: Record<string, string> = {
|
||||||
|
'meters/readings': 'readings',
|
||||||
|
'concentrators/meters': 'meters',
|
||||||
|
'gateways/devices': 'devices',
|
||||||
|
'projects/meters': 'meters',
|
||||||
|
'projects/concentrators': 'concentrators',
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstSegment = segments[0];
|
||||||
|
const baseTable = baseTableMapping[firstSegment] || firstSegment;
|
||||||
|
|
||||||
|
if (segments.length === 1) {
|
||||||
|
return baseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondSegment = segments[1];
|
||||||
|
const thirdSegment = segments[2];
|
||||||
|
|
||||||
|
if (segments.length >= 3 && isUUID(secondSegment)) {
|
||||||
|
const nestedKey = `${firstSegment}/${thirdSegment}`;
|
||||||
|
|
||||||
|
if (nestedResourceMapping[nestedKey]) {
|
||||||
|
return nestedResourceMapping[nestedKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operations.has(thirdSegment)) {
|
||||||
|
return baseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (baseTableMapping[thirdSegment] || isPluralResourceName(thirdSegment)) {
|
||||||
|
return baseTableMapping[thirdSegment] || thirdSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstSegment === 'bulk-upload' && secondSegment) {
|
||||||
|
return baseTableMapping[secondSegment] || secondSegment;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstSegment === 'audit-logs') {
|
||||||
|
if (secondSegment === 'record' && segments.length >= 4) {
|
||||||
|
return segments[3];
|
||||||
|
}
|
||||||
|
return 'audit_logs';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstSegment === 'auth') {
|
||||||
|
return 'users';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstSegment === 'devices' && secondSegment === 'dev-eui') {
|
||||||
|
return 'devices';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segments.length === 2 && isUUID(secondSegment)) {
|
||||||
|
return baseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseTable;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUUID(str: string): boolean {
|
||||||
|
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
return uuidRegex.test(str) || /^\d+$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPluralResourceName(str: string): boolean {
|
||||||
|
return str.endsWith('s') || str.endsWith('ies') || str.includes('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractRecordId(req: Request): string | undefined {
|
function extractRecordId(req: Request): string | undefined {
|
||||||
@@ -54,74 +177,157 @@ function shouldExclude(path: string): boolean {
|
|||||||
return EXCLUDED_PATHS.some(excluded => path.startsWith(excluded));
|
return EXCLUDED_PATHS.some(excluded => path.startsWith(excluded));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateDescription(
|
||||||
|
fullPath: string,
|
||||||
|
action: AuditAction,
|
||||||
|
tableName: string,
|
||||||
|
recordId?: string,
|
||||||
|
section?: string
|
||||||
|
): string {
|
||||||
|
const actionDescriptions: Record<AuditAction, string> = {
|
||||||
|
'CREATE': 'Created',
|
||||||
|
'UPDATE': 'Updated',
|
||||||
|
'DELETE': 'Deleted',
|
||||||
|
'READ': 'Viewed',
|
||||||
|
'LOGIN': 'Logged in',
|
||||||
|
'LOGOUT': 'Logged out',
|
||||||
|
'EXPORT': 'Exported',
|
||||||
|
'BULK_UPLOAD': 'Bulk uploaded',
|
||||||
|
'STATUS_CHANGE': 'Changed status of',
|
||||||
|
'PERMISSION_CHANGE': 'Changed permissions for',
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableLabels: Record<string, string> = {
|
||||||
|
'users': 'user',
|
||||||
|
'roles': 'role',
|
||||||
|
'meters': 'meter',
|
||||||
|
'concentrators': 'concentrator',
|
||||||
|
'gateways': 'gateway',
|
||||||
|
'devices': 'device',
|
||||||
|
'projects': 'project',
|
||||||
|
'readings': 'reading',
|
||||||
|
'bulk_operations': 'bulk operation',
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionLabel = actionDescriptions[action] || action;
|
||||||
|
const tableLabel = tableLabels[tableName] || tableName;
|
||||||
|
|
||||||
|
if (fullPath === '/api/me' || fullPath === '/me' || fullPath.endsWith('/auth/me')) {
|
||||||
|
return 'Viewed own profile';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPath.includes('/users') && action === 'READ' && !recordId) {
|
||||||
|
return 'Viewed users list';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPath.includes('/meters') && action === 'READ' && !recordId) {
|
||||||
|
return 'Viewed meters list';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPath.includes('/concentrators') && action === 'READ' && !recordId) {
|
||||||
|
return 'Viewed concentrators list';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPath.includes('/projects') && action === 'READ' && !recordId) {
|
||||||
|
return 'Viewed projects list';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fullPath.includes('/readings') && action === 'READ' && !recordId) {
|
||||||
|
return 'Viewed readings list';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordId) {
|
||||||
|
if (tableName === 'unknown' && section) {
|
||||||
|
return `${actionLabel} record in ${section} (ID: ${recordId.substring(0, 8)}...)`;
|
||||||
|
}
|
||||||
|
return `${actionLabel} ${tableLabel} (ID: ${recordId.substring(0, 8)}...)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableName === 'unknown' && section) {
|
||||||
|
return `${actionLabel} in ${section} section`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${actionLabel} ${tableLabel}`;
|
||||||
|
}
|
||||||
|
|
||||||
export function auditMiddleware(
|
export function auditMiddleware(
|
||||||
req: AuthenticatedRequest,
|
req: AuthenticatedRequest,
|
||||||
res: Response,
|
res: Response,
|
||||||
next: NextFunction
|
next: NextFunction
|
||||||
): void {
|
): void {
|
||||||
console.log(`[AUDIT-MIDDLEWARE] Request received: ${req.method} ${req.path}`);
|
|
||||||
|
|
||||||
if (shouldExclude(req.path)) {
|
if (shouldExclude(req.path)) {
|
||||||
console.log(`[AUDIT-MIDDLEWARE] Path excluded: ${req.path}`);
|
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.path.startsWith('/api')) {
|
if (!req.path.startsWith('/api')) {
|
||||||
console.log(`[AUDIT-MIDDLEWARE] Not an API path: ${req.path}`);
|
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[AUDIT-MIDDLEWARE] Setting up audit tracking for: ${req.method} ${req.path}`);
|
|
||||||
|
|
||||||
const originalSend = res.send;
|
const originalSend = res.send;
|
||||||
const originalJson = res.json;
|
const originalJson = res.json;
|
||||||
let responseBody: any;
|
let responseBody: unknown;
|
||||||
|
|
||||||
res.send = function (body: any): Response {
|
res.send = function (body: unknown): Response {
|
||||||
responseBody = body;
|
responseBody = body;
|
||||||
return originalSend.call(this, body);
|
return originalSend.call(this, body);
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json = function (body: any): Response {
|
res.json = function (body: unknown): Response {
|
||||||
responseBody = body;
|
responseBody = body;
|
||||||
return originalJson.call(this, body);
|
return originalJson.call(this, body);
|
||||||
};
|
};
|
||||||
|
|
||||||
res.on('finish', async () => {
|
res.on('finish', async () => {
|
||||||
try {
|
try {
|
||||||
console.log('🔍 [Audit] Response finished:', {
|
if (!req.user || !req.user.userId || !req.user.email) {
|
||||||
path: req.path,
|
|
||||||
method: req.method,
|
|
||||||
user: req.user ? req.user.email : 'NO USER',
|
|
||||||
statusCode: res.statusCode
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!req.user) {
|
|
||||||
console.log('⚠️ [Audit] Skipping - no authenticated user');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const action = methodToAction(req.method);
|
const action = methodToAction(req.method);
|
||||||
const tableName = extractTableName(req.path);
|
const fullPath = (req.originalUrl || req.url).split('?')[0];
|
||||||
|
const section = extractSection(fullPath);
|
||||||
|
const tableName = extractTableName(fullPath);
|
||||||
const recordId = extractRecordId(req);
|
const recordId = extractRecordId(req);
|
||||||
const success = res.statusCode >= 200 && res.statusCode < 400;
|
const success = res.statusCode >= 200 && res.statusCode < 400;
|
||||||
|
|
||||||
let description = `${req.method} ${req.path}`;
|
let description = generateDescription(fullPath, action, tableName, recordId, section);
|
||||||
|
|
||||||
if (req.path.includes('/login')) {
|
if (fullPath.includes('/login')) {
|
||||||
description = 'User logged in';
|
description = 'User logged in successfully';
|
||||||
} else if (req.path.includes('/logout')) {
|
} else if (fullPath.includes('/logout')) {
|
||||||
description = 'User logged out';
|
description = 'User logged out';
|
||||||
} else if (req.path.includes('/password')) {
|
} else if (fullPath.includes('/password')) {
|
||||||
description = 'Password changed';
|
description = 'Password changed';
|
||||||
|
} else if (fullPath.includes('/bulk-upload')) {
|
||||||
|
description = `Bulk upload for ${tableName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newValues: unknown = undefined;
|
||||||
|
let oldValues: unknown = undefined;
|
||||||
|
|
||||||
|
if (['POST', 'PUT', 'PATCH'].includes(req.method) && req.body && Object.keys(req.body).length > 0) {
|
||||||
|
newValues = sanitizeData(req.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodyData = (responseBody as Record<string, unknown>)?.data;
|
||||||
|
if (req.method === 'GET' && recordId && bodyData && !Array.isArray(bodyData)) {
|
||||||
|
newValues = sanitizeData(bodyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'DELETE' && bodyData) {
|
||||||
|
oldValues = sanitizeData(bodyData);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userWithName = req.user as { name?: string; userName?: string; roleName: string; email: string };
|
||||||
|
const userName = userWithName.name || userWithName.userName || userWithName.roleName || userWithName.email;
|
||||||
|
|
||||||
const auditData = {
|
const auditData = {
|
||||||
userId: req.user.userId,
|
userId: req.user.userId,
|
||||||
userEmail: req.user.email,
|
userEmail: req.user.email,
|
||||||
userName: req.user.roleName || req.user.email,
|
userName,
|
||||||
action,
|
action,
|
||||||
tableName,
|
tableName,
|
||||||
recordId,
|
recordId,
|
||||||
@@ -129,21 +335,13 @@ export function auditMiddleware(
|
|||||||
ipAddress: getIpAddress(req),
|
ipAddress: getIpAddress(req),
|
||||||
userAgent: getUserAgent(req),
|
userAgent: getUserAgent(req),
|
||||||
success,
|
success,
|
||||||
newValues: ['POST', 'PUT', 'PATCH'].includes(req.method)
|
newValues: newValues as Record<string, unknown> | undefined,
|
||||||
? sanitizeData(req.body)
|
oldValues: oldValues as Record<string, unknown> | undefined,
|
||||||
: undefined,
|
errorMessage: !success && typeof (responseBody as Record<string, unknown>)?.error === 'string'
|
||||||
errorMessage: !success && responseBody?.error
|
? (responseBody as Record<string, unknown>).error as string
|
||||||
? responseBody.error
|
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('✅ [Audit] Creating audit log...', {
|
|
||||||
action,
|
|
||||||
tableName,
|
|
||||||
recordId,
|
|
||||||
userEmail: req.user.email
|
|
||||||
});
|
|
||||||
|
|
||||||
createAuditLog(auditData)
|
createAuditLog(auditData)
|
||||||
.then(logId => {
|
.then(logId => {
|
||||||
console.log('✅ [Audit] Log created successfully:', logId);
|
console.log('✅ [Audit] Log created successfully:', logId);
|
||||||
@@ -161,16 +359,25 @@ export function auditMiddleware(
|
|||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeData(data: any): any {
|
function sanitizeData(data: unknown): unknown {
|
||||||
if (!data || typeof data !== 'object') {
|
if (!data) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitized = { ...data };
|
if (typeof data !== 'object') {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data.map(item => sanitizeData(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized: Record<string, unknown> = {};
|
||||||
|
|
||||||
const sensitiveFields = [
|
const sensitiveFields = [
|
||||||
'password',
|
'password',
|
||||||
'password_hash',
|
'password_hash',
|
||||||
|
'passwordHash',
|
||||||
'currentPassword',
|
'currentPassword',
|
||||||
'newPassword',
|
'newPassword',
|
||||||
'token',
|
'token',
|
||||||
@@ -179,13 +386,21 @@ function sanitizeData(data: any): any {
|
|||||||
'secret',
|
'secret',
|
||||||
'api_key',
|
'api_key',
|
||||||
'apiKey',
|
'apiKey',
|
||||||
|
'credit_card',
|
||||||
|
'creditCard',
|
||||||
|
'ssn',
|
||||||
|
'cvv',
|
||||||
];
|
];
|
||||||
|
|
||||||
sensitiveFields.forEach(field => {
|
for (const [key, value] of Object.entries(data)) {
|
||||||
if (field in sanitized) {
|
if (sensitiveFields.includes(key)) {
|
||||||
sanitized[field] = '[REDACTED]';
|
sanitized[key] = '[REDACTED]';
|
||||||
|
} else if (value && typeof value === 'object') {
|
||||||
|
sanitized[key] = sanitizeData(value);
|
||||||
|
} else {
|
||||||
|
sanitized[key] = value;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
return sanitized;
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Response, NextFunction } from 'express';
|
||||||
import { verifyAccessToken } from '../utils/jwt';
|
import { verifyAccessToken } from '../utils/jwt';
|
||||||
|
import { AuthenticatedRequest } from '../types';
|
||||||
/**
|
|
||||||
* Extended Request interface with authenticated user
|
|
||||||
*/
|
|
||||||
export interface AuthenticatedRequest extends Request {
|
|
||||||
user?: {
|
|
||||||
id: string;
|
|
||||||
email: string;
|
|
||||||
role: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to authenticate JWT access tokens
|
* Middleware to authenticate JWT access tokens
|
||||||
@@ -47,9 +37,10 @@ export function authenticateToken(
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.user = {
|
req.user = {
|
||||||
id: decoded.id,
|
userId: (decoded as any).userId || (decoded as any).id,
|
||||||
email: decoded.email,
|
email: (decoded as any).email,
|
||||||
role: decoded.role,
|
roleId: (decoded as any).roleId || (decoded as any).role,
|
||||||
|
roleName: (decoded as any).roleName || (decoded as any).role,
|
||||||
};
|
};
|
||||||
|
|
||||||
next();
|
next();
|
||||||
@@ -74,7 +65,7 @@ export function requireRole(...roles: string[]) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!roles.includes(req.user.role)) {
|
if (!roles.includes(req.user.roleId) && !roles.includes(req.user.roleName)) {
|
||||||
res.status(403).json({ error: 'Insufficient permissions' });
|
res.status(403).json({ error: 'Insufficient permissions' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,15 +78,15 @@ export async function login(
|
|||||||
throw new Error('Invalid email or password');
|
throw new Error('Invalid email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate tokens
|
|
||||||
const accessToken = generateAccessToken({
|
const accessToken = generateAccessToken({
|
||||||
id: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role_name,
|
roleId: user.id,
|
||||||
|
roleName: user.role_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
const refreshToken = generateRefreshToken({
|
const refreshToken = generateRefreshToken({
|
||||||
id: user.id,
|
userId: user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Hash and store refresh token
|
// Hash and store refresh token
|
||||||
@@ -136,7 +136,8 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
|||||||
// Hash token to check against database
|
// Hash token to check against database
|
||||||
const hashedToken = hashToken(refreshToken);
|
const hashedToken = hashToken(refreshToken);
|
||||||
|
|
||||||
// Find token in database
|
const userId = (decoded as any).userId || (decoded as any).id;
|
||||||
|
|
||||||
const tokenResult = await query<{
|
const tokenResult = await query<{
|
||||||
id: string;
|
id: string;
|
||||||
expires_at: Date;
|
expires_at: Date;
|
||||||
@@ -144,7 +145,7 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
|||||||
`SELECT id, expires_at FROM refresh_tokens
|
`SELECT id, expires_at FROM refresh_tokens
|
||||||
WHERE token_hash = $1 AND user_id = $2 AND revoked_at IS NULL
|
WHERE token_hash = $1 AND user_id = $2 AND revoked_at IS NULL
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[hashedToken, decoded.id]
|
[hashedToken, userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const storedToken = tokenResult.rows[0];
|
const storedToken = tokenResult.rows[0];
|
||||||
@@ -175,7 +176,7 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
|||||||
JOIN roles r ON u.role_id = r.id
|
JOIN roles r ON u.role_id = r.id
|
||||||
WHERE u.id = $1 AND u.is_active = true
|
WHERE u.id = $1 AND u.is_active = true
|
||||||
LIMIT 1`,
|
LIMIT 1`,
|
||||||
[decoded.id]
|
[userId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const user = userResult.rows[0];
|
const user = userResult.rows[0];
|
||||||
@@ -184,11 +185,11 @@ export async function refresh(refreshToken: string): Promise<{ accessToken: stri
|
|||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new access token
|
|
||||||
const accessToken = generateAccessToken({
|
const accessToken = generateAccessToken({
|
||||||
id: user.id,
|
userId: user.id,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
role: user.role_name,
|
roleId: user.id,
|
||||||
|
roleName: user.role_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { accessToken };
|
return { accessToken };
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import jwt, { JwtPayload, SignOptions, VerifyOptions } from 'jsonwebtoken';
|
import jwt, { SignOptions, VerifyOptions } from 'jsonwebtoken';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
|
import type { JwtPayload } from '../types';
|
||||||
|
|
||||||
interface TokenPayload {
|
interface TokenPayload {
|
||||||
id: string;
|
userId?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
|
roleId?: string;
|
||||||
|
roleName?: string;
|
||||||
|
id?: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
@@ -58,7 +62,7 @@ export const generateRefreshToken = (payload: TokenPayload): string => {
|
|||||||
* @param token - JWT access token to verify
|
* @param token - JWT access token to verify
|
||||||
* @returns Decoded payload if valid, null if invalid or expired
|
* @returns Decoded payload if valid, null if invalid or expired
|
||||||
*/
|
*/
|
||||||
export const verifyAccessToken = (token: string): JwtPayload | null => {
|
export const verifyAccessToken = (token: string): any => {
|
||||||
const options: VerifyOptions = {
|
const options: VerifyOptions = {
|
||||||
algorithms: ['HS256'],
|
algorithms: ['HS256'],
|
||||||
};
|
};
|
||||||
@@ -68,7 +72,7 @@ export const verifyAccessToken = (token: string): JwtPayload | null => {
|
|||||||
token,
|
token,
|
||||||
config.jwt.accessTokenSecret,
|
config.jwt.accessTokenSecret,
|
||||||
options
|
options
|
||||||
) as JwtPayload;
|
);
|
||||||
return decoded;
|
return decoded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof jwt.TokenExpiredError) {
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
@@ -91,7 +95,7 @@ export const verifyAccessToken = (token: string): JwtPayload | null => {
|
|||||||
* @param token - JWT refresh token to verify
|
* @param token - JWT refresh token to verify
|
||||||
* @returns Decoded payload if valid, null if invalid or expired
|
* @returns Decoded payload if valid, null if invalid or expired
|
||||||
*/
|
*/
|
||||||
export const verifyRefreshToken = (token: string): JwtPayload | null => {
|
export const verifyRefreshToken = (token: string): any => {
|
||||||
const options: VerifyOptions = {
|
const options: VerifyOptions = {
|
||||||
algorithms: ['HS256'],
|
algorithms: ['HS256'],
|
||||||
};
|
};
|
||||||
@@ -101,7 +105,7 @@ export const verifyRefreshToken = (token: string): JwtPayload | null => {
|
|||||||
token,
|
token,
|
||||||
config.jwt.refreshTokenSecret,
|
config.jwt.refreshTokenSecret,
|
||||||
options
|
options
|
||||||
) as JwtPayload;
|
);
|
||||||
return decoded;
|
return decoded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof jwt.TokenExpiredError) {
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
@@ -124,9 +128,9 @@ export const verifyRefreshToken = (token: string): JwtPayload | null => {
|
|||||||
* @param token - JWT token to decode
|
* @param token - JWT token to decode
|
||||||
* @returns Decoded payload or null
|
* @returns Decoded payload or null
|
||||||
*/
|
*/
|
||||||
export const decodeToken = (token: string): JwtPayload | null => {
|
export const decodeToken = (token: string): any => {
|
||||||
try {
|
try {
|
||||||
const decoded = jwt.decode(token) as JwtPayload | null;
|
const decoded = jwt.decode(token);
|
||||||
return decoded;
|
return decoded;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error decoding token', {
|
logger.error('Error decoding token', {
|
||||||
|
|||||||
Reference in New Issue
Block a user