checking api
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
202
src/app/auth-session.service.ts
Normal file
202
src/app/auth-session.service.ts
Normal 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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,37 +279,52 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers: Record<string, string> = {
|
this.authSession.validateSession(sessionId).subscribe({
|
||||||
Authorization: JSON.stringify({ sessionID: sessionId, partnerID: id })
|
next: (response) => {
|
||||||
};
|
if (!response) {
|
||||||
|
|
||||||
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.openAuth('new');
|
this.openAuth('new');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
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 {
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user