feat(sprint18): editor autosave/reset, admin auth, QR reuse, Ed25519 prep
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- Project editor: persist draft to localStorage, restore on reload, last-saved/draft-restored status indicators, section/whole-draft reset with confirmation. - Extract shared QR/polling/expiry engine from TelegramLoginComponent (shared/qr-login) and reuse it for a new admin login flow. - Admin authentication kept fully separate from customer session: own cookie/localStorage keys, signals, guard, and header interceptor (core/admin-auth). - ?login=true / ?adminLogin=true open the respective login dialog for manual testing. - Ed25519 challenge/verify interfaces (fail-closed no-op binding) ready for backend delivery. - Document autosave/reset/admin-auth/QR-reuse/Ed25519 model and the remaining full-field-coverage gap in docs/Project-Editor.md.
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, inject, signal, computed, effect, OnDestroy } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, inject, effect, OnDestroy } from '@angular/core';
|
||||
import { AuthService } from '../../services/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 { AuthSession } from '../../models/auth.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-telegram-login',
|
||||
@@ -15,153 +18,39 @@ export class TelegramLoginComponent implements OnDestroy {
|
||||
showDialog = this.authService.showLoginDialog;
|
||||
status = this.authService.status;
|
||||
|
||||
loginUrl = signal('');
|
||||
webSessionID = signal('');
|
||||
qrStatus = signal<'loading' | 'ready' | 'expired' | 'error'>('loading');
|
||||
encodedQrUrl = computed(() => encodeURIComponent(this.loginUrl()));
|
||||
awaitingTelegramReturn = signal(false);
|
||||
private readonly adapter: QrLoginAdapter<AuthSession> = {
|
||||
createSession: () => this.authService.createWebSession(),
|
||||
checkSessionOnce: webSessionID => this.authService.checkSessionOnce(webSessionID),
|
||||
isSessionActive: session => !!session?.active,
|
||||
getAppLoginUrl: webSessionID => this.authService.getTelegramAppLoginUrl(webSessionID),
|
||||
onLoginComplete: () => this.authService.onTelegramLoginComplete(),
|
||||
};
|
||||
|
||||
private readonly pollIntervalMs = 5000;
|
||||
private pollTimer?: ReturnType<typeof setInterval>;
|
||||
private readonly handleVisibilityChange = () => {
|
||||
if (typeof document !== 'undefined' && document.visibilityState === 'visible') {
|
||||
this.checkLoginAfterReturn();
|
||||
}
|
||||
};
|
||||
private readonly handleWindowFocus = () => {
|
||||
this.checkLoginAfterReturn();
|
||||
};
|
||||
private readonly handlePageShow = () => {
|
||||
this.checkLoginAfterReturn();
|
||||
};
|
||||
private readonly engine = new QrLoginEngine<AuthSession>(this.adapter);
|
||||
readonly loginUrl = this.engine.loginUrl;
|
||||
readonly webSessionID = this.engine.webSessionID;
|
||||
readonly qrStatus = this.engine.qrStatus;
|
||||
readonly encodedQrUrl = this.engine.encodedQrUrl;
|
||||
readonly awaitingTelegramReturn = this.engine.awaitingAppReturn;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.showDialog()) {
|
||||
this.initQrLogin();
|
||||
} else {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.stopPolling();
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
window.addEventListener('focus', this.handleWindowFocus);
|
||||
window.addEventListener('pageshow', this.handlePageShow);
|
||||
}
|
||||
effect(() => this.engine.setActive(this.showDialog()));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.stopPolling();
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
|
||||
window.removeEventListener('focus', this.handleWindowFocus);
|
||||
window.removeEventListener('pageshow', this.handlePageShow);
|
||||
}
|
||||
this.engine.destroy();
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.engine.setActive(false);
|
||||
this.authService.hideLogin();
|
||||
this.stopPolling();
|
||||
}
|
||||
|
||||
openTelegramLogin(): void {
|
||||
const webSessionID = this.webSessionID();
|
||||
if (!webSessionID || typeof window === 'undefined') return;
|
||||
|
||||
if (!this.pollTimer) {
|
||||
this.startPolling(webSessionID);
|
||||
}
|
||||
|
||||
this.awaitingTelegramReturn.set(true);
|
||||
window.location.href = this.authService.getTelegramAppLoginUrl(webSessionID);
|
||||
this.engine.openAppLogin();
|
||||
}
|
||||
|
||||
refreshQr(): void {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.stopPolling();
|
||||
this.initQrLogin();
|
||||
}
|
||||
|
||||
private initQrLogin(): void {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.qrStatus.set('loading');
|
||||
this.loginUrl.set('');
|
||||
this.webSessionID.set('');
|
||||
|
||||
this.authService.createWebSession().subscribe({
|
||||
next: (res) => {
|
||||
this.loginUrl.set(res.url);
|
||||
this.webSessionID.set(res.webSessionID);
|
||||
this.qrStatus.set('ready');
|
||||
this.startPolling(res.webSessionID);
|
||||
},
|
||||
error: () => {
|
||||
this.qrStatus.set('error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startPolling(webSessionID: string): void {
|
||||
this.stopPolling();
|
||||
if (!webSessionID) return;
|
||||
|
||||
let checks = 0;
|
||||
this.pollTimer = setInterval(() => {
|
||||
checks++;
|
||||
if (checks > 100) {
|
||||
this.stopPolling();
|
||||
this.qrStatus.set('expired');
|
||||
return;
|
||||
}
|
||||
|
||||
this.authService.checkSessionOnce(webSessionID).subscribe({
|
||||
next: (session) => {
|
||||
if (session?.active) {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.stopPolling();
|
||||
this.authService.onTelegramLoginComplete();
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
// Network error — keep polling
|
||||
}
|
||||
});
|
||||
}, this.pollIntervalMs);
|
||||
}
|
||||
|
||||
private stopPolling(): void {
|
||||
if (this.pollTimer) {
|
||||
clearInterval(this.pollTimer);
|
||||
this.pollTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private checkLoginAfterReturn(): void {
|
||||
if (!this.showDialog() || !this.awaitingTelegramReturn()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const webSessionID = this.webSessionID();
|
||||
if (!webSessionID) {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.pollTimer) {
|
||||
this.startPolling(webSessionID);
|
||||
}
|
||||
|
||||
this.authService.checkSessionOnce(webSessionID).subscribe(session => {
|
||||
if (session?.active) {
|
||||
this.awaitingTelegramReturn.set(false);
|
||||
this.stopPolling();
|
||||
this.authService.onTelegramLoginComplete();
|
||||
}
|
||||
});
|
||||
this.engine.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user