initialize git, basic setup for crm
This commit is contained in:
90
src/middlewares/auth/authMiddleware.js
Normal file
90
src/middlewares/auth/authMiddleware.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { verifyAccessToken } from '../../utils/jwt.js';
|
||||
import { AuthenticationError } from '../../utils/errors.js';
|
||||
import { getUserById } from '../../services/auth.service.js';
|
||||
|
||||
/**
|
||||
* Middleware na overenie JWT tokenu
|
||||
* Pridá user objekt do req.user ak je token validný
|
||||
*/
|
||||
export const authenticate = async (req, res, next) => {
|
||||
try {
|
||||
// Získaj token z Authorization header alebo cookies
|
||||
let token = null;
|
||||
|
||||
// Skús Authorization header (Bearer token)
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.substring(7);
|
||||
}
|
||||
|
||||
// Ak nie je v header, skús cookies
|
||||
if (!token && req.cookies && req.cookies.accessToken) {
|
||||
token = req.cookies.accessToken;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new AuthenticationError('Token nie je poskytnutý');
|
||||
}
|
||||
|
||||
// Overenie tokenu
|
||||
const decoded = verifyAccessToken(token);
|
||||
|
||||
// Načítaj aktuálne user data z DB
|
||||
const user = await getUserById(decoded.id);
|
||||
|
||||
// Pridaj user do requestu
|
||||
req.user = user;
|
||||
req.userId = user.id;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
statusCode: 401,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Neplatný alebo expirovaný token',
|
||||
statusCode: 401,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Optional authentication - nepovinnné overenie
|
||||
* Ak je token poskytnutý, overí ho, ale nehodí error ak nie je
|
||||
*/
|
||||
export const optionalAuthenticate = async (req, res, next) => {
|
||||
try {
|
||||
let token = null;
|
||||
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.substring(7);
|
||||
}
|
||||
|
||||
if (!token && req.cookies && req.cookies.accessToken) {
|
||||
token = req.cookies.accessToken;
|
||||
}
|
||||
|
||||
if (token) {
|
||||
const decoded = verifyAccessToken(token);
|
||||
const user = await getUserById(decoded.id);
|
||||
req.user = user;
|
||||
req.userId = user.id;
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
// Ignoruj chyby, len pokračuj bez user objektu
|
||||
next();
|
||||
}
|
||||
};
|
||||
88
src/middlewares/auth/roleMiddleware.js
Normal file
88
src/middlewares/auth/roleMiddleware.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ForbiddenError } from '../../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Middleware na overenie role používateľa
|
||||
* Musí byť použité PO authenticate middleware
|
||||
* @param {...string} allowedRoles - Povolené role (napr. 'admin', 'member')
|
||||
*/
|
||||
export const requireRole = (...allowedRoles) => {
|
||||
return (req, res, next) => {
|
||||
// Skontroluj či je user autentifikovaný
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Musíte byť prihlásený',
|
||||
statusCode: 401,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Skontroluj či user má jednu z povolených rolí
|
||||
if (!allowedRoles.includes(req.user.role)) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Nemáte oprávnenie na túto operáciu',
|
||||
statusCode: 403,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware špecificky pre admin rolu
|
||||
*/
|
||||
export const requireAdmin = requireRole('admin');
|
||||
|
||||
/**
|
||||
* Middleware pre kontrolu či user môže upravovať resource
|
||||
* Buď je to admin, alebo je to vlastník resource
|
||||
* @param {function} getResourceUserId - Funkcia ktorá vráti userId vlastníka resource
|
||||
*/
|
||||
export const requireOwnerOrAdmin = (getResourceUserId) => {
|
||||
return async (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Musíte byť prihlásený',
|
||||
statusCode: 401,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Admin môže všetko
|
||||
if (req.user.role === 'admin') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Inak skontroluj ownership
|
||||
try {
|
||||
const resourceUserId = await getResourceUserId(req);
|
||||
|
||||
if (req.user.id !== resourceUserId) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Nemáte oprávnenie na túto operáciu',
|
||||
statusCode: 403,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Chyba pri overovaní oprávnenia',
|
||||
statusCode: 500,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
15
src/middlewares/global/errorHandler.js
Normal file
15
src/middlewares/global/errorHandler.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { formatErrorResponse } from '../../utils/errors.js';
|
||||
import { logger } from '../../utils/logger.js';
|
||||
|
||||
export function errorHandler(err, req, res, next) {
|
||||
// Log error
|
||||
logger.error('Unhandled error', err);
|
||||
|
||||
// Get status code
|
||||
const statusCode = err.statusCode || res.statusCode !== 200 ? res.statusCode : 500;
|
||||
|
||||
// Format error response
|
||||
const errorResponse = formatErrorResponse(err, process.env.NODE_ENV === 'development');
|
||||
|
||||
res.status(statusCode).json(errorResponse);
|
||||
}
|
||||
5
src/middlewares/global/notFound.js
Normal file
5
src/middlewares/global/notFound.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export function notFound(req, res, next) {
|
||||
res.status(404);
|
||||
const error = new Error(`🔍 Not Found - ${req.originalUrl}`);
|
||||
next(error);
|
||||
}
|
||||
18
src/middlewares/global/validateBody.js
Normal file
18
src/middlewares/global/validateBody.js
Normal file
@@ -0,0 +1,18 @@
|
||||
export function validateBody(req, res, next) {
|
||||
const data = JSON.stringify({ body: req.body, query: req.query, params: req.params });
|
||||
const dangerousPatterns = [
|
||||
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|EXEC|UNION|LOAD_FILE|OUTFILE)\b.*\b(FROM|INTO|TABLE|DATABASE)\b)/gi,
|
||||
/\b(OR 1=1|AND 1=1|OR '1'='1'|--|#|\/\*|\*\/|;|\bUNION\b.*?\bSELECT\b)/gi,
|
||||
/\b(\$where|\$ne|\$gt|\$lt|\$regex|\$exists|\$not|\$or|\$and)\b/gi,
|
||||
/(<script|<\/script>|document\.cookie|eval\(|alert\(|javascript:|onerror=|onmouseover=)/gi,
|
||||
/(\bexec\s*xp_cmdshell|\bshutdown\b|\bdrop\s+database|\bdelete\s+from)/gi,
|
||||
/(\b(base64_decode|cmd|powershell|wget|curl|rm -rf|nc -e|perl -e|python -c)\b)/gi,
|
||||
];
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(data)) {
|
||||
console.warn(`❌ Suspicious input detected: ${data}`);
|
||||
return res.status(400).json({ message: '🚨 Malicious content detected in request data' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
92
src/middlewares/security/rateLimiter.js
Normal file
92
src/middlewares/security/rateLimiter.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import { RateLimitError } from '../../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Rate limiter pre login endpoint
|
||||
* Max 5 pokusov za 15 minút
|
||||
*/
|
||||
export const loginRateLimiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minút
|
||||
max: parseInt(process.env.RATE_LIMIT_LOGIN_MAX) || 5,
|
||||
message: {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Príliš veľa prihlasovacích pokusov. Skúste znova o 15 minút.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
// Skip successful requests - len zlyhané pokusy sa počítajú
|
||||
skip: (req, res) => {
|
||||
// Ak je response success, nerátaj to do limitu
|
||||
return res.statusCode < 400;
|
||||
},
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Príliš veľa prihlasovacích pokusov. Skúste znova o 15 minút.',
|
||||
statusCode: 429,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter pre všeobecné API endpointy
|
||||
* Development: 1000 requestov za 15 minút (viac pre development)
|
||||
* Production: 100 requestov za 15 minút
|
||||
*/
|
||||
export const apiRateLimiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||
max: process.env.NODE_ENV === 'production'
|
||||
? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100
|
||||
: 1000, // Higher limit for development
|
||||
message: {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Príliš veľa požiadaviek. Skúste znova neskôr.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Príliš veľa požiadaviek. Skúste znova neskôr.',
|
||||
statusCode: 429,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter pre citlivé operácie (password reset, email change, atď.)
|
||||
* Development: 50 pokusov za 15 minút
|
||||
* Production: 3 pokusy za 15 minút
|
||||
*/
|
||||
export const sensitiveOperationLimiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: process.env.NODE_ENV === 'production' ? 3 : 50,
|
||||
message: {
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Príliš veľa pokusov. Skúste znova o 15 minút.',
|
||||
statusCode: 429,
|
||||
},
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: (req, res) => {
|
||||
res.status(429).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Príliš veľa pokusov. Skúste znova o 15 minút.',
|
||||
statusCode: 429,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
128
src/middlewares/security/validateInput.js
Normal file
128
src/middlewares/security/validateInput.js
Normal file
@@ -0,0 +1,128 @@
|
||||
import { ZodError } from 'zod';
|
||||
import { ValidationError } from '../../utils/errors.js';
|
||||
|
||||
/**
|
||||
* Middleware na validáciu request body pomocou Zod schema
|
||||
* @param {ZodSchema} schema - Zod validačná schéma
|
||||
*/
|
||||
export const validateBody = (schema) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
// Validuj request body
|
||||
const validated = await schema.parseAsync(req.body);
|
||||
|
||||
// Nahraď body validovanými dátami
|
||||
req.body = validated;
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError && error.errors) {
|
||||
// Zformátuj Zod chyby
|
||||
const formattedErrors = error.errors.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Validačná chyba',
|
||||
statusCode: 400,
|
||||
details: formattedErrors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
console.error('Validation error:', error);
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error?.message || 'Neplatné vstupné dáta',
|
||||
statusCode: 400,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware na validáciu query parametrov
|
||||
* @param {ZodSchema} schema - Zod validačná schéma
|
||||
*/
|
||||
export const validateQuery = (schema) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const validated = await schema.parseAsync(req.query);
|
||||
req.query = validated;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError && error.errors) {
|
||||
const formattedErrors = error.errors.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Validačná chyba v query parametroch',
|
||||
statusCode: 400,
|
||||
details: formattedErrors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Query validation error:', error);
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error?.message || 'Neplatné query parametre',
|
||||
statusCode: 400,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware na validáciu URL parametrov
|
||||
* @param {ZodSchema} schema - Zod validačná schéma
|
||||
*/
|
||||
export const validateParams = (schema) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
const validated = await schema.parseAsync(req.params);
|
||||
req.params = validated;
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError && error.errors) {
|
||||
const formattedErrors = error.errors.map((err) => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message,
|
||||
}));
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: 'Validačná chyba v URL parametroch',
|
||||
statusCode: 400,
|
||||
details: formattedErrors,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.error('Params validation error:', error);
|
||||
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: {
|
||||
message: error?.message || 'Neplatné URL parametre',
|
||||
statusCode: 400,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user