Add settings page with password change
Deploy to LXC / deploy (push) Successful in 17s

Settings page shows account info and a change password form.
Validates current password, minimum 8 chars, confirmation match.
Added Settings link in sidebar.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 16:45:46 +07:00
parent 0baf17897c
commit 5e4021bc60
3 changed files with 124 additions and 0 deletions
+5
View File
@@ -51,6 +51,11 @@
href: '/gallery',
label: 'Gallery',
icon: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
},
{
href: '/settings',
label: 'Settings',
icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z'
}
]);
</script>
+51
View File
@@ -0,0 +1,51 @@
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db/index.js';
import { users } from '$lib/server/db/schema.js';
import { eq } from 'drizzle-orm';
import { fail } from '@sveltejs/kit';
import { verifyPassword, hashPassword } from '$lib/server/auth/password.js';
export const load: PageServerLoad = async ({ locals }) => {
return { user: locals.user! };
};
export const actions: Actions = {
changePassword: async ({ request, locals }) => {
const formData = await request.formData();
const currentPassword = formData.get('currentPassword') as string;
const newPassword = formData.get('newPassword') as string;
const confirmPassword = formData.get('confirmPassword') as string;
if (!currentPassword || !newPassword || !confirmPassword) {
return fail(400, { error: 'All fields are required' });
}
if (newPassword.length < 8) {
return fail(400, { error: 'New password must be at least 8 characters' });
}
if (newPassword !== confirmPassword) {
return fail(400, { error: 'New passwords do not match' });
}
const [user] = await db
.select({ passwordHash: users.passwordHash })
.from(users)
.where(eq(users.id, locals.user!.id));
if (!user) return fail(400, { error: 'User not found' });
const valid = await verifyPassword(user.passwordHash, currentPassword);
if (!valid) {
return fail(400, { error: 'Current password is incorrect' });
}
const newHash = await hashPassword(newPassword);
await db
.update(users)
.set({ passwordHash: newHash, updatedAt: new Date() })
.where(eq(users.id, locals.user!.id));
return { success: true };
}
};
+68
View File
@@ -0,0 +1,68 @@
<script lang="ts">
import { enhance } from '$app/forms';
let { data, form } = $props();
</script>
<svelte:head>
<title>Settings - B4L Repair</title>
</svelte:head>
<div class="mx-auto max-w-lg">
<h1 class="mb-6 text-2xl font-bold text-gray-900 dark:text-white">Settings</h1>
<!-- Account info -->
<div class="mb-6 rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Account</h2>
<dl class="space-y-2 text-sm">
<div>
<dt class="text-gray-500 dark:text-gray-400">Email</dt>
<dd class="text-gray-900 dark:text-white">{data.user.email}</dd>
</div>
{#if data.user.displayName}
<div>
<dt class="text-gray-500 dark:text-gray-400">Display Name</dt>
<dd class="text-gray-900 dark:text-white">{data.user.displayName}</dd>
</div>
{/if}
</dl>
</div>
<!-- Change password -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<h2 class="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Change Password</h2>
{#if form?.error}
<div class="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-300">
{form.error}
</div>
{/if}
{#if form?.success}
<div class="mb-4 rounded-md bg-green-50 p-3 text-sm text-green-700 dark:bg-green-900/30 dark:text-green-300">
Password changed successfully.
</div>
{/if}
<form method="POST" action="?/changePassword" use:enhance class="space-y-4">
<div>
<label for="currentPassword" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Current Password</label>
<input type="password" id="currentPassword" name="currentPassword" required autocomplete="current-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
</div>
<div>
<label for="newPassword" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">New Password</label>
<input type="password" id="newPassword" name="newPassword" required minlength="8" autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
</div>
<div>
<label for="confirmPassword" class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm New Password</label>
<input type="password" id="confirmPassword" name="confirmPassword" required minlength="8" autocomplete="new-password"
class="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
</div>
<button type="submit" class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
Change Password
</button>
</form>
</div>
</div>