This commit is contained in:
Drake Marino 2026-03-30 15:44:11 -05:00
parent 09cdce350d
commit a0efc6c628
6 changed files with 158 additions and 70 deletions

View File

@ -14,7 +14,9 @@ export const handle = async ({ event, resolve }) => {
event.locals.user = null; event.locals.user = null;
} else { } else {
try { try {
console.log(jwt.verify(JWT, process.env.JWT_SECRET));
event.locals.user = jwt.verify(JWT, process.env.JWT_SECRET); event.locals.user = jwt.verify(JWT, process.env.JWT_SECRET);
// console.log(event.locals.user);
} catch { } catch {
event.cookies.delete('jwt', { path: '/' }); event.cookies.delete('jwt', { path: '/' });
event.locals.user = null; event.locals.user = null;

View File

@ -1,4 +1,4 @@
import type { User, UserPayload } from '$lib/types/user'; import { DefaultUserSettings, type User, type UserPayload } from '$lib/types/user';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import { type Cookies, error } from '@sveltejs/kit'; import { type Cookies, error } from '@sveltejs/kit';
@ -9,7 +9,10 @@ export function setJWTCookie(cookies: Cookies, user: User) {
const payload = { const payload = {
id: user.id, id: user.id,
email: user.email, email: user.email,
name: user.name name: user.name,
settings: user.settings || DefaultUserSettings,
createdAt: user.createdAt,
lastSignIn: user.lastSignIn
}; };
if (process.env.JWT_SECRET === undefined) { if (process.env.JWT_SECRET === undefined) {
@ -65,8 +68,7 @@ export async function login(email: string, password: string): Promise<User> {
if (await bcrypt.compare(password, user.passwordHash!)) { if (await bcrypt.compare(password, user.passwordHash!)) {
delete user.passwordHash; delete user.passwordHash;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions await sql`
sql`
UPDATE users UPDATE users
SET last_sign_in = NOW() SET last_sign_in = NOW()
WHERE id = ${user.id}; WHERE id = ${user.id};

View File

@ -16,19 +16,21 @@
import { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote'; import { approveDenyItem, restoreClaimedItem } from '$lib/db/items.remote';
import { invalidateAll } from '$app/navigation'; import { invalidateAll } from '$app/navigation';
import NoImagePlaceholder from './no-image-placeholder.svelte'; import NoImagePlaceholder from './no-image-placeholder.svelte';
import type { User } from '$lib/types/user';
export let item: Item = <Item>{}; export let item: Item = <Item>{};
export let admin = false; export let user: User | null = null;
// export let admin = false;
export let editCallback: (item: Item) => void; export let editCallback: (item: Item) => void;
export let inquireCallback: (item: Item) => void; export let inquireCallback: (item: Item) => void;
export let claimCallback: (item: Item) => void; export let claimCallback: (item: Item) => void;
let timeSincePosted: number | string = (new Date().getTime() - item.foundDate.getTime()) / 1000 / 60 / 60 / 24; // days let timeSincePosted: number | string = (new Date().getTime() - item.foundDate.getTime()) / 1000 / 60 / 60 / 24; // days
if (timeSincePosted < 1) { // if (timeSincePosted < 1) {
timeSincePosted = '<1'; // timeSincePosted = '<1';
} else { // } else {
timeSincePosted = Math.round(timeSincePosted); // timeSincePosted = Math.round(timeSincePosted);
} // }
</script> </script>
@ -39,20 +41,14 @@
alt="Lost item"> alt="Lost item">
{:else} {:else}
<div class="min-h-48 w-full bg-accent flex flex-col justify-center"> <div class="min-h-48 w-full bg-accent flex flex-col justify-center">
<div class="justify-center flex "> <div class="justify-center flex ">
<NoImagePlaceholder className="" /> <NoImagePlaceholder className="" />
</div> </div>
<p class="text-center mt-4">No image available</p> <p class="text-center mt-4">No image available</p>
</div> </div>
{/if} {/if}
<div class="flex-col flex h-full px-2 pb-2"> <div class="flex-col flex h-full px-2 pb-2">
<!-- <div class="font-bold inline-block">{item.title}</div>-->
<!-- <div class="inline-block">-->
<div> <div>
{#if item.transferred} {#if item.transferred}
<Badge variant="secondary" class="inline-block">In Lost & Found</Badge> <Badge variant="secondary" class="inline-block">In Lost & Found</Badge>
{:else} {:else}
@ -62,8 +58,9 @@
<Tooltip.Root> <Tooltip.Root>
<Tooltip.Trigger <Tooltip.Trigger
> >
<Badge variant="outline" class="inline-block">{timeSincePosted} <Badge variant="outline"
day{(timeSincePosted === 1 || timeSincePosted === '<1') ? '' : 's'} ago class="inline-block {user?.settings !== null && user?.settings !== undefined && timeSincePosted >= user.settings.staleItemDays ? 'text-warning' : ''}">{timeSincePosted < 1 ? "<1" : Math.round(timeSincePosted)}
day{(timeSincePosted <= 1) ? '' : 's'} ago
</Badge> </Badge>
</Tooltip.Trigger </Tooltip.Trigger
> >
@ -72,9 +69,7 @@
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
</div> </div>
<div class="flex-1">{item.description}</div> <div class="flex-1">{item.description}</div>
{#if item.foundLocation} {#if item.foundLocation}
<div class="mt-2"> <div class="mt-2">
@ -82,8 +77,7 @@
<div>{item.foundLocation}</div> <div>{item.foundLocation}</div>
</div> </div>
{/if} {/if}
{#if user !== null}
{#if admin}
<div class="mt-2 justify-between flex"> <div class="mt-2 justify-between flex">
{#if item.approvedDate === null} {#if item.approvedDate === null}

View File

@ -1,6 +1,9 @@
import type { PageServerLoad } from '$types'; import type { PageServerLoad } from '$types';
import sql from '$lib/db/db.server'; import sql from '$lib/db/db.server';
import type { User } from '$lib/types/user'; import type { User, UserSettings } from '$lib/types/user';
import { type Actions, error, fail } from '@sveltejs/kit';
import { getFormString, getRequiredFormString } from '$lib/shared';
import bcrypt from 'bcrypt';
export const load: PageServerLoad = async ({ locals }) => { export const load: PageServerLoad = async ({ locals }) => {
if (!locals || !locals.user) { if (!locals || !locals.user) {
@ -15,3 +18,58 @@ export const load: PageServerLoad = async ({ locals }) => {
return { userData }; return { userData };
}; };
export const actions: Actions = {
default: async ({ request, url, locals, params }) => {
if (!locals || !locals.user) {
throw error(403, 'Need to be logged in!');
}
const data = await request.formData();
let name: string;
let email: string;
let newPassword: string | null;
let retypeNewPassword: string | null;
let staleItemDays: number;
let notifyAllApprovedInquiries: boolean;
let notifyAllTurnedInInquiries: boolean;
try {
name = getRequiredFormString(data, 'name');
email = getRequiredFormString(data, 'email');
newPassword = getFormString(data, 'newPassword');
retypeNewPassword = getFormString(data, 'retypeNewPassword');
staleItemDays = parseInt(getRequiredFormString(data, 'staleItemDays'));
notifyAllApprovedInquiries = getFormString(data, 'notifyAllApprovedInquiries') == 'on';
notifyAllTurnedInInquiries = getFormString(data, 'notifyAllTurnedInInquiries') == 'on';
if (newPassword !== retypeNewPassword) {
fail(400, { password: 'New passwords dont match!' });
}
const passwordHash = await bcrypt.hash(newPassword!, 12);
const settings: UserSettings = {
staleItemDays,
notifyAllApprovedInquiries,
notifyAllTurnedInInquiries
};
return await sql`
UPDATE users SET name = ${name},
email = ${email},
password_hash = ${passwordHash},
settings = ${settings.toString()}
WHERE id = ${locals.user.id}
RETURNING *;
`;
} catch (e) {
return fail(400, {
message: e instanceof Error ? e.message : 'Unknown error occurred',
success: false
});
}
return { success: true };
}
} satisfies Actions;

View File

@ -17,16 +17,16 @@
window.location.href = '/'; window.location.href = '/';
} }
let form = $derived({ // Use top-level variables for two-way binding. Binding to object properties
name: data.userData.name, // like `form.name` doesn't create proper reactive two-way bindings in Svelte.
email: data.userData.email, let name: string = $state(data.userData.name);
staleItemDays: let email: string = $state(data.userData.email);
data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays, let staleItemDays: number =
notifyAllApprovedInquiries: $state(data.userData.settings?.staleItemDays ?? DefaultUserSettings.staleItemDays);
data.userData.settings?.notifyAllApprovedInquiries ?? false, let notifyAllApprovedInquiries: boolean =
notifyAllTurnedInInquiries: $state(data.userData.settings?.notifyAllApprovedInquiries ?? false);
data.userData.settings?.notifyAllTurnedInInquiries ?? false let notifyAllTurnedInInquiries: boolean =
}); $state(data.userData.settings?.notifyAllTurnedInInquiries ?? false);
const formatDate = (date: Date | string) => { const formatDate = (date: Date | string) => {
const d = new Date(date); const d = new Date(date);
@ -50,6 +50,7 @@
<CardContent class="space-y-6"> <CardContent class="space-y-6">
<!-- Editable Profile Form --> <!-- Editable Profile Form -->
<form method="POST" use:enhance class="space-y-6"> <form method="POST" use:enhance class="space-y-6">
<div class="grid gap-4 sm:grid-cols-2"> <div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2"> <div class="space-y-2">
<Label for="username">Username</Label> <Label for="username">Username</Label>
@ -57,64 +58,95 @@
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="name">Full Name</Label> <Label for="name">Full Name<span class="text-error">*</span></Label>
<Input id="name" name="name" bind:value={form.name} /> <Input id="name" name="name" bind:value={name} />
</div> </div>
<div class="space-y-2 sm:col-span-2"> <div class="space-y-2 sm:col-span-2">
<Label for="email">Email</Label> <Label for="email">Email<span class="text-error">*</span></Label>
<Input id="email" name="email" type="email" bind:value={form.email} /> <Input id="email" name="email" type="email" bind:value={email} />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<Label for="staleItemDays">Stale Item Days</Label> <Label for="newPassword">New Password</Label>
<Input <Input id="newPassword" name="newPassword" />
id="staleItemDays"
name="staleItemDays"
type="number"
bind:value={form.staleItemDays}
min="0"
/>
</div> </div>
<div class="space-y-2">
<Label for="retypeNewPassword">Retype New Password</Label>
<Input id="retypeNewPassword" name="retypeNewPassword" />
</div>
</div> </div>
<Separator /> <Separator />
<div class="space-y-4"> <div class="space-y-4">
<h2 class="text-lg font-semibold">Notifications</h2> <h2 class="text-lg font-semibold">Settings</h2>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <Label for="staleItemDays" class="text-base">
<p class="font-medium">Notify All Approved</p> <div>
<p class="text-sm text-muted-foreground"> <p class="font-medium">Stale Item Days</p>
Receive notifications when inquiries are approved. <p class="text-sm text-muted-foreground">
</p> Number of days without activity before items show up as stale.
</div> </p>
<Switch </div>
name="notifyAllApprovedInquiries" </Label>
bind:checked={form.notifyAllApprovedInquiries} <Input
class="w-min inline-block"
id="staleItemDays"
name="staleItemDays"
type="number"
bind:value={staleItemDays}
min="0"
/> />
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <Label for="notifyAllApprovedInquiries" class="text-base">
<p class="font-medium">Notify All Turned In</p> <div>
<p class="text-sm text-muted-foreground">
Receive notifications when inquiries are turned in. <p class="font-medium">Notify for All Approved Items</p>
</p> <p class="text-sm text-muted-foreground">
</div> Receive notifications for all approved items, not just yours.
</p>
</div>
</Label>
<Switch
name="notifyAllApprovedInquiries"
id="notifyAllApprovedInquiries"
bind:checked={notifyAllApprovedInquiries}
/>
</div>
<div class="flex items-center justify-between">
<Label for="notifyAllTurnedInInquiries" class="text-base">
<div>
<p class="font-medium">Notify for All Turned In Items</p>
<p class="text-sm text-muted-foreground">
Receive notifications for all items turned in to the school lost-and-found.
</p>
</div>
</Label>
<Switch <Switch
name="notifyAllTurnedInInquiries" name="notifyAllTurnedInInquiries"
bind:checked={form.notifyAllTurnedInInquiries} id="notifyAllTurnedInInquiries"
bind:checked={notifyAllTurnedInInquiries}
/> />
</div> </div>
</div> </div>
<div class="flex items-center justify-between pt-4"> <div class="flex items-center justify-between pt-4">
<div class="text-sm text-muted-foreground"> <div class="text-sm text-muted-foreground">
Member since {formatDate(data.userData.createdAt)} · Last sign in {formatDate( <p>
data.userData.lastSignIn
)} Member since {formatDate(data.userData.createdAt)}
</p>
<p>
Last sign in {formatDate(data.userData.lastSignIn)}
</p>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">

View File

@ -86,7 +86,7 @@
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.approvedDate === null} {#if item.approvedDate === null}
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog} <ItemListing item={item} user={data.user} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
@ -102,7 +102,7 @@
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.approvedDate !== null && item.claimedDate === null} {#if item.approvedDate !== null && item.claimedDate === null}
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog} <ItemListing item={item} user={data.user} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}
@ -115,7 +115,7 @@
</FieldSeparator> </FieldSeparator>
{#each data.items as item (item.id)} {#each data.items as item (item.id)}
{#if item.claimedDate !== null} {#if item.claimedDate !== null}
<ItemListing item={item} admin={data.user !== null} editCallback={openEditDialog} <ItemListing item={item} user={data.user} editCallback={openEditDialog}
inquireCallback={openInquireDialog} claimCallback={openClaimDialog} /> inquireCallback={openInquireDialog} claimCallback={openClaimDialog} />
{/if} {/if}
{/each} {/each}