fix(freight-permissions): split company stamp from per-officer teeter permission

This commit is contained in:
ghost2023
2026-08-12 13:18:29 +03:00
parent 02a264930b
commit 2a816912fc
7 changed files with 40 additions and 28 deletions

View File

@@ -15,21 +15,21 @@ export class StampSettingsController {
constructor(private readonly service: StampSettingsService) {} constructor(private readonly service: StampSettingsService) {}
@Get() @Get()
@BookingStaff([FREIGHT_PERMS.settings.invoiceStamp.view, FREIGHT_PERMS.admin]) @BookingStaff([FREIGHT_PERMS.settings.stamp.view, FREIGHT_PERMS.admin])
@ApiOperation({ summary: "Current company stamp used on invoice/receipt PDFs" }) @ApiOperation({ summary: "Current company stamp used on invoice/receipt PDFs" })
get() { get() {
return this.service.getView(); return this.service.getView();
} }
@Put() @Put()
@BookingStaff([FREIGHT_PERMS.settings.invoiceStamp.manage, FREIGHT_PERMS.admin]) @BookingStaff([FREIGHT_PERMS.settings.stamp.manage, FREIGHT_PERMS.admin])
@ApiOperation({ summary: "Replace the company stamp" }) @ApiOperation({ summary: "Replace the company stamp" })
update(@Body() dto: UpdateStampSettingDto, @CurrentUser() user: TCurrentUser) { update(@Body() dto: UpdateStampSettingDto, @CurrentUser() user: TCurrentUser) {
return this.service.setStamp(dto.stampImageBase64, user?.id ?? null); return this.service.setStamp(dto.stampImageBase64, user?.id ?? null);
} }
@Delete() @Delete()
@BookingStaff([FREIGHT_PERMS.settings.invoiceStamp.manage, FREIGHT_PERMS.admin]) @BookingStaff([FREIGHT_PERMS.settings.stamp.manage, FREIGHT_PERMS.admin])
@ApiOperation({ @ApiOperation({
summary: "Clear the company stamp (invoices fall back to the plain seal)", summary: "Clear the company stamp (invoices fall back to the plain seal)",
}) })

View File

@@ -1190,22 +1190,28 @@ export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [
perm( perm(
"b4b00002-0001-4000-8000-000000000001", "b4b00002-0001-4000-8000-000000000001",
"edr_freight_app:settings:stamp:view", "edr_freight_app:settings:stamp:view",
"View stamp settings", "View the company stamp",
), ),
perm( perm(
"b4b00002-0001-4000-8000-000000000002", "b4b00002-0001-4000-8000-000000000002",
"edr_freight_app:settings:stamp:manage", "edr_freight_app:settings:stamp:manage",
"Manage stamp settings", "Manage the company stamp",
), ),
// The per-officer approval teeter (ማህተም) — an individual's own stamp +
// signature, not the company seal. It used to ride on settings:stamp:*, which
// now gates the ONE company stamp; this key was split out when the two were
// untangled. `settings:invoice_stamp:*` retired at the same time: it gated the
// company stamp before the fold and is deliberately left orphaned in any DB
// that already seeded it (the seeder upserts by key and never deletes).
perm( perm(
"b4b00003-0001-4000-8000-000000000001", "b4b00003-0001-4000-8000-000000000001",
"edr_freight_app:settings:invoice_stamp:view", "edr_freight_app:settings:teeter:view",
"View invoice stamp settings", "View own approval teeter and signature",
), ),
perm( perm(
"b4b00003-0001-4000-8000-000000000002", "b4b00003-0001-4000-8000-000000000002",
"edr_freight_app:settings:invoice_stamp:manage", "edr_freight_app:settings:teeter:manage",
"Manage invoice stamp settings", "Manage own approval teeter and signature",
), ),
perm( perm(
"b4c00001-0001-4000-8000-000000000001", "b4c00001-0001-4000-8000-000000000001",
@@ -1907,15 +1913,18 @@ export const FREIGHT_PERMS = {
view: "edr_freight_app:settings:dropdown:view", view: "edr_freight_app:settings:dropdown:view",
manage: "edr_freight_app:settings:dropdown:manage", manage: "edr_freight_app:settings:dropdown:manage",
}, },
// The ONE company stamp/seal, applied to every generated document
// (invoices, receipts, warehouse papers, the EDR side of contracts).
stamp: { stamp: {
view: "edr_freight_app:settings:stamp:view", view: "edr_freight_app:settings:stamp:view",
manage: "edr_freight_app:settings:stamp:manage", manage: "edr_freight_app:settings:stamp:manage",
}, },
// Company stamp/seal image stamped onto invoice/receipt PDFs — separate // The per-officer approval teeter (ማህተም) + signature — genuinely per-person,
// from `stamp` above, which is the per-employee approval-record teeter. // and NOT the company seal above. Retired: `invoiceStamp`, which used to
invoiceStamp: { // gate the company stamp before the two were untangled.
view: "edr_freight_app:settings:invoice_stamp:view", teeter: {
manage: "edr_freight_app:settings:invoice_stamp:manage", view: "edr_freight_app:settings:teeter:view",
manage: "edr_freight_app:settings:teeter:manage",
}, },
exchangeRate: { exchangeRate: {
view: "edr_freight_app:settings:exchange_rate:view", view: "edr_freight_app:settings:exchange_rate:view",

View File

@@ -796,7 +796,7 @@ const App = () => {
path="stamp-settings" path="stamp-settings"
element={ element={
<RequirePermission <RequirePermission
permission={FREIGHT_PERMS.settings.invoiceStamp.view} permission={FREIGHT_PERMS.settings.stamp.view}
> >
<CompanyStampSettingsPage /> <CompanyStampSettingsPage />
</RequirePermission> </RequirePermission>

View File

@@ -493,7 +493,7 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
label: "Company stamp", label: "Company stamp",
href: "/dashboard/stamp-settings", href: "/dashboard/stamp-settings",
icon: <Stamp />, icon: <Stamp />,
permission: FREIGHT_PERMS.settings.invoiceStamp.view, permission: FREIGHT_PERMS.settings.stamp.view,
}, },
{ {
label: "Contract templates", label: "Contract templates",

View File

@@ -328,15 +328,18 @@ export const FREIGHT_PERMS = {
view: "edr_freight_app:settings:dropdown:view", view: "edr_freight_app:settings:dropdown:view",
manage: "edr_freight_app:settings:dropdown:manage", manage: "edr_freight_app:settings:dropdown:manage",
}, },
// The ONE company stamp/seal, applied to every generated document
// (invoices, receipts, warehouse papers, the EDR side of contracts).
stamp: { stamp: {
view: "edr_freight_app:settings:stamp:view", view: "edr_freight_app:settings:stamp:view",
manage: "edr_freight_app:settings:stamp:manage", manage: "edr_freight_app:settings:stamp:manage",
}, },
// Company stamp/seal image stamped onto invoice/receipt PDFs — separate // The per-officer approval teeter (ማህተም) + signature — genuinely per-person,
// from `stamp` above, which is the per-employee approval-record teeter. // and NOT the company seal above. Retired: `invoiceStamp`, which used to
invoiceStamp: { // gate the company stamp before the two were untangled.
view: "edr_freight_app:settings:invoice_stamp:view", teeter: {
manage: "edr_freight_app:settings:invoice_stamp:manage", view: "edr_freight_app:settings:teeter:view",
manage: "edr_freight_app:settings:teeter:manage",
}, },
exchangeRate: { exchangeRate: {
view: "edr_freight_app:settings:exchange_rate:view", view: "edr_freight_app:settings:exchange_rate:view",

View File

@@ -147,13 +147,17 @@ export function UserManagementRoutes(): ReactElement {
(am/en) and genuinely per-person. It used to sit at (am/en) and genuinely per-person. It used to sit at
/dashboard/stamp-settings under Settings, next to the /dashboard/stamp-settings under Settings, next to the
single global company stamp, which read as duplication. single global company stamp, which read as duplication.
Permission gate unchanged from that route.
Gated by settings:teeter:*, split out of settings:stamp:*
when the two were untangled — settings:stamp:* now means
the company stamp, so anyone who held it for the teeter
needs the new key granted.
*/} */}
<Route <Route
path="user-management/teeter-and-signature" path="user-management/teeter-and-signature"
element={ element={
<RequirePermission <RequirePermission
permission={FREIGHT_PERMS.settings.stamp.view} permission={FREIGHT_PERMS.settings.teeter.view}
> >
<UploadTeeterAndSignature /> <UploadTeeterAndSignature />
</RequirePermission> </RequirePermission>

View File

@@ -1,5 +1,5 @@
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { Injectable, Logger, NestMiddleware } from "@nestjs/common"; import { Injectable, NestMiddleware } from "@nestjs/common";
import { import {
RequestLogContext, RequestLogContext,
@@ -58,8 +58,6 @@ const userId = (req: LoggedRequest): string | undefined => {
*/ */
@Injectable() @Injectable()
export class RequestLogMiddleware implements NestMiddleware { export class RequestLogMiddleware implements NestMiddleware {
private readonly logger = new Logger("HTTP");
use(req: LoggedRequest, res: LoggedResponse, next: () => void): void { use(req: LoggedRequest, res: LoggedResponse, next: () => void): void {
const start = Date.now(); const start = Date.now();
const requestId = header(req, "x-request-id") ?? randomUUID(); const requestId = header(req, "x-request-id") ?? randomUUID();
@@ -85,8 +83,6 @@ export class RequestLogMiddleware implements NestMiddleware {
const status = res.statusCode; const status = res.statusCode;
const durationMs = Date.now() - start; const durationMs = Date.now() - start;
this.logger.log(`${req.method} ${url} ${status} ${durationMs}ms`);
const line = { const line = {
...ctx, ...ctx,
// Fields the Nest console prefix used to carry. They are IN the JSON // Fields the Nest console prefix used to carry. They are IN the JSON