From 09f1e27c512535c6f5d5a34d59e2e160e0a59fa9 Mon Sep 17 00:00:00 2001 From: Muhammad Saffi Ullah <42832684+saffiullah200@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:25:02 +0200 Subject: [PATCH] feat: add lastOpenedAt field for accurate app-open tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a dedicated User.lastOpenedAt timestamp that updates ONLY on real app-open events (sign-in, social sign-ins, token refresh). Separates "user actually opened the app" from the existing lastActivityTime field, which is set on every authenticated API call and was the cause of the May 2026 bulk- update incident that mass-marked users as Active in Salesforce. Schema: • models/user.js: add `lastOpenedAt: { type: Date, default: null }` App-open write sites (5 routes, all set both lastLogin and lastOpenedAt): • routes/auth/sign-in.js • routes/auth/apple-sign-in.js • routes/auth/google-sign-in.js • routes/auth/facebook-sign-in.js • routes/auth/generate-token.js (token refresh = app reopen) Each write emits a `[app-open]` console log line for audit. NOT set in: • middleware (helpers/index.js) — would recreate the lastActivityTime trap • admin scripts in src/scripts/db/ • the AWS Lambda sync (sync-users does not write this field) Docs + verification tooling: • models/LASTOPENEDAT.md — operating doc • scripts/verify-last-opened-at.js — Mongo-side verification script (snapshot mode + --watch mode for manual sign-in testing) Salesforce mapping (handled in AWS Lambda repo, not here): Mongo `lastOpenedAt` → SF `Contact.Last_App_Opened_At__c` with the omit-when-absent pattern so a future backfill cannot recreate the May 2026 mass-overwrite issue. Note: the production API was hot-deployed with these changes earlier this week to unblock the SF User Status migration; this PR brings the code into git. Co-Authored-By: Claude Opus 4.7 --- src/models/LASTOPENEDAT.md | 116 +++++++++++++++++++++++++++ src/models/user.js | 9 +++ src/routes/auth/apple-sign-in.js | 14 ++-- src/routes/auth/facebook-sign-in.js | 14 ++-- src/routes/auth/generate-token.js | 14 ++++ src/routes/auth/google-sign-in.js | 16 ++-- src/routes/auth/sign-in.js | 12 ++- src/scripts/verify-last-opened-at.js | 105 ++++++++++++++++++++++++ 8 files changed, 280 insertions(+), 20 deletions(-) create mode 100644 src/models/LASTOPENEDAT.md create mode 100644 src/scripts/verify-last-opened-at.js diff --git a/src/models/LASTOPENEDAT.md b/src/models/LASTOPENEDAT.md new file mode 100644 index 00000000..44867988 --- /dev/null +++ b/src/models/LASTOPENEDAT.md @@ -0,0 +1,116 @@ +# `lastOpenedAt` — operating notes + +## What it is + +A Date field on the `User` schema that tracks **real app-open events** — when an +authenticated user genuinely opens or re-opens the AXS Map app. + +Distinct from existing fields: + +| Field | When set | Semantics | +|---|---|---| +| `lastLogin` | Sign-in flows | Last successful password / social sign-in | +| `lastActivityTime` | ⚠️ **Every authenticated API call** (`helpers/index.js:isAuthenticated`) | Way too noisy — every background poll updates this. Source of the May 2026 user-status bug. | +| **`lastOpenedAt`** | **Sign-in flows + token refresh** | Real app-open signal. Used by future Salesforce User Status logic. | + +## Where it IS set (only these places) + +| File | When | +|---|---| +| `routes/auth/sign-in.js` | Successful email+password sign-in | +| `routes/auth/apple-sign-in.js` | Successful Apple sign-in (new or existing user) | +| `routes/auth/google-sign-in.js` | Successful Google sign-in (new or existing user) | +| `routes/auth/facebook-sign-in.js` | Successful Facebook sign-in (new or existing user) | +| `routes/auth/generate-token.js` | Successful refresh-token → new JWT (the AXS Map app was re-opened after the previous JWT expired) | + +Every update logs to stdout: +``` +[app-open] sign-in: userId= lastOpenedAt=2026-05-23T18:00:00.000Z +[app-open] google-sign-in (existing): userId= lastOpenedAt=... +[app-open] token-refresh: userId= lastOpenedAt=... +``` + +This is the audit trail — easy to grep for in CloudWatch / log aggregators. + +## Where it is NOT set (and must NEVER be set) + +- ❌ `helpers/index.js:isAuthenticated` middleware — this was the trap with + `lastActivityTime`. Setting any "user activity" field on every API request + means even silent background polling counts as activity. **Do not extend this.** +- ❌ Any of the AWS Lambda sync functions (`axs-map-sync-users` etc.) — they + don't read or write this field. Confirmed by code grep. +- ❌ Admin bulk scripts in `src/scripts/db/*` — `import-users.js`, + `update-users-avatars.js`, `migrate-scores.js` — none touch this field. +- ❌ MongoDB Atlas Triggers — they fire change events to AWS EventBridge but + don't update the source doc. +- ❌ Salesforce → MongoDB direction — there is no sync in that direction today. + +## How to verify before/after a deploy + +```bash +cd "/Users/saffiullah/AXS Map API" + +# Snapshot — read schema definition and population counts +node src/scripts/verify-last-opened-at.js + +# Watch a specific user in real time (do this BEFORE signing in via the app): +node src/scripts/verify-last-opened-at.js --watch --user me@example.com + +# Then sign in via the app or POST /auth/sign-in. You should see a single line: +# [