diff --git a/src/lib/server/auth/index.ts b/src/lib/server/auth/index.ts
index 43e0173..27bb6d3 100644
--- a/src/lib/server/auth/index.ts
+++ b/src/lib/server/auth/index.ts
@@ -60,7 +60,8 @@ export async function validateSession(
email: users.email,
username: users.username,
displayName: users.displayName,
- isSystemAdmin: users.isSystemAdmin
+ isSystemAdmin: users.isSystemAdmin,
+ disabledAt: users.disabledAt
}
})
.from(sessions)
@@ -74,6 +75,12 @@ export async function validateSession(
const { session, user } = result[0];
+ // Disabled user — kill session
+ if (user.disabledAt) {
+ await db.delete(sessions).where(eq(sessions.id, sessionId));
+ return { session: null, user: null };
+ }
+
// Session expired
if (Date.now() >= session.expiresAt.getTime()) {
await db.delete(sessions).where(eq(sessions.id, sessionId));
diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts
index e0dbd0f..264cf38 100644
--- a/src/lib/server/db/schema.ts
+++ b/src/lib/server/db/schema.ts
@@ -31,6 +31,7 @@ export const users = pgTable(
oidcProvider: text('oidc_provider'),
oidcSubject: text('oidc_subject'),
isSystemAdmin: boolean('is_system_admin').notNull().default(false),
+ disabledAt: timestamp('disabled_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow()
},
diff --git a/src/routes/(app)/admin/users/+page.server.ts b/src/routes/(app)/admin/users/+page.server.ts
index 7cbda74..10c76c2 100644
--- a/src/routes/(app)/admin/users/+page.server.ts
+++ b/src/routes/(app)/admin/users/+page.server.ts
@@ -1,7 +1,7 @@
import { fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js';
-import { users } from '$lib/server/db/schema.js';
+import { users, sessions, companyMembers, expenses } from '$lib/server/db/schema.js';
import { eq, sql } from 'drizzle-orm';
export const load: PageServerLoad = async () => {
@@ -12,6 +12,7 @@ export const load: PageServerLoad = async () => {
username: users.username,
displayName: users.displayName,
isSystemAdmin: users.isSystemAdmin,
+ disabledAt: users.disabledAt,
oidcProvider: users.oidcProvider,
createdAt: users.createdAt
})
@@ -22,11 +23,12 @@ export const load: PageServerLoad = async () => {
};
export const actions: Actions = {
- toggleAdmin: async ({ request }) => {
+ toggleAdmin: async ({ request, locals }) => {
const formData = await request.formData();
const userId = formData.get('userId')?.toString();
if (!userId) return fail(400, { error: 'User ID required' });
+ if (userId === locals.user!.id) return fail(400, { error: 'Cannot change your own admin status' });
await db
.update(users)
@@ -34,5 +36,54 @@ export const actions: Actions = {
.where(eq(users.id, userId));
return { success: true };
+ },
+
+ toggleDisable: async ({ request, locals }) => {
+ const formData = await request.formData();
+ const userId = formData.get('userId')?.toString();
+
+ if (!userId) return fail(400, { error: 'User ID required' });
+ if (userId === locals.user!.id) return fail(400, { error: 'Cannot disable yourself' });
+
+ const [user] = await db
+ .select({ disabledAt: users.disabledAt })
+ .from(users)
+ .where(eq(users.id, userId))
+ .limit(1);
+
+ if (!user) return fail(404, { error: 'User not found' });
+
+ if (user.disabledAt) {
+ // Re-enable
+ await db
+ .update(users)
+ .set({ disabledAt: null, updatedAt: new Date() })
+ .where(eq(users.id, userId));
+ } else {
+ // Disable + kill all sessions
+ await db
+ .update(users)
+ .set({ disabledAt: new Date(), updatedAt: new Date() })
+ .where(eq(users.id, userId));
+ await db.delete(sessions).where(eq(sessions.userId, userId));
+ }
+
+ return { success: true };
+ },
+
+ permanentDelete: async ({ request, locals }) => {
+ const formData = await request.formData();
+ const userId = formData.get('userId')?.toString();
+
+ if (!userId) return fail(400, { error: 'User ID required' });
+ if (userId === locals.user!.id) return fail(400, { error: 'Cannot delete yourself' });
+
+ // Delete sessions, company memberships, then the user
+ // Expenses are preserved (submittedBy FK won't cascade) — set to null or keep reference
+ await db.delete(sessions).where(eq(sessions.userId, userId));
+ await db.delete(companyMembers).where(eq(companyMembers.userId, userId));
+ await db.delete(users).where(eq(users.id, userId));
+
+ return { success: true, deleted: true };
}
};
diff --git a/src/routes/(app)/admin/users/+page.svelte b/src/routes/(app)/admin/users/+page.svelte
index a864913..1c6a9fc 100644
--- a/src/routes/(app)/admin/users/+page.svelte
+++ b/src/routes/(app)/admin/users/+page.svelte
@@ -1,24 +1,34 @@
| Name | Status | Auth | Admin | Joined | @@ -27,9 +37,16 @@||
|---|---|---|---|---|---|---|
| {user.displayName ?? '—'} | {user.email} | ++ {#if user.disabledAt} + Disabled + {:else} + Active + {/if} + | {user.oidcProvider ? 'SSO' : 'Local'} @@ -37,20 +54,39 @@ | {#if user.isSystemAdmin} - Yes + Admin {/if} | {formatDateTime(user.createdAt)} | - |
+ This will permanently delete {confirmDeleteName} and remove them from all companies. This action cannot be undone. +
++ Their submitted expenses will be preserved but no longer linked to their account. +
+