feat(auth): admin Users page + allow_registration toggle (5a frontend)
New admin-only Users page (sidebar): Users & roles (list + promote/demote with confirm; self/demo/last-admin guarded server-side), plus the Access requests (Invite whitelist) and Demo whitelist+reset migrated out of Settings → Account (same data/tables — UI relocated only). Settings → Account now holds personal content only. ConfigPanel learns a boolean field type (toggle) for the new allow_registration setting. api methods, new 'users' page/route/nav/header, and EN/HU/DE strings (new users namespace + access group).
This commit is contained in:
parent
7efd4f4867
commit
737da6bd96
17 changed files with 528 additions and 281 deletions
|
|
@ -11,7 +11,7 @@ import Tooltip from "./Tooltip";
|
|||
// applied together via one Save/Discard bar (consistent with the Settings page); nothing hits
|
||||
// the server until Save. Group order is fixed where known so logically related settings (e.g.
|
||||
// Email/SMTP) stay together.
|
||||
const GROUP_ORDER = ["email", "youtube", "quota", "backfill", "shorts", "batch"];
|
||||
const GROUP_ORDER = ["access", "email", "youtube", "quota", "backfill", "shorts", "batch"];
|
||||
type SaveState = "idle" | "saving" | "saved" | "error";
|
||||
|
||||
export default function ConfigPanel() {
|
||||
|
|
@ -58,6 +58,8 @@ export default function ConfigPanel() {
|
|||
if (it.secret) {
|
||||
if (secretReset[key] && !raw.trim()) await api.resetConfig(key);
|
||||
else if (raw.trim()) await api.setConfig(key, raw);
|
||||
} else if (it.type === "bool") {
|
||||
await api.setConfig(key, raw === "true");
|
||||
} else if (raw === "") {
|
||||
if (it.is_set) await api.resetConfig(key); // cleared → fall back to default
|
||||
} else {
|
||||
|
|
@ -191,6 +193,7 @@ function Field({
|
|||
}) {
|
||||
const { t } = useTranslation();
|
||||
const isNum = item.type === "int";
|
||||
const isBool = item.type === "bool";
|
||||
const secretDisabled = item.secret && !secretsManageable;
|
||||
|
||||
// Status line: never reveal a secret's value.
|
||||
|
|
@ -210,8 +213,9 @@ function Field({
|
|||
}
|
||||
|
||||
// Reset affordance shows when a DB override exists: clears a non-secret to its default, or
|
||||
// toggles a secret's reset-on-save (applied on Save, not immediately).
|
||||
const showReset = item.is_set;
|
||||
// toggles a secret's reset-on-save (applied on Save, not immediately). A boolean toggle is
|
||||
// its own control — storing its value is equivalent to the default, so no reset link.
|
||||
const showReset = item.is_set && !isBool;
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-3 py-3">
|
||||
|
|
@ -224,16 +228,20 @@ function Field({
|
|||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-1.5 shrink-0">
|
||||
<input
|
||||
type={item.secret ? "password" : isNum ? "number" : "text"}
|
||||
value={value}
|
||||
disabled={secretDisabled || (item.secret && pendingSecretReset)}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
min={isNum && item.min != null ? item.min : undefined}
|
||||
max={isNum && item.max != null ? item.max : undefined}
|
||||
placeholder={item.secret ? "••••••••" : String(item.default ?? "")}
|
||||
className="w-52 bg-card border border-border rounded-xl px-3 py-1.5 text-sm outline-none focus:border-accent disabled:opacity-50"
|
||||
/>
|
||||
{isBool ? (
|
||||
<Toggle checked={value === "true"} onChange={(v) => onChange(v ? "true" : "false")} />
|
||||
) : (
|
||||
<input
|
||||
type={item.secret ? "password" : isNum ? "number" : "text"}
|
||||
value={value}
|
||||
disabled={secretDisabled || (item.secret && pendingSecretReset)}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
min={isNum && item.min != null ? item.min : undefined}
|
||||
max={isNum && item.max != null ? item.max : undefined}
|
||||
placeholder={item.secret ? "••••••••" : String(item.default ?? "")}
|
||||
className="w-52 bg-card border border-border rounded-xl px-3 py-1.5 text-sm outline-none focus:border-accent disabled:opacity-50"
|
||||
/>
|
||||
)}
|
||||
{showReset && !secretDisabled && (
|
||||
<Tooltip hint={t("config.resetHint")}>
|
||||
<button
|
||||
|
|
@ -253,3 +261,19 @@ function Field({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`w-9 h-5 rounded-full transition relative shrink-0 ${checked ? "bg-accent" : "bg-border"}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white shadow transition-all ${
|
||||
checked ? "left-[18px]" : "left-0.5"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue