feat(POS-002): completed feature
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { AppError } from './errors.js';
|
||||
|
||||
export type Role = 'customer' | 'admin' | 'editor';
|
||||
export type Role = 'customer' | 'admin' | 'editor' | 'pos_cashier' | 'pos_manager';
|
||||
|
||||
export interface CurrentUser {
|
||||
id: string;
|
||||
@@ -27,6 +27,15 @@ export function requireRole(user: CurrentUser, role: Role): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Throws AppError(403) unless the user holds at least one of the allowed roles.
|
||||
* Use when an endpoint accepts multiple roles (e.g. POS-002 endpoints accept
|
||||
* `pos_cashier`, `pos_manager`, and `admin`). */
|
||||
export function requireAnyRole(user: CurrentUser, roles: ReadonlyArray<Role>): void {
|
||||
if (!roles.includes(user.role)) {
|
||||
throw new AppError(403, 'FORBIDDEN', 'Access denied');
|
||||
}
|
||||
}
|
||||
|
||||
/** Throws AppError(403) unless the user is the resource owner or an admin. */
|
||||
export function requireOwnerOrAdmin(user: CurrentUser, ownerId: string): void {
|
||||
if (user.role !== 'admin' && user.id !== ownerId) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { AppError } from '../errors.js';
|
||||
import { requireOwnerOrAdmin, requireRole, type CurrentUser } from '../auth.js';
|
||||
import { requireAnyRole, requireOwnerOrAdmin, requireRole, type CurrentUser } from '../auth.js';
|
||||
|
||||
const customer: CurrentUser = { id: 'user-a', email: 'a@example.com', role: 'customer' };
|
||||
const admin: CurrentUser = { id: 'user-admin', email: 'admin@example.com', role: 'admin' };
|
||||
const posCashier: CurrentUser = { id: 'user-cashier', email: 'cashier@example.com', role: 'pos_cashier' };
|
||||
const posManager: CurrentUser = { id: 'user-manager', email: 'manager@example.com', role: 'pos_manager' };
|
||||
|
||||
function codeOf(fn: () => void): string | undefined {
|
||||
try {
|
||||
@@ -25,6 +27,23 @@ describe('requireRole', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('requireAnyRole', () => {
|
||||
it('allows a user holding any of the allowed roles', () => {
|
||||
expect(() => requireAnyRole(admin, ['pos_cashier', 'pos_manager', 'admin'])).not.toThrow();
|
||||
expect(() => requireAnyRole(posCashier, ['pos_cashier', 'pos_manager'])).not.toThrow();
|
||||
expect(() => requireAnyRole(posManager, ['pos_cashier', 'pos_manager'])).not.toThrow();
|
||||
});
|
||||
|
||||
it('throws 403 FORBIDDEN when no role matches', () => {
|
||||
expect(codeOf(() => requireAnyRole(customer, ['pos_cashier', 'pos_manager']))).toBe('FORBIDDEN');
|
||||
expect(codeOf(() => requireAnyRole(posCashier, ['pos_manager']))).toBe('FORBIDDEN');
|
||||
});
|
||||
|
||||
it('throws 403 FORBIDDEN for empty role list', () => {
|
||||
expect(codeOf(() => requireAnyRole(admin, []))).toBe('FORBIDDEN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('requireOwnerOrAdmin', () => {
|
||||
it('allows the owner regardless of role', () => {
|
||||
expect(() => requireOwnerOrAdmin(customer, 'user-a')).not.toThrow();
|
||||
|
||||
Reference in New Issue
Block a user