initialize git, basic setup for crm

This commit is contained in:
richardtekula
2025-11-18 13:53:28 +01:00
commit da01d586fc
47 changed files with 12776 additions and 0 deletions

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

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

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

View File

@@ -0,0 +1,5 @@
export function notFound(req, res, next) {
res.status(404);
const error = new Error(`🔍 Not Found - ${req.originalUrl}`);
next(error);
}

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

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

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