Compare commits

...

2 Commits

Author SHA1 Message Date
d7ff30dca0 feat: self service account updating - wip #1 2025-04-18 22:59:33 +02:00
078e8db2c7 fix: unwanted reset behavior 2025-04-18 22:56:44 +02:00
4 changed files with 89 additions and 1 deletions

View File

@ -9,7 +9,15 @@
</script>
<div class="w-full sm:px-6 px-3 py-4 max-w-xl mx-auto mb-12">
<form method="POST" action="?/update" use:enhance>
<form
method="POST"
action="?/update"
use:enhance={() => {
return async ({ update }) => {
update({ reset: false });
};
}}
>
{@render children()}
<div class="flex gap-2 justify-between sm:text-base text-sm">

View File

@ -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 @@
</DropdownMenu.Item>
{/snippet}
{@render DropDownLink(IconUserCircle, 'Account', '/dash/account')}
{@render DropDownLink(IconDeviceDesktopPin, 'Sessions', '/dash/account/sessions')}
{@render DropDownLink(IconLogout, 'Sign out', '/logout', false)}
</div>

View File

@ -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<User> = { 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;

View File

@ -0,0 +1,29 @@
<script lang="ts">
import InputText from '$lib/v2/forms/InputText.svelte';
import { pageTitle } from '$lib/v2/globalStores';
import EditPage from '$lib/v2/snippets/EditPage.svelte';
let { data, form } = $props();
$pageTitle = 'Account details';
</script>
<p class="text-white">{form?.error}</p>
<EditPage createOnly>
<InputText
name="name"
label="Name"
description="Name you will use to sign in to the dashboard"
value={data.user.name}
class="w-full"
required
/>
<InputText
name="password"
label="Password"
description="Leave blank to keep your current password"
type="password"
class="w-full"
/>
</EditPage>