From 093b7a8135efcfe4f1fe29359e38204b801e24a5 Mon Sep 17 00:00:00 2001 From: Chad Freeman Date: Sun, 18 Aug 2024 19:36:08 -0400 Subject: [PATCH] almost done fixing up oauth --- .github/workflows/deploy.yml | 2 +- bun.lockb | Bin 136404 -> 136740 bytes package.json | 89 ++--- postcss.config.js | 8 +- src/app.d.ts | 12 +- src/app.pcss | 2 +- src/hooks.server.ts | 52 +-- src/lib/Tab.svelte | 39 ++- src/lib/apiclient.ts | 18 +- src/lib/server/auth.ts | 36 +- src/lib/server/db.ts | 73 ++-- src/lib/server/hono.ts | 24 +- src/lib/server/oauth.ts | 322 +++++++++++------- src/routes/+layout.svelte | 6 +- src/routes/+page.ts | 6 +- src/routes/api/[...path]/+server.ts | 6 +- src/routes/app/+layout.server.ts | 16 +- src/routes/app/+layout.svelte | 153 +++++---- src/routes/app/+page.svelte | 46 +-- src/routes/app/:[roomID]/+page.svelte | 4 +- src/routes/app/:[roomID]/+page.ts | 4 +- src/routes/auth/+page.server.ts | 137 ++++---- src/routes/auth/+page.svelte | 77 +++-- src/routes/auth/signout/+server.ts | 20 +- src/routes/oauth/[provider]/+server.ts | 4 +- .../oauth/[provider]/callback/+page.server.ts | 4 + .../oauth/[provider]/callback/+page.svelte | 8 + .../oauth/[provider]/callback/+server.ts | 0 svelte.config.js | 10 +- tailwind.config.js | 13 +- vite.config.ts | 6 +- 31 files changed, 677 insertions(+), 520 deletions(-) create mode 100644 src/routes/oauth/[provider]/callback/+page.server.ts create mode 100644 src/routes/oauth/[provider]/callback/+page.svelte delete mode 100644 src/routes/oauth/[provider]/callback/+server.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bba427d..0662f03 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -38,4 +38,4 @@ jobs: with: project: 'spiel-place' entrypoint: 'mod.ts' - root: 'build' \ No newline at end of file + root: 'build' diff --git a/bun.lockb b/bun.lockb index 55fe5bbe22d9864eb8fca03a222e74d68bba4381..3dfa21c86aba6a90f8a8a5252bca64d2e68d286d 100755 GIT binary patch delta 8483 zcmeI2dsJ0b9>@2-fO5f`5GaD7*+V4b@=&}Q=|%H}S1eSxK1LA%6?~wgX1*>ylTi}f z=3$z_N0@1oE6bHcO{RmXlvX}!W$8?*zIiA`Xxb5k{a(nFCx8HO6%QcJpjeBhRGp%~Rv&CBXNSpItoyz&>!69?f z7HyB+(8eN|@+;gOEg3n_&TkjgqOhnqUkLR`P?V=;iV$suD9J1k!U{cu&{oi>LLpj1 zTSJ4Peo*>9MI1G2s<&Q#ac<7EoV@%r*f#i+q2bV+*|~EHa)l@=o>s)}FHRF84E_bk zvwX#LAwq=klpI3lCOG_On2wTxT5F;7Z-cgh8c=qSjpk6Xq#8DeuN~b!l8{kWVPpD7~@)WED)2gsL ztl^j|Z&}4H4olVyH6*4_z+pHOl+BqEa2`2_vfYwmu@|W!)B6MzVUD}2^yw*<-KN9( zAsmTjhZ!*r>&=+T!DizbXnYJTZ{x@NJFFkV8fuo&ikl_Guq)O=Sl)UllXa@JD8j4v${zq|VqRot%s0`ew)iAw6P+8$I?fTGd@IlV8#!_s|#cilR`M5X^AU|WZ@ zeHh9_rN169?lr)4Ee%BZ3BdT1yjL@G3NTUG;5#&sd^HUACjdV(;sUU#U2`HLkr6G> z1Y2o|18ZApsIzlAc48)&2dyl%!L~x%Lj6_r+?ZGfMC&LjqdIEaSN&Ak+<0@sZr7#P zQij}t15a8P9e+K_QQW2LQJL*3%^R?pG0#*2y((^=_dF)*up}s}ch`0@qmZba|K8fZ z5z0TWuhh!~8FO{?*Bwwfkb&C%HOl&fQ9lBjt=svj>Ko@z3sCEt6>m_v=c*(7p#$Ey zCm}8bI#0(_nSD^(zRIaxsQtc5e`9}ChImSF+$i1KFnsLgke1*EW^c%wcUerL6xt%02v1dP4^`r7U_A2X=5=$N4H< zC$yi+>`5H>d+@II`zl?haiG4}xc-qb6O|pEfpUZ&X#dremAKm^Ow{Y!dFu85J8VhK z0X5YwXZ*^}Q}_Ra1BJX}-LRdZIgG2f=lMN5YKzW)X6HHVH*Y%_ z_l$#jHkx;y%{xzA3P0Mp@`t5)=b7EO6U^4Xu}x|7&XbeVyz}(kp>p~BKkPhv;O{WE zpjY%4?L7BfTzPcH&{LJ3hEV_gN75&JQ5PR@c|AKxn!pL1Xks@HkJOZOS;vc zzICg?-`Uhs%ndE~P)lT*iV z(`~-=fu?F7ujC2dZuo<0(H-$5NX2wnxe6}<#r27H%L4pNNKxw8Fj4IBwf2`s=L@Kb?N>0KQn>395PB zZ$st@<9NP|kVVEP`O@jZmo4-0o$n6#LZk+~0BXS|l+}WhfbXMl$(mQ_JZN9g5AcoT zL6pA=_JZwz9~tI>2f=*s5a4≨=f#B5NFdM|Pb^77{8%=NPba6NMU9YN3`@FZ9T z9tJDGaxk(Vqb#RgVo@VU@ce)_$avqaG{qVZYfv>R)Ce@FJh1oRVds5_{qQk zOF;=(g8+VJSPk6><;xU4{_}=#6!eDO2kb|Fmoa^^94m*L;u;Q*2PIt9T*X|q%VBd> zD!{KC{NlkC$pyFyW%M4>*jI;&ySLcH}|1S|@@WkU9$orpJx8Gi7HZb*AjvQjSlsC)tx~X3vzj%68uG z;aEEQJ8&THiAXi&v*d!Vvc?Fml1pW#QC1}*do+FlFRFaA^^(;?4T~J#-JaCV-c2-q zBk#68cfo5tp9v z4C2~1eqPVI)P49M`sBa<7yuYYdYHE78jEg$xm3HF}sNHl(dKQZpTc10aV_57jasBsKcgC2O?dxk=) z`CFaZ!kl|yBy5%YgHpM0kw&zsxwKVIn9`c(Dn2fr5|?c6VH`LqJJpN{vea6G%JqMB M*wDs^4!6|&7eZYa$p8QV delta 8218 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 eCZ$!r, string> | null; - session: FlatDocumentData, string> | null; + user: FlatDocumentData, string> | null + session: FlatDocumentData, string> | null } } } -export { }; +export {} diff --git a/src/app.pcss b/src/app.pcss index bd6213e..b5c61c9 100644 --- a/src/app.pcss +++ b/src/app.pcss @@ -1,3 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 9841ca8..b4473fd 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -1,33 +1,33 @@ -import { cookieController, cookieExpiration, getUserAndSession } from "$lib/server/auth"; -import type { Handle } from "@sveltejs/kit"; -import { createDate } from "oslo"; +import { cookieController, cookieExpiration, getUserAndSession } from '$lib/server/auth' +import type { Handle } from '@sveltejs/kit' +import { createDate } from 'oslo' export const handle: Handle = async ({ event, resolve }) => { - const sessionId = event.cookies.get("auth_session"); + const sessionId = event.cookies.get('auth_session') if (!sessionId) { - event.locals.user = null; - event.locals.session = null; - return resolve(event); + event.locals.user = null + event.locals.session = null + return resolve(event) } - const res = await getUserAndSession(sessionId); - if(res.isNone()) { - const sessionCookie = cookieController.createBlankCookie(); + const res = await getUserAndSession(sessionId) + if (res.isNone()) { + const sessionCookie = cookieController.createBlankCookie() event.cookies.set(sessionCookie.name, sessionCookie.value, { - path: ".", + path: '.', ...sessionCookie.attributes - }); - event.locals.user = null; - event.locals.session = null; - return resolve(event); - } - const { session, user } = res.unwrap() - const sessionCookie = cookieController.createCookie(session.id) - event.cookies.set(sessionCookie.name, sessionCookie.value, { - path: ".", - ...sessionCookie.attributes - }) - event.locals.user = user; - event.locals.session = session; - return resolve(event); -}; \ No newline at end of file + }) + event.locals.user = null + event.locals.session = null + return resolve(event) + } + const { session, user } = res.unwrap() + const sessionCookie = cookieController.createCookie(session.id) + event.cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }) + event.locals.user = user + event.locals.session = session + return resolve(event) +} diff --git a/src/lib/Tab.svelte b/src/lib/Tab.svelte index 986f79b..c4a2585 100644 --- a/src/lib/Tab.svelte +++ b/src/lib/Tab.svelte @@ -1,14 +1,23 @@ \ No newline at end of file + diff --git a/src/lib/apiclient.ts b/src/lib/apiclient.ts index 18748e9..b40c488 100644 --- a/src/lib/apiclient.ts +++ b/src/lib/apiclient.ts @@ -1,10 +1,10 @@ -import type { api as API } from "./server/hono" -import { hc } from "hono/client" +import type { api as API } from './server/hono' +import { hc } from 'hono/client' export const api = hc('/api', { - async fetch(input: RequestInfo | URL, init?: RequestInit): Promise { - return await fetch(input, { - ...init, - credentials: 'include', - }) - } -}) \ No newline at end of file + async fetch(input: RequestInfo | URL, init?: RequestInit): Promise { + return await fetch(input, { + ...init, + credentials: 'include' + }) + } +}) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 074566a..7a82808 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,12 +1,10 @@ import type { z } from 'zod' import { db, session } from './db' -import { Err, Ok, Result } from '@oxi/result' import { Option, None, Some } from '@oxi/option' import type { FlatDocumentData } from '@olli/kvdex' -import { nanoid } from 'nanoid' import { TimeSpan, createDate } from 'oslo' import { alphabet, generateRandomString } from 'oslo/crypto' -import { CookieController } from "oslo/cookie" +import { CookieController } from 'oslo/cookie' const sessionTimeSpan = new TimeSpan(1, 'w') @@ -32,9 +30,7 @@ async function deleteSession(sessionId: string): Promise { await db.session.delete(sessionId) } -async function getUserAndSession( - sessionId: string -): Promise< +async function getUserAndSession(sessionId: string): Promise< Option<{ user: FlatDocumentData, string> session: FlatDocumentData, string> @@ -44,17 +40,25 @@ async function getUserAndSession( if (!session) return None const user = (await db.user.find(session.userId))?.flat() if (!user) return None - await db.session.update(sessionId, { - expiresAt: createDate(sessionTimeSpan) - }, { expireIn: sessionTimeSpan.milliseconds() }) - return Some({ user, session }) + await db.session.update( + sessionId, + { + expiresAt: createDate(sessionTimeSpan) + }, + { expireIn: sessionTimeSpan.milliseconds() } + ) + return Some({ user, session }) } export const cookieExpiration = new TimeSpan(365 * 2, 'd') -export const cookieController = new CookieController('auth_session', { - httpOnly: true, - secure: true, - sameSite: "lax", - path: "/", -}, { expiresIn: cookieExpiration }) +export const cookieController = new CookieController( + 'auth_session', + { + httpOnly: true, + secure: true, + sameSite: 'lax', + path: '/' + }, + { expiresIn: cookieExpiration } +) export { createSessionForUser, deleteSession, getUserAndSession } diff --git a/src/lib/server/db.ts b/src/lib/server/db.ts index 906790e..b971ef5 100644 --- a/src/lib/server/db.ts +++ b/src/lib/server/db.ts @@ -1,50 +1,51 @@ -import { kvdex as kvdex, collection, model } from "@olli/kvdex" -import { openKv } from "@deno/kv" -import { z } from "zod" +import { kvdex as kvdex, collection, model } from '@olli/kvdex' +import { openKv } from '@deno/kv' +import { z } from 'zod' export const user = z.object({ - displayName: z.string(), - username: z.string(), - id: z.string(), - password: z.optional(z.string()), - oauth_github_id: z.optional(z.string()), - oauth_google_id: z.optional(z.string()), - oauth_discord_id: z.optional(z.string()) + displayName: z.string(), + username: z.string(), + id: z.string(), + password: z.optional(z.string()), + oauth_github_id: z.optional(z.string()), + oauth_google_id: z.optional(z.string()), + oauth_discord_id: z.optional(z.string()) }) export const publicUser = user.pick({ displayName: true, id: true }) export const session = z.object({ - expiresAt: z.date(), - userId: z.string(), + expiresAt: z.date(), + userId: z.string() }) export const chat = z.object({ - name: z.string(), - creator: z.string().describe('id'), - createdAt: z.date() + name: z.string(), + creator: z.string().describe('id'), + createdAt: z.date() }) export const kv = await openKv('http://0.0.0.0:4512') export const db = kvdex(kv, { - user: collection(user, { - idGenerator: ({ id }) => id, - indices: { - username: 'primary', - oauth_github_id: 'primary', - oauth_google_id: 'primary', - oauth_discord_id: 'primary' - } - }), - session: collection(session, { - indices: { - userId: 'secondary' - } - }), - chat: { - boxes: collection(model<{ roomID: string, userID: string, text: string }>()), - users: collection(publicUser), - updatekey: collection(model()), - data: collection(chat) - } -}) \ No newline at end of file + user: collection(user, { + idGenerator: ({ id }) => id, + indices: { + username: 'primary', + oauth_github_id: 'primary', + oauth_google_id: 'primary', + oauth_discord_id: 'primary' + } + }), + session: collection(session, { + indices: { + userId: 'secondary' + } + }), + chat: { + boxes: collection(model<{ roomID: string; userID: string; text: string }>()), + users: collection(publicUser), + updatekey: collection(model()), + data: collection(chat) + }, + saved_oauth_data: collection(model<{ type: 'create' | 'link'; oauth_id: string }>()) +}) diff --git a/src/lib/server/hono.ts b/src/lib/server/hono.ts index 5776f08..8e70ea5 100644 --- a/src/lib/server/hono.ts +++ b/src/lib/server/hono.ts @@ -40,18 +40,18 @@ const api = new Hono<{ Bindings: Bindings }>() 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) - } - }) - }) + .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 diff --git a/src/lib/server/oauth.ts b/src/lib/server/oauth.ts index b9e2ae5..7d424aa 100644 --- a/src/lib/server/oauth.ts +++ b/src/lib/server/oauth.ts @@ -1,131 +1,211 @@ -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"; +import { Google, Discord, GitHub, generateState, generateCodeVerifier } from 'arctic' +import { error, redirect, type Actions, type RequestHandler, type ServerLoad } from '@sveltejs/kit' +import { z } from 'zod' +import { db, publicUser } from './db' +import { env } from '$env/dynamic/private' +import { dev } from '$app/environment' +import { alphabet, generateRandomString } from 'oslo/crypto' +import { decodeJwt } from 'jose' +import { superForm, type SuperForm } from 'sveltekit-superforms' +import { zod, type ValidationAdapter } from 'sveltekit-superforms/adapters' + +const { + DISCORD_CLIENT_ID, + DISCORD_CLIENT_SECRET, + GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET, + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET +} = env //TODO: oauth -export const google = new Google(); -export const discord = new Discord(); -export const github = new GitHub(); +const DEVURL = (prov: string) => `http://localhost:5173/oauth/${prov}/callback` +const PRODURL = (prov: string) => `https://spiel.place/oauth/${prov}/callback` + +export const github = new GitHub(GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, { + redirectURI: dev ? DEVURL('github') : PRODURL('github') +}) +export const discord = new Discord( + DISCORD_CLIENT_ID, + DISCORD_CLIENT_SECRET, + dev ? DEVURL('discord') : PRODURL('discord') +) +export const google = new Google( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + dev ? DEVURL('google') : PRODURL('google') +) 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); - } + 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', +export function oauth_callback(): ServerLoad< + { provider: string }, + any, + { + type: 'create' | 'link' + name: string + user: z.infer + form: SuperForm< + ValidationAdapter< + { + token: string + }, + { + token: string + } + >, + any + > + prov: string + } +> { + 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') - } - } 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 - - } - } + 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 + let id + let name + if (provider instanceof Google) { + tokens = await provider.validateAuthorizationCode(code, storedCodeVerifier) + console.log(tokens.idToken) + const { sub, name: Uname } = decodeJwt(tokens.idToken) + id = sub + name = Uname + } else { + tokens = await provider.validateAuthorizationCode(code) + } + const formToken = generateRandomString(12, alphabet('0-9', 'a-z')) + const form = superForm( + zod(z.object({ token: z.string() }), { + defaults: { token: formToken } + }) + ) + 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 + await db.saved_oauth_data.set(formToken, { + oauth_id: id, + type: 'link' + }) + return { + type: 'link', + name, + prov: providerID, + user: publicUser.safeParse(locals.user).data!, + form + } + } 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 + await db.saved_oauth_data.set(formToken, { + oauth_id: id, + type: 'create' + }) + return { + type: 'create', + name, + prov: providerID, + user: publicUser.safeParse(locals.user).data!, + form + } + } + } } -export function callback_actions(): Actions<{ provider: string }> { - return { - - } -} \ No newline at end of file +export function oauth_callback_actions(): Actions<{ provider: string }> { + return {} +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1af3ad6..1161996 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,6 +1,6 @@ -{@render children()} \ No newline at end of file +{@render children()} diff --git a/src/routes/+page.ts b/src/routes/+page.ts index f7d2f63..cd00a67 100644 --- a/src/routes/+page.ts +++ b/src/routes/+page.ts @@ -1,5 +1,5 @@ -import { redirect } from "@sveltejs/kit"; +import { redirect } from '@sveltejs/kit' export async function load() { - return redirect(302, '/app') -} \ No newline at end of file + return redirect(302, '/app') +} diff --git a/src/routes/api/[...path]/+server.ts b/src/routes/api/[...path]/+server.ts index 8a3a252..dd06038 100644 --- a/src/routes/api/[...path]/+server.ts +++ b/src/routes/api/[...path]/+server.ts @@ -1,5 +1,5 @@ -import { hono } from '$lib/server/hono.js'; +import { hono } from '$lib/server/hono' export async function fallback({ request, locals }) { - return hono.fetch(request, { locals }) -} \ No newline at end of file + return hono.fetch(request, { locals }) +} diff --git a/src/routes/app/+layout.server.ts b/src/routes/app/+layout.server.ts index dd65cc0..faeee3b 100644 --- a/src/routes/app/+layout.server.ts +++ b/src/routes/app/+layout.server.ts @@ -1,12 +1,12 @@ -import { publicUser } from '$lib/server/db.js'; -import { redirect } from '@sveltejs/kit'; +import { publicUser } from '$lib/server/db' +import { redirect } from '@sveltejs/kit' export async function load({ locals }) { - if(!locals.session) { - return redirect(302, "/auth") - } - return publicUser.safeParse(locals.user).data! + if (!locals.session) { + return redirect(302, '/auth') + } + return publicUser.safeParse(locals.user).data! } -export const csr = true; -export const ssr = false; \ No newline at end of file +export const csr = true +export const ssr = false diff --git a/src/routes/app/+layout.svelte b/src/routes/app/+layout.svelte index d05b9aa..d74b2b8 100644 --- a/src/routes/app/+layout.svelte +++ b/src/routes/app/+layout.svelte @@ -5,73 +5,92 @@
-
- -
-
- {@render children()} -
+
+ +
+
+ {@render children()} +
-
- New message arrived. -
-
\ No newline at end of file +
+ New message arrived. +
+ diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index caf23ce..bb70fc6 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,29 +1,33 @@
-
- new room - - -
+
+ new room + + +
-
- list -
\ No newline at end of file +
list
diff --git a/src/routes/app/:[roomID]/+page.svelte b/src/routes/app/:[roomID]/+page.svelte index 775080e..649e0e3 100644 --- a/src/routes/app/:[roomID]/+page.svelte +++ b/src/routes/app/:[roomID]/+page.svelte @@ -1,5 +1,5 @@ -{data.roomID} \ No newline at end of file +{data.roomID} diff --git a/src/routes/app/:[roomID]/+page.ts b/src/routes/app/:[roomID]/+page.ts index 2328ef5..2afb1b9 100644 --- a/src/routes/app/:[roomID]/+page.ts +++ b/src/routes/app/:[roomID]/+page.ts @@ -1,3 +1,3 @@ export async function load({ params: { roomID } }) { - return { roomID } -} \ No newline at end of file + return { roomID } +} diff --git a/src/routes/auth/+page.server.ts b/src/routes/auth/+page.server.ts index e072409..60d0780 100644 --- a/src/routes/auth/+page.server.ts +++ b/src/routes/auth/+page.server.ts @@ -1,75 +1,80 @@ -import { alphabet, generateRandomString } from 'oslo/crypto'; -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/server/db.js'; -import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth.js'; -import { createDate } from 'oslo'; -import { redirect } from '@sveltejs/kit'; +import { alphabet, generateRandomString } from 'oslo/crypto' +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/server/db' +import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth' +import { createDate } from 'oslo' +import { redirect } from '@sveltejs/kit' const schema = z.object({ - username: z.string().min(4, "must be atleast 4 characters").max(32, "must be less than 32 characters").regex(/^[a-z0-9_\-]+$/i, `must be alphanumeric, with the exception of "_" and "-"`), - password: z.string().min(8, "must be atleast 8 characters").max(255) -}); + username: z + .string() + .min(4, 'must be atleast 4 characters') + .max(32, 'must be less than 32 characters') + .regex(/^[a-z0-9_\-]+$/i, `must be alphanumeric, with the exception of "_" and "-"`), + password: z.string().min(8, 'must be atleast 8 characters').max(255) +}) export async function load({ locals }) { - if(locals.session) { - return redirect(302, '/app') - } - const form = await superValidate(zod(schema)); - return { form }; -}; + if (locals.session) { + return redirect(302, '/app') + } + const form = await superValidate(zod(schema)) + return { form } +} export const actions = { - login: async ({ request, cookies }) => { - const form = await superValidate(request, zod(schema)); + login: async ({ request, cookies }) => { + const form = await superValidate(request, zod(schema)) - if (!form.valid) return fail(400, { form }); - const { username, password } = form.data - const user = (await db.user.findByPrimaryIndex('username', username))?.flat(); - if (!user) return setError(form, "user does not exist") - if (!user.password) return setError(form, "this account does not have a password, maybe try a different method?") - const isvalid = verify(password, user.password); - if (!isvalid) return setError(form, "incorrect password") - const session = (await createSessionForUser(user.id)) - if (session.isSome()) { - const sessionCookie = cookieController.createCookie(session.unwrap().id) - cookies.set(sessionCookie.name, sessionCookie.value, { - path: ".", - ...sessionCookie.attributes - }) - return redirect(302, '/app') - } else { - return fail(500, { form }) - } - }, - signup: async ({ request, cookies }) => { - const form = await superValidate(request, zod(schema)); - if (!form.valid) return fail(400, { form }); - const { username, password } = form.data + if (!form.valid) return fail(400, { form }) + const { username, password } = form.data + const user = (await db.user.findByPrimaryIndex('username', username))?.flat() + if (!user) return setError(form, 'user does not exist') + if (!user.password) + return setError(form, 'this account does not have a password, maybe try a different method?') + const isvalid = verify(password, user.password) + if (!isvalid) return setError(form, 'incorrect password') + const session = await createSessionForUser(user.id) + if (session.isSome()) { + const sessionCookie = cookieController.createCookie(session.unwrap().id) + cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }) + return redirect(302, '/app') + } else { + return fail(500, { form }) + } + }, + signup: async ({ request, cookies }) => { + const form = await superValidate(request, zod(schema)) + if (!form.valid) return fail(400, { form }) + const { username, password } = form.data - const userId = generateRandomString(10, alphabet("0-9", "a-z")) - const passwordHash = hash(password) + const userId = generateRandomString(10, alphabet('0-9', 'a-z')) + const passwordHash = hash(password) - const user = (await db.user.findByPrimaryIndex('username', username))?.flat() - if (user) return setError(form, "username", 'username already exists') - await db.user.set(userId, { - displayName: username, - username, - id: userId, - password: passwordHash - }) - const session = (await createSessionForUser(userId)) - if (session.isSome()) { - const sessionCookie = cookieController.createCookie(session.unwrap().id) - cookies.set(sessionCookie.name, sessionCookie.value, { - path: ".", - ...sessionCookie.attributes - }) - return redirect(302, '/app') - } else { - return fail(500, { form }) - } - } -} \ No newline at end of file + const user = (await db.user.findByPrimaryIndex('username', username))?.flat() + if (user) return setError(form, 'username', 'username already exists') + await db.user.set(userId, { + displayName: username, + username, + id: userId, + password: passwordHash + }) + const session = await createSessionForUser(userId) + if (session.isSome()) { + const sessionCookie = cookieController.createCookie(session.unwrap().id) + cookies.set(sessionCookie.name, sessionCookie.value, { + path: '.', + ...sessionCookie.attributes + }) + return redirect(302, '/app') + } else { + return fail(500, { form }) + } + } +} diff --git a/src/routes/auth/+page.svelte b/src/routes/auth/+page.svelte index 2a8aaaa..1805fdd 100644 --- a/src/routes/auth/+page.svelte +++ b/src/routes/auth/+page.svelte @@ -18,9 +18,9 @@

sign up/in

or
+ class:showerror={$errors.username}>{$errors.username?.join(' & ')} + class:showerror={$errors.password}>{$errors.password}
+{:else if data.type === 'link'}{/if} diff --git a/src/routes/oauth/[provider]/callback/+server.ts b/src/routes/oauth/[provider]/callback/+server.ts deleted file mode 100644 index e69de29..0000000 diff --git a/svelte.config.js b/svelte.config.js index d6f38cf..0118511 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,7 +1,7 @@ -import adapter from 'sveltekit-adapter-deno'; -import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +import adapter from 'sveltekit-adapter-deno' +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' -import { Float16Array } from "@petamoriken/float16" +import { Float16Array } from '@petamoriken/float16' // kvdex uses float16array under the hood (doesn't exist in node), filling that in here so it works during dev globalThis.Float16Array = Float16Array @@ -23,6 +23,6 @@ const config = { } }) } -}; +} -export default config; +export default config diff --git a/tailwind.config.js b/tailwind.config.js index 08be3d2..042ca31 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,11 +1,10 @@ -import daisyui from "daisyui" +import daisyui from 'daisyui' /** @type {import('tailwindcss').Config} */ export default { - content: ['./src/**/*.{html,js,svelte,ts}'], - theme: { - extend: {}, - }, - plugins: [daisyui], + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: {} + }, + plugins: [daisyui] } - diff --git a/vite.config.ts b/vite.config.ts index 5cf7a7b..2ae69f3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,5 +1,5 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { sveltekit } from '@sveltejs/kit/vite' +import { defineConfig } from 'vite' export default defineConfig({ plugins: [sveltekit()], @@ -11,4 +11,4 @@ export default defineConfig({ target: ['deno1.45.5'] } } -}); +})