2026-07-20 09:07:27 +04:00
|
|
|
|
# Admin Authentication — Ed25519 Foundation
|
|
|
|
|
|
|
|
|
|
|
|
Status: **FRONTEND PREPARED, NOT LIVE.** Everything in this document describes
|
|
|
|
|
|
code that exists in `src/app/core/auth/` today, wired to endpoints that do
|
|
|
|
|
|
not exist on the backend yet. No route currently requires this flow — the
|
|
|
|
|
|
live admin gate remains the Telegram-QR-based `AdminAuthService` /
|
|
|
|
|
|
`adminAuthGuard` (`src/app/core/admin-auth/`, documented in
|
docs: consolidate documentation and archive temporary reports
Step 1-2 (audit + plan): classified 35 project markdown files into
Core/Architecture/ADR/Temporary-audit/Sprint-report/Generated-review/
Duplicate/Obsolete/Historical. Agent-tooling files (.agents/skills/**,
.superpowers/**, docs/context/**, CLAUDE.md/GEMINI.md/AGENTS.md/
.github/copilot-instructions.md) explicitly out of scope — intentional
per-tool duplication, not documentation debt.
Step 3 (merge, no information lost):
- docs/PROJECT.md -> docs/PROJECT_INDEX.md, rewritten as the single
entry point: system overview, living-doc index, archive pointer,
current status, and a critical-finding callout up top.
- docs/backend/BACKEND-INTEGRATION.md -> docs/BACKEND_API.md,
docs/backend/REMAINING-BACKEND-WORK.md ->
docs/BACKEND_API_REMAINING_WORK.md (also folded in a legitimate
uncommitted status update that had been sitting unstaged all
session: categories marked DONE, order-creation endpoint noted done).
- RELEASE-NOTES.md merged into CHANGELOG.md (was a near-duplicate of
the same release content in friendlier prose), then deleted.
- KNOWN-ISSUES.md: added item 13 (see below) and item 14 (missing
canDeactivate on admin/products edit, from the archived PROJECT-STATE
audit, re-verified still true); added a correction note to Fixed
item 7.
- All cross-references to renamed/moved files fixed across every
kept doc (grep+sed pass, then verified with a link-existence check
across all 58 in-scope markdown files -> 0 broken links).
Step 4 (archive, nothing deleted without merging first): created
docs/archive/, moved 19 files there (3 root sprint reports, 1 platform
report, SPRINT-PLAN.md, and 14 one-off audit/review/report docs).
Added correction headers to the 3 archived docs whose conclusions were
affected by the finding below, rather than silently leaving them
misleading.
Step 5: docs/PROJECT_INDEX.md rewritten per the mission brief -
someone opening the repo should understand the whole system from it.
IMPORTANT FINDING (surfaced during this audit, not the mission's
primary goal but too significant to bury): pages/category/*,
pages/search/*, pages/item-detail/*, pages/info/**, pages/legal/**
(40+ files) are entirely unrouted dead code - app.routes.ts's
cmsContentRoutes is a literal empty array, and category/search/product
routes redirect to CatalogContainerComponent/
ProductDetailsContainerComponent, not these files. Confirmed against
app.routes.ts directly and cross-checked against FRONTEND.md's own
routing description. This means several fixes from earlier this cycle
(RC-Premium-01, RC STORE-01) and the dead-code cleanup sprint's
conclusion that these files were live were all wrong - documented as
KNOWN-ISSUES.md item 13, flagged at the top of PROJECT_INDEX.md, and
noted on the 3 archived docs whose conclusions it affects. No
application code was changed to fix this (out of scope per this
session's 'documentation only' constraint) - it needs a wire-it-up-or-
delete-it decision first.
Verification: tsc --noEmit clean, npm run build green, all markdown
links across 58 in-scope files resolve (checked programmatically).
No application/Angular/backend code modified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:10:49 +04:00
|
|
|
|
`docs/BACKEND_API.md` §2.4–2.5). This module is the
|
2026-07-20 09:07:27 +04:00
|
|
|
|
integration target once the backend ships the endpoints below.
|
|
|
|
|
|
|
|
|
|
|
|
Do not point any live route's `canActivate` at `ed25519AuthGuard` until the
|
|
|
|
|
|
backend endpoints in §API Contracts exist and have been verified — doing so
|
|
|
|
|
|
before then would lock every admin out.
|
|
|
|
|
|
|
|
|
|
|
|
## 1. Why this exists
|
|
|
|
|
|
|
docs: consolidate documentation and archive temporary reports
Step 1-2 (audit + plan): classified 35 project markdown files into
Core/Architecture/ADR/Temporary-audit/Sprint-report/Generated-review/
Duplicate/Obsolete/Historical. Agent-tooling files (.agents/skills/**,
.superpowers/**, docs/context/**, CLAUDE.md/GEMINI.md/AGENTS.md/
.github/copilot-instructions.md) explicitly out of scope — intentional
per-tool duplication, not documentation debt.
Step 3 (merge, no information lost):
- docs/PROJECT.md -> docs/PROJECT_INDEX.md, rewritten as the single
entry point: system overview, living-doc index, archive pointer,
current status, and a critical-finding callout up top.
- docs/backend/BACKEND-INTEGRATION.md -> docs/BACKEND_API.md,
docs/backend/REMAINING-BACKEND-WORK.md ->
docs/BACKEND_API_REMAINING_WORK.md (also folded in a legitimate
uncommitted status update that had been sitting unstaged all
session: categories marked DONE, order-creation endpoint noted done).
- RELEASE-NOTES.md merged into CHANGELOG.md (was a near-duplicate of
the same release content in friendlier prose), then deleted.
- KNOWN-ISSUES.md: added item 13 (see below) and item 14 (missing
canDeactivate on admin/products edit, from the archived PROJECT-STATE
audit, re-verified still true); added a correction note to Fixed
item 7.
- All cross-references to renamed/moved files fixed across every
kept doc (grep+sed pass, then verified with a link-existence check
across all 58 in-scope markdown files -> 0 broken links).
Step 4 (archive, nothing deleted without merging first): created
docs/archive/, moved 19 files there (3 root sprint reports, 1 platform
report, SPRINT-PLAN.md, and 14 one-off audit/review/report docs).
Added correction headers to the 3 archived docs whose conclusions were
affected by the finding below, rather than silently leaving them
misleading.
Step 5: docs/PROJECT_INDEX.md rewritten per the mission brief -
someone opening the repo should understand the whole system from it.
IMPORTANT FINDING (surfaced during this audit, not the mission's
primary goal but too significant to bury): pages/category/*,
pages/search/*, pages/item-detail/*, pages/info/**, pages/legal/**
(40+ files) are entirely unrouted dead code - app.routes.ts's
cmsContentRoutes is a literal empty array, and category/search/product
routes redirect to CatalogContainerComponent/
ProductDetailsContainerComponent, not these files. Confirmed against
app.routes.ts directly and cross-checked against FRONTEND.md's own
routing description. This means several fixes from earlier this cycle
(RC-Premium-01, RC STORE-01) and the dead-code cleanup sprint's
conclusion that these files were live were all wrong - documented as
KNOWN-ISSUES.md item 13, flagged at the top of PROJECT_INDEX.md, and
noted on the 3 archived docs whose conclusions it affects. No
application code was changed to fix this (out of scope per this
session's 'documentation only' constraint) - it needs a wire-it-up-or-
delete-it decision first.
Verification: tsc --noEmit clean, npm run build green, all markdown
links across 58 in-scope files resolve (checked programmatically).
No application/Angular/backend code modified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:10:49 +04:00
|
|
|
|
`docs/BACKEND_API.md` §2.5 documents the current system's
|
2026-07-20 09:07:27 +04:00
|
|
|
|
biggest security gap: admin and customer login hit the *same* Telegram
|
|
|
|
|
|
session endpoint, so the backend has no way to distinguish an admin login
|
|
|
|
|
|
attempt from a customer one at the moment of login — authorization is
|
|
|
|
|
|
effectively unenforced. Ed25519 challenge/response auth closes this by
|
|
|
|
|
|
requiring proof of possession of a specific, pre-registered private key
|
|
|
|
|
|
before a session is ever issued, instead of "any Telegram account that
|
|
|
|
|
|
happened to scan the right QR code."
|
|
|
|
|
|
|
|
|
|
|
|
## 2. Sequence diagram
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
|
participant Admin as Admin (browser)
|
|
|
|
|
|
participant FE as Frontend (AuthService)
|
|
|
|
|
|
participant BE as Backend
|
|
|
|
|
|
|
|
|
|
|
|
Admin->>FE: Click "Sign in"
|
|
|
|
|
|
FE->>BE: GET /api/admin/auth/challenge
|
|
|
|
|
|
BE-->>FE: { nonce, issuedAt, expiresAt }
|
|
|
|
|
|
FE->>FE: Ed25519KeypairService.sign(nonce)<br/>(WebCrypto, non-extractable private key)
|
|
|
|
|
|
FE->>BE: POST /api/admin/auth/verify<br/>{ publicKey, signature, nonce }
|
|
|
|
|
|
alt signature valid & publicKey is a provisioned admin key
|
|
|
|
|
|
BE-->>FE: 200 { token, refreshToken }
|
|
|
|
|
|
FE->>FE: SessionService.activate(tokens)<br/>decode JWT claims, schedule refresh
|
|
|
|
|
|
FE-->>Admin: Redirect to /backoffice
|
|
|
|
|
|
else invalid signature / unknown key / expired nonce
|
|
|
|
|
|
BE-->>FE: 401/403
|
|
|
|
|
|
FE-->>Admin: Redirect to /admin-login/error/invalid-signature
|
|
|
|
|
|
end
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### Refresh sequence
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
sequenceDiagram
|
|
|
|
|
|
participant FE as Frontend (SessionService)
|
|
|
|
|
|
participant IC as authInterceptor
|
|
|
|
|
|
participant BE as Backend
|
|
|
|
|
|
|
|
|
|
|
|
Note over FE: Timer fires ~60s before JWT exp
|
|
|
|
|
|
FE->>BE: POST /api/admin/auth/refresh { refreshToken }
|
|
|
|
|
|
alt refresh token still valid
|
|
|
|
|
|
BE-->>FE: 200 { token, refreshToken }
|
|
|
|
|
|
FE->>FE: activate(tokens) - reschedules next refresh
|
|
|
|
|
|
else refresh token expired/revoked
|
|
|
|
|
|
BE-->>FE: 401
|
|
|
|
|
|
FE->>FE: SessionService.markExpired()
|
|
|
|
|
|
FE-->>FE: Route to /admin-login/error/session-expired
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
Note over IC: Reactive path - any 401 on an admin request
|
|
|
|
|
|
IC->>BE: Admin API request (expired token)
|
|
|
|
|
|
BE-->>IC: 401
|
|
|
|
|
|
IC->>BE: POST /api/admin/auth/refresh (single retry)
|
|
|
|
|
|
alt refresh succeeds
|
|
|
|
|
|
BE-->>IC: 200 tokens
|
|
|
|
|
|
IC->>BE: Retry original request with new token
|
|
|
|
|
|
else refresh fails
|
|
|
|
|
|
IC-->>FE: Propagate error, route to session-expired
|
|
|
|
|
|
end
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
## 3. Ed25519 flow, step by step
|
|
|
|
|
|
|
|
|
|
|
|
1. **Key generation (once per device):** `Ed25519KeypairService.getOrCreateKeyPair()`
|
|
|
|
|
|
generates a non-extractable Ed25519 keypair via `crypto.subtle.generateKey`
|
|
|
|
|
|
and persists the `CryptoKey` handles in IndexedDB (`admin-auth-ed25519` DB).
|
|
|
|
|
|
The private key is never exported, serialized, or transmitted — by
|
|
|
|
|
|
construction, not by convention.
|
|
|
|
|
|
2. **Registering the public key with the backend is out of scope for this
|
|
|
|
|
|
frontend.** An Owner/Administrator must associate a new device's
|
|
|
|
|
|
`publicKeyBase64` with an admin account through some out-of-band
|
|
|
|
|
|
mechanism (e.g. a backend admin tool, a one-time enrollment link) before
|
|
|
|
|
|
that device can complete step 4. This document does not prescribe that
|
|
|
|
|
|
mechanism — it is a backend/ops concern.
|
|
|
|
|
|
3. **Challenge:** `GET /api/admin/auth/challenge` returns a fresh `nonce` the
|
|
|
|
|
|
client must sign before `expiresAt`.
|
|
|
|
|
|
4. **Sign:** the raw `nonce` string is signed with the device's private key
|
|
|
|
|
|
(`Ed25519KeypairService.sign`), producing a base64 signature.
|
|
|
|
|
|
5. **Verify:** `POST /api/admin/auth/verify` sends `{ publicKey, signature,
|
|
|
|
|
|
nonce }`. The backend re-derives the signed message from the nonce it
|
|
|
|
|
|
issued, verifies the signature against its own record of that
|
|
|
|
|
|
`publicKey → admin account` mapping, and only then issues tokens.
|
|
|
|
|
|
6. **Session:** the returned `{ token, refreshToken }` pair is stored
|
|
|
|
|
|
(`SessionService`, `localStorage: ed25519AdminToken` /
|
|
|
|
|
|
`ed25519AdminRefreshToken`) and the JWT is decoded client-side for
|
|
|
|
|
|
`role`/`exp` — decoding only, never signature verification (the frontend
|
|
|
|
|
|
has no trusted key to check it against).
|
|
|
|
|
|
|
|
|
|
|
|
## 4. API contracts
|
|
|
|
|
|
|
|
|
|
|
|
All under `{environment.authApiUrl}/api/admin/auth` (see
|
|
|
|
|
|
`src/environments/environment.ts`). None of these exist on the backend
|
|
|
|
|
|
today — this is the contract the frontend was built against, not a
|
|
|
|
|
|
confirmed backend spec.
|
|
|
|
|
|
|
|
|
|
|
|
| Method | Path | Request body | Response | Notes |
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
| GET | `/challenge` | — | `200 AuthChallenge` | `{ nonce, issuedAt, expiresAt }`, all ISO 8601 except `nonce` |
|
|
|
|
|
|
| POST | `/verify` | `VerifySignatureRequest` | `200 AuthTokenPair` \| `401` \| `403` | `{ publicKey, signature, nonce }` → `{ token, refreshToken }` |
|
|
|
|
|
|
| POST | `/refresh` | `RefreshTokenRequest` | `200 AuthTokenPair` \| `401` | `{ refreshToken }` → new pair (rotation expected — old refresh token should be invalidated server-side) |
|
|
|
|
|
|
| POST | `/logout` | `{ refreshToken }` | `204` | Should revoke the refresh token server-side; frontend clears local state regardless of response |
|
|
|
|
|
|
|
|
|
|
|
|
Types: `src/app/core/auth/models/auth-api.model.ts`.
|
|
|
|
|
|
|
|
|
|
|
|
## 5. JWT claims
|
|
|
|
|
|
|
|
|
|
|
|
```ts
|
|
|
|
|
|
interface JwtClaims {
|
|
|
|
|
|
sub: string; // admin account id
|
|
|
|
|
|
role: AdminRole; // 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly'
|
|
|
|
|
|
iat: number; // seconds since epoch
|
|
|
|
|
|
exp: number; // seconds since epoch
|
|
|
|
|
|
publicKey: string; // the Ed25519 public key this token was issued for
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
The frontend decodes these (`JwtService.decode`) for UX only — role-based UI
|
|
|
|
|
|
gating, expiry countdowns, refresh scheduling. **Every admin API request
|
|
|
|
|
|
must be independently authorized server-side**; a decoded-but-unverified
|
|
|
|
|
|
claim is not proof of anything to the backend.
|
|
|
|
|
|
|
|
|
|
|
|
## 6. Permission model
|
|
|
|
|
|
|
|
|
|
|
|
Five roles, coarse-grained permission keys (`src/app/core/auth/models/permission.model.ts`):
|
|
|
|
|
|
|
|
|
|
|
|
| Role | Permissions |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| Owner | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage`, `settings.manage` |
|
|
|
|
|
|
| Administrator | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage` |
|
|
|
|
|
|
| Editor | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write` |
|
|
|
|
|
|
| Support | `backoffice.read` |
|
|
|
|
|
|
| ReadOnly | `backoffice.read`, `builder.read` |
|
|
|
|
|
|
|
|
|
|
|
|
This is deliberately coarse and mirrors the existing bootstrap-level
|
|
|
|
|
|
`PermissionsConfig` shape (`src/app/shared/models/config/permissions.model.ts`).
|
|
|
|
|
|
Finer-grained, per-domain permissions (e.g. "can edit prices but not delete
|
|
|
|
|
|
products") stay server-side until a real permission model exists there —
|
docs: consolidate documentation and archive temporary reports
Step 1-2 (audit + plan): classified 35 project markdown files into
Core/Architecture/ADR/Temporary-audit/Sprint-report/Generated-review/
Duplicate/Obsolete/Historical. Agent-tooling files (.agents/skills/**,
.superpowers/**, docs/context/**, CLAUDE.md/GEMINI.md/AGENTS.md/
.github/copilot-instructions.md) explicitly out of scope — intentional
per-tool duplication, not documentation debt.
Step 3 (merge, no information lost):
- docs/PROJECT.md -> docs/PROJECT_INDEX.md, rewritten as the single
entry point: system overview, living-doc index, archive pointer,
current status, and a critical-finding callout up top.
- docs/backend/BACKEND-INTEGRATION.md -> docs/BACKEND_API.md,
docs/backend/REMAINING-BACKEND-WORK.md ->
docs/BACKEND_API_REMAINING_WORK.md (also folded in a legitimate
uncommitted status update that had been sitting unstaged all
session: categories marked DONE, order-creation endpoint noted done).
- RELEASE-NOTES.md merged into CHANGELOG.md (was a near-duplicate of
the same release content in friendlier prose), then deleted.
- KNOWN-ISSUES.md: added item 13 (see below) and item 14 (missing
canDeactivate on admin/products edit, from the archived PROJECT-STATE
audit, re-verified still true); added a correction note to Fixed
item 7.
- All cross-references to renamed/moved files fixed across every
kept doc (grep+sed pass, then verified with a link-existence check
across all 58 in-scope markdown files -> 0 broken links).
Step 4 (archive, nothing deleted without merging first): created
docs/archive/, moved 19 files there (3 root sprint reports, 1 platform
report, SPRINT-PLAN.md, and 14 one-off audit/review/report docs).
Added correction headers to the 3 archived docs whose conclusions were
affected by the finding below, rather than silently leaving them
misleading.
Step 5: docs/PROJECT_INDEX.md rewritten per the mission brief -
someone opening the repo should understand the whole system from it.
IMPORTANT FINDING (surfaced during this audit, not the mission's
primary goal but too significant to bury): pages/category/*,
pages/search/*, pages/item-detail/*, pages/info/**, pages/legal/**
(40+ files) are entirely unrouted dead code - app.routes.ts's
cmsContentRoutes is a literal empty array, and category/search/product
routes redirect to CatalogContainerComponent/
ProductDetailsContainerComponent, not these files. Confirmed against
app.routes.ts directly and cross-checked against FRONTEND.md's own
routing description. This means several fixes from earlier this cycle
(RC-Premium-01, RC STORE-01) and the dead-code cleanup sprint's
conclusion that these files were live were all wrong - documented as
KNOWN-ISSUES.md item 13, flagged at the top of PROJECT_INDEX.md, and
noted on the 3 archived docs whose conclusions it affects. No
application code was changed to fix this (out of scope per this
session's 'documentation only' constraint) - it needs a wire-it-up-or-
delete-it decision first.
Verification: tsc --noEmit clean, npm run build green, all markdown
links across 58 in-scope files resolve (checked programmatically).
No application/Angular/backend code modified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:10:49 +04:00
|
|
|
|
see `docs/BACKEND_API.md` §"Admin-role-required". Use
|
2026-07-20 09:07:27 +04:00
|
|
|
|
`PermissionService.has(permission)` / `permissionGuard(permission)` to gate
|
|
|
|
|
|
UI and routes; never treat a passing client-side check as authorization by
|
|
|
|
|
|
itself.
|
|
|
|
|
|
|
|
|
|
|
|
## 7. Error screens
|
|
|
|
|
|
|
|
|
|
|
|
Single component (`AuthErrorPageComponent`, `/admin-login/error/:code`)
|
|
|
|
|
|
renders all five, keyed by route param:
|
|
|
|
|
|
|
|
|
|
|
|
| Code | Trigger | User action offered |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `session-expired` | Refresh token rejected/expired | Sign in again |
|
|
|
|
|
|
| `invalid-signature` | `verify` returns 401/403 during login | Try again |
|
|
|
|
|
|
| `unauthorized` | Route guard sees no active session | Sign in |
|
|
|
|
|
|
| `forbidden` | `permissionGuard` denies (authenticated but insufficient role) | Back to dashboard |
|
|
|
|
|
|
| `backend-unavailable` | Network error / 5xx / status 0 | Retry |
|
|
|
|
|
|
|
|
|
|
|
|
`authErrorCodeFromStatus` (`models/auth-error.model.ts`) maps HTTP status →
|
|
|
|
|
|
code: `401→unauthorized`, `403→forbidden`, `0→backend-unavailable`,
|
|
|
|
|
|
`5xx→backend-unavailable`, else `unauthorized`. `AuthService.login()`
|
|
|
|
|
|
additionally maps any failure during the challenge/sign/verify sequence to
|
|
|
|
|
|
`invalid-signature` when it isn't a clearer HTTP-status-derived code.
|
|
|
|
|
|
|
|
|
|
|
|
## 8. Refresh lifecycle
|
|
|
|
|
|
|
|
|
|
|
|
- On `SessionService.activate(tokens)`, a timer is scheduled for
|
|
|
|
|
|
`max(exp - now - 60s, 5s)` — refresh fires ~60 seconds before expiry so a
|
|
|
|
|
|
concurrent request never races an expiring token.
|
|
|
|
|
|
- **Proactive path:** the timer fires `AuthService.refresh()` directly.
|
|
|
|
|
|
- **Reactive path:** `authInterceptor` catches a 401 on any admin-gated
|
|
|
|
|
|
request, attempts one `refresh()`, retries the original request once on
|
|
|
|
|
|
success, and routes to `session-expired` on failure. It does not retry
|
|
|
|
|
|
more than once — a second 401 after a successful-looking refresh means
|
|
|
|
|
|
something is wrong server-side, not a transient race.
|
|
|
|
|
|
- `SessionService.restore()` runs on app bootstrap (call
|
|
|
|
|
|
`AuthFacade.restoreSession()` from an app initializer once this flow goes
|
|
|
|
|
|
live) — reads persisted tokens, decodes claims, and either resumes with a
|
|
|
|
|
|
scheduled refresh or marks `expired` without any network call, so a stale
|
|
|
|
|
|
session is caught before it reaches any component.
|
|
|
|
|
|
|
|
|
|
|
|
## 9. Module map
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
src/app/core/auth/
|
|
|
|
|
|
├── auth.routes.ts # /admin-login, /admin-login/error/:code
|
|
|
|
|
|
├── models/
|
|
|
|
|
|
│ ├── auth-api.model.ts # AuthChallenge, VerifySignatureRequest, AuthTokenPair, JwtClaims
|
|
|
|
|
|
│ ├── auth-error.model.ts # AuthErrorCode, authErrorCodeFromStatus()
|
|
|
|
|
|
│ └── permission.model.ts # AdminRole, Permission, ROLE_PERMISSIONS
|
|
|
|
|
|
├── services/
|
|
|
|
|
|
│ ├── ed25519-keypair.service.ts # WebCrypto keygen/sign, IndexedDB persistence
|
|
|
|
|
|
│ ├── auth-api.service.ts # HttpClient calls to the 4 endpoints in §4
|
|
|
|
|
|
│ ├── jwt.service.ts # decode-only JWT parsing
|
|
|
|
|
|
│ ├── session.service.ts # token/claims state, persistence, refresh scheduling
|
|
|
|
|
|
│ ├── permission.service.ts # role -> permission set
|
|
|
|
|
|
│ ├── auth.service.ts # orchestrates challenge -> sign -> verify -> refresh -> logout
|
|
|
|
|
|
│ └── auth-facade.service.ts # public surface for components
|
|
|
|
|
|
├── interceptors/
|
|
|
|
|
|
│ └── auth.interceptor.ts # Authorization: Bearer + 401 refresh-and-retry
|
|
|
|
|
|
├── guards/
|
|
|
|
|
|
│ ├── ed25519-auth.guard.ts # requires SessionService.isAuthenticated()
|
|
|
|
|
|
│ └── permission.guard.ts # permissionGuard(permission) factory
|
|
|
|
|
|
└── pages/
|
|
|
|
|
|
├── admin-login-page.component.* # sign-in UI
|
|
|
|
|
|
└── auth-error-page.component.* # parameterized error screen (§7)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
`AuthFacade` is the only thing components/pages should depend on;
|
|
|
|
|
|
`AuthService`/`SessionService`/`PermissionService` are internal
|
|
|
|
|
|
collaborators reachable through it.
|
|
|
|
|
|
|
|
|
|
|
|
## 10. Cutover plan (when the backend ships)
|
|
|
|
|
|
|
|
|
|
|
|
1. Verify the four endpoints in §4 against a real backend, including error
|
|
|
|
|
|
shapes.
|
|
|
|
|
|
2. Register `authInterceptor` in `app.config.ts`'s `withInterceptors([...])`
|
|
|
|
|
|
list (currently not registered).
|
|
|
|
|
|
3. Decide the relationship to the existing Telegram flow: replace
|
|
|
|
|
|
`adminAuthGuard` with `ed25519AuthGuard` outright, or run both and let
|
|
|
|
|
|
role/tenant config pick — this is a product decision, not made here.
|
|
|
|
|
|
4. Wire `AuthFacade.restoreSession()` into an `APP_INITIALIZER` (or root
|
|
|
|
|
|
component `ngOnInit`) so a page refresh restores state before any guard
|
|
|
|
|
|
runs.
|
|
|
|
|
|
5. Only after 1–4: point `/backoffice` and `/edit`'s `canActivate` at
|
|
|
|
|
|
`ed25519AuthGuard` (and `permissionGuard(...)` where a route needs a
|
|
|
|
|
|
specific role).
|
|
|
|
|
|
|
|
|
|
|
|
## 11. Security considerations
|
|
|
|
|
|
|
|
|
|
|
|
- **Private key never leaves the device.** Generated non-extractable via
|
|
|
|
|
|
WebCrypto; `Ed25519KeypairService` has no export path. Losing the device
|
|
|
|
|
|
means losing the key — key rotation/recovery (revoking a lost device's
|
|
|
|
|
|
public key, provisioning a new one) is a backend/ops process, not
|
|
|
|
|
|
implemented here.
|
|
|
|
|
|
- **The frontend is not the authorization boundary.** Every admin
|
|
|
|
|
|
request must be independently checked server-side against the caller's
|
docs: consolidate documentation and archive temporary reports
Step 1-2 (audit + plan): classified 35 project markdown files into
Core/Architecture/ADR/Temporary-audit/Sprint-report/Generated-review/
Duplicate/Obsolete/Historical. Agent-tooling files (.agents/skills/**,
.superpowers/**, docs/context/**, CLAUDE.md/GEMINI.md/AGENTS.md/
.github/copilot-instructions.md) explicitly out of scope — intentional
per-tool duplication, not documentation debt.
Step 3 (merge, no information lost):
- docs/PROJECT.md -> docs/PROJECT_INDEX.md, rewritten as the single
entry point: system overview, living-doc index, archive pointer,
current status, and a critical-finding callout up top.
- docs/backend/BACKEND-INTEGRATION.md -> docs/BACKEND_API.md,
docs/backend/REMAINING-BACKEND-WORK.md ->
docs/BACKEND_API_REMAINING_WORK.md (also folded in a legitimate
uncommitted status update that had been sitting unstaged all
session: categories marked DONE, order-creation endpoint noted done).
- RELEASE-NOTES.md merged into CHANGELOG.md (was a near-duplicate of
the same release content in friendlier prose), then deleted.
- KNOWN-ISSUES.md: added item 13 (see below) and item 14 (missing
canDeactivate on admin/products edit, from the archived PROJECT-STATE
audit, re-verified still true); added a correction note to Fixed
item 7.
- All cross-references to renamed/moved files fixed across every
kept doc (grep+sed pass, then verified with a link-existence check
across all 58 in-scope markdown files -> 0 broken links).
Step 4 (archive, nothing deleted without merging first): created
docs/archive/, moved 19 files there (3 root sprint reports, 1 platform
report, SPRINT-PLAN.md, and 14 one-off audit/review/report docs).
Added correction headers to the 3 archived docs whose conclusions were
affected by the finding below, rather than silently leaving them
misleading.
Step 5: docs/PROJECT_INDEX.md rewritten per the mission brief -
someone opening the repo should understand the whole system from it.
IMPORTANT FINDING (surfaced during this audit, not the mission's
primary goal but too significant to bury): pages/category/*,
pages/search/*, pages/item-detail/*, pages/info/**, pages/legal/**
(40+ files) are entirely unrouted dead code - app.routes.ts's
cmsContentRoutes is a literal empty array, and category/search/product
routes redirect to CatalogContainerComponent/
ProductDetailsContainerComponent, not these files. Confirmed against
app.routes.ts directly and cross-checked against FRONTEND.md's own
routing description. This means several fixes from earlier this cycle
(RC-Premium-01, RC STORE-01) and the dead-code cleanup sprint's
conclusion that these files were live were all wrong - documented as
KNOWN-ISSUES.md item 13, flagged at the top of PROJECT_INDEX.md, and
noted on the 3 archived docs whose conclusions it affects. No
application code was changed to fix this (out of scope per this
session's 'documentation only' constraint) - it needs a wire-it-up-or-
delete-it decision first.
Verification: tsc --noEmit clean, npm run build green, all markdown
links across 58 in-scope files resolve (checked programmatically).
No application/Angular/backend code modified.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 19:10:49 +04:00
|
|
|
|
actual role, exactly as `docs/BACKEND_API.md` §2.5
|
2026-07-20 09:07:27 +04:00
|
|
|
|
already states for the Telegram flow. A decoded JWT claim or a passing
|
|
|
|
|
|
`PermissionService.has()` check is UX, not proof.
|
|
|
|
|
|
- **Refresh tokens should rotate.** Every `POST /refresh` response is
|
|
|
|
|
|
expected to include a *new* refresh token; the backend should invalidate
|
|
|
|
|
|
the one just used. The frontend always stores whatever pair it receives
|
|
|
|
|
|
and never reuses an old refresh token after a successful rotation.
|
|
|
|
|
|
- **CSRF/replay:** the nonce from `/challenge` must be single-use and
|
|
|
|
|
|
time-boxed server-side (`expiresAt`) — the frontend enforces nothing here
|
|
|
|
|
|
beyond passing the nonce back unmodified; replay protection is the
|
|
|
|
|
|
backend's responsibility.
|
|
|
|
|
|
- **No fallback to unsigned auth.** There is no code path in this module
|
|
|
|
|
|
that issues a session without a valid signature. If the backend is
|
|
|
|
|
|
unreachable, the user sees `backend-unavailable`, never a degraded or
|
|
|
|
|
|
bypassed login.
|
|
|
|
|
|
- **Dev bypass exclusion:** unlike `AdminAuthService.devBypassLogin()` in
|
|
|
|
|
|
the Telegram flow, this module intentionally has no dev bypass — an
|
|
|
|
|
|
Ed25519 keypair is cheap to generate locally, so local testing should
|
|
|
|
|
|
point at a real (even if mocked-in-dev) `/challenge`/`/verify` pair
|
|
|
|
|
|
rather than fabricating a session.
|