feat: guestbook needs auth now
This commit is contained in:
parent
d4136e8394
commit
06c027e14e
@ -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
45
src/lib/guestbookAuth.ts
Normal 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,
|
||||||
|
}
|
@ -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
|
||||||
|
@ -6,14 +6,22 @@
|
|||||||
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 :)
|
||||||
|
<br />
|
||||||
|
just fill the post in and click on your preferred auth method to post
|
||||||
|
</p>
|
||||||
|
<p>rules: be a good human bean pretty please</p>
|
||||||
|
<p>
|
||||||
|
(note: the author name must only include alphanumerical characters or underscore, and must
|
||||||
|
be less than 32 characters)
|
||||||
|
</p>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="entry">
|
<div class="entry entryflex">
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<p class="place-self-start grow text-2xl font-monospace">###</p>
|
<p class="place-self-start grow text-2xl font-monospace">###</p>
|
||||||
<p class="justify-end self-center text-sm font-monospace">...</p>
|
<p class="justify-end self-center text-sm font-monospace">...</p>
|
||||||
@ -30,22 +38,28 @@
|
|||||||
name="author"
|
name="author"
|
||||||
placeholder="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]"
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-4 mt-4">
|
<div class="entry flex flex-wrap gap-1.5 p-1">
|
||||||
|
<p class="text-xl ms-2">auth via:</p>
|
||||||
|
{#each ['discord', 'github'] as platform}
|
||||||
<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>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
|
{#if data.sendRatelimited}
|
||||||
|
<p class="text-error">you are ratelimited, try again in 30 seconds</p>
|
||||||
|
{/if}
|
||||||
{#if data.sendError}
|
{#if data.sendError}
|
||||||
<p class="text-error">got error trying to send post, pls tell me about this</p>
|
<p class="text-error">got error trying to send post</p>
|
||||||
<details>
|
<details>
|
||||||
<summary>error</summary>
|
<summary>error</summary>
|
||||||
<p>{data.sendError}</p>
|
<p>{data.sendError}</p>
|
||||||
@ -54,9 +68,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
<div class="grow" />
|
<div class="grow" />
|
||||||
<div class="mb-[7vh] 2xl:mb-0">
|
|
||||||
<Window title="entries">
|
<Window title="entries">
|
||||||
<div class="flex flex-col gap-2 md:gap-4 2xl:w-[60ch]">
|
<div class="flex flex-col gap-2 md:gap-4 2xl:w-[60ch]">
|
||||||
{#if data.getRatelimited}
|
{#if data.getRatelimited}
|
||||||
@ -72,7 +84,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#each data.entries as [entry_id, entry] (entry_id)}
|
{#each data.entries as [entry_id, entry] (entry_id)}
|
||||||
{@const date = new Date(entry.timestamp * 1e3).toLocaleString()}
|
{@const date = new Date(entry.timestamp * 1e3).toLocaleString()}
|
||||||
<div class="entry">
|
<div class="entry entryflex">
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<p class="place-self-start grow text-2xl font-monospace">
|
<p class="place-self-start grow text-2xl font-monospace">
|
||||||
#{entry_id}
|
#{entry_id}
|
||||||
@ -80,7 +92,12 @@
|
|||||||
<p class="justify-end self-center text-sm font-monospace">{date}</p>
|
<p class="justify-end self-center text-sm font-monospace">{date}</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg ml-0.5">{entry.content}</p>
|
<p class="text-lg ml-0.5">{entry.content}</p>
|
||||||
<p class="place-self-end text-sm font-monospace">--- posted by {entry.author}</p>
|
<p
|
||||||
|
class="place-self-end text-sm font-monospace max-w-[16ch] md:max-w-[24ch] lg:max-w-[32ch] overflow-hidden text-ellipsis"
|
||||||
|
title={entry.author}
|
||||||
|
>
|
||||||
|
--- posted by {entry.author}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p>looks like there are no entries :(</p>
|
<p>looks like there are no entries :(</p>
|
||||||
@ -103,11 +120,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Window>
|
</Window>
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
Loading…
Reference in New Issue
Block a user