harden admin api authorization and add authz regression tests
This commit is contained in:
1 parent
272c9b36dd
commit
10b58245f5
28 files changed
+1336
-240
No files matched your search
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
|
||||
import { NextRequest } from "next/server";
|
||||
import { POST } from "../route";
|
||||
|
||||
jest.mock("@clerk/nextjs/server", () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/database", () => ({
|
||||
getDatabase: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/sync-user", () => ({
|
||||
ensureUserSynced: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("@/lib/logger", () => ({
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("POST /api/notifications authz", () => {
|
||||
const mockAuth = require("@clerk/nextjs/server").auth as jest.Mock;
|
||||
const mockGetDatabase = require("@/lib/database").getDatabase as jest.Mock;
|
||||
const mockEnsureUserSynced = require("@/lib/sync-user")
|
||||
.ensureUserSynced as jest.Mock;
|
||||
|
||||
const mockDb = {
|
||||
getUserById: jest.fn(),
|
||||
createNotification: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetDatabase.mockResolvedValue(mockDb);
|
||||
});
|
||||
|
||||
it("returns 403 for non-staff user", async () => {
|
||||
mockAuth.mockResolvedValue({ userId: "client_1" });
|
||||
mockEnsureUserSynced.mockResolvedValue({
|
||||
id: "client_1",
|
||||
role: "client",
|
||||
gymId: "gym_a",
|
||||
});
|
||||
|
||||
const request = new NextRequest("http://localhost/api/notifications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
targetUserId: "client_2",
|
||||
title: "Hello",
|
||||
message: "Test",
|
||||
type: "system",
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
|
||||
it("returns 403 for cross-gym notify by admin", async () => {
|
||||
mockAuth.mockResolvedValue({ userId: "admin_1" });
|
||||
mockEnsureUserSynced.mockResolvedValue({
|
||||
id: "admin_1",
|
||||
role: "admin",
|
||||
gymId: "gym_a",
|
||||
});
|
||||
mockDb.getUserById.mockResolvedValue({ id: "client_2", gymId: "gym_b" });
|
||||
|
||||
const request = new NextRequest("http://localhost/api/notifications", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
targetUserId: "client_2",
|
||||
title: "Hello",
|
||||
message: "Test",
|
||||
type: "system",
|
||||
}),
|
||||
});
|
||||
|
||||
const response = await POST(request);
|
||||
expect(response.status).toBe(403);
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { getDatabase } from "@/lib/database";
|
||||
import log from "@/lib/logger";
|
||||
import { ensureUserSynced } from "@/lib/sync-user";
|
||||
|
||||
/**
|
||||
* GET /api/notifications
|
||||
@@ -84,6 +85,39 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const db = await getDatabase();
|
||||
const currentUser = await ensureUserSynced(userId, db);
|
||||
|
||||
if (!currentUser) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const canCreateNotifications =
|
||||
currentUser.role === "superAdmin" ||
|
||||
currentUser.role === "admin" ||
|
||||
currentUser.role === "trainer";
|
||||
|
||||
if (!canCreateNotifications) {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const targetUser = await db.getUserById(targetUserId);
|
||||
if (!targetUser) {
|
||||
return NextResponse.json(
|
||||
{ error: "Target user not found" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
currentUser.role !== "superAdmin" &&
|
||||
(!currentUser.gymId || targetUser.gymId !== currentUser.gymId)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Forbidden - Cannot notify users from other gyms" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
const notification = await db.createNotification({
|
||||
id: crypto.randomUUID(),
|
||||
userId: targetUserId,
|
||||
|
||||
Reference in new issue
Block a user