This commit is contained in:
Matthias Hochmeister
2026-03-13 21:01:54 +01:00
parent ab29c43735
commit b7b4fe2fc9
14 changed files with 566 additions and 60 deletions

View File

@@ -34,14 +34,20 @@ const authLimiter = rateLimit({
});
app.use('/api/auth', authLimiter);
// General rate limiter — skip auth routes (they have their own limiter above)
// 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) => req.path.startsWith('/auth'),
skip: (req) => {
if (req.path.startsWith('/auth')) return true;
const auth = req.headers.authorization;
return typeof auth === 'string' && auth.startsWith('Bearer ');
},
}));
// Body parsing middleware

View File

@@ -0,0 +1,2 @@
-- Remove mitglieds_nr column (replaced by fdisk_standesbuch_nr as the canonical member number)
ALTER TABLE mitglieder_profile DROP COLUMN IF EXISTS mitglieds_nr;

View File

@@ -0,0 +1,6 @@
-- Migration 032: Add FDISK-scraped profile fields to mitglieder_profile
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS geburtsort VARCHAR(128);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS geschlecht VARCHAR(1);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS beruf VARCHAR(255);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS wohnort VARCHAR(128);
ALTER TABLE mitglieder_profile ADD COLUMN IF NOT EXISTS plz VARCHAR(16);

View File

@@ -0,0 +1,13 @@
-- Migration 033: Create befoerderungen table (FDISK sync)
CREATE TABLE IF NOT EXISTS befoerderungen (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
datum DATE,
dienstgrad VARCHAR(64) NOT NULL,
fdisk_sync_key VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, fdisk_sync_key)
);
CREATE INDEX IF NOT EXISTS idx_befoerderungen_user_id ON befoerderungen(user_id);

View File

@@ -0,0 +1,16 @@
-- Migration 034: Create untersuchungen table (FDISK sync)
CREATE TABLE IF NOT EXISTS untersuchungen (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
datum DATE,
anmerkungen TEXT,
art VARCHAR(128) NOT NULL,
ergebnis VARCHAR(128),
fdisk_sync_key VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, fdisk_sync_key)
);
CREATE INDEX IF NOT EXISTS idx_untersuchungen_user_id ON untersuchungen(user_id);
CREATE INDEX IF NOT EXISTS idx_untersuchungen_art ON untersuchungen(user_id, art);

View File

@@ -0,0 +1,16 @@
-- Migration 035: Create fahrgenehmigungen table (FDISK sync)
CREATE TABLE IF NOT EXISTS fahrgenehmigungen (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
ausstellungsdatum DATE,
gueltig_bis DATE,
behoerde VARCHAR(128),
nummer VARCHAR(64),
klasse VARCHAR(128) NOT NULL,
fdisk_sync_key VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, fdisk_sync_key)
);
CREATE INDEX IF NOT EXISTS idx_fahrgenehmigungen_user_id ON fahrgenehmigungen(user_id);

View File

@@ -63,7 +63,6 @@ export interface MitgliederProfile {
id: string;
user_id: string;
mitglieds_nr: string | null;
fdisk_standesbuch_nr: string | null;
dienstgrad: DienstgradEnum | null;
dienstgrad_seit: Date | null;
@@ -86,6 +85,13 @@ export interface MitgliederProfile {
bemerkungen: string | null;
bild_url: string | null;
// FDISK-synced extended profile fields
geburtsort: string | null;
geschlecht: string | null;
beruf: string | null;
wohnort: string | null;
plz: string | null;
created_at: Date;
updated_at: Date;
}
@@ -143,7 +149,6 @@ export interface MemberListItem {
// profile fields (null when no profile exists)
profile_id: string | null;
mitglieds_nr: string | null;
fdisk_standesbuch_nr: string | null;
dienstgrad: DienstgradEnum | null;
funktion: FunktionEnum[];
@@ -156,7 +161,7 @@ export interface MemberListItem {
// Filter parameters for getAllMembers()
// ============================================================
export interface MemberFilters {
search?: string; // matches name, email, mitglieds_nr
search?: string; // matches name, email, fdisk_standesbuch_nr
status?: StatusEnum[];
dienstgrad?: DienstgradEnum[];
page?: number; // 1-based
@@ -173,7 +178,6 @@ export interface MemberFilters {
* status has a default; every other field may be omitted.
*/
export const CreateMemberProfileSchema = z.object({
mitglieds_nr: z.string().max(32).optional(),
fdisk_standesbuch_nr: z.string().max(32).optional(),
dienstgrad: z.enum(DIENSTGRAD_VALUES).optional(),
dienstgrad_seit: z.coerce.date().optional(),

View File

@@ -36,7 +36,6 @@ class MemberService {
-- profile columns (aliased with mp_ prefix to avoid collision)
mp.id AS mp_id,
mp.user_id AS mp_user_id,
mp.mitglieds_nr AS mp_mitglieds_nr,
mp.fdisk_standesbuch_nr AS mp_fdisk_standesbuch_nr,
mp.dienstgrad AS mp_dienstgrad,
mp.dienstgrad_seit AS mp_dienstgrad_seit,
@@ -54,6 +53,11 @@ class MemberService {
mp.schuhgroesse AS mp_schuhgroesse,
mp.bemerkungen AS mp_bemerkungen,
mp.bild_url AS mp_bild_url,
mp.geburtsort AS mp_geburtsort,
mp.geschlecht AS mp_geschlecht,
mp.beruf AS mp_beruf,
mp.wohnort AS mp_wohnort,
mp.plz AS mp_plz,
mp.created_at AS mp_created_at,
mp.updated_at AS mp_updated_at
FROM users u
@@ -83,7 +87,6 @@ class MemberService {
? {
id: row.mp_id,
user_id: row.mp_user_id,
mitglieds_nr: row.mp_mitglieds_nr,
fdisk_standesbuch_nr: row.mp_fdisk_standesbuch_nr ?? null,
dienstgrad: row.mp_dienstgrad,
dienstgrad_seit: row.mp_dienstgrad_seit,
@@ -101,6 +104,11 @@ class MemberService {
schuhgroesse: row.mp_schuhgroesse,
bemerkungen: row.mp_bemerkungen,
bild_url: row.mp_bild_url,
geburtsort: row.mp_geburtsort ?? null,
geschlecht: row.mp_geschlecht ?? null,
beruf: row.mp_beruf ?? null,
wohnort: row.mp_wohnort ?? null,
plz: row.mp_plz ?? null,
created_at: row.mp_created_at,
updated_at: row.mp_updated_at,
}
@@ -137,7 +145,6 @@ class MemberService {
OR u.email ILIKE $${paramIdx}
OR u.given_name ILIKE $${paramIdx}
OR u.family_name ILIKE $${paramIdx}
OR mp.mitglieds_nr ILIKE $${paramIdx}
)`);
values.push(`%${search}%`);
paramIdx++;
@@ -168,7 +175,6 @@ class MemberService {
u.profile_picture_url,
u.is_active,
mp.id AS profile_id,
mp.mitglieds_nr,
mp.fdisk_standesbuch_nr,
mp.dienstgrad,
mp.funktion,
@@ -204,7 +210,6 @@ class MemberService {
profile_picture_url: row.profile_picture_url,
is_active: row.is_active,
profile_id: row.profile_id ?? null,
mitglieds_nr: row.mitglieds_nr ?? null,
fdisk_standesbuch_nr: row.fdisk_standesbuch_nr ?? null,
dienstgrad: row.dienstgrad ?? null,
funktion: row.funktion ?? [],
@@ -286,7 +291,6 @@ class MemberService {
const query = `
INSERT INTO mitglieder_profile (
user_id,
mitglieds_nr,
fdisk_standesbuch_nr,
dienstgrad,
dienstgrad_seit,
@@ -306,14 +310,13 @@ class MemberService {
bild_url
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, $19
$11, $12, $13, $14, $15, $16, $17, $18
)
RETURNING *
`;
const values = [
userId,
data.mitglieds_nr ?? null,
data.fdisk_standesbuch_nr ?? null,
data.dienstgrad ?? null,
data.dienstgrad_seit ?? null,
@@ -392,7 +395,6 @@ class MemberService {
let paramIdx = 1;
const fieldMap: Record<string, any> = {
mitglieds_nr: rest.mitglieds_nr,
fdisk_standesbuch_nr: rest.fdisk_standesbuch_nr,
funktion: rest.funktion,
status: rest.status,