108 lines
5.0 KiB
JavaScript
108 lines
5.0 KiB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
import { Injectable, inject, signal } from '@angular/core';
|
|
import { HttpErrorResponse } from '@angular/common/http';
|
|
import { catchError, switchMap, tap, throwError } from 'rxjs';
|
|
import { Observable } from 'rxjs';
|
|
import { authErrorCodeFromBackendCode, authErrorCodeFromStatus } from './models/auth-error.model';
|
|
import { AuthApiService } from './auth-api.service';
|
|
import { Ed25519KeypairService } from './ed25519-keypair.service';
|
|
import { SessionService } from './session.service';
|
|
/**
|
|
* Orchestrates the Ed25519 challenge/response admin auth flow end to end:
|
|
*
|
|
* GET /api/admin/auth/challenge -> { nonce }
|
|
* sign(nonce) with local Ed25519 key -> signature
|
|
* POST /api/admin/auth/verify -> { token, refreshToken }
|
|
*
|
|
* This is the lowest-level orchestrator; components should go through
|
|
* AuthFacade rather than calling this directly. Exported from the package
|
|
* barrel as `Ed25519AuthService` to avoid colliding with the telegram
|
|
* module's `AuthService`.
|
|
*/
|
|
let AuthService = class AuthService {
|
|
constructor() {
|
|
this.api = inject(AuthApiService);
|
|
this.keypair = inject(Ed25519KeypairService);
|
|
this.session = inject(SessionService);
|
|
this.loginPhaseSignal = signal('idle');
|
|
this.lastErrorSignal = signal(null);
|
|
this.loginPhase = this.loginPhaseSignal.asReadonly();
|
|
this.lastError = this.lastErrorSignal.asReadonly();
|
|
this.session.onRefreshDue(() => this.refresh().subscribe());
|
|
}
|
|
/** Restores a persisted session on app bootstrap. Call once from an APP_INITIALIZER or root component. */
|
|
restoreSession() {
|
|
this.session.restore();
|
|
}
|
|
login() {
|
|
this.lastErrorSignal.set(null);
|
|
this.loginPhaseSignal.set('requesting-challenge');
|
|
return this.api.requestChallenge().pipe(switchMap(challenge => this.signChallenge(challenge.nonce).pipe(switchMap(({ publicKeyBase64, signature }) => {
|
|
this.loginPhaseSignal.set('verifying');
|
|
return this.api.verifySignature({ publicKey: publicKeyBase64, signature, nonce: challenge.nonce });
|
|
}))), tap(tokens => {
|
|
this.session.activate(tokens);
|
|
this.loginPhaseSignal.set('done');
|
|
}), catchError(error => this.handleAuthError(error, 'invalid-signature')));
|
|
}
|
|
refresh() {
|
|
const refreshToken = this.session.getRefreshToken();
|
|
if (!refreshToken) {
|
|
this.session.markExpired();
|
|
return throwError(() => this.toAuthError({ code: 'session-expired', message: 'No refresh token available.' }));
|
|
}
|
|
return this.api.refresh({ refreshToken }).pipe(tap(tokens => this.session.activate(tokens)), catchError(error => this.handleAuthError(error, 'session-expired', () => this.session.markExpired())));
|
|
}
|
|
logout() {
|
|
const refreshToken = this.session.getRefreshToken();
|
|
this.session.clear();
|
|
if (!refreshToken) {
|
|
return new Observable(subscriber => {
|
|
subscriber.next();
|
|
subscriber.complete();
|
|
});
|
|
}
|
|
return this.api.logout(refreshToken).pipe(catchError(() => throwError(() => null)));
|
|
}
|
|
signChallenge(nonce) {
|
|
this.loginPhaseSignal.set('signing');
|
|
return new Observable(subscriber => {
|
|
this.keypair
|
|
.getOrCreateKeyPair()
|
|
.then(({ publicKeyBase64 }) => this.keypair.sign(nonce).then(signature => {
|
|
subscriber.next({ publicKeyBase64, signature });
|
|
subscriber.complete();
|
|
}))
|
|
.catch(error => subscriber.error(error));
|
|
});
|
|
}
|
|
handleAuthError(error, fallbackCode, onError) {
|
|
onError?.();
|
|
return throwError(() => this.toAuthError(this.toAuthErrorShape(error, fallbackCode)));
|
|
}
|
|
toAuthErrorShape(error, fallbackCode) {
|
|
if (error instanceof HttpErrorResponse) {
|
|
const bodyCode = error.error?.error?.code;
|
|
const code = authErrorCodeFromBackendCode(bodyCode) ?? authErrorCodeFromStatus(error.status);
|
|
return { code, message: error.message, status: error.status };
|
|
}
|
|
if (error instanceof Error) {
|
|
return { code: fallbackCode, message: error.message };
|
|
}
|
|
return { code: fallbackCode, message: 'Unknown authentication error.' };
|
|
}
|
|
toAuthError(error) {
|
|
this.lastErrorSignal.set(error);
|
|
return error;
|
|
}
|
|
};
|
|
AuthService = __decorate([
|
|
Injectable({ providedIn: 'root' })
|
|
], AuthService);
|
|
export { AuthService };
|