Merge branch 'downloads-and-deletion-flows' (task 11)
# Conflicts: # src/routes/privacy-policy/life-and-lineage.tsx
This commit is contained in:
84
src/routes/nessa/deletion-content.test.ts
Normal file
84
src/routes/nessa/deletion-content.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
53
src/routes/nessa/deletion-content.ts
Normal file
53
src/routes/nessa/deletion-content.ts
Normal 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."
|
||||
};
|
||||
61
src/routes/nessa/deletion.tsx
Normal file
61
src/routes/nessa/deletion.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user