feat: admin controls

This commit is contained in:
Zoe
2026-10-15 10:07:41 -05:00
parent b6f4d85ed7
commit ff7797d3f8
41 changed files with 612 additions and 427 deletions
+19 -21
View File
@@ -1,7 +1,6 @@
import { and, eq, ne } from 'drizzle-orm';
import * as z from 'zod';
import { users } from '~~/drizzle/schema';
import { auth } from '~~/lib/auth';
import { hasUsableAdministrator } from '~~/server/lib/account-security';
import { db } from '~~/server/lib/db';
/**
@@ -43,23 +42,16 @@ export default defineEventHandler(async (event) => {
const twoFactorEnabled = !!account?.twoFactorEnabled;
// an instance always keeps one admin, someone has to be able to manage it
if (account?.role === 'admin') {
const otherAdmins = await db
.select({ id: users.id })
.from(users)
.where(and(eq(users.role, 'admin'), ne(users.id, userId)))
.limit(1);
if (otherAdmins.length === 0) {
throw createError({
statusCode: 409,
statusMessage: 'The last admin cannot be deleted',
data: {
code: 'LAST_ADMIN',
},
});
}
// Checked again when the account is deleted. Doing it here as well keeps
// a deletion that is refused anyway from using up a backup code.
if (account?.role === 'admin' && !await db.transaction(tx => hasUsableAdministrator(tx, userId))) {
throw createError({
statusCode: 409,
statusMessage: 'The last usable admin cannot be deleted',
data: {
code: 'LAST_ADMIN',
},
});
}
if (twoFactorEnabled && code === undefined) {
@@ -111,7 +103,14 @@ export default defineEventHandler(async (event) => {
});
}
await deleteAccount(userId);
const deleted = await deleteAccount(userId);
if (!deleted.ok) {
throw createError({
statusCode: 409,
statusMessage: "The last usable admin cannot be deleted",
data: { code: deleted.error },
});
}
// the session is gone, but the browser would keep trusting its cached
// copy for a few minutes unless the cookies go too
@@ -121,7 +120,6 @@ export default defineEventHandler(async (event) => {
deleteCookie(event, cookie.name, {
path: cookie.attributes.path,
secure: cookie.attributes.secure,
sameSite: cookie.attributes.sameSite,
httpOnly: cookie.attributes.httpOnly,
});
}