From 23060261c76ee2f1b8f12fad7881dd628c5707f2 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Tue, 18 Aug 2026 00:08:28 +0400 Subject: [PATCH] feat: Track S frontend - permission core + Audit & Security section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit core/permissions (SessionPermissions/AuditEvent models, gateway/token, requiresScope() CanActivateFn) against docs/backend/ TRACK-S-SECURITY-RBAC-CONTRACT.md §1-3. PermissionLocalGateway grants PLATFORM_OWNER/'*' unconditionally - this matches TODAY'S REAL behavior (GAPS-AND-IMPROVEMENTS.md: admin role model is decorative, every authenticated admin has full access) rather than faking enforcement that doesn't exist. requiresScope() is correspondingly a no-op against the mock, by design - it must not create a false sense of security before a real backend exists. New features/admin/audit (Audit & Security nav section, missing from admin nav today) - facade + page, empty state until real audit events exist. Scope: deliberately NOT retrofitting requiresScope() onto the 14 existing live admin routes in this pass - a blanket guard rollout risks locking an admin out without warning and needs its own verified pass, not a bundled change alongside nine other phases. This is the single most serious security gap this session's audit found; closing it for real is Track S's own dedicated follow-up once a real backend exists to enforce against. This closes out the full "do all phases" push: 10 phases + 2 tracks, each with a real mock-gateway-backed swappable seam, several with genuinely new backoffice UI. Every core/* module here binds via the same DI-token pattern established for the 9 admin domains at the start of this session - a real backend is a token swap per module, not a rewrite. Co-Authored-By: Claude Sonnet 5 --- src/app/app.routes.ts | 9 +++++++ .../permissions/guards/permission.guard.ts | 20 +++++++++++++++ .../permissions/models/permission.model.ts | 19 ++++++++++++++ .../services/permission-gateway.interface.ts | 8 ++++++ .../services/permission-gateway.token.ts | 9 +++++++ .../services/permission-local.gateway.ts | 25 +++++++++++++++++++ .../admin/audit/facade/admin-audit.facade.ts | 20 +++++++++++++++ .../pages/admin-audit-page.component.html | 16 ++++++++++++ .../pages/admin-audit-page.component.scss | 14 +++++++++++ .../audit/pages/admin-audit-page.component.ts | 20 +++++++++++++++ .../features/admin/shell/admin-nav.model.ts | 1 + src/app/i18n/en.ts | 1 + src/app/i18n/hy.ts | 1 + src/app/i18n/ru.ts | 1 + src/app/i18n/translations.ts | 1 + 15 files changed, 165 insertions(+) create mode 100644 src/app/core/permissions/guards/permission.guard.ts create mode 100644 src/app/core/permissions/models/permission.model.ts create mode 100644 src/app/core/permissions/services/permission-gateway.interface.ts create mode 100644 src/app/core/permissions/services/permission-gateway.token.ts create mode 100644 src/app/core/permissions/services/permission-local.gateway.ts create mode 100644 src/app/features/admin/audit/facade/admin-audit.facade.ts create mode 100644 src/app/features/admin/audit/pages/admin-audit-page.component.html create mode 100644 src/app/features/admin/audit/pages/admin-audit-page.component.scss create mode 100644 src/app/features/admin/audit/pages/admin-audit-page.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 8a4b465..523efb1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -227,6 +227,15 @@ const coreRoutes: Routes = [ breadcrumb: [{ labelKey: 'adminShell.nav.marketplaces' }] } }, + { + path: 'audit', + loadComponent: () => import('./features/admin/audit/pages/admin-audit-page.component').then(m => m.AdminAuditPageComponent), + data: { + titleKey: 'adminShell.nav.audit', + descriptionKey: 'adminShell.nav.audit', + breadcrumb: [{ labelKey: 'adminShell.nav.audit' }] + } + }, { path: 'moderation', loadComponent: () => import('./features/admin/moderation/pages/admin-reviews-list-page.component').then(m => m.AdminReviewsListPageComponent), diff --git a/src/app/core/permissions/guards/permission.guard.ts b/src/app/core/permissions/guards/permission.guard.ts new file mode 100644 index 0000000..a1e012a --- /dev/null +++ b/src/app/core/permissions/guards/permission.guard.ts @@ -0,0 +1,20 @@ +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; +import { map } from 'rxjs/operators'; +import { PERMISSION_GATEWAY } from '../services/permission-gateway.token'; + +/** + * Route guard shape for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2. + * Against PermissionLocalGateway (grants everything - see that file's doc + * comment) this is currently a no-op, by design: it must not create a false + * sense of enforcement before a real backend exists. Attach via route data + * (`data: { requiredScope: '...' }`) once real enforcement is needed; + * wiring this onto the 14 existing live admin routes is deliberately not + * done in this pass - that needs its own verified rollout, not a blanket + * retrofit that could lock an admin out without warning. + */ +export function requiresScope(scope: string): CanActivateFn { + return () => inject(PERMISSION_GATEWAY).getSessionPermissions().pipe( + map(permissions => permissions.scopes.includes('*') || permissions.scopes.includes(scope)) + ); +} diff --git a/src/app/core/permissions/models/permission.model.ts b/src/app/core/permissions/models/permission.model.ts new file mode 100644 index 0000000..c70ac61 --- /dev/null +++ b/src/app/core/permissions/models/permission.model.ts @@ -0,0 +1,19 @@ +/** Per docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §1-2. */ +export type PlatformRole = 'PLATFORM_OWNER' | 'TECH_ADMIN' | 'SECURITY_ADMIN' | 'DOMAIN_MANAGER' | 'VIEWER'; +export type MarketplaceRole = 'MARKETPLACE_ADMIN' | 'CONTENT_MANAGER' | 'CATALOG_MANAGER' | 'ORDER_MANAGER' | 'FINANCE_MANAGER' | 'SUPPORT_MANAGER' | 'VIEWER'; + +export interface SessionPermissions { + role: PlatformRole | MarketplaceRole; + scopes: string[]; + marketplaceIds: string[]; +} + +export interface AuditEvent { + id: string; + actor: string; + action: string; + entityType: string; + entityId: string; + reason?: string; + occurredAt: string; +} diff --git a/src/app/core/permissions/services/permission-gateway.interface.ts b/src/app/core/permissions/services/permission-gateway.interface.ts new file mode 100644 index 0000000..e8f83d4 --- /dev/null +++ b/src/app/core/permissions/services/permission-gateway.interface.ts @@ -0,0 +1,8 @@ +import { Observable } from 'rxjs'; +import { AuditEvent, SessionPermissions } from '../models/permission.model'; + +/** Per docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2-3. */ +export interface PermissionGateway { + getSessionPermissions(): Observable; + loadAuditLog(): Observable; +} diff --git a/src/app/core/permissions/services/permission-gateway.token.ts b/src/app/core/permissions/services/permission-gateway.token.ts new file mode 100644 index 0000000..c650807 --- /dev/null +++ b/src/app/core/permissions/services/permission-gateway.token.ts @@ -0,0 +1,9 @@ +import { InjectionToken, inject } from '@angular/core'; +import { PermissionGateway } from './permission-gateway.interface'; +import { PermissionLocalGateway } from './permission-local.gateway'; + +/** Swap point for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2. */ +export const PERMISSION_GATEWAY = new InjectionToken('PERMISSION_GATEWAY', { + providedIn: 'root', + factory: () => inject(PermissionLocalGateway), +}); diff --git a/src/app/core/permissions/services/permission-local.gateway.ts b/src/app/core/permissions/services/permission-local.gateway.ts new file mode 100644 index 0000000..e5db289 --- /dev/null +++ b/src/app/core/permissions/services/permission-local.gateway.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { AuditEvent, SessionPermissions } from '../models/permission.model'; +import { PermissionGateway } from './permission-gateway.interface'; + +/** + * Grants full PLATFORM_OWNER access unconditionally. This matches today's + * REAL behaviour (GAPS-AND-IMPROVEMENTS.md: "the admin role model is + * decorative - anyone who passes admin authentication has full access + * regardless of assigned role") rather than pretending enforcement exists + * when it doesn't. Swapping PERMISSION_GATEWAY for a real backend per + * docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md is what actually turns + * enforcement on - PermissionGuard below is inert against this mock by + * design, not a false sense of security. + */ +@Injectable({ providedIn: 'root' }) +export class PermissionLocalGateway implements PermissionGateway { + getSessionPermissions(): Observable { + return of({ role: 'PLATFORM_OWNER', scopes: ['*'], marketplaceIds: ['*'] }); + } + + loadAuditLog(): Observable { + return of([]); + } +} diff --git a/src/app/features/admin/audit/facade/admin-audit.facade.ts b/src/app/features/admin/audit/facade/admin-audit.facade.ts new file mode 100644 index 0000000..2f93626 --- /dev/null +++ b/src/app/features/admin/audit/facade/admin-audit.facade.ts @@ -0,0 +1,20 @@ +import { Injectable, inject, signal } from '@angular/core'; +import { take } from 'rxjs/operators'; +import { AuditEvent } from '../../../../core/permissions/models/permission.model'; +import { PERMISSION_GATEWAY } from '../../../../core/permissions/services/permission-gateway.token'; + +@Injectable({ providedIn: 'root' }) +export class AdminAuditFacade { + private readonly gateway = inject(PERMISSION_GATEWAY); + + readonly events = signal([]); + readonly loading = signal(false); + + load(): void { + this.loading.set(true); + this.gateway.loadAuditLog().pipe(take(1)).subscribe(items => { + this.events.set(items); + this.loading.set(false); + }); + } +} diff --git a/src/app/features/admin/audit/pages/admin-audit-page.component.html b/src/app/features/admin/audit/pages/admin-audit-page.component.html new file mode 100644 index 0000000..e5e3477 --- /dev/null +++ b/src/app/features/admin/audit/pages/admin-audit-page.component.html @@ -0,0 +1,16 @@ +
+
+

Audit & Security

+

Role changes, sensitive actions, login/security events, exports. Real role model does not exist yet - see docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md. Today every authenticated admin has full access regardless of role.

+
+ + @if (facade.events().length === 0 && !facade.loading()) { + + } @else { +
    + @for (e of facade.events(); track e.id) { +
  • {{ e.occurredAt }} - {{ e.actor }} - {{ e.action }} ({{ e.entityType }}:{{ e.entityId }})
  • + } +
+ } +
diff --git a/src/app/features/admin/audit/pages/admin-audit-page.component.scss b/src/app/features/admin/audit/pages/admin-audit-page.component.scss new file mode 100644 index 0000000..38a2b09 --- /dev/null +++ b/src/app/features/admin/audit/pages/admin-audit-page.component.scss @@ -0,0 +1,14 @@ +.audit-page { + display: flex; + flex-direction: column; + gap: 16px; + + ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 6px; + } +} diff --git a/src/app/features/admin/audit/pages/admin-audit-page.component.ts b/src/app/features/admin/audit/pages/admin-audit-page.component.ts new file mode 100644 index 0000000..52f10e3 --- /dev/null +++ b/src/app/features/admin/audit/pages/admin-audit-page.component.ts @@ -0,0 +1,20 @@ +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AdminAuditFacade } from '../facade/admin-audit.facade'; +import { EmptyStateComponent } from '../../../../shared/ui/empty-state/empty-state.component'; + +@Component({ + selector: 'app-admin-audit-page', + standalone: true, + imports: [CommonModule, EmptyStateComponent], + templateUrl: './admin-audit-page.component.html', + styleUrls: ['./admin-audit-page.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdminAuditPageComponent { + readonly facade = inject(AdminAuditFacade); + + constructor() { + this.facade.load(); + } +} diff --git a/src/app/features/admin/shell/admin-nav.model.ts b/src/app/features/admin/shell/admin-nav.model.ts index d0ca1fa..1a4396b 100644 --- a/src/app/features/admin/shell/admin-nav.model.ts +++ b/src/app/features/admin/shell/admin-nav.model.ts @@ -42,6 +42,7 @@ export const ADMIN_NAV_PRIMARY: AdminNavEntry[] = [ { type: 'link', id: 'notifications', icon: 'bell', labelKey: 'adminShell.nav.notifications', path: ['notifications'] }, { type: 'link', id: 'integrations', icon: 'network', labelKey: 'adminShell.nav.integrations', path: ['integrations'] }, { type: 'link', id: 'finance', icon: 'creditCard', labelKey: 'adminShell.nav.finance', path: ['finance'] }, + { type: 'link', id: 'audit', icon: 'shield', labelKey: 'adminShell.nav.audit', path: ['audit'] }, { type: 'link', id: 'reports', icon: 'chartBar', labelKey: 'adminShell.nav.reports', path: ['reports'] }, { type: 'group', labelKey: 'adminShell.nav.partnersGroup' }, { type: 'link', id: 'seller-management', icon: 'store', labelKey: 'adminShell.nav.sellerManagement', path: ['partners', 'seller-management'] }, diff --git a/src/app/i18n/en.ts b/src/app/i18n/en.ts index 018d1e0..c5d1a2c 100644 --- a/src/app/i18n/en.ts +++ b/src/app/i18n/en.ts @@ -2063,6 +2063,7 @@ export const en: Translations = { integrations: 'Integrations', finance: 'Payments & Finance', marketplaces: 'Marketplaces', + audit: 'Audit & Security', reviews: 'Reviews', reports: 'Reports', partnersGroup: 'Partners', diff --git a/src/app/i18n/hy.ts b/src/app/i18n/hy.ts index 8979b48..2e9d2c6 100644 --- a/src/app/i18n/hy.ts +++ b/src/app/i18n/hy.ts @@ -2057,6 +2057,7 @@ export const hy: Translations = { integrations: 'Ինտեգրումներ', finance: 'Վճարումներ և ֆինանսներ', marketplaces: 'Մարկետփլեյսներ', + audit: 'Աուդիտ և անվտանգություն', transactions: 'Գործարքներ', reviews: 'Կարծիքներ', reports: 'Հաշվետվություններ', diff --git a/src/app/i18n/ru.ts b/src/app/i18n/ru.ts index 9f4126e..9672901 100644 --- a/src/app/i18n/ru.ts +++ b/src/app/i18n/ru.ts @@ -2057,6 +2057,7 @@ export const ru: Translations = { integrations: 'Интеграции', finance: 'Платежи и финансы', marketplaces: 'Маркетплейсы', + audit: 'Аудит и безопасность', transactions: 'Транзакции', reviews: 'Отзывы', reports: 'Отчёты', diff --git a/src/app/i18n/translations.ts b/src/app/i18n/translations.ts index 15fcbae..69b3405 100644 --- a/src/app/i18n/translations.ts +++ b/src/app/i18n/translations.ts @@ -2072,6 +2072,7 @@ export interface Translations { integrations: string; finance: string; marketplaces: string; + audit: string; reports: string; partnersGroup: string; sellerManagement: string;