feat(auth): ALLOW_SELF_REGISTER toggle + public /auth/config endpoint (0.42.0)
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.
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
||||
me,
|
||||
changePassword,
|
||||
createToken,
|
||||
deleteToken
|
||||
deleteToken,
|
||||
getAuthConfig
|
||||
} from './auth';
|
||||
|
||||
function ok(body: unknown, status = 200): Response {
|
||||
@@ -169,6 +170,17 @@ describe('auth api client', () => {
|
||||
expect(url).toMatch(/\/v1\/auth\/tokens$/);
|
||||
});
|
||||
|
||||
it('getAuthConfig GETs /v1/auth/config and parses the flag', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(ok({ self_register_enabled: false }));
|
||||
const cfg = await getAuthConfig();
|
||||
expect(cfg.self_register_enabled).toBe(false);
|
||||
const url = fetchSpy.mock.calls[0][0] as string;
|
||||
expect(url).toMatch(/\/v1\/auth\/config$/);
|
||||
const init = fetchSpy.mock.calls[0][1] as RequestInit;
|
||||
// Public endpoint; no method override means default GET.
|
||||
expect(init?.method ?? 'GET').toBe('GET');
|
||||
});
|
||||
|
||||
it('deleteToken DELETEs to /v1/auth/tokens/{id} and handles 204', async () => {
|
||||
fetchSpy.mockResolvedValueOnce(noContent());
|
||||
await expect(deleteToken('t1')).resolves.toBeUndefined();
|
||||
|
||||
Reference in New Issue
Block a user