diff --git a/app/api/auth/me/data-export/route.ts b/app/api/auth/me/data-export/route.ts new file mode 100644 index 0000000..9f0ebfa --- /dev/null +++ b/app/api/auth/me/data-export/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "auth/me/data-export"); +} diff --git a/app/api/auth/me/privacy-summary/route.ts b/app/api/auth/me/privacy-summary/route.ts new file mode 100644 index 0000000..fe88cec --- /dev/null +++ b/app/api/auth/me/privacy-summary/route.ts @@ -0,0 +1,6 @@ +import { NextRequest } from "next/server"; +import { proxyRequest } from "@/lib/backend"; + +export async function GET(req: NextRequest) { + return proxyRequest(req, "auth/me/privacy-summary"); +} diff --git a/app/settings/profile/page.tsx b/app/settings/profile/page.tsx index 09baa73..9afeadc 100644 --- a/app/settings/profile/page.tsx +++ b/app/settings/profile/page.tsx @@ -20,9 +20,27 @@ type ProfileData = { }; }; +type PrivacySummary = { + subject: { + email: string; + createdAt: string; + emailVerified: boolean; + twoFactorEnabled: boolean; + }; + dataCategories: Record; + controls: { + exportEndpoint: string; + deletionEndpoint: string; + excludedFromExport?: string[]; + }; +}; + export default function ProfilePage() { const [status, setStatus] = useState(""); const [isError, setIsError] = useState(false); + const [privacySummary, setPrivacySummary] = useState(null); + const [privacyStatus, setPrivacyStatus] = useState(""); + const [privacyError, setPrivacyError] = useState(false); const [fullName, setFullName] = useState(""); const [phone, setPhone] = useState(""); const [companyName, setCompanyName] = useState(""); @@ -52,6 +70,14 @@ export default function ProfilePage() { } }) .catch(() => {}); + + apiFetch("/api/auth/me/privacy-summary") + .then((res) => { + if (!res.error && res.data) { + setPrivacySummary(res.data as PrivacySummary); + } + }) + .catch(() => {}); }, []); const onSubmit = async (event: React.FormEvent) => { @@ -80,12 +106,49 @@ export default function ProfilePage() { setStatus("Profile saved successfully."); }; + const downloadPersonalData = async () => { + setPrivacyStatus("Preparing personal data export..."); + setPrivacyError(false); + const res = await apiFetch>("/api/auth/me/data-export"); + if (res.error || !res.data) { + setPrivacyStatus(res.error?.message ?? "Personal data export failed."); + setPrivacyError(true); + return; + } + const blob = new Blob([JSON.stringify(res.data, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = `ledgerone-personal-data-${new Date().toISOString().slice(0, 10)}.json`; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + URL.revokeObjectURL(url); + setPrivacyStatus("Personal data export downloaded."); + }; + + const deleteAccount = async () => { + const confirmed = window.confirm("Delete this LedgerOne account and associated personal data? This cannot be undone."); + if (!confirmed) return; + setPrivacyStatus("Deleting account..."); + setPrivacyError(false); + const res = await apiFetch<{ message: string }>("/api/auth/me", { method: "DELETE" }); + if (res.error) { + setPrivacyStatus(res.error.message ?? "Account deletion failed."); + setPrivacyError(true); + return; + } + setPrivacyStatus("Account deleted. Redirecting to sign in..."); + window.location.href = "/login"; + }; + const inputCls = "w-full rounded-lg border border-border bg-background/50 px-3 py-2 text-sm focus:border-primary focus:outline-none focus:ring-primary transition-all"; const labelCls = "block text-xs font-medium text-muted-foreground mb-1 uppercase tracking-wide"; + const dataCategoryEntries = privacySummary ? Object.entries(privacySummary.dataCategories) : []; return ( -
+

Personal & Business Details

@@ -143,6 +206,59 @@ export default function ProfilePage() {

)}
+
+

Data & Privacy

+

+ Review stored data categories, download a minimized personal data export, or erase your account. +

+ +
+ {dataCategoryEntries.length ? dataCategoryEntries.map(([key, value]) => ( +
+
+ {key.replace(/([A-Z])/g, " $1")} +
+
{value}
+
+ )) : ( +
+ Privacy inventory is not available yet. +
+ )} +
+ + {privacySummary?.controls.excludedFromExport?.length ? ( +
+
Excluded from export
+

+ {privacySummary.controls.excludedFromExport.join(", ")} +

+
+ ) : null} + +
+ + +
+ + {privacyStatus && ( +
+ {privacyStatus} +
+ )} +
);