feat: extract auth into @marketplaces/auth package, add backoffice admin provisioning spec
- ADR-0001: decision to extract auth/payment into shared @marketplaces/* packages - Scaffold packages/auth, packages/payment; @marketplaces/auth now holds the real telegram (customer+admin QR/session) and ed25519 (future admin challenge/response) auth implementation, pushed to sources.vitanova.network/sdarbinyan/vitanovaPackages - Rewire ~30 call sites to import from @marketplaces/auth; delete migrated originals from core/auth, core/admin-auth, services/, models/ - Replace environment coupling with AUTH_API_URL/TELEGRAM_BOT_USERNAME injection tokens and isDevMode(); wired as file:packages/auth pending registry publish - Add TRACK-S §8: bootstrap per-marketplace admin login + marketplace-scoped sub-admin invite/role endpoints - Build, arch:check:boundaries, and full test suite (103/103) all green Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
# Compiled output
|
# Compiled output
|
||||||
/dist
|
/dist
|
||||||
|
packages/*/dist
|
||||||
/tmp
|
/tmp
|
||||||
/out-tsc
|
/out-tsc
|
||||||
/bazel-out
|
/bazel-out
|
||||||
|
|||||||
50
docs/PACKAGE-EXTRACTION.md
Normal file
50
docs/PACKAGE-EXTRACTION.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# @marketplaces/auth & @marketplaces/payment — build, version, publish, consume
|
||||||
|
|
||||||
|
See [ADR-0001](context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md) for why. This doc is the how.
|
||||||
|
|
||||||
|
## Current state
|
||||||
|
|
||||||
|
[packages/](../packages) here is now just a reference copy (`package.json`/`tsconfig.json`, no CI) — the real source, CI, and versioning live in the pushed [vitanovaPackages](https://sources.vitanova.network/sdarbinyan/vitanovaPackages.git) repo (§1). No app code has moved yet — `src/app/core/auth`, `src/app/core/admin-auth`, `src/app/core/finance`, `src/app/core/pricing` are still the live implementation in `marketplaces`.
|
||||||
|
|
||||||
|
## 1. Target repo
|
||||||
|
|
||||||
|
Pushed: [sources.vitanova.network/sdarbinyan/vitanovaPackages](https://sources.vitanova.network/sdarbinyan/vitanovaPackages.git) — single monorepo (npm workspaces) hosting both `packages/auth` and `packages/payment`, `main` branch, initial scaffold commit `5567154`. The `packages/*` scaffold here in `marketplaces` stays as the pre-push staging copy; the pushed repo is now the source of truth for the package source itself.
|
||||||
|
|
||||||
|
## 2. Versioning
|
||||||
|
|
||||||
|
Both packages live in one monorepo (vitanovaPackages, npm workspaces), so versioning uses [Changesets](https://github.com/changesets/changesets), not per-package semantic-release — Changesets is built for exactly this "many packages, one repo, independent versions" shape. A PR that changes `packages/auth` adds a changeset file (`npx changeset` from repo root, picks package + bump type + writes a short description) alongside the code change.
|
||||||
|
|
||||||
|
## 3. Publishing (CI)
|
||||||
|
|
||||||
|
`vitanovaPackages/.github/workflows/release.yml`: on push to `main`, installs, builds, tests, then `changesets/action`:
|
||||||
|
- if unreleased changesets exist, opens/updates a "Version Packages" PR that bumps `package.json` versions and writes changelogs,
|
||||||
|
- once that PR is merged, the next push to `main` publishes the bumped package(s) to the registry.
|
||||||
|
|
||||||
|
Requires two repo secrets: `NPM_TOKEN` (publish token) and `GITHUB_TOKEN` (auto-provided on GitHub Actions; use the Gitea/Forgejo equivalent if this host isn't GitHub-Actions-native — check with whoever administers `sources.vitanova.network`).
|
||||||
|
|
||||||
|
Registry choice — pick one before first publish:
|
||||||
|
- **npm private scope** (`@marketplaces` org on npmjs.com) — simplest, works with the workflow as-is.
|
||||||
|
- **GitHub Packages** — swap `registry-url` in the workflow to `https://npm.pkg.github.com`.
|
||||||
|
- **Self-hosted (Verdaccio) on the dev server** — point `registry-url` at the server's registry endpoint; requires the registry to be stood up on `213.21.246.138` first (not done yet).
|
||||||
|
|
||||||
|
## 4. Consuming from `marketplaces` (and other projects)
|
||||||
|
|
||||||
|
Once published:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @marketplaces/auth @marketplaces/payment
|
||||||
|
```
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { ... } from '@marketplaces/auth';
|
||||||
|
```
|
||||||
|
|
||||||
|
Pin exact versions (no `^`/`~` ranges) per ADR-0001's consequence about registry-outage blast radius — bump deliberately, not automatically, on this side.
|
||||||
|
|
||||||
|
[renovate.json](../renovate.json) at repo root opens a grouped PR whenever either package publishes a new version — review and merge it manually (`automerge: false`), it does not land unattended.
|
||||||
|
|
||||||
|
## 5. Migration cutover
|
||||||
|
|
||||||
|
**Auth: done.** `@marketplaces/auth` now holds the real implementation — two independent modules, `telegram/` (live Telegram QR/session auth, customer + admin) and `ed25519/` (future challenge/response admin auth, backend not shipped). Environment coupling was replaced with `AUTH_API_URL`/`TELEGRAM_BOT_USERNAME` injection tokens, provided from `app.config.ts`; `environment.production` became Angular's `isDevMode()`. `AdminPermissionsService` and `requireAdminPermission` stayed in `marketplaces` (`core/admin-auth/`) since they read this app's mock Users domain, not a portable auth concern. All ~30 call sites now import `@marketplaces/auth`; the old `src/app/core/auth`, `src/app/core/admin-auth/admin-auth.service.ts` (+ interceptor, ed25519 files), `src/app/services/auth.service.ts`, `src/app/services/telegram-session-api.service.ts`, and `src/app/models/auth.model.ts`/`admin-auth.model.ts` are deleted. `npm run build`, `npm run arch:check:boundaries`, and `npm test` (103/103) all pass. Wired as a `file:packages/auth` dependency until the registry (§3) is live — swap to a real semver range once published.
|
||||||
|
|
||||||
|
**Payment: not started.** `core/finance`/`core/pricing` still live in `marketplaces`, same process as above once prioritized.
|
||||||
@@ -75,7 +75,35 @@ Required before: bank/payment detail changes (Phase 5 §5), production launch (P
|
|||||||
|
|
||||||
Customer/seller PII is exposed only to roles that need it for their scope (e.g. `FINANCE_VIEWER` sees payout totals, not raw bank account numbers unless `FINANCE_MANAGER`+). Export endpoints (`GET .../export`) are themselves audit-logged actions per §3.
|
Customer/seller PII is exposed only to roles that need it for their scope (e.g. `FINANCE_VIEWER` sees payout totals, not raw bank account numbers unless `FINANCE_MANAGER`+). Export endpoints (`GET .../export`) are themselves audit-logged actions per §3.
|
||||||
|
|
||||||
## 8. What the frontend will start doing once this ships
|
## 8. Initial admin provisioning & self-service admin management
|
||||||
|
|
||||||
|
Each marketplace ships with one bootstrap `MARKETPLACE_ADMIN` account, seeded at provisioning time (Phase 9 launch step):
|
||||||
|
|
||||||
|
- `login` = marketplace slug (`projectName`)
|
||||||
|
- `password` = `{projectName}2026$`, flagged `mustChangePassword: true`
|
||||||
|
- Login succeeds but every non-auth request 403s with `PASSWORD_CHANGE_REQUIRED` until password is changed.
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/identity/v1/session/change-password { currentPassword, newPassword }
|
||||||
|
```
|
||||||
|
|
||||||
|
A `MARKETPLACE_ADMIN` can then provision sub-admins scoped to their own marketplace only — mirrors the seller-team invite pattern in [Phase 5](PHASE-5-SELLER-PORTAL-CONTRACT.md) (`POST /api/seller/v1/team/invite`):
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/admin/v2/team/invite { email, role: MarketplaceRole, marketplaceId }
|
||||||
|
GET /api/admin/v2/team?marketplaceId=
|
||||||
|
PATCH /api/admin/v2/team/{userId} { role }
|
||||||
|
DELETE /api/admin/v2/team/{userId}
|
||||||
|
```
|
||||||
|
|
||||||
|
Invariants:
|
||||||
|
- `role` must be one of the `MarketplaceRole` set (§1) — never `PlatformRole`. Backend rejects any attempt to grant a platform-scope role through this endpoint (`403 SCOPE_ESCALATION_DENIED`).
|
||||||
|
- `marketplaceId` is forced server-side to the caller's own tenant scope — request body value is ignored/validated, never trusted.
|
||||||
|
- Every invite/role-change/removal is an audit-logged action (§3, `action: 'admin_team.invited' | 'admin_team.role_changed' | 'admin_team.removed'`).
|
||||||
|
- Role grants at `MARKETPLACE_ADMIN` level require step-up auth (§6).
|
||||||
|
- Invited admins get their own credentials (email + set-password flow), not the shared bootstrap login — the bootstrap account is for first login only and should be rotated/retired once real admins exist.
|
||||||
|
|
||||||
|
## 9. What the frontend will start doing once this ships
|
||||||
|
|
||||||
- Route guards and action-level permission checks across the entire backoffice — currently none exist.
|
- Route guards and action-level permission checks across the entire backoffice — currently none exist.
|
||||||
- Backoffice **Audit & Security** section (missing from admin nav today): role changes, sensitive actions, login/security events, exports.
|
- Backoffice **Audit & Security** section (missing from admin nav today): role changes, sensitive actions, login/security events, exports.
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
id: ADR-0001
|
||||||
|
title: Extract auth and payment into shared @marketplaces packages
|
||||||
|
status: active
|
||||||
|
date: 2026-08-17
|
||||||
|
supersedes: []
|
||||||
|
tags: [architecture, auth, payment, monorepo]
|
||||||
|
---
|
||||||
|
|
||||||
|
# ADR-0001: Extract auth and payment into shared @marketplaces packages
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`marketplaces` currently owns auth end-to-end: customer auth (`core/auth` — VK ID, OTP, session, facade), admin auth (`core/admin-auth` — ed25519-verified admin sessions, permission guards, interceptor), and a legacy `services/auth.service.ts`. Payment/finance logic (`core/finance`, `core/pricing`) is server-owned per [Phase 1](../../backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md) and [Phase 7](../../backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md) contracts — the frontend piece is thin (gateways/tokens, no business logic).
|
||||||
|
|
||||||
|
Multiple marketplace projects beyond this repo need the same auth and payment client logic. Duplicating it per-project drifts fast (auth bugs get fixed in one place, not others) and blocks a consistent security posture across projects — directly relevant to [TRACK-S-SECURITY-RBAC-CONTRACT.md](../../backend/TRACK-S-SECURITY-RBAC-CONTRACT.md), which already treats auth/RBAC as the single most serious cross-cutting concern.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Extract auth and payment client logic into two standalone, independently versioned npm packages:
|
||||||
|
|
||||||
|
- `@marketplaces/auth` — customer auth (VK ID/OTP/session), admin auth (ed25519 verification, permission guards, interceptors), token/session management.
|
||||||
|
- `@marketplaces/payment` — payment/finance client gateways, FX/pricing models, checkout client contracts (thin — business logic stays backend per Phase 1/7).
|
||||||
|
|
||||||
|
Each package:
|
||||||
|
1. Lives in its own git repo (handed over separately; this repo does not host it long-term).
|
||||||
|
2. Is consumed by `marketplaces` (and other projects) as an installed node_modules dependency — imported, never copy-pasted.
|
||||||
|
3. Is versioned with semver; CI on the package repo auto-bumps and publishes on push to `main`, driven by conventional commit prefixes already used in this repo (`feat:`/`fix:`/etc — semantic-release reads these directly).
|
||||||
|
4. Ships with its own test suite; `marketplaces` treats it as a black-box dependency, not source to edit in place.
|
||||||
|
|
||||||
|
Rollout order: scaffold packages and CI in this repo first (reversible, local-only) → hand over target git repo → publish → migrate `marketplaces` call sites to import from the package → delete the in-repo originals only after the app builds and passes tests against the package.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `marketplaces` loses direct edit access to auth/payment source — changes go through the package's own repo/PR/release cycle. Slower iteration, but consistent behavior across all consuming projects.
|
||||||
|
- ~30 call sites in `marketplaces` (see `core/auth`, `core/admin-auth`, `services/auth.service.ts`, interceptors) need import rewiring during migration — tracked as follow-up work, not done in this ADR.
|
||||||
|
- New failure mode: package registry/CI outage blocks `marketplaces` builds if a version bump lands mid-incident. Pin exact versions, do not use floating ranges, to keep this bounded.
|
||||||
|
- [TRACK-S-SECURITY-RBAC-CONTRACT.md](../../backend/TRACK-S-SECURITY-RBAC-CONTRACT.md) §8 (admin provisioning) becomes package-owned behavior once migrated — that doc's endpoint contracts stay backend-side and unaffected, only the frontend client implementation moves.
|
||||||
@@ -4,3 +4,4 @@
|
|||||||
{"id":"PV-20260713T000000Z-0004","subject":"translatable-fields","predicate":"must-be-modeled-as","object":"generic translations.{lang} map so adding/removing a language automatically exposes/removes translation fields across all translatable objects","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["i18n","constraint"]}
|
{"id":"PV-20260713T000000Z-0004","subject":"translatable-fields","predicate":"must-be-modeled-as","object":"generic translations.{lang} map so adding/removing a language automatically exposes/removes translation fields across all translatable objects","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["i18n","constraint"]}
|
||||||
{"id":"PV-20260713T000000Z-0005","subject":"admin-app","predicate":"is-isolated-from","object":"marketplace storefront bundle: admin code never ships to storefront and vice versa, though they may share a domain","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["admin","security"]}
|
{"id":"PV-20260713T000000Z-0005","subject":"admin-app","predicate":"is-isolated-from","object":"marketplace storefront bundle: admin code never ships to storefront and vice versa, though they may share a domain","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["admin","security"]}
|
||||||
{"id":"PV-20260713T000000Z-0006","subject":"widgets","predicate":"must-not-own","object":"page spacing or page width; the renderer owns sections, spacing, and page width, widgets own only their internal layout","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["widgets","layout"]}
|
{"id":"PV-20260713T000000Z-0006","subject":"widgets","predicate":"must-not-own","object":"page spacing or page width; the renderer owns sections, spacing, and page width, widgets own only their internal layout","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["widgets","layout"]}
|
||||||
|
{"id":"PV-20260818T001500Z-a1f3","subject":"auth-and-payment-client-logic","predicate":"is-decided-to-extract-into","object":"standalone versioned npm packages @marketplaces/auth and @marketplaces/payment, installed as dependencies rather than edited in-repo","src":["docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md"],"status":"active","kind":"decision","updated_at":"2026-08-18T00:15:00Z","confidence":"high","tags":["architecture","auth","payment","decision"]}
|
||||||
|
|||||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"@angular/platform-browser": "22.0.8",
|
"@angular/platform-browser": "22.0.8",
|
||||||
"@angular/router": "22.0.8",
|
"@angular/router": "22.0.8",
|
||||||
"@angular/service-worker": "22.0.8",
|
"@angular/service-worker": "22.0.8",
|
||||||
|
"@marketplaces/auth": "file:packages/auth",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.8.0",
|
"tslib": "^2.8.0",
|
||||||
"zone.js": "~0.16.0"
|
"zone.js": "~0.16.0"
|
||||||
@@ -1968,6 +1969,10 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@marketplaces/auth": {
|
||||||
|
"resolved": "packages/auth",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.29.0",
|
"version": "1.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
|
||||||
@@ -9234,6 +9239,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.0.tgz",
|
||||||
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
|
"integrity": "sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"packages/auth": {
|
||||||
|
"name": "@marketplaces/auth",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">=22.0.0",
|
||||||
|
"@angular/core": ">=22.0.0",
|
||||||
|
"rxjs": ">=7.8.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@marketplaces/auth": "file:packages/auth",
|
||||||
"@angular/animations": "22.0.8",
|
"@angular/animations": "22.0.8",
|
||||||
"@angular/cdk": "22.0.6",
|
"@angular/cdk": "22.0.6",
|
||||||
"@angular/common": "22.0.8",
|
"@angular/common": "22.0.8",
|
||||||
|
|||||||
11
packages/README.md
Normal file
11
packages/README.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# packages/
|
||||||
|
|
||||||
|
Scaffolding for `@marketplaces/auth` and `@marketplaces/payment` — see [ADR-0001](../docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md).
|
||||||
|
|
||||||
|
These are standalone-buildable packages (their own `package.json`/`tsconfig.json`), not yet wired into this repo's Angular build and not yet containing migrated code. They live here temporarily so the structure, versioning, and CI can be reviewed before:
|
||||||
|
|
||||||
|
1. a target git repo is created for each package (or one shared repo, per ADR),
|
||||||
|
2. code moves out of `src/app/core/auth`, `src/app/core/admin-auth`, `src/app/core/finance`, `src/app/core/pricing`,
|
||||||
|
3. this repo switches to installing them as normal npm dependencies.
|
||||||
|
|
||||||
|
See [docs/PACKAGE-EXTRACTION.md](../docs/PACKAGE-EXTRACTION.md) for the full build/version/publish/consume workflow.
|
||||||
21
packages/auth/package.json
Normal file
21
packages/auth/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@marketplaces/auth",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Shared customer + admin auth client (VK ID/OTP/session, ed25519 admin verification, guards, interceptors) for marketplaces projects.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": ["dist"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"test": "echo \"no tests yet\" && exit 0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=22.0.0",
|
||||||
|
"@angular/common": ">=22.0.0",
|
||||||
|
"rxjs": ">=7.8.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "restricted"
|
||||||
|
},
|
||||||
|
"license": "UNLICENSED"
|
||||||
|
}
|
||||||
7
packages/auth/src/config.ts
Normal file
7
packages/auth/src/config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
|
||||||
|
/** Base URL for the auth backend, e.g. `https://api.example.com`. Provide from the consuming app's environment config. */
|
||||||
|
export const AUTH_API_URL = new InjectionToken<string>('@marketplaces/auth AUTH_API_URL');
|
||||||
|
|
||||||
|
/** Telegram bot username used to build QR/deep-link login URLs. Optional — falls back to a default if not provided. */
|
||||||
|
export const TELEGRAM_BOT_USERNAME = new InjectionToken<string>('@marketplaces/auth TELEGRAM_BOT_USERNAME');
|
||||||
@@ -1,22 +1,20 @@
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { AUTH_API_URL } from '../config';
|
||||||
import { AuthChallenge, AuthTokenPair, RefreshTokenRequest, VerifySignatureRequest } from '../models/auth-api.model';
|
import { AuthChallenge, AuthTokenPair, RefreshTokenRequest, VerifySignatureRequest } from './models/auth-api.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thin HTTP client for the Ed25519 admin auth endpoints documented in
|
* Thin HTTP client for the Ed25519 admin auth endpoints. These endpoints may
|
||||||
* docs/AUTH.md. These endpoints do not exist on the backend yet (FUTURE -
|
* not exist on every backend yet - calling them before the backend ships
|
||||||
* see docs/backend/BACKEND-INTEGRATION.md §2.5) - calling them today 404s
|
* 404s or connection-errors, which AuthService maps to the
|
||||||
* or connection-errors, which AuthService maps to the
|
|
||||||
* `backend-unavailable` error screen. No mock/fake responses are fabricated
|
* `backend-unavailable` error screen. No mock/fake responses are fabricated
|
||||||
* here; this is real HttpClient wiring against the real contract, ready for
|
* here; this is real HttpClient wiring against the real contract.
|
||||||
* the moment the backend ships.
|
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthApiService {
|
export class AuthApiService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
private readonly baseUrl = `${environment.authApiUrl}/api/admin/auth`;
|
private readonly baseUrl = `${inject(AUTH_API_URL)}/api/admin/auth`;
|
||||||
|
|
||||||
requestChallenge(): Observable<AuthChallenge> {
|
requestChallenge(): Observable<AuthChallenge> {
|
||||||
return this.http.get<AuthChallenge>(`${this.baseUrl}/challenge`);
|
return this.http.get<AuthChallenge>(`${this.baseUrl}/challenge`);
|
||||||
@@ -4,7 +4,7 @@ import { finalize } from 'rxjs';
|
|||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { PermissionService } from './permission.service';
|
import { PermissionService } from './permission.service';
|
||||||
import { SessionService } from './session.service';
|
import { SessionService } from './session.service';
|
||||||
import { Permission } from '../models/permission.model';
|
import { Permission } from './models/permission.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public surface for components/pages. Components should depend on this,
|
* Public surface for components/pages. Components should depend on this,
|
||||||
@@ -2,8 +2,8 @@ import { Injectable, inject, signal } from '@angular/core';
|
|||||||
import { HttpErrorResponse } from '@angular/common/http';
|
import { HttpErrorResponse } from '@angular/common/http';
|
||||||
import { catchError, switchMap, tap, throwError } from 'rxjs';
|
import { catchError, switchMap, tap, throwError } from 'rxjs';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { AuthTokenPair } from '../models/auth-api.model';
|
import { AuthTokenPair } from './models/auth-api.model';
|
||||||
import { AuthError, authErrorCodeFromBackendCode, authErrorCodeFromStatus } from '../models/auth-error.model';
|
import { AuthError, authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './models/auth-error.model';
|
||||||
import { AuthApiService } from './auth-api.service';
|
import { AuthApiService } from './auth-api.service';
|
||||||
import { Ed25519KeypairService } from './ed25519-keypair.service';
|
import { Ed25519KeypairService } from './ed25519-keypair.service';
|
||||||
import { SessionService } from './session.service';
|
import { SessionService } from './session.service';
|
||||||
@@ -18,7 +18,9 @@ export type LoginPhase = 'idle' | 'requesting-challenge' | 'signing' | 'verifyin
|
|||||||
* POST /api/admin/auth/verify -> { token, refreshToken }
|
* POST /api/admin/auth/verify -> { token, refreshToken }
|
||||||
*
|
*
|
||||||
* This is the lowest-level orchestrator; components should go through
|
* This is the lowest-level orchestrator; components should go through
|
||||||
* AuthFacade rather than calling this directly.
|
* AuthFacade rather than calling this directly. Exported from the package
|
||||||
|
* barrel as `Ed25519AuthService` to avoid colliding with the telegram
|
||||||
|
* module's `AuthService`.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { JwtClaims } from '../models/auth-api.model';
|
import { JwtClaims } from './models/auth-api.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client-side JWT *decoding* only - never verification. The signature is
|
* Client-side JWT *decoding* only - never verification. The signature is
|
||||||
@@ -1,11 +1,6 @@
|
|||||||
import { AdminRole } from './permission.model';
|
import { AdminRole } from './permission.model';
|
||||||
|
|
||||||
/**
|
/** Wire contracts for the Ed25519 challenge/response admin auth flow. */
|
||||||
* Wire contracts for the Ed25519 challenge/response admin auth flow. These
|
|
||||||
* are documented in docs/AUTH.md and match the endpoints listed there
|
|
||||||
* exactly - none of this is invented beyond what's documented as FUTURE
|
|
||||||
* there and in docs/backend/BACKEND-INTEGRATION.md §2.5.
|
|
||||||
*/
|
|
||||||
export interface AuthChallenge {
|
export interface AuthChallenge {
|
||||||
nonce: string;
|
nonce: string;
|
||||||
/** ISO 8601 issue time of the challenge. */
|
/** ISO 8601 issue time of the challenge. */
|
||||||
@@ -33,7 +28,7 @@ export interface RefreshTokenRequest {
|
|||||||
* Claims expected in the JWT `token`. Decoded client-side for display/UX
|
* Claims expected in the JWT `token`. Decoded client-side for display/UX
|
||||||
* only (role-gating UI, expiry countdown) - the frontend never treats this
|
* only (role-gating UI, expiry countdown) - the frontend never treats this
|
||||||
* as proof of authorization; every admin request is still re-checked
|
* as proof of authorization; every admin request is still re-checked
|
||||||
* server-side per docs/AUTH.md security considerations.
|
* server-side.
|
||||||
*/
|
*/
|
||||||
export interface JwtClaims {
|
export interface JwtClaims {
|
||||||
sub: string;
|
sub: string;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Error codes the Ed25519 admin auth flow can surface to the UI. Each maps to
|
* Error codes the Ed25519 admin auth flow can surface to the UI. Each maps to
|
||||||
* a dedicated screen (see `core/auth/pages`) rather than a generic toast,
|
* a dedicated screen rather than a generic toast, because the recovery
|
||||||
* because the recovery action differs per code (re-login vs. retry vs. wait).
|
* action differs per code (re-login vs. retry vs. wait).
|
||||||
*/
|
*/
|
||||||
export type AuthErrorCode =
|
export type AuthErrorCode =
|
||||||
| 'session-expired'
|
| 'session-expired'
|
||||||
@@ -17,12 +17,7 @@ export interface AuthError {
|
|||||||
status?: number;
|
status?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Maps a backend error envelope's `error.code` to the client's AuthErrorCode screens. Only codes with a dedicated screen are mapped; anything else falls back to the HTTP-status-derived code via authErrorCodeFromStatus. */
|
||||||
* Maps the backend error envelope's `error.code` (see
|
|
||||||
* BACKEND-API-REFERENCE.md §5) to the client's AuthErrorCode screens.
|
|
||||||
* Only codes with a dedicated screen are mapped; anything else falls back
|
|
||||||
* to the HTTP-status-derived code via authErrorCodeFromStatus.
|
|
||||||
*/
|
|
||||||
const BACKEND_ERROR_CODE_MAP: Record<string, AuthErrorCode> = {
|
const BACKEND_ERROR_CODE_MAP: Record<string, AuthErrorCode> = {
|
||||||
TOKEN_EXPIRED: 'session-expired',
|
TOKEN_EXPIRED: 'session-expired',
|
||||||
INVALID_SIGNATURE: 'invalid-signature',
|
INVALID_SIGNATURE: 'invalid-signature',
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
/**
|
/** Roles the Ed25519 JWT `role` claim is expected to carry. Ordered highest-to-lowest privilege; PermissionService does not rely on the order, it is documentation only. */
|
||||||
* Roles the Ed25519 JWT `role` claim is expected to carry (see docs/AUTH.md
|
|
||||||
* §JWT Claims). Ordered highest-to-lowest privilege; PermissionService does
|
|
||||||
* not rely on the order, it is documentation only.
|
|
||||||
*/
|
|
||||||
export type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
|
export type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coarse-grained permission keys. Intentionally small and domain-agnostic
|
* Coarse-grained permission keys. Intentionally small and domain-agnostic -
|
||||||
* (mirrors the existing bootstrap-level `PermissionsConfig` shape in
|
* fine-grained, per-domain permissions stay server-side; the frontend only
|
||||||
* `shared/models/config/permissions.model.ts`) - fine-grained, per-domain
|
* needs enough to hide/disable UI, never to be the source of truth for
|
||||||
* permissions stay server-side until the backend ships a real permission
|
* authorization.
|
||||||
* model; the frontend only needs enough to hide/disable UI, never to be the
|
|
||||||
* source of truth for authorization.
|
|
||||||
*/
|
*/
|
||||||
export type Permission =
|
export type Permission =
|
||||||
| 'backoffice.read'
|
| 'backoffice.read'
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable, computed, inject } from '@angular/core';
|
import { Injectable, computed, inject } from '@angular/core';
|
||||||
import { Permission, ROLE_PERMISSIONS } from '../models/permission.model';
|
import { Permission, ROLE_PERMISSIONS } from './models/permission.model';
|
||||||
import { SessionService } from './session.service';
|
import { SessionService } from './session.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Derives the current admin's permission set from their JWT `role` claim.
|
* Derives the current admin's permission set from their JWT `role` claim.
|
||||||
* UI-only gate (hide/disable) - the backend must independently enforce
|
* UI-only gate (hide/disable) - the backend must independently enforce
|
||||||
* every mutation server-side; see docs/AUTH.md security considerations.
|
* every mutation server-side.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PermissionService {
|
export class PermissionService {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, computed, signal } from '@angular/core';
|
import { Injectable, computed, signal } from '@angular/core';
|
||||||
import { AuthTokenPair, JwtClaims } from '../models/auth-api.model';
|
import { AuthTokenPair, JwtClaims } from './models/auth-api.model';
|
||||||
import { JwtService } from './jwt.service';
|
import { JwtService } from './jwt.service';
|
||||||
|
|
||||||
export type SessionStatus = 'unknown' | 'restoring' | 'authenticated' | 'unauthenticated' | 'expired';
|
export type SessionStatus = 'unknown' | 'restoring' | 'authenticated' | 'unauthenticated' | 'expired';
|
||||||
@@ -11,9 +11,9 @@ const REFRESH_SKEW_MS = 60_000;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate
|
* Holds the Ed25519-flow JWT/refresh-token pair and derived claims. Separate
|
||||||
* from AdminAuthService (Telegram-session state) by design - the two auth
|
* from the telegram module's AdminAuthService (Telegram-session state) by
|
||||||
* mechanisms are not merged until the backend actually ships the Ed25519
|
* design - the two auth mechanisms are not merged until both ship on the
|
||||||
* endpoints and a migration decision is made (see docs/AUTH.md).
|
* same backend and a migration decision is made.
|
||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class SessionService {
|
export class SessionService {
|
||||||
43
packages/auth/src/index.ts
Normal file
43
packages/auth/src/index.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// @marketplaces/auth — public API barrel.
|
||||||
|
// Two independent auth mechanisms, per ADR-0001 (marketplaces repo:
|
||||||
|
// docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md):
|
||||||
|
// - telegram/ — live Telegram QR/session auth (customer + admin)
|
||||||
|
// - ed25519/ — future Ed25519 challenge/response admin auth (backend not shipped yet)
|
||||||
|
// Provide AUTH_API_URL (and optionally TELEGRAM_BOT_USERNAME) from the consuming app's config.
|
||||||
|
|
||||||
|
export { AUTH_API_URL, TELEGRAM_BOT_USERNAME } from './config';
|
||||||
|
|
||||||
|
// Telegram module
|
||||||
|
export { AuthSession, WebSessionStart, AuthStatus, AdminAuthStatus } from './telegram/models/session.model';
|
||||||
|
export { TelegramSessionApiService } from './telegram/telegram-session-api.service';
|
||||||
|
export { AuthService } from './telegram/auth.service';
|
||||||
|
export { AdminAuthService } from './telegram/admin-auth.service';
|
||||||
|
export { adminAuthGuard } from './telegram/admin-auth.guard';
|
||||||
|
export { adminAuthHeadersInterceptor } from './telegram/admin-auth-headers.interceptor';
|
||||||
|
|
||||||
|
// Ed25519 module (namespaced re-exports to avoid colliding with the telegram module's AuthService)
|
||||||
|
export { AuthService as Ed25519AuthService } from './ed25519/auth.service';
|
||||||
|
export { AuthFacade } from './ed25519/auth-facade.service';
|
||||||
|
export { AuthApiService } from './ed25519/auth-api.service';
|
||||||
|
export { SessionService } from './ed25519/session.service';
|
||||||
|
export { JwtService } from './ed25519/jwt.service';
|
||||||
|
export { Ed25519KeypairService } from './ed25519/ed25519-keypair.service';
|
||||||
|
export { PermissionService } from './ed25519/permission.service';
|
||||||
|
export {
|
||||||
|
Ed25519VerificationService,
|
||||||
|
Ed25519Challenge,
|
||||||
|
Ed25519SignedResponse,
|
||||||
|
Ed25519VerificationResult
|
||||||
|
} from './ed25519/ed25519-verification.model';
|
||||||
|
export { NoopEd25519VerificationService } from './ed25519/noop-ed25519-verification.service';
|
||||||
|
export {
|
||||||
|
AuthChallenge,
|
||||||
|
VerifySignatureRequest,
|
||||||
|
AuthTokenPair,
|
||||||
|
RefreshTokenRequest,
|
||||||
|
JwtClaims
|
||||||
|
} from './ed25519/models/auth-api.model';
|
||||||
|
export { AuthErrorCode, AuthError, authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './ed25519/models/auth-error.model';
|
||||||
|
export { AdminRole, Permission, ROLE_PERMISSIONS } from './ed25519/models/permission.model';
|
||||||
|
export type { LoginPhase } from './ed25519/auth.service';
|
||||||
|
export type { SessionStatus } from './ed25519/session.service';
|
||||||
@@ -2,14 +2,13 @@ import { HttpInterceptorFn } from '@angular/common/http';
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { AdminAuthService } from './admin-auth.service';
|
import { AdminAuthService } from './admin-auth.service';
|
||||||
|
|
||||||
/** Backend paths that require an active AdminWebSessionID per API-REFERENCE.md §0. */
|
/** Backend paths that require an active AdminWebSessionID. Adjust to match your API surface if consuming this outside marketplaces. */
|
||||||
const ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/'];
|
const ADMIN_GATED_PATH_SEGMENTS = ['/admin/', '/backoffice/', '/builder/', '/media/'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches admin session/token headers only to admin API requests. Mirrors
|
* Attaches admin session/token headers only to admin API requests. Scoped to
|
||||||
* apiHeadersInterceptor's self-guarding pattern but scoped to admin-gated
|
* admin-gated paths so it never touches customer requests and never reads
|
||||||
* paths so it never touches customer requests and never reads AuthService's
|
* the customer AuthService's session.
|
||||||
* session.
|
|
||||||
*/
|
*/
|
||||||
export const adminAuthHeadersInterceptor: HttpInterceptorFn = (req, next) => {
|
export const adminAuthHeadersInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment));
|
const isAdminRequest = ADMIN_GATED_PATH_SEGMENTS.some(segment => req.url.includes(segment));
|
||||||
15
packages/auth/src/telegram/admin-auth.guard.ts
Normal file
15
packages/auth/src/telegram/admin-auth.guard.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { inject } from '@angular/core';
|
||||||
|
import { CanActivateFn } from '@angular/router';
|
||||||
|
import { AdminAuthService } from './admin-auth.service';
|
||||||
|
|
||||||
|
/** Guards `/admin/**`-style routes. Never shares state with the customer auth guard/service. */
|
||||||
|
export const adminAuthGuard: CanActivateFn = () => {
|
||||||
|
const adminAuth = inject(AdminAuthService);
|
||||||
|
|
||||||
|
if (adminAuth.isAuthenticated()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
adminAuth.requestLogin();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
import { Injectable, signal, computed, inject, isDevMode } from '@angular/core';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { Observable, tap } from 'rxjs';
|
||||||
import { AdminAuthStatus } from '../../models/admin-auth.model';
|
import { AdminAuthStatus, AuthSession, WebSessionStart } from './models/session.model';
|
||||||
import { AuthSession, WebSessionStart } from '../../models/auth.model';
|
import { TelegramSessionApiService } from './telegram-session-api.service';
|
||||||
import { TelegramSessionApiService } from '../../services/telegram-session-api.service';
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin login uses the exact same Telegram QR/session API as the customer
|
* Admin login uses the exact same Telegram QR/session API as the customer
|
||||||
* login (TelegramSessionApiService, `{authApiUrl}/users/sessions`) - there is
|
* login (TelegramSessionApiService) - there is no separate admin backend
|
||||||
* no separate admin backend endpoint, and none should be invented client-side.
|
* endpoint, and none should be invented client-side. Only the *storage* is
|
||||||
* Only the *storage* is kept separate from AuthService, so an admin QR scan
|
* kept separate from AuthService, so an admin QR scan never authenticates
|
||||||
* never authenticates the customer session or vice versa: distinct cookie
|
* the customer session or vice versa: distinct cookie name, distinct
|
||||||
* name, distinct signals, distinct guard/interceptor.
|
* signals, distinct guard/interceptor.
|
||||||
*
|
*
|
||||||
* Backend gap this creates (see docs/backend/BACKEND-INTEGRATION.md §2.5): since the session
|
* Since the session API itself has no concept of "admin", the frontend
|
||||||
* API itself has no concept of "admin", the frontend cannot tell an admin
|
* cannot tell an admin Telegram session from a regular one. Actual admin
|
||||||
* Telegram session from a regular one. Actual admin authorization must be
|
* authorization must be enforced server-side when admin API calls are made
|
||||||
* enforced server-side when admin API calls are made with the resulting
|
* with the resulting session id - the frontend only decides where to
|
||||||
* session id - the frontend only decides where to *store* the result.
|
* *store* the result.
|
||||||
*/
|
*/
|
||||||
const ADMIN_SESSION_COOKIE = 'adminSessionID';
|
const ADMIN_SESSION_COOKIE = 'adminSessionID';
|
||||||
const ADMIN_TOKEN_STORAGE_KEY = 'adminToken';
|
const ADMIN_TOKEN_STORAGE_KEY = 'adminToken';
|
||||||
@@ -93,12 +91,12 @@ export class AdminAuthService {
|
|||||||
/**
|
/**
|
||||||
* Dev-only shortcut for local testing without a reachable Telegram/session
|
* Dev-only shortcut for local testing without a reachable Telegram/session
|
||||||
* backend: fabricates a local session and activates it directly, skipping
|
* backend: fabricates a local session and activates it directly, skipping
|
||||||
* the QR flow entirely. No-ops in production builds (checked at runtime,
|
* the QR flow entirely. No-ops in production builds (checked via Angular's
|
||||||
* not just build-time, so it is safe even if this code ships). Never call
|
* isDevMode() at runtime, not just build-time, so it is safe even if this
|
||||||
* this from anywhere reachable in a production build.
|
* code ships). Never call this from anywhere reachable in a production build.
|
||||||
*/
|
*/
|
||||||
devBypassLogin(): void {
|
devBypassLogin(): void {
|
||||||
if (environment.production) {
|
if (!isDevMode()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hideLogin();
|
this.hideLogin();
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||||
import { Observable, tap } from 'rxjs';
|
import { Observable, tap } from 'rxjs';
|
||||||
import { AuthSession, AuthStatus, WebSessionStart } from '../models/auth.model';
|
import { AuthSession, AuthStatus, WebSessionStart } from './models/session.model';
|
||||||
import { TelegramSessionApiService } from './telegram-session-api.service';
|
import { TelegramSessionApiService } from './telegram-session-api.service';
|
||||||
|
|
||||||
const WEB_SESSION_COOKIE = 'webSessionID';
|
const WEB_SESSION_COOKIE = 'webSessionID';
|
||||||
const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;
|
const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;
|
||||||
|
|
||||||
|
/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@@ -13,3 +13,4 @@ export interface WebSessionStart {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated';
|
export type AuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated';
|
||||||
|
export type AdminAuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated';
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable, of, catchError, map } from 'rxjs';
|
import { Observable, of, catchError, map } from 'rxjs';
|
||||||
import { AuthSession, WebSessionStart } from '../models/auth.model';
|
import { AuthSession, WebSessionStart } from './models/session.model';
|
||||||
import { environment } from '../../environments/environment';
|
import { AUTH_API_URL, TELEGRAM_BOT_USERNAME } from '../config';
|
||||||
import { generateGuid } from '../shared/util/guid.util';
|
import { generateGuid } from '../util/guid.util';
|
||||||
|
|
||||||
const SESSION_MAX_AGE_SECONDS = 60 * 60;
|
const SESSION_MAX_AGE_SECONDS = 60 * 60;
|
||||||
|
const DEFAULT_TELEGRAM_BOT_USERNAME = 'DexarSupport_bot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer
|
* The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer
|
||||||
@@ -17,9 +18,9 @@ const SESSION_MAX_AGE_SECONDS = 60 * 60;
|
|||||||
*/
|
*/
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class TelegramSessionApiService {
|
export class TelegramSessionApiService {
|
||||||
private readonly authApiUrl = environment.authApiUrl;
|
private readonly http = inject(HttpClient);
|
||||||
|
private readonly authApiUrl = inject(AUTH_API_URL);
|
||||||
constructor(private readonly http: HttpClient) {}
|
private readonly telegramBotUsername = inject(TELEGRAM_BOT_USERNAME, { optional: true });
|
||||||
|
|
||||||
createSession(): Observable<WebSessionStart> {
|
createSession(): Observable<WebSessionStart> {
|
||||||
const webSessionID = generateGuid();
|
const webSessionID = generateGuid();
|
||||||
@@ -67,7 +68,7 @@ export class TelegramSessionApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getBotUsername(): string {
|
private getBotUsername(): string {
|
||||||
return (environment as Record<string, unknown>)['telegramBot'] as string || 'DexarSupport_bot';
|
return this.telegramBotUsername || DEFAULT_TELEGRAM_BOT_USERNAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
private normalizeWebSession(response: Record<string, unknown> | null, fallbackSessionId: string): AuthSession | null {
|
private normalizeWebSession(response: Record<string, unknown> | null, fallbackSessionId: string): AuthSession | null {
|
||||||
21
packages/auth/src/util/guid.util.ts
Normal file
21
packages/auth/src/util/guid.util.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
/** RFC4122 v4-ish GUID, using crypto when available. Shared by customer and admin session creation. */
|
||||||
|
export function generateGuid(): string {
|
||||||
|
if (globalThis.crypto?.randomUUID) {
|
||||||
|
return globalThis.crypto.randomUUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(16);
|
||||||
|
if (globalThis.crypto?.getRandomValues) {
|
||||||
|
globalThis.crypto.getRandomValues(bytes);
|
||||||
|
} else {
|
||||||
|
for (let index = 0; index < bytes.length; index++) {
|
||||||
|
bytes[index] = Math.floor(Math.random() * 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
|
|
||||||
|
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0'));
|
||||||
|
return `${hex.slice(0, 4).join('')}-${hex.slice(4, 6).join('')}-${hex.slice(6, 8).join('')}-${hex.slice(8, 10).join('')}-${hex.slice(10, 16).join('')}`;
|
||||||
|
}
|
||||||
15
packages/auth/tsconfig.json
Normal file
15
packages/auth/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
21
packages/payment/package.json
Normal file
21
packages/payment/package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "@marketplaces/payment",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Shared payment/finance client (FX, pricing, checkout gateways) for marketplaces projects. Thin by design — payment business logic stays server-side per Phase 1/7 backend contracts.",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"files": ["dist"],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"test": "echo \"no tests yet\" && exit 0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/core": ">=22.0.0",
|
||||||
|
"@angular/common": ">=22.0.0",
|
||||||
|
"rxjs": ">=7.8.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "restricted"
|
||||||
|
},
|
||||||
|
"license": "UNLICENSED"
|
||||||
|
}
|
||||||
5
packages/payment/src/index.ts
Normal file
5
packages/payment/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// @marketplaces/payment — public API barrel.
|
||||||
|
// Scaffold only: code migrates here from src/app/core/finance and src/app/core/pricing
|
||||||
|
// per ADR-0001 (docs/context/adrs/ADR-0001-extract-auth-and-payment-into-shared-marketplaces-packages.md).
|
||||||
|
// Nothing is exported yet — this repo still owns the live implementation until migration lands.
|
||||||
|
export {};
|
||||||
15
packages/payment/tsconfig.json
Normal file
15
packages/payment/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
12
renovate.json
Normal file
12
renovate.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["@marketplaces/auth", "@marketplaces/payment"],
|
||||||
|
"groupName": "marketplaces shared packages",
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["shared-package-update"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,12 +7,11 @@ import { cacheInterceptor } from './interceptors/cache.interceptor';
|
|||||||
import { apiBaseUrlInterceptor } from './interceptors/api-base-url.interceptor';
|
import { apiBaseUrlInterceptor } from './interceptors/api-base-url.interceptor';
|
||||||
import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor';
|
import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor';
|
||||||
import { mockDataInterceptor } from './interceptors/mock-data.interceptor';
|
import { mockDataInterceptor } from './interceptors/mock-data.interceptor';
|
||||||
import { adminAuthHeadersInterceptor } from './core/admin-auth/admin-auth-headers.interceptor';
|
import { adminAuthHeadersInterceptor, Ed25519VerificationService, NoopEd25519VerificationService, AUTH_API_URL, TELEGRAM_BOT_USERNAME } from '@marketplaces/auth';
|
||||||
import { Ed25519VerificationService } from './core/admin-auth/ed25519-verification.model';
|
|
||||||
import { NoopEd25519VerificationService } from './core/admin-auth/noop-ed25519-verification.service';
|
|
||||||
import { provideServiceWorker } from '@angular/service-worker';
|
import { provideServiceWorker } from '@angular/service-worker';
|
||||||
import { MediaRepository } from './core/media/media-repository';
|
import { MediaRepository } from './core/media/media-repository';
|
||||||
import { MockMediaRepository } from './core/media/mock-media-repository.service';
|
import { MockMediaRepository } from './core/media/mock-media-repository.service';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -25,6 +24,8 @@ export const appConfig: ApplicationConfig = {
|
|||||||
provideHttpClient(withXhr(),
|
provideHttpClient(withXhr(),
|
||||||
withInterceptors([mockDataInterceptor, apiBaseUrlInterceptor, apiHeadersInterceptor, adminAuthHeadersInterceptor, cacheInterceptor])
|
withInterceptors([mockDataInterceptor, apiBaseUrlInterceptor, apiHeadersInterceptor, adminAuthHeadersInterceptor, cacheInterceptor])
|
||||||
),
|
),
|
||||||
|
{ provide: AUTH_API_URL, useValue: environment.authApiUrl },
|
||||||
|
{ provide: TELEGRAM_BOT_USERNAME, useValue: environment.telegramBot },
|
||||||
{ provide: Ed25519VerificationService, useClass: NoopEd25519VerificationService },
|
{ provide: Ed25519VerificationService, useClass: NoopEd25519VerificationService },
|
||||||
{ provide: MediaRepository, useClass: MockMediaRepository },
|
{ provide: MediaRepository, useClass: MockMediaRepository },
|
||||||
provideServiceWorker('ngsw-worker.js', {
|
provideServiceWorker('ngsw-worker.js', {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { languageGuard } from './guards/language.guard';
|
import { languageGuard } from './guards/language.guard';
|
||||||
import { projectEditorDirtyGuard } from './features/project-editor/guards/project-editor-dirty.guard';
|
import { projectEditorDirtyGuard } from './features/project-editor/guards/project-editor-dirty.guard';
|
||||||
import { adminAuthGuard, requireAdminPermission } from './core/admin-auth/admin-auth.guard';
|
import { adminAuthGuard } from '@marketplaces/auth';
|
||||||
|
import { requireAdminPermission } from './core/admin-auth/admin-auth.guard';
|
||||||
import { authRoutes } from './core/auth/auth.routes';
|
import { authRoutes } from './core/auth/auth.routes';
|
||||||
import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard';
|
import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard';
|
||||||
import { adminProductDirtyGuard } from './features/admin/products/guards/admin-product-dirty.guard';
|
import { adminProductDirtyGuard } from './features/admin/products/guards/admin-product-dirty.guard';
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import { UiRuntimeFacade } from './facades/runtime/ui-runtime.facade';
|
|||||||
import { ApiHealthService } from './services/api-health.service';
|
import { ApiHealthService } from './services/api-health.service';
|
||||||
import { SeoService } from './services/seo.service';
|
import { SeoService } from './services/seo.service';
|
||||||
import { FloatingNotificationsComponent } from './features/website/user-experience/components/floating-notifications/floating-notifications.component';
|
import { FloatingNotificationsComponent } from './features/website/user-experience/components/floating-notifications/floating-notifications.component';
|
||||||
import { AdminAuthService } from './core/admin-auth/admin-auth.service';
|
import { AdminAuthService, AuthService } from '@marketplaces/auth';
|
||||||
import { AuthService } from './services/auth.service';
|
|
||||||
import { TelegramLoginComponent } from './components/telegram-login/telegram-login.component';
|
import { TelegramLoginComponent } from './components/telegram-login/telegram-login.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { of } from 'rxjs';
|
|||||||
import { BootstrapConfig } from '../../shared/models/config';
|
import { BootstrapConfig } from '../../shared/models/config';
|
||||||
import { CONFIG_PROVIDER } from '../../core/config/config-provider.token';
|
import { CONFIG_PROVIDER } from '../../core/config/config-provider.token';
|
||||||
import { ConfigService } from '../../core/config/config.service';
|
import { ConfigService } from '../../core/config/config.service';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService, AUTH_API_URL } from '@marketplaces/auth';
|
||||||
import { HeaderComponent } from './header.component';
|
import { HeaderComponent } from './header.component';
|
||||||
|
|
||||||
function makeBootstrap(): BootstrapConfig {
|
function makeBootstrap(): BootstrapConfig {
|
||||||
@@ -52,6 +52,7 @@ describe('HeaderComponent profile control (login/logout gating regression)', ()
|
|||||||
provideHttpClient(),
|
provideHttpClient(),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
{ provide: CONFIG_PROVIDER, useValue: { loadBootstrap: () => of(makeBootstrap()) } },
|
{ provide: CONFIG_PROVIDER, useValue: { loadBootstrap: () => of(makeBootstrap()) } },
|
||||||
|
{ provide: AUTH_API_URL, useValue: 'https://test.local' },
|
||||||
{ provide: AuthService, useValue: fakeAuth },
|
{ provide: AuthService, useValue: fakeAuth },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { FeatureConfigService } from '../../core/config/feature-config.service';
|
|||||||
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
|
import { DEFAULT_HEADER_CONFIG, DEFAULT_USER_EXPERIENCE_CONFIG } from '../../shared/models/config';
|
||||||
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
|
import { StaticPageResolverService } from '../../core/config/static-page-resolver.service';
|
||||||
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '@marketplaces/auth';
|
||||||
import { TelegramLoginComponent } from '../telegram-login/telegram-login.component';
|
import { TelegramLoginComponent } from '../telegram-login/telegram-login.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Component, ChangeDetectionStrategy, Input, Injector, Signal, inject, effect, OnDestroy, OnInit } from '@angular/core';
|
import { Component, ChangeDetectionStrategy, Input, Injector, Signal, inject, effect, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService, AdminAuthService, AuthSession } from '@marketplaces/auth';
|
||||||
import { AdminAuthService } from '../../core/admin-auth/admin-auth.service';
|
|
||||||
import { LanguageService } from '../../services/language.service';
|
import { LanguageService } from '../../services/language.service';
|
||||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
import { QrLoginEngine } from '../../shared/qr-login/qr-login.engine';
|
import { QrLoginEngine } from '../../shared/qr-login/qr-login.engine';
|
||||||
import { QrLoginAdapter, QrLoginStatus } from '../../shared/qr-login/qr-login.model';
|
import { QrLoginAdapter, QrLoginStatus } from '../../shared/qr-login/qr-login.model';
|
||||||
import { AuthSession } from '../../models/auth.model';
|
|
||||||
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
import { IconComponent } from '../../shared/ui/icon/icon.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
import { inject } from '@angular/core';
|
import { inject } from '@angular/core';
|
||||||
import { CanActivateFn } from '@angular/router';
|
import { CanActivateFn } from '@angular/router';
|
||||||
import { AdminAuthService } from './admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
import { AdminPermissionsService } from './admin-permissions.service';
|
import { AdminPermissionsService } from './admin-permissions.service';
|
||||||
|
|
||||||
/** Guards `/admin/**` routes. Never shares state with the customer auth guard/service. */
|
|
||||||
export const adminAuthGuard: CanActivateFn = () => {
|
|
||||||
const adminAuth = inject(AdminAuthService);
|
|
||||||
|
|
||||||
if (adminAuth.isAuthenticated()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
adminAuth.requestLogin();
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI-only gate for a specific permission, on top of adminAuthGuard's
|
* UI-only gate for a specific permission, on top of the package's
|
||||||
* authentication check. See AdminPermissionsService for why this is
|
* adminAuthGuard authentication check. See AdminPermissionsService for why
|
||||||
* cosmetic until the backend ships real admin-role enforcement.
|
* this is cosmetic until the backend ships real admin-role enforcement.
|
||||||
|
* Kept app-local because it depends on AdminPermissionsService, which reads
|
||||||
|
* this app's mock Users domain - not a portable auth concern.
|
||||||
*/
|
*/
|
||||||
export function requireAdminPermission(permission: string): CanActivateFn {
|
export function requireAdminPermission(permission: string): CanActivateFn {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable, computed, inject } from '@angular/core';
|
import { Injectable, computed, inject } from '@angular/core';
|
||||||
import { toSignal } from '@angular/core/rxjs-interop';
|
import { toSignal } from '@angular/core/rxjs-interop';
|
||||||
import { AdminAuthService } from './admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
import { AdminUsersLocalGateway } from '../../features/admin/users/services/admin-users-local.gateway';
|
import { AdminUsersLocalGateway } from '../../features/admin/users/services/admin-users-local.gateway';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||||
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
||||||
import { AuthFacade } from '../services/auth-facade.service';
|
import { AuthFacade, Ed25519KeypairService } from '@marketplaces/auth';
|
||||||
import { Ed25519KeypairService } from '../services/ed25519-keypair.service';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ed25519 admin login page. Prepared UI for the flow described in
|
* Ed25519 admin login page. Prepared UI for the flow described in
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
|
|||||||
import { map } from 'rxjs';
|
import { map } from 'rxjs';
|
||||||
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
||||||
import { EmptyStateComponent } from '../../../shared/ui/empty-state/empty-state.component';
|
import { EmptyStateComponent } from '../../../shared/ui/empty-state/empty-state.component';
|
||||||
import { AuthErrorCode } from '../models/auth-error.model';
|
import { AuthErrorCode } from '@marketplaces/auth';
|
||||||
|
|
||||||
interface AuthErrorCopy {
|
interface AuthErrorCopy {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Observable, map } from 'rxjs';
|
import { Observable, map } from 'rxjs';
|
||||||
import { ApiService } from '../../../services';
|
import { ApiService } from '../../../services';
|
||||||
import { AuthService } from '../../../services/auth.service';
|
import { AuthService } from '@marketplaces/auth';
|
||||||
import { CategoryService } from '../../categories/category.service';
|
import { CategoryService } from '../../categories/category.service';
|
||||||
import { ProductDataProvider } from './product-data-provider.interface';
|
import { ProductDataProvider } from './product-data-provider.interface';
|
||||||
import { Product, ProductCategory, ProductListQuery, ProductListResult, ProductSearchQuery, RelatedProductsQuery } from '../models/product-domain.model';
|
import { Product, ProductCategory, ProductListQuery, ProductListResult, ProductSearchQuery, RelatedProductsQuery } from '../models/product-domain.model';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Injectable, computed, effect, inject, signal } from '@angular/core';
|
|||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade';
|
import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade';
|
||||||
import { EditorSchemaService } from '../../../project-editor/schema/editor-schema.service';
|
import { EditorSchemaService } from '../../../project-editor/schema/editor-schema.service';
|
||||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
import { environment } from '../../../../../environments/environment';
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { ADMIN_DASHBOARD_METRICS_GATEWAY } from '../services/admin-dashboard-metrics-gateway.token';
|
import { ADMIN_DASHBOARD_METRICS_GATEWAY } from '../services/admin-dashboard-metrics-gateway.token';
|
||||||
import { AdminDashboardHistoryService } from '../services/admin-dashboard-history.service';
|
import { AdminDashboardHistoryService } from '../services/admin-dashboard-history.service';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Observable, of } from 'rxjs';
|
|||||||
import { delay } from 'rxjs/operators';
|
import { delay } from 'rxjs/operators';
|
||||||
import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model';
|
import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus, TERMINAL_ORDER_STATUSES } from '../models/admin-order.model';
|
||||||
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
|
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
|
||||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
const STATUSES: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
const STATUSES: AdminOrderStatus[] = ['pending', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
||||||
const CUSTOMER_NAMES = ['Anna Petrova', 'Karen Sargsyan', 'Ivan Ivanov', 'Mariam Grigoryan', 'Sergey Volkov', 'Lilit Hakobyan'];
|
const CUSTOMER_NAMES = ['Anna Petrova', 'Karen Sargsyan', 'Ivan Ivanov', 'Mariam Grigoryan', 'Sergey Volkov', 'Lilit Hakobyan'];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { signal } from '@angular/core';
|
import { signal } from '@angular/core';
|
||||||
|
import { AUTH_API_URL } from '@marketplaces/auth';
|
||||||
import { AdminLayoutComponent } from './admin-layout.component';
|
import { AdminLayoutComponent } from './admin-layout.component';
|
||||||
import { AdminOrderWatcherService } from './services/admin-order-watcher.service';
|
import { AdminOrderWatcherService } from './services/admin-order-watcher.service';
|
||||||
import { AdminOrder } from '../orders/models/admin-order.model';
|
import { AdminOrder } from '../orders/models/admin-order.model';
|
||||||
@@ -47,6 +48,7 @@ describe('AdminLayoutComponent notifications bell', () => {
|
|||||||
imports: [AdminLayoutComponent],
|
imports: [AdminLayoutComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
|
{ provide: AUTH_API_URL, useValue: 'https://test.local' },
|
||||||
{ provide: AdminOrderWatcherService, useValue: watcherStub },
|
{ provide: AdminOrderWatcherService, useValue: watcherStub },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|||||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||||
import { TranslateService } from '../../../i18n/translate.service';
|
import { TranslateService } from '../../../i18n/translate.service';
|
||||||
import { LanguageService } from '../../../services/language.service';
|
import { LanguageService } from '../../../services/language.service';
|
||||||
import { AdminAuthService } from '../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
import { ADMIN_NAV_BOTTOM, ADMIN_NAV_PRIMARY, AdminBreadcrumbEntry, AdminNavEntry } from './admin-nav.model';
|
import { ADMIN_NAV_BOTTOM, ADMIN_NAV_PRIMARY, AdminBreadcrumbEntry, AdminNavEntry } from './admin-nav.model';
|
||||||
import { IconComponent } from '../../../shared/ui/icon/icon.component';
|
import { IconComponent } from '../../../shared/ui/icon/icon.component';
|
||||||
import { AdminPreferencesService } from '../settings/services/admin-preferences.service';
|
import { AdminPreferencesService } from '../settings/services/admin-preferences.service';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { AdminOrderWatcherService } from './admin-order-watcher.service';
|
|||||||
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
||||||
import { AdminOrder, AdminOrdersListResult } from '../../orders/models/admin-order.model';
|
import { AdminOrder, AdminOrdersListResult } from '../../orders/models/admin-order.model';
|
||||||
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
||||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
function makeOrder(id: string, orderNumber: string, createdAt: string): AdminOrder {
|
function makeOrder(id: string, orderNumber: string, createdAt: string): AdminOrder {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { LocalStorageService } from '../../../../core/storage/local-storage.serv
|
|||||||
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
||||||
import { LanguageService } from '../../../../services/language.service';
|
import { LanguageService } from '../../../../services/language.service';
|
||||||
import { TranslateService } from '../../../../i18n/translate.service';
|
import { TranslateService } from '../../../../i18n/translate.service';
|
||||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
const LAST_NOTIFIED_KEY = 'adminOrderWatcher.lastNotifiedOrderId.v1';
|
const LAST_NOTIFIED_KEY = 'adminOrderWatcher.lastNotifiedOrderId.v1';
|
||||||
const LAST_NOTIFIED_AT_KEY = 'adminOrderWatcher.lastNotifiedOrderCreatedAt.v1';
|
const LAST_NOTIFIED_AT_KEY = 'adminOrderWatcher.lastNotifiedOrderCreatedAt.v1';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { delay } from 'rxjs/operators';
|
|||||||
import { AdminTransaction, AdminTransactionListFilters, AdminTransactionsListResult } from '../models/admin-transaction.model';
|
import { AdminTransaction, AdminTransactionListFilters, AdminTransactionsListResult } from '../models/admin-transaction.model';
|
||||||
import { AdminTransactionsGateway } from './admin-transactions-gateway.interface';
|
import { AdminTransactionsGateway } from './admin-transactions-gateway.interface';
|
||||||
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
||||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
const METHODS = ['card', 'qr', 'cash_on_delivery'];
|
const METHODS = ['card', 'qr', 'cash_on_delivery'];
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Observable, of } from 'rxjs';
|
|||||||
import { delay } from 'rxjs/operators';
|
import { delay } from 'rxjs/operators';
|
||||||
import { AdminInvitation, AdminUserRoleRecord, AdminSession, AdminUser, AdminUserAuditEntry, AdminUserScope, AdminUserStatus } from '../models/admin-user.model';
|
import { AdminInvitation, AdminUserRoleRecord, AdminSession, AdminUser, AdminUserAuditEntry, AdminUserScope, AdminUserStatus } from '../models/admin-user.model';
|
||||||
import { AdminUsersGateway } from './admin-users-gateway.interface';
|
import { AdminUsersGateway } from './admin-users-gateway.interface';
|
||||||
import { AdminAuthService } from '../../../../core/admin-auth/admin-auth.service';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
const BUILT_IN_ROLES: AdminUserRoleRecord[] = [
|
const BUILT_IN_ROLES: AdminUserRoleRecord[] = [
|
||||||
{ id: 'owner', name: 'Owner', permissions: ['*'], builtIn: true },
|
{ id: 'owner', name: 'Owner', permissions: ['*'], builtIn: true },
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { AuthService } from '../../../services/auth.service';
|
import { AuthService } from '@marketplaces/auth';
|
||||||
import { SearchHistory } from '../models/search.model';
|
import { SearchHistory } from '../models/search.model';
|
||||||
import { BackendSearchHistoryRepository, LocalSearchHistoryRepository, SearchHistoryRepository } from './search-history.repository';
|
import { BackendSearchHistoryRepository, LocalSearchHistoryRepository, SearchHistoryRepository } from './search-history.repository';
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { AnalyticsService } from '../../../../core/analytics/services/analytics.
|
|||||||
import { ApiService } from '../../../../services/api.service';
|
import { ApiService } from '../../../../services/api.service';
|
||||||
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
||||||
import { UserNotificationService } from '../../user-experience/services/user-notification.service';
|
import { UserNotificationService } from '../../user-experience/services/user-notification.service';
|
||||||
import { AuthService } from '../../../../services/auth.service';
|
import { AuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
const RESTOCK_SUBSCRIPTIONS_KEY = 'restockSubscriptions';
|
const RESTOCK_SUBSCRIPTIONS_KEY = 'restockSubscriptions';
|
||||||
import { ProductDeliveryInformationComponent } from '../components/delivery-information/delivery-information.component';
|
import { ProductDeliveryInformationComponent } from '../components/delivery-information/delivery-information.component';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { inject } from '@angular/core';
|
|||||||
import { ApiConfigService } from '../core/config/api-config.service';
|
import { ApiConfigService } from '../core/config/api-config.service';
|
||||||
import { LocationService } from '../services/location.service';
|
import { LocationService } from '../services/location.service';
|
||||||
import { LanguageService } from '../services/language.service';
|
import { LanguageService } from '../services/language.service';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
/** Map internal language codes to API header values */
|
/** Map internal language codes to API header values */
|
||||||
const LANG_HEADER_MAP: Record<string, string> = {
|
const LANG_HEADER_MAP: Record<string, string> = {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ResolvedWidget } from '../../widgets/contracts/widget-component.contrac
|
|||||||
import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-host.service';
|
import { WidgetHostService } from '../../dynamic-renderer/widget-host/widget-host.service';
|
||||||
import { LanguageService } from '../../services/language.service';
|
import { LanguageService } from '../../services/language.service';
|
||||||
import { Category } from '../../core/categories/models/category-domain.model';
|
import { Category } from '../../core/categories/models/category-domain.model';
|
||||||
import { AuthService } from '../../services/auth.service';
|
import { AuthService } from '@marketplaces/auth';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dynamic-page-layout',
|
selector: 'app-dynamic-page-layout',
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export type AdminAuthStatus = 'unknown' | 'checking' | 'authenticated' | 'expired' | 'unauthenticated';
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './category.model';
|
export * from './category.model';
|
||||||
export * from './item.model';
|
export * from './item.model';
|
||||||
export * from './location.model';
|
export * from './location.model';
|
||||||
export * from './auth.model';
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { DecimalPipe } from '@angular/common';
|
|||||||
import { Router, RouterLink } from '@angular/router';
|
import { Router, RouterLink } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
|
||||||
import { CartService, ApiService, LanguageService, AuthService } from '../../services';
|
import { CartService, ApiService, LanguageService } from '../../services';
|
||||||
|
import { AuthService } from '@marketplaces/auth';
|
||||||
import { Item, CartItem, DeliveryOption } from '../../models';
|
import { Item, CartItem, DeliveryOption } from '../../models';
|
||||||
import { EMPTY, interval, of, Subscription } from 'rxjs';
|
import { EMPTY, interval, of, Subscription } from 'rxjs';
|
||||||
import { catchError, exhaustMap, take, timeout } from 'rxjs/operators';
|
import { catchError, exhaustMap, take, timeout } from 'rxjs/operators';
|
||||||
|
|||||||
@@ -3,4 +3,3 @@ export * from './cart.service';
|
|||||||
export * from './language.service';
|
export * from './language.service';
|
||||||
export * from './seo.service';
|
export * from './seo.service';
|
||||||
export * from './location.service';
|
export * from './location.service';
|
||||||
export * from './auth.service';
|
|
||||||
|
|||||||
Reference in New Issue
Block a user