[object Object]

← back to CelebritySignatures

backend: add POST /api/auth/delete (Apple 5.1.1(v) account deletion)

77c8879525e6e60345fc2a542bff79ab654b6e31 · 2026-08-05 15:43:37 -0700 · Steve

Cookie-authed; removes the user from users.json, their sessions, saves, and
private uploads (+ unlinks the files). Required by App Store guidelines for the
new native iOS app. Reuses existing currentUser/load/store/sendJSON/clearCookie
helpers. node --check passes. Prod deploy stays gated.

Files touched

Diff

commit 77c8879525e6e60345fc2a542bff79ab654b6e31
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Aug 5 15:43:37 2026 -0700

    backend: add POST /api/auth/delete (Apple 5.1.1(v) account deletion)
    
    Cookie-authed; removes the user from users.json, their sessions, saves, and
    private uploads (+ unlinks the files). Required by App Store guidelines for the
    new native iOS app. Reuses existing currentUser/load/store/sendJSON/clearCookie
    helpers. node --check passes. Prod deploy stays gated.
---
 server.js | 29 ++++++++++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 65188cd..fa6b203 100644
--- a/server.js
+++ b/server.js
@@ -3,7 +3,7 @@
 // Adds lightweight email/password accounts (scrypt + cookie sessions, JSON-backed)
 // so a visitor can SAVE a mural placement they set in the wall studio.
 import { createServer } from 'node:http';
-import { readFile, writeFile, appendFile, mkdir } from 'node:fs/promises';
+import { readFile, writeFile, appendFile, mkdir, unlink } from 'node:fs/promises';
 import { extname, join } from 'node:path';
 import { fileURLToPath } from 'node:url';
 import { scryptSync, randomBytes, timingSafeEqual, createHash } from 'node:crypto';
@@ -225,6 +225,33 @@ createServer(async (req, res) => {
       const u = await currentUser(req);
       return sendJSON(res, 200, u ? { ok: true, user: pubUser(u) } : { ok: true, user: null });
     }
+    // Account deletion — Apple App Store Guideline 5.1.1(v). Cookie-authed via the
+    // same currentUser(req) mechanism as the rest of /api/auth/*. Removes the user
+    // and ALL their data (sessions, saved placements, uploaded signature files),
+    // then clears the session cookie exactly like /api/auth/logout.
+    if (path === '/api/auth/delete' && M === 'POST') {
+      const u = await currentUser(req);
+      if (!u) return sendJSON(res, 401, { ok: false, error: 'not signed in' });
+      const uid = u.id;
+      // a. drop the user object
+      const users = await load('users.json', []);
+      await store('users.json', users.filter(x => x.id !== uid));
+      // b. drop every session belonging to this user
+      const sessions = await load('sessions.json', {});
+      for (const [tok, s] of Object.entries(sessions)) { if (s && s.userId === uid) delete sessions[tok]; }
+      await store('sessions.json', sessions);
+      // c. drop this user's saved placements
+      const saves = await load('saves.json', []);
+      await store('saves.json', saves.filter(s => s.userId !== uid));
+      // d. delete this user's private upload files (best-effort), then drop the rows
+      const ups = await load('celebrity-uploads.json', []);
+      for (const x of ups.filter(e => e.userId === uid)) {
+        if (x.file) { try { await unlink(join(UPLOADS_DIR, x.file)); } catch {} }
+      }
+      await store('celebrity-uploads.json', ups.filter(e => e.userId !== uid));
+      // e. clear the session cookie (same header the logout route uses)
+      return sendJSON(res, 200, { ok: true }, { 'Set-Cookie': clearCookie });
+    }
 
     // ===== GAME LEADERBOARD (public, no account needed) =====
     // Top-20 per game+difficulty. HARDENED (Cody gate, yoloforever C2):

← 4bbb628 AdSense onboarding: add /privacy policy page + site-wide foo  ·  back to CelebritySignatures  ·  iOS app: native Expo/EAS scaffold over celebsignatures.com A 73d9137 →