import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, catchError, of } from 'rxjs'; /** * Reflet de LicenseStatus (enum cote backend). */ export type LicenseStatus = 'NONE' | 'VALID' | 'GRACE' | 'EXPIRED' | 'UNVERIFIABLE'; export interface LicenseStatusDTO { enabled: boolean; status: LicenseStatus; patreonUserId: string | null; tierId: string | null; instanceId: string | null; expiresAt: string | null; lastRefreshAttemptAt: string | null; lastRefreshSucceeded: boolean | null; betaChannelEnabled: boolean; } /** * Etat du canal courant + dernier resultat de bascule (cf. ChannelStatusDTO cote backend). */ export type ChannelName = 'stable' | 'beta'; export type SwitchStatus = 'IN_PROGRESS' | 'SUCCESS' | 'ERROR'; export interface ChannelStatusDTO { currentChannel: ChannelName; switcherAvailable: boolean; lastSwitch: { id: string; status: SwitchStatus; channel: ChannelName; message: string; completedAt: string; } | null; } /** * Reflet de UpdateCheckService.BetaStatus. */ export interface BetaStatusDTO { enabled: boolean; updateAvailable: boolean; anyUnknown: boolean; images: Array<{ image: string; localVersion: string | null; remoteVersion: string | null; status: 'UP_TO_DATE' | 'UPDATE_AVAILABLE' | 'UNKNOWN'; updateAvailable: boolean; }>; checkedAt: string; disabledReason: string | null; } /** * Service Angular pour la gestion de la licence Patreon. * Tous les endpoints sont proteges par HTTP Basic (admin). */ @Injectable({ providedIn: 'root' }) export class LicenseService { private readonly apiUrl = '/api/license'; private readonly authOptions = { withCredentials: true }; constructor(private http: HttpClient) {} getStatus(): Observable { return this.http.get(this.apiUrl, this.authOptions).pipe( catchError(() => of(null)) ); } getConnectUrl(): Observable<{ url: string } | null> { return this.http.get<{ url: string }>(`${this.apiUrl}/connect-url`, this.authOptions).pipe( catchError(() => of(null)) ); } install(jwt: string): Observable { return this.http.post(`${this.apiUrl}/install`, { jwt }, this.authOptions).pipe( catchError((err) => of({ error: err?.error?.error ?? 'Echec de l\'installation' })) ); } disconnect(): Observable { return this.http.delete(this.apiUrl, this.authOptions).pipe( // Convertit en boolean : true = succes, false = erreur // (catchError plus bas masque les detail HTTP) catchError(() => of(false as any)) ) as unknown as Observable; } refresh(): Observable { return this.http.post(`${this.apiUrl}/refresh`, null, this.authOptions).pipe( catchError(() => of(null)) ); } setBetaChannel(enabled: boolean): Observable { return this.http.put(`${this.apiUrl}/beta-channel`, { enabled }, this.authOptions).pipe( catchError(() => of(null)) ); } checkBeta(): Observable { return this.http.get('/api/admin/updates/check-beta', this.authOptions).pipe( catchError(() => of(null)) ); } /** Etat du canal courant et dernier resultat de switch (pour polling UI). */ getChannelStatus(): Observable { return this.http.get(`${this.apiUrl}/channel`, this.authOptions).pipe( catchError(() => of(null)) ); } /** * Declenche un switch de canal. 202 + { id, channel } si accepte, * sinon erreur (403 = pas de licence, 503 = sidecar indispo, etc.). */ switchChannel(channel: ChannelName): Observable<{ id: string; channel: ChannelName } | { error: string }> { return this.http.post<{ id: string; channel: ChannelName }>( `${this.apiUrl}/channel/switch`, { channel }, this.authOptions ).pipe( catchError((err) => of({ error: err?.error?.error ?? 'Echec du switch de canal' })) ); } }