Backend-side admin-auth. Admin-App (apps/admin/) braucht das damit
useAdminAuth.verifyAdminRole() nach Login server-side prüfen kann ob User
in admin_users-tabelle steht.
New schema:
- model AdminUser → table rebreak.admin_users (user_id UUID PK FK Profile.id,
created_at, added_by). Migration 20260508_admin_users/migration.sql.
- ⚠️ SCHEMA-MIGRATION — NICHT autopushen. User entscheidet wann pipeline
triggert.
New backend code:
- backend/server/db/admin.ts: isAdminUser(userId) → boolean
- backend/server/utils/auth.ts: requireAdmin(event) wraps requireUser +
isAdminUser-check. Throws 403 wenn nicht admin.
- backend/server/api/admin/verify-admin.get.ts: GET endpoint. Returns
{ isAdmin: true, userId, email } bei success, 403 sonst, 401 if not auth'd.
Tests (5 cases in tests/admin/verify-admin.test.ts):
- isAdminUser DB-layer: row exists/null
- requireAdmin: admin → user, non-admin → 403, no token → 401
- Endpoint: admin → success, non-admin → 403
Pending User-Actions nach Push+Deploy:
1. Migration deploy auf staging:
ssh rebreak-server && cd /srv/rebreak && pnpm exec prisma migrate deploy
2. Seed-Admin eintragen:
INSERT INTO "rebreak"."admin_users" ("user_id", "created_at")
VALUES ('128df360-2008-4d6f-8aa1-bdb41ec1362f', NOW())
ON CONFLICT DO NOTHING;
3. Admin-App composables/useAdminAuth.ts kann dann verifyAdminRole()
gegen GET /api/admin/verify-admin aufrufen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
/**
|
|
* Tests for GET /api/admin/verify-admin
|
|
*
|
|
* Covers:
|
|
* - happy path: User ist admin → isAdmin: true
|
|
* - 403: User ist authentifiziert aber NICHT in admin_users
|
|
* - 401: User ist nicht eingeloggt
|
|
*
|
|
* Strategy:
|
|
* - isAdminUser DB-layer: tested directly with mocked Prisma
|
|
* - requireAdmin util: tested directly with mocked requireUser + isAdminUser
|
|
* - endpoint handler: tested with mocked requireAdmin
|
|
*/
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
// ─── Prisma mock ─────────────────────────────────────────────────────────────
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
adminUser: {
|
|
findUnique: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock("../../server/utils/prisma", () => ({
|
|
usePrisma: () => prismaMock,
|
|
}));
|
|
|
|
// ─── requireUser mock (used by requireAdmin tests) ───────────────────────────
|
|
|
|
const requireUserMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock("../../server/utils/auth", async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import("../../server/utils/auth")>();
|
|
return {
|
|
...actual,
|
|
requireUser: requireUserMock,
|
|
};
|
|
});
|
|
|
|
import { isAdminUser } from "../../server/db/admin";
|
|
import { requireAdmin } from "../../server/utils/auth";
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
// ─── isAdminUser DB-layer ────────────────────────────────────────────────────
|
|
|
|
describe("isAdminUser — user IS in admin_users", () => {
|
|
it("returns true when row exists", async () => {
|
|
prismaMock.adminUser.findUnique.mockResolvedValueOnce({
|
|
userId: "128df360-2008-4d6f-8aa1-bdb41ec1362f",
|
|
createdAt: new Date(),
|
|
addedBy: null,
|
|
});
|
|
|
|
const result = await isAdminUser("128df360-2008-4d6f-8aa1-bdb41ec1362f");
|
|
|
|
expect(result).toBe(true);
|
|
expect(prismaMock.adminUser.findUnique).toHaveBeenCalledWith({
|
|
where: { userId: "128df360-2008-4d6f-8aa1-bdb41ec1362f" },
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isAdminUser — user NOT in admin_users", () => {
|
|
it("returns false when row is null", async () => {
|
|
prismaMock.adminUser.findUnique.mockResolvedValueOnce(null);
|
|
|
|
const result = await isAdminUser("some-random-user-id");
|
|
|
|
expect(result).toBe(false);
|
|
});
|
|
});
|
|
|
|
// ─── requireAdmin util ───────────────────────────────────────────────────────
|
|
|
|
describe("requireAdmin — happy path (admin user)", () => {
|
|
it("returns user object when authenticated and in admin_users", async () => {
|
|
const fakeUser = {
|
|
id: "128df360-2008-4d6f-8aa1-bdb41ec1362f",
|
|
email: "chahinebrini@gmail.com",
|
|
};
|
|
requireUserMock.mockResolvedValueOnce(fakeUser);
|
|
prismaMock.adminUser.findUnique.mockResolvedValueOnce({
|
|
userId: fakeUser.id,
|
|
createdAt: new Date(),
|
|
addedBy: null,
|
|
});
|
|
|
|
const result = await requireAdmin({} as Parameters<typeof requireAdmin>[0]);
|
|
|
|
expect(result).toMatchObject({ id: fakeUser.id, email: fakeUser.email });
|
|
});
|
|
});
|
|
|
|
describe("requireAdmin — 403 (non-admin)", () => {
|
|
it("throws 403 when user is authenticated but not in admin_users", async () => {
|
|
requireUserMock.mockResolvedValueOnce({
|
|
id: "regular-user-id",
|
|
email: "user@example.com",
|
|
});
|
|
prismaMock.adminUser.findUnique.mockResolvedValueOnce(null);
|
|
|
|
await expect(
|
|
requireAdmin({} as Parameters<typeof requireAdmin>[0]),
|
|
).rejects.toMatchObject({ statusCode: 403 });
|
|
});
|
|
});
|
|
|
|
describe("requireAdmin — 401 (not logged in)", () => {
|
|
it("propagates 401 from requireUser when token missing", async () => {
|
|
const authError = Object.assign(new Error("Nicht eingeloggt"), {
|
|
statusCode: 401,
|
|
});
|
|
requireUserMock.mockRejectedValueOnce(authError);
|
|
|
|
await expect(
|
|
requireAdmin({} as Parameters<typeof requireAdmin>[0]),
|
|
).rejects.toMatchObject({ statusCode: 401 });
|
|
});
|
|
});
|
|
|
|
// ─── verify-admin endpoint handler ───────────────────────────────────────────
|
|
|
|
describe("verify-admin endpoint — returns isAdmin: true for admin", () => {
|
|
it("returns { success: true, data: { isAdmin: true, userId, email } }", async () => {
|
|
const fakeUser = {
|
|
id: "128df360-2008-4d6f-8aa1-bdb41ec1362f",
|
|
email: "chahinebrini@gmail.com",
|
|
};
|
|
// requireAdmin is backed by requireUser mock (via spread in vi.mock above)
|
|
requireUserMock.mockResolvedValueOnce(fakeUser);
|
|
prismaMock.adminUser.findUnique.mockResolvedValueOnce({
|
|
userId: fakeUser.id,
|
|
createdAt: new Date(),
|
|
addedBy: null,
|
|
});
|
|
|
|
const mod = await import("../../server/api/admin/verify-admin.get");
|
|
const handler =
|
|
typeof mod.default === "function"
|
|
? mod.default
|
|
: (mod.default as { handler?: unknown }).handler;
|
|
|
|
const result = await (handler as (e: unknown) => Promise<unknown>)({});
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: {
|
|
isAdmin: true,
|
|
userId: fakeUser.id,
|
|
email: fakeUser.email,
|
|
},
|
|
});
|
|
});
|
|
});
|