Files
marketplaces/src/app/services/auth.service.ts

190 lines
5.6 KiB
TypeScript
Raw Normal View History

import { Injectable, signal, computed, inject } from '@angular/core';
import { Observable, tap } from 'rxjs';
2026-06-01 00:47:26 +04:00
import { AuthSession, AuthStatus, WebSessionStart } from '../models/auth.model';
import { TelegramSessionApiService } from './telegram-session-api.service';
2026-02-28 17:18:24 +04:00
2026-06-01 00:47:26 +04:00
const WEB_SESSION_COOKIE = 'webSessionID';
const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;
2026-02-28 17:18:24 +04:00
@Injectable({
providedIn: 'root'
})
export class AuthService {
private readonly api = inject(TelegramSessionApiService);
2026-02-28 17:18:24 +04:00
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);
2026-06-01 00:47:26 +04:00
private sessionCheckTimer?: ReturnType<typeof setTimeout>;
2026-02-28 17:18:24 +04:00
constructor() {
2026-02-28 17:18:24 +04:00
// On init, check existing session via cookie
this.checkSession();
}
2026-06-01 00:47:26 +04:00
/** Check the current webSessionID cookie against the auth backend. */
2026-02-28 17:18:24 +04:00
checkSession(): void {
2026-06-01 00:47:26 +04:00
const webSessionID = this.getStoredWebSessionID();
if (!webSessionID) {
this.clearAuthState('unauthenticated');
return;
}
2026-02-28 17:18:24 +04:00
this.statusSignal.set('checking');
2026-06-01 00:47:26 +04:00
this.checkSessionOnce(webSessionID).subscribe(session => {
if (!session?.active) {
this.clearAuthState('unauthenticated');
2026-02-28 17:18:24 +04:00
}
});
}
/** Check session without updating internal state beyond activating on success (used for polling). */
2026-06-01 00:47:26 +04:00
checkSessionOnce(webSessionID = this.getStoredWebSessionID()): Observable<AuthSession | null> {
return this.api.checkSessionOnce(webSessionID).pipe(
2026-04-14 23:14:26 +04:00
tap(session => {
2026-06-01 00:47:26 +04:00
if (session?.active) {
this.activateSession(session);
2026-04-14 23:14:26 +04:00
}
})
2026-04-14 23:14:26 +04:00
);
}
2026-02-28 17:18:24 +04:00
/**
* Called after user completes Telegram login.
*/
onTelegramLoginComplete(): void {
this.hideLogin();
2026-06-01 00:47:26 +04:00
if (!this.isAuthenticated()) {
this.checkSession();
}
2026-02-28 17:18:24 +04:00
}
/** Generate the Telegram login URL for bot-based auth */
getTelegramLoginUrl(webSessionID: string): string {
return this.api.getBotLoginUrl(webSessionID);
2026-02-28 17:18:24 +04:00
}
2026-06-20 14:40:22 +04:00
/** Generate a Telegram app deep link for mobile login without opening a browser tab. */
getTelegramAppLoginUrl(webSessionID: string): string {
return this.api.getBotAppLoginUrl(webSessionID);
2026-02-28 17:18:24 +04:00
}
2026-06-01 00:47:26 +04:00
/** Create a backend web session and return the Telegram start link for it. */
createWebSession(): Observable<WebSessionStart> {
return this.api.createSession();
2026-03-25 15:32:50 +04:00
}
2026-02-28 17:18:24 +04:00
/** 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 {
2026-06-01 00:47:26 +04:00
const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID();
if (!webSessionID) {
this.clearAuthState('unauthenticated');
return;
}
this.api.logout(webSessionID).subscribe(() => {
2026-06-01 00:47:26 +04:00
this.clearAuthState('unauthenticated');
2026-02-28 17:18:24 +04:00
});
}
2026-06-01 00:47:26 +04:00
private activateSession(session: AuthSession): void {
this.sessionSignal.set(session);
this.statusSignal.set('authenticated');
this.setStoredWebSessionID(session.sessionId);
2026-06-19 12:43:25 +04:00
this.scheduleSessionRefresh(session.expires);
2026-06-01 00:47:26 +04:00
}
private clearAuthState(status: AuthStatus): void {
this.sessionSignal.set(null);
this.statusSignal.set(status);
this.clearStoredWebSessionID();
this.clearSessionRefresh();
}
2026-02-28 17:18:24 +04:00
/** 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
2026-06-01 00:47:26 +04:00
const refreshIn = Number.isFinite(expiresMs)
? Math.max(expiresMs - nowMs - 60_000, 30_000)
: WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000;
2026-02-28 17:18:24 +04:00
this.sessionCheckTimer = setTimeout(() => {
this.checkSession();
}, refreshIn);
}
private clearSessionRefresh(): void {
if (this.sessionCheckTimer) {
clearTimeout(this.sessionCheckTimer);
this.sessionCheckTimer = undefined;
}
}
2026-06-01 00:47:26 +04:00
private getStoredWebSessionID(): string | null {
if (typeof document === 'undefined') {
return null;
}
const cookie = document.cookie
.split('; ')
.find(row => row.startsWith(`${WEB_SESSION_COOKIE}=`));
if (!cookie) {
return null;
}
try {
return decodeURIComponent(cookie.substring(WEB_SESSION_COOKIE.length + 1));
} catch {
return null;
}
}
private setStoredWebSessionID(webSessionID: string): void {
if (typeof document === 'undefined') {
return;
}
const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : '';
document.cookie = `${WEB_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${WEB_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Lax${secure}`;
}
private clearStoredWebSessionID(): void {
if (typeof document === 'undefined') {
return;
}
document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`;
}
2026-02-28 17:18:24 +04:00
}