checking api

This commit is contained in:
sdarbinyan
2026-06-01 00:21:58 +04:00
parent 0d5cc8b28f
commit 7c7cdf8da2
4 changed files with 257 additions and 49 deletions

View File

@@ -1,13 +1,20 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { APP_INITIALIZER, ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import { AuthSessionService } from './auth-session.service';
import { routes } from './app.routes'; import { routes } from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideRouter(routes), provideRouter(routes),
provideHttpClient() provideHttpClient(),
{
provide: APP_INITIALIZER,
multi: true,
deps: [AuthSessionService],
useFactory: (authSession: AuthSessionService) => () => authSession.initialize()
}
] ]
}; };

View File

@@ -1,22 +1,9 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component, computed, effect, inject, input, output, signal } from '@angular/core'; import { Component, computed, effect, inject, input, output, signal } from '@angular/core';
import { USERS_VITANOVA_API } from '../api'; import { USERS_VITANOVA_API } from '../api';
import { AuthSessionService, WebSessionResponse } from '../auth-session.service';
import { TranslatePipe } from '../translate/translate.pipe'; 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 type AuthDialogMode = 'payment' | 'login' | 'new';
export interface AuthDialogAuthorizedEvent { export interface AuthDialogAuthorizedEvent {
@@ -35,8 +22,8 @@ type AuthDialogState = 'loading' | 'ready' | 'checking' | 'expired' | 'error';
}) })
export class AuthDialogComponent { export class AuthDialogComponent {
private readonly http = inject(HttpClient); private readonly http = inject(HttpClient);
private readonly authSession = inject(AuthSessionService);
private readonly telegramBot = 'myAMLKYCBOT'; private readonly telegramBot = 'myAMLKYCBOT';
private readonly sessionStorageKey = 'fc_session';
open = input(false); open = input(false);
mode = input<AuthDialogMode>('login'); mode = input<AuthDialogMode>('login');
@@ -114,7 +101,7 @@ export class AuthDialogComponent {
this.webSessionId.set(''); this.webSessionId.set('');
this.state.set('checking'); this.state.set('checking');
const existingSession = localStorage.getItem(this.sessionStorageKey) ?? ''; const existingSession = this.authSession.getSessionId();
if (existingSession) { if (existingSession) {
this.checkExistingSession(existingSession); this.checkExistingSession(existingSession);
return; return;
@@ -124,21 +111,17 @@ export class AuthDialogComponent {
} }
private checkExistingSession(sessionId: string): void { private checkExistingSession(sessionId: string): void {
this.http.get<WebSessionResponse>(`${USERS_VITANOVA_API}/users/sessions/${sessionId}`).subscribe({ this.authSession.validateSession(sessionId).subscribe({
next: (response) => { next: (response) => {
if (this.isAuthorized(response)) { if (response) {
this.webSessionId.set(sessionId); this.webSessionId.set(sessionId);
this.handleAuthorized(response, sessionId); this.handleAuthorized(response, sessionId);
return; return;
} }
localStorage.removeItem(this.sessionStorageKey);
this.createSession(); this.createSession();
}, },
error: () => { error: () => this.createSession()
localStorage.removeItem(this.sessionStorageKey);
this.createSession();
}
}); });
} }
@@ -195,6 +178,7 @@ export class AuthDialogComponent {
this.authenticated = true; this.authenticated = true;
this.stopPolling(); this.stopPolling();
this.webSessionId.set(sessionId); this.webSessionId.set(sessionId);
this.authSession.persistAuthorizedSession(sessionId, response.expires);
this.state.set('checking'); this.state.set('checking');
this.authorized.emit({ this.authorized.emit({
sessionId, sessionId,
@@ -217,14 +201,10 @@ export class AuthDialogComponent {
const sessionId = this.webSessionId(); const sessionId = this.webSessionId();
if (!sessionId) { if (!sessionId) {
if (!persistSession) {
localStorage.removeItem(this.sessionStorageKey);
}
return; return;
} }
if (persistSession) { if (persistSession) {
localStorage.setItem(this.sessionStorageKey, sessionId);
return; return;
} }
@@ -232,7 +212,9 @@ export class AuthDialogComponent {
.delete(`${USERS_VITANOVA_API}/users/sessions/${sessionId}`) .delete(`${USERS_VITANOVA_API}/users/sessions/${sessionId}`)
.subscribe({ error: () => undefined }); .subscribe({ error: () => undefined });
localStorage.removeItem(this.sessionStorageKey); if (this.authSession.getSessionId() === sessionId) {
this.authSession.clearSession();
}
} }
private getSessionId(response: WebSessionResponse | null | undefined): string { private getSessionId(response: WebSessionResponse | null | undefined): string {

View File

@@ -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<void> {
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<WebSessionResponse | null> {
return this.validateSession(this.getSessionId());
}
validateSession(sessionId: string): Observable<WebSessionResponse | null> {
const normalizedSessionId = sessionId.trim();
if (!normalizedSessionId) {
this.clearSession();
return of(null);
}
this.validating.set(true);
return this.http
.get<WebSessionResponse>(`${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`;
}
}

View File

@@ -4,6 +4,7 @@ import { HttpClient } from '@angular/common/http';
import { FastcheckService } from '../../fastcheck.service'; import { FastcheckService } from '../../fastcheck.service';
import { API_VITANOVA_NETWORK, FASTCHECK_API, FASTCHECK_STORE_API, QR_VITANOVA_API } from '../../api'; import { API_VITANOVA_NETWORK, FASTCHECK_API, FASTCHECK_STORE_API, QR_VITANOVA_API } from '../../api';
import { AuthDialogAuthorizedEvent, AuthDialogComponent, AuthDialogMode } from '../../auth-dialog/auth-dialog'; import { AuthDialogAuthorizedEvent, AuthDialogComponent, AuthDialogMode } from '../../auth-dialog/auth-dialog';
import { AuthSessionService } from '../../auth-session.service';
import { TranslatePipe } from '../../translate/translate.pipe'; import { TranslatePipe } from '../../translate/translate.pipe';
import { TranslationService } from '../../translate/translation.service'; import { TranslationService } from '../../translate/translation.service';
@@ -55,6 +56,7 @@ export class FastcheckPage {
private readonly defaultPartnerId = 'fast-c202-4062-bcfb-8b4c8cc59adc'; private readonly defaultPartnerId = 'fast-c202-4062-bcfb-8b4c8cc59adc';
private http = inject(HttpClient); private http = inject(HttpClient);
private authSession = inject(AuthSessionService);
private store = inject(FastcheckService); private store = inject(FastcheckService);
private i18n = inject(TranslationService); private i18n = inject(TranslationService);
@@ -277,13 +279,20 @@ export class FastcheckPage {
event.preventDefault(); event.preventDefault();
const id = this.partnerId() || this.defaultPartnerId; const id = this.partnerId() || this.defaultPartnerId;
const sessionId = (localStorage.getItem('fc_session') ?? '').trim(); const sessionId = this.authSession.getSessionId();
if (!sessionId) { if (!sessionId) {
this.openAuth('new'); this.openAuth('new');
return; return;
} }
this.authSession.validateSession(sessionId).subscribe({
next: (response) => {
if (!response) {
this.openAuth('new');
return;
}
const headers: Record<string, string> = { const headers: Record<string, string> = {
Authorization: JSON.stringify({ sessionID: sessionId, partnerID: id }) Authorization: JSON.stringify({ sessionID: sessionId, partnerID: id })
}; };
@@ -297,17 +306,25 @@ export class FastcheckPage {
}, },
error: () => { error: () => {
// Not authorized: force fresh Telegram auth QR popup. // Not authorized: force fresh Telegram auth QR popup.
localStorage.removeItem('fc_session'); this.authSession.clearSession();
this.openAuth('new');
}
});
},
error: () => {
this.authSession.clearSession();
this.openAuth('new'); this.openAuth('new');
} }
}); });
} }
private doRedirectToNew(sessionId?: string): void { private doRedirectToNew(sessionId?: string): void {
const tok = sessionId || localStorage.getItem('fc_session') || ''; const tok = sessionId || this.authSession.getSessionId() || '';
if (tok) { if (!tok) {
localStorage.setItem('fc_session', tok); this.openAuth('new');
return;
} }
window.location.href = this.newQrUrl(); window.location.href = this.newQrUrl();
} }