reset user data
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 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,
|
||||
* workspaces, screens, projects,
|
||||
* tasks, skills, processes, tools, extensions (+ item_chats),
|
||||
* task_logs, queue_jobs, terminal_containers
|
||||
* - Filesystem: entire $DATA_PATH/<email>/ directory
|
||||
* (home, settings, state, workspaces, 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, workspaces, 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',
|
||||
'workspaces',
|
||||
'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);
|
||||
Reference in New Issue
Block a user