add isAdmin to secure specific endpoints

This commit is contained in:
StarAppeal
2025-09-15 20:54:19 +02:00
parent 6a9ffde9f6
commit da19a0aaf1
5 changed files with 229 additions and 62 deletions
+23
View File
@@ -0,0 +1,23 @@
import type {NextFunction, Request, Response} from "express";
import {UserService} from "../../db/services/db/UserService";
import {notFound} from "../utils/responses";
export async function isAdmin(
req: Request,
res: Response,
next: NextFunction) {
try {
const payload = req.payload;
const userService = await UserService.create();
const user = await userService.getUserByUUID(payload.uuid);
if (user && user.config.isAdmin) {
next();
} else {
return notFound(res);
}
} catch (error) {
next(error);
}
}
+3 -1
View File
@@ -4,12 +4,13 @@ import {PasswordUtils} from "../utils/passwordUtils";
import {asyncHandler} from "./middleware/asyncHandler";
import {v, validateBody, validateParams} from "./middleware/validate";
import {badRequest, ok} from "./utils/responses";
import {isAdmin} from "./middleware/isAdmin";
export class RestUser {
public createRouter() {
const router = express.Router();
router.get("/", asyncHandler(async (_req, res) => {
router.get("/",isAdmin, asyncHandler(async (_req, res) => {
const userService = await UserService.create();
const users = await userService.getAllUsers();
return ok(res, { users });
@@ -99,6 +100,7 @@ export class RestUser {
validateParams({
id: { required: true, validator: v.isString({ nonEmpty: true }) },
}),
isAdmin,
asyncHandler(async (req, res) => {
const userService = await UserService.create();
const id = req.params.id;
+1 -1
View File
@@ -69,8 +69,8 @@ export class ExtendedWebSocketServer {
// send initial state and settings
// think about emitting the data needed directly to the event Handler
ws.emit(WebsocketEventType.GET_STATE, {});
ws.emit(WebsocketEventType.GET_SETTINGS, {});
ws.emit(WebsocketEventType.GET_STATE, {});
// initiate update user event
ws.emit(WebsocketEventType.UPDATE_USER, {});
+95
View File
@@ -0,0 +1,95 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import { Request, Response, NextFunction } from "express";
import { isAdmin } from "../../../src/rest/middleware/isAdmin";
import { createMockUserService } from "../../helpers/testSetup";
import { UserService } from "../../../src/db/services/db/UserService";
import { notFound } from "../../../src/rest/utils/responses";
vi.mock("../../../src/db/services/db/UserService", () => ({
UserService: {
create: vi.fn(),
},
}));
vi.mock("../../../src/rest/utils/responses", () => ({
notFound: vi.fn(),
}));
describe("isAdmin middleware", () => {
let mockedUserService: any;
let req: Partial<Request>;
let res: Mocked<Response>;
let next: Mocked<NextFunction>;
const uuid = "abcd-coffe";
beforeEach(() => {
vi.clearAllMocks();
mockedUserService = createMockUserService();
vi.mocked(UserService.create).mockResolvedValue(mockedUserService);
req = {
payload: { uuid, username: "username", id: ""}
};
res = {
status: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
} as unknown as Mocked<Response>;
next = vi.fn();
});
afterEach(() => {
vi.resetAllMocks();
});
describe("Success Scenarios", () => {
it("should call next() if user is an admin", async () => {
const mockUser = { uuid, config: { isAdmin: true } };
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
await isAdmin(req as Request, res, next);
expect(UserService.create).toHaveBeenCalledOnce();
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(next).toHaveBeenCalledOnce();
expect(res.status).not.toHaveBeenCalled();
expect(res.send).not.toHaveBeenCalled();
expect(notFound).not.toHaveBeenCalled();
});
});
describe("Failure Scenarios", () => {
it("should call notFound if user is not an admin", async () => {
const mockUser = { uuid, config: { isAdmin: false } };
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
await isAdmin(req as Request, res, next);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(notFound).toHaveBeenCalledWith(res);
expect(next).not.toHaveBeenCalled();
});
it("should call notFound if user does not exist", async () => {
mockedUserService.getUserByUUID.mockResolvedValue(null);
await isAdmin(req as Request, res, next);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(notFound).toHaveBeenCalledWith(res);
expect(next).not.toHaveBeenCalled();
});
it("should handle errors during user fetching", async () => {
const dbError = new Error("Database error");
mockedUserService.getUserByUUID.mockRejectedValue(dbError);
await isAdmin(req as Request, res, next);
expect(next).toHaveBeenCalledWith(expect.objectContaining({ message: 'Database error' }));
});
});
});
+107 -60
View File
@@ -1,8 +1,8 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
import request from "supertest";
import { RestUser } from "../../src/rest/restUser";
import { setupTestEnvironment, type TestEnvironment } from "../helpers/testSetup";
import {RestUser} from "../../src/rest/restUser";
import {setupTestEnvironment, type TestEnvironment} from "../helpers/testSetup";
vi.mock("../../src/db/services/db/UserService", () => ({
UserService: {
@@ -21,6 +21,10 @@ vi.mock("../../src/utils/passwordUtils", () => ({
describe("RestUser", () => {
let testEnv: TestEnvironment;
const requestingUserUUID = "test-user-uuid";
const adminUser = { uuid: requestingUserUUID, config: { isAdmin: true } };
const nonAdminUser = { uuid: requestingUserUUID, config: { isAdmin: false } };
beforeEach(() => {
vi.clearAllMocks();
@@ -32,35 +36,6 @@ describe("RestUser", () => {
vi.resetAllMocks();
});
describe("GET /", () => {
it("should return all users", async () => {
const mockUsers = [
{id: "1", name: "user1", uuid: "uuid1"},
{id: "2", name: "user2", uuid: "uuid2"}
];
testEnv.mockUserService.getAllUsers.mockResolvedValue(mockUsers);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual(mockUsers);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
});
it("should handle empty user list", async () => {
testEnv. mockUserService.getAllUsers.mockResolvedValue([]);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual([]);
});
});
describe("GET /me", () => {
it("should return current user", async () => {
const mockUser = {
@@ -118,7 +93,7 @@ describe("RestUser", () => {
});
it("should return bad request when user not found", async () => {
testEnv. mockUserService.getUserByUUID.mockResolvedValue(null);
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.put("/user/me/spotify")
@@ -240,7 +215,7 @@ describe("RestUser", () => {
password: "old-hashed-password"
};
testEnv. mockUserService.getUserByUUID.mockResolvedValue(mockUser);
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
vi.mocked(PasswordUtils.validatePassword).mockReturnValue({valid: true});
vi.mocked(PasswordUtils.hashPassword).mockResolvedValue("new-hashed-password");
testEnv.mockUserService.updateUser.mockResolvedValue(mockUser);
@@ -352,40 +327,112 @@ describe("RestUser", () => {
});
});
describe("GET /:id", () => {
it("should return user by id", async () => {
const mockUser = {
id: "specific-user-id",
name: "specificuser",
uuid: "specific-uuid"
};
describe("GET / (Admin only)", () => {
testEnv.mockUserService.getUserById.mockResolvedValue(mockUser);
describe("when user is an admin", () => {
beforeEach(() => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(adminUser);
});
const response = await request(testEnv.app)
.get("/user/specific-user-id")
.expect(200);
it("should return all users", async () => {
const mockUsers = [
{id: "1", name: "user1", uuid: "uuid1"},
{id: "2", name: "user2", uuid: "uuid2"}
];
testEnv.mockUserService.getAllUsers.mockResolvedValue(mockUsers);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserById).toHaveBeenCalledWith("specific-user-id");
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual(mockUsers);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
});
it("should handle empty user list", async () => {
testEnv.mockUserService.getAllUsers.mockResolvedValue([]);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual([]);
});
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(null);
describe("when user is not an admin", () => {
it("should return 404 Not Found if user is not an admin", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
const response = await request(testEnv.app)
.get("/user/nonexistent-id")
.expect(400);
await request(testEnv.app)
.get("/user/")
.expect(404);
});
expect(response.body.data.message).toBe("Unable to find matching document with id: nonexistent-id");
});
it("should return 404 Not Found if user does not exist", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
it("should return all users when id is empty", async () => {
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
await request(testEnv.app)
.get("/user/")
.expect(404);
});
});
});
describe("GET /:id (Admin only)", () => {
const specificUserId = "specific-user-id";
const mockUser = {
id: specificUserId,
name: "specificuser",
uuid: "specific-uuid"
};
describe("when user is an admin", () => {
beforeEach(() => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(adminUser);
});
it("should return user by id", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(200);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(testEnv.mockUserService.getUserById).toHaveBeenCalledWith(specificUserId);
});
it("should return bad request when target user is not found", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(null);
const response = await request(testEnv.app)
.get(`/user/nonexistent-id`)
.expect(400);
expect(response.body.data.message).toBe("Unable to find matching document with id: nonexistent-id");
});
});
describe("when user is not an admin", () => {
it("should return 404 Not Found if user is not an admin", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(404);
});
it("should return 404 Not Found if user does not exist", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(404);
});
});
});
});