From d6b9159f1a66921d8e2b8d7b21b46bb5e9079291 Mon Sep 17 00:00:00 2001 From: Vargha Csongor Date: Fri, 25 Apr 2025 21:33:25 +0200 Subject: [PATCH] fixup login and registration --- apps/app/package.json | 2 +- apps/app/src/app.html | 4 +- apps/app/src/hooks.server.ts | 2 + apps/app/src/lib/api/client.ts | 15 +- apps/app/src/lib/logout.svelte | 8 + apps/app/src/lib/server/session.ts | 25 ++- apps/app/src/routes/+layout.svelte | 5 +- apps/app/src/routes/+page.server.ts | 42 ++--- apps/app/src/routes/+page.svelte | 27 ++- .../login/google/callback/+page.server.ts | 166 +++++++++--------- .../routes/login/google/callback/+page.svelte | 50 ++++-- apps/app/src/routes/logout/+server.ts | 20 +++ apps/app/wrangler.jsonc | 24 +-- 13 files changed, 223 insertions(+), 167 deletions(-) create mode 100644 apps/app/src/lib/logout.svelte create mode 100644 apps/app/src/routes/logout/+server.ts diff --git a/apps/app/package.json b/apps/app/package.json index 89d2641..476b7bd 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite dev", "build": "vite build", - "preview": "npm run build && wrangler pages dev", + "preview": "npm run build && wrangler pages dev --port 5173", "prepare": "svelte-kit sync || echo ''", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", diff --git a/apps/app/src/app.html b/apps/app/src/app.html index 5b4a34b..4761450 100644 --- a/apps/app/src/app.html +++ b/apps/app/src/app.html @@ -6,7 +6,7 @@ %sveltekit.head% - -
%sveltekit.body%
+ +
%sveltekit.body%
diff --git a/apps/app/src/hooks.server.ts b/apps/app/src/hooks.server.ts index 05d3bfb..da8b91b 100644 --- a/apps/app/src/hooks.server.ts +++ b/apps/app/src/hooks.server.ts @@ -27,6 +27,8 @@ const authHandle: Handle = async ({ event, resolve }) => { if (session !== null) { setSessionTokenCookie(event, token, session.expiresAt); } else { + console.log('Session token is invalid'); + console.log(session, token); deleteSessionTokenCookie(event); } diff --git a/apps/app/src/lib/api/client.ts b/apps/app/src/lib/api/client.ts index b900090..94b265b 100644 --- a/apps/app/src/lib/api/client.ts +++ b/apps/app/src/lib/api/client.ts @@ -1,10 +1,11 @@ -import createClient from "openapi-fetch"; -import type { paths } from "$lib/api/api.gen"; // generated by openapi-typescript +import createClient from 'openapi-fetch'; +import type { paths } from '$lib/api/api.gen'; // generated by openapi-typescript import { DB_ADAPTER, CF_ACCESS_CLIENT_ID, CF_ACCESS_CLIENT_SECRET } from '$env/static/private'; export const client = createClient({ - baseUrl: DB_ADAPTER || "http://localhost:5237", headers: { - "CF-Access-Client-Secret": CF_ACCESS_CLIENT_SECRET || "", - "CF-Access-Client-Id": CF_ACCESS_CLIENT_ID || "", - } -}); \ No newline at end of file + baseUrl: DB_ADAPTER || 'http://localhost:5237', + headers: { + 'CF-Access-Client-Secret': CF_ACCESS_CLIENT_SECRET || '', + 'CF-Access-Client-Id': CF_ACCESS_CLIENT_ID || '' + } +}); diff --git a/apps/app/src/lib/logout.svelte b/apps/app/src/lib/logout.svelte new file mode 100644 index 0000000..b292abd --- /dev/null +++ b/apps/app/src/lib/logout.svelte @@ -0,0 +1,8 @@ + + +{#if show} + {logout()} +{/if} \ No newline at end of file diff --git a/apps/app/src/lib/server/session.ts b/apps/app/src/lib/server/session.ts index 333757e..1650fad 100644 --- a/apps/app/src/lib/server/session.ts +++ b/apps/app/src/lib/server/session.ts @@ -11,14 +11,13 @@ export async function validateSessionToken( token: string, sessions: KVNamespace ): Promise { - const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const session: Session | null = await sessions.get(sessionId, { type: 'json' }); + const session: Session | null = await sessions.get(token, { type: 'json' }); if (!session) { return null; } - if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { - await sessions.put(sessionId, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL }); + if (Date.now() >= session.expiresAt - 1000 * 60 * 60 * 24 * 15) { + await sessions.put(token, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL }); } return session; @@ -35,13 +34,13 @@ export async function invalidateUserSessions(userId: number, sessions: KVNamespa } } -export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date): void { +export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: EpochTimeStamp): void { event.cookies.set('session', token, { httpOnly: true, path: '/', secure: import.meta.env.PROD, sameSite: 'lax', - expires: expiresAt + expires: new Date(expiresAt) }); } @@ -55,11 +54,11 @@ export function deleteSessionTokenCookie(event: RequestEvent): void { }); } -export function generateSessionToken(): string { +export function generateSessionToken(userId: string): string { const tokenBytes = new Uint8Array(20); crypto.getRandomValues(tokenBytes); const token = encodeBase32(tokenBytes).toLowerCase(); - return token; + return `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`; } export async function createSession( @@ -67,19 +66,19 @@ export async function createSession( userId: number, sessions: KVNamespace ): Promise { - const sessionId = `${userId}:${encodeHexLowerCase(sha256(new TextEncoder().encode(token)))}`; const session: Session = { - id: sessionId, + id: token, userId, - expiresAt: new Date(Date.now() + 1000 * EXPIRATION_TTL) + expiresAt: Date.now() + 1000 * EXPIRATION_TTL }; - await sessions.put(sessionId, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL }); + await sessions.put(token, JSON.stringify(session), { expirationTtl: EXPIRATION_TTL }); + return session; } export interface Session { id: string; - expiresAt: Date; + expiresAt: EpochTimeStamp; userId: number; } diff --git a/apps/app/src/routes/+layout.svelte b/apps/app/src/routes/+layout.svelte index cc57316..34395f7 100644 --- a/apps/app/src/routes/+layout.svelte +++ b/apps/app/src/routes/+layout.svelte @@ -4,11 +4,14 @@ import { ParaglideJS } from '@inlang/paraglide-sveltekit'; let { children } = $props(); import ThemeButton from '$lib/theme-select.svelte'; + import Logout from '$lib/logout.svelte'; + import { page } from '$app/state'; {@render children()} -
+
+
diff --git a/apps/app/src/routes/+page.server.ts b/apps/app/src/routes/+page.server.ts index 2ff99c3..01918da 100644 --- a/apps/app/src/routes/+page.server.ts +++ b/apps/app/src/routes/+page.server.ts @@ -1,7 +1,6 @@ import { fail, redirect } from '@sveltejs/kit'; -import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/session'; import { client } from '$lib/api/client'; -import type { Actions, RequestEvent } from './$types'; +import type { RequestEvent } from './$types'; import { browser } from '$app/environment'; export async function load(event: RequestEvent) { @@ -14,36 +13,19 @@ export async function load(event: RequestEvent) { return {}; } - client.GET('/family-tree-with-spouses', { - params: { - header: { "X-User-ID": event.locals.session }, - } - }).then((response) => { + const response = await client + .GET('/family-tree-with-spouses', { + params: { + header: { 'X-User-ID': event.locals.session.userId }, + } + }) - if (response.response.status === 200) { - return response.data; - } else { - return fail(response.response.status, { message: response.error?.msg || 'An error occurred' }); - } - }); -} - -export const actions: Actions = { - logout: logout -}; - -async function logout(event: RequestEvent) { - if (event.locals.session === null) { - return fail(401); - } - - if (event.platform && event.platform.env && event.platform.env.GH_SESSIONS) { - invalidateSession(event.locals.session.id, event.platform.env.GH_SESSIONS); + if (response.response.status === 200) { + return response.data; } else { - return fail(500, { message: 'Server configuration error' }); + return fail(response.response.status, { + message: response.error?.msg || 'An error occurred' + }); } - deleteSessionTokenCookie(event); - - return redirect(302, '/login'); } diff --git a/apps/app/src/routes/+page.svelte b/apps/app/src/routes/+page.svelte index f67aaf2..f385168 100644 --- a/apps/app/src/routes/+page.svelte +++ b/apps/app/src/routes/+page.svelte @@ -11,7 +11,24 @@ } from '@xyflow/svelte'; import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte'; let data: PageData = $props(); - let nodes = $state.raw([]); + let nodes = $state.raw([ + { + id: '1', + type: 'input', + data: { label: 'Input Node' }, + position: { x: 0, y: 0 } + }, + { + id: '2', + data: { label: 'Default Node' }, + position: { x: 100, y: 100 } + }, + { + id: '3', + data: { label: 'Output Node' }, + position: { x: 200, y: 200 } + } + ]); let edges = $state.raw([]); @@ -19,11 +36,11 @@ {title({ page: family_tree() })} -
+
- - - + + +
diff --git a/apps/app/src/routes/login/google/callback/+page.server.ts b/apps/app/src/routes/login/google/callback/+page.server.ts index 50353cf..52c5781 100644 --- a/apps/app/src/routes/login/google/callback/+page.server.ts +++ b/apps/app/src/routes/login/google/callback/+page.server.ts @@ -15,7 +15,7 @@ import { failed_to_create_user } from '$lib/paraglide/messages'; -import type { PageServerLoad, Actions, RequestEvent, PageData } from './$types'; +import type { PageServerLoad, Actions, RequestEvent } from './$types'; import type { OAuth2Tokens } from 'arctic'; import type { PersonProperties } from '$lib/model'; import { error, redirect, fail } from '@sveltejs/kit'; @@ -55,44 +55,41 @@ export const load: PageServerLoad = async (event: RequestEvent) => { const first_name = claimsParser.getString('given_name'); const email = claimsParser.getString('email'); - client.GET('/person/google/{google_id}', - { - params: { - path: { google_id: sub }, - }, - } - ).then((response) => { - if (response.response.status !== 200) { - return error(500, { - message: "Failed to get user from Google ID: " + response.error?.msg - }); + const response = await client.GET('/person/google/{google_id}', { + params: { + path: { google_id: sub } } + }); + if (response.response.status === 200) { if (response.data?.Id) { if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) { - return error(500, { message: "Server configuration error. GH_SESSIONS KeyValue store missing" }); + return error(500, { + message: 'Server configuration error. GH_SESSIONS KeyValue store missing' + }); } - const sessionToken = generateSessionToken(); - createSession(sessionToken, response.data.Id, event.platform.env.GH_SESSIONS).then((session) => { - if (session === null) { - return error(500, { - message: 'Failed to create session' - }); - } + const sessionToken = generateSessionToken(String(response.data.Id)); + const session = await createSession(sessionToken, response.data.Id, event.platform.env.GH_SESSIONS) + if (session === null) { + return error(500, { + message: 'Failed to create session' + }); + } - setSessionTokenCookie(event, sessionToken, session.expiresAt); + setSessionTokenCookie(event, sessionToken, session.expiresAt); - return redirect(302, '/'); - }); + return redirect(302, '/'); } - }) + } + + let personP: PersonProperties = { google_id: sub, first_name: first_name, last_name: family_name, - email: email, + email: email }; return { @@ -105,37 +102,44 @@ export const actions: Actions = { }; async function register(event: RequestEvent) { - const data = await event.request.formData(); - if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) { - return fail(500, { message: "Server configuration error. GH_SESSIONS KeyValue store missing" }); + if (browser) { + return {}; } - const google_id = data.get('google_id'); - if (google_id === null) { - return fail(400, { - message: missing_field({ - field: 'google_id' - }) - }); + const data = await event.request.formData(); + if (!event.platform || !event.platform.env || !event.platform.env.GH_SESSIONS) { + return fail(500, { message: 'Server configuration error. GH_SESSIONS KeyValue store missing' }); } + const first_name_f = data.get('first_name'); - if (first_name_f === null) { + if (first_name_f === null || first_name_f === '') { return fail(400, { message: missing_field({ field: first_name() }) }); } + + const google_id = data.get('google_id'); + if (google_id === null || google_id === '') { + return fail(400, { + message: missing_field({ + field: 'google_id' + }) + }); + } + const last_name_f = data.get('last_name'); - if (last_name_f === null) { + if (last_name_f === null || last_name_f === '') { return fail(400, { message: missing_field({ field: last_name() }) }); } + const email = data.get('email'); - if (email === null) { + if (email === null || email === '') { return fail(400, { message: missing_field({ field: 'Email' @@ -143,7 +147,7 @@ async function register(event: RequestEvent) { }); } const birth_date = data.get('birth_date'); - if (birth_date === null) { + if (birth_date === null || birth_date === '') { return fail(400, { message: missing_field({ field: born() @@ -151,7 +155,7 @@ async function register(event: RequestEvent) { }); } const mothers_first_name_f = data.get('mothers_first_name'); - if (mothers_first_name_f === null) { + if (mothers_first_name_f === null || mothers_first_name_f === '') { return fail(400, { message: missing_field({ field: mothers_first_name() @@ -168,64 +172,56 @@ async function register(event: RequestEvent) { } const parsed_date = new Date(birth_date as string); - let personP: components['schemas']['PersonRegistration'] = { first_name: first_name_f as string, last_name: last_name_f as string, email: email as string, - born: parsed_date.toISOString(), + born: parsed_date.toISOString().split('T')[0], mothers_first_name: mothers_first_name_f as string, mothers_last_name: mothers_last_name_f as string, - limit: StorageLimit, + limit: StorageLimit }; - client.POST('/person/google/{google_id}', - { + let response = await client + .POST('/person/google/{google_id}', { params: { - path: { google_id: google_id.toString() }, + path: { google_id: google_id.toString() } }, body: personP - } - ).then((response) => { - if (response.response.status !== 200) { - return fail(400, { - message: failed_to_create_user({ - error: response.error?.msg - }) - }); - } + }) - const sessionToken = generateSessionToken(); - if (!response.data?.Id) { - return fail(400, { - message: failed_to_create_user({ - error: 'No user ID returned' - }) - }); - } - - if (!event.platform) { - return fail(500, { - message: 'Server configuration error. GH_SESSIONS KeyValue store missing' - }); - } - - const session = createSession(sessionToken, response.data.Id, event.platform.env.GH_SESSIONS).then((session) => { - if (session === null) { - return fail(500, { - message: 'Failed to create session' - }); - } - - setSessionTokenCookie(event, sessionToken, session.expiresAt); - - return redirect(302, '/'); + if (response.response.status !== 200) { + return fail(400, { + message: failed_to_create_user() + response.error?.msg }); - }).catch((error) => { + } + + const sessionToken = generateSessionToken(); + if (!response.data?.Id) { + console.log(response.data) + return fail(400, { + message: failed_to_create_user() + 'No user ID returned' + }); + } + + if (!event.platform) { return fail(500, { - message: failed_to_create_user({ - error: error.message - }) + message: 'Server configuration error. GH_SESSIONS KeyValue store missing' }); - }) + } + + const session = await createSession( + sessionToken, + response.data.Id, + event.platform.env.GH_SESSIONS + ) + if (session === null) { + return fail(500, { + message: failed_to_create_user() + 'Failed to create session' + }); + } + + setSessionTokenCookie(event, sessionToken, session.expiresAt); + + return redirect(302, '/'); } diff --git a/apps/app/src/routes/login/google/callback/+page.svelte b/apps/app/src/routes/login/google/callback/+page.svelte index 22fe9c9..111b24e 100644 --- a/apps/app/src/routes/login/google/callback/+page.svelte +++ b/apps/app/src/routes/login/google/callback/+page.svelte @@ -11,20 +11,32 @@ mothers_last_name, last_name, first_name, - email, - allow_family_tree_admin_access + email } from '$lib/paraglide/messages'; + import { onMount } from 'svelte'; + import { enhance } from '$app/forms'; import FamilyTree from '../../highresolution_icon_no_background_croped.png'; let { data, form }: PageProps = $props(); - import Pikaday from 'pikaday'; let birth_date: HTMLInputElement; - $effect(() => { + let birth_date_value: HTMLInputElement; + onMount(() => { if (birth_date) { - const picker = new Pikaday({ - field: birth_date + import('pikaday').then(({ default: Pikaday }) => { + const picker = new Pikaday({ + format: 'YYYY-MM-DD', + minDate: new Date(1900, 0, 1), + field: birth_date, + onOpen: function () { + birth_date_value.placeholder = ''; + }, + onSelect: function (date) { + birth_date_value.value = date.toISOString(); + } + }); + // Clean up when component unmounts + return () => picker.destroy(); }); - return () => picker.destroy(); } }); @@ -35,8 +47,8 @@
-
-
+
+
{family_tree()}

{welcome()}

@@ -46,7 +58,7 @@
-
+
{#if form?.message} {/if} - - + + + @@ -110,6 +133,7 @@ diff --git a/apps/app/src/routes/logout/+server.ts b/apps/app/src/routes/logout/+server.ts new file mode 100644 index 0000000..af72b57 --- /dev/null +++ b/apps/app/src/routes/logout/+server.ts @@ -0,0 +1,20 @@ +import { error, redirect } from '@sveltejs/kit'; +import { invalidateSession, deleteSessionTokenCookie } from '$lib/server/session'; + +import type { RequestEvent } from './$types'; + +export function GET(event: RequestEvent): Response { + if (event.locals.session === null) { + return error(401, { message: 'Unauthorized' }); + } + + if (event.platform && event.platform.env && event.platform.env.GH_SESSIONS) { + invalidateSession(event.locals.session.id, event.platform.env.GH_SESSIONS); + } else { + return error(500, { message: 'Server configuration error' }); + } + + deleteSessionTokenCookie(event); + + return redirect(302, '/login'); +} \ No newline at end of file diff --git a/apps/app/wrangler.jsonc b/apps/app/wrangler.jsonc index 4d9b5b1..fabbc01 100644 --- a/apps/app/wrangler.jsonc +++ b/apps/app/wrangler.jsonc @@ -5,7 +5,9 @@ { "$schema": "node_modules/wrangler/config-schema.json", "name": "generations-heritage", - "compatibility_flags": ["nodejs_compat"], + "compatibility_flags": [ + "nodejs_compat" + ], "compatibility_date": "2025-02-14", "pages_build_output_dir": ".svelte-kit/cloudflare", "observability": { @@ -26,11 +28,13 @@ */ "kv_namespaces": [ { + "id": "6f793c8813ab46549234572f4c6ae5a1", "binding": "GH_SESSIONS" } ], "r2_buckets": [ { + "bucket_name": "ghstaging", "binding": "GH_MEDIA" } ], @@ -49,14 +53,14 @@ }, "kv_namespaces": [ { - "binding": "GH_SESSIONS", - "id": "6f793c8813ab46549234572f4c6ae5a1" + "id": "6f793c8813ab46549234572f4c6ae5a1", + "binding": "GH_SESSIONS" } ], "r2_buckets": [ { - "binding": "GH_MEDIA", - "bucket_name": "ghstaging" + "bucket_name": "ghstaging", + "binding": "GH_MEDIA" } ] }, @@ -69,14 +73,14 @@ }, "kv_namespaces": [ { - "binding": "GH_SESSIONS", - "id": "4cedee65c36d49d7afc654bcc798d169" + "id": "4cedee65c36d49d7afc654bcc798d169", + "binding": "GH_SESSIONS" } ], "r2_buckets": [ { - "binding": "GH_MEDIA", - "bucket_name": "generations-heritage" + "bucket_name": "generations-heritage", + "binding": "GH_MEDIA" } ] } @@ -95,4 +99,4 @@ * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings */ // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] -} +} \ No newline at end of file