feat(admin): delete a whitelist entry, and stop hiding a broken mail setup
Three operator gaps, all found the same way — a person could not sign up and the
app showed nobody anything.
- DELETE /api/admin/invites/{id} with a delete control on both the pending
requests and the previously-decided list. Deny keeps the row so the person
cannot immediately re-ask; delete forgets the address. Refused with 409 while
a User row exists for it, because is_allowed reads that row and removing it
would revoke a live user's access with no other sign — the list ships
has_account so the UI withholds the button instead of offering one that fails.
- Email is fail-soft by design, which had come to mean invisible. _send() now
records an email_undelivered notification for every admin when a message is
skipped or rejected, naming the message that was lost, throttled to one an
hour. /api/me carries email_configured, driving a standing warning on
Configuration → Email / SMTP and above the access-request list.
- The sign-in card's Create account / Forgot password / Didn't get the email
views are history entries (useHistorySubview), so Back returns to the previous
view instead of only moving the address bar.
Also: the per-test rate-limiter reset sweeps every loaded app.* module rather
than a hand-kept list of three — this change added a limiter outside it.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useHistorySubview, type SubviewUrl } from "../lib/history";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ArrowRight,
|
||||
@@ -27,7 +28,19 @@ import { inputCls } from "./ui/form";
|
||||
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/;
|
||||
const PASSWORD_MIN = 10;
|
||||
|
||||
type Mode = "signin" | "register" | "forgot" | "reset" | "resend";
|
||||
export type Mode = "signin" | "register" | "forgot" | "reset" | "resend";
|
||||
|
||||
// The card's sub-views are real history entries, so browser/mouse Back leaves one the way it leaves
|
||||
// any other page — they used to be plain state, and Back silently changed only the address bar.
|
||||
// "signin" is the root (no param) and "reset" is deliberately NOT mirrored: that view is only
|
||||
// reachable with the one-time token from the #reset fragment, so a bookmarked ?m=reset would open a
|
||||
// form that cannot submit.
|
||||
export const MODE_URL: SubviewUrl<Mode> = {
|
||||
key: "m",
|
||||
encode: (m: Mode) => (m === "signin" || m === "reset" ? null : m),
|
||||
decode: (raw: string) =>
|
||||
raw === "register" || raw === "forgot" || raw === "resend" ? (raw as Mode) : null,
|
||||
};
|
||||
|
||||
// Per-mode i18n keys for the card title and the submit button. A flat map (vs nested ternaries)
|
||||
// keeps adding a new mode a one-line change and makes a missing key a compile error.
|
||||
@@ -328,7 +341,10 @@ function AuthCard() {
|
||||
.catch(() => setVerify("invalid"));
|
||||
}, [verifyToken]);
|
||||
|
||||
const [mode, setMode] = useState<Mode>(resetToken ? "reset" : "signin");
|
||||
const { view: mode, open: setMode } = useHistorySubview<Mode>(
|
||||
resetToken ? "reset" : "signin",
|
||||
MODE_URL,
|
||||
);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -344,6 +360,13 @@ function AuthCard() {
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
const [done, setDone] = useState<string>(""); // success/info message replacing the form
|
||||
// `done` and `error` belong to the view that produced them, and the render is `done ? msg : form`
|
||||
// — without this, Back out of a submitted form would show the old success message under the
|
||||
// previous view's title. Clearing on view change covers both Back/Forward and the mode links.
|
||||
useEffect(() => {
|
||||
setDone("");
|
||||
setError("");
|
||||
}, [mode]);
|
||||
|
||||
// Explicit demo entry (replaces the old hidden probe): a labelled email field, still gated
|
||||
// by the demo whitelist server-side.
|
||||
|
||||
Reference in New Issue
Block a user