almost done fixing up oauth

This commit is contained in:
Chad Freeman 2024-08-18 19:36:08 -04:00
parent 3341ea2bf6
commit 093b7a8135
31 changed files with 677 additions and 520 deletions

View file

@ -38,4 +38,4 @@ jobs:
with: with:
project: 'spiel-place' project: 'spiel-place'
entrypoint: 'mod.ts' entrypoint: 'mod.ts'
root: 'build' root: 'build'

BIN
bun.lockb

Binary file not shown.

View file

@ -1,46 +1,47 @@
{ {
"name": "talkomatic", "name": "talkomatic",
"version": "0.0.1", "version": "0.0.1",
"devDependencies": { "devDependencies": {
"@deno/kv": "^0.8.1", "@deno/kv": "^0.8.1",
"@hono/zod-validator": "^0.2.2", "@hono/zod-validator": "^0.2.2",
"@olli/kvdex": "npm:@jsr/olli__kvdex", "@olli/kvdex": "npm:@jsr/olli__kvdex",
"@oxi/option": "npm:@jsr/oxi__option", "@oxi/option": "npm:@jsr/oxi__option",
"@oxi/result": "npm:@jsr/oxi__result", "@oxi/result": "npm:@jsr/oxi__result",
"@petamoriken/float16": "^3.8.7", "@petamoriken/float16": "^3.8.7",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@ts-rex/argon2": "npm:@jsr/ts-rex__argon2", "@ts-rex/argon2": "npm:@jsr/ts-rex__argon2",
"arctic": "^1.9.2", "arctic": "^1.9.2",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"daisyui": "^4.12.10", "daisyui": "^4.12.10",
"hono": "^4.5.5", "hono": "^4.5.5",
"nanoid": "^5.0.7", "jose": "^5.6.3",
"oslo": "^1.2.1", "nanoid": "^5.0.7",
"postcss": "^8.4.41", "oslo": "^1.2.1",
"prettier": "^3.3.2", "postcss": "^8.4.41",
"prettier-plugin-svelte": "^3.2.5", "prettier": "^3.3.2",
"svelte": "^5.0.0-next.1", "prettier-plugin-svelte": "^3.2.5",
"svelte-check": "^3.6.0", "svelte": "^5.0.0-next.1",
"sveltekit-adapter-deno": "^0.12.1", "svelte-check": "^3.6.0",
"sveltekit-superforms": "^2.17.0", "sveltekit-adapter-deno": "^0.12.1",
"tailwindcss": "^3.4.10", "sveltekit-superforms": "^2.17.0",
"typescript": "^5.0.0", "tailwindcss": "^3.4.10",
"vite": "^5.0.3", "typescript": "^5.0.0",
"zod": "^3.23.8" "vite": "^5.0.3",
}, "zod": "^3.23.8"
"private": true, },
"scripts": { "private": true,
"dev": "vite dev", "scripts": {
"build": "vite build", "dev": "vite dev",
"preview": "vite preview", "build": "vite build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "preview": "vite preview",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --write .", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check ." "format": "prettier --write .",
}, "lint": "prettier --check ."
"trustedDependencies": [ },
"svelte-preprocess" "trustedDependencies": [
], "svelte-preprocess"
"type": "module" ],
"type": "module"
} }

View file

@ -1,6 +1,6 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {}
}, }
} }

12
src/app.d.ts vendored
View file

@ -1,9 +1,9 @@
// See https://kit.svelte.dev/docs/types#app // See https://kit.svelte.dev/docs/types#app
// for information about these interfaces // for information about these interfaces
import type { FlatDocumentData } from "@olli/kvdex" import type { FlatDocumentData } from '@olli/kvdex'
import { user, session } from "$lib/server/db" import { user, session } from '$lib/server/db'
import z from "zod" import z from 'zod'
declare global { declare global {
namespace App { namespace App {
// interface Error {} // interface Error {}
@ -12,10 +12,10 @@ declare global {
// interface PageState {} // interface PageState {}
// interface Platform {} // interface Platform {}
interface Locals { interface Locals {
user: FlatDocumentData<z.infer<typeof user>, string> | null; user: FlatDocumentData<z.infer<typeof user>, string> | null
session: FlatDocumentData<z.infer<typeof session>, string> | null; session: FlatDocumentData<z.infer<typeof session>, string> | null
} }
} }
} }
export { }; export {}

View file

@ -1,3 +1,3 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View file

@ -1,33 +1,33 @@
import { cookieController, cookieExpiration, getUserAndSession } from "$lib/server/auth"; import { cookieController, cookieExpiration, getUserAndSession } from '$lib/server/auth'
import type { Handle } from "@sveltejs/kit"; import type { Handle } from '@sveltejs/kit'
import { createDate } from "oslo"; import { createDate } from 'oslo'
export const handle: Handle = async ({ event, resolve }) => { export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get("auth_session"); const sessionId = event.cookies.get('auth_session')
if (!sessionId) { if (!sessionId) {
event.locals.user = null; event.locals.user = null
event.locals.session = null; event.locals.session = null
return resolve(event); return resolve(event)
} }
const res = await getUserAndSession(sessionId); const res = await getUserAndSession(sessionId)
if(res.isNone()) { if (res.isNone()) {
const sessionCookie = cookieController.createBlankCookie(); const sessionCookie = cookieController.createBlankCookie()
event.cookies.set(sessionCookie.name, sessionCookie.value, { event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".", path: '.',
...sessionCookie.attributes ...sessionCookie.attributes
}); })
event.locals.user = null; event.locals.user = null
event.locals.session = null; event.locals.session = null
return resolve(event); return resolve(event)
} }
const { session, user } = res.unwrap() const { session, user } = res.unwrap()
const sessionCookie = cookieController.createCookie(session.id) const sessionCookie = cookieController.createCookie(session.id)
event.cookies.set(sessionCookie.name, sessionCookie.value, { event.cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".", path: '.',
...sessionCookie.attributes ...sessionCookie.attributes
}) })
event.locals.user = user; event.locals.user = user
event.locals.session = session; event.locals.session = session
return resolve(event); return resolve(event)
}; }

View file

@ -1,14 +1,23 @@
<script lang="ts"> <script lang="ts">
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte'
let { tab = $bindable(), disabled = $bindable(false), map, ...props }: { let {
tab: string | undefined, tab = $bindable(),
disabled?: boolean, disabled = $bindable(false),
map?: Record<string, string>, map,
class?: string, ...props
[x: `_${string}`]: Snippet<[]>; }: {
} = $props(); tab: string | undefined
const tabs = Object.fromEntries(Object.entries(props).filter((v) => v[0].startsWith('_')).map(([name, snippet]): [string, Snippet<[]>] => [name.replace('_', ''), snippet])); disabled?: boolean
map?: Record<string, string>
class?: string
[x: `_${string}`]: Snippet<[]>
} = $props()
const tabs = Object.fromEntries(
Object.entries(props)
.filter((v) => v[0].startsWith('_'))
.map(([name, snippet]): [string, Snippet<[]>] => [name.replace('_', ''), snippet])
)
</script> </script>
<div role="tablist" class="tabs tabs-boxed {props.class}"> <div role="tablist" class="tabs tabs-boxed {props.class}">
@ -16,6 +25,14 @@
<!-- svelte-ignore a11y_interactive_supports_focus --> <!-- svelte-ignore a11y_interactive_supports_focus -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_missing_attribute --> <!-- svelte-ignore a11y_missing_attribute -->
<a class:cursor-not-allowed={disabled} class:pointer-events-none={disabled} class:tab-disabled={disabled} role="tab" class="tab uppercase" onclick={() => tab = tabID} class:tab-active={tabID == tab}>{@render tabs[tabID]()}</a> <a
class:cursor-not-allowed={disabled}
class:pointer-events-none={disabled}
class:tab-disabled={disabled}
role="tab"
class="tab uppercase"
onclick={() => (tab = tabID)}
class:tab-active={tabID == tab}>{@render tabs[tabID]()}</a
>
{/each} {/each}
</div> </div>

View file

@ -1,10 +1,10 @@
import type { api as API } from "./server/hono" import type { api as API } from './server/hono'
import { hc } from "hono/client" import { hc } from 'hono/client'
export const api = hc<API>('/api', { export const api = hc<API>('/api', {
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> { async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return await fetch(input, { return await fetch(input, {
...init, ...init,
credentials: 'include', credentials: 'include'
}) })
} }
}) })

View file

@ -1,12 +1,10 @@
import type { z } from 'zod' import type { z } from 'zod'
import { db, session } from './db' import { db, session } from './db'
import { Err, Ok, Result } from '@oxi/result'
import { Option, None, Some } from '@oxi/option' import { Option, None, Some } from '@oxi/option'
import type { FlatDocumentData } from '@olli/kvdex' import type { FlatDocumentData } from '@olli/kvdex'
import { nanoid } from 'nanoid'
import { TimeSpan, createDate } from 'oslo' import { TimeSpan, createDate } from 'oslo'
import { alphabet, generateRandomString } from 'oslo/crypto' import { alphabet, generateRandomString } from 'oslo/crypto'
import { CookieController } from "oslo/cookie" import { CookieController } from 'oslo/cookie'
const sessionTimeSpan = new TimeSpan(1, 'w') const sessionTimeSpan = new TimeSpan(1, 'w')
@ -32,9 +30,7 @@ async function deleteSession(sessionId: string): Promise<void> {
await db.session.delete(sessionId) await db.session.delete(sessionId)
} }
async function getUserAndSession( async function getUserAndSession(sessionId: string): Promise<
sessionId: string
): Promise<
Option<{ Option<{
user: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['user']>, string> user: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['user']>, string>
session: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['session']>, string> session: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['session']>, string>
@ -44,17 +40,25 @@ async function getUserAndSession(
if (!session) return None if (!session) return None
const user = (await db.user.find(session.userId))?.flat() const user = (await db.user.find(session.userId))?.flat()
if (!user) return None if (!user) return None
await db.session.update(sessionId, { await db.session.update(
expiresAt: createDate(sessionTimeSpan) sessionId,
}, { expireIn: sessionTimeSpan.milliseconds() }) {
return Some({ user, session }) expiresAt: createDate(sessionTimeSpan)
},
{ expireIn: sessionTimeSpan.milliseconds() }
)
return Some({ user, session })
} }
export const cookieExpiration = new TimeSpan(365 * 2, 'd') export const cookieExpiration = new TimeSpan(365 * 2, 'd')
export const cookieController = new CookieController('auth_session', { export const cookieController = new CookieController(
httpOnly: true, 'auth_session',
secure: true, {
sameSite: "lax", httpOnly: true,
path: "/", secure: true,
}, { expiresIn: cookieExpiration }) sameSite: 'lax',
path: '/'
},
{ expiresIn: cookieExpiration }
)
export { createSessionForUser, deleteSession, getUserAndSession } export { createSessionForUser, deleteSession, getUserAndSession }

View file

@ -1,50 +1,51 @@
import { kvdex as kvdex, collection, model } from "@olli/kvdex" import { kvdex as kvdex, collection, model } from '@olli/kvdex'
import { openKv } from "@deno/kv" import { openKv } from '@deno/kv'
import { z } from "zod" import { z } from 'zod'
export const user = z.object({ export const user = z.object({
displayName: z.string(), displayName: z.string(),
username: z.string(), username: z.string(),
id: z.string(), id: z.string(),
password: z.optional(z.string()), password: z.optional(z.string()),
oauth_github_id: z.optional(z.string()), oauth_github_id: z.optional(z.string()),
oauth_google_id: z.optional(z.string()), oauth_google_id: z.optional(z.string()),
oauth_discord_id: z.optional(z.string()) oauth_discord_id: z.optional(z.string())
}) })
export const publicUser = user.pick({ displayName: true, id: true }) export const publicUser = user.pick({ displayName: true, id: true })
export const session = z.object({ export const session = z.object({
expiresAt: z.date(), expiresAt: z.date(),
userId: z.string(), userId: z.string()
}) })
export const chat = z.object({ export const chat = z.object({
name: z.string(), name: z.string(),
creator: z.string().describe('id'), creator: z.string().describe('id'),
createdAt: z.date() createdAt: z.date()
}) })
export const kv = await openKv('http://0.0.0.0:4512') export const kv = await openKv('http://0.0.0.0:4512')
export const db = kvdex(kv, { export const db = kvdex(kv, {
user: collection(user, { user: collection(user, {
idGenerator: ({ id }) => id, idGenerator: ({ id }) => id,
indices: { indices: {
username: 'primary', username: 'primary',
oauth_github_id: 'primary', oauth_github_id: 'primary',
oauth_google_id: 'primary', oauth_google_id: 'primary',
oauth_discord_id: 'primary' oauth_discord_id: 'primary'
} }
}), }),
session: collection(session, { session: collection(session, {
indices: { indices: {
userId: 'secondary' userId: 'secondary'
} }
}), }),
chat: { chat: {
boxes: collection(model<{ roomID: string, userID: string, text: string }>()), boxes: collection(model<{ roomID: string; userID: string; text: string }>()),
users: collection(publicUser), users: collection(publicUser),
updatekey: collection(model<true>()), updatekey: collection(model<true>()),
data: collection(chat) data: collection(chat)
} },
}) saved_oauth_data: collection(model<{ type: 'create' | 'link'; oauth_id: string }>())
})

View file

@ -40,18 +40,18 @@ const api = new Hono<{ Bindings: Bindings }>()
return text(roomId) return text(roomId)
} }
) )
.get('/rooms/connect/:id', (c) => { .get('/rooms/connect/:id', (c) => {
return streamSSE(c, async (stream) => { return streamSSE(c, async (stream) => {
while (true) { while (true) {
const message = `It is ${new Date().toISOString()}` const message = `It is ${new Date().toISOString()}`
await stream.writeSSE({ await stream.writeSSE({
data: message, data: message,
event: 'time-update', event: 'time-update'
}) })
await stream.sleep(1000) await stream.sleep(1000)
} }
}) })
}) })
export type api = typeof api export type api = typeof api

View file

@ -1,131 +1,211 @@
import { Google, Discord, GitHub, type OAuth2Provider, type OAuth2ProviderWithPKCE, generateState, generateCodeVerifier } from "arctic" import { Google, Discord, GitHub, generateState, generateCodeVerifier } from 'arctic'
import { error, redirect, type Actions, type RequestHandler, type ServerLoad } from "@sveltejs/kit"; import { error, redirect, type Actions, type RequestHandler, type ServerLoad } from '@sveltejs/kit'
import type { z } from "zod"; import { z } from 'zod'
import type { publicUser } from "./db"; 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 //TODO: oauth
export const google = new Google(); const DEVURL = (prov: string) => `http://localhost:5173/oauth/${prov}/callback`
export const discord = new Discord(); const PRODURL = (prov: string) => `https://spiel.place/oauth/${prov}/callback`
export const github = new GitHub();
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 }> { export function oauth_handler(): RequestHandler<{ provider: string }> {
return async ({ cookies, params: { provider: providerID }, url }) => { return async ({ cookies, params: { provider: providerID }, url }) => {
let provider: Google | Discord | GitHub; let provider: Google | Discord | GitHub
let scopes: string[] let scopes: string[]
let noop = false; let noop = false
switch(providerID) { switch (providerID) {
case "discord": { case 'discord': {
provider = discord provider = discord
scopes = ['identify'] scopes = ['identify']
break break
} }
case "google": { case 'google': {
provider = google provider = google
scopes = ['profile'] scopes = ['profile']
break break
} }
case "github": { case 'github': {
provider = github provider = github
scopes = [] scopes = []
break break
} }
default: { default: {
noop = true noop = true
break break
} }
} }
// @ts-expect-error: I know im using it before assignment that's the point // @ts-expect-error: I know im using it before assignment that's the point
if(noop || !provider || !scopes) error(404, "provider not found") if (noop || !provider || !scopes) error(404, 'provider not found')
let redir: URL; let redir: URL
const state = generateState(); const state = generateState()
let codeVerifier: string; let codeVerifier: string
if(provider instanceof Google) { if (provider instanceof Google) {
codeVerifier = generateCodeVerifier() codeVerifier = generateCodeVerifier()
redir = await provider.createAuthorizationURL(state, codeVerifier, { redir = await provider.createAuthorizationURL(state, codeVerifier, {
scopes scopes
}) })
} else { } else {
redir = await provider.createAuthorizationURL(state, { redir = await provider.createAuthorizationURL(state, {
scopes scopes
}) })
} }
cookies.set("state", state, { cookies.set('state', state, {
secure: true, secure: true,
path: "/", path: '/',
httpOnly: true, httpOnly: true,
maxAge: 60 * 10 maxAge: 60 * 10
}); })
// @ts-expect-error: I know im using it before assignment that's the point // @ts-expect-error: I know im using it before assignment that's the point
if(codeVerifier) { if (codeVerifier) {
cookies.set("code_verifier", codeVerifier, { cookies.set('code_verifier', codeVerifier, {
secure: true, secure: true,
path: "/", path: '/',
httpOnly: true, httpOnly: true,
maxAge: 60 * 10 maxAge: 60 * 10
}); })
} }
redirect(302, redir); redirect(302, redir)
} }
} }
export function oauth_callback(): ServerLoad<{ provider: string }, any, { type: "create" | "link", name: string, user: z.infer<typeof publicUser> }> { export function oauth_callback(): ServerLoad<
return async ({ cookies, params: { provider: providerID }, locals, url }) => { { provider: string },
let provider: Google | Discord | GitHub; any,
let scopes: string[] {
let noop = false; type: 'create' | 'link'
switch(providerID) { name: string
case "discord": { user: z.infer<typeof publicUser>
provider = discord form: SuperForm<
scopes = ['identify'] ValidationAdapter<
break {
} token: string
case "google": { },
provider = google {
scopes = ['profile'] token: string
break }
} >,
case "github": { any
provider = github >
scopes = [] prov: string
break }
} > {
default: { return async ({ cookies, params: { provider: providerID }, locals, url }) => {
noop = true let provider: Google | Discord | GitHub
break let scopes: string[]
} let noop = false
} switch (providerID) {
// @ts-expect-error: I know im using it before assignment that's the point case 'discord': {
if(noop || !provider || !scopes) error(404, "provider not found") provider = discord
const code = url.searchParams.get("code"); scopes = ['identify']
const state = url.searchParams.get("state"); break
}
const storedState = cookies.get("state"); case 'google': {
const storedCodeVerifier = cookies.get("code_verifier"); provider = google
if (!code || !storedState || state !== storedState || (provider instanceof Google && !storedCodeVerifier)) { scopes = ['profile']
error(400, "Invalid request") break
} }
let tokens case 'github': {
if(provider instanceof Google) { provider = github
tokens = await provider.validateAuthorizationCode(code, storedCodeVerifier) scopes = []
} else { break
tokens = await provider.validateAuthorizationCode(code) }
} default: {
if(locals.user) { noop = true
// 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 break
return { }
type: 'create', }
// @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')
} else { const storedCodeVerifier = cookies.get('code_verifier')
// 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 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 }> { export function oauth_callback_actions(): Actions<{ provider: string }> {
return { return {}
}
}
}

View file

@ -1,6 +1,6 @@
<script> <script>
import "../app.pcss" import '../app.pcss'
const { children } = $props() const { children } = $props()
</script> </script>
{@render children()} {@render children()}

View file

@ -1,5 +1,5 @@
import { redirect } from "@sveltejs/kit"; import { redirect } from '@sveltejs/kit'
export async function load() { export async function load() {
return redirect(302, '/app') return redirect(302, '/app')
} }

View file

@ -1,5 +1,5 @@
import { hono } from '$lib/server/hono.js'; import { hono } from '$lib/server/hono'
export async function fallback({ request, locals }) { export async function fallback({ request, locals }) {
return hono.fetch(request, { locals }) return hono.fetch(request, { locals })
} }

View file

@ -1,12 +1,12 @@
import { publicUser } from '$lib/server/db.js'; import { publicUser } from '$lib/server/db'
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'
export async function load({ locals }) { export async function load({ locals }) {
if(!locals.session) { if (!locals.session) {
return redirect(302, "/auth") return redirect(302, '/auth')
} }
return publicUser.safeParse(locals.user).data! return publicUser.safeParse(locals.user).data!
} }
export const csr = true; export const csr = true
export const ssr = false; export const ssr = false

View file

@ -5,73 +5,92 @@
</script> </script>
<div class="h-[100vh] w-[100vw] flex flex-col"> <div class="h-[100vh] w-[100vw] flex flex-col">
<div class="px-4"> <div class="px-4">
<div class="navbar bg-base-200 rounded-b-xl"> <div class="navbar bg-base-200 rounded-b-xl">
<div class="flex-1"> <div class="flex-1">
<button class="btn btn-ghost text-xl">spiel.place</button> <button class="btn btn-ghost text-xl">spiel.place</button>
</div> </div>
<div class="flex-none"> <div class="flex-none">
<details <details
class="dropdown dropdown-end" class="dropdown dropdown-end"
ontoggle={({ newState }) => (dropdownOpen = newState === 'open')}> ontoggle={({ newState }) => (dropdownOpen = newState === 'open')}
<summary class="btn btn-circle btn-ghost m-1 swap swap-rotate"> >
<input type="checkbox" bind:checked={dropdownOpen} /> <summary class="btn btn-circle btn-ghost m-1 swap swap-rotate">
<svg <input type="checkbox" bind:checked={dropdownOpen} />
xmlns="http://www.w3.org/2000/svg" <svg
viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"
fill="currentColor" viewBox="0 0 20 20"
class="size-5 swap-off"> fill="currentColor"
<path class="size-5 swap-off"
fill-rule="evenodd" >
d="M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75ZM2 10a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 10Zm0 5.25a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1-.75-.75Z" <path
clip-rule="evenodd"></path> fill-rule="evenodd"
</svg> d="M2 4.75A.75.75 0 0 1 2.75 4h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 4.75ZM2 10a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75A.75.75 0 0 1 2 10Zm0 5.25a.75.75 0 0 1 .75-.75h14.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1-.75-.75Z"
<svg clip-rule="evenodd"
xmlns="http://www.w3.org/2000/svg" ></path>
fill="none" </svg>
viewBox="0 0 24 24" <svg
stroke-width="1.5" xmlns="http://www.w3.org/2000/svg"
stroke="currentColor" fill="none"
class="size-5 swap-on stroke-2"> viewBox="0 0 24 24"
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path> stroke-width="1.5"
</svg> stroke="currentColor"
</summary> class="size-5 swap-on stroke-2"
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow"> >
<li><a href="/app/settings"> <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"> </svg>
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> </summary>
</svg> <ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
{data.displayName} <li>
</a></li> <a href="/app/settings">
<li> <svg
<a href="/auth/signout"> xmlns="http://www.w3.org/2000/svg"
<svg fill="none"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
fill="none" stroke-width="1.5"
viewBox="0 0 24 24" stroke="currentColor"
stroke-width="1.5" class="size-6"
stroke="currentColor" >
class="size-6"> <path
<path stroke-linecap="round"
stroke-linecap="round" stroke-linejoin="round"
stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9" ></path>
></path> </svg>
</svg> {data.displayName}
sign out</a> </a>
</li> </li>
</ul> <li>
</details> <a href="/auth/signout">
</div> <svg
</div> xmlns="http://www.w3.org/2000/svg"
</div> fill="none"
<div class="flex-grow flex flex-row"> viewBox="0 0 24 24"
{@render children()} stroke-width="1.5"
</div> stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15m3 0 3-3m0 0-3-3m3 3H9"
></path>
</svg>
sign out</a
>
</li>
</ul>
</details>
</div>
</div>
</div>
<div class="flex-grow flex flex-row">
{@render children()}
</div>
</div> </div>
<div class="toast"> <div class="toast">
<div class="alert alert-info"> <div class="alert alert-info">
<span>New message arrived.</span> <span>New message arrived.</span>
</div> </div>
</div> </div>

View file

@ -1,29 +1,33 @@
<script> <script>
import { api } from '$lib/apiclient' import { api } from '$lib/apiclient'
import { goto } from "$app/navigation" import { goto } from '$app/navigation'
const { data } = $props() const { data } = $props()
let roomname = $state('') let roomname = $state('')
async function createRoom() { async function createRoom() {
const res = await api.rooms.create.$post({ const res = await api.rooms.create.$post({
json: { json: {
name: roomname name: roomname
} }
}) })
if(!res.ok) return if (!res.ok) return
const id = await res.text() const id = await res.text()
goto(`/app/:${id}`) goto(`/app/:${id}`)
} }
</script> </script>
<div class="w-56 flex flex-col justify-center"> <div class="w-56 flex flex-col justify-center">
<div class="my-10 px-2 py-2 rounded-r-lg bg-base-200"> <div class="my-10 px-2 py-2 rounded-r-lg bg-base-200">
<span class="text-md font-bold mb-2">new room</span> <span class="text-md font-bold mb-2">new room</span>
<input bind:value={roomname} minlength=4 class="input input-bordered w-full placeholder:text-base-content/50 mb-2" type="text" placeholder="name"> <input
<button on:click={createRoom} class="btn btn-secondary w-full">create</button> bind:value={roomname}
</div> minlength="4"
class="input input-bordered w-full placeholder:text-base-content/50 mb-2"
type="text"
placeholder="name"
/>
<button on:click={createRoom} class="btn btn-secondary w-full">create</button>
</div>
</div> </div>
<main class="flex-grow"> <main class="flex-grow">list</main>
list
</main>

View file

@ -1,5 +1,5 @@
<script> <script>
const { data } = $props() const { data } = $props()
</script> </script>
{data.roomID} {data.roomID}

View file

@ -1,3 +1,3 @@
export async function load({ params: { roomID } }) { export async function load({ params: { roomID } }) {
return { roomID } return { roomID }
} }

View file

@ -1,75 +1,80 @@
import { alphabet, generateRandomString } from 'oslo/crypto'; import { alphabet, generateRandomString } from 'oslo/crypto'
import { fail, message, setError, superValidate } from 'sveltekit-superforms'; import { fail, message, setError, superValidate } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'; import { zod } from 'sveltekit-superforms/adapters'
import { z } from 'zod'; import { z } from 'zod'
import { hash, verify } from "@ts-rex/argon2" import { hash, verify } from '@ts-rex/argon2'
import { db } from '$lib/server/db.js'; import { db } from '$lib/server/db'
import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth.js'; import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth'
import { createDate } from 'oslo'; import { createDate } from 'oslo'
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'
const schema = z.object({ 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 "-"`), username: z
password: z.string().min(8, "must be atleast 8 characters").max(255) .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 }) { export async function load({ locals }) {
if(locals.session) { if (locals.session) {
return redirect(302, '/app') return redirect(302, '/app')
} }
const form = await superValidate(zod(schema)); const form = await superValidate(zod(schema))
return { form }; return { form }
}; }
export const actions = { export const actions = {
login: async ({ request, cookies }) => { login: async ({ request, cookies }) => {
const form = await superValidate(request, zod(schema)); const form = await superValidate(request, zod(schema))
if (!form.valid) return fail(400, { form }); if (!form.valid) return fail(400, { form })
const { username, password } = form.data const { username, password } = form.data
const user = (await db.user.findByPrimaryIndex('username', username))?.flat(); const user = (await db.user.findByPrimaryIndex('username', username))?.flat()
if (!user) return setError(form, "user does not exist") 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?") if (!user.password)
const isvalid = verify(password, user.password); return setError(form, 'this account does not have a password, maybe try a different method?')
if (!isvalid) return setError(form, "incorrect password") const isvalid = verify(password, user.password)
const session = (await createSessionForUser(user.id)) if (!isvalid) return setError(form, 'incorrect password')
if (session.isSome()) { const session = await createSessionForUser(user.id)
const sessionCookie = cookieController.createCookie(session.unwrap().id) if (session.isSome()) {
cookies.set(sessionCookie.name, sessionCookie.value, { const sessionCookie = cookieController.createCookie(session.unwrap().id)
path: ".", cookies.set(sessionCookie.name, sessionCookie.value, {
...sessionCookie.attributes path: '.',
}) ...sessionCookie.attributes
return redirect(302, '/app') })
} else { return redirect(302, '/app')
return fail(500, { form }) } else {
} return fail(500, { form })
}, }
signup: async ({ request, cookies }) => { },
const form = await superValidate(request, zod(schema)); signup: async ({ request, cookies }) => {
if (!form.valid) return fail(400, { form }); const form = await superValidate(request, zod(schema))
const { username, password } = form.data if (!form.valid) return fail(400, { form })
const { username, password } = form.data
const userId = generateRandomString(10, alphabet("0-9", "a-z")) const userId = generateRandomString(10, alphabet('0-9', 'a-z'))
const passwordHash = hash(password) const passwordHash = hash(password)
const user = (await db.user.findByPrimaryIndex('username', username))?.flat() const user = (await db.user.findByPrimaryIndex('username', username))?.flat()
if (user) return setError(form, "username", 'username already exists') if (user) return setError(form, 'username', 'username already exists')
await db.user.set(userId, { await db.user.set(userId, {
displayName: username, displayName: username,
username, username,
id: userId, id: userId,
password: passwordHash password: passwordHash
}) })
const session = (await createSessionForUser(userId)) const session = await createSessionForUser(userId)
if (session.isSome()) { if (session.isSome()) {
const sessionCookie = cookieController.createCookie(session.unwrap().id) const sessionCookie = cookieController.createCookie(session.unwrap().id)
cookies.set(sessionCookie.name, sessionCookie.value, { cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".", path: '.',
...sessionCookie.attributes ...sessionCookie.attributes
}) })
return redirect(302, '/app') return redirect(302, '/app')
} else { } else {
return fail(500, { form }) return fail(500, { form })
} }
} }
} }

View file

@ -18,9 +18,9 @@
<div class="card bg-base-200 w-96 shadow-xl p-4"> <div class="card bg-base-200 w-96 shadow-xl p-4">
<h1 class="text-xl mb-2 font-bold">sign up/in</h1> <h1 class="text-xl mb-2 font-bold">sign up/in</h1>
<div class="join"> <div class="join">
<a href="{$page.url.pathname}/oauth/google" class="btn-disabled btn grow btn-outline join-item"> <a href="/oauth/google" class="btn grow btn-outline join-item">
<svg <svg
class="size-4 brightness-50" class="size-4"
viewBox="-3 0 262 262" viewBox="-3 0 262 262"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid" preserveAspectRatio="xMidYMid"
@ -28,32 +28,36 @@
><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g ><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g
id="SVGRepo_tracerCarrier" id="SVGRepo_tracerCarrier"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round"></g stroke-linejoin="round"
><g id="SVGRepo_iconCarrier" ></g><g id="SVGRepo_iconCarrier"
><path ><path
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027" d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622 38.755 30.023 2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
fill="#4285F4"></path fill="#4285F4"
><path ></path><path
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1" d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055-34.523 0-63.824-22.773-74.269-54.25l-1.531.13-40.298 31.187-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
fill="#34A853"></path fill="#34A853"
><path ></path><path
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782" d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82 0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602l42.356-32.782"
fill="#FBBC05"></path fill="#FBBC05"
><path ></path><path
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251" d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0 79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
fill="#EB4335"></path fill="#EB4335"
></g ></path></g
></svg> ></svg
>
</a> </a>
<a href="{$page.url.pathname}/oauth/discord" class="btn-disabled btn grow btn-outline join-item"> <a href="/oauth/discord" class="btn-disabled btn grow btn-outline join-item">
<svg class="size-4 brightness-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36" <svg
class="size-4 brightness-50"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 127.14 96.36"
><path ><path
fill="#5865f2" fill="#5865f2"
d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"
></path ></path></svg
></svg> >
</a> </a>
<a href="{$page.url.pathname}/oauth/github" class="btn-disabled btn grow btn-outline join-item group"> <a href="/oauth/github" class="btn-disabled btn grow btn-outline join-item group">
<svg <svg
class="size-5 brightness-50 text-[#24292f] dark:text-[#fff] group-hover:text-[#fff] dark:group-hover:text-[#24292f] transition-colors" class="size-5 brightness-50 text-[#24292f] dark:text-[#fff] group-hover:text-[#fff] dark:group-hover:text-[#24292f] transition-colors"
viewBox="0 0 98 96" viewBox="0 0 98 96"
@ -62,24 +66,28 @@
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="currentColor"></path fill="currentColor"
></svg> ></path></svg
>
</a> </a>
</div> </div>
<span class="divider">or</span> <span class="divider">or</span>
<form class="flex-col flex gap-y-4" method="post" use:enhance> <form class="flex-col flex gap-y-4" method="post" use:enhance>
<label <label
class="input input-bordered flex items-center gap-2" class="input input-bordered flex items-center gap-2"
class:input-error={!!$errors.username}> class:input-error={!!$errors.username}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="size-4"> class="size-4"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M11.89 4.111a5.5 5.5 0 1 0 0 7.778.75.75 0 1 1 1.06 1.061A7 7 0 1 1 15 8a2.5 2.5 0 0 1-4.083 1.935A3.5 3.5 0 1 1 11.5 8a1 1 0 0 0 2 0 5.48 5.48 0 0 0-1.61-3.889ZM10 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0Z" d="M11.89 4.111a5.5 5.5 0 1 0 0 7.778.75.75 0 1 1 1.06 1.061A7 7 0 1 1 15 8a2.5 2.5 0 0 1-4.083 1.935A3.5 3.5 0 1 1 11.5 8a1 1 0 0 0 2 0 5.48 5.48 0 0 0-1.61-3.889ZM10 8a2 2 0 1 0-4 0 2 2 0 0 0 4 0Z"
clip-rule="evenodd"></path> clip-rule="evenodd"
></path>
</svg> </svg>
<input <input
@ -88,23 +96,28 @@
name="username" name="username"
type="text" type="text"
class="grow placeholder:text-base-content/20" class="grow placeholder:text-base-content/20"
placeholder="kaii" /> placeholder="kaii"
/>
</label> </label>
<span <span
class="opacity-0 hidden transition-opacity duration-1000 text-error" class="opacity-0 hidden transition-opacity duration-1000 text-error"
class:showerror={$errors.username}>{$errors.username?.join(' & ')}</span> class:showerror={$errors.username}>{$errors.username?.join(' & ')}</span
>
<label <label
class="input input-bordered flex items-center gap-2" class="input input-bordered flex items-center gap-2"
class:input-error={!!$errors.password}> class:input-error={!!$errors.password}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="currentColor" fill="currentColor"
class="size-4"> class="size-4"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z" d="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
clip-rule="evenodd"></path> clip-rule="evenodd"
></path>
</svg> </svg>
<input <input
bind:value={password} bind:value={password}
@ -112,11 +125,13 @@
name="password" name="password"
type="password" type="password"
class="grow placeholder:text-base-content/20" class="grow placeholder:text-base-content/20"
placeholder="verygoodpassword" /> placeholder="verygoodpassword"
/>
</label> </label>
<span <span
class="opacity-0 hidden transition-opacity duration-1000 text-error" class="opacity-0 hidden transition-opacity duration-1000 text-error"
class:showerror={$errors.password}>{$errors.password}</span> class:showerror={$errors.password}>{$errors.password}</span
>
<div class="flex flex-row gap-x-4"> <div class="flex flex-row gap-x-4">
<button formaction="?/signup" type="submit" class="btn btn-secondary flex-grow"> <button formaction="?/signup" type="submit" class="btn btn-secondary flex-grow">
sign up sign up
@ -134,4 +149,4 @@
.showerror { .showerror {
@apply block opacity-100; @apply block opacity-100;
} }
</style> </style>

View file

@ -1,14 +1,14 @@
import { cookieController, deleteSession } from '$lib/server/auth'; import { cookieController, deleteSession } from '$lib/server/auth'
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'
export async function GET({ locals, cookies }) { export async function GET({ locals, cookies }) {
if(locals.session) { if (locals.session) {
await deleteSession(locals.session.id) await deleteSession(locals.session.id)
const sessionCookie = cookieController.createBlankCookie(); const sessionCookie = cookieController.createBlankCookie()
cookies.set(sessionCookie.name, sessionCookie.value, { cookies.set(sessionCookie.name, sessionCookie.value, {
path: ".", path: '.',
...sessionCookie.attributes ...sessionCookie.attributes
}); })
} }
return redirect(302, '/') return redirect(302, '/')
} }

View file

@ -1,3 +1,3 @@
import { oauth_handler } from "$lib/server/oauth"; import { oauth_handler } from '$lib/server/oauth'
export const GET = oauth_handler(); export const GET = oauth_handler()

View file

@ -0,0 +1,4 @@
import { oauth_callback, oauth_callback_actions } from '$lib/server/oauth'
export const load = oauth_callback()
export const actions = oauth_callback_actions()

View file

@ -0,0 +1,8 @@
<script>
const { data } = $props()
</script>
{#if data.type === 'create'}
do you want to create a new account using <code class="font-bold">{data.name}</code> from {data.prov}?
<button>yes</button> <button>no</button>
{:else if data.type === 'link'}{/if}

View file

@ -1,7 +1,7 @@
import adapter from 'sveltekit-adapter-deno'; import adapter from 'sveltekit-adapter-deno'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 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 // kvdex uses float16array under the hood (doesn't exist in node), filling that in here so it works during dev
globalThis.Float16Array = Float16Array globalThis.Float16Array = Float16Array
@ -23,6 +23,6 @@ const config = {
} }
}) })
} }
}; }
export default config; export default config

View file

@ -1,11 +1,10 @@
import daisyui from "daisyui" import daisyui from 'daisyui'
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./src/**/*.{html,js,svelte,ts}'], content: ['./src/**/*.{html,js,svelte,ts}'],
theme: { theme: {
extend: {}, extend: {}
}, },
plugins: [daisyui], plugins: [daisyui]
} }

View file

@ -1,5 +1,5 @@
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite'
import { defineConfig } from 'vite'; import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()], plugins: [sveltekit()],
@ -11,4 +11,4 @@ export default defineConfig({
target: ['deno1.45.5'] target: ['deno1.45.5']
} }
} }
}); })