import { Injectable, computed, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, map, tap } from 'rxjs'; export interface AuthUser { id?: number | string; displayName?: string; email?: string; } interface AuthState { user: AuthUser | null; accessToken?: string; refreshToken?: string; } const AUTH_KEY = 'auth'; @Injectable({ providedIn: 'root' }) export class AuthService { private _state = signal({ user: null }); readonly user = computed(() => this._state().user); readonly isAuthenticated = computed(() => !!this._state().user && !!this._state().accessToken); private readonly baseUrl = location.hostname.endsWith('hf.space') ? 'https://pykara-pytrade-backend.hf.space' : 'http://127.0.0.1:5000'; private readonly refreshUrl = `${this.baseUrl}/refresh`; constructor(private http: HttpClient) { this.restoreFromStorage(); } setSession( res: { userId?: number | string; accessToken?: string; refreshToken?: string; displayName?: string }, opts: { email?: string; rememberMe?: boolean } = {} ) { const displayName = res.displayName ?? this.tryGetNameFromJwt(res.accessToken) ?? opts.email ?? // fallback undefined; const user: AuthUser = { id: res.userId, displayName, email: opts.email }; const newState: AuthState = { user, accessToken: res.accessToken, refreshToken: res.refreshToken, }; this._state.set(newState); const storage = opts.rememberMe ? localStorage : sessionStorage; const other = opts.rememberMe ? sessionStorage : localStorage; storage.setItem(AUTH_KEY, JSON.stringify(newState)); other.removeItem(AUTH_KEY); } logout() { this._state.set({ user: null }); localStorage.removeItem(AUTH_KEY); sessionStorage.removeItem(AUTH_KEY); } getAccessToken(): string | undefined { return this._state().accessToken; } getRefreshToken(): string | undefined { return this._state().refreshToken; } tokenExpired(): boolean { const token = this._state().accessToken; if (!token) return true; try { const payloadPart = token.split('.')[1]; const payload = JSON.parse(atob(payloadPart.replace(/-/g, '+').replace(/_/g, '/'))); const expSec = payload.exp as number | undefined; if (!expSec) return false; const nowSec = Math.floor(Date.now() / 1000); return expSec <= nowSec; } catch { return true; } } refreshAccessToken(): Observable { const refreshToken = this.getRefreshToken(); if (!refreshToken) throw new Error('No refresh token'); return this.http .post<{ accessToken: string; refreshToken?: string }>(this.refreshUrl, { refreshToken }) .pipe( tap(res => { const state = this._state(); // keep same rememberMe behavior based on where it was stored const rememberMe = !!localStorage.getItem(AUTH_KEY); this.setSession( { userId: state.user?.id, displayName: state.user?.displayName, accessToken: res.accessToken, refreshToken: res.refreshToken ?? state.refreshToken }, { email: state.user?.email, rememberMe } ); }), map(res => res.accessToken) ); } private restoreFromStorage() { const raw = localStorage.getItem(AUTH_KEY) || sessionStorage.getItem(AUTH_KEY); if (!raw) return; try { const state = JSON.parse(raw) as AuthState; this._state.set(state); } catch { localStorage.removeItem(AUTH_KEY); sessionStorage.removeItem(AUTH_KEY); } } private tryGetNameFromJwt(token?: string): string | undefined { if (!token) return undefined; try { const payloadPart = token.split('.')[1]; const payload = JSON.parse(atob(payloadPart.replace(/-/g, '+').replace(/_/g, '/'))); return payload.name || payload.preferred_username || [payload.given_name, payload.family_name].filter(Boolean).join(' ') || undefined; } catch { return undefined; } } }