Files
vitanovaPackages/dist/ed25519/auth.service.js

108 lines
5.0 KiB
JavaScript
Raw Normal View History

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 };