Files
Assclaw f852e7c941
Magent CI/CD / verify (push) Failing after 9m34s
Magent CI/CD / deploy-beta (push) Skipped
chore: standardize security and quality foundations
2026-09-17 20:03:47 +12:00

146 lines
5.3 KiB
TypeScript
Raw Permalink 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, useState } from "react";
import { getApiBase } from "../lib/auth";
import BrandingLogo from "../ui/BrandingLogo";
import "./recaps.css";
type LinkAction = { action: "confirm" | "unsubscribe"; token: string };
export default function EmailRecapLinkPage() {
const [link, setLink] = useState<LinkAction | null>(null);
const [state, setState] = useState("loading");
const [error, setError] = useState("");
const [busy, setBusy] = useState(false);
useEffect(() => {
let controller: AbortController | null = null;
const checkLink = () => {
controller?.abort();
const abort = new AbortController();
controller = abort;
setError("");
setState("loading");
setLink(null);
// Fragments stay out of web-server access logs and referrers. Opening the link only checks it.
const params = new URLSearchParams(window.location.hash.slice(1));
const action = params.get("action");
const token = params.get("token") || "";
if ((action !== "confirm" && action !== "unsubscribe") || !/^[A-Za-z0-9_-]{40,100}$/.test(token)) {
setError("This email link is incomplete. Open Profile to manage your monthly recaps.");
setState("error");
return;
}
const payload = { action, token } as LinkAction;
setLink(payload);
void fetch(`${getApiBase()}/email-recaps/check`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
signal: abort.signal,
credentials: "omit",
})
.then(async (response) => {
const result = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(
typeof result.detail === "string"
? result.detail
: "Could not check this email link. Please open it again.",
);
if (!abort.signal.aborted) setState(result.state);
})
.catch((err: Error) => {
if (!abort.signal.aborted) {
setError(err.message);
setState("error");
}
});
};
checkLink();
window.addEventListener("hashchange", checkLink);
return () => {
controller?.abort();
window.removeEventListener("hashchange", checkLink);
};
}, []);
const apply = async () => {
if (!link || busy) return;
setBusy(true);
setError("");
try {
const response = await fetch(`${getApiBase()}/email-recaps/confirm`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(link),
credentials: "omit",
});
const result = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(
typeof result.detail === "string" ? result.detail : "Could not update your preference. Please try again.",
);
setState(result.state);
window.history.replaceState(null, "", "/email-recaps");
} catch (err) {
setError(err instanceof Error ? err.message : "Could not update your preference.");
} finally {
setBusy(false);
}
};
const done = state === "enabled" || state === "off";
return (
<main className="recap-link-page">
<a className="recap-brand" href="/login">
<BrandingLogo className="brand-logo" />
<span>Magent</span>
</a>
<section className="account-panel">
<span className="recap-eyebrow">Personal viewing reports</span>
<h1>
{state === "enabled"
? "Youre on the list."
: state === "off"
? "Recaps are turned off."
: state === "loading"
? "Checking your email link"
: state === "error"
? "This link needs another look"
: link?.action === "unsubscribe"
? "Unsubscribe from recaps?"
: "Your month, delivered."}
</h1>
<p>
{state === "enabled"
? "Your email is confirmed. Youll receive your personal viewing recap when the monthly schedule runs."
: state === "off"
? "You wont receive further monthly recaps. You can turn them back on in Profile."
: state === "ready" && link?.action === "unsubscribe"
? "This turns off all personal viewing report emails. You can still explore all your reports in Magent."
: state === "ready"
? "Confirm to email yourself your minutes, movies, episodes, longest run and requests. Manage automatic monthly delivery separately in Profile."
: ""}
</p>
{error && (
<p className="account-notice is-error" role="alert">
{error}
</p>
)}
{state === "ready" && (
<button type="button" className="account-primary" disabled={busy} onClick={() => void apply()}>
{busy ? "Updating…" : link?.action === "unsubscribe" ? "Unsubscribe from recaps" : "Confirm email recaps"}
</button>
)}
{(done || state === "error") && (
<a className="recap-text-link" href="/profile#monthly-recaps">
Manage email preferences
</a>
)}
{state === "loading" && <p role="status">One moment</p>}
</section>
</main>
);
}