Skip to content

Commit

Permalink
login flow
Browse files Browse the repository at this point in the history
  • Loading branch information
OliverGrack committed Jan 19, 2025
1 parent 72d764d commit f2168ad
Show file tree
Hide file tree
Showing 16 changed files with 379 additions and 66 deletions.
4 changes: 2 additions & 2 deletions src/components/main-nav/theme-switcher.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Moon, Sun } from 'lucide-solid';
import { type Component, Show } from 'solid-js';
import { COOKIE_NAME_THEME } from '~/lib/cookies/cookie-names';
import { COOKIE_THEME } from '~/lib/cookies/cookie-names';
import { cookiesClientSet } from '~/lib/cookies/cookies-client';
import { useThemeStore } from '~/lib/viz/store/theme-store';
import { Button } from '../ui/button';
Expand All @@ -14,7 +14,7 @@ export const ThemeSwitcher: Component = () => {
function toggleTheme() {
const theme = themeStore.currentTheme() === 'light' ? 'dark' : 'light';
themeStore.setCurrentTheme(theme);
cookiesClientSet(COOKIE_NAME_THEME, theme, 365 * 5);
cookiesClientSet(COOKIE_THEME, theme, 365 * 5);
}

return (
Expand Down
12 changes: 11 additions & 1 deletion src/entry-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,29 @@
import { createHandler, StartServer } from '@solidjs/start/server';
import { FaviconsHead } from './components/favicons-head';
import { serverCookiesGetSyncDontUse } from './lib/cookies/cookies-server';
import { COOKIE_THEME } from './lib/cookies/cookie-names';

const jsonLd = JSON.stringify({
'@context': 'https://schema.org',
'@type': 'WebSite',
url: 'https://www.hkviz.org',
name: 'HKViz',
});

export default createHandler(() => {
return (
<StartServer
document={({ assets, children, scripts }) => {
const theme = serverCookiesGetSyncDontUse().get('theme') === 'light' ? 'light' : 'dark';
const theme = serverCookiesGetSyncDontUse().getSafe(COOKIE_THEME);

return (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{/* eslint-disable-next-line solid/no-innerhtml */}
<script type="application/ld+json" innerHTML={jsonLd} />
<FaviconsHead theme={theme} />
{assets}
</head>
Expand Down
53 changes: 51 additions & 2 deletions src/lib/cookies/cookie-names.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,51 @@
export const COOKIE_NAME_INGAME_AUTH_URL_ID = 'ingameAuthUrlId';
export const COOKIE_NAME_THEME = 'theme';
import * as v from 'valibot';

export class CookieDefinition<T> {
readonly name: string;
readonly schema: v.BaseSchema<string | null | undefined, T, v.BaseIssue<unknown>>;
readonly maxAge: number;
readonly httpOnly: boolean;
readonly sameSite: 'strict' | 'lax' | 'none';

constructor({
name,
schema,
maxAge,
httpOnly,
sameSite,
}: {
name: string;
schema: v.BaseSchema<string | null | undefined, T, v.BaseIssue<unknown>>;
maxAge?: number;
httpOnly?: boolean;
sameSite?: 'strict' | 'lax' | 'none';
}) {
this.name = name;
this.schema = schema;
this.maxAge = maxAge ?? 60 * 60 * 24 * 365 * 1; // 1 year
this.httpOnly = httpOnly ?? true;
this.sameSite = sameSite ?? 'strict';
}
}

export type CookieNameLike = string | CookieDefinition<unknown>;
export function getCookieName(name: CookieNameLike): string {
return typeof name === 'string' ? name : name.name;
}

export const COOKIE_INGAME_AUTH_URL_ID = new CookieDefinition({
name: 'ingameAuthUrlId',
schema: v.nullish(v.pipe(v.string(), v.uuid())),
maxAge: 60 * 15, // 15 minutes
sameSite: 'lax', // not quite sure why this needs to be lax, but the cookie seems to be not present after the redirect from the discord login
});

export const COOKIE_THEME = new CookieDefinition({
name: 'theme',
schema: v.pipe(
v.nullish(v.string()),
v.transform((v) => (v === 'light' ? 'light' : 'dark')),
),
maxAge: 60 * 60 * 24 * 365 * 10, // 10 year
httpOnly: false,
});
16 changes: 10 additions & 6 deletions src/lib/cookies/cookies-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export function cookiesClientSet(name: string, value: string, days: number) {
import { CookieDefinition, CookieNameLike, getCookieName } from './cookie-names';
import * as v from 'valibot';

export function cookiesClientSet(name: CookieNameLike, value: string, days: number) {
const _name = getCookieName(name);
let expires: string;
if (days) {
const date = new Date();
Expand All @@ -7,21 +11,21 @@ export function cookiesClientSet(name: string, value: string, days: number) {
} else {
expires = '';
}
document.cookie = name + '=' + value + expires + '; path=/';
document.cookie = _name + '=' + value + expires + '; path=/';
}

export function cookiesClientRead(name: string) {
const nameEQ = name + '=';
export function cookiesClientRead<T>(definition: CookieDefinition<T>): T {
const nameEQ = definition.name + '=';
const ca = document.cookie.split(';');
for (let c of ca) {
while (c.startsWith(' ')) {
c = c.substring(1, c.length);
}
if (c.startsWith(nameEQ)) {
return c.substring(nameEQ.length, c.length);
return v.parse(definition.schema, c.substring(nameEQ.length, c.length));
}
}
return null;
return v.parse(definition.schema, null);
}

export function cookiesClientDelete(name: string) {
Expand Down
23 changes: 21 additions & 2 deletions src/lib/cookies/cookies-response-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
import { CustomResponse, json, RouterResponseInit } from '@solidjs/router';
import { CustomResponse, json, redirect, RouterResponseInit } from '@solidjs/router';
import { ServerCookies } from './cookies-server';
import { combineHeaders } from './headers';

export function jsonWithCookies<T>(data: T, cookies: ServerCookies, init?: RouterResponseInit): CustomResponse<T> {
export function withCookies(
cookies: ServerCookies,
init?: RouterResponseInit | undefined,
): RouterResponseInit | undefined {
let initModified = init;

const headers = cookies.getSetHeaders();
console.log({ headers });

if (headers.length) {
initModified ??= {};
initModified.headers = combineHeaders(initModified.headers, headers);
}

return initModified;
}

export function jsonWithCookies<T>(data: T, cookies: ServerCookies, init?: RouterResponseInit): CustomResponse<T> {
const initModified = withCookies(cookies, init);
console.log(JSON.stringify({ initModified }, null, 2));
return json(data, initModified);
}

export function redirectWithCookies(
url: string,
cookies: ServerCookies,
init?: RouterResponseInit,
): CustomResponse<never> {
const initModified = withCookies(cookies, init);
return redirect(url, initModified);
}
42 changes: 28 additions & 14 deletions src/lib/cookies/cookies-server.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { revalidate } from '@solidjs/router';
import { afterAll, beforeEach, expect, test, vi } from 'vitest';
import { serverCookiesGet } from './cookies-server';
import { CookieDefinition } from './cookie-names';
import * as v from 'valibot';

let currentCookieHeader = '';
vi.mock('vinxi/http', async (importOriginal) => {
Expand All @@ -21,6 +23,16 @@ beforeEach(() => {
revalidate(serverCookiesGet.key);
});

const COOKIE_NAME = new CookieDefinition({
name: 'name',
schema: v.string(),
});

const COOKIE_NAME2 = new CookieDefinition({
name: 'name2',
schema: v.string(),
});

test('parses simple cookies', async () => {
currentCookieHeader = 'name=value; name2=value2'; // update for this test
const cookies = await serverCookiesGet();
Expand Down Expand Up @@ -53,8 +65,10 @@ test('parses cookies with spaces', async () => {

test('sets cookie correctly', async () => {
const cookies = await serverCookiesGet();
cookies.set('name', 'value');
expect(cookies.getSetHeaders()).toEqual([['Set-Cookie', 'name=value']]);
cookies.set(COOKIE_NAME, 'value');
expect(cookies.getSetHeaders()).toEqual([
['Set-Cookie', 'name=value; Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict'],
]);
expect(cookies.getAll()).toEqual(new Map([['name', 'value']]));
});

Expand All @@ -63,19 +77,19 @@ test('deletes cookie correctly', async () => {
const cookies = await serverCookiesGet();
cookies.delete('name');

expect(cookies.getSetHeaders()).toEqual([['Set-Cookie', 'name=; Max-Age=-1']]);
expect(cookies.getSetHeaders()).toEqual([['Set-Cookie', 'name=; Max-Age=-1; Path=/']]);
expect(cookies.getAll()).toEqual(new Map());
});

test('deduplicates set headers', async () => {
// initial values
const cookies = await serverCookiesGet();
cookies.set('name', 'value');
cookies.set('name2', 'value2');
cookies.set(COOKIE_NAME, 'value');
cookies.set(COOKIE_NAME2, 'value2');

expect(cookies.getSetHeaders()).toEqual([
['Set-Cookie', 'name=value'],
['Set-Cookie', 'name2=value2'],
['Set-Cookie', 'name=value; Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict'],
['Set-Cookie', 'name2=value2; Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict'],
]);
expect(cookies.getAll()).toEqual(
new Map([
Expand All @@ -85,11 +99,11 @@ test('deduplicates set headers', async () => {
);

// update values
cookies.set('name', 'new-value');
cookies.set(COOKIE_NAME, 'new-value');

expect(cookies.getSetHeaders()).toEqual([
['Set-Cookie', 'name=new-value'],
['Set-Cookie', 'name2=value2'],
['Set-Cookie', 'name=new-value; Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict'],
['Set-Cookie', 'name2=value2; Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict'],
]);
expect(cookies.getAll()).toEqual(
new Map([
Expand All @@ -102,8 +116,8 @@ test('deduplicates set headers', async () => {
cookies.delete('name');

expect(cookies.getSetHeaders()).toEqual([
['Set-Cookie', 'name=; Max-Age=-1'],
['Set-Cookie', 'name2=value2'],
['Set-Cookie', 'name=; Max-Age=-1; Path=/'],
['Set-Cookie', 'name2=value2; Max-Age=31536000; Path=/; HttpOnly; SameSite=Strict'],
]);
expect(cookies.getAll()).toEqual(new Map([['name2', 'value2']]));
});
Expand Down Expand Up @@ -138,7 +152,7 @@ test('does not parse cookies if getHeaders is called', async () => {
test('does not parse cookies when reading already set cookie', async () => {
currentCookieHeader = 'name=value';
const cookies = await serverCookiesGet();
cookies.set('name2', 'value2');
cookies.set(COOKIE_NAME2, 'value2');
expect(cookies.get('name2')).toBe('value2');
expect(cookies.didParse).toBe(false);
});
Expand All @@ -154,7 +168,7 @@ test('does not parse cookies when reading already set cookie', async () => {
test('does not overwrite already set cookie when parsing after set', async () => {
currentCookieHeader = 'name=value; name2=value2';
const cookies = await serverCookiesGet();
cookies.set('name', 'new-value');
cookies.set(COOKIE_NAME, 'new-value');
expect(cookies.get('name')).toBe('new-value');
expect(cookies.didParse).toBe(false);

Expand Down
33 changes: 24 additions & 9 deletions src/lib/cookies/cookies-server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { query } from '@solidjs/router';
import { parse, serialize } from 'cookie';
import { getWebRequest } from 'vinxi/http';
import * as v from 'valibot';
import { CookieSerializeOptions, getWebRequest } from 'vinxi/http';
import { CookieDefinition, CookieNameLike, getCookieName } from './cookie-names';

export class ServerCookies {
#cookies = new Map<string, string>();
Expand Down Expand Up @@ -32,16 +34,24 @@ export class ServerCookies {
}
}

set(name: string, value: string, options: { maxAge?: number } = {}) {
const setCookieHeader = serialize(name, value, options);
this.#setHeaders.set(name, ['Set-Cookie', setCookieHeader]);
this.#cookies.set(name, value);
set<T>(definition: CookieDefinition<T>, value: string, options: CookieSerializeOptions = {}) {
const optionsWithDefaults = options ?? {};
optionsWithDefaults.path ??= '/';
optionsWithDefaults.httpOnly ??= definition.httpOnly;
optionsWithDefaults.sameSite ??= definition.sameSite;
optionsWithDefaults.maxAge ??= definition.maxAge;

const _name = getCookieName(definition);
const setCookieHeader = serialize(_name, value, options);
this.#setHeaders.set(_name, ['Set-Cookie', setCookieHeader]);
this.#cookies.set(_name, value);
}

delete(name: string) {
const deleteCookieHeader = serialize(name, '', { maxAge: -1 });
this.#setHeaders.set(name, ['Set-Cookie', deleteCookieHeader]);
this.#cookies.delete(name);
delete(name: CookieNameLike) {
const _name = typeof name === 'string' ? name : name.name;
const deleteCookieHeader = serialize(_name, '', { maxAge: -1, path: '/' });
this.#setHeaders.set(_name, ['Set-Cookie', deleteCookieHeader]);
this.#cookies.delete(_name);
}

get(name: string): string | undefined {
Expand All @@ -51,6 +61,11 @@ export class ServerCookies {
return this.#cookies.get(name);
}

getSafe<T>(definition: CookieDefinition<T>): T {
const value = this.get(definition.name);
return v.parse(definition.schema, value);
}

getAll(): ReadonlyMap<string, string> {
this.#ensureParsed();
return this.#cookies;
Expand Down
4 changes: 2 additions & 2 deletions src/lib/viz/store/theme-store.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContext, createEffect, createSignal, untrack, useContext } from 'solid-js';
import { COOKIE_NAME_THEME } from '~/lib/cookies/cookie-names';
import { COOKIE_THEME } from '~/lib/cookies/cookie-names';
import { cookiesClientRead } from '~/lib/cookies/cookies-client';
// import { effect } from 'solid-js/web';
// import { COOKIE_NAME_THEME } from '~/lib/cookies/cookie-names';
Expand All @@ -26,7 +26,7 @@ export function createThemeStore() {

createEffect(() => {
untrack(() => {
const theme = cookiesClientRead(COOKIE_NAME_THEME) === 'light' ? 'light' : 'dark';
const theme = cookiesClientRead(COOKIE_THEME);
setCurrentTheme(theme);
});
});
Expand Down
2 changes: 1 addition & 1 deletion src/routes/api/rest/ingameauth/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ import { ingameAuthInit } from '~/server/ingameauth/init';
export async function POST({ request }: APIEvent) {
const body: unknown = await request.json();
// will be validated by valibot, so ok to pass any

return await ingameAuthInit(body as any);
}
7 changes: 7 additions & 0 deletions src/routes/ingameauth/[urlId].tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { APIEvent } from '@solidjs/start/server';
import { ingameAuthGetByUrlId } from '~/server/ingameauth/get-active-by-url-id-set-cookie';

export async function GET({ params }: APIEvent) {
console.log('urkId', params.urlId);
return await ingameAuthGetByUrlId({ urlId: params.urlId });
}
Loading

0 comments on commit f2168ad

Please sign in to comment.