Files
Magent/frontend/app/admin/identities/ResolveIdentityLink.tsx
T
Assclaw 77f2c1b42a
Magent CI/CD / verify (push) Successful in 10m59s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 1m29s
Add reviewed resolution for missing user identity links
2026-09-10 15:02:04 +12:00

84 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useEffect, useRef, useState } from 'react'
import { authFetch, getApiBase } from '../../lib/auth'
import type { Row } from './page'
type Preview = { revision: string; server_id: string; row: Row }
export default function ResolveIdentityLink({ row, accounts, onClose, onSaved }: {
row: Row; accounts: { id: string; name: string }[]; onClose: () => void; onSaved: () => void
}) {
const dialog = useRef<HTMLDialogElement>(null)
const controller = useRef<AbortController | null>(null)
const [chosen, setChosen] = useState('')
const [preview, setPreview] = useState<Preview | null>(null)
const [busy, setBusy] = useState(false)
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
const previous = document.activeElement as HTMLElement | null
const overflow = document.body.style.overflow
document.body.style.overflow = 'hidden'
dialog.current?.showModal()
return () => {
controller.current?.abort()
document.body.style.overflow = overflow
previous?.focus()
}
}, [])
const submit = async (confirm: boolean) => {
if (!chosen || busy || saving || (confirm && !preview?.row.can_confirm)) return
const abort = new AbortController()
controller.current = abort
setError('')
if (confirm) setSaving(true)
else { setBusy(true); setPreview(null) }
try {
const response = await authFetch(`${getApiBase()}/admin/identities/resolve/${confirm ? 'confirm' : 'check'}`, {
method: 'POST', signal: abort.signal, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ user_id: row.user.id, jellyfin_user_id: chosen, ...(confirm ? { revision: preview?.revision } : {}) }),
})
const data = await response.json().catch(() => ({}))
if (!response.ok) throw new Error(response.status === 401 ? 'Your session has ended. Sign in again.' : typeof data.detail === 'string' ? data.detail : 'Could not check the account links. Try again.')
if (!abort.signal.aborted) {
if (confirm) onSaved()
else setPreview(data)
}
} catch (err) {
if (!abort.signal.aborted) {
setError(err instanceof Error ? err.message : 'Could not resolve the link.')
setPreview(null)
}
} finally { if (!abort.signal.aborted) { setBusy(false); setSaving(false) } }
}
return <dialog ref={dialog} className="identity-resolve-dialog" aria-labelledby="resolve-title" onCancel={(event) => { event.preventDefault(); if (!saving) onClose() }}>
<div className="identity-resolve-content">
<header><h2 id="resolve-title">Resolve missing link</h2><button type="button" className="ghost-button" onClick={onClose} disabled={saving}>Close</button></header>
<p>Link <strong>{row.user.username}</strong> (Magent {row.user.id}) to their Jellyfin account. Review the IDs below to confirm this is the same person.</p>
<label>Jellyfin account<select value={chosen} disabled={saving} onChange={(event) => {
controller.current?.abort(); setBusy(false); setPreview(null); setError(''); setChosen(event.target.value)
}}><option value="">Choose an account</option>{[...accounts].sort((a, b) => a.name.localeCompare(b.name)).map((account) => <option key={account.id} value={account.id}>{account.name} {account.id}</option>)}</select></label>
<button type="button" onClick={() => void submit(false)} disabled={!chosen || busy || saving}>{busy ? 'Checking all platform links…' : 'Check selected account'}</button>
{error && <p className="error-banner" role="alert">{error}</p>}
{busy && <p role="status">Checking live IDs and whether another Magent account already owns this identity.</p>}
{preview && <section className="identity-confirm-panel" aria-label="Selected account links" aria-live="polite">
<h3>{preview.row.can_confirm ? 'Ready to confirm' : 'This link needs attention'}</h3>
<p className="identity-meta">Jellyfin server <code>{preview.server_id ?? 'Unavailable'}</code></p>
<dl className="identity-mapping">
<div><dt>Jellyfin</dt><dd>{preview.row.jellyfin?.name ?? 'Account not found'}<code>{preview.row.candidate_jellyfin_id}</code></dd></div>
<div><dt>Seerr</dt><dd>{preview.row.seerr.length ? preview.row.seerr.map((account) => `${account.name} (ID ${account.id})`).join(', ') : 'No matching Jellyfin ID. Check this users Jellyfin account link in Seerr, then check again.'}</dd></div>
<div><dt>Jellystat</dt><dd><code>{preview.row.jellystat.id ?? 'Not verified'}</code>{preview.row.jellystat.state === 'matched' ? 'Same Jellyfin ID verified' : preview.row.jellystat.state === 'missing' ? 'This ID is missing from Jellystat. Check its Jellyfin sync, then check again.' : 'Could not verify this ID. Check the Jellystat connection and try again.'}</dd></div>
</dl>
{preview.row.issues.length > 0 && <ul className="identity-issues">{preview.row.issues.map((issue) => <li key={issue}>{issue}</li>)}</ul>}
{preview.row.state === 'unavailable' && <p>A required service is unavailable. Restore its connection and check again.</p>}
<p>Saving stores the verified Jellyfin and Seerr links in Magent, with your administrator name and confirmation time. All platform IDs and duplicate ownership are checked again before saving.</p>
<button type="button" disabled={!preview.row.can_confirm || saving} onClick={() => void submit(true)}>{saving ? 'Rechecking and saving…' : 'Confirm and save link'}</button>
</section>}
</div>
</dialog>
}