Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/lib/server/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ export function getDb(): Database.Database {
return db;
}

export function deleteOrganization(db: Database.Database, orgId: string): void {
const tx = db.transaction(() => {
// Instance events do not cascade, so remove them before their instances.
db.prepare(
'DELETE FROM instance_events WHERE instance_id IN (SELECT id FROM instances WHERE org_id = ?)',
).run(orgId);
db.prepare('DELETE FROM instances WHERE org_id = ?').run(orgId);
db.prepare('DELETE FROM org_invitations WHERE org_id = ?').run(orgId);
db.prepare('DELETE FROM org_members WHERE org_id = ?').run(orgId);
db.prepare('DELETE FROM organizations WHERE id = ?').run(orgId);
});

tx();
}

// Fresh-install schema. Arc Launchpad creates its SQLite database on first
// run; there is no upgrade path from older databases, so this is a single
// flat set of CREATE TABLE statements rather than an incremental migration
Expand Down
36 changes: 36 additions & 0 deletions src/lib/server/orgDeletion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'vitest';
import { deleteOrganization, getDb } from './db';

describe('organization deletion', () => {
it('deletes instances and their retained data before the organization', () => {
const db = getDb();
db.exec('DELETE FROM instance_events; DELETE FROM instances; DELETE FROM org_members; DELETE FROM organizations; DELETE FROM users;');
db.prepare('INSERT INTO users (id, email) VALUES (?, ?)').run('user-1', 'user@example.test');
db.prepare('INSERT INTO organizations (id, name, owner_user_id) VALUES (?, ?, ?)').run('org-1', 'Test org', 'user-1');
db.prepare('INSERT INTO org_members (org_id, user_id, role) VALUES (?, ?, ?)').run('org-1', 'user-1', 'owner');
db.prepare('INSERT INTO instances (id, org_id, resource_id, admin_token) VALUES (?, ?, ?, ?)').run(
'instance-1',
'org-1',
'resource-1',
'secret-token',
);
db.prepare('INSERT INTO instance_events (instance_id, event_type) VALUES (?, ?)').run('instance-1', 'connected');
db.prepare('INSERT INTO org_invitations (id, org_id, email, token, invited_by, expires_at) VALUES (?, ?, ?, ?, ?, ?)').run(
'invite-1',
'org-1',
'invitee@example.test',
'invite-token',
'user-1',
'2099-01-01T00:00:00.000Z',
);

deleteOrganization(db, 'org-1');

expect(db.prepare('SELECT 1 FROM organizations WHERE id = ?').get('org-1')).toBeUndefined();
expect(db.prepare('SELECT 1 FROM org_members WHERE org_id = ?').get('org-1')).toBeUndefined();
expect(db.prepare('SELECT 1 FROM org_invitations WHERE org_id = ?').get('org-1')).toBeUndefined();
expect(db.prepare('SELECT 1 FROM instances WHERE org_id = ?').get('org-1')).toBeUndefined();
expect(db.prepare('SELECT 1 FROM instance_events WHERE instance_id = ?').get('instance-1')).toBeUndefined();
expect(db.prepare('SELECT 1 FROM instances WHERE admin_token = ?').get('secret-token')).toBeUndefined();
});
});
10 changes: 2 additions & 8 deletions src/routes/api/v1/admin/orgs/[org_id]/+server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getDb } from '$lib/server/db';
import { deleteOrganization, getDb } from '$lib/server/db';
import { logOperatorAction } from '$lib/server/audit';

export const PATCH: RequestHandler = async ({ locals, params, request }) => {
Expand Down Expand Up @@ -40,13 +40,7 @@ export const DELETE: RequestHandler = async ({ locals, params }) => {

// Removing an org drops its instance connections, members, and pending invites.
// Connections are just stored URLs/tokens — nothing external to tear down.
const tx = db.transaction(() => {
db.prepare("UPDATE instances SET deleted_at = datetime('now'), status = 'deleted' WHERE org_id = ? AND deleted_at IS NULL").run(params.org_id);
db.prepare('DELETE FROM org_invitations WHERE org_id = ?').run(params.org_id);
db.prepare('DELETE FROM org_members WHERE org_id = ?').run(params.org_id);
db.prepare('DELETE FROM organizations WHERE id = ?').run(params.org_id);
});
tx();
deleteOrganization(db, params.org_id);

logOperatorAction(locals.user.id, 'delete_org', 'org', params.org_id, { name: org.name });

Expand Down
Loading