fix csrf_token and GuildSelect

This commit is contained in:
Yusur 2025-10-23 03:09:31 +02:00
parent 58d6a248c7
commit 54cafaa1da
3 changed files with 71 additions and 19 deletions

View file

@ -25,7 +25,7 @@ let { children, title = null } = $props();
position: relative;
font: inherit;
a {
:global(a) {
color: inherit;
text-decoration: underline;
}

View file

@ -1,29 +1,46 @@
<script lang="ts">
import type { GuildEntry } from "./backend";
import { backend, type GuildEntry } from "./backend";
let value = $state("");
let suggestions: GuildEntry[] = $state([]);
let suggestions: Promise<GuildEntry[]> = $derived(getSuggestions(value));
async function getSuggestions (query: string): Promise<GuildEntry[]> {
if (!query.trim()) return [];
let readyBackend = await backend.withEvent(null).oath();
let result = await readyBackend.submitJson("suggest/guild", { query });
if (result.status !== 200) return [];
let resultJ = await result.json();
return resultJ?.has || [];
}
</script>
<div class="inset-suggestion">
<input type="text" name="to" bind:value placeholder="your user page or +guild name" autocomplete="off" />
<ul class="column">
{#each suggestions as sug}
<li><button onclick={(ev)=>{
{#await suggestions}
<!-- loading... -->
{:then suggs}
{#each suggs as sug}
<li><button class="inline" onclick={(ev)=>{
ev.preventDefault();
value = sug.name;
}}>
<faint>+</faint>{sug}
<faint>+</faint>{sug.name}
</button></li>
{:else}
<li>
<faint>+{value}</faint>
</li>
{/each}
{:catch ex}
<li>
<faint>+{value} ({ex})</faint>
</li>
{/await}
</ul>
</div>
@ -46,7 +63,7 @@ let suggestions: GuildEntry[] = $state([]);
border-bottom: 1px solid var(--border);
}
:not(:focus) + ul {
:not(:focus) + ul:not(:active) {
display: none;
}

View file

@ -44,6 +44,32 @@ export type ServerHealth = {
};
// caches an oath (CSRF token)
class OathKeeper {
lastOath : number;
csrfToken: string | null;
constructor () {
this.lastOath = 0;
this.csrfToken = null;
}
set (tok: string) {
this.lastOath = +new Date;
this.csrfToken = tok;
}
get () {
if (this.lastOath === 0 || +new Date() - this.lastOath > 1800000) {
// 30 minutes decay
return null;
}
return this.csrfToken;
}
}
const oathKeeper = new OathKeeper();
export class Backend {
static ENDPOINT_BASE = '/v1';
@ -60,33 +86,42 @@ export class Backend {
}
class EventBackend extends Backend {
event: any;
event: any | null;
constructor (event: any) {
constructor (event: any | null) {
super();
this.event = event;
}
async oath (): Promise<CsrfEventBackend> {
const resp = await this.fetch("oath");
if (resp.status !== 200) {
throw new Error();
let csrfToken = oathKeeper.get();
if (!csrfToken) {
const resp = await this.fetch("oath");
if (resp.status !== 200) {
throw new Error();
}
const respJ = await resp.json();
csrfToken = respJ.csrf_token;
}
if (!csrfToken) {
console.warn("csrf_token is null");
}
else {
oathKeeper.set(csrfToken);
}
const respJ = await resp.json();
const { csrfToken } = respJ;
return new CsrfEventBackend(this.event, csrfToken);
}
async fetch(url: string, options?: RequestInit): Promise<Response> {
console.debug(`fetch ${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`);
return await this.event.fetch(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
return await (this.event?.fetch||fetch)(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
}
}
class CsrfEventBackend extends EventBackend {
csrfToken: string;
csrfToken: string | null;
constructor(event:EventBackend, csrfToken: string) {
constructor(event: EventBackend | null, csrfToken: string | null) {
super(event);
this.csrfToken = csrfToken;
}
@ -97,7 +132,7 @@ class CsrfEventBackend extends EventBackend {
body: JSON.stringify(data),
headers: {
'content-type': 'application/json',
'x-csrftoken': this.csrfToken
'x-csrftoken': this.csrfToken || ""
}
});
}