feat: setup and roles
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and, eq, ne } from 'drizzle-orm';
|
||||
import * as z from 'zod';
|
||||
import {
|
||||
agents,
|
||||
@@ -57,6 +57,25 @@ 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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (twoFactorEnabled && code === undefined) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
// Public on purpose: the sign in pages ask this before anyone has an account.
|
||||
export default defineEventHandler(async () => {
|
||||
return { needed: await isSetupNeeded() };
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import * as z from 'zod';
|
||||
import { users } from '~~/drizzle/schema';
|
||||
import { auth } from '~~/lib/auth';
|
||||
import { db } from '~~/server/lib/db';
|
||||
|
||||
// any fixed number works, it only has to be the same for every request
|
||||
const SETUP_LOCK_ID = 7_261_008;
|
||||
|
||||
/**
|
||||
* Creates the first account of the instance as its admin.
|
||||
*
|
||||
* Public, because nobody can be signed in yet, and only works while there are
|
||||
* no accounts at all. It does not go through sign up, so it also works when
|
||||
* registration is turned off.
|
||||
*/
|
||||
export default defineEventHandler(async (event) => {
|
||||
const result = await readValidatedBody(event, (body) =>
|
||||
z
|
||||
.object({
|
||||
name: z.string().trim().min(1).max(128),
|
||||
email: z.email(),
|
||||
password: z.string().min(8).max(128),
|
||||
})
|
||||
.safeParse(body),
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw createError({
|
||||
statusCode: 400,
|
||||
message: result.error.issues[0]!.message,
|
||||
data: {
|
||||
code: 'INVALID_BODY',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { name, email, password } = result.data;
|
||||
|
||||
let alreadyDone = false;
|
||||
|
||||
try {
|
||||
await db.transaction(async (tx) => {
|
||||
// two people submitting the form at the same moment wait here, and
|
||||
// the second one then finds the first one's account
|
||||
await tx.execute(sql`select pg_advisory_xact_lock(${SETUP_LOCK_ID})`);
|
||||
|
||||
const existing = await tx.select({ id: users.id }).from(users).limit(1);
|
||||
if (existing.length > 0) {
|
||||
alreadyDone = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await auth.api.createUser({
|
||||
body: {
|
||||
name,
|
||||
email,
|
||||
password,
|
||||
role: 'admin',
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error: any) {
|
||||
const code = error?.body?.code as string | undefined;
|
||||
|
||||
if (code === undefined) {
|
||||
console.error('Failed to create the admin account:', error);
|
||||
}
|
||||
|
||||
throw createError({
|
||||
statusCode: code === undefined ? 500 : 400,
|
||||
statusMessage: 'Failed to set up the instance',
|
||||
message: error?.body?.message ?? 'Failed to set up the instance',
|
||||
data: {
|
||||
code: code ?? 'INTERNAL_ERROR',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (alreadyDone) {
|
||||
throw createError({
|
||||
statusCode: 409,
|
||||
statusMessage: 'This instance is already set up',
|
||||
data: {
|
||||
code: 'SETUP_ALREADY_DONE',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { users } from '~~/drizzle/schema';
|
||||
import { db } from '~~/server/lib/db';
|
||||
|
||||
/**
|
||||
* An instance needs setting up for as long as it has no accounts. The first
|
||||
* account is created by the setup page and becomes the admin.
|
||||
*/
|
||||
export const isSetupNeeded = async (): Promise<boolean> => {
|
||||
const existing = await db.select({ id: users.id }).from(users).limit(1);
|
||||
|
||||
return existing.length === 0;
|
||||
};
|
||||
Reference in New Issue
Block a user