warning message changes

This commit is contained in:
Gagan7900
2026-08-17 10:13:07 +07:00
parent 6c22f57273
commit 17c6292521
3 changed files with 75 additions and 21 deletions
@@ -6,19 +6,21 @@ import { merge, fromEvent, Subscription } from 'rxjs';
import { throttleTime } from 'rxjs/operators';
import { environment } from '../../../../environments/environment';
import { AuthService } from '../../../features/authentication/data-access/auth.service';
import { TokenStorageService } from './token-storage.service';
@Injectable()
@Injectable({ providedIn: 'root' })
export class SessionTimeoutService implements OnDestroy {
private readonly document = inject(DOCUMENT);
private readonly router = inject(Router);
private readonly authService = inject(AuthService);
private readonly tokenStorage = inject(TokenStorageService);
private readonly destroyRef = inject(DestroyRef);
readonly showWarning = signal(false);
readonly remainingSeconds = signal(0);
private readonly warningAfterMs = environment.sessionTimeout?.warningAfterMs ?? 25 * 60 * 1000;
private readonly logoutAfterMs = environment.sessionTimeout?.logoutAfterMs ?? 30 * 60 * 1000;
private readonly configWarningAfterMs = environment.sessionTimeout?.warningAfterMs ?? 25 * 60 * 1000;
private readonly configLogoutAfterMs = environment.sessionTimeout?.logoutAfterMs ?? 30 * 60 * 1000;
private activitySubscription: Subscription | null = null;
private warningTimer: ReturnType<typeof setTimeout> | null = null;
@@ -79,10 +81,13 @@ export class SessionTimeoutService implements OnDestroy {
return;
}
// Include touch events for mobile devices
this.activitySubscription = merge(
fromEvent(this.document, 'mousemove'),
fromEvent(this.document, 'keydown'),
fromEvent(this.document, 'click'),
fromEvent(this.document, 'touchstart'),
fromEvent(this.document, 'touchmove'),
fromEvent(window, 'scroll')
)
.pipe(throttleTime(1000), takeUntilDestroyed(this.destroyRef))
@@ -104,8 +109,11 @@ export class SessionTimeoutService implements OnDestroy {
this.showWarning.set(false);
this.remainingSeconds.set(0);
const safeWarningDelay = Math.max(Math.min(this.warningAfterMs, this.logoutAfterMs), 0);
const safeLogoutDelay = Math.max(this.logoutAfterMs, 0);
// Calculate effective delays based on token expiry if available
const { warningDelay, logoutDelay } = this.calculateEffectiveDelays();
const safeWarningDelay = Math.max(Math.min(warningDelay, logoutDelay), 0);
const safeLogoutDelay = Math.max(logoutDelay, 0);
this.warningTimer = setTimeout(() => {
this.showWarning.set(true);
@@ -121,6 +129,43 @@ export class SessionTimeoutService implements OnDestroy {
}, safeLogoutDelay);
}
/**
* Calculate effective warning/logout delays based on the sooner of:
* - Configured timeout values
* - Actual token expiry from backend
*/
private calculateEffectiveDelays(): { warningDelay: number; logoutDelay: number } {
const accessTokenExpiresOn = this.tokenStorage.getAccessTokenExpiresOn();
if (!accessTokenExpiresOn) {
// No token expiry info - use configured values
return {
warningDelay: this.configWarningAfterMs,
logoutDelay: this.configLogoutAfterMs
};
}
const tokenExpiryMs = Date.parse(accessTokenExpiresOn);
const now = Date.now();
if (Number.isNaN(tokenExpiryMs) || tokenExpiryMs <= now) {
// Token already expired or invalid - logout immediately
return { warningDelay: 0, logoutDelay: 0 };
}
const tokenTimeRemaining = tokenExpiryMs - now;
// Use the sooner of configured timeout or token expiry
// Warning at 80% of token lifetime or configured warning, whichever is sooner
const tokenBasedWarning = Math.floor(tokenTimeRemaining * 0.8);
const tokenBasedLogout = tokenTimeRemaining;
return {
warningDelay: Math.min(this.configWarningAfterMs, tokenBasedWarning),
logoutDelay: Math.min(this.configLogoutAfterMs, tokenBasedLogout)
};
}
private updateRemainingSeconds(): void {
const remainingMs = Math.max(this.logoutDeadline - Date.now(), 0);
this.remainingSeconds.set(Math.ceil(remainingMs / 1000));
@@ -5,42 +5,43 @@
<app-sidebar appHoverEffectSidebar />
<div class="content">
<div class="main-content" (click)="clickOnBody()">
<app-page-header />
<router-outlet />
<app-page-header />
<router-outlet />
</div>
</div>
<app-footer />
<app-tab-to-top />
</div>
<div #responsiveoverlay id="responsive-overlay" (click)="clearToggle()"></div>
@if (sessionTimeoutService.showWarning()) {
<div
class="hs-overlay ti-modal mt-[1.75rem] hs-overlay-backdrop-open:!bg-[#32325180] dark:hs-overlay-backdrop-open:!bg-[#323251cc] pointer-events-none !block">
class="hs-overlay ti-modal mt-[1.75rem] hs-overlay-backdrop-open:!bg-[#32325180] dark:hs-overlay-backdrop-open:!bg-[#323251cc] pointer-events-none !block"
role="dialog"
aria-modal="true"
aria-labelledby="session-timeout-title"
aria-describedby="session-timeout-message"
tabindex="-1">
<div class="ti-modal-box pointer-events-auto">
<div class="ti-modal-content !border !border-defaultborder dark:!border-defaultborder/10 !rounded-[0.5rem]">
<div class="ti-modal-body !p-[1.5rem]">
<div class="flex items-start gap-4">
<span class="!w-[3rem] !h-[3rem] !leading-[3rem] rounded-[50%] avatar bg-warning/10 !text-warning text-center">
<span class="!w-[3rem] !h-[3rem] !leading-[3rem] rounded-[50%] avatar bg-warning/10 !text-warning text-center" aria-hidden="true">
<i class="fe fe-alert-triangle text-[1.125rem]"></i>
</span>
<div>
<p class="text-[1rem] font-semibold text-defaulttextcolor dark:text-defaulttextcolor/70 mb-2">Session Expiring Soon</p>
<p class="text-[#8c9097] dark:text-white/50 text-[0.875rem] mb-0">
<p id="session-timeout-title" class="text-[1rem] font-semibold text-defaulttextcolor dark:text-defaulttextcolor/70 mb-2">Session Expiring Soon</p>
<p id="session-timeout-message" class="text-[#8c9097] dark:text-white/50 text-[0.875rem] mb-0">
You have been inactive. You will be signed out in {{ sessionTimeoutService.remainingSeconds() }} seconds unless you continue your session.
</p>
</div>
</div>
</div>
<div class="ti-modal-footer !border !border-defaultborder dark:!border-defaultborder/10 !py-[1rem] !px-[1.25rem] justify-end gap-2">
<button type="button" class="ti-btn ti-btn-light" (click)="sessionTimeoutService.logoutNow()">Log Out</button>
<button type="button" class="ti-btn ti-btn-primary-full" (click)="sessionTimeoutService.staySignedIn()">Stay Signed In</button>
<button type="button" class="ti-btn ti-btn-light" (click)="sessionTimeoutService.logoutNow()" autofocus>Log Out</button>
<button type="button" class="ti-btn ti-btn-primary-full" (click)="sessionTimeoutService.staySignedIn()" data-primary-action>Stay Signed In</button>
</div>
</div>
</div>
</div>
}
@@ -1,4 +1,4 @@
import { Component, DOCUMENT, ElementRef, Renderer2, ViewChild, inject } from '@angular/core';
import { Component, DOCUMENT, ElementRef, HostListener, Renderer2, ViewChild, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Menu, NavService } from '../../../core/services/common/nav.service';
import { Router, RouterOutlet } from '@angular/router';
@@ -18,7 +18,7 @@ import { AuthService } from '../../../features/authentication/data-access/auth.s
selector: 'app-content-layout',
templateUrl: './content-layout.html',
styleUrl: './content-layout.scss',
providers: [SessionTimeoutService],
imports: [
Switcher,
Header,
@@ -132,13 +132,21 @@ export class ContentLayout {
}
clearToggle() {
clearToggle() {
let html = this.elementRef.nativeElement.ownerDocument.documentElement;
html?.setAttribute('data-toggled', 'close');
document.querySelector('#responsive-overlay')?.classList.remove('active');
}
@HostListener('document:keydown.escape')
onEscapeKey(): void {
if (this.sessionTimeoutService.showWarning()) {
// Escape key closes modal - user wants to stay signed in
this.sessionTimeoutService.staySignedIn();
}
}
}