diff --git a/bun.lockb b/bun.lockb index 44f564e..57a9f2a 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index e41facc..5d9d264 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "type": "module", "dependencies": { "@std/toml": "npm:@jsr/std__toml", + "arctic": "^2.0.0-next.5", "rehype-autolink-headings": "^7.1.0", "rehype-slug": "^6.0.0" }, diff --git a/src/lib/guestbookAuth.ts b/src/lib/guestbookAuth.ts new file mode 100644 index 0000000..0c2ba77 --- /dev/null +++ b/src/lib/guestbookAuth.ts @@ -0,0 +1,45 @@ +import { dev } from "$app/environment"; +import { DISCORD_CLIENT_ID, GITHUB_CLIENT_ID } from "$env/static/private"; +import { PUBLIC_BASE_URL } from "$env/static/public"; +import type { Cookies } from "@sveltejs/kit"; +import { Discord, generateState, GitHub } from "arctic"; + +export const callbackUrl = `${PUBLIC_BASE_URL}/guestbook/` + +export const discord = new Discord(DISCORD_CLIENT_ID, "", callbackUrl) +export const github = new GitHub(GITHUB_CLIENT_ID, "", callbackUrl) + +export const createAuthUrl = (authCb: (state: string) => URL, cookies: Cookies) => { + const state = generateState() + const url = authCb(state) + cookies.set("state", state, { + secure: !dev, + path: "/guestbook/", + httpOnly: true, + maxAge: 60 * 10, + }) + return url +} + +export const extractCode = (url: URL, cookies: Cookies) => { + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + const storedState = cookies.get("state"); + + if (code === null || state === null) { + return null + } + if (state !== storedState) { + throw new Error("Invalid OAuth request"); + } + + return code +} + +export default { + callbackUrl, + discord, github, + createAuthUrl, + extractCode, +} \ No newline at end of file diff --git a/src/routes/guestbook/+page.server.ts b/src/routes/guestbook/+page.server.ts index c7e48f7..b5d3b7a 100644 --- a/src/routes/guestbook/+page.server.ts +++ b/src/routes/guestbook/+page.server.ts @@ -1,43 +1,97 @@ import { GUESTBOOK_BASE_URL } from '$env/static/private' -import { PUBLIC_BASE_URL } from '$env/static/public' -import { redirect } from '@sveltejs/kit' +import { redirect, type Cookies } from '@sveltejs/kit' +import auth from '$lib/guestbookAuth' interface Entry { - author: String, - content: String, + author: string, + content: string, timestamp: number, } -export const actions = { - default: async ({ request, cookies }) => { - const body = await request.text() - let respRaw: Response - try { - respRaw = await fetch(`${GUESTBOOK_BASE_URL}`, { method: 'POST', body }) - } catch (err: any) { - cookies.set("sendError", err.toString(), { path: "/guestbook" }) - redirect(303, `${PUBLIC_BASE_URL}/guestbook/`) +const scopeCookies = (cookies: Cookies) => { + return { + get: (key: string) => { + return cookies.get(key) + }, + set: (key: string, value: string, props: import('cookie').CookieSerializeOptions = {}) => { + cookies.set(key, value, { ...props, path: "/guestbook/" }) + }, + delete: (key: string, props: import('cookie').CookieSerializeOptions = {}) => { + cookies.delete(key, { ...props, path: "/guestbook/" }) } - if (respRaw.status === 429) { - cookies.set("sendRatelimited", "true", { path: "/guestbook" }) - } - redirect(303, `${PUBLIC_BASE_URL}/guestbook/`) } } +const postAction = (client: any, scopes: string[]) => { + return async ({ request, cookies }: { request: Request, cookies: Cookies }) => { + const form = await request.formData() + const author = form.get("author")?.toString().replace(/([^_a-z0-9]+)/gi, '') + const content = form.get("content")?.toString() + const scopedCookies = scopeCookies(cookies) + if (author === undefined || content === undefined) { + scopedCookies.set("sendError", "one of author or content fields are missing") + redirect(303, auth.callbackUrl) + } + if (['dusk', 'yusdacra'].includes(author.trim())) { + scopedCookies.set("sendError", "author cannot be dusk or yusdacra (those are my names choose something else smh)") + redirect(303, auth.callbackUrl) + } + // save form content in a cookie + const params = new URLSearchParams({ author, content }) + scopedCookies.set("postData", params.toString()) + // get auth url to redirect user to + const authUrl = auth.createAuthUrl((state) => client.createAuthorizationURL(state, scopes), cookies) + redirect(303, authUrl) + } +} + +export const actions = { + post_discord: postAction(auth.discord, ["identify"]), + post_github: postAction(auth.github, []), +} + export async function load({ url, fetch, cookies }) { + const scopedCookies = scopeCookies(cookies) var data = { entries: [] as [number, Entry][], page: parseInt(url.searchParams.get('page') || "1"), hasNext: false, - sendError: cookies.get("sendError") || "", + sendError: scopedCookies.get("sendError") || "", getError: "", - sendRatelimited: cookies.get('sendRatelimited') || "", + sendRatelimited: scopedCookies.get('sendRatelimited') || "", getRatelimited: false, } + const rawPostData = scopedCookies.get("postData") || null + if (rawPostData !== null) { + // delete the postData cookie after we got it cause we dont need it anymore + scopedCookies.delete("postData") + // check if we are landing from an auth from a post action + let code: string | null = null + // try to get the code, fails if invalid oauth request + try { + code = auth.extractCode(url, cookies) + } catch (err: any) { + data.sendError = err.toString() + } + // if we do have a code, then actually make the put request to guestbook server + if (code !== null) { + let respRaw: Response + try { + const postData = new URLSearchParams(rawPostData) + respRaw = await fetch(`${GUESTBOOK_BASE_URL}`, { method: 'POST', body: postData }) + } catch (err: any) { + scopedCookies.set("sendError", `${err.toString()} (is guestbook server running?)`) + redirect(303, auth.callbackUrl) + } + if (respRaw.status === 429) { + scopedCookies.set("sendRatelimited", "true") + } + redirect(303, auth.callbackUrl) + } + } // delete the cookies after we get em since we dont really need these more than once - cookies.delete("sendError", { path: "/guestbook" }) - cookies.delete("sendRatelimited", { path: "/guestbook" }) + scopedCookies.delete("sendError") + scopedCookies.delete("sendRatelimited") // handle cases where the page query might be a string so we just return back page 1 instead data.page = isNaN(data.page) ? 1 : data.page data.page = Math.max(data.page, 1) @@ -45,7 +99,7 @@ export async function load({ url, fetch, cookies }) { try { respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page) } catch (err: any) { - data.getError = err.toString() + data.getError = `${err.toString()} (is guestbook server running?)` return data } data.getRatelimited = respRaw.status === 429 diff --git a/src/routes/guestbook/+page.svelte b/src/routes/guestbook/+page.svelte index a170056..50b96be 100644 --- a/src/routes/guestbook/+page.svelte +++ b/src/routes/guestbook/+page.svelte @@ -6,108 +6,127 @@ const hasNextPage = data.hasNext; -
-
- -
-

hia, here is the guestbook if you wanna post anything :)

-

just be a good human bean pretty please

-
-
-
-

###

-

...

-
-