fix: toAuthErrorShape() ignored backend error.code, session-expired screen unreachable

Known bug per BACKEND-API-REFERENCE.md §5: the client has dedicated
session-expired/invalid-signature recovery screens fully built, but
the mapper only ever derived the error code from HTTP status, never
the response body - so a real 401 with error.code: 'TOKEN_EXPIRED'
rendered the generic 'Unauthorized' screen instead.

Frontend half of the fix: prefer error.code from the body when present
(mapped via authErrorCodeFromBackendCode), fall back to status-derived
code otherwise. Stays dormant until the backend actually sends the
code, per the doc.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-13 09:00:39 +04:00
parent 6231128288
commit 461cd8421c
2 changed files with 22 additions and 2 deletions

View File

@@ -17,6 +17,24 @@ export interface AuthError {
status?: number; status?: number;
} }
/**
* 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> = {
TOKEN_EXPIRED: 'session-expired',
INVALID_SIGNATURE: 'invalid-signature',
UNAUTHENTICATED: 'unauthorized',
FORBIDDEN: 'forbidden',
SERVICE_UNAVAILABLE: 'backend-unavailable',
};
export function authErrorCodeFromBackendCode(code: unknown): AuthErrorCode | undefined {
return typeof code === 'string' ? BACKEND_ERROR_CODE_MAP[code] : undefined;
}
/** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */ /** Maps a backend HTTP status to the AuthErrorCode screen it should route to. */
export function authErrorCodeFromStatus(status: number): AuthErrorCode { export function authErrorCodeFromStatus(status: number): AuthErrorCode {
switch (status) { switch (status) {

View File

@@ -3,7 +3,7 @@ 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, 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';
@@ -109,7 +109,9 @@ export class AuthService {
private toAuthErrorShape(error: unknown, fallbackCode: AuthError['code']): AuthError { private toAuthErrorShape(error: unknown, fallbackCode: AuthError['code']): AuthError {
if (error instanceof HttpErrorResponse) { if (error instanceof HttpErrorResponse) {
return { code: authErrorCodeFromStatus(error.status), message: error.message, status: error.status }; const bodyCode = (error.error as { error?: { code?: unknown } } | null)?.error?.code;
const code = authErrorCodeFromBackendCode(bodyCode) ?? authErrorCodeFromStatus(error.status);
return { code, message: error.message, status: error.status };
} }
if (error instanceof Error) { if (error instanceof Error) {
return { code: fallbackCode, message: error.message }; return { code: fallbackCode, message: error.message };