fix(web): add account deletion page (#16)

Add the public deletion resource and repair the monorepo web container build contract.
This commit is contained in:
jjbrunton authored and GitHub committed 2026-08-30 16:23:30 +01:00
commit 24de9af4cd
11 files changed
+183 -35

No files matched your search

+6
View File
@@ -1,5 +1,7 @@
node_modules
**/node_modules
.next
**/.next
.git
.gitignore
*.md
@@ -23,11 +25,15 @@ apps/mcp
apps/mobile
apps/supabase
apps/web
!apps/web
!apps/web/**
e2e
evidence
etc
infra
scripts
!scripts
!scripts/fix-hoisting.js
terraform
.agents
.claude
+12 -14
View File
@@ -12,15 +12,12 @@ ENV NEXT_PUBLIC_POSTHOG_KEY=$NEXT_PUBLIC_POSTHOG_KEY
ENV NEXT_PUBLIC_POSTHOG_HOST=$NEXT_PUBLIC_POSTHOG_HOST
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
# Install dependencies
COPY package*.json ./
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Install dependencies from the committed monorepo lockfile.
COPY . .
RUN npm ci
# Build the web workspace from the repository root.
RUN npm run build -w @sauci/web
# Production stage
FROM node:20-alpine AS runner
@@ -37,10 +34,11 @@ RUN apk add --no-cache curl
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built assets from builder
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
# Next preserves the workspace path in standalone output. Keep that layout so
# the generated server can resolve its .next directory and public assets.
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
USER nextjs
@@ -49,4 +47,4 @@ EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
CMD ["node", "apps/web/server.js"]
+19
View File
@@ -0,0 +1,19 @@
import { readFile } from 'node:fs/promises'
import { describe, expect, it } from 'vitest'
const pagePath = new URL('./page.tsx', import.meta.url)
describe('/delete-account public deletion resource', () => {
it('provides an accessible request route and the required deletion details', async () => {
const page = await readFile(pagePath, 'utf8')
expect(page).toContain('Delete your Sauci account')
expect(page).toContain('mailto:privacy@sauci.app')
expect(page).toContain('without reinstalling the app')
expect(page).toContain('Open Sauci and go to your Profile.')
expect(page).toContain('Choose Delete Account and follow the confirmation steps.')
expect(page).toContain('within 30 days')
expect(page).toContain('required for legal purposes')
expect(page).toContain('relationship data, question responses, match data, chat messages and photos')
})
})
+89
View File
@@ -0,0 +1,89 @@
import Link from 'next/link'
import { ArrowLeft } from 'lucide-react'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Delete your Sauci account',
description: 'How to delete your Sauci account and associated data.',
}
export default function DeleteAccountPage() {
return (
<main className="min-h-screen bg-gradient-to-b from-background-light to-background">
<header className="py-6 px-6 border-b border-white/5">
<div className="max-w-4xl mx-auto flex items-center gap-4">
<Link href="/" className="flex items-center gap-2 text-white/60 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
<span>Back to Home</span>
</Link>
</div>
</header>
<div className="max-w-4xl mx-auto px-6 py-12">
<h1 className="text-4xl font-bold mb-4">Delete your Sauci account</h1>
<p className="text-white/70 leading-relaxed text-lg">
You can request deletion of your Sauci account and associated data without reinstalling the app.
</p>
<div className="glass p-6 my-8">
<h2 className="text-2xl font-semibold text-white mb-3">Request deletion by email</h2>
<p className="text-white/70 leading-relaxed mb-4">
Email us from the address associated with your Sauci account and ask us to delete your account.
</p>
<a
href="mailto:privacy@sauci.app?subject=Sauci%20account%20deletion%20request"
className="inline-flex rounded-lg bg-primary px-5 py-3 font-semibold text-white hover:opacity-90 transition-opacity"
>
Request account deletion
</a>
<p className="text-white/60 mt-4">
Or email <a href="mailto:privacy@sauci.app" className="text-primary hover:underline">privacy@sauci.app</a>.
</p>
</div>
<div className="prose prose-invert prose-lg max-w-none space-y-8">
<section>
<h2 className="text-2xl font-semibold text-white mb-4">Delete your account in the Sauci app</h2>
<ol className="list-decimal pl-6 text-white/70 space-y-2">
<li>Open Sauci and go to your Profile.</li>
<li>Select Account.</li>
<li>Choose Delete Account and follow the confirmation steps.</li>
</ol>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">What deletion covers</h2>
<p className="text-white/70 leading-relaxed">
Deleting an account permanently removes the account and profile, relationship data, question responses, match data, chat messages and photos, encryption keys, and other cloud-stored data associated with that account.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">Timing and limited retention</h2>
<p className="text-white/70 leading-relaxed">
We will delete or anonymize your personal information within 30 days. We may retain information where required for legal purposes.
</p>
</section>
<section>
<h2 className="text-2xl font-semibold text-white mb-4">Need help?</h2>
<p className="text-white/70 leading-relaxed">
For questions about deletion or your data, contact <a href="mailto:privacy@sauci.app" className="text-primary hover:underline">privacy@sauci.app</a>.
</p>
</section>
</div>
</div>
<footer className="py-8 px-6 border-t border-white/5">
<div className="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-xl font-bold gradient-text">Sauci</span>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/" className="hover:text-white transition-colors">Home</Link>
</div>
</div>
</footer>
</main>
)
}
+10 -6
View File
@@ -129,9 +129,12 @@ export default function PrivacyPolicy() {
<li><strong>Portability:</strong> Request a copy of your data in a portable format.</li>
<li><strong>Opt-out:</strong> Disable push notifications through your device settings.</li>
</ul>
<p className="text-white/70 leading-relaxed mt-4">
To exercise these rights, please contact us at <a href="mailto:privacy@sauci.app" className="text-primary hover:underline">privacy@sauci.app</a>.
</p>
<p className="text-white/70 leading-relaxed mt-4">
To exercise these rights, please contact us at <a href="mailto:privacy@sauci.app" className="text-primary hover:underline">privacy@sauci.app</a>.
</p>
<p className="text-white/70 leading-relaxed mt-4">
For account deletion instructions and an email deletion request, visit our <Link href="/delete-account" className="text-primary hover:underline">account deletion page</Link>.
</p>
</section>
<section>
@@ -174,9 +177,10 @@ export default function PrivacyPolicy() {
<footer className="py-8 px-6 border-t border-white/5">
<div className="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-xl font-bold gradient-text">Sauci</span>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/delete-account" className="hover:text-white transition-colors">Delete Account</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/" className="hover:text-white transition-colors">Home</Link>
</div>
</div>
+4 -3
View File
@@ -173,9 +173,10 @@ export default function RedeemPage() {
<footer className="py-8 px-6 border-t border-white/5 mt-auto">
<div className="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-xl font-bold gradient-text">Sauci</span>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/delete-account" className="hover:text-white transition-colors">Delete Account</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/" className="hover:text-white transition-colors">Home</Link>
</div>
</div>
+4 -3
View File
@@ -235,9 +235,10 @@ export default function TermsAndConditions() {
<footer className="py-8 px-6 border-t border-white/5">
<div className="max-w-4xl mx-auto flex flex-col sm:flex-row items-center justify-between gap-4">
<span className="text-xl font-bold gradient-text">Sauci</span>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<div className="flex gap-6 text-sm text-white/40">
<Link href="/privacy" className="hover:text-white transition-colors">Privacy</Link>
<Link href="/delete-account" className="hover:text-white transition-colors">Delete Account</Link>
<Link href="/terms" className="hover:text-white transition-colors">Terms</Link>
<Link href="/" className="hover:text-white transition-colors">Home</Link>
</div>
</div>
+9 -6
View File
@@ -24,12 +24,15 @@ export default function Footer() {
{/* Links */}
<div className="flex flex-wrap items-center justify-center gap-6">
<Link href="/privacy" className="text-white/60 hover:text-white transition-colors text-sm">
Privacy Policy
</Link>
<Link href="/terms" className="text-white/60 hover:text-white transition-colors text-sm">
Terms & Conditions
</Link>
<Link href="/privacy" className="text-white/60 hover:text-white transition-colors text-sm">
Privacy Policy
</Link>
<Link href="/delete-account" className="text-white/60 hover:text-white transition-colors text-sm">
Delete Account
</Link>
<Link href="/terms" className="text-white/60 hover:text-white transition-colors text-sm">
Terms & Conditions
</Link>
<a href="mailto:support@sauci.app" className="text-white/60 hover:text-white transition-colors text-sm">
Contact
</a>
+3 -3
View File
@@ -2,9 +2,9 @@ version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
build:
context: ../..
dockerfile: apps/web/Dockerfile
args:
- NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
- NEXT_PUBLIC_POSTHOG_HOST=${NEXT_PUBLIC_POSTHOG_HOST:-https://eu.i.posthog.com}
+1
View File
@@ -11,6 +11,7 @@ historical notes. Agents should load only the section relevant to their task.
- [Testing strategy](testing/strategy.md)
- [Database migration safety](operations/database-migrations.md)
- [Backend deployment and branching](operations/backend-deployments.md)
- [Website deployment](operations/web-deployments.md)
- [Agent routing and effort](agents/routing.md)
- [Learning promotion](agents/learnings.md)
- [Authentication](authentication.md)
+26
View File
@@ -0,0 +1,26 @@
# Website deployment
Dokploy deploys the public Sauci website from the `main` branch using
`apps/web/docker-compose.prod.yml`. The Compose file builds from the repository
root so the Docker build can use the committed workspace lockfile. Its Dockerfile
builds `@sauci/web` and runs the Next standalone server from its workspace path.
## Required configuration
`NEXT_PUBLIC_API_URL` is required at build time and must be the intended public
standalone API URL. `NEXT_PUBLIC_POSTHOG_KEY` is optional. When it is set,
`NEXT_PUBLIC_POSTHOG_HOST` defaults to `https://eu.i.posthog.com` unless Dokploy
provides a different value. These public values are inlined into the Next build;
do not place server credentials in them.
## Promotion and acceptance
Promote the verified website commit to `main` before allowing Dokploy to deploy
it. After the deployment reports success, verify valid TLS and HTTP 200 for:
- `https://sauci.app/`
- `https://sauci.app/delete-account`
The second check is required for the Google Play account-deletion resource. A
successful Dokploy deployment alone is not sufficient evidence that either URL
is publicly reachable.