fixup login and registration

This commit is contained in:
2025-04-25 21:33:25 +02:00
parent 9e02317ab1
commit d6b9159f1a
13 changed files with 223 additions and 167 deletions

View File

@@ -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",

View File

@@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover" class="bg-base-200">
<div style="display: contents">%sveltekit.body%</div>
<body data-sveltekit-preload-data="hover" style="width: 100vw; height: 100vh" class="bg-base-200">
<div style="display: contents;width: 100vw; height: 100vh" class="bg-base-200">%sveltekit.body%</div>
</body>
</html>

View File

@@ -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);
}

View File

@@ -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<paths>({
baseUrl: DB_ADAPTER || "http://localhost:5237", headers: {
"CF-Access-Client-Secret": CF_ACCESS_CLIENT_SECRET || "",
"CF-Access-Client-Id": CF_ACCESS_CLIENT_ID || "",
}
});
baseUrl: DB_ADAPTER || 'http://localhost:5237',
headers: {
'CF-Access-Client-Secret': CF_ACCESS_CLIENT_SECRET || '',
'CF-Access-Client-Id': CF_ACCESS_CLIENT_ID || ''
}
});

View File

@@ -0,0 +1,8 @@
<script lang="ts">
import { logout } from '$lib/paraglide/messages.js';
export let show = false;
</script>
{#if show}
<a class="btn btn-error btn-xs" href="/logout">{logout()}</a>
{/if}

View File

@@ -11,14 +11,13 @@ export async function validateSessionToken(
token: string,
sessions: KVNamespace
): Promise<SessionValidationResult> {
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<Session> {
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;
}

View File

@@ -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';
</script>
<ParaglideJS {i18n}>
{@render children()}
<div class="absolute top-2 right-2">
<div class="flex flex-row absolute top-2 right-2">
<ThemeButton />
<Logout show={!page.url.pathname.includes("login")}/>
</div>
</ParaglideJS>

View File

@@ -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');
}

View File

@@ -11,7 +11,24 @@
} from '@xyflow/svelte';
import type { Node, Edge, NodeTypes, NodeProps } from '@xyflow/svelte';
let data: PageData = $props();
let nodes = $state.raw<Node[]>([]);
let nodes = $state.raw<Node[]>([
{
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<Edge[]>([]);
</script>
@@ -19,11 +36,11 @@
<title>{title({ page: family_tree() })}</title>
</svelte:head>
<div style="height:100vh;">
<div style="height:100vh;" class="flex flex-col bg-base-200">
<SvelteFlowProvider>
<SvelteFlow bind:nodes bind:edges class="bg-base-100" fitView onlyRenderVisibleElements>
<Controls class="bg-base-300 text-base-content" />
<MiniMap class="bg-base-200" />
<SvelteFlow bind:nodes bind:edges class="bg-base-200" fitView onlyRenderVisibleElements>
<MiniMap />
<Controls />
</SvelteFlow>
</SvelteFlowProvider>
</div>

View File

@@ -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, '/');
}

View File

@@ -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();
}
});
</script>
@@ -35,8 +47,8 @@
<div class="hero bg-base-200 min-h-screen">
<div class="hero-content flex-col lg:flex-row-reverse">
<div class="text-center lg:text-left">
<figure class="top-margin-10 px-10 pt-10">
<div class="max-w-xxl flex flex-col items-center justify-center text-center">
<figure class="top-margin-10 max-w-sm px-10 pt-10">
<img src={FamilyTree} alt={family_tree()} class="rounded-xl" />
</figure>
<h1 class="text-5xl font-bold">{welcome()}</h1>
@@ -46,7 +58,7 @@
</div>
<div class="card bg-base-100 w-full max-w-sm shrink-0 shadow-2xl">
<div class="card-body">
<form method="POST" action="/register">
<form method="POST" action="?/register" use:enhance>
<fieldset class="fieldset">
{#if form?.message}
<div role="alert" class="alert alert-error">
@@ -66,10 +78,17 @@
<span>{form.message}</span>
</div>
{/if}
<label class="fieldset-label" for="email">Email</label>
<input type="email" class="input" placeholder="Email" value={data.props?.email} />
<label class="fieldset-label" for="email">{email()}</label>
<input
type="email"
name="email"
class="input"
placeholder={email()}
value={data.props?.email}
/>
<input
type="text"
name="google_id"
class="hidden"
id="google_id"
placeholder="Google ID"
@@ -79,6 +98,7 @@
<input
type="text"
class="input"
name="first_name"
id="first_name"
placeholder={first_name()}
value={data.props?.first_name}
@@ -87,6 +107,7 @@
<input
type="text"
class="input"
name="last_name"
id="last_name"
placeholder={last_name()}
value={data.props?.last_name}
@@ -96,13 +117,15 @@
type="text"
class="input pika-single"
id="birth_date"
bind:this={birth_date}
value={born()}
bind:this={birth_date}
/>
<input type="text" class="hidden" name="birth_date" bind:this={birth_date_value} />
<label class="fieldset-label" for="mothers_last_name">{mothers_last_name()}</label>
<input
type="text"
class="input"
name="mothers_last_name"
id="mothers_last_name"
placeholder={mothers_last_name()}
/>
@@ -110,6 +133,7 @@
<input
type="text"
class="input"
name="mothers_first_name"
id="mothers_first_name"
placeholder={mothers_first_name()}
/>

View File

@@ -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');
}

View File

@@ -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" }]
}
}