Rewrites the web app's comments to rule 32. Task, milestone, rule and spec ids are gone; regression stories are restated as the trap they guard (public auth routes, the menu's stopPropagation, universal loads under adapter-static, metric deltas with their margin). Flutter and Tauri mentions now name the Android app or a native shell, operator quotes and dates are dropped, and one library's counts no longer appear outside test fixtures. Verified with a TypeScript-scanner token comparison (template literals and regex literals re-scanned): only comment text changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
111 lines
3.9 KiB
Svelte
111 lines
3.9 KiB
Svelte
<script lang="ts">
|
||
import { plural } from '#lib/utils/plural.js';
|
||
import { onMount } from 'svelte';
|
||
import {
|
||
getNetworkSettings,
|
||
updateNetworkSettings,
|
||
type NetworkSettings
|
||
} from '#lib/api/admin.js';
|
||
import { pushToast } from '#lib/stores/toast.svelte.js';
|
||
import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte';
|
||
import SettingRow from '#lib/components/settings/SettingRow.svelte';
|
||
|
||
// Client IP detection, on Admin › Email & network: decides which address
|
||
// is recorded for each sign-in on the Active sessions list.
|
||
const button =
|
||
'rounded-md border border-border px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary disabled:opacity-50';
|
||
|
||
let settings = $state<NetworkSettings | null>(null);
|
||
let hops = $state(1);
|
||
let saving = $state(false);
|
||
let loadError = $state(false);
|
||
|
||
const dirty = $derived(!!settings && hops !== settings.trusted_proxy_hops);
|
||
const chain = $derived(
|
||
(settings?.forwarded_chain ?? '')
|
||
.split(',')
|
||
.map((s) => s.trim())
|
||
.filter(Boolean)
|
||
);
|
||
// An admin can count their proxies from what actually arrived rather
|
||
// than guessing — one XFF entry per proxy in front of us.
|
||
const suggested = $derived(chain.length);
|
||
|
||
async function load() {
|
||
try {
|
||
settings = await getNetworkSettings();
|
||
hops = settings.trusted_proxy_hops;
|
||
loadError = false;
|
||
} catch {
|
||
loadError = true;
|
||
}
|
||
}
|
||
|
||
onMount(load);
|
||
|
||
async function save() {
|
||
saving = true;
|
||
try {
|
||
settings = await updateNetworkSettings(hops);
|
||
hops = settings.trusted_proxy_hops;
|
||
pushToast('Proxy depth saved.');
|
||
} catch {
|
||
pushToast("Couldn't save proxy depth.", 'error');
|
||
} finally {
|
||
saving = false;
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<SettingsGroup title="Proxies" id="proxies" hint="How many reverse proxies sit in front of Minstrel.">
|
||
{#if loadError}
|
||
<p class="px-4 py-3 text-sm text-error-fg">
|
||
Couldn't load network settings.
|
||
<button type="button" class="underline hover:no-underline" onclick={load}>Try again</button>
|
||
</p>
|
||
{:else if settings === null}
|
||
<p class="px-4 py-3 text-sm text-text-secondary">Loading…</p>
|
||
{:else}
|
||
<SettingRow
|
||
label="Trusted proxies"
|
||
for="proxy-hops"
|
||
hint="0 is none, 1 a proxy such as nginx or Caddy, 2 a CDN in front of it."
|
||
>
|
||
<input
|
||
id="proxy-hops"
|
||
type="number"
|
||
min="0"
|
||
max={settings.max_hops}
|
||
bind:value={hops}
|
||
class="w-20 rounded-md border border-border bg-background px-2 py-1.5 text-sm"
|
||
/>
|
||
<button type="button" class={button} disabled={saving || !dirty} onclick={save}>
|
||
{saving ? 'Saving…' : 'Save'}
|
||
</button>
|
||
</SettingRow>
|
||
{#if suggested > 0 && settings.trusted_proxy_hops !== suggested}
|
||
<p class="px-4 py-3 text-sm text-text-primary">
|
||
This request arrived with {plural(suggested, 'forwarded address', 'forwarded addresses')}, so
|
||
{plural(suggested, 'proxy is', 'proxies are')} likely.
|
||
</p>
|
||
{/if}
|
||
<!-- Verification, not decoration: "the address Minstrel sees for you"
|
||
can be checked against the machine you're at. -->
|
||
<SettingRow label="Your address right now">
|
||
<span class="font-mono text-sm">{settings.detected_client_ip || 'unknown'}</span>
|
||
</SettingRow>
|
||
<SettingRow label="Direct connection from">
|
||
<span class="font-mono text-sm">{settings.remote_addr || 'unknown'}</span>
|
||
</SettingRow>
|
||
<SettingRow label="Forwarded chain">
|
||
<span class="break-all font-mono text-sm">{settings.forwarded_chain || 'none'}</span>
|
||
</SettingRow>
|
||
<!-- X-Forwarded-For is written by whoever connects, so a depth set too
|
||
high lets a visitor pick the address their session shows. -->
|
||
<p class="px-4 py-3 text-xs text-text-secondary">
|
||
Count your proxies, don't guess high: set too high, a visitor can choose the address their
|
||
session shows.
|
||
</p>
|
||
{/if}
|
||
</SettingsGroup>
|