import { Injectable, computed, inject } from '@angular/core'; import { toObservable } from '@angular/core/rxjs-interop'; import { filter, firstValueFrom } from 'rxjs'; import { RemoteData, fromResource } from '@shared/application/remote-data'; import { Capability } from '@shared/domain/capability'; import { MeAdapter, parseMe } from '@shared/infrastructure/me.adapter'; type Err = Error | undefined; /** * The current principal's capabilities (PRD-0002 §6) — one root singleton, like * `SessionStore`/`BigProfileStore`. Global capabilities load once from `GET /me`; * a screen's own decision DTO (e.g. `BriefViewDto.decisions`) covers anything tied * to a specific resource's live status — no extra round-trip needed for that. * * `can()` is deny-by-default: loading, failed, or an unrecognized capability all * resolve to `false`. This store never derives a capability from a role — it only * mirrors what the server already resolved. */ @Injectable({ providedIn: 'root' }) export class AccessStore { private adapter = inject(MeAdapter); private meRes = this.adapter.meResource(); private capabilities = computed>(() => { const rd = fromResource(this.meRes); if (rd.tag !== 'Success') return rd; const parsed = parseMe(rd.value); return parsed.ok ? { tag: 'Success', value: parsed.value } : { tag: 'Failure', error: new Error(parsed.error) }; }); can(capability: Capability): boolean { const rd = this.capabilities(); return rd.tag === 'Success' && rd.value.includes(capability); } /** True once `/me` has resolved (success or failure) — lets a page-level gate tell "still loading" apart from "denied", so an admin doesn't flash the denial alert. */ readonly ready = computed(() => { const tag = this.capabilities().tag; return tag === 'Success' || tag === 'Failure'; }); private ready$ = toObservable(this.ready); /** Resolves once `/me` has settled (success or failure). The `capabilityGuard` awaits this before deciding — otherwise it reads `can()` while `/me` is still loading and wrongly denies (deny-by-default), bouncing even an entitled user. */ async whenReady(): Promise { if (this.ready()) return; await firstValueFrom(this.ready$.pipe(filter((r) => r))); } }