Initial commit
Dieser Commit ist enthalten in:
189
backend/middleware/auth.js
Normale Datei
189
backend/middleware/auth.js
Normale Datei
@ -0,0 +1,189 @@
|
||||
/**
|
||||
* TASKMATE - Auth Middleware
|
||||
* ==========================
|
||||
* JWT-basierte Authentifizierung
|
||||
*/
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'UNSICHER_BITTE_AENDERN';
|
||||
const SESSION_TIMEOUT = parseInt(process.env.SESSION_TIMEOUT) || 30; // Minuten
|
||||
|
||||
/**
|
||||
* JWT-Token generieren
|
||||
*/
|
||||
function generateToken(user) {
|
||||
// Permissions parsen falls als String gespeichert
|
||||
let permissions = user.permissions || [];
|
||||
if (typeof permissions === 'string') {
|
||||
try {
|
||||
permissions = JSON.parse(permissions);
|
||||
} catch (e) {
|
||||
permissions = [];
|
||||
}
|
||||
}
|
||||
|
||||
return jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
displayName: user.display_name,
|
||||
color: user.color,
|
||||
role: user.role || 'user',
|
||||
permissions: permissions
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: `${SESSION_TIMEOUT}m` }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT-Token verifizieren
|
||||
*/
|
||||
function verifyToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, JWT_SECRET);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Express Middleware: Token aus Header, Cookie oder Query-Parameter prüfen
|
||||
*/
|
||||
function authenticateToken(req, res, next) {
|
||||
// Token aus Authorization Header, Cookie oder Query-Parameter (für img src etc.)
|
||||
let token = null;
|
||||
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.substring(7);
|
||||
} else if (req.cookies && req.cookies.token) {
|
||||
token = req.cookies.token;
|
||||
} else if (req.query && req.query.token) {
|
||||
// Token aus Query-Parameter (für Ressourcen die in img/video tags geladen werden)
|
||||
token = req.query.token;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Nicht authentifiziert' });
|
||||
}
|
||||
|
||||
const user = verifyToken(token);
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Token ungültig oder abgelaufen' });
|
||||
}
|
||||
|
||||
// User-Info an Request anhängen
|
||||
req.user = user;
|
||||
|
||||
// Token-Refresh: Wenn Token bald ablaeuft, neuen ausstellen
|
||||
const tokenExp = user.exp * 1000; // exp ist in Sekunden
|
||||
const now = Date.now();
|
||||
const refreshThreshold = 5 * 60 * 1000; // 5 Minuten vor Ablauf
|
||||
|
||||
if (tokenExp - now < refreshThreshold) {
|
||||
const newToken = generateToken({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
display_name: user.displayName,
|
||||
color: user.color,
|
||||
role: user.role,
|
||||
permissions: user.permissions
|
||||
});
|
||||
res.setHeader('X-New-Token', newToken);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware: Nur Admins erlauben
|
||||
*/
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Nicht authentifiziert' });
|
||||
}
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ error: 'Keine Admin-Berechtigung' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware: Nur regulaere User erlauben (blockiert Admins)
|
||||
*/
|
||||
function requireRegularUser(req, res, next) {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Nicht authentifiziert' });
|
||||
}
|
||||
|
||||
if (req.user.role === 'admin') {
|
||||
return res.status(403).json({ error: 'Admin hat keinen Zugang zur regulaeren App' });
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware-Factory: Bestimmte Berechtigung pruefen
|
||||
*/
|
||||
function checkPermission(permission) {
|
||||
return (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Nicht authentifiziert' });
|
||||
}
|
||||
|
||||
const permissions = req.user.permissions || [];
|
||||
if (!permissions.includes(permission)) {
|
||||
return res.status(403).json({ error: `Berechtigung "${permission}" fehlt` });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket.io Middleware: Token prüfen
|
||||
*/
|
||||
function authenticateSocket(socket, next) {
|
||||
const token = socket.handshake.auth.token ||
|
||||
socket.handshake.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return next(new Error('Nicht authentifiziert'));
|
||||
}
|
||||
|
||||
const user = verifyToken(token);
|
||||
if (!user) {
|
||||
return next(new Error('Token ungültig oder abgelaufen'));
|
||||
}
|
||||
|
||||
// User-Info an Socket anhängen
|
||||
socket.user = user;
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF-Token generieren (für Forms)
|
||||
*/
|
||||
function generateCsrfToken() {
|
||||
const { randomBytes } = require('crypto');
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
verifyToken,
|
||||
authenticateToken,
|
||||
authenticateSocket,
|
||||
generateCsrfToken,
|
||||
requireAdmin,
|
||||
requireRegularUser,
|
||||
checkPermission,
|
||||
JWT_SECRET,
|
||||
SESSION_TIMEOUT
|
||||
};
|
||||
125
backend/middleware/csrf.js
Normale Datei
125
backend/middleware/csrf.js
Normale Datei
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* TASKMATE - CSRF Schutz
|
||||
* ======================
|
||||
* Cross-Site Request Forgery Schutz
|
||||
*
|
||||
* Vereinfachtes System: Token wird beim Login generiert und bleibt
|
||||
* für die gesamte Sitzung gültig (24 Stunden).
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// CSRF-Tokens speichern (in-memory, pro User)
|
||||
const csrfTokens = new Map();
|
||||
|
||||
// Token-Gültigkeit: 24 Stunden
|
||||
const TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* CSRF-Token generieren
|
||||
*/
|
||||
function generateToken(userId) {
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const expires = Date.now() + TOKEN_VALIDITY;
|
||||
|
||||
csrfTokens.set(userId, { token, expires });
|
||||
cleanupExpiredTokens();
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF-Token validieren
|
||||
*/
|
||||
function validateToken(userId, token) {
|
||||
const stored = csrfTokens.get(userId);
|
||||
|
||||
if (!stored) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Date.now() > stored.expires) {
|
||||
csrfTokens.delete(userId);
|
||||
return false;
|
||||
}
|
||||
|
||||
return stored.token === token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abgelaufene Tokens aufräumen
|
||||
*/
|
||||
function cleanupExpiredTokens() {
|
||||
const now = Date.now();
|
||||
for (const [userId, data] of csrfTokens.entries()) {
|
||||
if (now > data.expires) {
|
||||
csrfTokens.delete(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regelmäßig aufräumen (alle 30 Minuten)
|
||||
setInterval(cleanupExpiredTokens, 30 * 60 * 1000);
|
||||
|
||||
/**
|
||||
* Express Middleware
|
||||
*/
|
||||
function csrfProtection(req, res, next) {
|
||||
// GET-Anfragen sind sicher (lesen nur)
|
||||
if (req.method === 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// User muss authentifiziert sein
|
||||
if (!req.user) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const userId = req.user.id;
|
||||
|
||||
// Token aus Header oder Body
|
||||
const token = req.headers['x-csrf-token'] || req.body?._csrf;
|
||||
|
||||
// Wenn kein Token vom Client gesendet oder kein Token auf Server gespeichert
|
||||
if (!token || !csrfTokens.has(userId)) {
|
||||
const newToken = generateToken(userId);
|
||||
logger.info(`CSRF: Token missing or not stored for user ${userId}, generated new token`);
|
||||
return res.status(403).json({
|
||||
error: 'CSRF-Token fehlt oder abgelaufen',
|
||||
csrfToken: newToken,
|
||||
code: 'CSRF_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
// Token validieren
|
||||
if (!validateToken(userId, token)) {
|
||||
const newToken = generateToken(userId);
|
||||
logger.info(`CSRF: Token mismatch for user ${userId}`);
|
||||
return res.status(403).json({
|
||||
error: 'Ungültiges CSRF-Token',
|
||||
csrfToken: newToken,
|
||||
code: 'CSRF_ERROR'
|
||||
});
|
||||
}
|
||||
|
||||
// Token ist gültig - KEIN neuer Token generiert (bleibt für die Sitzung gleich)
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuellen Token für User abrufen (oder neuen generieren)
|
||||
*/
|
||||
function getTokenForUser(userId) {
|
||||
const stored = csrfTokens.get(userId);
|
||||
|
||||
if (stored && Date.now() < stored.expires) {
|
||||
return stored.token;
|
||||
}
|
||||
|
||||
return generateToken(userId);
|
||||
}
|
||||
|
||||
module.exports = csrfProtection;
|
||||
module.exports.generateToken = generateToken;
|
||||
module.exports.getTokenForUser = getTokenForUser;
|
||||
198
backend/middleware/upload.js
Normale Datei
198
backend/middleware/upload.js
Normale Datei
@ -0,0 +1,198 @@
|
||||
/**
|
||||
* TASKMATE - File Upload
|
||||
* ======================
|
||||
* Multer-Konfiguration für Datei-Uploads
|
||||
*/
|
||||
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Upload-Verzeichnis
|
||||
const UPLOAD_DIR = process.env.UPLOAD_DIR || path.join(__dirname, '..', 'uploads');
|
||||
|
||||
// Verzeichnis erstellen falls nicht vorhanden
|
||||
if (!fs.existsSync(UPLOAD_DIR)) {
|
||||
fs.mkdirSync(UPLOAD_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Standard-Werte (Fallback)
|
||||
let MAX_FILE_SIZE = (parseInt(process.env.MAX_FILE_SIZE_MB) || 15) * 1024 * 1024;
|
||||
let ALLOWED_MIME_TYPES = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
|
||||
'application/pdf',
|
||||
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain', 'text/csv', 'text/markdown',
|
||||
'application/zip', 'application/x-rar-compressed', 'application/x-7z-compressed',
|
||||
'application/json'
|
||||
];
|
||||
|
||||
/**
|
||||
* Lädt Upload-Einstellungen aus der Datenbank
|
||||
*/
|
||||
function loadUploadSettings() {
|
||||
try {
|
||||
// Lazy-Load um zirkuläre Abhängigkeiten zu vermeiden
|
||||
const { getUploadSettings } = require('../routes/admin');
|
||||
const settings = getUploadSettings();
|
||||
|
||||
if (settings) {
|
||||
MAX_FILE_SIZE = (settings.maxFileSizeMB || 15) * 1024 * 1024;
|
||||
|
||||
// Erlaubte MIME-Types aus den aktiven Kategorien zusammenstellen
|
||||
const types = [];
|
||||
if (settings.allowedTypes) {
|
||||
Object.values(settings.allowedTypes).forEach(category => {
|
||||
if (category.enabled && Array.isArray(category.types)) {
|
||||
types.push(...category.types);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (types.length > 0) {
|
||||
ALLOWED_MIME_TYPES = types;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Bei Fehler Standard-Werte beibehalten
|
||||
logger.warn('Upload-Einstellungen konnten nicht geladen werden, verwende Standards');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuelle Einstellungen abrufen (für dynamische Prüfung)
|
||||
*/
|
||||
function getCurrentSettings() {
|
||||
loadUploadSettings();
|
||||
return { maxFileSize: MAX_FILE_SIZE, allowedMimeTypes: ALLOWED_MIME_TYPES };
|
||||
}
|
||||
|
||||
/**
|
||||
* Storage-Konfiguration
|
||||
*/
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
// Task-ID aus URL oder Body
|
||||
const taskId = req.params.taskId || req.body.taskId;
|
||||
|
||||
if (taskId) {
|
||||
// Unterordner pro Task
|
||||
const taskDir = path.join(UPLOAD_DIR, `task_${taskId}`);
|
||||
if (!fs.existsSync(taskDir)) {
|
||||
fs.mkdirSync(taskDir, { recursive: true });
|
||||
}
|
||||
cb(null, taskDir);
|
||||
} else {
|
||||
cb(null, UPLOAD_DIR);
|
||||
}
|
||||
},
|
||||
|
||||
filename: (req, file, cb) => {
|
||||
// Eindeutiger Dateiname mit Original-Extension
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
const uniqueName = `${uuidv4()}${ext}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Datei-Filter
|
||||
*/
|
||||
const fileFilter = (req, file, cb) => {
|
||||
// Aktuelle Einstellungen laden
|
||||
const settings = getCurrentSettings();
|
||||
|
||||
// MIME-Type prüfen
|
||||
if (settings.allowedMimeTypes.includes(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
logger.warn(`Abgelehnter Upload: ${file.originalname} (${file.mimetype})`);
|
||||
cb(new Error(`Dateityp nicht erlaubt: ${file.mimetype}`), false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Dynamische Multer-Instanz erstellen
|
||||
*/
|
||||
function createUpload() {
|
||||
const settings = getCurrentSettings();
|
||||
return multer({
|
||||
storage,
|
||||
fileFilter,
|
||||
limits: {
|
||||
fileSize: settings.maxFileSize,
|
||||
files: 10 // Maximal 10 Dateien pro Request
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Standard-Instanz für Rückwärtskompatibilität
|
||||
const upload = createUpload();
|
||||
|
||||
/**
|
||||
* Datei löschen
|
||||
*/
|
||||
function deleteFile(filePath) {
|
||||
try {
|
||||
const fullPath = path.join(UPLOAD_DIR, filePath);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
fs.unlinkSync(fullPath);
|
||||
logger.info(`Datei gelöscht: ${filePath}`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`Fehler beim Löschen: ${filePath}`, { error: error.message });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dateigröße formatieren
|
||||
*/
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ist Bild?
|
||||
*/
|
||||
function isImage(mimeType) {
|
||||
return mimeType && mimeType.startsWith('image/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Datei-Icon basierend auf MIME-Type
|
||||
*/
|
||||
function getFileIcon(mimeType) {
|
||||
if (mimeType.startsWith('image/')) return 'image';
|
||||
if (mimeType === 'application/pdf') return 'pdf';
|
||||
if (mimeType.includes('word')) return 'word';
|
||||
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'excel';
|
||||
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'powerpoint';
|
||||
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('7z')) return 'archive';
|
||||
if (mimeType.startsWith('text/')) return 'text';
|
||||
return 'file';
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
upload,
|
||||
createUpload,
|
||||
deleteFile,
|
||||
formatFileSize,
|
||||
isImage,
|
||||
getFileIcon,
|
||||
getCurrentSettings,
|
||||
UPLOAD_DIR,
|
||||
MAX_FILE_SIZE,
|
||||
ALLOWED_MIME_TYPES
|
||||
};
|
||||
249
backend/middleware/validation.js
Normale Datei
249
backend/middleware/validation.js
Normale Datei
@ -0,0 +1,249 @@
|
||||
/**
|
||||
* TASKMATE - Input Validierung
|
||||
* ============================
|
||||
* Schutz vor SQL-Injection und XSS
|
||||
*/
|
||||
|
||||
const sanitizeHtml = require('sanitize-html');
|
||||
|
||||
/**
|
||||
* HTML-Tags entfernen (für reine Text-Felder)
|
||||
*/
|
||||
function stripHtml(input) {
|
||||
if (typeof input !== 'string') return input;
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: {}
|
||||
}).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Markdown-sichere Bereinigung (erlaubt bestimmte Tags)
|
||||
*/
|
||||
function sanitizeMarkdown(input) {
|
||||
if (typeof input !== 'string') return input;
|
||||
return sanitizeHtml(input, {
|
||||
allowedTags: [
|
||||
'p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
|
||||
'ul', 'ol', 'li', 'blockquote', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'
|
||||
],
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'title', 'target', 'rel']
|
||||
},
|
||||
allowedSchemes: ['http', 'https', 'mailto'],
|
||||
transformTags: {
|
||||
'a': (tagName, attribs) => {
|
||||
return {
|
||||
tagName: 'a',
|
||||
attribs: {
|
||||
...attribs,
|
||||
target: '_blank',
|
||||
rel: 'noopener noreferrer'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Objekt rekursiv bereinigen
|
||||
*/
|
||||
function sanitizeObject(obj, options = {}) {
|
||||
if (obj === null || obj === undefined) return obj;
|
||||
|
||||
if (typeof obj === 'string') {
|
||||
return options.allowHtml ? sanitizeMarkdown(obj) : stripHtml(obj);
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => sanitizeObject(item, options));
|
||||
}
|
||||
|
||||
if (typeof obj === 'object') {
|
||||
const sanitized = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
// Bestimmte Felder dürfen Markdown enthalten
|
||||
const allowHtml = ['description', 'content'].includes(key);
|
||||
sanitized[key] = sanitizeObject(value, { allowHtml });
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validierungsfunktionen
|
||||
*/
|
||||
const validators = {
|
||||
/**
|
||||
* Pflichtfeld prüfen
|
||||
*/
|
||||
required: (value, fieldName) => {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return `${fieldName} ist erforderlich`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Minimale Länge prüfen
|
||||
*/
|
||||
minLength: (value, min, fieldName) => {
|
||||
if (typeof value === 'string' && value.length < min) {
|
||||
return `${fieldName} muss mindestens ${min} Zeichen haben`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Maximale Länge prüfen
|
||||
*/
|
||||
maxLength: (value, max, fieldName) => {
|
||||
if (typeof value === 'string' && value.length > max) {
|
||||
return `${fieldName} darf maximal ${max} Zeichen haben`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* E-Mail-Format prüfen
|
||||
*/
|
||||
email: (value, fieldName) => {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (value && !emailRegex.test(value)) {
|
||||
return `${fieldName} muss eine gültige E-Mail-Adresse sein`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* URL-Format prüfen
|
||||
*/
|
||||
url: (value, fieldName) => {
|
||||
try {
|
||||
if (value) {
|
||||
new URL(value);
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return `${fieldName} muss eine gültige URL sein`;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Integer prüfen
|
||||
*/
|
||||
integer: (value, fieldName) => {
|
||||
if (value !== undefined && value !== null && !Number.isInteger(Number(value))) {
|
||||
return `${fieldName} muss eine ganze Zahl sein`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Positiver Integer prüfen
|
||||
*/
|
||||
positiveInteger: (value, fieldName) => {
|
||||
const num = Number(value);
|
||||
if (value !== undefined && value !== null && (!Number.isInteger(num) || num < 0)) {
|
||||
return `${fieldName} muss eine positive ganze Zahl sein`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Datum prüfen (YYYY-MM-DD)
|
||||
*/
|
||||
date: (value, fieldName) => {
|
||||
if (value) {
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!dateRegex.test(value)) {
|
||||
return `${fieldName} muss im Format YYYY-MM-DD sein`;
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (isNaN(date.getTime())) {
|
||||
return `${fieldName} ist kein gültiges Datum`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Enum-Wert prüfen
|
||||
*/
|
||||
enum: (value, allowedValues, fieldName) => {
|
||||
if (value && !allowedValues.includes(value)) {
|
||||
return `${fieldName} muss einer der folgenden Werte sein: ${allowedValues.join(', ')}`;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Hex-Farbe prüfen
|
||||
*/
|
||||
hexColor: (value, fieldName) => {
|
||||
if (value) {
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
|
||||
if (!hexRegex.test(value)) {
|
||||
return `${fieldName} muss eine gültige Hex-Farbe sein (z.B. #FF0000)`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Passwort-Richtlinien prüfen
|
||||
*/
|
||||
function validatePassword(password) {
|
||||
const errors = [];
|
||||
const minLength = 8;
|
||||
|
||||
if (!password || password.length < minLength) {
|
||||
errors.push(`Passwort muss mindestens ${minLength} Zeichen haben`);
|
||||
}
|
||||
|
||||
if (password && !/[a-z]/.test(password)) {
|
||||
errors.push('Passwort muss mindestens einen Kleinbuchstaben enthalten');
|
||||
}
|
||||
|
||||
if (password && !/[A-Z]/.test(password)) {
|
||||
errors.push('Passwort muss mindestens einen Großbuchstaben enthalten');
|
||||
}
|
||||
|
||||
if (password && !/[0-9]/.test(password)) {
|
||||
errors.push('Passwort muss mindestens eine Zahl enthalten');
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Express Middleware: Request-Body bereinigen
|
||||
*/
|
||||
function sanitizeMiddleware(req, res, next) {
|
||||
if (req.body && typeof req.body === 'object') {
|
||||
req.body = sanitizeObject(req.body);
|
||||
}
|
||||
|
||||
if (req.query && typeof req.query === 'object') {
|
||||
req.query = sanitizeObject(req.query);
|
||||
}
|
||||
|
||||
if (req.params && typeof req.params === 'object') {
|
||||
req.params = sanitizeObject(req.params);
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stripHtml,
|
||||
sanitizeMarkdown,
|
||||
sanitizeObject,
|
||||
validators,
|
||||
validatePassword,
|
||||
sanitizeMiddleware
|
||||
};
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren