Spaces:
Running
Running
| 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'; | |
| ({ providedIn: 'root' }) | |
| export class AuthService { | |
| private _state = signal<AuthState>({ 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<string> { | |
| 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; | |
| } | |
| } | |
| } | |