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>
73 lines
2.8 KiB
JavaScript
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);
|
|
}
|