feat(modules): gate achievements behind the achievements module flag

When an account turns the achievements module off the whole gamification
surface disappears. The route enforces the gate after auth and returns the
403 module.disabled envelope, so no badge evaluation, unlock persistence,
or payload is produced. The achievements page, the dashboard recent-unlocks
tile, and the unlock notifier read the resolved module map from
GET /api/auth/me and render nothing / disable their query when the module
is off. Default-on: an absent key reads as enabled, a stale /me payload
coerces to an empty map so no surface vanishes on a partial-deploy
rollback.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 21:25:40 +02:00
1 parent 3264dd2e86
commit 70402ea95a
10 files changed
+361 -5

No files matched your search

+16
View File
@@ -80,6 +80,17 @@ export interface AuthUser {
* stale /me payload (older server image without the field) in `fetchMe`.
*/
cycleTrackingEnabled: boolean;
/**
* v1.18.0 — per-user module enable/disable map resolved server-side
* (`resolveModuleMap`). A `false` entry hides every surface that module
* owns; an absent key reads as enabled (default-on, fail-open), matching
* the gate's disabled-allowlist semantics. A stale /me payload without
* the field coerces to an empty map in `fetchMe`, so every surface stays
* visible rather than vanishing on a partial-deploy rollback. Optional
* so existing `AuthUser` test fixtures stay valid; `fetchMe` always
* populates it (empty map when absent), so live code reads a real map.
*/
modules?: Record<string, boolean>;
}
async function fetchMe(): Promise<AuthUser> {
@@ -117,6 +128,11 @@ async function fetchMe(): Promise<AuthUser> {
// v1.15.0 — coerce against a stale /me payload so the cycle nav entry
// stays hidden by default when the field is absent.
cycleTrackingEnabled: data.cycleTrackingEnabled === true,
// v1.18.0 — coerce against a stale /me payload so module gates default
// to enabled (empty map ⇒ every key reads on) rather than hiding a
// surface on an older server image without the field.
modules:
data.modules && typeof data.modules === "object" ? data.modules : {},
};
}