import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, of, catchError, map } from 'rxjs'; import { AuthSession, WebSessionStart } from '../models/auth.model'; import { environment } from '../../environments/environment'; import { generateGuid } from '../shared/util/guid.util'; const SESSION_MAX_AGE_SECONDS = 60 * 60; /** * The one Telegram QR/session API (`{authApiUrl}/users/sessions`). Customer * login (AuthService) and admin login (AdminAuthService) both call this same * service against this same endpoint - there is no separate admin backend. * This class only does the HTTP call + response normalization; it holds no * session state and writes no cookies, so each caller manages its own * storage/signals independently on top of it. */ @Injectable({ providedIn: 'root' }) export class TelegramSessionApiService { private readonly authApiUrl = environment.authApiUrl; constructor(private readonly http: HttpClient) {} createSession(): Observable { const webSessionID = generateGuid(); return this.http.post>( `${this.authApiUrl}/users/sessions`, { webSessionID }, { headers: { WebSessionID: webSessionID } } ).pipe( map(response => { const responseWebSessionID = this.extractSessionId(response, webSessionID); return { webSessionID: responseWebSessionID, url: this.getBotLoginUrl(responseWebSessionID), }; }) ); } checkSessionOnce(webSessionID: string | null): Observable { if (!webSessionID) { return of(null); } return this.http.get>( `${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}` ).pipe( map(response => this.normalizeWebSession(response, webSessionID)), catchError(() => of(null)) ); } logout(webSessionID: string): Observable { return this.http.delete(`${this.authApiUrl}/users/sessions/${encodeURIComponent(webSessionID)}`, { headers: { WebSessionID: webSessionID } }).pipe(catchError(() => of(null))); } getBotLoginUrl(webSessionID: string): string { return `https://t.me/${this.getBotUsername()}?start=${encodeURIComponent(webSessionID)}`; } getBotAppLoginUrl(webSessionID: string): string { return `tg://resolve?domain=${encodeURIComponent(this.getBotUsername())}&start=${encodeURIComponent(webSessionID)}`; } private getBotUsername(): string { return (environment as Record)['telegramBot'] as string || 'DexarSupport_bot'; } private normalizeWebSession(response: Record | null, fallbackSessionId: string): AuthSession | null { if (!response) { return null; } const user = this.asRecord(this.readFirst(response, ['user', 'User', 'telegramUser', 'TelegramUser'])) ?? response; const status = this.readFirst(response, [ 'status', 'Status', 'active', 'Active', 'loggedIn', 'LoggedIn', 'isLoggedIn', 'IsLoggedIn', 'authenticated', 'Authenticated' ]); const active = this.isActiveStatus(status); const sessionId = this.extractSessionId(response, fallbackSessionId); const username = this.readString(this.readFirst(user, ['username', 'Username'])) ?? this.readString(this.readFirst(response, ['username', 'Username'])); const firstName = this.readString(this.readFirst(user, ['firstName', 'first_name', 'FirstName', 'First_name'])); const lastName = this.readString(this.readFirst(user, ['lastName', 'last_name', 'LastName', 'Last_name'])); const fullName = [firstName, lastName].filter(Boolean).join(' '); const explicitDisplayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name'])) ?? this.readString(this.readFirst(user, ['displayName', 'DisplayName', 'name', 'Name'])); const displayName = explicitDisplayName ?? username ?? (fullName || 'Telegram User'); const telegramUserId = this.readNumber(this.readFirst(user, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'id', 'ID'])) ?? this.readNumber(this.readFirst(response, ['userId', 'telegramUserId', 'telegramUserID', 'TelegramUserID', 'userID', 'UserID', 'UserId'])) ?? null; const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires'])) ?? new Date(Date.now() + SESSION_MAX_AGE_SECONDS * 1000).toISOString(); return { sessionId, userId: telegramUserId, username, displayName, active, expires: expiresAt }; } private extractSessionId(response: Record | null, fallbackSessionId: string): string { if (!response) { return fallbackSessionId; } return this.readString(this.readFirst(response, [ 'webSessionID', 'WebSessionID', 'webSessionId', 'sessionID', 'SessionID', 'sessionId', 'id', 'ID' ])) ?? fallbackSessionId; } private readFirst(source: Record, keys: string[]): unknown { for (const key of keys) { if (Object.prototype.hasOwnProperty.call(source, key)) { return source[key]; } } return undefined; } private readString(value: unknown): string | null { if (typeof value === 'string' && value.trim()) { return value; } if (typeof value === 'number' || typeof value === 'bigint') { return value.toString(); } return null; } private readNumber(value: unknown): number | null { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string') { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : null; } return null; } private asRecord(value: unknown): Record | null { return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record : null; } private isActiveStatus(status: unknown): boolean { if (status === true || status === 1) { return true; } if (typeof status !== 'string') { return false; } return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase()); } }