feat: add product-aware deletion emails and subdomain deletion/downloads routes

- Extend DeletionForm with product discriminator and configurable cooldown cookie
- Extract env-free deletion email helpers into deletion-email.ts with Lineage/Nessa branding
- Make misc.sendDeletionRequestEmail mutation product-aware (subject, html content, cookie)
- Replace legacy /deletion/life-and-lineage page with 308 redirect to lineage subdomain
- Add Lineage and Nessa subdomain deletion route modules with shared content components
- Add Lineage downloads route modules with shared content
- Update privacy policy deletion link to lineage subdomain
This commit is contained in:
2026-07-23 12:32:14 -04:00
parent 5705c5add7
commit 59d129b46b
17 changed files with 1138 additions and 41 deletions

View File

@@ -0,0 +1,84 @@
/**
* Unit tests for the Nessa per-subdomain account-deletion page content
* (task 11).
*
* Asserts against pure constants exported from `deletion-content.ts` — no
* solid-js / router / DOM. Covers the task-11 acceptance matrix for the
* Nessa deletion flow:
* - Product discriminator is `"nessa"` (selects Nessa-branded email).
* - Cooldown cookie name is Nessa-specific + matches the server-side
* `deletionCookieName("nessa")`.
* - PageHead base title + description.
* - (Assessment rationale documented in `deletion-content.ts`.)
*/
import { describe, it, expect } from "bun:test";
import {
DELETION_PRODUCT_KEY,
DELETION_COOKIE_NAME,
DELETION_GRACE_PERIOD_LABEL,
PAGE_META
} from "~/routes/nessa/deletion-content";
import {
DELETION_PRODUCT_SCHEMA,
deletionCookieName
} from "~/server/api/routers/deletion-email";
describe("Nessa deletion — assessment outcome", () => {
it("defines a product discriminator (deletion flow IS implemented)", () => {
// Nessa stores user data (nessa.ts: users, workouts, workoutPlans, … +
// nessa-community.ts: clubs, clubMemberships). Per task 11 spec step 5,
// a deletion flow IS needed — this page provides it.
expect(typeof DELETION_PRODUCT_KEY).toBe("string");
});
});
describe("Nessa deletion — product discriminator", () => {
it("is \"nessa\" (selects Nessa-branded email)", () => {
expect(DELETION_PRODUCT_KEY).toBe("nessa");
});
it("is accepted by the server-side product schema", () => {
expect(DELETION_PRODUCT_SCHEMA.safeParse(DELETION_PRODUCT_KEY).success).toBe(
true
);
});
});
describe("Nessa deletion — cooldown cookie", () => {
it("uses a Nessa-specific cookie name (independent of Lineage cooldown)", () => {
expect(DELETION_COOKIE_NAME).toBe("nessaDeletionRequestSent");
});
it("matches the server-side deletionCookieName(\"nessa\")", () => {
expect(DELETION_COOKIE_NAME).toBe(
deletionCookieName(DELETION_PRODUCT_KEY)
);
});
it("does NOT collide with the Lineage cooldown cookie", () => {
expect(DELETION_COOKIE_NAME).not.toBe("deletionRequestSent");
});
});
describe("Nessa deletion — grace period label", () => {
it("surfaces a human-readable 24-hour label in the copy", () => {
expect(DELETION_GRACE_PERIOD_LABEL).toBe("24-hour");
});
});
describe("Nessa deletion — PageHead inputs", () => {
it("passes the base title (suffix is appended by PageHead)", () => {
expect(PAGE_META.title).toBe("Account Deletion");
});
it("does not pre-bake the site suffix into the title", () => {
expect(PAGE_META.title).not.toContain("|");
});
it("description mentions Nessa + data removal + grace period", () => {
const desc = PAGE_META.description.toLowerCase();
expect(desc).toContain("nessa");
expect(desc).toContain("removed");
expect(desc).toContain("24-hour");
});
});

View File

@@ -0,0 +1,53 @@
/**
* Pure content + metadata for the Nessa per-subdomain account-deletion page
* (task 11).
*
* Mirrors the `lineage/deletion-content.ts` pattern: imports NOTHING from
* solid-js / @solidjs/router / @solidjs/meta so the constants here can be
* unit-tested in `bun:test` without spinning up the router / MetaProvider.
*
* Nessa deletion assessment (see task 11 spec, step 5):
* - Nessa DOES store user data. `src/server/api/routers/nessa.ts` defines
* per-user tables (`users`, `authProviders`, `workouts`, `workoutPlans`,
* `planExercises`, `planSets`, `routePoints`, `exerciseLibrary`) backed
* by a per-user Turso DB, and `nessa-community.ts` defines shared
* community tables (`clubs`, `clubMemberships`, …) keyed by `userId` /
* `ownerId`. Auth is Clerk. → A deletion flow IS needed.
* - Implemented here as the SAME email-request pattern Lineage uses: the
* requester submits their email via `DeletionForm`; the generalized
* `misc.sendDeletionRequestEmail` mutation sends a Nessa-branded email
* to michael@freno.me + the requester; Mike then manually drops the
* Nessa `users` row (+ cascades), the per-user Turso DB, and the user's
* community memberships within the 24h grace window. An authenticated
* self-delete via `nessa.deleteUser` + the Clerk Users API remains a
* follow-up (it requires Clerk backend secret wiring that is out of
* scope for the subdomain-routing feature); the email-request flow gives
* users a real, immediate deletion path today.
*
* Cross-task contracts:
* - `DELETION_PRODUCT_KEY = "nessa"` selects Nessa branding + the
* `nessaDeletionRequestSent` cooldown cookie (server-side
* `deletionCookieName("nessa")`).
* - `DELETION_COOKIE_NAME` MUST match `deletionCookieName("nessa")` so the
* client countdown reads the cookie the server actually sets.
*/
import type { PageHeadProps } from "~/components/page-head-meta";
/** Product discriminator forwarded to `misc.sendDeletionRequestEmail`. */
export const DELETION_PRODUCT_KEY = "nessa" as const;
/**
* Cooldown cookie name — MUST match `deletionCookieName("nessa")` on the
* server (`nessaDeletionRequestSent`).
*/
export const DELETION_COOKIE_NAME = "nessaDeletionRequestSent";
/** Human-readable grace-window copy interpolated into the page body. */
export const DELETION_GRACE_PERIOD_LABEL = "24-hour";
/** Base page title (site suffix appended by PageHead). */
export const PAGE_META: PageHeadProps = {
title: "Account Deletion",
description:
"Request account deletion for Nessa. Your Nessa account, workout data, and community memberships are removed after a 24-hour grace period."
};

View File

@@ -0,0 +1,61 @@
/**
* Nessa per-subdomain account-deletion page — `nessa.freno.me/deletion`
* (task 11).
*
* Served at the public browser path `/deletion` (vercel.json host rewrites
* `nessa.freno.me/*` → the internal `/nessa/*` route prefix, leaving the
* browser URL clean — task 02 canonical rule).
*
* Nessa deletion assessment (see `./deletion-content.ts` for the full
* rationale): Nessa stores user data (`users`, `workouts`, `workoutPlans`,
* `exerciseLibrary`, community memberships) in a per-user Turso DB +
* shared community tables, authenticated via Clerk. → A deletion flow IS
* needed; this page provides it via the same email-request pattern Lineage
* uses, reusing the shared `DeletionForm` with `product="nessa"` so the
* generalized `misc.sendDeletionRequestEmail` mutation sends Nessa-branded
* email + writes a Nessa-specific cooldown cookie.
*
* Auth: NO freno.me web-auth — Nessa authenticates via Clerk; the deletion
* request is email-based (the requester may be locked out of their Clerk
* session), NOT an authenticated self-delete. The nav-config does NOT list
* a Nessa deletion link by default, so this page is reachable by direct URL
* + from the Nessa privacy policy (task-provided).
*
* Acceptance: `nessa.localhost:3000/deletion` renders the deletion form.
*/
import { PageHead } from "~/components/PageHead";
import DeletionForm from "~/components/DeletionForm";
import {
DELETION_PRODUCT_KEY,
DELETION_COOKIE_NAME,
DELETION_GRACE_PERIOD_LABEL,
PAGE_META
} from "~/routes/nessa/deletion-content";
export default function NessaDeletionPage() {
return (
<>
<PageHead title={PAGE_META.title} description={PAGE_META.description} />
<div class="pt-20">
<div class="mx-auto p-4 md:p-6 lg:p-12">
<div class="text-text w-full justify-center">
<div class="text-xl">
<em>What will happen</em>:
</div>
Once you send, if a match to the email provided is found in our
system, a {DELETION_GRACE_PERIOD_LABEL} grace period is started
where you can request a cancellation of the account deletion. Once
the grace period ends, your Nessa account entry, your workout and
plan data, and your community memberships will be completely
removed. No data related to the account is retained in any way.
</div>
<DeletionForm
product={DELETION_PRODUCT_KEY}
cookieName={DELETION_COOKIE_NAME}
/>
</div>
</div>
</>
);
}