From 7c7cdf8da2da98d6b5a6eb714dc959f5e65d9351 Mon Sep 17 00:00:00 2001 From: sdarbinyan Date: Mon, 1 Jun 2026 00:21:58 +0400 Subject: [PATCH] checking api --- src/app/app.config.ts | 11 +- src/app/auth-dialog/auth-dialog.ts | 38 +--- src/app/auth-session.service.ts | 202 ++++++++++++++++++ .../pages/fastcheck-page/fastcheck-page.ts | 55 +++-- 4 files changed, 257 insertions(+), 49 deletions(-) create mode 100644 src/app/auth-session.service.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 94dd9d2..001c2b0 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,13 +1,20 @@ -import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { APP_INITIALIZER, ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { provideRouter } from '@angular/router'; import { provideHttpClient } from '@angular/common/http'; +import { AuthSessionService } from './auth-session.service'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { providers: [ provideBrowserGlobalErrorListeners(), provideRouter(routes), - provideHttpClient() + provideHttpClient(), + { + provide: APP_INITIALIZER, + multi: true, + deps: [AuthSessionService], + useFactory: (authSession: AuthSessionService) => () => authSession.initialize() + } ] }; diff --git a/src/app/auth-dialog/auth-dialog.ts b/src/app/auth-dialog/auth-dialog.ts index ad40147..f7e4c71 100644 --- a/src/app/auth-dialog/auth-dialog.ts +++ b/src/app/auth-dialog/auth-dialog.ts @@ -1,22 +1,9 @@ import { HttpClient } from '@angular/common/http'; import { Component, computed, effect, inject, input, output, signal } from '@angular/core'; import { USERS_VITANOVA_API } from '../api'; +import { AuthSessionService, WebSessionResponse } from '../auth-session.service'; import { TranslatePipe } from '../translate/translate.pipe'; -interface WebSessionResponse { - sessionId?: string; - webSessionID?: string; - webSessionId?: string; - userId?: string; - userID?: string; - telegramID?: string; - expires?: string; - userSessionId?: string; - userSessionID?: string; - Status?: boolean; - status?: boolean | string; -} - export type AuthDialogMode = 'payment' | 'login' | 'new'; export interface AuthDialogAuthorizedEvent { @@ -35,8 +22,8 @@ type AuthDialogState = 'loading' | 'ready' | 'checking' | 'expired' | 'error'; }) export class AuthDialogComponent { private readonly http = inject(HttpClient); + private readonly authSession = inject(AuthSessionService); private readonly telegramBot = 'myAMLKYCBOT'; - private readonly sessionStorageKey = 'fc_session'; open = input(false); mode = input('login'); @@ -114,7 +101,7 @@ export class AuthDialogComponent { this.webSessionId.set(''); this.state.set('checking'); - const existingSession = localStorage.getItem(this.sessionStorageKey) ?? ''; + const existingSession = this.authSession.getSessionId(); if (existingSession) { this.checkExistingSession(existingSession); return; @@ -124,21 +111,17 @@ export class AuthDialogComponent { } private checkExistingSession(sessionId: string): void { - this.http.get(`${USERS_VITANOVA_API}/users/sessions/${sessionId}`).subscribe({ + this.authSession.validateSession(sessionId).subscribe({ next: (response) => { - if (this.isAuthorized(response)) { + if (response) { this.webSessionId.set(sessionId); this.handleAuthorized(response, sessionId); return; } - localStorage.removeItem(this.sessionStorageKey); this.createSession(); }, - error: () => { - localStorage.removeItem(this.sessionStorageKey); - this.createSession(); - } + error: () => this.createSession() }); } @@ -195,6 +178,7 @@ export class AuthDialogComponent { this.authenticated = true; this.stopPolling(); this.webSessionId.set(sessionId); + this.authSession.persistAuthorizedSession(sessionId, response.expires); this.state.set('checking'); this.authorized.emit({ sessionId, @@ -217,14 +201,10 @@ export class AuthDialogComponent { const sessionId = this.webSessionId(); if (!sessionId) { - if (!persistSession) { - localStorage.removeItem(this.sessionStorageKey); - } return; } if (persistSession) { - localStorage.setItem(this.sessionStorageKey, sessionId); return; } @@ -232,7 +212,9 @@ export class AuthDialogComponent { .delete(`${USERS_VITANOVA_API}/users/sessions/${sessionId}`) .subscribe({ error: () => undefined }); - localStorage.removeItem(this.sessionStorageKey); + if (this.authSession.getSessionId() === sessionId) { + this.authSession.clearSession(); + } } private getSessionId(response: WebSessionResponse | null | undefined): string { diff --git a/src/app/auth-session.service.ts b/src/app/auth-session.service.ts new file mode 100644 index 0000000..830b10a --- /dev/null +++ b/src/app/auth-session.service.ts @@ -0,0 +1,202 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable, inject, signal } from '@angular/core'; +import { catchError, finalize, firstValueFrom, map, Observable, of, tap } from 'rxjs'; +import { USERS_VITANOVA_API } from './api'; + +export interface WebSessionResponse { + sessionId?: string; + webSessionID?: string; + webSessionId?: string; + userId?: string; + userID?: string; + telegramID?: string; + expires?: string; + userSessionId?: string; + userSessionID?: string; + Status?: boolean; + status?: boolean | string; +} + +@Injectable({ providedIn: 'root' }) +export class AuthSessionService { + private readonly http = inject(HttpClient); + private readonly cookieName = 'fc_session'; + private readonly legacyStorageKey = 'fc_session'; + private readonly cookieLifetimeMs = 60 * 60 * 1000; + private initialized = false; + + readonly sessionId = signal(''); + readonly authenticated = signal(false); + readonly validating = signal(false); + + initialize(): Promise { + if (this.initialized) { + return Promise.resolve(); + } + + this.initialized = true; + + const storedSessionId = this.readStoredSessionId(); + if (!storedSessionId) { + this.clearSession(); + return Promise.resolve(); + } + + this.sessionId.set(storedSessionId); + this.authenticated.set(false); + + return firstValueFrom(this.validateSession(storedSessionId).pipe(map(() => undefined))); + } + + getSessionId(): string { + const current = this.sessionId().trim(); + if (current) { + return current; + } + + const storedSessionId = this.readStoredSessionId(); + if (storedSessionId) { + this.sessionId.set(storedSessionId); + } + + return storedSessionId; + } + + persistAuthorizedSession(sessionId: string, expires?: string): void { + const normalizedSessionId = sessionId.trim(); + if (!normalizedSessionId) { + this.clearSession(); + return; + } + + this.writeCookie(normalizedSessionId, this.resolveExpiry(expires)); + this.removeLegacySession(); + this.sessionId.set(normalizedSessionId); + this.authenticated.set(true); + } + + validateStoredSession(): Observable { + return this.validateSession(this.getSessionId()); + } + + validateSession(sessionId: string): Observable { + const normalizedSessionId = sessionId.trim(); + if (!normalizedSessionId) { + this.clearSession(); + return of(null); + } + + this.validating.set(true); + + return this.http + .get(`${USERS_VITANOVA_API}/users/sessions/${normalizedSessionId}`) + .pipe( + tap((response) => { + if (this.isAuthorized(response)) { + this.persistAuthorizedSession(normalizedSessionId, response.expires); + return; + } + + this.clearSession(); + }), + map((response) => (this.isAuthorized(response) ? response : null)), + catchError(() => { + this.clearSession(); + return of(null); + }), + finalize(() => this.validating.set(false)) + ); + } + + clearSession(): void { + this.deleteCookie(); + this.removeLegacySession(); + this.sessionId.set(''); + this.authenticated.set(false); + } + + private readStoredSessionId(): string { + const cookieSessionId = this.readCookie(); + if (cookieSessionId) { + return cookieSessionId; + } + + if (typeof localStorage === 'undefined') { + return ''; + } + + return (localStorage.getItem(this.legacyStorageKey) ?? '').trim(); + } + + private removeLegacySession(): void { + if (typeof localStorage === 'undefined') { + return; + } + + localStorage.removeItem(this.legacyStorageKey); + } + + private resolveExpiry(expires?: string): Date { + const fallback = new Date(Date.now() + this.cookieLifetimeMs); + if (!expires) { + return fallback; + } + + const parsed = new Date(expires); + if (Number.isNaN(parsed.getTime())) { + return fallback; + } + + return new Date(Math.min(parsed.getTime(), fallback.getTime())); + } + + private isAuthorized(response: WebSessionResponse | null | undefined): boolean { + const status = response?.Status ?? response?.status; + return status === true || String(status).toLowerCase() === 'true'; + } + + private readCookie(): string { + if (typeof document === 'undefined') { + return ''; + } + + const prefix = `${this.cookieName}=`; + for (const part of document.cookie.split(';')) { + const segment = part.trim(); + if (!segment.startsWith(prefix)) { + continue; + } + + return decodeURIComponent(segment.slice(prefix.length)); + } + + return ''; + } + + private writeCookie(sessionId: string, expiresAt: Date): void { + if (typeof document === 'undefined') { + return; + } + + const cookieParts = [ + `${this.cookieName}=${encodeURIComponent(sessionId)}`, + 'Path=/', + `Expires=${expiresAt.toUTCString()}`, + 'SameSite=Lax' + ]; + + if (typeof location !== 'undefined' && location.protocol === 'https:') { + cookieParts.push('Secure'); + } + + document.cookie = cookieParts.join('; '); + } + + private deleteCookie(): void { + if (typeof document === 'undefined') { + return; + } + + document.cookie = `${this.cookieName}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`; + } +} \ No newline at end of file diff --git a/src/app/pages/fastcheck-page/fastcheck-page.ts b/src/app/pages/fastcheck-page/fastcheck-page.ts index 09bbabb..b03be8c 100644 --- a/src/app/pages/fastcheck-page/fastcheck-page.ts +++ b/src/app/pages/fastcheck-page/fastcheck-page.ts @@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http'; import { FastcheckService } from '../../fastcheck.service'; import { API_VITANOVA_NETWORK, FASTCHECK_API, FASTCHECK_STORE_API, QR_VITANOVA_API } from '../../api'; import { AuthDialogAuthorizedEvent, AuthDialogComponent, AuthDialogMode } from '../../auth-dialog/auth-dialog'; +import { AuthSessionService } from '../../auth-session.service'; import { TranslatePipe } from '../../translate/translate.pipe'; import { TranslationService } from '../../translate/translation.service'; @@ -55,6 +56,7 @@ export class FastcheckPage { private readonly defaultPartnerId = 'fast-c202-4062-bcfb-8b4c8cc59adc'; private http = inject(HttpClient); + private authSession = inject(AuthSessionService); private store = inject(FastcheckService); private i18n = inject(TranslationService); @@ -277,37 +279,52 @@ export class FastcheckPage { event.preventDefault(); const id = this.partnerId() || this.defaultPartnerId; - const sessionId = (localStorage.getItem('fc_session') ?? '').trim(); + const sessionId = this.authSession.getSessionId(); if (!sessionId) { this.openAuth('new'); return; } - const headers: Record = { - Authorization: JSON.stringify({ sessionID: sessionId, partnerID: id }) - }; - - this.http - .get(`${API_VITANOVA_NETWORK}/partners/${encodeURIComponent(id)}`, { headers }) - .subscribe({ - next: () => { - // Authorized partner: skip Telegram auth popup and go directly. - this.doRedirectToNew(sessionId); - }, - error: () => { - // Not authorized: force fresh Telegram auth QR popup. - localStorage.removeItem('fc_session'); + this.authSession.validateSession(sessionId).subscribe({ + next: (response) => { + if (!response) { this.openAuth('new'); + return; } - }); + + const headers: Record = { + Authorization: JSON.stringify({ sessionID: sessionId, partnerID: id }) + }; + + this.http + .get(`${API_VITANOVA_NETWORK}/partners/${encodeURIComponent(id)}`, { headers }) + .subscribe({ + next: () => { + // Authorized partner: skip Telegram auth popup and go directly. + this.doRedirectToNew(sessionId); + }, + error: () => { + // Not authorized: force fresh Telegram auth QR popup. + this.authSession.clearSession(); + this.openAuth('new'); + } + }); + }, + error: () => { + this.authSession.clearSession(); + this.openAuth('new'); + } + }); } private doRedirectToNew(sessionId?: string): void { - const tok = sessionId || localStorage.getItem('fc_session') || ''; - if (tok) { - localStorage.setItem('fc_session', tok); + const tok = sessionId || this.authSession.getSessionId() || ''; + if (!tok) { + this.openAuth('new'); + return; } + window.location.href = this.newQrUrl(); }