feat(internal-admin): user lifecycle — suspend / reactivate / ban
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled

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>
This commit is contained in:
2026-05-18 09:34:52 +03:00
parent 3514272936
commit 96a9d11015
9 changed files with 5593 additions and 2 deletions

View File

@@ -36,6 +36,17 @@ export class AuthGuard implements CanActivate {
throw new UnauthorizedException("Kimlik doğrulama gerekli");
}
// Süper Panel admin lifecycle controls: block non-active users.
// The status column is added by migration 0006; tolerate older rows by
// treating missing/empty as "active".
const status = (session.user as { status?: string }).status ?? "active";
if (status === "suspended") {
throw new UnauthorizedException("Hesabınız askıya alınmıştır. Destek ile iletişime geçin.");
}
if (status === "banned") {
throw new UnauthorizedException("Hesabınız kapatılmıştır.");
}
request.user = session.user;
request.session = session.session;
return true;