mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
- Add CYCLE_FERTILE_SOON to the cycle-purge push_attempts eventType list so a data reset leaves no dated reproductive-intent delivery trace. - Rate-limit the custom-symptom mutations (POST create, PATCH, DELETE) under a shared cycle:symptom:custom:<userId> bucket (30/min), mirroring the insights route, so a session can't flood the encrypted-label catalogue. - Re-run the active-count cap inside PATCH when a symptom reactivates (hidden → active); a hidden row doesn't count toward the cap, so without the recheck a user could exceed it by hiding then re-enabling rows. - Gate the log-day "limit reached" copy on the cap errorCode rather than any create failure, so a transient/validation error no longer masquerades as the cap being hit; thread the returned key so a new custom symptom auto-selects. - Move the history-chart bar fill alphas into named phase-tokens (HISTORY_REST_OPACITY / HISTORY_PERIOD_OPACITY) so bar intensity lives in one token source alongside FLOW_OPACITY. - Strengthen the confirmed-ovulation calendar marker (ring 1.5px → 2px, fill alpha 0.16 → 0.22) so it stays distinct from the predicted dot on small light screens; keeps its data-attr + aria naming. - Raise the phase-education chip border from /10 to /15 for legible contrast on the tinted tile.
115 lines
4.3 KiB
TypeScript
115 lines
4.3 KiB
TypeScript
/**
|
|
* `DELETE /api/cycle/all` — one-click cycle hard-delete purge.
|
|
*
|
|
* Distinct from the per-row soft-delete (`DELETE /api/cycle/day-logs/{id}`,
|
|
* which leaves a tombstone for the sync feed): this HARD-deletes every
|
|
* cycle row the user owns — day-logs (and their symptom links, by cascade),
|
|
* menstrual cycles, predictions, the cycle audit trail, and the cycle
|
|
* reminder-delivery rows in the push-attempts ledger — so no dated
|
|
* reproductive trace survives in Postgres (the post-Dobbs threat model). The
|
|
* `CycleProfile` row is left in place (gate + settings keep working) but its
|
|
* intent-revealing fields — goal + cycle/period/luteal priors — are reset to
|
|
* neutral defaults, so no reproductive intent survives either.
|
|
*
|
|
* Gated (`cycle.disabled` 403) and owner-scoped. The purge action itself IS
|
|
* audited (written after the transaction, so it survives) — its `details`
|
|
* carry only counts, no reproductive content.
|
|
*/
|
|
import { NextRequest } from "next/server";
|
|
|
|
import { prisma } from "@/lib/db";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { annotate } from "@/lib/logging/context";
|
|
import { auditLog } from "@/lib/auth/audit";
|
|
import { apiSuccess, getClientIp } from "@/lib/api-response";
|
|
import { requireCycleEnabled } from "@/lib/cycle/gate";
|
|
|
|
export const DELETE = apiHandler(async (request: NextRequest) => {
|
|
const { user } = await requireAuth();
|
|
|
|
const gate = await requireCycleEnabled(user.id, user.gender);
|
|
if (!gate.enabled) return gate.response;
|
|
|
|
// Hard delete in a transaction. Symptom links cascade off the day-log
|
|
// delete (the schema's onDelete: Cascade). Order child-before-parent for
|
|
// the rows that don't cascade.
|
|
const cleared = await prisma.$transaction(async (tx) => {
|
|
const dayLogs = await tx.cycleDayLog.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
// Per-user custom symptoms carry an intent-revealing free-text label
|
|
// (encrypted at rest). A purge that promises "nothing reproductive
|
|
// persists" must drop them too; their links cascade off the row delete.
|
|
const customSymptoms = await tx.cycleSymptom.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
const predictions = await tx.cyclePrediction.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
const cycles = await tx.menstrualCycle.deleteMany({
|
|
where: { userId: user.id },
|
|
});
|
|
// The cycle audit trail carries dated reproductive-adjacent details
|
|
// (period boundaries, the AVOID_PREGNANCY goal-nudge, goal changes). A
|
|
// purge that promises "nothing reproductive persists" must clear it too.
|
|
// The purge's own audit row is written AFTER this transaction, so it
|
|
// survives (counts only, no reproductive content).
|
|
const auditRows = await tx.auditLog.deleteMany({
|
|
where: {
|
|
userId: user.id,
|
|
OR: [
|
|
{ action: { startsWith: "cycle." } },
|
|
{ action: "user.cycle-prefs.update" },
|
|
],
|
|
},
|
|
});
|
|
// The shared push-attempts ledger holds dated cycle reminder-delivery
|
|
// rows (eventType + createdAt) for up to 90 days — drop the cycle ones.
|
|
const pushRows = await tx.pushAttempt.deleteMany({
|
|
where: {
|
|
userId: user.id,
|
|
eventType: {
|
|
in: [
|
|
"CYCLE_PERIOD_SOON",
|
|
"CYCLE_PERIOD_CONFIRM",
|
|
"CYCLE_FERTILE_SOON",
|
|
],
|
|
},
|
|
},
|
|
});
|
|
// Reset the reproductive INTENT carried on the profile (goal + cycle/period/
|
|
// luteal priors) to neutral defaults so a purge leaves no intent-revealing
|
|
// state, while keeping the row so the gate + settings keep working.
|
|
await tx.cycleProfile.updateMany({
|
|
where: { userId: user.id },
|
|
data: {
|
|
goal: "GENERAL_HEALTH",
|
|
typicalCycleLength: null,
|
|
typicalPeriodLength: null,
|
|
lutealPhaseLength: null,
|
|
},
|
|
});
|
|
return {
|
|
dayLogs: dayLogs.count,
|
|
customSymptoms: customSymptoms.count,
|
|
predictions: predictions.count,
|
|
cycles: cycles.count,
|
|
auditRows: auditRows.count,
|
|
pushRows: pushRows.count,
|
|
};
|
|
});
|
|
|
|
await auditLog("cycle.purge", {
|
|
userId: user.id,
|
|
ipAddress: getClientIp(request),
|
|
details: cleared,
|
|
});
|
|
|
|
annotate({
|
|
action: { name: "cycle.purge" },
|
|
meta: cleared,
|
|
});
|
|
|
|
return apiSuccess({ purged: true, ...cleared });
|
|
});
|