From 5b72ab28dd512c36f0e278c5d2742b07b49ef53d Mon Sep 17 00:00:00 2001 From: Yuri Tatishchev Date: Fri, 11 Apr 2025 18:43:51 -0700 Subject: [PATCH] WIP: temp --- README.md | 9 +- .../auth/google/callback/+page.server.ts | 108 ++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/routes/auth/google/callback/+page.server.ts diff --git a/README.md b/README.md index 37e7ff1..c8f1cd6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ and [wg-quick](https://www.wireguard.com/quickstart/) for standalone setups. ## Development Development uses bun. -An additional prepare step is needed to set up typia for type validation. +An additional prepare step is needed to set up typia for type validation. For example .env settings, see [.env.example](.env.example) @@ -27,3 +27,10 @@ bun install bun run prepare bun run dev ``` + +## To Do + +- [ ] Proper invite page +- [ ] Proper error page for login without invite +- [ ] Support file provider (for wg-quick) +- [ ] wg-quick scripts (maybe?) diff --git a/src/routes/auth/google/callback/+page.server.ts b/src/routes/auth/google/callback/+page.server.ts new file mode 100644 index 0000000..57a8fed --- /dev/null +++ b/src/routes/auth/google/callback/+page.server.ts @@ -0,0 +1,108 @@ +import { error } from '@sveltejs/kit'; +import * as arctic from 'arctic'; +import { google } from '$lib/server/oauth'; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; +import * as table from '$lib/server/db/schema'; +import { createSession, isValidInviteToken, setSessionTokenCookie } from '$lib/server/auth'; +import type { OAuth2Tokens } from 'arctic'; +import { assertGuard } from 'typia'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ url, cookies }) => { + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const storedState = cookies.get('google_oauth_state') ?? null; + const codeVerifier = cookies.get('google_code_verifier') ?? null; + + if (code === null || state === null || storedState === null || codeVerifier === null) { + error(400, 'Missing url parameters'); + return; + } + + const stateGeneratedToken = state.slice(0, storedState.length); + const stateInviteToken = state.slice(storedState.length); + if (stateGeneratedToken !== storedState) { + return new Response(null, { + status: 400, + }); + } + + let tokens: OAuth2Tokens; + try { + tokens = await google.validateAuthorizationCode(code, codeVerifier); + } catch (e) { + if (e instanceof arctic.OAuth2RequestError) { + console.debug('Arctic: OAuth: invalid authorization code, credentials, or redirect URI', e); + return new Response(null, { + status: 400, + }); + } + if (e instanceof arctic.ArcticFetchError) { + console.debug('Arctic: failed to call `fetch()`', e); + return new Response(null, { + status: 400, + }); + } + + return new Response(null, { + status: 500, + }); + } + + const idToken = tokens.idToken(); + const claims = arctic.decodeIdToken(idToken); + + console.log('claims', claims); + + assertGuard<{ + sub: string; + email: string; + name: string; + }>(claims); + + const userId = claims.sub; + const existingUser = await db.query.users.findFirst({ where: eq(table.users.id, userId) }); + + if (existingUser) { + const session = await createSession(existingUser.id); + setSessionTokenCookie(event, session.id, session.expiresAt); + return new Response(null, { + status: 302, + headers: { + Location: '/', + }, + }); + } + + if (!isValidInviteToken(stateInviteToken)) { + const message = + stateInviteToken.length === 0 ? 'sign up with an invite link first' : 'invalid invite link'; + + return new Response('Not Authorized: ' + message, { + status: 403, + }); + } + + const user: table.User = { + id: userId, + authSource: 'google', + username: claims.email, + name: claims.name, + }; + + // TODO: proper error handling, delete cookies + await db.insert(table.users).values(user); + console.log('created user', user, 'with invite token', stateInviteToken); + + const session = await createSession(user.id); + + setSessionTokenCookie(event, session.id, session.expiresAt); + + return new Response(null, { + status: 302, + headers: { + Location: '/', + }, + }); +};