feat(internal-admin): user lifecycle — suspend / reactivate / ban
Some checks failed
QA Gate (P0/P1) / Test affected app (pull_request) Has been cancelled
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:
@@ -22,6 +22,15 @@ export const users = pgTable(
|
||||
emailVerified: boolean("email_verified").default(false).notNull(),
|
||||
image: text("image"),
|
||||
role: varchar("role", { length: 20 }).default("user").notNull(),
|
||||
// Lifecycle state controlled by Süper Panel admin actions.
|
||||
// active - normal
|
||||
// suspended - temporary block (payment issue, abuse review, etc.)
|
||||
// banned - permanent block (fraud)
|
||||
// AuthGuard rejects sessions for non-active users.
|
||||
status: varchar("status", { length: 20 }).default("active").notNull(),
|
||||
statusReason: text("status_reason"),
|
||||
statusChangedAt: timestamp("status_changed_at", { withTimezone: true }),
|
||||
statusChangedBy: uuid("status_changed_by"),
|
||||
referralCode: varchar("referral_code", { length: 20 }),
|
||||
referredBy: uuid("referred_by"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||
@@ -30,6 +39,7 @@ export const users = pgTable(
|
||||
(table) => [
|
||||
uniqueIndex("users_email_idx").on(table.email),
|
||||
uniqueIndex("users_referral_code_idx").on(table.referralCode),
|
||||
index("users_status_idx").on(table.status),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user