auth done & oauth almost done

This commit is contained in:
Chad Freeman 2024-08-18 16:52:27 -04:00
parent 0845eb1bbb
commit 3341ea2bf6
23 changed files with 262 additions and 60 deletions

View file

@ -31,7 +31,7 @@ jobs:
run: 'bun install' run: 'bun install'
- name: Build step - name: Build step
run: 'bun build' run: 'bun run build'
- name: Upload to Deno Deploy - name: Upload to Deno Deploy
uses: denoland/deployctl@v1 uses: denoland/deployctl@v1

2
.gitignore vendored
View file

@ -19,3 +19,5 @@ Thumbs.db
# Vite # Vite
vite.config.js.timestamp-* vite.config.js.timestamp-*
vite.config.ts.timestamp-* vite.config.ts.timestamp-*
dev

2
src/app.d.ts vendored
View file

@ -2,7 +2,7 @@
// 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/db" import { user, session } from "$lib/server/db"
import z from "zod" import z from "zod"
declare global { declare global {
namespace App { namespace App {

View file

@ -1,4 +1,4 @@
import { cookieController, cookieExpiration, getUserAndSession } from "$lib/auth"; import { cookieController, cookieExpiration, getUserAndSession } from "$lib/server/auth";
import type { Handle } from "@sveltejs/kit"; import type { Handle } from "@sveltejs/kit";
import { createDate } from "oslo"; import { createDate } from "oslo";

View file

@ -1,6 +1,6 @@
import { type api } from "./hono" import type { api as API } from "./server/hono"
import { hc } from "hono/client" import { hc } from "hono/client"
export const client = 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,

View file

@ -1,32 +0,0 @@
import { Hono } from "hono"
import { zValidator } from "@hono/zod-validator"
import z from "zod"
import { HTTPException } from 'hono/http-exception'
import { db } from "./db"
import { alphabet, generateRandomString } from "oslo/crypto"
import { text } from "@sveltejs/kit"
type Bindings = {
locals: App.Locals
}
const api = new Hono<{ Bindings: Bindings }>()
.use(async (ctx, next) => {
if(!ctx.env.locals.session) throw new HTTPException(401)
await next()
})
.post('/rooms/create', zValidator('json', z.object({
name: z.string()
})), async ({ req, env: { locals: { user } } }) => {
const body = req.valid('json')
const roomId = generateRandomString(10, alphabet('0-9', 'a-z'))
await db.chat.data.set(roomId, {
createdAt: new Date(),
creator: user?.id!,
name: body.name
})
return text(roomId)
})
export type api = typeof api
export const hono = new Hono<{ Bindings: Bindings }>().route('/api', api)

View file

@ -1,7 +0,0 @@
import { Google, Discord, GitHub } from "arctic"
//TODO: oauth
export const google = new Google();
export const discord = new Discord();
export const github = new GitHub();

View file

@ -36,8 +36,8 @@ async function getUserAndSession(
sessionId: string sessionId: string
): Promise< ): Promise<
Option<{ Option<{
user: FlatDocumentData<z.infer<(typeof import('$lib/db'))['user']>, string> user: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['user']>, string>
session: FlatDocumentData<z.infer<(typeof import('$lib/db'))['session']>, string> session: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['session']>, string>
}> }>
> { > {
const session = (await db.session.find(sessionId))?.flat() const session = (await db.session.find(sessionId))?.flat()

View file

@ -25,7 +25,7 @@ export const chat = z.object({
createdAt: z.date() createdAt: z.date()
}) })
export const kv = await openKv() 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,

58
src/lib/server/hono.ts Normal file
View file

@ -0,0 +1,58 @@
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import z from 'zod'
import { HTTPException } from 'hono/http-exception'
import { db } from './db'
import { alphabet, generateRandomString } from 'oslo/crypto'
import { text } from '@sveltejs/kit'
import { streamSSE } from 'hono/streaming'
type Bindings = {
locals: App.Locals
}
const api = new Hono<{ Bindings: Bindings }>()
.use(async (ctx, next) => {
if (!ctx.env.locals.session) throw new HTTPException(401)
await next()
})
.post(
'/rooms/create',
zValidator(
'json',
z.object({
name: z.string()
})
),
async ({
req,
env: {
locals: { user }
}
}) => {
const body = req.valid('json')
const roomId = generateRandomString(10, alphabet('0-9', 'a-z'))
await db.chat.data.set(roomId, {
createdAt: new Date(),
creator: user?.id!,
name: body.name
})
return text(roomId)
}
)
.get('/rooms/connect/:id', (c) => {
return streamSSE(c, async (stream) => {
while (true) {
const message = `It is ${new Date().toISOString()}`
await stream.writeSSE({
data: message,
event: 'time-update',
})
await stream.sleep(1000)
}
})
})
export type api = typeof api
export const hono = new Hono<{ Bindings: Bindings }>().route('/api', api)

131
src/lib/server/oauth.ts Normal file
View file

@ -0,0 +1,131 @@
import { Google, Discord, GitHub, type OAuth2Provider, type OAuth2ProviderWithPKCE, generateState, generateCodeVerifier } from "arctic"
import { error, redirect, type Actions, type RequestHandler, type ServerLoad } from "@sveltejs/kit";
import type { z } from "zod";
import type { publicUser } from "./db";
//TODO: oauth
export const google = new Google();
export const discord = new Discord();
export const github = new GitHub();
export function oauth_handler(): RequestHandler<{ provider: string }> {
return async ({ cookies, params: { provider: providerID }, url }) => {
let provider: Google | Discord | GitHub;
let scopes: string[]
let noop = false;
switch(providerID) {
case "discord": {
provider = discord
scopes = ['identify']
break
}
case "google": {
provider = google
scopes = ['profile']
break
}
case "github": {
provider = github
scopes = []
break
}
default: {
noop = true
break
}
}
// @ts-expect-error: I know im using it before assignment that's the point
if(noop || !provider || !scopes) error(404, "provider not found")
let redir: URL;
const state = generateState();
let codeVerifier: string;
if(provider instanceof Google) {
codeVerifier = generateCodeVerifier()
redir = await provider.createAuthorizationURL(state, codeVerifier, {
scopes
})
} else {
redir = await provider.createAuthorizationURL(state, {
scopes
})
}
cookies.set("state", state, {
secure: true,
path: "/",
httpOnly: true,
maxAge: 60 * 10
});
// @ts-expect-error: I know im using it before assignment that's the point
if(codeVerifier) {
cookies.set("code_verifier", codeVerifier, {
secure: true,
path: "/",
httpOnly: true,
maxAge: 60 * 10
});
}
redirect(302, redir);
}
}
export function oauth_callback(): ServerLoad<{ provider: string }, any, { type: "create" | "link", name: string, user: z.infer<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',
}
} else {
// the user is NOT logged in, log them in, if there is no account linked to that provided user, ask them if they want to create an account
}
}
}
export function callback_actions(): Actions<{ provider: string }> {
return {
}
}

View file

@ -1,4 +1,4 @@
import { hono } from '$lib/hono.js'; import { hono } from '$lib/server/hono.js';
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,4 +1,4 @@
import { publicUser } from '$lib/db.js'; import { publicUser } from '$lib/server/db.js';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
export async function load({ locals }) { export async function load({ locals }) {

View file

@ -65,7 +65,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="flex-grow"> <div class="flex-grow flex flex-row">
{@render children()} {@render children()}
</div> </div>
</div> </div>
<div class="toast">
<div class="alert alert-info">
<span>New message arrived.</span>
</div>
</div>

View file

@ -1,7 +1,29 @@
<script> <script>
import { api } from '$lib/apiclient'
import { goto } from "$app/navigation"
const { data } = $props() 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}`)
}
</script> </script>
<div class=""> <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> </div>
<main class="flex-grow">
list
</main>

View file

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

View file

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

View file

@ -3,8 +3,8 @@ 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/db.js'; import { db } from '$lib/server/db.js';
import { cookieController, cookieExpiration, createSessionForUser } from '$lib/auth.js'; import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth.js';
import { createDate } from 'oslo'; import { createDate } from 'oslo';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';

View file

@ -1,4 +1,4 @@
import { cookieController, deleteSession } from '$lib/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 }) {

View file

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

View file

@ -15,7 +15,13 @@ const config = {
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter() adapter: adapter({
buildOptions: {
loader: {
'.node': 'empty'
}
}
})
} }
}; };

View file

@ -2,5 +2,13 @@ import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [sveltekit()],
optimizeDeps: {
esbuildOptions: {
loader: {
'.node': 'empty'
},
target: ['deno1.45.5']
}
}
}); });