fix(auth): break circular DI that logged every returning session out
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled

apiHeadersInterceptor injected @marketplaces/auth's AuthService to attach
a WebSessionID header. AuthService's own constructor makes a synchronous
GET /users/sessions/:id call to verify a persisted session, which runs
through this exact interceptor - Angular throws NG0200 (circular
dependency) mid-construction, silently swallowed by the package's
catchError(() => of(null)), read as "session invalid," and the cookie
gets cleared on every single page load. This is what was gating the
architecture-governance e2e job on Gitea (3 checkout tests failing on a
disabled QR button). Session-check requests are the identity mechanism
itself and never needed that header - skip AuthService injection for
them instead.

Also fixes mock-data.interceptor's session-check mock, which required
3 polls before reporting an id active with no way to represent a
returning session with an already-valid cookie - not the actual trigger
for this bug (useMockData is false in the dev config CI uses), but a
real gap in the mock's fidelity worth closing while in this file.
This commit is contained in:
sdarbinyan
2026-08-21 22:44:04 +04:00
parent 885f4d1299
commit dda0a3d2df
2 changed files with 28 additions and 4 deletions

View File

@@ -38,6 +38,21 @@ function getAnonymousSessionId(): string {
return id; return id;
} }
/**
* @marketplaces/auth's AuthService checks for a persisted session in its own
* constructor (a synchronous HTTP call to GET /users/sessions/:id before the
* constructor returns). If this interceptor injects AuthService for that
* exact call, Angular sees AuthService requesting itself mid-construction
* and throws NG0200 (circular dependency) - silently, since it's swallowed
* by TelegramSessionApiService's catchError(() => of(null)), which reads as
* "session invalid" and logs the user straight back out on every load.
* These endpoints are the identity mechanism itself (the session id is
* already the URL/body), so they never needed a WebSessionID header from an
* existing session in the first place - skipping AuthService injection here
* is correct, not a workaround.
*/
const AUTH_SESSION_PATH = '/users/sessions';
export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => { export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => {
const apiConfig = inject(ApiConfigService); const apiConfig = inject(ApiConfigService);
if (!apiConfig.isApiRequest(req.url)) { if (!apiConfig.isApiRequest(req.url)) {
@@ -46,12 +61,10 @@ export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => {
const locationService = inject(LocationService); const locationService = inject(LocationService);
const languageService = inject(LanguageService); const languageService = inject(LanguageService);
const authService = inject(AuthService);
const regionId = locationService.regionId(); const regionId = locationService.regionId();
const lang = languageService.currentLanguage(); const lang = languageService.currentLanguage();
const currency = languageService.currentCurrency(); const currency = languageService.currentCurrency();
const session = authService.session();
let headers = req.headers; let headers = req.headers;
@@ -62,7 +75,12 @@ export const apiHeadersInterceptor: HttpInterceptorFn = (req, next) => {
headers = headers.set('X-Language', LANG_HEADER_MAP[lang] ?? lang.toUpperCase()); headers = headers.set('X-Language', LANG_HEADER_MAP[lang] ?? lang.toUpperCase());
} }
headers = headers.set('Currency', currency || 'RUB'); headers = headers.set('Currency', currency || 'RUB');
if (!req.url.includes(AUTH_SESSION_PATH)) {
const authService = inject(AuthService);
const session = authService.session();
headers = headers.set('WebSessionID', session?.sessionId || getAnonymousSessionId()); headers = headers.set('WebSessionID', session?.sessionId || getAnonymousSessionId());
}
return next(req.clone({ headers })); return next(req.clone({ headers }));
}; };

View File

@@ -741,10 +741,16 @@ export const mockDataInterceptor: HttpInterceptorFn = (req, next) => {
const userSessionMatch = url.match(/\/users\/sessions\/([^/?]+)$/); const userSessionMatch = url.match(/\/users\/sessions\/([^/?]+)$/);
if (userSessionMatch && req.method === 'GET') { if (userSessionMatch && req.method === 'GET') {
const webSessionID = decodeURIComponent(userSessionMatch[1]); const webSessionID = decodeURIComponent(userSessionMatch[1]);
// An id never seen via POST /users/sessions didn't start a fresh QR
// login in this session - it's a cookie carried over from an earlier
// visit (AuthService.checkSession's one-shot check on page load), which
// a real backend would already recognize. Only ids the mock itself put
// through the polling flow need the checks>=3 gate below.
const isReturningSession = !mockWebSessionChecks.has(webSessionID);
const checks = (mockWebSessionChecks.get(webSessionID) ?? 0) + 1; const checks = (mockWebSessionChecks.get(webSessionID) ?? 0) + 1;
mockWebSessionChecks.set(webSessionID, checks); mockWebSessionChecks.set(webSessionID, checks);
if (checks >= 3) { if (isReturningSession || checks >= 3) {
return respond({ return respond({
webSessionID, webSessionID,
status: true, status: true,