feat(POS-002): completed feature

This commit is contained in:
chattie
2026-08-21 21:55:43 +02:00
parent c68cb25ed6
commit 0a1d32ecc3
33 changed files with 1169 additions and 107 deletions

View File

@@ -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) {

View File

@@ -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();