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:
@@ -6,6 +6,8 @@ import { Plug, RotateCcw, Send } from "lucide-react";
|
||||
import { api, type ConfigItem, type PlexTestResult } from "../lib/api";
|
||||
import { notify } from "../lib/notifications";
|
||||
import Tooltip from "./Tooltip";
|
||||
import InlineNotice from "./InlineNotice";
|
||||
import { useMe } from "../lib/useMe";
|
||||
import Tabs, { usePersistedTab } from "./Tabs";
|
||||
import { LoadingState, StateMessage } from "./QueryState";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
@@ -35,6 +37,7 @@ const GROUP_ORDER = [
|
||||
export default function ConfigPanel() {
|
||||
const { t } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
const me = useMe();
|
||||
const q = useQuery({ queryKey: qk.adminConfig(), queryFn: api.adminConfig });
|
||||
const data = q.data;
|
||||
|
||||
@@ -204,7 +207,11 @@ export default function ConfigPanel() {
|
||||
</div>
|
||||
|
||||
{activeGroup === "email" && (
|
||||
<div className="mt-3 pt-3 border-t border-border/60">
|
||||
<div className="mt-3 pt-3 border-t border-border/60 space-y-3">
|
||||
{/* The one place an operator can see that mail is off. Sending is fail-soft, so a
|
||||
half-filled config costs verification links, resets and access-request notices
|
||||
with no error anywhere — say so next to the fields that fix it. */}
|
||||
{!me.email_configured && <InlineNotice>{t("config.emailOff")}</InlineNotice>}
|
||||
<Tooltip hint={t("config.testEmailHint")}>
|
||||
<button
|
||||
onClick={() => testEmail.mutate()}
|
||||
|
||||
Reference in New Issue
Block a user