feat: guestbook needs auth now

This commit is contained in:
dusk 2024-08-24 01:55:27 +03:00
parent d4136e8394
commit 06c027e14e
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
5 changed files with 230 additions and 111 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -39,6 +39,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@std/toml": "npm:@jsr/std__toml", "@std/toml": "npm:@jsr/std__toml",
"arctic": "^2.0.0-next.5",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-slug": "^6.0.0" "rehype-slug": "^6.0.0"
}, },

45
src/lib/guestbookAuth.ts Normal file
View File

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

View File

@ -1,43 +1,97 @@
import { GUESTBOOK_BASE_URL } from '$env/static/private' import { GUESTBOOK_BASE_URL } from '$env/static/private'
import { PUBLIC_BASE_URL } from '$env/static/public' import { redirect, type Cookies } from '@sveltejs/kit'
import { redirect } from '@sveltejs/kit' import auth from '$lib/guestbookAuth'
interface Entry { interface Entry {
author: String, author: string,
content: String, content: string,
timestamp: number, timestamp: number,
} }
export const actions = { const scopeCookies = (cookies: Cookies) => {
default: async ({ request, cookies }) => { return {
const body = await request.text() get: (key: string) => {
let respRaw: Response return cookies.get(key)
try { },
respRaw = await fetch(`${GUESTBOOK_BASE_URL}`, { method: 'POST', body }) set: (key: string, value: string, props: import('cookie').CookieSerializeOptions = {}) => {
} catch (err: any) { cookies.set(key, value, { ...props, path: "/guestbook/" })
cookies.set("sendError", err.toString(), { path: "/guestbook" }) },
redirect(303, `${PUBLIC_BASE_URL}/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 }) { export async function load({ url, fetch, cookies }) {
const scopedCookies = scopeCookies(cookies)
var data = { var data = {
entries: [] as [number, Entry][], entries: [] as [number, Entry][],
page: parseInt(url.searchParams.get('page') || "1"), page: parseInt(url.searchParams.get('page') || "1"),
hasNext: false, hasNext: false,
sendError: cookies.get("sendError") || "", sendError: scopedCookies.get("sendError") || "",
getError: "", getError: "",
sendRatelimited: cookies.get('sendRatelimited') || "", sendRatelimited: scopedCookies.get('sendRatelimited') || "",
getRatelimited: false, 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 // delete the cookies after we get em since we dont really need these more than once
cookies.delete("sendError", { path: "/guestbook" }) scopedCookies.delete("sendError")
cookies.delete("sendRatelimited", { path: "/guestbook" }) scopedCookies.delete("sendRatelimited")
// handle cases where the page query might be a string so we just return back page 1 instead // 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 = isNaN(data.page) ? 1 : data.page
data.page = Math.max(data.page, 1) data.page = Math.max(data.page, 1)
@ -45,7 +99,7 @@ export async function load({ url, fetch, cookies }) {
try { try {
respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page) respRaw = await fetch(GUESTBOOK_BASE_URL + "/" + data.page)
} catch (err: any) { } catch (err: any) {
data.getError = err.toString() data.getError = `${err.toString()} (is guestbook server running?)`
return data return data
} }
data.getRatelimited = respRaw.status === 429 data.getRatelimited = respRaw.status === 429

View File

@ -6,108 +6,127 @@
const hasNextPage = data.hasNext; const hasNextPage = data.hasNext;
</script> </script>
<div class="flex flex-col md:flex-row gap-2 md:gap-4"> <div class="flex flex-col-reverse md:flex-row gap-2 md:gap-4">
<div> <div class="mb-[7vh] md:hidden" />
<Window title="guestbook"> <Window title="guestbook">
<div class="flex flex-col gap-4 2xl:w-[60ch]"> <div class="flex flex-col gap-4 2xl:w-[60ch]">
<p>hia, here is the guestbook if you wanna post anything :)</p> <p>
<p>just be a good human bean pretty please</p> hia, here is the guestbook if you wanna post anything :)
<form method="post"> <br />
<div class="entry"> just fill the post in and click on your preferred auth method to post
<div class="flex flex-row"> </p>
<p class="place-self-start grow text-2xl font-monospace">###</p> <p>rules: be a good human bean pretty please</p>
<p class="justify-end self-center text-sm font-monospace">...</p> <p>
</div> (note: the author name must only include alphanumerical characters or underscore, and must
<textarea be less than 32 characters)
class="text-lg ml-0.5 bg-inherit resize-none text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]" </p>
name="content" <form method="post">
placeholder="say meow!" <div class="entry entryflex">
<div class="flex flex-row">
<p class="place-self-start grow text-2xl font-monospace">###</p>
<p class="justify-end self-center text-sm font-monospace">...</p>
</div>
<textarea
class="text-lg ml-0.5 bg-inherit resize-none text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]"
name="content"
placeholder="say meow!"
required
/>
<p class="place-self-end text-sm font-monospace">
--- posted by <input
type="text"
name="author"
placeholder="author"
class="p-0 bg-inherit border-hidden max-w-[16ch] text-right text-sm text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]"
pattern="[_a-zA-Z0-9]+"
maxlength="32"
required required
/> />
<p class="place-self-end text-sm font-monospace"> </p>
--- posted by <input </div>
type="text" <div class="entry flex flex-wrap gap-1.5 p-1">
name="author" <p class="text-xl ms-2">auth via:</p>
placeholder="author" {#each ['discord', 'github'] as platform}
class="p-0 bg-inherit border-hidden max-w-[16ch] text-right text-sm text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content]"
required
/>
</p>
</div>
<div class="flex gap-4 mt-4">
<input <input
type="submit" type="submit"
value="post" value={platform}
class="text-xl text-ralsei-green-light leading-5 motion-safe:hover:animate-bounce w-fit border-double border-4 p-1 pb-2" formaction="?/post_{platform}"
class="text-lg text-ralsei-green-light leading-5 motion-safe:hover:animate-bounce w-fit p-0.5"
/> />
{#if data.sendRatelimited} {/each}
<p class="text-error self-center">you are ratelimited, try again in 30 seconds</p> </div>
{/if} {#if data.sendRatelimited}
</div> <p class="text-error">you are ratelimited, try again in 30 seconds</p>
{#if data.sendError} {/if}
<p class="text-error">got error trying to send post, pls tell me about this</p> {#if data.sendError}
<details> <p class="text-error">got error trying to send post</p>
<summary>error</summary>
<p>{data.sendError}</p>
</details>
{/if}
</form>
</div>
</Window>
</div>
<div class="grow" />
<div class="mb-[7vh] 2xl:mb-0">
<Window title="entries">
<div class="flex flex-col gap-2 md:gap-4 2xl:w-[60ch]">
{#if data.getRatelimited}
<p class="text-error">
woops, looks like you are being ratelimited, try again in like half a minute :3
</p>
{:else if data.getError}
<p class="text-error">got error trying to fetch entries, pls tell me about this</p>
<details> <details>
<summary>error</summary> <summary>error</summary>
<p>{data.getError}</p> <p>{data.sendError}</p>
</details> </details>
{:else} {/if}
{#each data.entries as [entry_id, entry] (entry_id)} </form>
{@const date = new Date(entry.timestamp * 1e3).toLocaleString()} </div>
<div class="entry"> </Window>
<div class="flex flex-row"> <div class="grow" />
<p class="place-self-start grow text-2xl font-monospace"> <Window title="entries">
#{entry_id} <div class="flex flex-col gap-2 md:gap-4 2xl:w-[60ch]">
</p> {#if data.getRatelimited}
<p class="justify-end self-center text-sm font-monospace">{date}</p> <p class="text-error">
</div> woops, looks like you are being ratelimited, try again in like half a minute :3
<p class="text-lg ml-0.5">{entry.content}</p> </p>
<p class="place-self-end text-sm font-monospace">--- posted by {entry.author}</p> {:else if data.getError}
<p class="text-error">got error trying to fetch entries, pls tell me about this</p>
<details>
<summary>error</summary>
<p>{data.getError}</p>
</details>
{:else}
{#each data.entries as [entry_id, entry] (entry_id)}
{@const date = new Date(entry.timestamp * 1e3).toLocaleString()}
<div class="entry entryflex">
<div class="flex flex-row">
<p class="place-self-start grow text-2xl font-monospace">
#{entry_id}
</p>
<p class="justify-end self-center text-sm font-monospace">{date}</p>
</div> </div>
{:else} <p class="text-lg ml-0.5">{entry.content}</p>
<p>looks like there are no entries :(</p> <p
{/each} class="place-self-end text-sm font-monospace max-w-[16ch] md:max-w-[24ch] lg:max-w-[32ch] overflow-hidden text-ellipsis"
{/if} title={entry.author}
{#if hasPreviousPage || hasNextPage} >
<div class="flex flex-row w-full justify-center items-center font-monospace"> --- posted by {entry.author}
{#if hasPreviousPage} </p>
<a href="/guestbook/?page={data.entries.length < 0 ? data.page - 1 : 1}"
>&lt;&lt; previous</a
>
{/if}
{#if hasNextPage && hasPreviousPage}
<div class="w-1/12" />
{/if}
{#if hasNextPage}
<a href="/guestbook/?page={data.page + 1}">next &gt;&gt;</a>
{/if}
</div> </div>
{/if} {:else}
</div> <p>looks like there are no entries :(</p>
</Window> {/each}
</div> {/if}
{#if hasPreviousPage || hasNextPage}
<div class="flex flex-row w-full justify-center items-center font-monospace">
{#if hasPreviousPage}
<a href="/guestbook/?page={data.entries.length < 0 ? data.page - 1 : 1}"
>&lt;&lt; previous</a
>
{/if}
{#if hasNextPage && hasPreviousPage}
<div class="w-1/12" />
{/if}
{#if hasNextPage}
<a href="/guestbook/?page={data.page + 1}">next &gt;&gt;</a>
{/if}
</div>
{/if}
</div>
</Window>
</div> </div>
<style lang="postcss"> <style lang="postcss">
.entry { .entry {
@apply flex flex-col gap-3 py-2 px-3 bg-ralsei-green-dark/70 border-ralsei-green-light/30 border-x-[3px] border-y-4; @apply bg-ralsei-green-dark/70 border-ralsei-green-light/30 border-x-[3px] border-y-4;
}
.entryflex {
@apply flex flex-col gap-3 py-2 px-3;
} }
</style> </style>