Skip to content

Commit

Permalink
chore: wip
Browse files Browse the repository at this point in the history
  • Loading branch information
shivan-s committed Oct 6, 2024
1 parent 70c937d commit 6f25ee2
Show file tree
Hide file tree
Showing 13 changed files with 128 additions and 86 deletions.
9 changes: 5 additions & 4 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { dev } from '$app/environment';
import { decodeJWT } from '$lib/server/crypto';
import { fetchUserById } from '$lib/server/db/users';
import { redirect, type Handle } from '@sveltejs/kit';

const WHITELISTED_PATHS: readonly string[] = ['/', '/login', '/signup', '/healthz'];

export const handle: Handle = async ({ event, resolve }) => {
// Debugging
const requestPath = event.url.pathname;
console.log('Path:', requestPath);
if (dev) {
const requestPath = event.url.pathname;
console.log('Path:', requestPath);
}
// Verify User
const authToken = event.cookies.get('auth-token');
if (authToken) {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/components/Card.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
font-size: 2rem;
color: var(--bg);
background-color: var(--primary);
padding: 1rem 1.5rem;
padding: 0.5rem 1rem;
}
div {
width: 100%;
Expand Down
7 changes: 6 additions & 1 deletion src/lib/components/Modal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@
><CloseIcon /></button
></span
>
<span>Body</span>
<span><slot /></span>
</Card>
</dialog>

<style>
dialog {
position: fixed;
background: transparent;
width: fit-content;
height: fit-content;
margin: auto;
overflow: auto;
}
dialog::backdrop {
backdrop-filter: blur(10px);
Expand Down
51 changes: 23 additions & 28 deletions src/lib/components/Navbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { navigating, page } from '$app/stores';
import type { SelectUser } from '$lib/server/db/schema';
import { ROCK, POLE } from '$lib/characters';
import Modal from './Modal.svelte';
export let user: Pick<SelectUser, 'stagehandle' | 'isAdmin'>;
</script>
Expand All @@ -18,17 +19,24 @@
<a class:current={$page.url.pathname.startsWith('/admin')} href="/admin">Admin</a>
{/if}
</span>
<span>
<span class="handle">
{#if user}
<form class="logout" method="POST" action="/?logout">
<button>Logout</button>
</form>
<a class:current={$page.url.pathname.startsWith('/settings')} href="/settings"
>{user.stagehandle}</a
>
<button popovertarget="user" popovertargetaction="show">@{user.stagehandle}</button>
{/if}
</span>
</nav>
<Modal id="user">
<span slot="header">@{user.stagehandle}</span>
<span slot="body">
<ul>
<li>
<form class="logout" method="POST" action="?/logout">
<button>Logout</button>
</form>
</li>
</ul>
</span>
</Modal>

<style>
nav {
Expand All @@ -44,40 +52,27 @@
display: flex;
}
nav > span > a,
nav > span > form {
display: flex;
align-items: center;
padding: 0 2rem 0 2rem;
}
a,
button {
display: flex;
align-items: center;
border: 0;
background: transparent;
font-weight: 400;
padding: 0rem 2rem;
color: var(--primary);
text-decoration: none;
text-align: center;
}
a:hover,
.current,
form:hover {
a.current,
button:hover {
background: var(--primary);
color: var(--bg);
}
form.logout:hover {
background: var(--danger);
color: var(--bg);
}
form.logout > button {
width: 100%;
height: 100%;
background: none;
border: none;
color: inherit;
}
button:active,
a:active {
text-decoration: underline;
}
Expand Down
13 changes: 13 additions & 0 deletions src/lib/components/UL.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import type { HTMLOlAttributes } from 'svelte/elements';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface $$Props extends HTMLOlAttributes {}
</script>

<ul {...$$props}><slot /></ul>

<style>
ul {
list-style-position: inside;
}
</style>
6 changes: 3 additions & 3 deletions src/lib/server/crypto/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export async function hashPassword(password: string): Promise<Hash> {
* @param payload Typically user details
* @returns Signed JWT
*/
export async function issueJWT(user: Pick<SelectUser, 'id' | 'username'>): Promise<string> {
export async function issueJWT(user: Pick<SelectUser, 'id' | 'stagehandle'>): Promise<string> {
const secret = new TextEncoder().encode(SECRET);
const alg = 'HS256';
const JWT_EXPIRE_TIME = '1 week';
const jwt = await new jose.SignJWT({ user: { id: user.id, username: user.username } })
const jwt = await new jose.SignJWT({ user: { id: user.id, username: user.stagehandle } })
.setProtectedHeader({ alg })
.setIssuedAt()
.setExpirationTime(JWT_EXPIRE_TIME)
Expand All @@ -52,7 +52,7 @@ export async function issueJWT(user: Pick<SelectUser, 'id' | 'username'>): Promi

export async function decodeJWT(
jwt: string
): Promise<({ user?: Pick<SelectUser, 'id' | 'username'> } & jose.JWTPayload) | null> {
): Promise<({ user?: Pick<SelectUser, 'id' | 'stagehandle'> } & jose.JWTPayload) | null> {
const secret = new TextEncoder().encode(SECRET);
try {
const { payload } = (await jose.jwtVerify(jwt, secret)) as {
Expand Down
14 changes: 13 additions & 1 deletion src/lib/server/db/users.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { and, desc, eq, isNull, like } from 'drizzle-orm';
import { and, desc, eq, isNull, like, sql } from 'drizzle-orm';
import { passwordsTable, usersTable, type SelectPassword, type SelectUser } from './schema';
import { PAGE_LIMIT } from '$lib/constants';
import type { Hash } from '$lib/server/crypto';
Expand Down Expand Up @@ -116,3 +116,15 @@ export async function createUser(user: { stageHandle: string; hash: Hash }): Pro
{ behavior: 'deferred' }
);
}

/**
* Update user last login
*
* @param id User id
*/
export async function updateUserLastLoginById(id: number): Promise<void> {
await db
.update(usersTable)
.set({ lastLogin: sql`CURRENT_TIMESTAMP` })
.where(eq(usersTable.id, id));
}
2 changes: 2 additions & 0 deletions src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export { dateStamp, fromNow } from './time';

export { SignupSchema, LoginSchema } from './schemas';
28 changes: 28 additions & 0 deletions src/lib/utils/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { z } from 'zod';

const StageHandle = z
.string()
.trim()
.min(6)
.max(16)
.toLowerCase()
.refine(
(data) => /^[a-z\._0-9]*$/.test(data),

Check failure on line 10 in src/lib/utils/schemas/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Unnecessary escape character: \.

Check failure on line 10 in src/lib/utils/schemas/index.ts

View workflow job for this annotation

GitHub Actions / Lint

Unnecessary escape character: \.
"Stage handle can only contain lower case letters (a-z), '.' and/or '_'"
);

const Password = z.string().trim().min(8).max(128);

export const LoginSchema = z
.object({
stagehandle: StageHandle,
password: Password
})
.strip();

export const SignupSchema = z
.object({
stagehandle: StageHandle,
password: Password
})
.strip();
24 changes: 10 additions & 14 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { PageServerLoad, Actions } from './$types';
import { fetchManyUsers } from '$lib/server/db/users';
import { fetchManyUsers, updateUserLastLoginById } from '$lib/server/db/users';
import { WAVE } from '$lib/characters';
import { fail, setError, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { z } from 'zod';
import { redirect } from '@sveltejs/kit';
import { issueJWT, checkPassword } from '$lib/server/crypto';
import { fetchUserWithPasswordByStageHandle } from '$lib/server/db/users';
import { LoginSchema } from '$lib/utils';

export const load: PageServerLoad = async ({ url }) => {
const users = await fetchManyUsers();
Expand All @@ -20,13 +20,6 @@ export const load: PageServerLoad = async ({ url }) => {
};
};

const LoginSchema = z
.object({
username: z.string().trim().min(6).max(32),
password: z.string().trim().min(8).max(128)
})
.strip();

export const actions: Actions = {
login: async ({ request, locals, cookies }) => {
console.time('login');
Expand All @@ -35,28 +28,31 @@ export const actions: Actions = {
return fail(400, { form });
}
// NOTE: Ensure no time-based attacks, hence set errors at end
const user = await fetchUserWithPasswordByStageHandle(form.data.username);
const user = await fetchUserWithPasswordByStageHandle(form.data.stagehandle);
const validLogin = await checkPassword(form.data.password, user);
if (!validLogin || !user) {
if (!user) {
console.timeLog('login', 'Username is invalid');
console.timeLog('login', 'StageHandle is invalid');
console.timeEnd('login');
}
if (!validLogin) {
console.timeLog('login', 'Password is invalid');
console.timeEnd('login');
}
return setError(form, '', 'Incorrect username or password');
}
console.timeLog('login', 'Login Success for: ', user.users.username);
console.timeLog('login', 'Login Success for: ', user.users.stagehandle);
const jwt = await issueJWT(user.users);
cookies.set('auth-token', jwt, { httpOnly: true, path: '/' });
updateUserLastLoginById(user.users.id);
locals.user = user.users;
console.timeEnd('login');
return redirect(303, '/me');
redirect(303, '/');
},
logout: async ({ locals, cookies }) => {
if (locals.user) {
cookies.delete('auth-token', { httpOnly: true, path: '/' });
return redirect(303, '/login');
return redirect(303, '/');
}
}
};
35 changes: 21 additions & 14 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,30 @@
import Input from '$lib/components/Input.svelte';
import Label from '$lib/components/Label.svelte';
import { superForm } from 'sveltekit-superforms';
import { addToast, loading } from '$lib/stores';
import { addToast } from '$lib/stores';
import Alert from '$lib/components/Alert.svelte';
import A from '$lib/components/A.svelte';
import { fromNow } from '$lib/utils';
import { PROGRESS, JOINED, CHECKED } from '$lib/characters';
import H2 from '$lib/components/H2.svelte';
import UL from '$lib/components/UL.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { goto } from '$app/navigation';
export let data: PageData;
const { form, capture, restore, constraints, enhance, allErrors, delayed } = superForm(
data.form,
{
onUpdate: ({ result }) => {
if (result.type === 'success')
addToast({ message: 'Login successful', directive: 'success' }), loading.set(false);
onResult: ({ result }) => {
if (result.type === 'redirect') {
goto(result.location);
}
},
applyAction: false
onUpdated: ({ form }) => {
if (form.valid) {
addToast({ message: 'Login successful', directive: 'success' });
}
}
}
);
Expand All @@ -41,14 +48,14 @@
<form method="POST" action="?/login" use:enhance>
<div class="flex-col">
<FormSet>
<Label for="username"><span>Username</span></Label>
<Label for="stagehandle"><span>Stage Handle</span></Label>
<Input
id="username"
name="username"
id="stagehandle"
name="stagehandle"
type="text"
placeholder="Username (required)"
bind:value={$form.username}
{...$constraints.username}
placeholder="Stage Handle (required)"
bind:value={$form.stagehandle}
{...$constraints.stagehandle}
/>
<Label for="name"><span>Password</span></Label>
<Input
Expand All @@ -61,7 +68,7 @@
/>
<span />
<Button type="submit"
>{#if delayed}<Spinner />{:else}Log in{/if}</Button
>{#if $delayed}<Spinner />{:else}Log in{/if}</Button
>
<span />
<A href="/signup">No account? Sign up here</A>
Expand All @@ -71,13 +78,13 @@
{#if $allErrors.length > 0}
<Alert directive="danger"
><span slot="header">Error</span>
<ul>
<UL>
{#each $allErrors as e}
<li>
{e.messages.join('. ')}
</li>
{/each}
</ul>
</UL>
</Alert>
{/if}
</Card>
Expand Down
5 changes: 2 additions & 3 deletions src/routes/settings/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@
import type { PageData } from './$types';
export let data: PageData;
console.log('data', data);
</script>

<div class="wrapper">
<span class="header">Username</span>
<span class="data">{data.user.username}</span>
<span class="header">Stage Handle</span>
<span class="data">{data.user.stagehandle}</span>
<span class="header">Name</span>
{#if data.user.name}
<span class="data">{data.user.name}</span>
Expand Down
Loading

0 comments on commit 6f25ee2

Please sign in to comment.