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'
|
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
2
.gitignore
vendored
|
@ -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
2
src/app.d.ts
vendored
|
@ -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 {
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
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()
|
|
@ -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
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 }) {
|
export async function fallback({ request, locals }) {
|
||||||
return hono.fetch(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';
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load({ locals }) {
|
export async function load({ locals }) {
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,3 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
const { data } = $props()
|
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 { 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';
|
||||||
|
|
||||||
|
|
|
@ -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 }) {
|
||||||
|
|
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.
|
// 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue