Lets operators run a closed-membership deployment by setting
ALLOW_SELF_REGISTER=false (default true, so existing deploys are
unaffected). When off, POST /auth/register returns 403 forbidden. The
rate-limit token is consumed BEFORE the disabled check so the timing
doesn't distinguish enabled-but-rejected from disabled — closes the
toggle-state probe channel.
New public GET /auth/config returns { self_register_enabled: bool }
so the frontend can render its register affordances correctly
without conflating "disabled" with "rate-limited" (which a probe
attempt would).
Frontend: a lightweight reactive `authConfig` store loads the flag
once on root-layout mount (and again on /register direct navigation,
which bypasses the layout's onMount). Header hides the Register link
when the toggle is off; /register renders a "self-registration is
disabled — ask an administrator" notice instead of the form.
Admin-create endpoint that pairs with this toggle is intentionally
not in this PR — it lands as the next branch (feat/admin-user-create).
The toggle alone is independently useful for deployments that want
to lock down enrollment without yet wiring an admin UI.
120 lines
3.3 KiB
Svelte
120 lines
3.3 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { goto } from '$app/navigation';
|
|
import { register } from '$lib/api/auth';
|
|
import { authConfig } from '$lib/auth-config.svelte';
|
|
import { session } from '$lib/session.svelte';
|
|
|
|
let username = $state('');
|
|
let password = $state('');
|
|
let error: string | null = $state(null);
|
|
let submitting = $state(false);
|
|
|
|
// Direct navigation to /register bypasses the root layout's
|
|
// onMount — re-trigger the config load here so the disabled state
|
|
// renders correctly even when this is the first page hit.
|
|
onMount(() => {
|
|
if (!authConfig.loaded) authConfig.load();
|
|
});
|
|
|
|
async function submit(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
error = null;
|
|
submitting = true;
|
|
try {
|
|
const user = await register({ username, password });
|
|
session.setUser(user);
|
|
await goto('/');
|
|
} catch (e) {
|
|
error = (e as Error).message;
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<h1>Register</h1>
|
|
{#if authConfig.loaded && !authConfig.self_register_enabled}
|
|
<p class="notice" role="status" data-testid="register-disabled">
|
|
Self-registration is disabled on this server. Ask an administrator to
|
|
create an account for you.
|
|
</p>
|
|
{:else}
|
|
<form onsubmit={submit} action="javascript:void(0)" data-testid="register-form">
|
|
<label class="form-field">
|
|
<span>Username</span>
|
|
<input
|
|
type="text"
|
|
bind:value={username}
|
|
autocomplete="username"
|
|
minlength="3"
|
|
maxlength="32"
|
|
required
|
|
data-testid="register-username"
|
|
/>
|
|
</label>
|
|
<label class="form-field">
|
|
<span>Password</span>
|
|
<input
|
|
type="password"
|
|
bind:value={password}
|
|
autocomplete="new-password"
|
|
minlength="8"
|
|
required
|
|
data-testid="register-password"
|
|
/>
|
|
</label>
|
|
<button class="primary" type="submit" disabled={submitting} data-testid="register-submit">
|
|
{submitting ? 'Registering…' : 'Register'}
|
|
</button>
|
|
{#if error}
|
|
<p class="form-error" role="alert" data-testid="register-error">{error}</p>
|
|
{/if}
|
|
</form>
|
|
{/if}
|
|
<p class="hint">
|
|
Already have an account? <a href="/login">Log in</a>.
|
|
</p>
|
|
|
|
<style>
|
|
form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
max-width: 24rem;
|
|
}
|
|
|
|
.primary {
|
|
background: var(--primary);
|
|
color: var(--primary-contrast);
|
|
border-color: var(--primary);
|
|
margin-top: var(--space-1);
|
|
}
|
|
|
|
.primary:hover:not(:disabled) {
|
|
background: var(--primary-hover);
|
|
border-color: var(--primary-hover);
|
|
}
|
|
|
|
.form-error {
|
|
color: var(--danger);
|
|
font-size: var(--font-sm);
|
|
}
|
|
|
|
.hint {
|
|
margin-top: var(--space-3);
|
|
color: var(--text-muted);
|
|
font-size: var(--font-sm);
|
|
}
|
|
|
|
.notice {
|
|
padding: var(--space-3);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--surface-elevated);
|
|
color: var(--text);
|
|
max-width: 32rem;
|
|
margin: 0;
|
|
}
|
|
</style>
|