chore: standardize security and quality foundations
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped

This commit is contained in:
2026-09-17 20:03:47 +12:00
parent 5639dbcb83
commit f852e7c941
127 changed files with 17928 additions and 10741 deletions
@@ -1,74 +1,232 @@
'use client'
"use client";
import { useEffect, useRef, useState } from 'react'
import { authFetch, getApiBase } from '../../lib/auth'
import { FEATURES, type FeatureAccess } from '../../lib/features'
import type { Row } from './IdentityReviewPanel'
import { useEffect, useRef, useState } from "react";
import { authFetch, getApiBase } from "../../lib/auth";
import { FEATURES, type FeatureAccess } from "../../lib/features";
import type { Row } from "./IdentityReviewPanel";
type Account = { id: number; username: string; email: string | null; profile_id: number | null; last_login_at: string | null }
type Account = {
id: number;
username: string;
email: string | null;
profile_id: number | null;
last_login_at: string | null;
};
type Preview = {
accounts: Account[]; keep_id: number; recommended_id: number; revision: string; can_confirm: boolean; issues: string[]
proposed: Account & { jellyfin_user_id: string; seerr_user_id: number; features: FeatureAccess; expires_at: string | null; is_blocked: boolean; auto_search_enabled: boolean }
}
accounts: Account[];
keep_id: number;
recommended_id: number;
revision: string;
can_confirm: boolean;
issues: string[];
proposed: Account & {
jellyfin_user_id: string;
seerr_user_id: number;
features: FeatureAccess;
expires_at: string | null;
is_blocked: boolean;
auto_search_enabled: boolean;
};
};
export default function DuplicateAccountRepair({ row, onClose, onSaved }: { row: Row; onClose: () => void; onSaved: () => void }) {
const dialog = useRef<HTMLDialogElement>(null)
const controller = useRef<AbortController | null>(null)
const [preview, setPreview] = useState<Preview | null>(null)
const [busy, setBusy] = useState(false)
const [saving, setSaving] = useState(false)
const [acknowledged, setAcknowledged] = useState(false)
const [error, setError] = useState('')
export default function DuplicateAccountRepair({
row,
onClose,
onSaved,
}: {
row: Row;
onClose: () => void;
onSaved: () => void;
}) {
const dialog = useRef<HTMLDialogElement>(null);
const controller = useRef<AbortController | null>(null);
const [preview, setPreview] = useState<Preview | null>(null);
const [busy, setBusy] = useState(false);
const [saving, setSaving] = useState(false);
const [acknowledged, setAcknowledged] = useState(false);
const [error, setError] = useState("");
const submit = async (confirm = false, keepId?: number) => {
const abort = new AbortController()
controller.current?.abort(); controller.current = abort
setError(''); setAcknowledged(false)
if (confirm) setSaving(true)
else setBusy(true)
const abort = new AbortController();
controller.current?.abort();
controller.current = abort;
setError("");
setAcknowledged(false);
if (confirm) setSaving(true);
else setBusy(true);
try {
const response = await authFetch(`${getApiBase()}/admin/identities/duplicates/${confirm ? 'confirm' : 'check'}`, {
method: 'POST', signal: abort.signal, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: row.user.id, ...(keepId ? { keep_id: keepId } : {}), ...(confirm ? { keep_id: preview?.keep_id, revision: preview?.revision } : {}) }),
})
const data = await response.json()
if (!response.ok) throw new Error(typeof data.detail === 'string' ? data.detail : 'Could not review these accounts.')
if (!abort.signal.aborted) { if (confirm) onSaved(); else setPreview(data) }
} catch (err) { if (!abort.signal.aborted) { setError(err instanceof Error ? err.message : 'Repair failed. Preview again.'); setPreview(null) } }
finally { if (!abort.signal.aborted) { setBusy(false); setSaving(false) } }
}
const response = await authFetch(`${getApiBase()}/admin/identities/duplicates/${confirm ? "confirm" : "check"}`, {
method: "POST",
signal: abort.signal,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
user_id: row.user.id,
...(keepId ? { keep_id: keepId } : {}),
...(confirm ? { keep_id: preview?.keep_id, revision: preview?.revision } : {}),
}),
});
const data = await response.json();
if (!response.ok)
throw new Error(typeof data.detail === "string" ? data.detail : "Could not review these accounts.");
if (!abort.signal.aborted) {
if (confirm) onSaved();
else setPreview(data);
}
} catch (err) {
if (!abort.signal.aborted) {
setError(err instanceof Error ? err.message : "Repair failed. Preview again.");
setPreview(null);
}
} finally {
if (!abort.signal.aborted) {
setBusy(false);
setSaving(false);
}
}
};
// biome-ignore lint/correctness/useExhaustiveDependencies: The dialog preview runs once when this keyed modal mounts.
useEffect(() => {
const previous = document.activeElement as HTMLElement | null
const overflow = document.body.style.overflow
document.body.style.overflow = 'hidden'; dialog.current?.showModal()
void submit()
return () => { controller.current?.abort(); document.body.style.overflow = overflow; previous?.focus() }
}, [])
return <dialog ref={dialog} className="identity-resolve-dialog" aria-labelledby="duplicates-title" onCancel={(event) => { event.preventDefault(); if (!saving) onClose() }}>
<div className="identity-resolve-content">
<header><h2 id="duplicates-title">Repair duplicate accounts</h2><button type="button" className="ghost-button" disabled={saving} onClick={onClose}>Close</button></header>
<p>Review the Magent accounts for <strong>{row.user.username}</strong>. This repair keeps one account linked to the verified Jellyfin identity.</p>
{busy && <p role="status">Checking live service IDs and duplicate ownership...</p>}
{error && <p className="error-banner" role="alert">{error}</p>}
{!preview && !busy && <button type="button" disabled={saving} onClick={() => void submit()}>Check again</button>}
{preview && <section className="identity-confirm-panel" aria-label="Duplicate repair preview">
<label>Magent account to keep<select disabled={busy || saving} value={preview.keep_id} onChange={(event) => void submit(false, Number(event.target.value))}>
{preview.accounts.map((account) => <option key={account.id} value={account.id}>{account.username} Magent {account.id}{account.id === preview.recommended_id ? ' (recommended)' : ''}</option>)}
</select></label>
<p>The recommended row already owns the Jellyfin link, or is the oldest row when neither owns it.</p>
<div className="identity-mapping identity-duplicate-accounts">{preview.accounts.map((account) => <div key={account.id}><strong>Magent {account.id}{account.id === preview.keep_id ? ' · Keep' : ' · Consolidate'}</strong><p>{account.username}</p><p>{account.email || 'No email'} · Profile {account.profile_id ?? 'None'}</p><p>Last login: {account.last_login_at ? new Date(account.last_login_at).toLocaleString() : 'Never'}</p></div>)}</div>
<h3>Resulting account</h3>
<p><strong>{preview.proposed.username}</strong> · Magent {preview.keep_id} · Seerr {preview.proposed.seerr_user_id ?? 'Not verified'}</p>
<p>Jellyfin / Jellystat: <code>{preview.proposed.jellyfin_user_id ?? 'Not verified'}</code></p>
<p>Email: {preview.proposed.email || 'None'} · Profile: {preview.proposed.profile_id ?? 'None'}</p>
<p>Access: {preview.proposed.is_blocked ? 'Blocked' : 'Not blocked'} · Expiry: {preview.proposed.expires_at ? new Date(preview.proposed.expires_at).toLocaleString() : 'None'} · Automatic search: {preview.proposed.auto_search_enabled ? 'Enabled' : 'Disabled'}</p>
<ul>{FEATURES.map((feature) => <li key={feature.key}>{feature.label}: {preview.proposed.features[feature.key] ? 'Enabled' : 'Disabled'}</li>)}</ul>
<p>Request, issue, invitation and login activity history is retained. The selected account keeps its email and profile. Any block, earlier expiry or disabled permission on either row is preserved.</p>
<p>Extra Magent rows are removed from the active directory after their details are archived. Their outstanding emails are cancelled and their email subscriptions are not inherited. The kept account retains its own subscriptions where still eligible. Password reset links must be requested again.</p>
<p>Jellyfin, Seerr and Jellystat accounts and media are unchanged. This action does not merge different Jellyfin identities or delete upstream users.</p>
{preview.issues.length > 0 && <ul className="identity-issues">{preview.issues.map((issue) => <li key={issue}>{issue}</li>)}</ul>}
<label className="identity-import-option"><span><input type="checkbox" checked={acknowledged} disabled={busy || saving || !preview.can_confirm} onChange={(event) => setAcknowledged(event.target.checked)} /> I confirm these rows belong to the same person and have reviewed the account to keep.</span></label>
<button type="button" disabled={!preview.can_confirm || !acknowledged || busy || saving} onClick={() => void submit(true)}>{saving ? 'Rechecking and repairing...' : 'Confirm duplicate repair'}</button>
</section>}
</div>
</dialog>
const previous = document.activeElement as HTMLElement | null;
const overflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
dialog.current?.showModal();
void submit();
return () => {
controller.current?.abort();
document.body.style.overflow = overflow;
previous?.focus();
};
}, []);
return (
<dialog
ref={dialog}
className="identity-resolve-dialog"
aria-labelledby="duplicates-title"
onCancel={(event) => {
event.preventDefault();
if (!saving) onClose();
}}
>
<div className="identity-resolve-content">
<header>
<h2 id="duplicates-title">Repair duplicate accounts</h2>
<button type="button" className="ghost-button" disabled={saving} onClick={onClose}>
Close
</button>
</header>
<p>
Review the Magent accounts for <strong>{row.user.username}</strong>. This repair keeps one account linked to
the verified Jellyfin identity.
</p>
{busy && <p role="status">Checking live service IDs and duplicate ownership...</p>}
{error && (
<p className="error-banner" role="alert">
{error}
</p>
)}
{!preview && !busy && (
<button type="button" disabled={saving} onClick={() => void submit()}>
Check again
</button>
)}
{preview && (
<section className="identity-confirm-panel" aria-label="Duplicate repair preview">
<label>
Magent account to keep
<select
disabled={busy || saving}
value={preview.keep_id}
onChange={(event) => void submit(false, Number(event.target.value))}
>
{preview.accounts.map((account) => (
<option key={account.id} value={account.id}>
{account.username} Magent {account.id}
{account.id === preview.recommended_id ? " (recommended)" : ""}
</option>
))}
</select>
</label>
<p>The recommended row already owns the Jellyfin link, or is the oldest row when neither owns it.</p>
<div className="identity-mapping identity-duplicate-accounts">
{preview.accounts.map((account) => (
<div key={account.id}>
<strong>
Magent {account.id}
{account.id === preview.keep_id ? " · Keep" : " · Consolidate"}
</strong>
<p>{account.username}</p>
<p>
{account.email || "No email"} · Profile {account.profile_id ?? "None"}
</p>
<p>
Last login: {account.last_login_at ? new Date(account.last_login_at).toLocaleString() : "Never"}
</p>
</div>
))}
</div>
<h3>Resulting account</h3>
<p>
<strong>{preview.proposed.username}</strong> · Magent {preview.keep_id} · Seerr{" "}
{preview.proposed.seerr_user_id ?? "Not verified"}
</p>
<p>
Jellyfin / Jellystat: <code>{preview.proposed.jellyfin_user_id ?? "Not verified"}</code>
</p>
<p>
Email: {preview.proposed.email || "None"} · Profile: {preview.proposed.profile_id ?? "None"}
</p>
<p>
Access: {preview.proposed.is_blocked ? "Blocked" : "Not blocked"} · Expiry:{" "}
{preview.proposed.expires_at ? new Date(preview.proposed.expires_at).toLocaleString() : "None"} ·
Automatic search: {preview.proposed.auto_search_enabled ? "Enabled" : "Disabled"}
</p>
<ul>
{FEATURES.map((feature) => (
<li key={feature.key}>
{feature.label}: {preview.proposed.features[feature.key] ? "Enabled" : "Disabled"}
</li>
))}
</ul>
<p>
Request, issue, invitation and login activity history is retained. The selected account keeps its email
and profile. Any block, earlier expiry or disabled permission on either row is preserved.
</p>
<p>
Extra Magent rows are removed from the active directory after their details are archived. Their
outstanding emails are cancelled and their email subscriptions are not inherited. The kept account retains
its own subscriptions where still eligible. Password reset links must be requested again.
</p>
<p>
Jellyfin, Seerr and Jellystat accounts and media are unchanged. This action does not merge different
Jellyfin identities or delete upstream users.
</p>
{preview.issues.length > 0 && (
<ul className="identity-issues">
{preview.issues.map((issue) => (
<li key={issue}>{issue}</li>
))}
</ul>
)}
<label className="identity-import-option">
<span>
<input
type="checkbox"
checked={acknowledged}
disabled={busy || saving || !preview.can_confirm}
onChange={(event) => setAcknowledged(event.target.checked)}
/>{" "}
I confirm these rows belong to the same person and have reviewed the account to keep.
</span>
</label>
<button
type="button"
disabled={!preview.can_confirm || !acknowledged || busy || saving}
onClick={() => void submit(true)}
>
{saving ? "Rechecking and repairing..." : "Confirm duplicate repair"}
</button>
</section>
)}
</div>
</dialog>
);
}