162 lines
6.3 KiB
JavaScript
162 lines
6.3 KiB
JavaScript
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
||
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||
|
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||
|
|
};
|
||
|
|
import { Injectable, signal, computed, inject } from '@angular/core';
|
||
|
|
import { tap } from 'rxjs';
|
||
|
|
import { TelegramSessionApiService } from './telegram-session-api.service';
|
||
|
|
const WEB_SESSION_COOKIE = 'webSessionID';
|
||
|
|
const WEB_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;
|
||
|
|
/** Customer-facing Telegram QR/session auth. Distinct storage/state from AdminAuthService by design. */
|
||
|
|
let AuthService = class AuthService {
|
||
|
|
constructor() {
|
||
|
|
this.api = inject(TelegramSessionApiService);
|
||
|
|
this.sessionSignal = signal(null);
|
||
|
|
this.statusSignal = signal('unknown');
|
||
|
|
this.showLoginSignal = signal(false);
|
||
|
|
/** Current auth session */
|
||
|
|
this.session = this.sessionSignal.asReadonly();
|
||
|
|
/** Current auth status */
|
||
|
|
this.status = this.statusSignal.asReadonly();
|
||
|
|
/** Whether user is fully authenticated */
|
||
|
|
this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated');
|
||
|
|
/** Whether to show login dialog */
|
||
|
|
this.showLoginDialog = this.showLoginSignal.asReadonly();
|
||
|
|
/** Display name of authenticated user */
|
||
|
|
this.displayName = computed(() => this.sessionSignal()?.displayName ?? null);
|
||
|
|
// On init, check existing session via cookie
|
||
|
|
this.checkSession();
|
||
|
|
}
|
||
|
|
/** Check the current webSessionID cookie against the auth backend. */
|
||
|
|
checkSession() {
|
||
|
|
const webSessionID = this.getStoredWebSessionID();
|
||
|
|
if (!webSessionID) {
|
||
|
|
this.clearAuthState('unauthenticated');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.statusSignal.set('checking');
|
||
|
|
this.checkSessionOnce(webSessionID).subscribe(session => {
|
||
|
|
if (!session?.active) {
|
||
|
|
this.clearAuthState('unauthenticated');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/** Check session without updating internal state beyond activating on success (used for polling). */
|
||
|
|
checkSessionOnce(webSessionID = this.getStoredWebSessionID()) {
|
||
|
|
return this.api.checkSessionOnce(webSessionID).pipe(tap(session => {
|
||
|
|
if (session?.active) {
|
||
|
|
this.activateSession(session);
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Called after user completes Telegram login.
|
||
|
|
*/
|
||
|
|
onTelegramLoginComplete() {
|
||
|
|
this.hideLogin();
|
||
|
|
if (!this.isAuthenticated()) {
|
||
|
|
this.checkSession();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/** Generate the Telegram login URL for bot-based auth */
|
||
|
|
getTelegramLoginUrl(webSessionID) {
|
||
|
|
return this.api.getBotLoginUrl(webSessionID);
|
||
|
|
}
|
||
|
|
/** Generate a Telegram app deep link for mobile login without opening a browser tab. */
|
||
|
|
getTelegramAppLoginUrl(webSessionID) {
|
||
|
|
return this.api.getBotAppLoginUrl(webSessionID);
|
||
|
|
}
|
||
|
|
/** Create a backend web session and return the Telegram start link for it. */
|
||
|
|
createWebSession() {
|
||
|
|
return this.api.createSession();
|
||
|
|
}
|
||
|
|
/** Show login dialog (called when user tries to pay without being logged in) */
|
||
|
|
requestLogin() {
|
||
|
|
this.showLoginSignal.set(true);
|
||
|
|
}
|
||
|
|
/** Hide login dialog */
|
||
|
|
hideLogin() {
|
||
|
|
this.showLoginSignal.set(false);
|
||
|
|
}
|
||
|
|
/** Logout — clears session on backend and locally */
|
||
|
|
logout() {
|
||
|
|
const webSessionID = this.sessionSignal()?.sessionId || this.getStoredWebSessionID();
|
||
|
|
if (!webSessionID) {
|
||
|
|
this.clearAuthState('unauthenticated');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.api.logout(webSessionID).subscribe(() => {
|
||
|
|
this.clearAuthState('unauthenticated');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
activateSession(session) {
|
||
|
|
this.sessionSignal.set(session);
|
||
|
|
this.statusSignal.set('authenticated');
|
||
|
|
this.setStoredWebSessionID(session.sessionId);
|
||
|
|
this.scheduleSessionRefresh(session.expires);
|
||
|
|
}
|
||
|
|
clearAuthState(status) {
|
||
|
|
this.sessionSignal.set(null);
|
||
|
|
this.statusSignal.set(status);
|
||
|
|
this.clearStoredWebSessionID();
|
||
|
|
this.clearSessionRefresh();
|
||
|
|
}
|
||
|
|
/** Schedule a session re-check before it expires */
|
||
|
|
scheduleSessionRefresh(expiresAt) {
|
||
|
|
this.clearSessionRefresh();
|
||
|
|
const expiresMs = new Date(expiresAt).getTime();
|
||
|
|
const nowMs = Date.now();
|
||
|
|
// Re-check 60 seconds before expiry, minimum 30s from now
|
||
|
|
const refreshIn = Number.isFinite(expiresMs)
|
||
|
|
? Math.max(expiresMs - nowMs - 60_000, 30_000)
|
||
|
|
: WEB_SESSION_COOKIE_MAX_AGE_SECONDS * 1000;
|
||
|
|
this.sessionCheckTimer = setTimeout(() => {
|
||
|
|
this.checkSession();
|
||
|
|
}, refreshIn);
|
||
|
|
}
|
||
|
|
clearSessionRefresh() {
|
||
|
|
if (this.sessionCheckTimer) {
|
||
|
|
clearTimeout(this.sessionCheckTimer);
|
||
|
|
this.sessionCheckTimer = undefined;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
getStoredWebSessionID() {
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setStoredWebSessionID(webSessionID) {
|
||
|
|
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}`;
|
||
|
|
}
|
||
|
|
clearStoredWebSessionID() {
|
||
|
|
if (typeof document === 'undefined') {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
document.cookie = `${WEB_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Lax`;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
AuthService = __decorate([
|
||
|
|
Injectable({
|
||
|
|
providedIn: 'root'
|
||
|
|
})
|
||
|
|
], AuthService);
|
||
|
|
export { AuthService };
|