auth done & oauth almost done
This commit is contained in:
parent
0845eb1bbb
commit
3341ea2bf6
23 changed files with 262 additions and 60 deletions
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
|
@ -31,7 +31,7 @@ jobs:
|
|||
run: 'bun install'
|
||||
|
||||
- name: Build step
|
||||
run: 'bun build'
|
||||
run: 'bun run build'
|
||||
|
||||
- name: Upload to Deno Deploy
|
||||
uses: denoland/deployctl@v1
|
||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -19,3 +19,5 @@ Thumbs.db
|
|||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
dev
|
2
src/app.d.ts
vendored
2
src/app.d.ts
vendored
|
@ -2,7 +2,7 @@
|
|||
// for information about these interfaces
|
||||
|
||||
import type { FlatDocumentData } from "@olli/kvdex"
|
||||
import { user, session } from "$lib/db"
|
||||
import { user, session } from "$lib/server/db"
|
||||
import z from "zod"
|
||||
declare global {
|
||||
namespace App {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { cookieController, cookieExpiration, getUserAndSession } from "$lib/auth";
|
||||
import { cookieController, cookieExpiration, getUserAndSession } from "$lib/server/auth";
|
||||
import type { Handle } from "@sveltejs/kit";
|
||||
import { createDate } from "oslo";
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { type api } from "./hono"
|
||||
import type { api as API } from "./server/hono"
|
||||
import { hc } from "hono/client"
|
||||
export const client = hc<api>('/api', {
|
||||
export const api = hc<API>('/api', {
|
||||
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||
return await fetch(input, {
|
||||
...init,
|
||||
|
|
|
@ -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)
|
|
@ -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();
|
|
@ -36,8 +36,8 @@ async function getUserAndSession(
|
|||
sessionId: string
|
||||
): Promise<
|
||||
Option<{
|
||||
user: FlatDocumentData<z.infer<(typeof import('$lib/db'))['user']>, string>
|
||||
session: FlatDocumentData<z.infer<(typeof import('$lib/db'))['session']>, string>
|
||||
user: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['user']>, string>
|
||||
session: FlatDocumentData<z.infer<(typeof import('$lib/server/db'))['session']>, string>
|
||||
}>
|
||||
> {
|
||||
const session = (await db.session.find(sessionId))?.flat()
|
|
@ -25,7 +25,7 @@ export const chat = z.object({
|
|||
createdAt: z.date()
|
||||
})
|
||||
|
||||
export const kv = await openKv()
|
||||
export const kv = await openKv('http://0.0.0.0:4512')
|
||||
export const db = kvdex(kv, {
|
||||
user: collection(user, {
|
||||
idGenerator: ({ id }) => id,
|
58
src/lib/server/hono.ts
Normal file
58
src/lib/server/hono.ts
Normal 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
131
src/lib/server/oauth.ts
Normal 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 {
|
||||
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { hono } from '$lib/hono.js';
|
||||
import { hono } from '$lib/server/hono.js';
|
||||
|
||||
export async function fallback({ request, locals }) {
|
||||
return hono.fetch(request, { locals })
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { publicUser } from '$lib/db.js';
|
||||
import { publicUser } from '$lib/server/db.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load({ locals }) {
|
||||
|
|
|
@ -65,7 +65,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<div class="flex-grow flex flex-row">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toast">
|
||||
<div class="alert alert-info">
|
||||
<span>New message arrived.</span>
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,29 @@
|
|||
<script>
|
||||
import { api } from '$lib/apiclient'
|
||||
import { goto } from "$app/navigation"
|
||||
|
||||
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>
|
||||
|
||||
<div class="">
|
||||
|
||||
</div>
|
||||
<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>
|
||||
<main class="flex-grow">
|
||||
list
|
||||
</main>
|
|
@ -1,3 +1,5 @@
|
|||
<script>
|
||||
const { data } = $props()
|
||||
</script>
|
||||
</script>
|
||||
|
||||
{data.roomID}
|
3
src/routes/app/:[roomID]/+page.ts
Normal file
3
src/routes/app/:[roomID]/+page.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export async function load({ params: { roomID } }) {
|
||||
return { roomID }
|
||||
}
|
|
@ -3,8 +3,8 @@ import { fail, message, setError, superValidate } from 'sveltekit-superforms';
|
|||
import { zod } from 'sveltekit-superforms/adapters';
|
||||
import { z } from 'zod';
|
||||
import { hash, verify } from "@ts-rex/argon2"
|
||||
import { db } from '$lib/db.js';
|
||||
import { cookieController, cookieExpiration, createSessionForUser } from '$lib/auth.js';
|
||||
import { db } from '$lib/server/db.js';
|
||||
import { cookieController, cookieExpiration, createSessionForUser } from '$lib/server/auth.js';
|
||||
import { createDate } from 'oslo';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { cookieController, deleteSession } from '$lib/auth';
|
||||
import { cookieController, deleteSession } from '$lib/server/auth';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ locals, cookies }) {
|
||||
|
|
3
src/routes/oauth/[provider]/+server.ts
Normal file
3
src/routes/oauth/[provider]/+server.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { oauth_handler } from "$lib/server/oauth";
|
||||
|
||||
export const GET = oauth_handler();
|
0
src/routes/oauth/[provider]/callback/+server.ts
Normal file
0
src/routes/oauth/[provider]/callback/+server.ts
Normal file
|
@ -15,7 +15,13 @@ const config = {
|
|||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
adapter: adapter({
|
||||
buildOptions: {
|
||||
loader: {
|
||||
'.node': 'empty'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,5 +2,13 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()],
|
||||
optimizeDeps: {
|
||||
esbuildOptions: {
|
||||
loader: {
|
||||
'.node': 'empty'
|
||||
},
|
||||
target: ['deno1.45.5']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue