Compare commits

..

No commits in common. "f0ae648492e22e89bb42f0e9ae5dfbd8b5e0e8b9" and "de733a06824fdf0d15620c4e0a2b5609f245ef34" have entirely different histories.

16 changed files with 19 additions and 143 deletions

View file

@ -15,9 +15,6 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
[GeneratedRegex(@"(\{\w+\})")] [GeneratedRegex(@"(\{\w+\})")]
private static partial Regex PathVarRegex(); private static partial Regex PathVarRegex();
[GeneratedRegex(@"\{id\}")]
private static partial Regex IdCountRegex();
private static string GetCleanedTemplate(string template) private static string GetCleanedTemplate(string template)
{ {
if (template.StartsWith("api/v2")) if (template.StartsWith("api/v2"))
@ -25,19 +22,8 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
template = PathVarRegex() template = PathVarRegex()
.Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}` .Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}`
.Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}` .Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}`
// If there's at least one path parameter, we only return the *first* part of the path.
if (template.Contains("{id}")) if (template.Contains("{id}"))
{
// However, if the path starts with /users/{id} *and* there's another path parameter (such as a member ID)
// we ignore the leading /users/{id}. This is because a lot of routes are scoped by user, but should have
// separate rate limits from other user-scoped routes.
if (template.StartsWith("/users/{id}/") && IdCountRegex().Count(template) >= 2)
template = template["/users/{id}".Length..];
return template.Split("{id}")[0] + "{id}"; return template.Split("{id}")[0] + "{id}";
}
return template; return template;
} }

View file

@ -6,31 +6,11 @@ import log from "$lib/log";
export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
/**
* Optional arguments for a request. `load` and `action` functions should always pass `fetch` and `cookies`.
*/
export type RequestArgs = { export type RequestArgs = {
/**
* The token for this request. Where possible, `cookies` should be passed instead.
* Will override `cookies` if both are passed.
*/
token?: string; token?: string;
/**
* Whether this request is to an internal endpoint.
* Internal requests bypass the rate limiter and are prefixed with /api/internal/ rather than /api/v2/.
*/
isInternal?: boolean; isInternal?: boolean;
/**
* The body for this request, which will be serialized to JSON. Should be a plain JS object.
*/
body?: any; body?: any;
/**
* The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests.
*/
fetch?: typeof fetch; fetch?: typeof fetch;
/**
* The cookies object to try to get the token from. Can only be passed in loader and action functions.
*/
cookies?: Cookies; cookies?: Cookies;
}; };
@ -39,7 +19,7 @@ export type RequestArgs = {
* @param method The HTTP method for this request * @param method The HTTP method for this request
* @param path The path for this request, without the /api/v2 prefix, starting with a slash. * @param path The path for this request, without the /api/v2 prefix, starting with a slash.
* @param args Optional arguments to the request function. * @param args Optional arguments to the request function.
* @returns A Response object. * @returns A Promise object.
*/ */
export async function baseRequest( export async function baseRequest(
method: Method, method: Method,
@ -49,7 +29,7 @@ export async function baseRequest(
const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME); const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME);
const fetchFn = args.fetch ?? fetch; const fetchFn = args.fetch ?? fetch;
const url = `/${args.isInternal ? "internal" : "v2"}${path}`; const url = `${PUBLIC_API_BASE}/${args.isInternal ? "internal" : "v2"}${path}`;
log.debug("Sending request to %s %s", method, url); log.debug("Sending request to %s %s", method, url);
@ -58,7 +38,7 @@ export async function baseRequest(
...(token ? { Authorization: token } : {}), ...(token ? { Authorization: token } : {}),
}; };
return await fetchFn(PUBLIC_API_BASE + url, { return await fetchFn(url, {
method, method,
headers, headers,
body: args.body ? JSON.stringify(args.body) : undefined, body: args.body ? JSON.stringify(args.body) : undefined,

View file

@ -65,7 +65,7 @@
onclick={() => move(index, false)} onclick={() => move(index, false)}
/> />
<InputGroupText>{$t("editor.field-name")}</InputGroupText> <InputGroupText>{$t("editor.field-name")}</InputGroupText>
<input class="form-control" bind:value={name} autocomplete="off" /> <input class="form-control" bind:value={name} />
<IconButton <IconButton
color="danger" color="danger"
icon="trash3" icon="trash3"
@ -88,7 +88,6 @@
class="form-control" class="form-control"
bind:value={newEntry} bind:value={newEntry}
placeholder={$t("editor.new-entry")} placeholder={$t("editor.new-entry")}
autocomplete="off"
/> />
<IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-entry")} /> <IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-entry")} />
</form> </form>

View file

@ -40,7 +40,7 @@
tooltip={$t("editor.move-entry-down")} tooltip={$t("editor.move-entry-down")}
onclick={() => moveValue(index, false)} onclick={() => moveValue(index, false)}
/> />
<input type="text" class="form-control" bind:value={value.value} autocomplete="off" /> <input type="text" class="form-control" bind:value={value.value} />
<ButtonDropdown> <ButtonDropdown>
<span use:tippy={{ content: status.tooltip }}> <span use:tippy={{ content: status.tooltip }}>
<DropdownToggle color="secondary" caret> <DropdownToggle color="secondary" caret>

View file

@ -56,7 +56,6 @@
class="form-control" class="form-control"
bind:value={newFieldName} bind:value={newFieldName}
placeholder={$t("editor.field-name")} placeholder={$t("editor.field-name")}
autocomplete="off"
/> />
<IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-field")} /> <IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-field")} />
</form> </form>

View file

@ -50,7 +50,7 @@
tooltip={$t("editor.move-entry-down")} tooltip={$t("editor.move-entry-down")}
onclick={() => moveValue(index, true)} onclick={() => moveValue(index, true)}
/> />
<input type="text" class="form-control" bind:value={value.value} autocomplete="off" /> <input type="text" class="form-control" bind:value={value.value} />
<ButtonDropdown> <ButtonDropdown>
<span use:tippy={{ content: status.tooltip }}> <span use:tippy={{ content: status.tooltip }}>
<DropdownToggle color="secondary" caret> <DropdownToggle color="secondary" caret>
@ -88,7 +88,6 @@
type="text" type="text"
class="form-control" class="form-control"
bind:value={value.display_text} bind:value={value.display_text}
autocomplete="off"
/> />
<IconButton id="display-help" icon="question" tooltip="Help" color="secondary" /> <IconButton id="display-help" icon="question" tooltip="Help" color="secondary" />
<!-- TODO: remove children={false} once sveltestrap is updated <!-- TODO: remove children={false} once sveltestrap is updated

View file

@ -60,6 +60,6 @@
{/each} {/each}
<form class="input-group m-1" onsubmit={addEntry}> <form class="input-group m-1" onsubmit={addEntry}>
<input type="text" class="form-control" bind:value={newEntry} autocomplete="off" /> <input type="text" class="form-control" bind:value={newEntry} />
<IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-entry")} /> <IconButton type="submit" color="success" icon="plus" tooltip={$t("editor.add-entry")} />
</form> </form>

View file

@ -73,14 +73,7 @@
"extra-info-header": "Extra error information", "extra-info-header": "Extra error information",
"noscript-title": "This page requires JavaScript", "noscript-title": "This page requires JavaScript",
"noscript-info": "This page requires JavaScript to function correctly. Some buttons may not work, or the page may not work at all.", "noscript-info": "This page requires JavaScript to function correctly. Some buttons may not work, or the page may not work at all.",
"noscript-short": "Requires JavaScript", "noscript-short": "Requires JavaScript"
"404-description": "The page you were trying to visit was not found. If you're sure the page should exist, check your address bar for any typos.",
"back-to-profile-button": "Go back to your profile",
"back-to-main-page-button": "Go back to the main page",
"back-to-prev-page-button": "Go back to the previous page",
"400-description": "Something went wrong with your request. This error should never land you on this page, so it's probably a bug.",
"500-description": "Something went wrong on the server. Please try again later.",
"unknown-status-description": "Something went wrong, but we're not sure what. Please try again."
}, },
"settings": { "settings": {
"general-information-tab": "General information", "general-information-tab": "General information",
@ -110,9 +103,7 @@
"username-update-success": "Successfully changed your username!", "username-update-success": "Successfully changed your username!",
"create-member-title": "Create a new member", "create-member-title": "Create a new member",
"create-member-name-label": "Member name", "create-member-name-label": "Member name",
"auth-remove-method": "Remove", "auth-remove-method": "Remove"
"force-log-out-warning": "Make sure you're still able to log in before using this!",
"force-log-out-confirmation": "Are you sure you want to log out from all devices? If you just want to log out from this device, click the \"Log out\" button on your settings page."
}, },
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",

View file

@ -1,44 +0,0 @@
<script lang="ts">
import { page } from "$app/stores";
import { t } from "$lib/i18n";
import type { LayoutData } from "./$types";
let error = $derived($page.error!);
type Props = { data: LayoutData };
let { data }: Props = $props();
</script>
<svelte:head>
<title>{$t("title.an-error-occurred")} • pronouns.cc</title>
</svelte:head>
<div class="container">
<h3>{$t("title.an-error-occurred")}</h3>
<p>
<strong>{$page.status}</strong>: {error.message}
</p>
<p>
{#if $page.status === 400}
{$t("error.400-description")}
{:else if $page.status === 404}
{$t("error.404-description")}
{:else if $page.status === 500}
{$t("error.500-description")}
{:else}
{$t("error.unknown-status-description")}
{/if}
</p>
<div class="btn-group">
{#if data.meUser}
<a class="btn btn-primary" href="/@{data.meUser.username}">
{$t("error.back-to-profile-button")}
</a>
{:else}
<a class="btn btn-primary" href="/">{$t("error.back-to-main-page-button")}</a>
{/if}
<button class="btn btn-secondary" type="button" onclick={() => history.back()}>
{$t("error.back-to-prev-page-button")}
</button>
</div>
</div>

View file

@ -23,13 +23,7 @@
<form method="POST" action="?/changeUsername"> <form method="POST" action="?/changeUsername">
<FormGroup class="mb-3"> <FormGroup class="mb-3">
<InputGroup class="m-1 mt-3 w-md-75"> <InputGroup class="m-1 mt-3 w-md-75">
<Input <Input type="text" value={data.user.username} name="username" required />
type="text"
value={data.user.username}
name="username"
required
autocomplete="off"
/>
<Button type="submit" color="secondary">{$t("settings.change-username-button")}</Button> <Button type="submit" color="secondary">{$t("settings.change-username-button")}</Button>
</InputGroup> </InputGroup>
</FormGroup> </FormGroup>

View file

@ -1,11 +0,0 @@
import { fastRequest } from "$api";
import { clearToken } from "$lib";
import { redirect } from "@sveltejs/kit";
export const actions = {
default: async ({ fetch, cookies }) => {
await fastRequest("POST", "/auth/force-log-out", { isInternal: true, fetch, cookies }, true);
clearToken(cookies);
redirect(303, "/");
},
};

View file

@ -1,14 +0,0 @@
<script lang="ts">
import { t } from "$lib/i18n";
</script>
<h3>{$t("settings.force-log-out-title")}</h3>
<p>
{$t("settings.force-log-out-confirmation")}
<strong>{$t("settings.force-log-out-warning")}</strong>
</p>
<form method="POST">
<button type="submit" class="btn btn-danger">{$t("settings.force-log-out-button")}</button>
</form>

View file

@ -84,13 +84,7 @@
<h4>{$t("edit-profile.member-name")}</h4> <h4>{$t("edit-profile.member-name")}</h4>
<form method="POST" action="?/changeName" class="mb-3"> <form method="POST" action="?/changeName" class="mb-3">
<InputGroup> <InputGroup>
<input <input name="name" class="form-control" type="text" value={data.member.name} />
name="name"
class="form-control"
type="text"
value={data.member.name}
autocomplete="off"
/>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">
{$t("change")} {$t("change")}
</button> </button>
@ -105,7 +99,6 @@
name="display-name" name="display-name"
placeholder={data.member.name} placeholder={data.member.name}
value={data.member.display_name !== data.member.name ? data.member.display_name : null} value={data.member.display_name !== data.member.name ? data.member.display_name : null}
autocomplete="off"
/> />
<button class="btn btn-primary" type="submit">{$t("change")}</button> <button class="btn btn-primary" type="submit">{$t("change")}</button>
</InputGroup> </InputGroup>

View file

@ -16,7 +16,7 @@
<form method="POST"> <form method="POST">
<div class="my-3"> <div class="my-3">
<label class="form-label" for="name">{$t("settings.create-member-name-label")}</label> <label class="form-label" for="name">{$t("settings.create-member-name-label")}</label>
<input class="form-control" type="text" id="name" name="name" required autocomplete="off" /> <input class="form-control" type="text" id="name" name="name" required />
</div> </div>
<button class="btn btn-primary" type="submit">{$t("profile.create-member-button")}</button> <button class="btn btn-primary" type="submit">{$t("profile.create-member-button")}</button>
</form> </form>

View file

@ -114,7 +114,6 @@
name="display-name" name="display-name"
placeholder={data.user.username} placeholder={data.user.username}
value={data.user.display_name} value={data.user.display_name}
autocomplete="off"
/> />
<button class="btn btn-primary" type="submit">{$t("change")}</button> <button class="btn btn-primary" type="submit">{$t("change")}</button>
</InputGroup> </InputGroup>
@ -137,7 +136,6 @@
class="form-control" class="form-control"
value={data.user.member_title} value={data.user.member_title}
placeholder={$t("profile.default-members-header")} placeholder={$t("profile.default-members-header")}
autocomplete="off"
/> />
<p class="text-muted mt-1"> <p class="text-muted mt-1">
<Icon name="info-circle-fill" aria-hidden /> <Icon name="info-circle-fill" aria-hidden />

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
@ -90,7 +91,12 @@ func getReset(w http.ResponseWriter) int64 {
} }
func requestBucket(method, template string) string { func requestBucket(method, template string) string {
return hex.EncodeToString([]byte(method + "-" + template)) hasher := sha256.New()
_, err := hasher.Write([]byte(method + "-" + template))
if err != nil {
panic(err)
}
return hex.EncodeToString(hasher.Sum(nil))
} }
func (l *Limiter) globalLimiter(user string) *httprate.RateLimiter { func (l *Limiter) globalLimiter(user string) *httprate.RateLimiter {