feat: basic admin interface

This commit is contained in:
Zoe
2026-10-08 13:41:02 -05:00
parent 7b0800882a
commit f6e1bc1f47
27 changed files with 4095 additions and 49 deletions
+39
View File
@@ -0,0 +1,39 @@
import { eq } from 'drizzle-orm';
import {
agents,
files,
models,
providers,
settings,
toolCalls,
topics,
users,
} from '~~/drizzle/schema';
import { db } from '~~/server/lib/db';
/**
* Deletes an account and everything that belongs to it.
*
* The app tables only store a plain user id, nothing cascades from the users
* table, so every one of them is cleared here. It all happens in one
* transaction: either the whole account is gone or nothing changed.
*
* Objects in file storage are left alone on purpose.
*/
export const deleteAccount = async (userId: string) => {
await db.transaction(async (tx) => {
// messages, their parts, generations and attachments cascade from topics
await tx.delete(topics).where(eq(topics.userId, userId));
await tx.delete(agents).where(eq(agents.userId, userId));
await tx.delete(models).where(eq(models.userId, userId));
await tx.delete(providers).where(eq(providers.userId, userId));
// nothing points a tool call back at its message, so these never cascade
await tx.delete(toolCalls).where(eq(toolCalls.userId, userId));
// embeddings go with their file
await tx.delete(files).where(eq(files.userId, userId));
await tx.delete(settings).where(eq(settings.userId, userId));
// sessions, accounts and two-factor rows cascade from the user
await tx.delete(users).where(eq(users.id, userId));
});
};
+26
View File
@@ -0,0 +1,26 @@
import type { H3Event } from 'h3';
import { db } from '~~/server/lib/db';
/**
* Like protectRoute(), but only lets admins through.
*
* The role is read from the database: the session cookie keeps a copy of the
* user for a few minutes, and someone who just stopped being an admin should
* not keep the access for that long.
*/
export const protectAdminRoute = async (event: H3Event) => {
await protectRoute(event);
const account = await db.query.users.findFirst({
where: {
id: event.context.user!.id as string,
},
});
if (account?.role !== 'admin' || account.banned) {
throw createError({
statusCode: 403,
statusMessage: 'Forbidden',
});
}
};
+27
View File
@@ -0,0 +1,27 @@
import { INSTANCE_SETTINGS_ID, REGISTRATION_MODES, type RegistrationMode } from '../../drizzle/schema';
import { db } from '../lib/db';
/**
* DISABLE_SIGNUP in the environment wins over whatever admins pick in the UI,
* so a deployment that was locked down this way stays locked down.
*/
export const isRegistrationLocked = (): boolean => {
const value = process.env.DISABLE_SIGNUP?.toLowerCase();
return value === 'true' || value === '1';
};
export const getRegistrationMode = async (): Promise<RegistrationMode> => {
if (isRegistrationLocked()) return 'closed';
const row = await db.query.instanceSettings.findFirst({
where: {
id: INSTANCE_SETTINGS_ID,
},
});
const mode = row?.registration;
// instances that never touched the setting keep taking sign ups like before
return mode !== undefined && REGISTRATION_MODES.includes(mode) ? mode : 'open';
};