feat: migrate admins to better auth roles

This commit is contained in:
2026-06-05 23:55:45 +01:00
parent dd7eb028db
commit ee98b7147c
12 changed files with 1457 additions and 72 deletions
+36 -55
View File
@@ -1,22 +1,20 @@
import { hashPassword } from 'better-auth/crypto';
import { asc, eq } from 'drizzle-orm';
import { message, superValidate } from 'sveltekit-superforms/server';
import { zod4 } from 'sveltekit-superforms/adapters';
import { auth } from '$lib/server/auth';
import { db } from '$lib/server/db';
import { account, user } from '$lib/server/db/schema';
import { user } from '$lib/server/db/schema';
import { createAdminSchema, deleteAdminSchema, editAdminSchema } from '$lib/schemas/admin';
import type { Actions, PageServerLoad } from './$types';
function now() {
return new Date();
}
export const load: PageServerLoad = async ({ locals }) => {
const users = await db
.select({
id: user.id,
name: user.name,
email: user.email,
role: user.role,
banned: user.banned,
emailVerified: user.emailVerified,
createdAt: user.createdAt,
updatedAt: user.updatedAt
@@ -41,29 +39,15 @@ export const actions: Actions = {
return message(form, 'Check the highlighted fields.', { status: 400 });
}
const userId = crypto.randomUUID();
const accountId = crypto.randomUUID();
const createdAt = now();
const passwordHash = await hashPassword(form.data.password);
try {
await db.insert(user).values({
id: userId,
name: form.data.name,
email: form.data.email,
emailVerified: true,
createdAt,
updatedAt: createdAt
});
await db.insert(account).values({
id: accountId,
accountId: userId,
providerId: 'credential',
userId,
password: passwordHash,
createdAt,
updatedAt: createdAt
await auth.api.createUser({
headers: event.request.headers,
body: {
name: form.data.name,
email: form.data.email,
password: form.data.password,
role: form.data.role
}
});
} catch {
return message(form, 'Unable to create admin. Check whether that email already exists.', {
@@ -82,36 +66,33 @@ export const actions: Actions = {
}
try {
await db
.update(user)
.set({
name: form.data.name,
email: form.data.email,
updatedAt: now()
})
.where(eq(user.id, form.data.id));
await auth.api.adminUpdateUser({
headers: event.request.headers,
body: {
userId: form.data.id,
data: {
name: form.data.name,
email: form.data.email
}
}
});
await auth.api.setRole({
headers: event.request.headers,
body: {
userId: form.data.id,
role: form.data.role
}
});
if (form.data.password) {
const passwordHash = await hashPassword(form.data.password);
const updated = await db
.update(account)
.set({
password: passwordHash,
updatedAt: now()
})
.where(eq(account.userId, form.data.id));
if (updated.rowsAffected === 0) {
await db.insert(account).values({
id: crypto.randomUUID(),
accountId: form.data.id,
providerId: 'credential',
await auth.api.setUserPassword({
headers: event.request.headers,
body: {
userId: form.data.id,
password: passwordHash,
createdAt: now(),
updatedAt: now()
});
}
newPassword: form.data.password
}
});
}
} catch {
return message(form, 'Unable to update admin. Check whether that email already exists.', {
+2 -3
View File
@@ -79,6 +79,7 @@
id: user.id,
name: user.name,
email: user.email,
role: user.role === 'admin' ? 'admin' : 'user',
password: ''
}
});
@@ -103,9 +104,7 @@
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-semibold tracking-tight">Admins</h1>
<p class="text-sm text-muted-foreground">
Manage Better Auth users with administrator access.
</p>
<p class="text-sm text-muted-foreground">Manage Better Auth users and admin roles.</p>
</div>
<CreateAdminDialog bind:open={createOpen} {createForm} {createData} {enhanceCreate} />
@@ -1,6 +1,7 @@
<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
import { Badge } from '$lib/components/ui/badge/index.js';
import { Button } from '$lib/components/ui/button/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
import * as Table from '$lib/components/ui/table/index.js';
@@ -23,6 +24,7 @@
<Table.Row>
<Table.Head>Name</Table.Head>
<Table.Head>Email</Table.Head>
<Table.Head>Role</Table.Head>
<Table.Head>Status</Table.Head>
<Table.Head>Created</Table.Head>
<Table.Head class="w-12">
@@ -36,7 +38,14 @@
<Table.Cell class="font-medium">{user.name}</Table.Cell>
<Table.Cell>{user.email}</Table.Cell>
<Table.Cell>
{user.emailVerified ? 'Verified' : 'Unverified'}
<Badge variant="secondary" class="capitalize">{user.role ?? 'user'}</Badge>
</Table.Cell>
<Table.Cell>
{#if user.banned}
<Badge variant="destructive">Banned</Badge>
{:else}
{user.emailVerified ? 'Verified' : 'Unverified'}
{/if}
</Table.Cell>
<Table.Cell>{formatDate(user.createdAt)}</Table.Cell>
<Table.Cell>
@@ -69,6 +78,6 @@
{#if data.users.length === 0}
<div class="rounded-lg border p-8 text-center text-sm text-muted-foreground">
No admin users have been created yet.
No users have been created yet.
</div>
{/if}
@@ -6,6 +6,7 @@
import { Button } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import * as NativeSelect from '$lib/components/ui/native-select/index.js';
let {
open = $bindable(false),
createForm,
@@ -24,14 +25,14 @@
{#snippet child({ props })}
<Button {...props}>
<PlusIcon data-icon="inline-start" />
Create admin
Create user
</Button>
{/snippet}
</Dialog.Trigger>
<Dialog.Content class="sm:max-w-md">
<Dialog.Header>
<Dialog.Title>Create admin</Dialog.Title>
<Dialog.Description>Add a manually managed admin account.</Dialog.Description>
<Dialog.Title>Create user</Dialog.Title>
<Dialog.Description>Add a manually managed Better Auth user.</Dialog.Description>
</Dialog.Header>
<form method="POST" action="?/create" class="grid gap-2" use:enhanceCreate>
<FormField form={createForm} name="name">
@@ -72,13 +73,27 @@
<Field.Error><FieldErrors /></Field.Error>
</Field.Field>
</FormField>
<FormField form={createForm} name="role">
<Field.Field>
<Control id="create-role">
{#snippet children({ props })}
<Field.Label>Role</Field.Label>
<NativeSelect.Root {...props} bind:value={$createData.role} class="w-full">
<NativeSelect.Option value="admin">Admin</NativeSelect.Option>
<NativeSelect.Option value="user">User</NativeSelect.Option>
</NativeSelect.Root>
{/snippet}
</Control>
<Field.Error><FieldErrors /></Field.Error>
</Field.Field>
</FormField>
<Dialog.Footer>
<Dialog.Close>
{#snippet child({ props })}
<Button variant="outline" {...props}>Cancel</Button>
{/snippet}
</Dialog.Close>
<Button type="submit">Create admin</Button>
<Button type="submit">Create user</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
@@ -17,9 +17,9 @@
<AlertDialog.Root open={!!deletingUser} onOpenChange={(open) => !open && onClose()}>
<AlertDialog.Content>
<AlertDialog.Header>
<AlertDialog.Title>Delete admin?</AlertDialog.Title>
<AlertDialog.Title>Delete user?</AlertDialog.Title>
<AlertDialog.Description>
This will permanently delete {deletingUser?.name ?? 'this admin'} and revoke their sessions.
This will permanently delete {deletingUser?.name ?? 'this user'} and revoke their sessions.
</AlertDialog.Description>
</AlertDialog.Header>
<AlertDialog.Footer>
@@ -27,7 +27,7 @@
{#if deletingUser}
<form method="POST" action="?/delete" use:enhanceDelete>
<input type="hidden" name="id" value={$deleteData.id} />
<AlertDialog.Action type="submit" variant="destructive">Delete admin</AlertDialog.Action>
<AlertDialog.Action type="submit" variant="destructive">Delete user</AlertDialog.Action>
</form>
{/if}
</AlertDialog.Footer>
@@ -5,6 +5,7 @@
import { Button } from '$lib/components/ui/button/index.js';
import * as Dialog from '$lib/components/ui/dialog/index.js';
import { Input } from '$lib/components/ui/input/index.js';
import * as NativeSelect from '$lib/components/ui/native-select/index.js';
let {
editingUser,
editForm,
@@ -23,7 +24,7 @@
<Dialog.Root open={!!editingUser} onOpenChange={(open) => !open && onClose()}>
<Dialog.Content class="sm:max-w-md">
<Dialog.Header>
<Dialog.Title>Edit admin</Dialog.Title>
<Dialog.Title>Edit user</Dialog.Title>
<Dialog.Description>Update account details or set a new password.</Dialog.Description>
</Dialog.Header>
{#if editingUser}
@@ -68,6 +69,20 @@
<Field.Error><FieldErrors /></Field.Error>
</Field.Field>
</FormField>
<FormField form={editForm} name="role">
<Field.Field>
<Control id="edit-role">
{#snippet children({ props })}
<Field.Label>Role</Field.Label>
<NativeSelect.Root {...props} bind:value={$editData.role} class="w-full">
<NativeSelect.Option value="admin">Admin</NativeSelect.Option>
<NativeSelect.Option value="user">User</NativeSelect.Option>
</NativeSelect.Root>
{/snippet}
</Control>
<Field.Error><FieldErrors /></Field.Error>
</Field.Field>
</FormField>
<Dialog.Footer>
<Dialog.Close>
{#snippet child({ props })}