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>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { pool } from '../db.js';
|
||||
|
||||
export const ROLES = ['user', 'lead', 'management', 'admin'];
|
||||
const RANK = { user: 0, lead: 1, management: 2, admin: 3 };
|
||||
|
||||
function secret() {
|
||||
const value = process.env.JWT_SECRET;
|
||||
if (!value || value.length < 16) throw new Error('JWT_SECRET fehlt oder ist zu kurz (min. 16 Zeichen).');
|
||||
return value;
|
||||
}
|
||||
|
||||
export function signToken(user) {
|
||||
return jwt.sign({ sub: user.id, role: user.role }, secret(), {
|
||||
expiresIn: process.env.JWT_EXPIRES_IN || '12h',
|
||||
});
|
||||
}
|
||||
|
||||
export function signShort(payload, expiresIn = '10m') {
|
||||
return jwt.sign(payload, secret(), { expiresIn });
|
||||
}
|
||||
|
||||
export function verifyShort(token) {
|
||||
return jwt.verify(token, secret());
|
||||
}
|
||||
|
||||
export const USER_COLUMNS = `id, username, email, name, role, manager_id, job_title, is_external, company,
|
||||
source, external_id, weekly_hours, color, theme, theme_mode, is_active, last_login_at, created_at, updated_at`;
|
||||
|
||||
/**
|
||||
* Prueft das Bearer-Token und laedt den Benutzer bei jeder Anfrage frisch aus
|
||||
* der Datenbank - ein deaktiviertes Konto ist sofort draussen und eine
|
||||
* geaenderte Rolle gilt sofort.
|
||||
*/
|
||||
export async function requireAuth(req, res, next) {
|
||||
const header = req.headers.authorization || '';
|
||||
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
|
||||
if (!token) return res.status(401).json({ error: 'Nicht angemeldet.' });
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = jwt.verify(token, secret());
|
||||
} catch {
|
||||
return res.status(401).json({ error: 'Sitzung abgelaufen. Bitte neu anmelden.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const [rows] = await pool.query(`SELECT ${USER_COLUMNS} FROM pulse_users WHERE id = ? LIMIT 1`, [payload.sub]);
|
||||
const user = rows[0];
|
||||
if (!user) return res.status(401).json({ error: 'Benutzer existiert nicht mehr.' });
|
||||
if (!user.is_active) return res.status(401).json({ error: 'Benutzerkonto ist deaktiviert.' });
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
}
|
||||
|
||||
export function hasRole(user, minimum) {
|
||||
return RANK[user?.role] >= RANK[minimum];
|
||||
}
|
||||
|
||||
/** Mindestrolle: lead < management < admin. */
|
||||
export function requireRole(minimum) {
|
||||
return (req, res, next) => {
|
||||
if (!hasRole(req.user, minimum)) {
|
||||
return res.status(403).json({ error: 'Für diese Aktion fehlen die Berechtigungen.' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
export function publicUser(row) {
|
||||
return {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
email: row.email,
|
||||
name: row.name,
|
||||
role: row.role,
|
||||
manager_id: row.manager_id ?? null,
|
||||
job_title: row.job_title ?? null,
|
||||
is_external: Boolean(row.is_external),
|
||||
company: row.company ?? null,
|
||||
source: row.source,
|
||||
weekly_hours: row.weekly_hours == null ? null : Number(row.weekly_hours),
|
||||
color: row.color ?? null,
|
||||
theme: row.theme ?? null,
|
||||
theme_mode: row.theme_mode ?? null,
|
||||
is_active: Boolean(row.is_active),
|
||||
last_login_at: row.last_login_at ?? null,
|
||||
created_at: row.created_at,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user