Files
perinco-Pulse/server/src/server.js
T
schakal00783andClaude Opus 5 ef676a95f4 feat: perinco Pulse - Zeiterfassung fuer perinco
Erste Fassung, live unter https://pulse.perinco.de.

Frontend React 19 + TypeScript + Tailwind 4 (Vite), Backend Express mit
mysql2 auf MariaDB, Exporte als PDF (pdfkit), Excel (exceljs) und CSV.
Aufbau, Designsystem und die sieben Themes kommen aus dem perinco CRM.

Fachlich:
- Buchung mit Wochen-, Monats- und Jahresansicht, Zeiten als 1:23 oder 1,5
- Projekte mit n Budgettoepfen: Laufzeit, Volumen, Sperre bei Ablauf oder
  aufgebrauchtem Budget, Buchung nur auf den passenden Topf
- Kunden, Team mit Hierarchie, Reports im Harvest-Stil, Monatsexporte
- Rollen user/lead/management/admin, doppelt geprueft (Menue und Backend)
- SSO gegen Microsoft Entra ID, Uebernahme der AD-Benutzer per Drag & Drop

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 22:52:20 +02:00

73 lines
2.8 KiB
JavaScript

import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import { initDatabase, pool } from './db.js';
import { requireAuth, requireRole } from './middleware/auth.js';
import authRoutes from './routes/auth.js';
import userRoutes from './routes/users.js';
import customerRoutes from './routes/customers.js';
import projectRoutes from './routes/projects.js';
import entryRoutes from './routes/entries.js';
import dashboardRoutes from './routes/dashboard.js';
import reportRoutes from './routes/reports.js';
import exportRoutes from './routes/exports.js';
import settingsRoutes from './routes/settings.js';
import directoryRoutes from './routes/directory.js';
const app = express();
const PORT = Number(process.env.PORT || 5010);
const HOST = process.env.HOST || '127.0.0.1';
// Apache steht davor - fuer req.ip (Login-Bremse) die echte Client-IP nehmen.
app.set('trust proxy', 'loopback');
app.disable('x-powered-by');
// Logos kommen als Data-URL.
app.use(express.json({ limit: '4mb' }));
app.use(
cors({
origin: (process.env.CORS_ORIGINS || 'http://localhost:3110,http://127.0.0.1:3110').split(','),
})
);
app.get('/api/health', async (req, res) => {
try {
await pool.query('SELECT 1');
res.json({ ok: true, db: 'up' });
} catch {
res.status(503).json({ ok: false, db: 'down' });
}
});
app.use('/api/auth', authRoutes);
app.use('/api/users', requireAuth, userRoutes);
app.use('/api/customers', requireAuth, requireRole('lead'), customerRoutes);
app.use('/api/projects', requireAuth, projectRoutes);
app.use('/api/entries', requireAuth, entryRoutes);
app.use('/api/dashboard', requireAuth, requireRole('lead'), dashboardRoutes);
app.use('/api/reports', requireAuth, requireRole('lead'), reportRoutes);
app.use('/api/exports', requireAuth, exportRoutes);
app.use('/api/settings', requireAuth, settingsRoutes);
app.use('/api/directory', requireAuth, requireRole('management'), directoryRoutes);
app.use('/api', (req, res) => res.status(404).json({ error: 'Endpunkt nicht gefunden.' }));
app.use((err, req, res, next) => {
if (res.headersSent) return next(err);
if (err?.type === 'entity.parse.failed') return res.status(400).json({ error: 'Ungültige Anfrage.' });
if (err?.type === 'entity.too.large') return res.status(413).json({ error: 'Die Anfrage ist zu groß.' });
if (err?.code === 'ER_ROW_IS_REFERENCED_2') {
return res.status(409).json({ error: 'Der Datensatz wird noch verwendet und kann nicht gelöscht werden.' });
}
console.error('[Pulse] Unbehandelter Fehler:', err);
res.status(500).json({ error: 'Interner Serverfehler.' });
});
try {
await initDatabase();
app.listen(PORT, HOST, () => console.log(`[Pulse] Backend läuft auf http://${HOST}:${PORT}`));
} catch (err) {
console.error('[Pulse] Start fehlgeschlagen:', err);
process.exit(1);
}