feat(admin): users management with filters + force-logout action (T2.7 deferred)

Folds in the v1.4.6 deferred backlog item:

* `<UserManagementSection>` now exposes role filters (All / Admins /
  Users — "suspended" is documented as a no-op since the User model
  lacks a `suspended` field; mapped to passthrough for forward
  compatibility).
* New per-row "Force logout" action surfaces a confirmation dialog,
  then POSTs to the new endpoint below. Shows a toast with the
  number of sessions revoked.
* `POST /api/admin/users/[id]/force-logout` — admin-only kill switch
  that deletes every Session row for the target user. Audit-logged
  as `admin.user.force-logout`.

Existing role-toggle, edit, and reset-password actions are untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 16:59:48 +02:00
1 parent 69c5225cea
commit 5957e1827e
2 files changed
+203 -13

No files matched your search

@@ -0,0 +1,56 @@
/**
* POST /api/admin/users/[id]/force-logout — admin-only kill-switch.
*
* Deletes every active session for the target user. The next request
* from any of their browser/native clients will see the cookie/bearer
* rejected and bounce to /auth/login. API tokens are NOT touched here
* (admins manage those from the API-Tokens section); this endpoint is
* specifically for "I see this user's session active in audit-log,
* boot them now".
*/
import { prisma } from "@/lib/db";
import { apiHandler, requireAdmin } from "@/lib/api-handler";
import { auditLog } from "@/lib/auth/audit";
import { apiSuccess, apiError, getClientIp } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { NextRequest } from "next/server";
export const POST = apiHandler(
async (
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) => {
const { user: admin } = await requireAdmin();
const { id } = await params;
annotate({
action: {
name: "admin.users.force-logout",
entity_type: "user",
entity_id: id,
},
});
const target = await prisma.user.findUnique({
where: { id },
select: { id: true, username: true },
});
if (!target) return apiError("User not found", 404);
// Hard-delete all sessions; the cascading cookie + bearer guards in
// `requireAuth()` reject anything missing from this table.
const result = await prisma.session.deleteMany({
where: { userId: id },
});
await auditLog("admin.user.force-logout", {
userId: admin.id,
ipAddress: getClientIp(request),
details: { targetUserId: id, sessionsRevoked: result.count },
});
annotate({ meta: { sessions_revoked: result.count } });
return apiSuccess({ sessionsRevoked: result.count });
},
);