- Migration 084: new persoenliche_ausruestung table with catalog link, user assignment, soft delete; adds zuweisung_typ/ausruestung_id/persoenlich_id columns to ausruestung_anfrage_positionen; seeds feature group + 5 permissions - Fix user data purge: table was shop_anfragen, renamed to ausruestung_anfragen in mig 046 — caused full transaction rollback. Also keep mitglieder_profile row but NULL FDISK-synced fields (dienstgrad, geburtsdatum, etc.) instead of deleting the profile - Personal equipment CRUD: backend service/controller/routes at /api/persoenliche-ausruestung; frontend page with DataTable, user filter, catalog Autocomplete, FAB create dialog; widget in Status group; sidebar entry (Checkroom icon); card in MitgliedDetail Tab 0 - Ausruestungsanfrage item assignment: when a request reaches erledigt, auto-opens ItemAssignmentDialog listing all delivered positions; each item can be assigned as general equipment (vehicle/storage), personal item (user, prefilled with requester), or not tracked; POST /requests/:id/assign backend - StatCard refactored to use WidgetCard as outer shell for consistent header styling across all dashboard widget templates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
6.0 KiB
TypeScript
155 lines
6.0 KiB
TypeScript
import express, { Application, Request, Response } from 'express';
|
|
import cors from 'cors';
|
|
import helmet from 'helmet';
|
|
import rateLimit from 'express-rate-limit';
|
|
import path from 'path';
|
|
import environment from './config/environment';
|
|
import logger from './utils/logger';
|
|
import { errorHandler, notFoundHandler } from './middleware/error.middleware';
|
|
import { requestTimeout } from './middleware/request-timeout.middleware';
|
|
import { authenticate } from './middleware/auth.middleware';
|
|
|
|
const app: Application = express();
|
|
|
|
// Trust proxy (required for correct IP detection behind Traefik/Nginx)
|
|
app.set('trust proxy', 1);
|
|
|
|
// Security middleware
|
|
app.use(helmet());
|
|
|
|
// CORS configuration
|
|
app.use(cors({
|
|
origin: environment.cors.origin,
|
|
credentials: true,
|
|
}));
|
|
|
|
// Rate limiting - general API routes (applied below, after auth limiter)
|
|
|
|
// Rate limiting - auth routes (generous to avoid blocking logins during
|
|
// normal use; each OAuth flow = 1 callback + token exchange)
|
|
const authLimiter = rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 60, // 60 auth attempts per window (allows ~20 full login cycles)
|
|
message: 'Zu viele Anmeldeversuche. Bitte versuchen Sie es später erneut.',
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
});
|
|
|
|
app.use('/api/auth', authLimiter);
|
|
// General rate limiter — skip auth routes (own limiter above) and authenticated
|
|
// requests (Bearer token present). Auth middleware validates the token downstream;
|
|
// rate-limiting authenticated dashboard polling would cause 429 floods.
|
|
app.use('/api', rateLimit({
|
|
windowMs: environment.rateLimit.windowMs,
|
|
max: environment.rateLimit.max,
|
|
message: 'Too many requests from this IP, please try again later.',
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
skip: (req) => {
|
|
if (req.path.startsWith('/auth')) return true;
|
|
const auth = req.headers.authorization;
|
|
return typeof auth === 'string' && auth.startsWith('Bearer ');
|
|
},
|
|
}));
|
|
|
|
// Body parsing middleware
|
|
app.use(express.json({ limit: '10mb' }));
|
|
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
|
|
// Request timeout middleware
|
|
app.use(requestTimeout);
|
|
|
|
// Request logging middleware
|
|
app.use((req: Request, _res: Response, next) => {
|
|
logger.info('Incoming request', {
|
|
method: req.method,
|
|
path: req.path,
|
|
ip: req.ip,
|
|
});
|
|
next();
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.get('/health', (_req: Request, res: Response) => {
|
|
res.status(200).json({
|
|
status: 'ok',
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
environment: environment.nodeEnv,
|
|
});
|
|
});
|
|
|
|
// API routes
|
|
import authRoutes from './routes/auth.routes';
|
|
import userRoutes from './routes/user.routes';
|
|
import memberRoutes from './routes/member.routes';
|
|
import adminRoutes from './routes/admin.routes';
|
|
import trainingRoutes from './routes/training.routes';
|
|
import vehicleRoutes from './routes/vehicle.routes';
|
|
import incidentRoutes from './routes/incident.routes';
|
|
import equipmentRoutes from './routes/equipment.routes';
|
|
import nextcloudRoutes from './routes/nextcloud.routes';
|
|
import atemschutzRoutes from './routes/atemschutz.routes';
|
|
import eventsRoutes from './routes/events.routes';
|
|
import bookingRoutes from './routes/booking.routes';
|
|
import notificationRoutes from './routes/notification.routes';
|
|
import bookstackRoutes from './routes/bookstack.routes';
|
|
import vikunjaRoutes from './routes/vikunja.routes';
|
|
import bestellungRoutes from './routes/bestellung.routes';
|
|
import configRoutes from './routes/config.routes';
|
|
import serviceMonitorRoutes from './routes/serviceMonitor.routes';
|
|
import settingsRoutes from './routes/settings.routes';
|
|
import bannerRoutes from './routes/banner.routes';
|
|
import permissionRoutes from './routes/permission.routes';
|
|
import ausruestungsanfrageRoutes from './routes/ausruestungsanfrage.routes';
|
|
import issueRoutes from './routes/issue.routes';
|
|
import buchungskategorieRoutes from './routes/buchungskategorie.routes';
|
|
import checklistRoutes from './routes/checklist.routes';
|
|
import fahrzeugTypRoutes from './routes/fahrzeugTyp.routes';
|
|
import ausruestungTypRoutes from './routes/ausruestungTyp.routes';
|
|
import buchhaltungRoutes from './routes/buchhaltung.routes';
|
|
import personalEquipmentRoutes from './routes/personalEquipment.routes';
|
|
|
|
app.use('/api/auth', authRoutes);
|
|
app.use('/api/user', userRoutes);
|
|
app.use('/api/members', memberRoutes);
|
|
app.use('/api/admin', adminRoutes);
|
|
app.use('/api/training', trainingRoutes);
|
|
app.use('/api/vehicles', vehicleRoutes);
|
|
app.use('/api/incidents', incidentRoutes);
|
|
app.use('/api/equipment', equipmentRoutes);
|
|
app.use('/api/atemschutz', atemschutzRoutes);
|
|
app.use('/api/nextcloud/talk', nextcloudRoutes);
|
|
app.use('/api/events', eventsRoutes);
|
|
app.use('/api/bookings', bookingRoutes);
|
|
app.use('/api/notifications', notificationRoutes);
|
|
app.use('/api/bookstack', bookstackRoutes);
|
|
app.use('/api/vikunja', vikunjaRoutes);
|
|
app.use('/api/bestellungen', bestellungRoutes);
|
|
app.use('/api/config', configRoutes);
|
|
app.use('/api/admin', serviceMonitorRoutes);
|
|
app.use('/api/admin/settings', settingsRoutes);
|
|
app.use('/api/settings', settingsRoutes);
|
|
app.use('/api/banners', bannerRoutes);
|
|
app.use('/api/permissions', permissionRoutes);
|
|
app.use('/api/ausruestungsanfragen', ausruestungsanfrageRoutes);
|
|
app.use('/api/issues', issueRoutes);
|
|
app.use('/api/buchungskategorien', buchungskategorieRoutes);
|
|
app.use('/api/checklisten', checklistRoutes);
|
|
app.use('/api/fahrzeug-typen', fahrzeugTypRoutes);
|
|
app.use('/api/ausruestung-typen', ausruestungTypRoutes);
|
|
app.use('/api/buchhaltung', buchhaltungRoutes);
|
|
app.use('/api/persoenliche-ausruestung', personalEquipmentRoutes);
|
|
|
|
// Static file serving for uploads (authenticated)
|
|
const uploadsDir = process.env.NODE_ENV === 'production' ? '/app/uploads' : path.resolve(__dirname, '../../uploads');
|
|
app.use('/uploads', authenticate, express.static(uploadsDir));
|
|
|
|
// 404 handler
|
|
app.use(notFoundHandler);
|
|
|
|
// Error handling middleware (must be last)
|
|
app.use(errorHandler);
|
|
|
|
export default app;
|