From 3341ea2bf6240d7ecc08c9138f5dd79cf99732cd Mon Sep 17 00:00:00 2001 From: Chad Freeman Date: Sun, 18 Aug 2024 16:52:27 -0400 Subject: [PATCH] auth done & oauth almost done --- .github/workflows/deploy.yml | 2 +- .gitignore | 2 + src/app.d.ts | 2 +- src/hooks.server.ts | 2 +- src/lib/apiclient.ts | 4 +- src/lib/hono.ts | 32 ----- src/lib/oauth.ts | 7 - src/lib/{ => server}/auth.ts | 4 +- src/lib/{ => server}/db.ts | 2 +- src/lib/server/hono.ts | 58 ++++++++ src/lib/server/oauth.ts | 131 ++++++++++++++++++ src/routes/api/[...path]/+server.ts | 2 +- src/routes/app/+layout.server.ts | 2 +- src/routes/app/+layout.svelte | 10 +- src/routes/app/+page.svelte | 28 +++- .../app/{page2 => :[roomID]}/+page.svelte | 4 +- src/routes/app/:[roomID]/+page.ts | 3 + src/routes/auth/+page.server.ts | 4 +- src/routes/auth/signout/+server.ts | 2 +- src/routes/oauth/[provider]/+server.ts | 3 + .../oauth/[provider]/callback/+server.ts | 0 svelte.config.js | 8 +- vite.config.ts | 10 +- 23 files changed, 262 insertions(+), 60 deletions(-) delete mode 100644 src/lib/hono.ts delete mode 100644 src/lib/oauth.ts rename src/lib/{ => server}/auth.ts (91%) rename src/lib/{ => server}/db.ts (95%) create mode 100644 src/lib/server/hono.ts create mode 100644 src/lib/server/oauth.ts rename src/routes/app/{page2 => :[roomID]}/+page.svelte (61%) create mode 100644 src/routes/app/:[roomID]/+page.ts create mode 100644 src/routes/oauth/[provider]/+server.ts create mode 100644 src/routes/oauth/[provider]/callback/+server.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b08f541..bba427d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,7 +31,7 @@ jobs: run: 'bun install' - name: Build step - run: 'bun build' + run: 'bun run build' - name: Upload to Deno Deploy uses: denoland/deployctl@v1 diff --git a/.gitignore b/.gitignore index 79518f7..3b75e42 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +dev \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts index 82505ae..6b08e8e 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -2,7 +2,7 @@ // for information about these interfaces import type { FlatDocumentData } from "@olli/kvdex" -import { user, session } from "$lib/db" +import { user, session } from "$lib/server/db" import z from "zod" declare global { namespace App { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index fd4e407..9841ca8 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,4 +1,4 @@ -import { cookieController, cookieExpiration, getUserAndSession } from "$lib/auth"; +import { cookieController, cookieExpiration, getUserAndSession } from "$lib/server/auth"; import type { Handle } from "@sveltejs/kit"; import { createDate } from "oslo"; diff --git a/src/lib/apiclient.ts b/src/lib/apiclient.ts index c596f7a..18748e9 100644 --- a/src/lib/apiclient.ts +++ b/src/lib/apiclient.ts @@ -1,6 +1,6 @@ -import { type api } from "./hono" +import type { api as API } from "./server/hono" import { hc } from "hono/client" -export const client = hc('/api', { +export const api = hc('/api', { async fetch(input: RequestInfo | URL, init?: RequestInit): Promise { return await fetch(input, { ...init, diff --git a/src/lib/hono.ts b/src/lib/hono.ts deleted file mode 100644 index 7236bd3..0000000 --- a/src/lib/hono.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Hono } from "hono" -import { zValidator } from "@hono/zod-validator" -import z from "zod" -import { HTTPException } from 'hono/http-exception' -import { db } from "./db" -import { alphabet, generateRandomString } from "oslo/crypto" -import { text } from "@sveltejs/kit" - -type Bindings = { - locals: App.Locals -} - -const api = new Hono<{ Bindings: Bindings }>() - .use(async (ctx, next) => { - if(!ctx.env.locals.session) throw new HTTPException(401) - await next() - }) - .post('/rooms/create', zValidator('json', z.object({ - name: z.string() - })), async ({ req, env: { locals: { user } } }) => { - const body = req.valid('json') - const roomId = generateRandomString(10, alphabet('0-9', 'a-z')) - await db.chat.data.set(roomId, { - createdAt: new Date(), - creator: user?.id!, - name: body.name - }) - return text(roomId) - }) -export type api = typeof api - -export const hono = new Hono<{ Bindings: Bindings }>().route('/api', api) \ No newline at end of file diff --git a/src/lib/oauth.ts b/src/lib/oauth.ts deleted file mode 100644 index 671f76e..0000000 --- a/src/lib/oauth.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Google, Discord, GitHub } from "arctic" - -//TODO: oauth - -export const google = new Google(); -export const discord = new Discord(); -export const github = new GitHub(); \ No newline at end of file diff --git a/src/lib/auth.ts b/src/lib/server/auth.ts similarity index 91% rename from src/lib/auth.ts rename to src/lib/server/auth.ts index df72e98..074566a 100644 --- a/src/lib/auth.ts +++ b/src/lib/server/auth.ts @@ -36,8 +36,8 @@ async function getUserAndSession( sessionId: string ): Promise< Option<{ - user: FlatDocumentData, string> - session: FlatDocumentData, string> + user: FlatDocumentData, string> + session: FlatDocumentData, string> }> > { const session = (await db.session.find(sessionId))?.flat() diff --git a/src/lib/db.ts b/src/lib/server/db.ts similarity index 95% rename from src/lib/db.ts rename to src/lib/server/db.ts index 0419e37..906790e 100644 --- a/src/lib/db.ts +++ b/src/lib/server/db.ts @@ -25,7 +25,7 @@ export const chat = z.object({ createdAt: z.date() }) -export const kv = await openKv() +export const kv = await openKv('http://0.0.0.0:4512') export const db = kvdex(kv, { user: collection(user, { idGenerator: ({ id }) => id, diff --git a/src/lib/server/hono.ts b/src/lib/server/hono.ts new file mode 100644 index 0000000..5776f08 --- /dev/null +++ b/src/lib/server/hono.ts @@ -0,0 +1,58 @@ +import { Hono } from 'hono' +import { zValidator } from '@hono/zod-validator' +import z from 'zod' +import { HTTPException } from 'hono/http-exception' +import { db } from './db' +import { alphabet, generateRandomString } from 'oslo/crypto' +import { text } from '@sveltejs/kit' +import { streamSSE } from 'hono/streaming' + +type Bindings = { + locals: App.Locals +} + +const api = new Hono<{ Bindings: Bindings }>() + .use(async (ctx, next) => { + if (!ctx.env.locals.session) throw new HTTPException(401) + await next() + }) + .post( + '/rooms/create', + zValidator( + 'json', + z.object({ + name: z.string() + }) + ), + async ({ + req, + env: { + locals: { user } + } + }) => { + const body = req.valid('json') + const roomId = generateRandomString(10, alphabet('0-9', 'a-z')) + await db.chat.data.set(roomId, { + createdAt: new Date(), + creator: user?.id!, + name: body.name + }) + return text(roomId) + } + ) + .get('/rooms/connect/:id', (c) => { + return streamSSE(c, async (stream) => { + while (true) { + const message = `It is ${new Date().toISOString()}` + await stream.writeSSE({ + data: message, + event: 'time-update', + }) + await stream.sleep(1000) + } + }) + }) + +export type api = typeof api + +export const hono = new Hono<{ Bindings: Bindings }>().route('/api', api) diff --git a/src/lib/server/oauth.ts b/src/lib/server/oauth.ts new file mode 100644 index 0000000..b9e2ae5 --- /dev/null +++ b/src/lib/server/oauth.ts @@ -0,0 +1,131 @@ +import { Google, Discord, GitHub, type OAuth2Provider, type OAuth2ProviderWithPKCE, generateState, generateCodeVerifier } from "arctic" +import { error, redirect, type Actions, type RequestHandler, type ServerLoad } from "@sveltejs/kit"; +import type { z } from "zod"; +import type { publicUser } from "./db"; + +//TODO: oauth + +export const google = new Google(); +export const discord = new Discord(); +export const github = new GitHub(); + +export function oauth_handler(): RequestHandler<{ provider: string }> { + return async ({ cookies, params: { provider: providerID }, url }) => { + let provider: Google | Discord | GitHub; + let scopes: string[] + let noop = false; + switch(providerID) { + case "discord": { + provider = discord + scopes = ['identify'] + break + } + case "google": { + provider = google + scopes = ['profile'] + break + } + case "github": { + provider = github + scopes = [] + break + } + default: { + noop = true + break + } + } + // @ts-expect-error: I know im using it before assignment that's the point + if(noop || !provider || !scopes) error(404, "provider not found") + let redir: URL; + const state = generateState(); + let codeVerifier: string; + if(provider instanceof Google) { + codeVerifier = generateCodeVerifier() + redir = await provider.createAuthorizationURL(state, codeVerifier, { + scopes + }) + } else { + redir = await provider.createAuthorizationURL(state, { + scopes + }) + } + cookies.set("state", state, { + secure: true, + path: "/", + httpOnly: true, + maxAge: 60 * 10 + }); + // @ts-expect-error: I know im using it before assignment that's the point + if(codeVerifier) { + cookies.set("code_verifier", codeVerifier, { + secure: true, + path: "/", + httpOnly: true, + maxAge: 60 * 10 + }); + } + redirect(302, redir); + } +} + +export function oauth_callback(): ServerLoad<{ provider: string }, any, { type: "create" | "link", name: string, user: z.infer }> { + return async ({ cookies, params: { provider: providerID }, locals, url }) => { + let provider: Google | Discord | GitHub; + let scopes: string[] + let noop = false; + switch(providerID) { + case "discord": { + provider = discord + scopes = ['identify'] + break + } + case "google": { + provider = google + scopes = ['profile'] + break + } + case "github": { + provider = github + scopes = [] + break + } + default: { + noop = true + break + } + } + // @ts-expect-error: I know im using it before assignment that's the point + if(noop || !provider || !scopes) error(404, "provider not found") + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + const storedState = cookies.get("state"); + const storedCodeVerifier = cookies.get("code_verifier"); + if (!code || !storedState || state !== storedState || (provider instanceof Google && !storedCodeVerifier)) { + error(400, "Invalid request") + } + let tokens + if(provider instanceof Google) { + tokens = await provider.validateAuthorizationCode(code, storedCodeVerifier) + } else { + tokens = await provider.validateAuthorizationCode(code) + } + if(locals.user) { + // the user is already logged in, ask them if they want to link the account to their existing account, or log out and try again + return { + type: 'create', + + } + } else { + // the user is NOT logged in, log them in, if there is no account linked to that provided user, ask them if they want to create an account + + } + } +} + +export function callback_actions(): Actions<{ provider: string }> { + return { + + } +} \ No newline at end of file diff --git a/src/routes/api/[...path]/+server.ts b/src/routes/api/[...path]/+server.ts index ee70b08..8a3a252 100644 --- a/src/routes/api/[...path]/+server.ts +++ b/src/routes/api/[...path]/+server.ts @@ -1,4 +1,4 @@ -import { hono } from '$lib/hono.js'; +import { hono } from '$lib/server/hono.js'; export async function fallback({ request, locals }) { return hono.fetch(request, { locals }) diff --git a/src/routes/app/+layout.server.ts b/src/routes/app/+layout.server.ts index acd66fd..dd65cc0 100644 --- a/src/routes/app/+layout.server.ts +++ b/src/routes/app/+layout.server.ts @@ -1,4 +1,4 @@ -import { publicUser } from '$lib/db.js'; +import { publicUser } from '$lib/server/db.js'; import { redirect } from '@sveltejs/kit'; export async function load({ locals }) { diff --git a/src/routes/app/+layout.svelte b/src/routes/app/+layout.svelte index e11b9b2..d05b9aa 100644 --- a/src/routes/app/+layout.svelte +++ b/src/routes/app/+layout.svelte @@ -65,7 +65,13 @@ -
+
{@render children()}
-
\ No newline at end of file + + +
+
+ New message arrived. +
+
\ No newline at end of file diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index 0eb3d55..caf23ce 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,7 +1,29 @@ -
- -
\ No newline at end of file +
+
+ new room + + +
+
+
+ list +
\ No newline at end of file diff --git a/src/routes/app/page2/+page.svelte b/src/routes/app/:[roomID]/+page.svelte similarity index 61% rename from src/routes/app/page2/+page.svelte rename to src/routes/app/:[roomID]/+page.svelte index fd3a2fd..775080e 100644 --- a/src/routes/app/page2/+page.svelte +++ b/src/routes/app/:[roomID]/+page.svelte @@ -1,3 +1,5 @@ \ No newline at end of file + + +{data.roomID} \ No newline at end of file diff --git a/src/routes/app/:[roomID]/+page.ts b/src/routes/app/:[roomID]/+page.ts new file mode 100644 index 0000000..2328ef5 --- /dev/null +++ b/src/routes/app/:[roomID]/+page.ts @@ -0,0 +1,3 @@ +export async function load({ params: { roomID } }) { + return { roomID } +} \ No newline at end of file diff --git a/src/routes/auth/+page.server.ts b/src/routes/auth/+page.server.ts index 7a941a5..e072409 100644 --- a/src/routes/auth/+page.server.ts +++ b/src/routes/auth/+page.server.ts @@ -3,8 +3,8 @@ import { fail, message, setError, superValidate } from 'sveltekit-superforms'; import { zod } from 'sveltekit-superforms/adapters'; import { z } from 'zod'; import { hash, verify } from "@ts-rex/argon2" -import { db } from '$lib/db.js'; -import { cookieController, cookieExpiration, createSessionForUser } from '$lib/auth.js'; +import { db } from '$lib/server/db.js'; +import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth.js'; import { createDate } from 'oslo'; import { redirect } from '@sveltejs/kit'; diff --git a/src/routes/auth/signout/+server.ts b/src/routes/auth/signout/+server.ts index 1bf63dd..7d0223e 100644 --- a/src/routes/auth/signout/+server.ts +++ b/src/routes/auth/signout/+server.ts @@ -1,4 +1,4 @@ -import { cookieController, deleteSession } from '$lib/auth'; +import { cookieController, deleteSession } from '$lib/server/auth'; import { redirect } from '@sveltejs/kit'; export async function GET({ locals, cookies }) { diff --git a/src/routes/oauth/[provider]/+server.ts b/src/routes/oauth/[provider]/+server.ts new file mode 100644 index 0000000..306e1b5 --- /dev/null +++ b/src/routes/oauth/[provider]/+server.ts @@ -0,0 +1,3 @@ +import { oauth_handler } from "$lib/server/oauth"; + +export const GET = oauth_handler(); \ No newline at end of file diff --git a/src/routes/oauth/[provider]/callback/+server.ts b/src/routes/oauth/[provider]/callback/+server.ts new file mode 100644 index 0000000..e69de29 diff --git a/svelte.config.js b/svelte.config.js index cbe91cc..d6f38cf 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -15,7 +15,13 @@ const config = { // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // If your environment is not supported, or you settled on a specific environment, switch out the adapter. // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter() + adapter: adapter({ + buildOptions: { + loader: { + '.node': 'empty' + } + } + }) } }; diff --git a/vite.config.ts b/vite.config.ts index bbf8c7d..5cf7a7b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,5 +2,13 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [sveltekit()] + plugins: [sveltekit()], + optimizeDeps: { + esbuildOptions: { + loader: { + '.node': 'empty' + }, + target: ['deno1.45.5'] + } + } });