45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
/**
|
|
* Shared auth contracts. Identity implements the authenticator; consumers
|
|
* (users, future modules) receive it by injection from the composition root.
|
|
* No module ever imports another module for auth.
|
|
*/
|
|
import type { FastifyRequest } from 'fastify';
|
|
import { AppError } from './errors.js';
|
|
|
|
export type Role = 'customer' | 'admin' | 'editor' | 'pos_cashier' | 'pos_manager';
|
|
|
|
export interface CurrentUser {
|
|
id: string;
|
|
email: string;
|
|
role: Role;
|
|
}
|
|
|
|
/**
|
|
* Resolves the session on the request into the current user.
|
|
* Throws AppError(401) when the request is not authenticated.
|
|
*/
|
|
export type Authenticate = (request: FastifyRequest) => Promise<CurrentUser>;
|
|
|
|
/** Throws AppError(403) unless the user holds the required role. */
|
|
export function requireRole(user: CurrentUser, role: Role): void {
|
|
if (user.role !== role) {
|
|
throw new AppError(403, 'FORBIDDEN', 'Access denied');
|
|
}
|
|
}
|
|
|
|
/** 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) {
|
|
throw new AppError(403, 'FORBIDDEN', 'Access denied');
|
|
}
|
|
}
|