warning message changes
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user