mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
66 lines
3.6 KiB
TypeScript
66 lines
3.6 KiB
TypeScript
import fs from 'node:fs';
|
|
import { db } from '../db/database';
|
|
import { pluginsDataRoot } from '../nest/plugins/paths';
|
|
import { removeUserFromBudgetItems } from './budgetService';
|
|
|
|
/**
|
|
* Erase a user's PLUGIN-held data on account deletion. Two parts:
|
|
* 1. Host-side per-user plugin tables (encrypted config values, OAuth access/refresh
|
|
* tokens, in-flight OAuth state) — deleted directly; these live in trek.db, not in
|
|
* a plugin's own db, so nothing else ever removes them.
|
|
* 2. A durable erasure row per plugin that holds `hook:user-data`, so its OWN db is
|
|
* purged of the user (drained to the plugin when it is next active).
|
|
*
|
|
* Runs in the core deletion path (NOT via the plugin runtime), so it works even when
|
|
* TREK_PLUGINS_ENABLED=false or before the runtime has booted — otherwise a deletion in
|
|
* those windows would leave the user's plugin data behind forever. Best-effort per table
|
|
* so a slimmed-down schema (some tests) can't fail the user deletion itself.
|
|
*/
|
|
export function erasePluginUserData(userId: number): void {
|
|
for (const table of ['plugin_user_config', 'plugin_oauth_tokens', 'plugin_oauth_state']) {
|
|
try { db.prepare(`DELETE FROM ${table} WHERE user_id = ?`).run(userId); } catch { /* table absent (slim schema) */ }
|
|
}
|
|
try {
|
|
const rows = db.prepare('SELECT id, permissions FROM plugins').all() as Array<{ id: string; permissions: string | null }>;
|
|
const installed = new Set(rows.map((r) => r.id));
|
|
const insert = db.prepare('INSERT OR IGNORE INTO plugin_user_erasure_queue (plugin_id, user_id) VALUES (?, ?)');
|
|
for (const r of rows) {
|
|
let perms: unknown;
|
|
try { perms = JSON.parse(r.permissions ?? '[]'); } catch { perms = []; }
|
|
if (Array.isArray(perms) && perms.includes('hook:user-data')) insert.run(r.id, userId);
|
|
}
|
|
// Also enqueue for plugins UNINSTALLED with their data retained (deleteData=false):
|
|
// their data dir still holds the user's rows and a same-id reinstall would re-adopt
|
|
// them. No permissions record survives uninstall, so we can't check hook:user-data —
|
|
// enqueue for every orphan data dir; the row sits inert (no FK) and drains only if
|
|
// that id is reinstalled + active (erasure delivery is a duty, not grant-gated).
|
|
try {
|
|
for (const entry of fs.readdirSync(pluginsDataRoot(), { withFileTypes: true })) {
|
|
if (entry.isDirectory() && !installed.has(entry.name)) insert.run(entry.name, userId);
|
|
}
|
|
} catch { /* no plugin data root yet */ }
|
|
} catch { /* plugins / queue table absent (slim schema) */ }
|
|
}
|
|
|
|
function cleanupUserReferences(userId: number): void {
|
|
db.prepare('UPDATE trip_members SET invited_by = NULL WHERE invited_by = ?').run(userId);
|
|
db.prepare('UPDATE budget_items SET paid_by_user_id = NULL WHERE paid_by_user_id = ?').run(userId);
|
|
removeUserFromBudgetItems(userId);
|
|
db.prepare('DELETE FROM share_tokens WHERE created_by = ?').run(userId);
|
|
db.prepare('DELETE FROM journey_share_tokens WHERE created_by = ?').run(userId);
|
|
// Owned journeys cascade-delete their entries/contributors/share_tokens/photos via journey_id FKs
|
|
db.prepare('DELETE FROM journeys WHERE user_id = ?').run(userId);
|
|
// Entries authored on other users' journeys (not covered by the cascade above)
|
|
db.prepare('DELETE FROM journey_entries WHERE author_id = ?').run(userId);
|
|
db.prepare('DELETE FROM journey_contributors WHERE user_id = ?').run(userId);
|
|
}
|
|
|
|
export function deleteUserCompletely(userId: number): void {
|
|
const tx = db.transaction((id: number) => {
|
|
cleanupUserReferences(id);
|
|
erasePluginUserData(id);
|
|
db.prepare('DELETE FROM users WHERE id = ?').run(id);
|
|
});
|
|
tx(userId);
|
|
}
|