feat(internal-admin): user lifecycle (suspend/reactivate/ban) #27
Reference in New Issue
Block a user
Delete Branch "dev"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase B of Süper Panel mutations. Status column on users + AuthGuard enforcement + 3 InternalTokenGuard endpoints. Migration 0006.
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>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>