From ba6afaa33b7d623d814ef52784e4a669d9bfc4d2 Mon Sep 17 00:00:00 2001 From: David Haukeness Date: Wed, 6 Nov 2024 22:16:05 +0000 Subject: [PATCH] Implement user deletion and creation guards; add form validation and error handling; update database references for cascading deletes --- src/lib/server/db/schema.ts | 4 +- src/routes/+layout.server.ts | 2 + src/routes/admin/+page.server.ts | 41 ++++++++- src/routes/admin/+page.svelte | 139 +++++++++++++++---------------- 4 files changed, 111 insertions(+), 75 deletions(-) diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index 6f0fb01..310c1d0 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -17,7 +17,7 @@ export const sessions = sqliteTable('sessions', { id: text('id').primaryKey(), userId: integer('user_id', {mode: 'number'}) .notNull() - .references(() => users.id), + .references(() => users.id, {onDelete: 'cascade'}), expiresAt: integer('expires_at', {mode: 'timestamp'}).notNull(), }); @@ -25,7 +25,7 @@ export const registrations = sqliteTable('registrations', { id: integer('id', {mode: 'number'}).primaryKey({autoIncrement: true}), userId: integer('user_id', {mode: 'number'}) .notNull() - .references(() => users.id), + .references(() => users.id, {onDelete: 'cascade'}), token: text('token').notNull(), created_at: integer('created_at', {mode: 'timestamp'}).notNull().default(sql`(unixepoch())`), consumed: integer('consumed', {mode: 'boolean'}).notNull().default(false), diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 05493a5..e6964f5 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,6 +1,8 @@ import { sanitizeUser } from "$lib/server/db"; import type { LayoutServerLoad } from "./$types"; +export const ssr = false; + export const load: LayoutServerLoad = async (event) => { if (event.locals.user) { let clientSideUser = sanitizeUser(event.locals.user); diff --git a/src/routes/admin/+page.server.ts b/src/routes/admin/+page.server.ts index a9b464d..3438781 100644 --- a/src/routes/admin/+page.server.ts +++ b/src/routes/admin/+page.server.ts @@ -37,13 +37,45 @@ export const actions: Actions = { delete: async (event) => { // server-side guard - check for user if (!event.locals.user) { - return + return redirect(302, "/logout"); + } + // validate the form + const form = await superValidate,Message>(event, zod(userFormSchema)); + if (!form.valid) { + return message(form, { + text: 'Unable to delete user', + token: undefined, + }) + } + // ensure we have a user Id + if (!form.data.id) { + return setError(form, '', 'User ID is required'); + } + if (event.locals.user.id === form.data.id) { + return setError(form, '', 'Don\'t delete yourself.'); + } + // delete the user + try { + const deleted = await db.delete(users) + .where(eq(users.id, form.data.id)) + .returning(); + // make sure the user was deleted + if (deleted.length === 0) { + return setError(form, '', 'User not found'); + } + return message(form, { + text: `User ${deleted[0].email} deleted`, + token: undefined, + }); + } catch (e) { + console.log(e); + return setError(form, '', 'Error deleting user'); } }, create: async (event) => { // server-side guard - check for user if (!event.locals.user) { - return + return redirect(302, "/logout"); } // validate the form const form = await superValidate,Message>(event, zod(userFormSchema)); @@ -87,9 +119,10 @@ export const actions: Actions = { }); }, update: async (event) => { + const start = Date.now(); // server-side guard - check for user if (!event.locals.user) { - return + return redirect(302, "/logout"); } // validate the form const form = await superValidate,Message>(event, zod(userFormSchema)); @@ -110,6 +143,8 @@ export const actions: Actions = { if (updated.length === 0) { return setError(form, '', 'User not found'); } + const duration = Date.now() - start; + console.log(`update user took ${duration}ms`); // return the updated user return message(form, { text: 'User updated', diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte index 82b77f5..d31c787 100644 --- a/src/routes/admin/+page.svelte +++ b/src/routes/admin/+page.svelte @@ -2,13 +2,13 @@ import { superForm } from "sveltekit-superforms"; import { userFormSchema } from "./schema"; import { zodClient } from "sveltekit-superforms/adapters"; - import type { ActionData, PageData } from "./$types"; + import type { PageData } from "./$types"; import type { User } from "$lib/server/db"; import { Button } from "$lib/components/ui/button"; import { CircleCheckBig, CircleX, UserPlus, Ellipsis, Pencil, Trash } from "lucide-svelte"; import { Input } from '$lib/components/ui/input'; + import { page } from "$app/stores"; import { toast } from "svelte-sonner"; - import * as AlertDialog from "$lib/components/ui/alert-dialog"; import * as Card from "$lib/components/ui/card"; import * as Dialog from "$lib/components/ui/dialog"; import * as DropdownMenu from "$lib/components/ui/dropdown-menu"; @@ -21,45 +21,58 @@ const form = superForm(data.form, { validators: zodClient(userFormSchema), - dataType: 'json' + dataType: 'json', }); const { form: formData, enhance, errors, message } = form; - let deleteConfirmOpen = $state(false); - let deleteConfirmUser = $state(null as User | null); - let dialogOpen = $state(false); - let dialogAction = $state("create" as "create" | "update"); + let dialogAction = $state("create" as "create" | "update" | "delete" | "reg"); + let regToken = $state(undefined as string | undefined); - const openDeleteConfirm = (user: User) => { - deleteConfirmUser = user; - deleteConfirmOpen = true; + const openUserDelete = (user: User) => { + dialogAction = "delete"; + clearFormData(); + formData.set(user); + dialogOpen = true; } const openUserEdit = (user: User) => { dialogAction = "update"; + clearFormData(); formData.set(user); dialogOpen = true; } const openUserCreate = () => { dialogAction = "create"; + clearFormData(); + dialogOpen = true; + } + + const openRegDialog = (token: string) => { + dialogAction = "reg"; + regToken = token; + dialogOpen = true; + } + + const closeDialog = () => { + dialogOpen = false; + } + + const clearFormData = () => { formData.set({ firstname: "", lastname: "", email: "", id: undefined, }); - dialogOpen = true; } - const handleSubmit = (e: MouseEvent) => { - e.preventDefault(); - dialogOpen = false; - form.submit(); + const clearRegToken = () => { + regToken = undefined; } - + const dateFormatter = new Intl.DateTimeFormat("en-US", { year: "numeric", month: "2-digit", @@ -72,17 +85,22 @@ $effect(() => { if ($errors._errors) { + closeDialog(); + clearFormData(); + clearRegToken(); $errors._errors.forEach(error => { toast.error(error); }); } if ($message) { if ($message.text) { + closeDialog(); toast.success($message.text); $message.text = undefined; } if ($message.token) { - toast.success($message.token); + closeDialog(); + openRegDialog($message.token); $message.token = undefined; } } @@ -151,7 +169,7 @@ {m.adminUserEdit()} - {openDeleteConfirm(user)}}> + {openUserDelete(user)}}> {m.adminUserDelete()} @@ -165,69 +183,49 @@ - - { - if (!open) { - deleteConfirmUser = null; - } - }} - > - -
{ - console.log("submitting delete form"); - deleteConfirmOpen = false; - }} - class="space-y-4" - > - - - - Are you absolutely sure? - - - This action cannot be undone.
This will permanently delete the user {deleteConfirmUser?.email} -
-
- - - Cancel - - - -
-
-
- { - if (!open) { - formData.set({ - firstname: "", - lastname: "", - email: "", - id: undefined, - }); - } - }} >
+ {#if dialogAction === "reg"} + + New User Registration + +

User {$formData.firstname} {$formData.lastname} created successfully.

+

Copy the token and send it to the user.

+
+
+ + + + {:else if dialogAction === "delete"} + + Delete User + +

User {$formData.email} will be deleted.

+

Are you ABSOLUTELY sure? This action cannot be undone.

+
+
+ + + + + + + {:else} {dialogAction} User - Form Description + Description @@ -256,8 +254,9 @@ - + + {/if}