636fe0df8f
- Aufgabe 2: vier sichere Biome-Regeln (useImportType pfadgebunden auf apps/web+packages, noUselessEscapeInRegex, useConst, useExponentiationOperator) sowie fuenf ungesicherte Regeln (useNodejsImportProtocol, useLiteralKeys, useOptionalChain, useTemplate, useParseIntRadix) angewendet und den gesamten Diff von Hand gelesen (ldap.service.ts zeichenweise gegen Gross-/Kleinschreibung der AD-Merkmale, auth.service.ts/jwt.strategy.ts gegen Durchwinken bei fehlender Sitzung geprueft) - noUselessSwitchCase bleibt bewusst stehen (tender-normalizer.service.ts:60, die Fallmarke dokumentiert Absicht) - Toter Code (D-03): fuenf folgenlose Auffangvariablen entfernt, eine nicht benutzte Funktion (forSystemQuery, Pruefskript) entfernt, ein positionsgebundener Dekoratorparameter umbenannt (current-user.decorator.ts), fuenf Symptomfunde entfernt und als Folgeaufgaben zu melden (siehe unten) - Sechs weitere, im Plan nicht namentlich gelistete aber gleich-kategorische Dead-Code-Fundstellen in Testdateien zusaetzlich bereinigt (groups.service.spec.ts, cert-manager.test.tsx, ldap.service.spec.ts, prisma-tenant.extension.spec.ts x3) — noetig, um die vom Plan selbst verlangten Nullstaende bei noUnusedVariables/ noUnusedImports/noUnusedFunctionParameters zu erreichen Dekoratordaten aus apps/api unveraendert (593 Zeilen, sha256 6e1583f1...). Endstand 620 Befunde (541 echt, 79 Test) statt der im Plan geschaetzten 621/542 — eine Differenz von 1, weil das Streichen des Namens aus `catch (e: any)` in calendar.service.ts (Symptom-Fix) den dort ebenfalls gemeldeten noExplicitAny-Befund miteliminiert; das ist eine erwuenschte Nebenwirkung, keine Regression. Fehlerstufe 0, beide Testlaeufe punktgleich gruen (69/1124, 66/459), pnpm type-check 4/4, pnpm lint --force 5/5. Folgeaufgaben aus D-03 (nicht in diesem Vorgang behoben): - force-password-change.interceptor.ts: Freigabeliste prueft nur den Pfad, nicht die HTTP-Methode - change-password/page.tsx: nach erzwungenem Wechsel bleibt die Person auf der Seite stehen (keine Weiterleitung, keine Aktualisierung der Benutzerablage) - VehicleTable.tsx: Loeschschaltflaeche hat keinen Besetztzustand, laesst sich doppelt ausloesen - SplitTab.tsx: downloadAllAsZip erhielt eine ungenutzte Uebersetzungsfunktion, Hinweis auf fest verdrahtete Texte im Zip-Pfad Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
138 lines
4.1 KiB
TypeScript
138 lines
4.1 KiB
TypeScript
import { Injectable, Logger } from '@nestjs/common';
|
|
import { DAVClient } from 'tsdav';
|
|
import * as ical from 'node-ical';
|
|
import { CalendarEvent, CalendarProvider } from '../calendar.service';
|
|
|
|
/**
|
|
* CalDAV calendar provider — uses tsdav for protocol handling.
|
|
*
|
|
* Connects via Basic auth (username + decrypted password), fetches calendars,
|
|
* then fetchCalendarObjects with time-range filter (A2).
|
|
* Parses returned iCal data with node-ical and maps to CalendarEvent.
|
|
*/
|
|
@Injectable()
|
|
export class CalDAVProvider implements CalendarProvider {
|
|
private readonly logger = new Logger(CalDAVProvider.name);
|
|
|
|
/**
|
|
* Fetches events from a CalDAV server within the specified date range.
|
|
* Uses tsdav's time-range filter to only fetch relevant events (A2).
|
|
*/
|
|
async fetchEvents(
|
|
source: { url: string; username?: string; password?: string; id: string; color?: string | null },
|
|
from: Date,
|
|
to: Date,
|
|
): Promise<CalendarEvent[]> {
|
|
const events: CalendarEvent[] = [];
|
|
|
|
try {
|
|
const client = await this.createClient(source);
|
|
const calendars = await client.fetchCalendars();
|
|
|
|
for (const calendar of calendars) {
|
|
const objects = await client.fetchCalendarObjects({
|
|
calendar,
|
|
timeRange: {
|
|
start: from.toISOString(),
|
|
end: to.toISOString(),
|
|
},
|
|
expand: true,
|
|
});
|
|
|
|
for (const obj of objects) {
|
|
if (!obj.data) continue;
|
|
|
|
try {
|
|
const parsed = ical.sync.parseICS(obj.data);
|
|
|
|
for (const key of Object.keys(parsed)) {
|
|
const component = parsed[key];
|
|
if (component?.type !== 'VEVENT') continue;
|
|
|
|
const vevent = component as ical.VEvent;
|
|
const start = new Date(vevent.start);
|
|
const end = vevent.end
|
|
? new Date(vevent.end)
|
|
: new Date(start.getTime() + 3600000);
|
|
|
|
events.push({
|
|
id: `${source.id}-${vevent.uid || key}`,
|
|
sourceId: source.id,
|
|
title: extractString(vevent.summary),
|
|
start,
|
|
end,
|
|
allDay: vevent.datetype === 'date',
|
|
location: vevent.location
|
|
? extractString(vevent.location)
|
|
: undefined,
|
|
description: vevent.description
|
|
? extractString(vevent.description)
|
|
: undefined,
|
|
color: source.color ?? undefined,
|
|
});
|
|
}
|
|
} catch (parseErr) {
|
|
this.logger.warn(
|
|
`Failed to parse CalDAV object: ${(parseErr as Error).message}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(
|
|
`Failed to fetch CalDAV events from ${source.url}: ${(error as Error).message}`,
|
|
);
|
|
throw error;
|
|
}
|
|
|
|
return events;
|
|
}
|
|
|
|
/**
|
|
* Tests connection by attempting login and listing calendars.
|
|
*/
|
|
async testConnection(
|
|
source: { url: string; username?: string; password?: string; id: string },
|
|
): Promise<boolean> {
|
|
try {
|
|
const client = await this.createClient(source);
|
|
const calendars = await client.fetchCalendars();
|
|
return calendars.length > 0;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a tsdav DAVClient with Basic auth and logs in.
|
|
*/
|
|
private async createClient(
|
|
source: { url: string; username?: string; password?: string },
|
|
): Promise<DAVClient> {
|
|
const client = new DAVClient({
|
|
serverUrl: source.url,
|
|
credentials: {
|
|
username: source.username || '',
|
|
password: source.password || '',
|
|
},
|
|
authMethod: 'Basic',
|
|
defaultAccountType: 'caldav',
|
|
});
|
|
|
|
await client.login();
|
|
return client;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts a plain string from a node-ical ParameterValue.
|
|
*/
|
|
function extractString(
|
|
value: ical.ParameterValue | string | undefined,
|
|
): string {
|
|
if (!value) return '';
|
|
if (typeof value === 'string') return value;
|
|
if (typeof value === 'object' && 'val' in value) return String(value.val);
|
|
return String(value);
|
|
}
|