176 lines
5.6 KiB
TypeScript
176 lines
5.6 KiB
TypeScript
/**
|
|
* Reset all user data while keeping auth credentials.
|
|
*
|
|
* Deletes:
|
|
* - DB: user_settings, user_state, user_integrations, dock_configs,
|
|
* chat_sessions (cascades chat_messages), chat_groups,
|
|
* dashboards, screens, projects,
|
|
* tasks, skills, processes, tools, extensions (+ item_chats),
|
|
* task_logs, queue_jobs, terminal_containers
|
|
* - Filesystem: entire $DATA_PATH/<email>/ directory
|
|
* (home, settings, state, dashboards, chat_sessions, emails.db,
|
|
* Gmail, skills, tools, tasks, processes, extensions, logs, cache, etc.)
|
|
* - Queue job files: $DATA_PATH/queue/jobs/*.json owned by user
|
|
* - Terminal containers map: removes user entry from terminal-containers.json
|
|
*
|
|
* Preserves:
|
|
* - users table row (account, password, role, status)
|
|
* - passkeys table rows
|
|
* - passkey_challenges, token_blacklist
|
|
*
|
|
* Usage: bun run scripts/reset-user-data.ts <email>
|
|
* bun run scripts/reset-user-data.ts <email> --yes (skip confirmation)
|
|
*/
|
|
|
|
import { join } from 'node:path';
|
|
import { rm, readdir, unlink } from 'node:fs/promises';
|
|
import { db } from 'officerdb/db';
|
|
import { users } from 'officerdb/schema';
|
|
import { eq, sql } from 'drizzle-orm';
|
|
|
|
const DATA_PATH = process.env.DATA_PATH ?? join(process.cwd(), 'data');
|
|
|
|
const email = process.argv[2];
|
|
const skipConfirm = process.argv.includes('--yes');
|
|
|
|
if (!email) {
|
|
console.error('Usage: bun run scripts/reset-user-data.ts <email> [--yes]');
|
|
process.exit(1);
|
|
}
|
|
|
|
// ── Resolve user ──
|
|
|
|
const [user] = await db.select({ id: users.id, email: users.email }).from(users).where(eq(users.email, email));
|
|
|
|
if (!user) {
|
|
console.error(`User not found: ${email}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`\nUser: ${user.email} (id: ${user.id})`);
|
|
console.log(`Data dir: ${join(DATA_PATH, email)}`);
|
|
console.log('\nThis will delete ALL user data (settings, chats, dashboards, emails, home dir, etc.)');
|
|
console.log('Auth credentials (account, passkeys) will be preserved.\n');
|
|
|
|
if (!skipConfirm) {
|
|
process.stdout.write('Continue? [y/N] ');
|
|
const response = await new Promise<string>((resolve) => {
|
|
process.stdin.once('data', (data) => resolve(data.toString().trim()));
|
|
});
|
|
if (response.toLowerCase() !== 'y') {
|
|
console.log('Aborted.');
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
const userId = user.id;
|
|
|
|
// ── Database cleanup ──
|
|
// All these tables have ON DELETE CASCADE from users, but we don't want to delete the user.
|
|
// Delete explicitly by user_id.
|
|
|
|
console.log('\n── Database ──');
|
|
|
|
const tables = [
|
|
'user_settings',
|
|
'user_state',
|
|
'user_integrations',
|
|
'dock_configs',
|
|
'chat_sessions', // cascades chat_messages
|
|
'chat_groups',
|
|
'dashboards',
|
|
'screens',
|
|
'projects',
|
|
'task_logs',
|
|
'queue_jobs',
|
|
'terminal_containers',
|
|
];
|
|
|
|
for (const table of tables) {
|
|
const result = await db.execute(sql.raw(`DELETE FROM ${table} WHERE user_id = ${userId}`));
|
|
const count = result.length ?? 0;
|
|
console.log(` ${table}: ${count} rows deleted`);
|
|
}
|
|
|
|
// Agent items: tasks, skills, processes, tools, extensions
|
|
// These have nullable user_id — delete only rows belonging to this user
|
|
const agentTables = ['tasks', 'skills', 'processes', 'tools', 'extensions'];
|
|
|
|
for (const table of agentTables) {
|
|
// First collect item IDs to clean up item_chats
|
|
const items = await db.execute(sql.raw(`SELECT id FROM ${table} WHERE user_id = ${userId}`));
|
|
if (items.length > 0) {
|
|
const ids = items.map((r: Record<string, unknown>) => r.id).join(',');
|
|
const chatResult = await db.execute(sql.raw(`DELETE FROM item_chats WHERE item_type = '${table}' AND item_id IN (${ids})`));
|
|
console.log(` item_chats (${table}): ${chatResult.length ?? 0} rows deleted`);
|
|
}
|
|
const result = await db.execute(sql.raw(`DELETE FROM ${table} WHERE user_id = ${userId}`));
|
|
console.log(` ${table}: ${result.length ?? 0} rows deleted`);
|
|
}
|
|
|
|
// ── Queue job files ──
|
|
|
|
console.log('\n── Queue job files ──');
|
|
const queueDir = join(DATA_PATH, 'queue', 'jobs');
|
|
try {
|
|
const entries = await readdir(queueDir);
|
|
let deleted = 0;
|
|
for (const entry of entries) {
|
|
if (!entry.endsWith('.json')) continue;
|
|
try {
|
|
const file = Bun.file(join(queueDir, entry));
|
|
const job = await file.json();
|
|
if (job.userId === email) {
|
|
await unlink(join(queueDir, entry));
|
|
deleted++;
|
|
}
|
|
} catch {
|
|
// skip unreadable files
|
|
}
|
|
}
|
|
console.log(` ${deleted} job files deleted`);
|
|
} catch {
|
|
console.log(' queue dir not found, skipping');
|
|
}
|
|
|
|
// ── Terminal containers map ──
|
|
|
|
console.log('\n── Terminal containers ──');
|
|
const containerMapPath = join(DATA_PATH, 'terminal-containers.json');
|
|
try {
|
|
const file = Bun.file(containerMapPath);
|
|
if (await file.exists()) {
|
|
const map = await file.json();
|
|
let changed = false;
|
|
for (const key of Object.keys(map)) {
|
|
if (key === email || map[key]?.email === email) {
|
|
delete map[key];
|
|
changed = true;
|
|
}
|
|
}
|
|
if (changed) {
|
|
await Bun.write(containerMapPath, JSON.stringify(map, null, 2));
|
|
console.log(' removed from terminal-containers.json');
|
|
} else {
|
|
console.log(' no entry found');
|
|
}
|
|
}
|
|
} catch {
|
|
console.log(' terminal-containers.json not found, skipping');
|
|
}
|
|
|
|
// ── Filesystem ──
|
|
|
|
console.log('\n── Filesystem ──');
|
|
const userDir = join(DATA_PATH, email);
|
|
try {
|
|
await rm(userDir, { recursive: true, force: true });
|
|
console.log(` removed ${userDir}`);
|
|
} catch (err: unknown) {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
console.log(` failed to remove ${userDir}: ${msg}`);
|
|
}
|
|
|
|
console.log('\nDone. User auth preserved, all data wiped.');
|
|
process.exit(0);
|