updates
This commit is contained in:
parent
09cdce350d
commit
a0efc6c628
@ -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;
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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">
|
||||||
@ -127,4 +159,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user