import { Injectable, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, catchError, map, tap } from 'rxjs'; import { AuthSession, AuthStatus, QrPollResponse } from '../models/auth.model'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root' }) export class AuthService { private sessionSignal = signal(null); private statusSignal = signal('unknown'); private showLoginSignal = signal(false); /** Current auth session */ readonly session = this.sessionSignal.asReadonly(); /** Current auth status */ readonly status = this.statusSignal.asReadonly(); /** Whether user is fully authenticated */ readonly isAuthenticated = computed(() => this.statusSignal() === 'authenticated'); /** Whether to show login dialog */ readonly showLoginDialog = this.showLoginSignal.asReadonly(); /** Display name of authenticated user */ readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null); private readonly apiUrl = environment.apiUrl; private sessionCheckTimer?: ReturnType; constructor(private http: HttpClient) { // On init, check existing session via cookie this.checkSession(); } /** * Check current session status with backend. * The backend reads the session cookie and returns the session info. */ checkSession(): void { this.statusSignal.set('checking'); this.http.get(`${this.apiUrl}/auth/session`, { withCredentials: true }).pipe( catchError(() => { this.statusSignal.set('unauthenticated'); this.sessionSignal.set(null); return of(null); }) ).subscribe(session => { if (session && session.active) { this.sessionSignal.set(session); this.statusSignal.set('authenticated'); this.scheduleSessionRefresh(session.expiresAt); } else if (session && !session.active) { this.sessionSignal.set(null); this.statusSignal.set('expired'); } else { this.statusSignal.set('unauthenticated'); } }); } /** * Called after user completes Telegram login. * The callback URL from Telegram will hit our backend which sets the cookie. * Then we re-check the session. */ onTelegramLoginComplete(): void { this.checkSession(); this.hideLogin(); } /** Generate the Telegram login URL for bot-based auth */ getTelegramLoginUrl(): string { const botUsername = (environment as Record)['telegramBot'] as string || 'DexarSupport_bot'; const callbackUrl = encodeURIComponent(`${this.apiUrl}/auth/telegram/callback`); return `https://t.me/${botUsername}?start=auth_${callbackUrl}`; } /** Get QR code data URL for Telegram login */ getTelegramQrUrl(): string { return this.getTelegramLoginUrl(); } /** Create a one-time QR login token via backend */ createQrToken(): Observable<{ token: string; url: string }> { return this.http.post<{ token: string; url: string }>( `${this.apiUrl}/auth/qr/create`, {}, { withCredentials: true } ); } /** Poll the QR token status (pending → confirmed / expired) */ pollQrToken(token: string): Observable { return this.http.get( `${this.apiUrl}/auth/qr/poll`, { params: { token }, withCredentials: true, } ); } /** Sync local cart to the backend session after login */ syncCart(sessionId: string, items: Array<{ itemID: number; quantity: number; colour?: string; size?: string; price?: number }>): Observable { if (!items.length) return of(null); return this.http.post(`${this.apiUrl}/websession/${sessionId}`, items, { withCredentials: true, }).pipe(catchError(() => of(null))); } /** Show login dialog (called when user tries to pay without being logged in) */ requestLogin(): void { this.showLoginSignal.set(true); } /** Hide login dialog */ hideLogin(): void { this.showLoginSignal.set(false); } /** Logout — clears session on backend and locally */ logout(): void { this.http.post(`${this.apiUrl}/auth/logout`, {}, { withCredentials: true }).pipe( catchError(() => of(null)) ).subscribe(() => { this.sessionSignal.set(null); this.statusSignal.set('unauthenticated'); this.clearSessionRefresh(); }); } /** Schedule a session re-check before it expires */ private scheduleSessionRefresh(expiresAt: string): void { this.clearSessionRefresh(); const expiresMs = new Date(expiresAt).getTime(); const nowMs = Date.now(); // Re-check 60 seconds before expiry, minimum 30s from now const refreshIn = Math.max(expiresMs - nowMs - 60_000, 30_000); this.sessionCheckTimer = setTimeout(() => { this.checkSession(); }, refreshIn); } private clearSessionRefresh(): void { if (this.sessionCheckTimer) { clearTimeout(this.sessionCheckTimer); this.sessionCheckTimer = undefined; } } }