Skip to content

Commit

Permalink
Add UUID package and enhance user registration flow; update schemas a…
Browse files Browse the repository at this point in the history
…nd forms to support user creation with registration tokens
  • Loading branch information
haukened committed Nov 6, 2024
1 parent 119e659 commit 330cd93
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 23 deletions.
16 changes: 15 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@sveltejs/adapter-node": "^5.2.9",
"better-sqlite3": "^11.1.2",
"drizzle-orm": "^0.33.0",
"js-sha256": "^0.11.0"
"js-sha256": "^0.11.0",
"uuid": "^11.0.2"
}
}
8 changes: 4 additions & 4 deletions src/lib/server/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ export function sanitizeUser(user: User): User {
return user;
}

import { users, sessions } from './schema'; // Importing the users and sessions tables and their types
import type { User, Session } from './schema'; // Importing the User and Session types
import { users, sessions, registrations } from './schema'; // Importing the users and sessions tables and their types
import type { User, Session, Registration } from './schema'; // Importing the User and Session types
import { count, eq } from 'drizzle-orm';
export { users, sessions }; // Exporting the users and sessions tables and their types
export type { User, Session }; // Exporting the User and Session types
export { users, sessions, registrations }; // Exporting the users and sessions tables and their types
export type { User, Session, Registration }; // Exporting the User and Session types
1 change: 1 addition & 0 deletions src/lib/server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ export const registrations = sqliteTable('registrations', {

export type User = InferSelectModel<typeof users>;
export type Session = InferSelectModel<typeof sessions>;
export type Registration = InferSelectModel<typeof registrations>;
83 changes: 77 additions & 6 deletions src/routes/admin/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { db, users, type User } from "$lib/server/db";
import { message, setError, superValidate } from "sveltekit-superforms";
import { db, users, type User, registrations } from "$lib/server/db";
import { type Infer, message, setError, superValidate } from "sveltekit-superforms";
import type { Actions, PageServerLoad } from "./$types";
import { zod } from "sveltekit-superforms/adapters";
import { userFormSchema } from "./schema";
import { redirect } from "@sveltejs/kit";
import { userFormSchema, type UserFormSchema } from "./schema";
import { fail, redirect, text } from "@sveltejs/kit";
import { eq } from "drizzle-orm";
import { v4 as uuid4 } from "uuid";

type Message = {
text: string | undefined;
token: string | undefined;
}

export const load: PageServerLoad = async (event) => {
// set up the form
const form = await superValidate(zod(userFormSchema));
const form = await superValidate<Infer<UserFormSchema>,Message>(zod(userFormSchema));
// server-side guard - check for user
if (!event.locals.user) {
return redirect(302, "/logout");
Expand All @@ -22,7 +29,7 @@ export const load: PageServerLoad = async (event) => {
// return the public users
return {
users: publicUsers,
form: form
form: form,
}
}

Expand All @@ -38,11 +45,75 @@ export const actions: Actions = {
if (!event.locals.user) {
return
}
// validate the form
const form = await superValidate<Infer<UserFormSchema>,Message>(event, zod(userFormSchema));
if (!form.valid) {
return fail(400, { form });
}
// ensure we do not have a user Id
if (form.data.id) {
return setError(form, '', 'User ID is not allowed');
}
// create the user
const created = await db.insert(users).values({
firstname: form.data.firstname,
lastname: form.data.lastname,
email: form.data.email,
}).returning();
// ensure we created one and only one user
if (created.length === 0) {
return setError(form, '', 'User not created');
}
if (created.length > 1) {
// this should never happen, but hey, we are being safe
return setError(form, '', 'Multiple users created');
}
const createdUser = created[0];
// now we need to create a registration token so the user can create a password
const regToken = uuid4();
const registration = await db.insert(registrations).values({
userId: createdUser.id,
token: regToken,
created_at: new Date(Date.now()),
}).returning({ id: registrations.id });
// ensure we created one and only one registration
if (registration.length !== 1) {
return setError(form, '', 'Error creating registration token');
}
// return the created user and the registration token
return message(form, {
text: 'User created',
token: regToken,
});
},
update: async (event) => {
// server-side guard - check for user
if (!event.locals.user) {
return
}
// validate the form
const form = await superValidate<Infer<UserFormSchema>,Message>(event, zod(userFormSchema));
if (!form.valid) {
return fail(400, { form });
}
// ensure we have a user Id
if (!form.data.id) {
return setError(form, '', 'User ID is required');
}
// update the user
const updated: { id: number }[] = await db.update(users).set({
firstname: form.data.firstname,
lastname: form.data.lastname,
email: form.data.email,
}).where(eq(users.id, form.data.id)).returning({ id: users.id});
// make sure the user was updated
if (updated.length === 0) {
return setError(form, '', 'User not found');
}
// return the updated user
return message(form, {
text: 'User updated',
token: undefined,
});
}
}
50 changes: 39 additions & 11 deletions src/routes/admin/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script lang="ts">
import { superForm, type SuperValidated, type Infer } from "sveltekit-superforms";
import { userFormSchema, type UserFormSchema } from "./schema";
import { superForm } from "sveltekit-superforms";
import { userFormSchema } from "./schema";
import { zodClient } from "sveltekit-superforms/adapters";
import type { PageData } from "./$types";
import type { ActionData, 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 { 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";
Expand All @@ -23,13 +24,13 @@
dataType: 'json'
});
const { form: formData, enhance } = form;
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("add" as "add" | "update");
let dialogAction = $state("create" as "create" | "update");
const openDeleteConfirm = (user: User) => {
deleteConfirmUser = user;
Expand All @@ -42,15 +43,22 @@
dialogOpen = true;
}
const openUserAdd = () => {
dialogAction = "add";
const openUserCreate = () => {
dialogAction = "create";
formData.set({
firstname: "",
lastname: "",
email: ""
email: "",
id: undefined,
});
dialogOpen = true;
}
const handleSubmit = (e: MouseEvent) => {
e.preventDefault();
dialogOpen = false;
form.submit();
}
const dateFormatter = new Intl.DateTimeFormat("en-US", {
year: "numeric",
Expand All @@ -61,6 +69,24 @@
hour12: false,
timeZone: new Intl.DateTimeFormat().resolvedOptions().timeZone
});
$effect(() => {
if ($errors._errors) {
$errors._errors.forEach(error => {
toast.error(error);
});
}
if ($message) {
if ($message.text) {
toast.success($message.text);
$message.text = undefined;
}
if ($message.token) {
toast.success($message.token);
$message.token = undefined;
}
}
});
</script>

<Card.Root class="flex flex-col w-full">
Expand All @@ -69,7 +95,7 @@
<Card.Title>{m.adminTitle()}</Card.Title>
<Card.Description>{m.adminDescription()}</Card.Description>
</div>
<Button variant="outline" onclick={() => {openUserAdd()}}>
<Button variant="outline" onclick={() => {openUserCreate()}}>
<UserPlus class="mr-2 size-5"/>
{m.adminUserCreate()}
</Button>
Expand Down Expand Up @@ -185,7 +211,8 @@
formData.set({
firstname: "",
lastname: "",
email: ""
email: "",
id: undefined,
});
}
}}
Expand All @@ -197,6 +224,7 @@
use:enhance
class="space-y-4"
>
<input type="hidden" name="id" bind:value={$formData.id} />
<Dialog.Header>
<Dialog.Title class="capitalize">{dialogAction} User</Dialog.Title>
<Dialog.Description>Form Description</Dialog.Description>
Expand Down Expand Up @@ -228,7 +256,7 @@
<Form.FieldErrors/>
</Form.Field>
<Dialog.Footer>
<Button>Save</Button>
<Button type="submit" onclick={handleSubmit}>Save</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
Expand Down
1 change: 1 addition & 0 deletions src/routes/admin/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from 'zod';

// dont tell them errors, so not to disclose anything
export const userFormSchema = z.object({
id: z.number().int().positive().optional(),
firstname: z.string().min(3).max(50),
lastname: z.string().min(3).max(50),
email: z.string().email(),
Expand Down

0 comments on commit 330cd93

Please sign in to comment.