All checks were successful
Deploy to Production / deploy (push) Successful in 1m19s
Account deletion (DELETE /v1/account): re-type email/phone to confirm, stops live containers and hard-deletes every org where the caller is sole member (FK cascade clears servers, builds, logs, encrypted secrets), deletes the user (cascade drops sessions), audits the action, clears the session cookie. Frontend danger-zone replaces the old open-a-ticket placeholder. Closes audit ACC-001. Enterprise price unified to Custom on landing + pricing, removing the 499/999 inconsistency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
5.0 KiB
TypeScript
117 lines
5.0 KiB
TypeScript
'use client';
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
import { apiFetch, apiUrl } from '@/lib/api';
|
|
import Link from 'next/link';
|
|
import { useState } from 'react';
|
|
|
|
export default function AccountPage() {
|
|
const [downloading, setDownloading] = useState(false);
|
|
const [confirmText, setConfirmText] = useState('');
|
|
const [deleting, setDeleting] = useState(false);
|
|
const [delError, setDelError] = useState<string | null>(null);
|
|
|
|
async function deleteAccount() {
|
|
if (!confirmText.trim()) return;
|
|
if (!confirm('Permanently delete your account and all its data? This cannot be undone.')) return;
|
|
setDeleting(true);
|
|
setDelError(null);
|
|
try {
|
|
await apiFetch('/v1/account', {
|
|
method: 'DELETE',
|
|
body: JSON.stringify({ confirm: confirmText.trim() }),
|
|
});
|
|
window.location.href = '/';
|
|
} catch (e) {
|
|
const detail = (e as { detail?: { detail?: string; error?: string } }).detail;
|
|
setDelError(detail?.detail ?? detail?.error ?? (e as Error).message);
|
|
setDeleting(false);
|
|
}
|
|
}
|
|
|
|
async function downloadExport() {
|
|
setDownloading(true);
|
|
try {
|
|
// Trigger a same-origin attachment download. The cookie ships with the
|
|
// request because we're same-credentials with the API origin via CORS.
|
|
window.location.href = apiUrl('/v1/account/export');
|
|
} finally {
|
|
setTimeout(() => setDownloading(false), 1500);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto max-w-3xl px-6 py-10">
|
|
<h1 className="text-[22px] font-semibold tracking-tight">Account</h1>
|
|
<p className="mt-1 text-[13px] text-[--color-fg-muted]">
|
|
Your data, your rights. Swiss DSG Art. 25 / GDPR Art. 15 + 20.
|
|
</p>
|
|
|
|
<div className="mt-8 space-y-4">
|
|
<section className="panel p-5">
|
|
<h2 className="text-[14px] font-semibold tracking-tight">Download your data</h2>
|
|
<p className="mt-2 text-[12.5px] leading-relaxed text-[--color-fg-muted]">
|
|
One JSON file with everything we hold for your account: profile, organization, MCP
|
|
servers, build history (last 1000 entries), audit log (last 1000 events) and your
|
|
support-ticket history. Excludes password hashes, encrypted secrets and other
|
|
users' data.
|
|
</p>
|
|
<div className="mt-4">
|
|
<Button variant="primary" size="md" onClick={downloadExport} disabled={downloading}>
|
|
{downloading ? 'Preparing…' : 'Download .json'}
|
|
</Button>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="panel border-[--color-danger]/30 p-5">
|
|
<h2 className="text-[14px] font-semibold tracking-tight text-[--color-danger]">
|
|
Delete account
|
|
</h2>
|
|
<p className="mt-2 text-[12.5px] leading-relaxed text-[--color-fg-muted]">
|
|
Permanently erases your account and every organization where you are the only member —
|
|
servers, encrypted secrets, builds and history are wiped and running containers are
|
|
stopped. This cannot be undone. Swiss DSG Art. 32 / GDPR Art. 17.
|
|
</p>
|
|
<p className="mt-3 text-[12px] text-[--color-fg-subtle]">
|
|
Type your account email (or phone) to confirm:
|
|
</p>
|
|
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
<input
|
|
value={confirmText}
|
|
onChange={(e) => setConfirmText(e.target.value)}
|
|
placeholder="you@example.com"
|
|
className="h-9 w-64 rounded-md border border-[--color-border] bg-[--color-bg-subtle] px-3 text-[13px] outline-none transition-colors focus:border-[--color-border-strong]"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={deleteAccount}
|
|
disabled={deleting || !confirmText.trim()}
|
|
className="inline-flex h-9 items-center rounded-md border border-[--color-danger]/50 bg-[--color-danger]/10 px-4 text-[13px] font-medium text-[--color-danger] transition-colors hover:bg-[--color-danger]/20 disabled:opacity-50"
|
|
>
|
|
{deleting ? 'Deleting…' : 'Delete my account'}
|
|
</button>
|
|
</div>
|
|
{delError && <p className="mt-2 text-[12px] text-[--color-danger]">{delError}</p>}
|
|
</section>
|
|
|
|
<section className="panel p-5">
|
|
<h2 className="text-[14px] font-semibold tracking-tight">Cookies on this site</h2>
|
|
<p className="mt-2 text-[12.5px] leading-relaxed text-[--color-fg-muted]">
|
|
We use only strictly-necessary cookies: a session cookie (
|
|
<span className="mono">bmm_session</span>, httpOnly, 30 days) and a short-lived
|
|
OAuth-CSRF state cookie (<span className="mono">bmm_oauth_state</span>, 10 minutes
|
|
during a third-party login flow). No analytics, no tracking, no third-party cookies on
|
|
this domain.
|
|
</p>
|
|
</section>
|
|
</div>
|
|
|
|
<div className="mt-10 text-[12px] text-[--color-fg-subtle]">
|
|
<Link href="/privacy" className="hover:text-[--color-fg]">
|
|
← Privacy policy
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|