diff --git a/src/routes/dash/+layout.svelte b/src/routes/dash/+layout.svelte index 2a10f9f..6e53896 100644 --- a/src/routes/dash/+layout.svelte +++ b/src/routes/dash/+layout.svelte @@ -11,6 +11,7 @@ import IconDeviceDesktopPin from '~icons/tabler/device-desktop-pin'; import IconHome from '~icons/tabler/home'; import IconLogout from '~icons/tabler/logout'; + import IconUserCircle from '~icons/tabler/user-circle'; import IconUsers from '~icons/tabler/users'; import IconUsersGroup from '~icons/tabler/users-group'; @@ -84,6 +85,7 @@ {/snippet} + {@render DropDownLink(IconUserCircle, 'Account', '/dash/account')} {@render DropDownLink(IconDeviceDesktopPin, 'Sessions', '/dash/account/sessions')} {@render DropDownLink(IconLogout, 'Sign out', '/logout', false)} diff --git a/src/routes/dash/account/+page.server.ts b/src/routes/dash/account/+page.server.ts new file mode 100644 index 0000000..023a772 --- /dev/null +++ b/src/routes/dash/account/+page.server.ts @@ -0,0 +1,49 @@ +import { FORBIDDEN, PARSE_ERROR, SUCCESS } from '$lib/server/commonResponses'; +import { users } from '$lib/server/db'; +import type { Updated } from '$lib/server/db/types'; +import { toPublicUser, type User } from '$lib/server/db/types/user.js'; +import { type Actions, type ServerLoad } from '@sveltejs/kit'; +import bcrypt from 'bcryptjs'; +import { z } from 'zod'; + +export const load: ServerLoad = async ({ locals: { guard }, params }) => { + const user = guard.requiresAuth().orRedirects().getUser(); + + return { + user: toPublicUser(user), + }; +}; + +export const actions = { + update: async ({ request, locals: { guard } }) => { + const auth = guard.requiresAuth(); + if (auth.isFailed()) return FORBIDDEN; + + const user = guard.getUser(); + + const schema = z.object({ + name: z + .string({ message: 'Name is required.' }) + .min(3, { message: 'Name must be at least 3 characters.' }) + .max(24, { message: 'Name must be at most 24 characters.' }), + password: z.string().refine((v) => (v.length > 0 ? v.length >= 8 : true), { + message: 'Password must be at least 8 characters.', + }), + }); + + const parsed = schema.safeParse(Object.fromEntries(await request.formData())); + if (!parsed.success) return PARSE_ERROR(parsed.error); + + let updatedUser: Updated = { id: user.id, ...parsed.data }; + + if (parsed.data.password.length > 0) { + updatedUser.password = bcrypt.hashSync(parsed.data.password, 10); + } else { + // avoid saving empty passwpord + delete updatedUser.password; + } + + const err = await users.update(updatedUser); + return err ? err.toFail() : SUCCESS; + }, +} satisfies Actions; diff --git a/src/routes/dash/account/+page.svelte b/src/routes/dash/account/+page.svelte new file mode 100644 index 0000000..dd7e371 --- /dev/null +++ b/src/routes/dash/account/+page.svelte @@ -0,0 +1,29 @@ + + +

{form?.error}

+ + + + +