Compare commits
No commits in common. "f0ae648492e22e89bb42f0e9ae5dfbd8b5e0e8b9" and "de733a06824fdf0d15620c4e0a2b5609f245ef34" have entirely different histories.
f0ae648492
...
de733a0682
16 changed files with 19 additions and 143 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
|
@ -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, "/");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue