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
+108 -50
View File
@@ -1,63 +1,121 @@
'use client'
"use client";
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import { authFetch, getApiBase, clearToken } from '../../../lib/auth'
import ResolutionChoice from '../../../ui/ResolutionChoice'
import { useCallback, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { authFetch, getApiBase, clearToken } from "../../../lib/auth";
import ResolutionChoice from "../../../ui/ResolutionChoice";
type Issue = { id: number; kind: string; title: string; status: string; permissions?: { can_confirm_resolution?: boolean } }
type Issue = {
id: number;
kind: string;
title: string;
status: string;
permissions?: { can_confirm_resolution?: boolean };
};
export default function ConfirmIssuePage() {
const { id } = useParams<{ id: string }>()
const router = useRouter()
const [item, setItem] = useState<Issue | null>(null)
const [loading, setLoading] = useState(true)
const [busy, setBusy] = useState(false)
const [error, setError] = useState('')
const [result, setResult] = useState('')
const login = () => {
clearToken()
router.replace(`/login?next=${encodeURIComponent(`/issues/confirm/${id}`)}`)
}
const { id } = useParams<{ id: string }>();
const router = useRouter();
const [item, setItem] = useState<Issue | null>(null);
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [result, setResult] = useState("");
const login = useCallback(() => {
clearToken();
router.replace(`/login?next=${encodeURIComponent(`/issues/confirm/${id}`)}`);
}, [id, router]);
useEffect(() => {
const controller = new AbortController()
setLoading(true); setItem(null); setError(''); setResult('')
const controller = new AbortController();
setLoading(true);
setItem(null);
setError("");
setResult("");
const load = async () => {
try {
const response = await authFetch(`${getApiBase()}/portal/items/${id}`, { signal: controller.signal, cache: 'no-store' })
if (response.status === 401) { login(); return }
if (!response.ok) throw new Error('This issue is unavailable. Please sign in with the account that reported it.')
const data = await response.json()
if (data.item?.kind !== 'issue') throw new Error('This link does not belong to an issue.')
setItem(data.item)
} catch (err) { if (!controller.signal.aborted) setError(err instanceof Error ? err.message : 'Could not load this issue. Please try again.') }
finally { if (!controller.signal.aborted) setLoading(false) }
}
void load()
return () => controller.abort()
const response = await authFetch(`${getApiBase()}/portal/items/${id}`, {
signal: controller.signal,
cache: "no-store",
});
if (response.status === 401) {
login();
return;
}
if (!response.ok)
throw new Error("This issue is unavailable. Please sign in with the account that reported it.");
const data = await response.json();
if (data.item?.kind !== "issue") throw new Error("This link does not belong to an issue.");
setItem(data.item);
} catch (err) {
if (!controller.signal.aborted)
setError(err instanceof Error ? err.message : "Could not load this issue. Please try again.");
} finally {
if (!controller.signal.aborted) setLoading(false);
}
};
void load();
return () => controller.abort();
// The confirmation link identifies one issue. Never submit an answer on GET.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
}, [id, login]);
const answer = async (resolved: boolean) => {
if (busy) return
setBusy(true); setError('')
if (busy) return;
setBusy(true);
setError("");
try {
const response = await authFetch(`${getApiBase()}/portal/issues/${id}/resolution-response`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ resolved }),
})
if (response.status === 401) { login(); return }
if (!response.ok) throw new Error('Your answer could not be saved. This issue may already have been answered. Refresh to check before trying again.')
setResult(resolved ? 'Thanks! Your issue is now closed.' : 'Thanks for letting us know. Your issue stays open for another look.')
} catch (err) { setError(err instanceof Error ? err.message : 'Could not save your answer. Please try again.') }
finally { setBusy(false) }
}
return <main className="resolution-response-page">
{error && <p role="alert" className="status-banner">{error}</p>}
{loading ? <p role="status">Loading your issue</p> : result ? <section className="resolution-choice" role="status"><h2>{result}</h2><a href="/portal/issues">Back to issues</a></section> : item ? (
item.status === 'awaiting_confirmation' && item.permissions?.can_confirm_resolution
? <ResolutionChoice title={item.title} busy={busy} onAnswer={(value) => void answer(value)} />
: <section className="resolution-choice"><h2>{item.status === 'awaiting_confirmation' ? 'This question is for the person who reported the issue.' : 'No answer is needed right now.'}</h2><p>{item.title}</p><a href={`/portal/issues?item=${item.id}`}>View issue</a></section>
) : null}
</main>
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ resolved }),
});
if (response.status === 401) {
login();
return;
}
if (!response.ok)
throw new Error(
"Your answer could not be saved. This issue may already have been answered. Refresh to check before trying again.",
);
setResult(
resolved
? "Thanks! Your issue is now closed."
: "Thanks for letting us know. Your issue stays open for another look.",
);
} catch (err) {
setError(err instanceof Error ? err.message : "Could not save your answer. Please try again.");
} finally {
setBusy(false);
}
};
return (
<main className="resolution-response-page">
{error && (
<p role="alert" className="status-banner">
{error}
</p>
)}
{loading ? (
<p role="status">Loading your issue</p>
) : result ? (
<section className="resolution-choice" role="status">
<h2>{result}</h2>
<a href="/portal/issues">Back to issues</a>
</section>
) : item ? (
item.status === "awaiting_confirmation" && item.permissions?.can_confirm_resolution ? (
<ResolutionChoice title={item.title} busy={busy} onAnswer={(value) => void answer(value)} />
) : (
<section className="resolution-choice">
<h2>
{item.status === "awaiting_confirmation"
? "This question is for the person who reported the issue."
: "No answer is needed right now."}
</h2>
<p>{item.title}</p>
<a href={`/portal/issues?item=${item.id}`}>View issue</a>
</section>
)
) : null}
</main>
);
}