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+\})")]
private static partial Regex PathVarRegex();
[GeneratedRegex(@"\{id\}")]
private static partial Regex IdCountRegex();
private static string GetCleanedTemplate(string template)
{
if (template.StartsWith("api/v2"))
@ -25,19 +22,8 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
template = PathVarRegex()
.Replace(template, "{id}") // Replace all path variables (almost always IDs) 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}"))
{
// 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;
}

View file

@ -6,31 +6,11 @@ import log from "$lib/log";
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 = {
/**
* The token for this request. Where possible, `cookies` should be passed instead.
* Will override `cookies` if both are passed.
*/
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;
/**
* The body for this request, which will be serialized to JSON. Should be a plain JS object.
*/
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;
/**
* The cookies object to try to get the token from. Can only be passed in loader and action functions.
*/
cookies?: Cookies;
};
@ -39,7 +19,7 @@ export type RequestArgs = {
* @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 args Optional arguments to the request function.
* @returns A Response object.
* @returns A Promise object.
*/
export async function baseRequest(
method: Method,
@ -49,7 +29,7 @@ export async function baseRequest(
const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME);
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);
@ -58,7 +38,7 @@ export async function baseRequest(
...(token ? { Authorization: token } : {}),
};
return await fetchFn(PUBLIC_API_BASE + url, {
return await fetchFn(url, {
method,
headers,
body: args.body ? JSON.stringify(args.body) : undefined,

View file

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

View file

@ -40,7 +40,7 @@
tooltip={$t("editor.move-entry-down")}
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>
<span use:tippy={{ content: status.tooltip }}>
<DropdownToggle color="secondary" caret>

View file

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

View file

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

View file

@ -60,6 +60,6 @@
{/each}
<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")} />
</form>

View file

@ -73,14 +73,7 @@
"extra-info-header": "Extra error information",
"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-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."
"noscript-short": "Requires JavaScript"
},
"settings": {
"general-information-tab": "General information",
@ -110,9 +103,7 @@
"username-update-success": "Successfully changed your username!",
"create-member-title": "Create a new member",
"create-member-name-label": "Member name",
"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."
"auth-remove-method": "Remove"
},
"yes": "Yes",
"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">
<FormGroup class="mb-3">
<InputGroup class="m-1 mt-3 w-md-75">
<Input
type="text"
value={data.user.username}
name="username"
required
autocomplete="off"
/>
<Input type="text" value={data.user.username} name="username" required />
<Button type="submit" color="secondary">{$t("settings.change-username-button")}</Button>
</InputGroup>
</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>
<form method="POST" action="?/changeName" class="mb-3">
<InputGroup>
<input
name="name"
class="form-control"
type="text"
value={data.member.name}
autocomplete="off"
/>
<input name="name" class="form-control" type="text" value={data.member.name} />
<button type="submit" class="btn btn-primary">
{$t("change")}
</button>
@ -105,7 +99,6 @@
name="display-name"
placeholder={data.member.name}
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>
</InputGroup>

View file

@ -16,7 +16,7 @@
<form method="POST">
<div class="my-3">
<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>
<button class="btn btn-primary" type="submit">{$t("profile.create-member-button")}</button>
</form>

View file

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

View file

@ -1,6 +1,7 @@
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
@ -90,7 +91,12 @@ func getReset(w http.ResponseWriter) int64 {
}
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 {