fix(admin-auth): reuse exact same QR/session API and component for admin login
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- Removed invented adminAuthApiUrl endpoint and separate AdminLoginComponent.
Admin login now uses the exact same Telegram session backend
(TelegramSessionApiService, {authApiUrl}/users/sessions) and the exact
same TelegramLoginComponent (mode="customer" | "admin" input) as customer
login - only the storage (cookie/localStorage/signals) stays separate.
- Extracted the shared HTTP+normalization logic from AuthService into
TelegramSessionApiService so both AuthService and AdminAuthService call it
instead of duplicating request/parsing code.
- Documented the resulting backend gap in docs/Project-Editor.md: since the
session API has no concept of "admin", server-side role enforcement is
required when admin API calls are made - the frontend only decides where
to store the session, not whether the user is actually an admin.
This commit is contained in:
@@ -1,15 +1,22 @@
|
||||
import { Injectable, signal, computed } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, of, catchError, map, tap } from 'rxjs';
|
||||
import { AdminAuthStatus, AdminSession, AdminWebSessionStart } from '../../models/admin-auth.model';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { generateGuid } from '../../shared/util/guid.util';
|
||||
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { AdminAuthStatus } from '../../models/admin-auth.model';
|
||||
import { AuthSession, WebSessionStart } from '../../models/auth.model';
|
||||
import { TelegramSessionApiService } from '../../services/telegram-session-api.service';
|
||||
|
||||
/**
|
||||
* Admin session storage is completely separate from customer session storage
|
||||
* (AuthService uses cookie `webSessionID` + localStorage `web_session_id`).
|
||||
* Distinct cookie/localStorage names here are intentional: an admin login must
|
||||
* never authenticate the customer session and vice versa.
|
||||
* Admin login uses the exact same Telegram QR/session API as the customer
|
||||
* login (TelegramSessionApiService, `{authApiUrl}/users/sessions`) - 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.
|
||||
*
|
||||
* Backend gap this creates (see docs/Project-Editor.md): 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';
|
||||
@@ -18,7 +25,9 @@ const ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60;
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AdminAuthService {
|
||||
private readonly sessionSignal = signal<AdminSession | null>(null);
|
||||
private readonly api = inject(TelegramSessionApiService);
|
||||
|
||||
private readonly sessionSignal = signal<AuthSession | null>(null);
|
||||
private readonly statusSignal = signal<AdminAuthStatus>('unknown');
|
||||
private readonly showLoginSignal = signal(false);
|
||||
|
||||
@@ -27,13 +36,10 @@ export class AdminAuthService {
|
||||
readonly isAuthenticated = computed(() => this.statusSignal() === 'authenticated');
|
||||
readonly showLoginDialog = this.showLoginSignal.asReadonly();
|
||||
readonly displayName = computed(() => this.sessionSignal()?.displayName ?? null);
|
||||
readonly role = computed(() => this.sessionSignal()?.role ?? null);
|
||||
|
||||
private readonly adminAuthApiUrl = (environment as Record<string, unknown>)['adminAuthApiUrl'] as string
|
||||
?? `${environment.authApiUrl}/admin`;
|
||||
private sessionCheckTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
constructor(private readonly http: HttpClient) {
|
||||
constructor() {
|
||||
this.checkSession();
|
||||
}
|
||||
|
||||
@@ -52,52 +58,24 @@ export class AdminAuthService {
|
||||
});
|
||||
}
|
||||
|
||||
/** Check session without mutating internal state (used for polling). */
|
||||
checkSessionOnce(webSessionID = this.getStoredAdminSessionID()): Observable<AdminSession | null> {
|
||||
if (!webSessionID) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.http.get<Record<string, unknown>>(
|
||||
`${this.adminAuthApiUrl}/sessions/${encodeURIComponent(webSessionID)}`
|
||||
).pipe(
|
||||
map(response => this.normalizeSession(response, webSessionID)),
|
||||
/** Check session without mutating internal state beyond activating on success (used for polling). */
|
||||
checkSessionOnce(webSessionID = this.getStoredAdminSessionID()): Observable<AuthSession | null> {
|
||||
return this.api.checkSessionOnce(webSessionID).pipe(
|
||||
tap(session => {
|
||||
if (session?.active) {
|
||||
this.activateSession(session);
|
||||
}
|
||||
}),
|
||||
catchError(() => of(null))
|
||||
);
|
||||
}
|
||||
|
||||
/** Create a backend admin web session, to be scanned/opened the same way customer QR login works. */
|
||||
createWebSession(): Observable<AdminWebSessionStart> {
|
||||
const webSessionID = generateGuid();
|
||||
|
||||
return this.http.post<Record<string, unknown>>(
|
||||
`${this.adminAuthApiUrl}/sessions`,
|
||||
{ webSessionID },
|
||||
{ headers: { AdminWebSessionID: webSessionID } }
|
||||
).pipe(
|
||||
map(response => {
|
||||
const responseWebSessionID = this.extractSessionId(response, webSessionID);
|
||||
return {
|
||||
webSessionID: responseWebSessionID,
|
||||
url: this.getAdminLoginUrl(responseWebSessionID),
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getAdminLoginUrl(webSessionID: string): string {
|
||||
const botUsername = this.getAdminBotUsername();
|
||||
return `https://t.me/${botUsername}?start=admin_${encodeURIComponent(webSessionID)}`;
|
||||
/** Create a backend web session - identical call to the customer login (TelegramSessionApiService.createSession). */
|
||||
createWebSession(): Observable<WebSessionStart> {
|
||||
return this.api.createSession();
|
||||
}
|
||||
|
||||
getAdminAppLoginUrl(webSessionID: string): string {
|
||||
const botUsername = this.getAdminBotUsername();
|
||||
return `tg://resolve?domain=${encodeURIComponent(botUsername)}&start=admin_${encodeURIComponent(webSessionID)}`;
|
||||
return this.api.getBotAppLoginUrl(webSessionID);
|
||||
}
|
||||
|
||||
onLoginComplete(): void {
|
||||
@@ -122,9 +100,7 @@ export class AdminAuthService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.http.delete(`${this.adminAuthApiUrl}/sessions/${encodeURIComponent(webSessionID)}`, {
|
||||
headers: { AdminWebSessionID: webSessionID }
|
||||
}).pipe(catchError(() => of(null))).subscribe(() => this.clearAuthState('unauthenticated'));
|
||||
this.api.logout(webSessionID).subscribe(() => this.clearAuthState('unauthenticated'));
|
||||
}
|
||||
|
||||
/** JWT pair storage, reserved for once the backend issues admin access/refresh tokens. Unused until then. */
|
||||
@@ -148,7 +124,7 @@ export class AdminAuthService {
|
||||
localStorage.removeItem(ADMIN_REFRESH_STORAGE_KEY);
|
||||
}
|
||||
|
||||
private activateSession(session: AdminSession): void {
|
||||
private activateSession(session: AuthSession): void {
|
||||
this.sessionSignal.set(session);
|
||||
this.statusSignal.set('authenticated');
|
||||
this.setStoredAdminSessionID(session.sessionId);
|
||||
@@ -181,73 +157,6 @@ export class AdminAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeSession(response: Record<string, unknown> | null, fallbackSessionId: string): AdminSession | null {
|
||||
if (!response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const status = this.readFirst(response, ['status', 'Status', 'active', 'Active', 'authenticated', 'Authenticated']);
|
||||
const active = this.isActiveStatus(status);
|
||||
const sessionId = this.extractSessionId(response, fallbackSessionId);
|
||||
const username = this.readString(this.readFirst(response, ['username', 'Username']));
|
||||
const displayName = this.readString(this.readFirst(response, ['displayName', 'DisplayName', 'name', 'Name'])) ?? username ?? 'Admin';
|
||||
const role = this.readString(this.readFirst(response, ['role', 'Role']));
|
||||
const adminId = this.readNumber(this.readFirst(response, ['adminId', 'AdminId', 'userId', 'UserId', 'id', 'ID']));
|
||||
const expiresAt = this.readString(this.readFirst(response, ['expiresAt', 'ExpiresAt', 'expires', 'Expires']))
|
||||
?? new Date(Date.now() + ADMIN_SESSION_COOKIE_MAX_AGE_SECONDS * 1000).toISOString();
|
||||
|
||||
return { sessionId, adminId, username, displayName, role, active, expires: expiresAt };
|
||||
}
|
||||
|
||||
private extractSessionId(response: Record<string, unknown> | null, fallbackSessionId: string): string {
|
||||
if (!response) {
|
||||
return fallbackSessionId;
|
||||
}
|
||||
return this.readString(this.readFirst(response, [
|
||||
'webSessionID', 'WebSessionID', 'sessionID', 'SessionID', 'sessionId', 'id', 'ID'
|
||||
])) ?? fallbackSessionId;
|
||||
}
|
||||
|
||||
private readFirst(source: Record<string, unknown>, keys: string[]): unknown {
|
||||
for (const key of keys) {
|
||||
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
||||
return source[key];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private readString(value: unknown): string | null {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number' || typeof value === 'bigint') {
|
||||
return value.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private readNumber(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private isActiveStatus(status: unknown): boolean {
|
||||
if (status === true || status === 1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof status !== 'string') {
|
||||
return false;
|
||||
}
|
||||
return ['true', '1', 'active', 'authenticated', 'confirmed', 'success', 'logged_in'].includes(status.toLowerCase());
|
||||
}
|
||||
|
||||
private getStoredAdminSessionID(): string | null {
|
||||
if (typeof document === 'undefined') {
|
||||
return null;
|
||||
@@ -277,10 +186,4 @@ export class AdminAuthService {
|
||||
}
|
||||
document.cookie = `${ADMIN_SESSION_COOKIE}=; Max-Age=0; Path=/; SameSite=Strict`;
|
||||
}
|
||||
|
||||
private getAdminBotUsername(): string {
|
||||
return (environment as Record<string, unknown>)['adminTelegramBot'] as string
|
||||
?? (environment as Record<string, unknown>)['telegramBot'] as string
|
||||
?? 'DexarSupport_bot';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
@if (showDialog()) {
|
||||
<div class="login-overlay" (click)="close()">
|
||||
<div class="login-dialog" (click)="$event.stopPropagation()">
|
||||
<button class="close-btn" (click)="close()">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="login-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<path d="M12 2 3 6v6c0 5 4 8.7 9 10 5-1.3 9-5 9-10V6l-9-4z"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2>{{ 'adminAuth.loginRequired' | translate }}</h2>
|
||||
<p class="login-desc">{{ 'adminAuth.loginDescription' | translate }}</p>
|
||||
|
||||
@if (status() === 'checking') {
|
||||
<div class="login-status checking">
|
||||
<div class="spinner"></div>
|
||||
<span>{{ 'adminAuth.checking' | translate }}</span>
|
||||
</div>
|
||||
} @else {
|
||||
<button class="telegram-btn" (click)="openAppLogin()">
|
||||
{{ 'adminAuth.loginWithApp' | translate }}
|
||||
</button>
|
||||
|
||||
<div class="qr-section">
|
||||
<p class="qr-hint">{{ 'adminAuth.orScanQr' | translate }}</p>
|
||||
|
||||
@switch (qrStatus()) {
|
||||
@case ('loading') {
|
||||
<div class="qr-container qr-loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
}
|
||||
@case ('ready') {
|
||||
<div class="qr-container">
|
||||
<img [src]="'https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=' + encodedQrUrl()"
|
||||
alt="QR Code"
|
||||
width="180"
|
||||
height="180"
|
||||
loading="eager" />
|
||||
</div>
|
||||
}
|
||||
@case ('expired') {
|
||||
<div class="qr-container qr-expired" (click)="refreshQr()">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
<span>{{ 'adminAuth.qrExpired' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
@case ('error') {
|
||||
<div class="qr-container qr-error" (click)="refreshQr()">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6"/>
|
||||
<path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"/>
|
||||
</svg>
|
||||
<span>{{ 'adminAuth.qrError' | translate }}</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<p class="login-note">{{ 'adminAuth.loginNote' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
.login-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.login-dialog {
|
||||
position: relative;
|
||||
background: var(--bg-card, #fff);
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
animation: scaleIn 0.25s ease;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-hover, #f0f0f0);
|
||||
color: var(--text-secondary, #666);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
margin: 0 auto 16px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-light, rgba(73, 118, 113, 0.1));
|
||||
color: var(--accent-color, #497671);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary, #1a1a1a);
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
margin: 0 0 24px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary, #666);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.telegram-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
background: #2AABEE;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #229ED9;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(42, 171, 238, 0.3);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.tg-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bot-link {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
color: var(--accent-color, #497671);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
overflow-wrap: anywhere;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-section {
|
||||
margin-top: 20px;
|
||||
|
||||
.qr-hint {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary, #999);
|
||||
}
|
||||
|
||||
.qr-container {
|
||||
display: inline-flex;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e8e8e8;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&.qr-loading {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
|
||||
.spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid #e0e0e0;
|
||||
border-top-color: var(--accent-color, #497671);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
&.qr-expired {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary, #999);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color, #497671);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&.qr-error {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 204px;
|
||||
height: 204px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary, #999);
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--accent-color, #497671);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-note {
|
||||
margin: 16px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary, #999);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.login-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
color: var(--text-secondary, #666);
|
||||
font-size: 14px;
|
||||
|
||||
.spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-top-color: var(--accent-color, #497671);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-dialog {
|
||||
padding: 24px 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.qr-section .qr-container img {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, effect, OnDestroy } from '@angular/core';
|
||||
import { AdminAuthService } from './admin-auth.service';
|
||||
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||
import { QrLoginEngine } from '../../shared/qr-login/qr-login.engine';
|
||||
import { QrLoginAdapter } from '../../shared/qr-login/qr-login.model';
|
||||
import { AdminSession } from '../../models/admin-auth.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin-login',
|
||||
standalone: true,
|
||||
imports: [TranslatePipe],
|
||||
templateUrl: './admin-login.component.html',
|
||||
styleUrls: ['./admin-login.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminLoginComponent implements OnDestroy {
|
||||
private readonly adminAuth = inject(AdminAuthService);
|
||||
|
||||
readonly showDialog = this.adminAuth.showLoginDialog;
|
||||
readonly status = this.adminAuth.status;
|
||||
|
||||
private readonly adapter: QrLoginAdapter<AdminSession> = {
|
||||
createSession: () => this.adminAuth.createWebSession(),
|
||||
checkSessionOnce: webSessionID => this.adminAuth.checkSessionOnce(webSessionID),
|
||||
isSessionActive: session => !!session?.active,
|
||||
getAppLoginUrl: webSessionID => this.adminAuth.getAdminAppLoginUrl(webSessionID),
|
||||
onLoginComplete: () => this.adminAuth.onLoginComplete(),
|
||||
};
|
||||
|
||||
private readonly engine = new QrLoginEngine<AdminSession>(this.adapter);
|
||||
readonly qrStatus = this.engine.qrStatus;
|
||||
readonly encodedQrUrl = this.engine.encodedQrUrl;
|
||||
|
||||
constructor() {
|
||||
effect(() => this.engine.setActive(this.showDialog()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.engine.destroy();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.engine.setActive(false);
|
||||
this.adminAuth.hideLogin();
|
||||
}
|
||||
|
||||
openAppLogin(): void {
|
||||
this.engine.openAppLogin();
|
||||
}
|
||||
|
||||
refreshQr(): void {
|
||||
this.engine.refresh();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user