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:
project: 'spiel-place'
entrypoint: 'mod.ts'
root: 'build'
root: 'build'

BIN
bun.lockb

Binary file not shown.

View file

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

View file

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

12
src/app.d.ts vendored
View file

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

View file

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

View file

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

View file

@ -1,14 +1,23 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import type { Snippet } from 'svelte'
let { tab = $bindable(), disabled = $bindable(false), map, ...props }: {
tab: string | undefined,
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]));
let {
tab = $bindable(),
disabled = $bindable(false),
map,
...props
}: {
tab: string | undefined
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>
<div role="tablist" class="tabs tabs-boxed {props.class}">
@ -16,6 +25,14 @@
<!-- svelte-ignore a11y_interactive_supports_focus -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- 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}
</div>
</div>

View file

@ -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>('/api', {
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return await fetch(input, {
...init,
credentials: 'include',
})
}
})
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
return await fetch(input, {
...init,
credentials: 'include'
})
}
})

View file

@ -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<void> {
await db.session.delete(sessionId)
}
async function getUserAndSession(
sessionId: string
): Promise<
async function getUserAndSession(sessionId: string): Promise<
Option<{
user: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['user']>, string>
session: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['session']>, 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 }

View file

@ -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<true>()),
data: collection(chat)
}
})
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<true>()),
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)
}
)
.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

View file

@ -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<typeof publicUser> }> {
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<typeof publicUser>
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 {
}
}
export function oauth_callback_actions(): Actions<{ provider: string }> {
return {}
}

View file

@ -1,6 +1,6 @@
<script>
import "../app.pcss"
const { children } = $props()
import '../app.pcss'
const { children } = $props()
</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() {
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 }) {
return hono.fetch(request, { locals })
}
return hono.fetch(request, { locals })
}

View file

@ -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;
export const csr = true
export const ssr = false

View file

@ -5,73 +5,92 @@
</script>
<div class="h-[100vh] w-[100vw] flex flex-col">
<div class="px-4">
<div class="navbar bg-base-200 rounded-b-xl">
<div class="flex-1">
<button class="btn btn-ghost text-xl">spiel.place</button>
</div>
<div class="flex-none">
<details
class="dropdown dropdown-end"
ontoggle={({ newState }) => (dropdownOpen = newState === 'open')}>
<summary class="btn btn-circle btn-ghost m-1 swap swap-rotate">
<input type="checkbox" bind:checked={dropdownOpen} />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5 swap-off">
<path
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"
clip-rule="evenodd"></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5 swap-on stroke-2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
</svg>
</summary>
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li><a href="/app/settings">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<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" />
</svg>
{data.displayName}
</a></li>
<li>
<a href="/auth/signout">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
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 class="px-4">
<div class="navbar bg-base-200 rounded-b-xl">
<div class="flex-1">
<button class="btn btn-ghost text-xl">spiel.place</button>
</div>
<div class="flex-none">
<details
class="dropdown dropdown-end"
ontoggle={({ newState }) => (dropdownOpen = newState === 'open')}
>
<summary class="btn btn-circle btn-ghost m-1 swap swap-rotate">
<input type="checkbox" bind:checked={dropdownOpen} />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="size-5 swap-off"
>
<path
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"
clip-rule="evenodd"
></path>
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5 swap-on stroke-2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"></path>
</svg>
</summary>
<ul class="menu dropdown-content bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li>
<a href="/app/settings">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<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"
></path>
</svg>
{data.displayName}
</a>
</li>
<li>
<a href="/auth/signout">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
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 class="toast">
<div class="alert alert-info">
<span>New message arrived.</span>
</div>
</div>
<div class="alert alert-info">
<span>New message arrived.</span>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

@ -18,9 +18,9 @@
<div class="card bg-base-200 w-96 shadow-xl p-4">
<h1 class="text-xl mb-2 font-bold">sign up/in</h1>
<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
class="size-4 brightness-50"
class="size-4"
viewBox="-3 0 262 262"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid"
@ -28,32 +28,36 @@
><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"></g
><g id="SVGRepo_iconCarrier"
stroke-linejoin="round"
></g><g id="SVGRepo_iconCarrier"
><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"
fill="#4285F4"></path
><path
fill="#4285F4"
></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"
fill="#34A853"></path
><path
fill="#34A853"
></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"
fill="#FBBC05"></path
><path
fill="#FBBC05"
></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"
fill="#EB4335"></path
></g
></svg>
fill="#EB4335"
></path></g
></svg
>
</a>
<a href="{$page.url.pathname}/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"
<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"
><path
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"
></path
></svg>
></path></svg
>
</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
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"
@ -62,24 +66,28 @@
fill-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"
fill="currentColor"></path
></svg>
fill="currentColor"
></path></svg
>
</a>
</div>
<span class="divider">or</span>
<form class="flex-col flex gap-y-4" method="post" use:enhance>
<label
class="input input-bordered flex items-center gap-2"
class:input-error={!!$errors.username}>
class:input-error={!!$errors.username}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4">
class="size-4"
>
<path
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"
clip-rule="evenodd"></path>
clip-rule="evenodd"
></path>
</svg>
<input
@ -88,23 +96,28 @@
name="username"
type="text"
class="grow placeholder:text-base-content/20"
placeholder="kaii" />
placeholder="kaii"
/>
</label>
<span
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
class="input input-bordered flex items-center gap-2"
class:input-error={!!$errors.password}>
class:input-error={!!$errors.password}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-4">
class="size-4"
>
<path
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"
clip-rule="evenodd"></path>
clip-rule="evenodd"
></path>
</svg>
<input
bind:value={password}
@ -112,11 +125,13 @@
name="password"
type="password"
class="grow placeholder:text-base-content/20"
placeholder="verygoodpassword" />
placeholder="verygoodpassword"
/>
</label>
<span
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">
<button formaction="?/signup" type="submit" class="btn btn-secondary flex-grow">
sign up
@ -134,4 +149,4 @@
.showerror {
@apply block opacity-100;
}
</style>
</style>

View file

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

View file

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

View file

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