fix(admin-auth): reuse exact same QR/session API and component for admin login
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- Removed invented adminAuthApiUrl endpoint and separate AdminLoginComponent.
Admin login now uses the exact same Telegram session backend
(TelegramSessionApiService, {authApiUrl}/users/sessions) and the exact
same TelegramLoginComponent (mode="customer" | "admin" input) as customer
login - only the storage (cookie/localStorage/signals) stays separate.
- Extracted the shared HTTP+normalization logic from AuthService into
TelegramSessionApiService so both AuthService and AdminAuthService call it
instead of duplicating request/parsing code.
- Documented the resulting backend gap in docs/Project-Editor.md: since the
session API has no concept of "admin", server-side role enforcement is
required when admin API calls are made - the frontend only decides where
to store the session, not whether the user is actually an admin.
This commit is contained in:
@@ -194,41 +194,69 @@ Static page HTML is edited via `MarketplaceHtmlEditorComponent`
|
||||
external dependency. It emits raw HTML on every change and never sanitizes —
|
||||
sanitization remains a storefront-render concern.
|
||||
|
||||
## QR Login Reuse (Sprint 18)
|
||||
## QR Login Reuse (Sprint 18, corrected)
|
||||
|
||||
The Telegram QR-login flow (QR image, polling, expiry, "return from app"
|
||||
recovery via visibilitychange/focus/pageshow) was extracted from
|
||||
`TelegramLoginComponent` into `shared/qr-login/qr-login.engine.ts`
|
||||
(`QrLoginEngine<TSession>`) plus an adapter interface
|
||||
(`shared/qr-login/qr-login.model.ts`, `QrLoginAdapter<TSession>`). The engine
|
||||
is not a DI singleton - each login surface instantiates its own
|
||||
`new QrLoginEngine(adapter)` and drives it from an `effect()` watching its own
|
||||
dialog-visibility signal. `TelegramLoginComponent` was refactored onto this
|
||||
engine with no behavior change. `AdminLoginComponent`
|
||||
(`core/admin-auth/admin-login.component.ts`) reuses the same engine against a
|
||||
separate adapter backed by `AdminAuthService`, so QR/polling/timeout logic is
|
||||
not duplicated between customer and admin login.
|
||||
There is exactly **one** Telegram QR/session backend
|
||||
(`{authApiUrl}/users/sessions`) and exactly **one** QR login component/UI.
|
||||
Nothing about the QR flow is duplicated for admin:
|
||||
|
||||
## Admin Authentication (Sprint 18)
|
||||
- `TelegramSessionApiService` (`services/telegram-session-api.service.ts`) is
|
||||
the single place that calls `POST/GET/DELETE {authApiUrl}/users/sessions...`
|
||||
and normalizes the response into `AuthSession`. It holds no state and
|
||||
writes no cookies - it's a pure API wrapper.
|
||||
- `QrLoginEngine<TSession>` (`shared/qr-login/qr-login.engine.ts`) is the
|
||||
QR/polling/expiry/"return from Telegram app" state machine (extracted from
|
||||
the original `TelegramLoginComponent`), driven by a small
|
||||
`QrLoginAdapter<TSession>` (`shared/qr-login/qr-login.model.ts`).
|
||||
- `TelegramLoginComponent` (`components/telegram-login/`) is **the same
|
||||
component for both customer and admin login** - not two components. It
|
||||
takes a `mode: 'customer' | 'admin'` input; `ngOnInit` picks
|
||||
`AuthService` or `AdminAuthService` accordingly and builds the
|
||||
`QrLoginAdapter` from whichever one, but the QR image, polling loop,
|
||||
timeouts, and dialog markup are identical either way. Customer usage is
|
||||
unchanged (`<app-telegram-login />` on the cart page, `mode` defaults to
|
||||
`'customer'`); admin usage is `<app-telegram-login mode="admin" />`,
|
||||
mounted once globally in `app.html`.
|
||||
|
||||
Admin authentication is completely separate from the customer/storefront
|
||||
session (`AuthService`), by design - one must never authenticate the other:
|
||||
An earlier version of this sprint's work built a separate
|
||||
`AdminAuthService`/`AdminLoginComponent` pair that called its own
|
||||
`adminAuthApiUrl` placeholder endpoint. That was wrong: there is no separate
|
||||
admin backend, and inventing one client-side would have meant testing against
|
||||
an endpoint that doesn't exist. It was replaced with the shared-API approach
|
||||
described above.
|
||||
|
||||
## Admin Authentication (Sprint 18, corrected)
|
||||
|
||||
Only the **storage** is separate between customer and admin - the QR/session
|
||||
API and UI component are shared (see above), by design, since one Telegram
|
||||
QR/session backend serves both. What stays separate is everything needed so
|
||||
that scanning the admin QR can never authenticate the customer session (or
|
||||
vice versa):
|
||||
|
||||
| | Customer (`AuthService`) | Admin (`AdminAuthService`, `core/admin-auth/`) |
|
||||
|---|---|---|
|
||||
| Cookie | `webSessionID` | `adminSessionID` (`SameSite=Strict`) |
|
||||
| Anonymous/local id | `web_session_id` (localStorage, API attribution only) | `adminToken` / `adminRefreshToken` (localStorage, reserved for future JWT pair) |
|
||||
| Signals | `session`, `status`, `showLoginDialog` on `AuthService` | `session`, `status`, `showLoginDialog`, `role` on `AdminAuthService` |
|
||||
| Cookie | `webSessionID` (`SameSite=Lax`) | `adminSessionID` (`SameSite=Strict`) |
|
||||
| Token storage | `web_session_id` (localStorage, anonymous API attribution only, unrelated to auth) | `adminToken` / `adminRefreshToken` (localStorage, reserved for a future JWT pair - unused today) |
|
||||
| Signals | `session`, `status`, `showLoginDialog` on `AuthService` | `session`, `status`, `showLoginDialog` on `AdminAuthService` |
|
||||
| Guard | none yet for customer routes | `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) |
|
||||
| Interceptor | `apiHeadersInterceptor` | `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`), self-guards on `/admin/` in the URL, sets `AdminWebSessionID` + `Authorization: Bearer <adminToken>` when present |
|
||||
| Login UI | `TelegramLoginComponent` (mounted per-page, e.g. cart) | `AdminLoginComponent` (mounted once, globally, in `app.html`) |
|
||||
| Interceptor | `apiHeadersInterceptor` | `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`), self-guards on `/admin/` in the request URL, sets `AdminWebSessionID` + `Authorization: Bearer <adminToken>` when present |
|
||||
| Session/QR API | `TelegramSessionApiService` | same `TelegramSessionApiService` instance/endpoint |
|
||||
| Login UI | `TelegramLoginComponent` (`mode="customer"`, default) | same `TelegramLoginComponent` (`mode="admin"`) |
|
||||
|
||||
**Backend gap:** `environment.adminAuthApiUrl` (`https://users.vitanova.network:456/admin`)
|
||||
is a placeholder path under the existing auth host - there is no real admin
|
||||
session/login backend yet. `AdminAuthService.createWebSession()` /
|
||||
`checkSessionOnce()` / `logout()` call `POST|GET|DELETE {adminAuthApiUrl}/sessions...`
|
||||
following the same shape as the customer session API; confirm/repoint this
|
||||
once the backend ships dedicated admin endpoints.
|
||||
**Backend gap this creates, and why it matters:** because admin login goes
|
||||
through the exact same Telegram session API as customer login, the backend
|
||||
has **no concept of "this is an admin session"** at the point the QR is
|
||||
scanned - it's just a regular Telegram user session, identical in shape to a
|
||||
customer's. The frontend only decides *where to store* the resulting session
|
||||
id (admin cookie vs. customer cookie); it cannot and does not decide whether
|
||||
that Telegram user is actually allowed to act as an admin. **Real admin
|
||||
authorization must be enforced server-side**, at the point admin API calls
|
||||
are made with the `AdminWebSessionID` header - the backend must check the
|
||||
authenticated user against an admin/role list and reject non-admins, since
|
||||
nothing on the frontend prevents any Telegram user from completing the QR
|
||||
flow while `mode="admin"` is showing. This needs a backend decision (role
|
||||
check keyed off the session id, or a dedicated admin-scoped token issuance)
|
||||
before admin login can be considered secure, not just "separate storage."
|
||||
|
||||
### Login test mode
|
||||
|
||||
@@ -237,10 +265,10 @@ once the backend ships dedicated admin endpoints.
|
||||
`AuthService.requestLogin()` / `AdminAuthService.requestLogin()` respectively,
|
||||
for manual testing. This only sets the same signal a normal "please log in"
|
||||
action would set - it does not bypass authentication or change any other
|
||||
behavior, so it is safe in all environments. Note `TelegramLoginComponent` is
|
||||
currently mounted only on the cart page, so `?login=true` only shows a dialog
|
||||
there; `AdminLoginComponent` is mounted globally so `?adminLogin=true` works
|
||||
from any route.
|
||||
behavior, so it is safe in all environments. `TelegramLoginComponent` in
|
||||
customer mode is currently mounted only on the cart page, so `?login=true`
|
||||
only shows a dialog there; the admin-mode instance is mounted globally so
|
||||
`?adminLogin=true` works from any route.
|
||||
|
||||
### Ed25519 prep
|
||||
|
||||
|
||||
Reference in New Issue
Block a user