feat: Apple CalDAV credentials form + connect/disconnect UI (BL-04)
Admin can now enter CalDAV URL, Apple-ID and app-specific password directly in Settings; credentials are tested live before saving and stored in sync_config (take precedence over .env); disconnect clears DB-stored credentials without server restart. Auto-sync interval (15 min, configurable via SYNC_INTERVAL_MINUTES) was already in place. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -297,6 +297,52 @@ router.post('/apple/sync', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/v1/calendar/apple/connect
|
||||
* Apple-CalDAV-Credentials speichern und Verbindung testen.
|
||||
* Body: { url, username, password }
|
||||
* Response: { ok: true, calendarCount: number }
|
||||
*/
|
||||
router.post('/apple/connect', requireAdmin, async (req, res) => {
|
||||
const { url, username, password } = req.body;
|
||||
if (!url || typeof url !== 'string' || !url.startsWith('http')) {
|
||||
return res.status(400).json({ error: 'url muss eine gültige HTTP(S)-URL sein.', code: 400 });
|
||||
}
|
||||
if (!username || typeof username !== 'string' || username.length > 254) {
|
||||
return res.status(400).json({ error: 'username fehlt oder ungültig.', code: 400 });
|
||||
}
|
||||
if (!password || typeof password !== 'string' || password.length < 1) {
|
||||
return res.status(400).json({ error: 'password fehlt.', code: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// Zuerst temporär setzen, damit testConnection() sie findet
|
||||
appleCalendar.saveCredentials(url.trim(), username.trim(), password);
|
||||
const result = await appleCalendar.testConnection();
|
||||
res.json({ ok: true, calendarCount: result.calendarCount });
|
||||
} catch (err) {
|
||||
// Bei Fehler: gespeicherte Credentials wieder löschen
|
||||
appleCalendar.clearCredentials();
|
||||
console.error('[calendar/apple/connect]', err);
|
||||
res.status(400).json({ error: err.message.replace('[Apple] ', ''), code: 400 });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/v1/calendar/apple/disconnect
|
||||
* Apple-CalDAV-Credentials löschen.
|
||||
* Response: 204
|
||||
*/
|
||||
router.delete('/apple/disconnect', requireAdmin, (req, res) => {
|
||||
try {
|
||||
appleCalendar.clearCredentials();
|
||||
res.status(204).end();
|
||||
} catch (err) {
|
||||
console.error('[calendar/apple/disconnect]', err);
|
||||
res.status(500).json({ error: 'Interner Fehler', code: 500 });
|
||||
}
|
||||
});
|
||||
|
||||
// --------------------------------------------------------
|
||||
// GET /api/v1/calendar/:id
|
||||
// Einzelnen Termin abrufen.
|
||||
|
||||
@@ -36,18 +36,60 @@ function cfgSet(key, value) {
|
||||
`).run(key, value);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Credentials: sync_config hat Vorrang vor .env
|
||||
// --------------------------------------------------------
|
||||
|
||||
function getCredentials() {
|
||||
const url = cfgGet('apple_caldav_url') || process.env.APPLE_CALDAV_URL;
|
||||
const username = cfgGet('apple_username') || process.env.APPLE_USERNAME;
|
||||
const password = cfgGet('apple_app_password') || process.env.APPLE_APP_SPECIFIC_PASSWORD;
|
||||
if (!url || !username || !password) return null;
|
||||
return { url, username, password };
|
||||
}
|
||||
|
||||
function saveCredentials(url, username, password) {
|
||||
cfgSet('apple_caldav_url', url);
|
||||
cfgSet('apple_username', username);
|
||||
cfgSet('apple_app_password', password);
|
||||
}
|
||||
|
||||
function clearCredentials() {
|
||||
['apple_caldav_url', 'apple_username', 'apple_app_password', 'apple_last_sync'].forEach(cfgDel);
|
||||
console.log('[Apple] Verbindung getrennt.');
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Verbindungsstatus
|
||||
// --------------------------------------------------------
|
||||
|
||||
function getStatus() {
|
||||
const configured = !!(
|
||||
process.env.APPLE_CALDAV_URL &&
|
||||
process.env.APPLE_USERNAME &&
|
||||
process.env.APPLE_APP_SPECIFIC_PASSWORD
|
||||
);
|
||||
const lastSync = cfgGet('apple_last_sync');
|
||||
return { configured, lastSync };
|
||||
const creds = getCredentials();
|
||||
const configured = !!creds;
|
||||
const connected = !!(cfgGet('apple_caldav_url')); // via UI gespeichert
|
||||
const lastSync = cfgGet('apple_last_sync');
|
||||
return { configured, connected, lastSync };
|
||||
}
|
||||
|
||||
/**
|
||||
* Verbindungstest: CalDAV-Client erstellen und Kalender abrufen.
|
||||
* Wirft einen Fehler wenn die Credentials ungültig sind.
|
||||
*/
|
||||
async function testConnection() {
|
||||
const creds = getCredentials();
|
||||
if (!creds) throw new Error('[Apple] Keine Credentials konfiguriert.');
|
||||
|
||||
const { createDAVClient } = await import('tsdav');
|
||||
const client = await createDAVClient({
|
||||
serverUrl: creds.url,
|
||||
credentials: { username: creds.username, password: creds.password },
|
||||
authMethod: 'Basic',
|
||||
defaultAccountType: 'caldav',
|
||||
});
|
||||
|
||||
const calendars = await client.fetchCalendars();
|
||||
if (!calendars.length) throw new Error('[Apple] Verbunden, aber keine Kalender gefunden.');
|
||||
return { ok: true, calendarCount: calendars.length };
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
@@ -190,21 +232,18 @@ function escapeICS(str) {
|
||||
* Outbound: lokale Termine (external_source='local', external_calendar_id IS NULL) → iCloud
|
||||
*/
|
||||
async function sync() {
|
||||
const caldavUrl = process.env.APPLE_CALDAV_URL;
|
||||
const username = process.env.APPLE_USERNAME;
|
||||
const password = process.env.APPLE_APP_SPECIFIC_PASSWORD;
|
||||
|
||||
if (!caldavUrl || !username || !password) {
|
||||
throw new Error('[Apple] APPLE_CALDAV_URL, APPLE_USERNAME und APPLE_APP_SPECIFIC_PASSWORD müssen gesetzt sein.');
|
||||
const creds = getCredentials();
|
||||
if (!creds) {
|
||||
throw new Error('[Apple] Keine Credentials konfiguriert (weder in DB noch in .env).');
|
||||
}
|
||||
|
||||
// tsdav ist ESM-only — dynamischer Import aus CommonJS
|
||||
const { createDAVClient } = await import('tsdav');
|
||||
|
||||
const client = await createDAVClient({
|
||||
serverUrl: caldavUrl,
|
||||
credentials: { username, password },
|
||||
authMethod: 'Basic',
|
||||
serverUrl: creds.url,
|
||||
credentials: { username: creds.username, password: creds.password },
|
||||
authMethod: 'Basic',
|
||||
defaultAccountType: 'caldav',
|
||||
});
|
||||
|
||||
@@ -289,4 +328,4 @@ async function sync() {
|
||||
console.log(`[Apple] Sync abgeschlossen — ${calObjects.length} Objekte inbound, ${localEvents.length} lokal → iCloud.`);
|
||||
}
|
||||
|
||||
module.exports = { sync, getStatus };
|
||||
module.exports = { sync, getStatus, saveCredentials, clearCredentials, testConnection };
|
||||
|
||||
Reference in New Issue
Block a user