data endpoint for entity 90008 (aka. a website)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add a really simple 'api token' check for public facing apis so they are a bit less abusable hopefully

dusk f081a172 566a6181

+66 -11
+8 -2
src/components/pet.svelte
··· 9 9 import { draggable } from '@neodrag/svelte'; 10 10 import { browser } from '$app/environment'; 11 11 12 + interface Props { 13 + apiToken: string; 14 + } 15 + 16 + let { apiToken }: Props = $props(); 17 + 12 18 let lastDragged = 0; 13 19 let mouseX = 0; 14 20 let mouseY = 0; ··· 78 84 let bounciness = 0.8; // How much energy is preserved on bounce 79 85 80 86 const sendBounceMetrics = () => { 81 - fetch('/_api/pet/bounce'); 87 + fetch(`/_api/pet/bounce?_token=${apiToken}`); 82 88 localBounces.set(get(localBounces) + 1); 83 89 }; 84 90 ··· 96 102 }; 97 103 98 104 const sendTotalDistance = () => { 99 - fetch('/_api/pet/distance', { 105 + fetch(`/_api/pet/distance?_token=${apiToken}`, { 100 106 method: 'POST', 101 107 body: deltaTravelledTotal.toString() 102 108 });
+2
src/hooks.server.ts
··· 82 82 valid = incrementVisitCount(event.request, event.cookies); 83 83 } 84 84 85 + // actually resolve event 85 86 const resp = await resolve(event); 86 87 // remove visitors if it was a 404 87 88 if (resp.status === 404) { 88 89 if (id !== null) removeLastVisitor(id); 89 90 if (valid) decrementVisitCount(); 90 91 } 92 + 91 93 return resp; 92 94 };
+26
src/lib/apiToken.ts
··· 1 + import { nanoid } from 'nanoid'; 2 + import { get, writable } from 'svelte/store'; 3 + 4 + const tokens = writable<Map<string, number>>(new Map()); 5 + 6 + export const newToken = () => { 7 + const token = nanoid(100); 8 + tokens.update((v) => v.set(token, Date.now())); 9 + return token; 10 + }; 11 + export const useToken = (token: string) => { 12 + const _tokens = get(tokens); 13 + // delete older tokens 14 + for (const [_token, timestamp] of _tokens) { 15 + if (Date.now() - timestamp > 30 * 60 * 1000) { 16 + _tokens.delete(_token); 17 + } 18 + } 19 + tokens.set(_tokens); 20 + return _tokens.has(token); 21 + }; 22 + 23 + export const checkUrl = (url: URL) => { 24 + const token = url.searchParams.get('_token'); 25 + return token !== null && useToken(token); 26 + };
+3 -1
src/routes/+layout.server.ts
··· 1 + import { newToken as getApiToken } from '$lib/apiToken.js'; 1 2 import { bounceCount, distanceTravelled } from '$lib/metrics.js'; 2 3 import { lastVisitors, visitCount } from '$lib/visits.js'; 3 4 import { get } from 'svelte/store'; ··· 60 61 visitCount: get(visitCount), 61 62 lastVisitors: visitors, 62 63 recentVisitCount, 63 - eyePositions 64 + eyePositions, 65 + apiToken: getApiToken() 64 66 }; 65 67 }
+1 -1
src/routes/+layout.svelte
··· 161 161 </div> 162 162 163 163 {#if !isResumePage} 164 - <Pet /> 164 + <Pet apiToken={data.apiToken} /> 165 165 {/if} 166 166 167 167 <nav class="w-full min-h-[5vh] max-h-[5vh] fixed bottom-0 z-[999] bg-ralsei-black overflow-visible">
+3
src/routes/+page.server.ts
··· 5 5 import { pushNotification } from '$lib/pushnotif'; 6 6 import { getLastActivity } from '$lib/activity.js'; 7 7 import type { RequestEvent } from '@sveltejs/kit'; 8 + import { useToken as checkApiToken } from '$lib/apiToken.js'; 8 9 9 10 export const load = async () => { 10 11 const lastTrack = getNowPlaying(); ··· 23 24 export const actions = { 24 25 default: async ({ request }: RequestEvent) => { 25 26 const form = await request.formData(); 27 + const token = form.get('_token')?.toString() ?? ''; 28 + if (!checkApiToken(token)) return; 26 29 const content = form.get('content')?.toString().substring(0, 100); 27 30 if (content === undefined) return; 28 31 pushNotification(content);
+5 -2
src/routes/+page.svelte
··· 382 382 method="post" 383 383 onsubmit={(event) => { 384 384 event.preventDefault(); 385 - const data = new FormData(event.currentTarget); 385 + const formData = new FormData(event.currentTarget); 386 386 try { 387 - fetch(`${PUBLIC_BASE_URL}/_api/pushnotif/?content=${data.get('content')}`); 387 + fetch( 388 + `${PUBLIC_BASE_URL}/_api/pushnotif/?content=${formData.get('content')}&_token=${data.apiToken}` 389 + ); 388 390 } catch (err) { 389 391 console.log(`failed to send notif: ${err}`); 390 392 } ··· 399 401 maxlength="100" 400 402 required 401 403 /> 404 + <input type="hidden" name="_token" value={data.apiToken} /> 402 405 <input 403 406 type="submit" 404 407 value="send!!"
+3 -2
src/routes/_api/pet/bounce/+server.ts
··· 1 1 import { incrementBounceCount, pushMetric } from '$lib/metrics'; 2 2 import { isBot } from '$lib/visits'; 3 + import { checkUrl as checkApiToken } from '$lib/apiToken.js'; 3 4 4 - export const GET = async ({ request }) => { 5 - if (isBot(request)) return new Response(); 5 + export const GET = async ({ request, url }) => { 6 + if (isBot(request) || !checkApiToken(url)) return new Response(); 6 7 try { 7 8 await pushMetric({ gazesys_pet_bounce_total: incrementBounceCount() }); 8 9 } catch (error) {
+3 -2
src/routes/_api/pet/distance/+server.ts
··· 1 1 import { distanceTravelled, pushMetric } from '$lib/metrics'; 2 2 import { isBot } from '$lib/visits'; 3 + import { checkUrl as checkApiToken } from '$lib/apiToken.js'; 3 4 4 - export const POST = async ({ request }) => { 5 - if (isBot(request)) return new Response(); 5 + export const POST = async ({ request, url }) => { 6 + if (isBot(request) || !checkApiToken(url)) return new Response(); 6 7 try { 7 8 const delta = parseFloat(await request.text()); 8 9 await pushMetric({ gazesys_pet_distance_total: distanceTravelled.increment(delta) });
+2
src/routes/_api/pushnotif/+server.ts
··· 1 + import { checkUrl as checkApiToken } from '$lib/apiToken.js'; 1 2 import { pushNotification } from '$lib/pushnotif'; 2 3 3 4 export const GET = async ({ url }) => { 5 + if (!checkApiToken(url)) return new Response(); 4 6 const content = url.searchParams.get('content'); 5 7 if (content === null) return new Response(); 6 8 pushNotification(content);
+8 -1
src/routes/guestbook/+page.server.ts
··· 8 8 import { noteFromBskyPost, type NoteData } from '../../components/note.svelte'; 9 9 import { get, writable } from 'svelte/store'; 10 10 import type { Post } from '@skyware/bot'; 11 + import { useToken as checkApiToken, newToken } from '$lib/apiToken.js'; 11 12 12 13 export const prerender = false; 13 14 ··· 60 61 redirect(303, callbackUrl); 61 62 } 62 63 const form = await request.formData(); 64 + const apiToken = form.get('_token')?.toString() ?? ''; 65 + if (!checkApiToken(apiToken)) { 66 + scopedCookies.set('sendError', 'api token is invalid'); 67 + redirect(303, callbackUrl); 68 + } 63 69 const content = form.get('content')?.toString().substring(0, 300); 64 70 if (content === undefined) { 65 71 scopedCookies.set('sendError', 'content field is missing'); ··· 83 89 getError: '', 84 90 sendRatelimited: scopedCookies.get('sendRatelimited') || '', 85 91 getRatelimited: false, 86 - fillText: fancyText(getVisitorId(cookies) ?? nanoid()) 92 + fillText: fancyText(getVisitorId(cookies) ?? nanoid()), 93 + apiToken: newToken() 87 94 }; 88 95 const rawPostData = scopedCookies.get('postData') || null; 89 96 const postAuth = scopedCookies.get('postAuth') || null;
+2
src/routes/guestbook/+page.svelte
··· 11 11 sendRatelimited: string; 12 12 getRatelimited: boolean; 13 13 fillText: string; 14 + apiToken: string; 14 15 }; 15 16 } 16 17 ··· 32 33 </p> 33 34 </div> 34 35 <form method="post"> 36 + <input type="hidden" name="_token" value={data.apiToken} /> 35 37 <div class="entry entryflex"> 36 38 <textarea 37 39 class="text-lg p-1 m-0 ml-0.5 bg-transparent resize-none text-shadow-white placeholder-shown:[text-shadow:none] [field-sizing:content] border-none"