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
+52 -57
View File
@@ -1,68 +1,63 @@
import { and, eq } from "drizzle-orm";
import { providers } from "~~/drizzle/schema";
import { and, eq, gt } from "drizzle-orm";
import * as z from "zod";
import { providers, sessions } from "~~/drizzle/schema";
import { lockAccount } from "~~/server/lib/account-security";
import { db } from "~~/server/lib/db";
import * as z from 'zod';
export default defineEventHandler(async (event) => {
await protectRoute(event);
const userId = event.context.user!.id;
const result = await readValidatedBody(event, (body) =>
z
.object({
config: z.object({
apiKey: z.string().optional(),
apiProxyUrl: z.string().optional(),
}).optional(),
enabled: z.boolean().optional(),
})
.safeParse(body),
);
const { user, session } = await protectRoute(event);
const result = await readValidatedBody(event, (body) => z.object({
config: z.object({
apiKey: z.string().optional(),
apiProxyUrl: z.string().optional(),
}).optional(),
expectedUpdatedAt: z.iso.datetime().optional(),
enabled: z.boolean().optional(),
}).refine(body => body.config === undefined || body.expectedUpdatedAt !== undefined, {
message: "Provider config updates require a revision",
}).safeParse(body));
if (!result.success) {
throw createError({
statusCode: 400,
message: result.error.issues[0]!.message,
});
throw createError({ statusCode: 400, message: result.error.issues[0]?.message });
}
const providerId = getRouterParam(event, 'providerId')!;
const { config, enabled } = result.data;
const existing = await db.query.providers.findFirst({
where: {
id: providerId,
userId,
},
});
if (!existing) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid provider',
});
const providerId = getRouterParam(event, "providerId");
if (!providerId) {
throw createError({ statusCode: 400, statusMessage: "Invalid provider" });
}
const { config, enabled, expectedUpdatedAt } = result.data;
const res = await db.update(providers)
.set({
config,
return await db.transaction(async (tx) => {
await lockAccount(tx, user.id);
// A request may have waited behind a password rotation or recovery.
const [activeSession] = await tx.select({ id: sessions.id }).from(sessions).where(and(
eq(sessions.id, session.id), eq(sessions.userId, user.id), gt(sessions.expiresAt, new Date()),
));
if (!activeSession) {
throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
}
const [existing] = await tx.select().from(providers).where(and(
eq(providers.id, providerId), eq(providers.userId, user.id),
));
if (!existing) {
throw createError({ statusCode: 404, statusMessage: "Invalid provider" });
}
if (config !== undefined && existing.updatedAt.getTime() !== new Date(expectedUpdatedAt ?? "").getTime()) {
throw createError({
statusCode: 409,
statusMessage: "Provider config out of date",
data: { code: "PROVIDER_KEYS_OUT_OF_DATE" },
});
}
const updatedAt = new Date(Math.max(Date.now(), existing.updatedAt.getTime() + 1));
await tx.update(providers).set({
config: config === undefined ? undefined : { ...existing.config, ...config },
enabled,
})
.where(
and(
eq(providers.id, providerId),
eq(providers.userId, userId),
)
);
updatedAt,
}).where(and(eq(providers.id, providerId), eq(providers.userId, user.id)));
if (res.rowCount === 0) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid provider',
});
}
return { ok: true };
});
return { ok: true, updatedAt };
});
});