feat(internal-admin): user lifecycle (suspend/reactivate/ban) #27

Merged
root merged 2 commits from dev into main 2026-05-18 09:35:06 +03:00
Owner

Phase B of Süper Panel mutations. Status column on users + AuthGuard enforcement + 3 InternalTokenGuard endpoints. Migration 0006.

Phase B of Süper Panel mutations. Status column on users + AuthGuard enforcement + 3 InternalTokenGuard endpoints. Migration 0006.
root added 2 commits 2026-05-18 09:35:05 +03:00
EMEX leaf parts fetch was averaging 12.5s cold per category (Honda Civic
prod traces). Almost all of that lived in the scraper's two waterfall page
loads, each of which (a) waited for `networkidle` — which only fires after
analytics/ads stop chattering — then (b) slept an unconditional 2 seconds.
The actual extraction work runs in <100ms once the DOM is parsed.

Three focused changes in scripts/emex-vin-scraper.js:

1) getParts() resolves the Unit.aspx URL via a plain `fetch()` of
   QuickDetails.aspx instead of opening it in the browser. Probe showed
   the page is fully server-rendered and the anchor is present in the
   initial HTML, so the browser stage is dead weight (~3s saved). Falls
   back to a browser load if the fetch fails or the anchor isn't there,
   so catalogs that gate the link behind JS still work.

2) Both remaining `page.goto()` calls in getParts/getCategories/
   getCategoryTree drop `waitUntil:'networkidle'` for `'domcontentloaded'`
   plus a targeted `waitForSelector('img.dragger' | 'a[href*=…]', {timeout:5000-8000})`.
   `.catch(()=>null)` makes the wait advisory — the evaluate() below has
   its own null-safe fallbacks — but in practice the selector is present
   well before networkidle would have fired.

3) Removes the two unconditional `setTimeout(r,2000)` sleeps in getParts.
   They predate the selector-wait pattern and were belt-and-braces.

Expected: ~12.5s → ~3-5s cold leaf fetch. Warm path (DB-cached) is
unchanged at ~45ms. Dev-only push for verification before main merge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(internal-admin): user lifecycle — suspend / reactivate / ban
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
96a9d11015
Süper Panel Phase 7 — Phase B. Founder can suspend, reactivate, or ban a
Sase user from the panel. Status enforced in the AuthGuard so blocked
users can no longer make authenticated requests.

Schema (migration 0006)
- users.status varchar(20) default 'active' — active|suspended|banned
- users.status_reason text — free-text reason set on transition
- users.status_changed_at, status_changed_by uuid — audit metadata
- users_status_idx

Auth
- AuthGuard rejects 'suspended' / 'banned' with TR-localized message.
- auth.ts: declared `status` as a Better Auth additionalField so the
  session.user object exposes it (matches how `role` is wired).

Endpoints (InternalTokenGuard)
- POST /internal/admin/users/:id/suspend     { reason, founderId }
- POST /internal/admin/users/:id/reactivate  { founderId }
- POST /internal/admin/users/:id/ban         { reason, founderId }

Service
- LifecycleService.setStatus():
  - refuses to touch admin-role users
  - refuses no-op transitions (already in target state)
  - refuses suspended→banned→suspended downgrade path (must reactivate first)
  - on suspend/ban: deletes all sessions for the user (immediate sign-out)
  - returns { from, to, sessionsKilled, changedAt }

Wiring
- LifecycleService + LifecycleController added to InternalAdminModule.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
root merged commit 46ac5d6928 into main 2026-05-18 09:35:06 +03:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: root/sase.tr#27