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, isDevMode } from '@angular/core'; import { tap } from 'rxjs'; import { TelegramSessionApiService } from './telegram-session-api.service'; /** * Admin login uses the exact same Telegram QR/session API as the customer * login (TelegramSessionApiService) - there is no separate admin backend * endpoint, and none should be invented client-side. Only the *storage* is * kept separate from AuthService, so an admin QR scan never authenticates * the customer session or vice versa: distinct cookie name, distinct * signals, distinct guard/interceptor. * * Since the session API itself has no concept of "admin", the frontend * cannot tell an admin Telegram session from a regular one. Actual admin * authorization must be enforced server-side when admin API calls are made * with the resulting session id - the frontend only decides where to * *store* the result. */ const ADMIN_SESSION_COOKIE = 'adminSessionID'; const ADMIN_TOKEN_STORAGE_KEY = 'adminToken'; const ADMIN_REFRESH_STORAGE_KEY = 'adminRefreshToken'; const ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60; let AdminAuthService = class AdminAuthService { constructor() { this.api = inject(TelegramSessionApiService); this.sessionSignal = signal(null); this.statusSignal = signal('unknown'); this.showLoginSignal = signal(false); this.session = this.sessionSignal.asReadonly(); this.status = this.statusSignal.asReadonly(); this.isAuthenticated = computed(() => this.statusSignal() === 'authenticated'); this.showLoginDialog = this.showLoginSignal.asReadonly(); this.displayName = computed(() => this.sessionSignal()?.displayName ?? null); this.checkSession(); } checkSession() { const webSessionID = this.getStoredAdminSessionID(); if (!webSessionID) { this.clearAuthState('unauthenticated'); return; } this.statusSignal.set('checking'); this.checkSessionOnce(webSessionID).subscribe(session => { if (!session?.active) { this.clearAuthState('unauthenticated'); } }); } /** Check session without mutating internal state beyond activating on success (used for polling). */ checkSessionOnce(webSessionID = this.getStoredAdminSessionID()) { return this.api.checkSessionOnce(webSessionID).pipe(tap(session => { if (session?.active) { this.activateSession(session); } })); } /** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */ createWebSession() { return this.api.createSession(); } getAdminAppLoginUrl(webSessionID) { return this.api.getBotAppLoginUrl(webSessionID); } onLoginComplete() { this.hideLogin(); if (!this.isAuthenticated()) { this.checkSession(); } } requestLogin() { this.showLoginSignal.set(true); } /** * Dev-only shortcut for local testing without a reachable Telegram/session * backend: fabricates a local session and activates it directly, skipping * the QR flow entirely. No-ops in production builds (checked via Angular's * isDevMode() at runtime, not just build-time, so it is safe even if this * code ships). Never call this from anywhere reachable in a production build. */ devBypassLogin() { if (!isDevMode()) { return; } this.hideLogin(); this.activateSession({ sessionId: `dev-bypass-${Date.now()}`, userId: 0, username: 'dev-admin', displayName: 'Dev Admin (local bypass)', active: true, expires: new Date(Date.now() + 60 * 60 * 1000).toISOString(), }); } hideLogin() { this.showLoginSignal.set(false); } logout() { const webSessionID = this.sessionSignal()?.sessionId || this.getStoredAdminSessionID(); if (!webSessionID) { this.clearAuthState('unauthenticated'); return; } this.api.logout(webSessionID).subscribe(() => this.clearAuthState('unauthenticated')); } /** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */ getAdminToken() { return typeof localStorage === 'undefined' ? null : localStorage.getItem(ADMIN_TOKEN_STORAGE_KEY); } setAdminTokens(token, refreshToken) { if (typeof localStorage === 'undefined') { return; } localStorage.setItem(ADMIN_TOKEN_STORAGE_KEY, token); localStorage.setItem(ADMIN_REFRESH_STORAGE_KEY, refreshToken); } clearAdminTokens() { if (typeof localStorage === 'undefined') { return; } localStorage.removeItem(ADMIN_TOKEN_STORAGE_KEY); localStorage.removeItem(ADMIN_REFRESH_STORAGE_KEY); } activateSession(session) { this.sessionSignal.set(session); this.statusSignal.set('authenticated'); this.setStoredAdminSessionID(session.sessionId); this.scheduleSessionRefresh(session.expires); } clearAuthState(status) { this.sessionSignal.set(null); this.statusSignal.set(status); this.clearStoredAdminSessionID(); this.clearAdminTokens(); this.clearSessionRefresh(); } scheduleSessionRefresh(expiresAt) { this.clearSessionRefresh(); const expiresMs = new Date(expiresAt).getTime(); const nowMs = Date.now(); const refreshIn = Number.isFinite(expiresMs) ? Math.max(expiresMs - nowMs - 60_000, 30_000) : ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS * 1000; this.sessionCheckTimer = setTimeout(() => this.checkSession(), refreshIn); } clearSessionRefresh() { if (this.sessionCheckTimer) { clearTimeout(this.sessionCheckTimer); this.sessionCheckTimer = undefined; } } getStoredAdminSessionID() { if (typeof document === 'undefined') { return null; } const cookie = document.cookie.split('; ').find(row => row.startsWith(`${ADMIN_SESSION_COOKIE}=`)); if (!cookie) { return null; } try { return decodeURIComponent(cookie.substring(ADMIN_SESSION_COOKIE.length + 1)); } catch { return null; } } setStoredAdminSessionID(webSessionID) { if (typeof document === 'undefined') { return; } const secure = typeof window !== 'undefined' && window.location.protocol === 'https:' ? '; Secure' : ''; document.cookie = `${ADMIN_SESSION_COOKIE}=${encodeURIComponent(webSessionID)}; Max-Age=${ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS}; Path=/; SameSite=Strict${secure}`; } clearStoredAdminSessionID() { if (typeof document === 'undefined') { return; } document.cookie = `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Strict`; } }; AdminAuthService = __decorate([ Injectable({ providedIn: 'root' }) ], AdminAuthService); export { AdminAuthService };