129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
|
|
import { Injectable, signal, computed } from '@angular/core';
|
||
|
|
import { HttpClient } from '@angular/common/http';
|
||
|
|
import { Observable, of, catchError, map, tap } from 'rxjs';
|
||
|
|
import { AuthSession, AuthStatus } from '../models/auth.model';
|
||
|
|
import { environment } from '../../environments/environment';
|
||
|
|
|
||
|
|
@Injectable({
|
||
|
|
providedIn: 'root'
|
||
|
|
})
|
||
|
|
export class AuthService {
|
||
|
|
private sessionSignal = signal<AuthSession | null>(null);
|
||
|
|
private statusSignal = signal<AuthStatus>('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<typeof setInterval>;
|
||
|
|
|
||
|
|
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<AuthSession>(`${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<string, unknown>)['telegramBot'] as string || 'dexarmarket_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();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** 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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|