feat: add Master Admin application functionality
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { finalize, forkJoin, Observable, of, shareReplay, tap } from 'rxjs';
|
||||
import { AppContextState, MenuContext } from '../models/context.model';
|
||||
import { MenuService } from './menu.service';
|
||||
import { PermissionService } from './permission.service';
|
||||
import { TenantContextService } from './tenant-context.service';
|
||||
import { UserContextService } from './user-context.service';
|
||||
import { NavService } from '../../shared/services/nav.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AppContextService {
|
||||
private readonly userContextService = inject(UserContextService);
|
||||
private readonly tenantContextService = inject(TenantContextService);
|
||||
private readonly permissionService = inject(PermissionService);
|
||||
private readonly menuService = inject(MenuService);
|
||||
private readonly navService = inject(NavService);
|
||||
|
||||
private loadRequest$: Observable<AppContextState> | null = null;
|
||||
private menuLoadRequest$: Observable<MenuContext> | null = null;
|
||||
private loaded = false;
|
||||
|
||||
ensureMenuInitialized(forceReload = false): Observable<MenuContext> {
|
||||
const currentMenu = this.menuService.menuContext();
|
||||
if (currentMenu && !forceReload) {
|
||||
this.navService.setMenuItems(this.menuService.getNavigationMenu());
|
||||
return of(currentMenu);
|
||||
}
|
||||
|
||||
if (this.menuLoadRequest$ && !forceReload) {
|
||||
return this.menuLoadRequest$;
|
||||
}
|
||||
|
||||
this.menuLoadRequest$ = this.menuService.loadMenu().pipe(
|
||||
tap(() => {
|
||||
this.navService.setMenuItems(this.menuService.getNavigationMenu());
|
||||
}),
|
||||
finalize(() => {
|
||||
this.menuLoadRequest$ = null;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return this.menuLoadRequest$;
|
||||
}
|
||||
|
||||
loadAppContext(forceReload = false): Observable<AppContextState> {
|
||||
if (this.loaded && !forceReload) {
|
||||
return of(this.getCurrentState());
|
||||
}
|
||||
|
||||
if (this.loadRequest$ && !forceReload) {
|
||||
return this.loadRequest$;
|
||||
}
|
||||
|
||||
this.loadRequest$ = forkJoin({
|
||||
user: this.userContextService.loadCurrentUserProfile(),
|
||||
tenant: this.tenantContextService.loadTenantContext(),
|
||||
permissions: this.permissionService.loadPermissions(),
|
||||
menu: this.menuService.loadMenu(),
|
||||
}).pipe(
|
||||
tap((context) => {
|
||||
this.navService.setMenuItems(this.menuService.getNavigationMenu());
|
||||
this.loaded = true;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.loadRequest$ = null;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return this.loadRequest$;
|
||||
}
|
||||
|
||||
ensureContextLoaded(): Observable<AppContextState> {
|
||||
return this.loadAppContext();
|
||||
}
|
||||
|
||||
clearContext(): void {
|
||||
this.userContextService.clear();
|
||||
this.tenantContextService.clear();
|
||||
this.permissionService.clear();
|
||||
this.menuService.clear();
|
||||
this.navService.clearMenuItems();
|
||||
this.loaded = false;
|
||||
this.loadRequest$ = null;
|
||||
}
|
||||
|
||||
getDefaultLandingPage(): string {
|
||||
return this.userContextService.currentUserContext()?.defaultLandingPage
|
||||
?? this.permissionService.permissionContext()?.defaultLandingPage
|
||||
?? this.tenantContextService.tenantContext()?.defaultLandingPage
|
||||
?? this.menuService.getDefaultLandingPage()
|
||||
?? '/dashboards/crm';
|
||||
}
|
||||
|
||||
private getCurrentState(): AppContextState {
|
||||
return {
|
||||
user: this.userContextService.currentUserContext() ?? { id: '', email: '', roles: [] },
|
||||
tenant: this.tenantContextService.tenantContext() ?? {},
|
||||
permissions: this.permissionService.permissionContext() ?? { roles: [], permissions: [] },
|
||||
menu: this.menuService.menuContext() ?? { items: [] },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -121,7 +121,6 @@ export class TokenStorageService {
|
||||
}
|
||||
|
||||
private isExpired(expiresOn: string | null): boolean {
|
||||
debugger;
|
||||
if (!expiresOn) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { API_CONFIG } from '../config/api.config';
|
||||
import { ApiResponse } from '../models/api-response.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class BaseApiService {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
protected get<T>(resource: string, params?: Record<string, string | number | boolean>): Observable<T> {
|
||||
let httpParams = new HttpParams();
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
httpParams = httpParams.set(key, String(value));
|
||||
});
|
||||
}
|
||||
|
||||
return this.http.get<T>(`${API_CONFIG.baseUrl}${resource}`, { params: httpParams });
|
||||
}
|
||||
|
||||
protected post<T, R = unknown>(resource: string, body: T): Observable<R> {
|
||||
return this.http.post<R>(`${API_CONFIG.baseUrl}${resource}`, body);
|
||||
}
|
||||
|
||||
protected put<T, R = unknown>(resource: string, body: T): Observable<R> {
|
||||
return this.http.put<R>(`${API_CONFIG.baseUrl}${resource}`, body);
|
||||
}
|
||||
|
||||
protected delete<R = unknown>(resource: string): Observable<R> {
|
||||
return this.http.delete<R>(`${API_CONFIG.baseUrl}${resource}`);
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,6 @@ export const SAAS_MENU_DATA: MenuContext = {
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/tenants', title: 'Tenants', type: 'link', dirchange: false },
|
||||
{ path: '/users', title: 'Users', type: 'link', dirchange: false },
|
||||
{
|
||||
title: 'Global Master',
|
||||
type: 'sub',
|
||||
@@ -33,11 +31,26 @@ export const SAAS_MENU_DATA: MenuContext = {
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/global-masters/currencies', title: 'Currency', type: 'link', dirchange: false },
|
||||
{ path: '/global-masters/languages', title: 'Language', type: 'link', dirchange: false },
|
||||
{ path: '/global-masters/timezones', title: 'Timezone', type: 'link', dirchange: false },
|
||||
{ path: '/global-masters/countries', title: 'Country', type: 'link', dirchange: false },
|
||||
{ path: '/global-masters/states', title: 'State', type: 'link', dirchange: false },
|
||||
{ path: '/global-masters/cities', title: 'City', type: 'link', dirchange: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Tenant Master',
|
||||
type: 'sub',
|
||||
active: false,
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/tenants', title: 'Tenants', type: 'link', dirchange: false },
|
||||
{ path: '/tenants/tenant-currencies', title: 'Tenant Currencies', type: 'link', dirchange: false }
|
||||
],
|
||||
},
|
||||
{ path: '/users', title: 'Users', type: 'link', dirchange: false },
|
||||
{
|
||||
title: 'Configuration',
|
||||
type: 'sub',
|
||||
@@ -75,4 +88,4 @@ export const SAAS_MENU_DATA: MenuContext = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { TokenStorageService } from '../auth/token-storage.service';
|
||||
import { MenuService } from './menu.service';
|
||||
|
||||
describe('MenuService dependency and authorization', () => {
|
||||
let menuService: MenuService;
|
||||
let tokenStorage: TokenStorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideHttpClient(), provideRouter([])],
|
||||
});
|
||||
menuService = TestBed.inject(MenuService);
|
||||
tokenStorage = TestBed.inject(TokenStorageService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
it('constructs MenuService and AuthService without a circular dependency', () => {
|
||||
expect(menuService).toBeTruthy();
|
||||
expect(TestBed.inject(AuthService)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('hides the Users menu when the stored user is not a super administrator', async () => {
|
||||
tokenStorage.saveAuth(
|
||||
{ accessToken: 'token', user: { id: '1', email: 'user@example.com', roles: ['admin'] } },
|
||||
false,
|
||||
);
|
||||
|
||||
const context = await firstValueFrom(menuService.loadMenu());
|
||||
|
||||
expect(hasPath(context.items, '/users')).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the Users menu when the stored user is a super administrator', async () => {
|
||||
tokenStorage.saveAuth(
|
||||
{
|
||||
accessToken: 'token',
|
||||
user: { id: '1', email: 'super@example.com', roles: ['super_admin'] },
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
const context = await firstValueFrom(menuService.loadMenu());
|
||||
|
||||
expect(hasPath(context.items, '/users')).toBe(true);
|
||||
});
|
||||
|
||||
it('clears menu state during context cleanup', async () => {
|
||||
await firstValueFrom(menuService.loadMenu());
|
||||
|
||||
menuService.clear();
|
||||
|
||||
expect(menuService.menuContext()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function hasPath(items: ReturnType<MenuService['getNavigationMenu']>, path: string): boolean {
|
||||
return items.some(
|
||||
(item) =>
|
||||
item.path === path ||
|
||||
(item.children ? hasPath(item.children, path) : false) ||
|
||||
(item.children2 ? hasPath(item.children2, path) : false),
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { Observable, of, tap } from 'rxjs';
|
||||
import { MenuContext } from '../../models/context/context.model';
|
||||
import { SAAS_MENU_DATA } from './menu.data';
|
||||
import { Menu } from '../../../core/services/common/nav.service';
|
||||
import { TokenStorageService } from '../auth/token-storage.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MenuService {
|
||||
private readonly tokenStorage = inject(TokenStorageService);
|
||||
readonly menuContext = signal<MenuContext | null>(null);
|
||||
|
||||
loadMenu(): Observable<MenuContext> {
|
||||
const context = this.cloneMenuContext(SAAS_MENU_DATA);
|
||||
context.items = this.filterAuthorizedItems(context.items);
|
||||
return of(context).pipe(
|
||||
tap((menuContext) => {
|
||||
this.menuContext.set(menuContext);
|
||||
@@ -39,4 +42,15 @@ export class MenuService {
|
||||
private cloneMenuItems(items: Menu[]): Menu[] {
|
||||
return JSON.parse(JSON.stringify(items)) as Menu[];
|
||||
}
|
||||
}
|
||||
|
||||
private filterAuthorizedItems(items: Menu[]): Menu[] {
|
||||
const isSuperAdmin = (this.tokenStorage.getUser()?.roles ?? []).includes('super_admin');
|
||||
return items
|
||||
.filter(item => isSuperAdmin || item.path !== '/users')
|
||||
.map(item => ({
|
||||
...item,
|
||||
children: item.children ? this.filterAuthorizedItems(item.children) : item.children,
|
||||
children2: item.children2 ? this.filterAuthorizedItems(item.children2) : item.children2
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { COUNTRY_ENDPOINTS } from "../../../core/end-points/country/country.endpoints";
|
||||
import { Observable } from "rxjs";
|
||||
import { DataTableQuery, DataTableResult } from "../../../shared/components/data-table/data-table.types";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class CountryService {
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
getCountryDataTable(query: DataTableQuery): Observable<DataTableResult<any>> {
|
||||
return this.http.post<DataTableResult<any>>(`${COUNTRY_ENDPOINTS.dataTable}`, query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Injectable, signal} from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LoadingService {
|
||||
|
||||
private requestCount = 0;
|
||||
readonly isLoading = signal(false);
|
||||
|
||||
show(): void {
|
||||
this.requestCount++;
|
||||
this.isLoading.set(true);
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this.requestCount--;
|
||||
|
||||
if (this.requestCount <= 0) {
|
||||
this.requestCount = 0;
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { MenuContext } from '../models/context.model';
|
||||
|
||||
export const SAAS_MENU_DATA: MenuContext = {
|
||||
defaultLandingPage: '/dashboards/crm',
|
||||
items: [
|
||||
{ headTitle: 'MAIN' },
|
||||
{
|
||||
title: 'Dashboards',
|
||||
icon: '<i class="bx bx-home side-menu__icon"></i>',
|
||||
type: 'sub',
|
||||
active: false,
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/dashboards/crm', title: 'CRM', type: 'link', dirchange: false },
|
||||
],
|
||||
},
|
||||
{ headTitle: 'SAAS ADMIN' },
|
||||
{
|
||||
title: 'Management',
|
||||
icon: '<i class="bx bx-buildings side-menu__icon"></i>',
|
||||
type: 'sub',
|
||||
active: false,
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/tenants', title: 'Tenants', type: 'link', dirchange: false },
|
||||
{ path: '/users', title: 'Users', type: 'link', dirchange: false },
|
||||
{
|
||||
title: 'Configuration',
|
||||
type: 'sub',
|
||||
active: false,
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/global-masters', title: 'Global Masters', type: 'link', dirchange: false },
|
||||
{ path: '/localization', title: 'Localization', type: 'link', dirchange: false },
|
||||
{
|
||||
title: 'Branding',
|
||||
type: 'sub',
|
||||
active: false,
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/theming', title: 'Theming', type: 'link', dirchange: false },
|
||||
{ path: '/platform', title: 'Platform', type: 'link', dirchange: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Operations',
|
||||
icon: '<i class="bx bx-line-chart side-menu__icon"></i>',
|
||||
type: 'sub',
|
||||
active: false,
|
||||
selected: false,
|
||||
dirchange: false,
|
||||
children: [
|
||||
{ path: '/billing', title: 'Billing', type: 'link', dirchange: false },
|
||||
{ path: '/monitoring', title: 'Monitoring', type: 'link', dirchange: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { Observable, of, tap } from 'rxjs';
|
||||
import { MenuContext } from '../models/context.model';
|
||||
import { SAAS_MENU_DATA } from './menu.data';
|
||||
import { Menu } from '../../shared/services/nav.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MenuService {
|
||||
readonly menuContext = signal<MenuContext | null>(null);
|
||||
|
||||
loadMenu(): Observable<MenuContext> {
|
||||
const context = this.cloneMenuContext(SAAS_MENU_DATA);
|
||||
return of(context).pipe(
|
||||
tap((menuContext) => {
|
||||
this.menuContext.set(menuContext);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getNavigationMenu(): Menu[] {
|
||||
return this.cloneMenuItems(this.menuContext()?.items ?? []);
|
||||
}
|
||||
|
||||
getDefaultLandingPage(): string | null {
|
||||
return this.menuContext()?.defaultLandingPage ?? null;
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.menuContext.set(null);
|
||||
}
|
||||
|
||||
private cloneMenuContext(context: MenuContext): MenuContext {
|
||||
return {
|
||||
defaultLandingPage: context.defaultLandingPage,
|
||||
items: this.cloneMenuItems(context.items),
|
||||
};
|
||||
}
|
||||
|
||||
private cloneMenuItems(items: Menu[]): Menu[] {
|
||||
return JSON.parse(JSON.stringify(items)) as Menu[];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { map, Observable, tap } from 'rxjs';
|
||||
import { API_CONFIG } from '../config/api.config';
|
||||
import { PermissionContext } from '../models/context.model';
|
||||
|
||||
interface PermissionResponse {
|
||||
roles?: string[];
|
||||
permissions?: string[];
|
||||
defaultLandingPage?: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PermissionService {
|
||||
private readonly http = inject(HttpClient);
|
||||
readonly permissionContext = signal<PermissionContext | null>(null);
|
||||
|
||||
loadPermissions(): Observable<PermissionContext> {
|
||||
return this.http.get<PermissionResponse>(`${API_CONFIG.baseUrl}${API_CONFIG.endpoints.permissionContext}`).pipe(
|
||||
map((response) => ({
|
||||
roles: response.roles ?? [],
|
||||
permissions: response.permissions ?? [],
|
||||
defaultLandingPage: response.defaultLandingPage?.trim() || undefined,
|
||||
})),
|
||||
tap((context) => {
|
||||
this.permissionContext.set(context);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.permissionContext.set(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { DestroyRef, effect, inject, Injectable, OnDestroy, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { merge, fromEvent, Subscription } from 'rxjs';
|
||||
import { throttleTime } from 'rxjs/operators';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class SessionTimeoutService implements OnDestroy {
|
||||
private readonly document = inject(DOCUMENT);
|
||||
private readonly router = inject(Router);
|
||||
private readonly authService = inject(AuthService);
|
||||
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 activitySubscription: Subscription | null = null;
|
||||
private warningTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private logoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||
private logoutDeadline = 0;
|
||||
private trackingEnabled = false;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.authService.currentUserSignal()) {
|
||||
this.start();
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
});
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.trackingEnabled) {
|
||||
this.resetTimers();
|
||||
return;
|
||||
}
|
||||
|
||||
this.trackingEnabled = true;
|
||||
this.bindActivityTracking();
|
||||
this.resetTimers();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.trackingEnabled = false;
|
||||
this.showWarning.set(false);
|
||||
this.remainingSeconds.set(0);
|
||||
this.clearTimers();
|
||||
this.activitySubscription?.unsubscribe();
|
||||
this.activitySubscription = null;
|
||||
}
|
||||
|
||||
staySignedIn(): void {
|
||||
if (!this.trackingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetTimers();
|
||||
}
|
||||
|
||||
logoutNow(): void {
|
||||
this.handleTimeoutLogout();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
private bindActivityTracking(): void {
|
||||
if (this.activitySubscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.activitySubscription = merge(
|
||||
fromEvent(this.document, 'mousemove'),
|
||||
fromEvent(this.document, 'keydown'),
|
||||
fromEvent(this.document, 'click'),
|
||||
fromEvent(window, 'scroll')
|
||||
)
|
||||
.pipe(throttleTime(1000), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => {
|
||||
if (!this.trackingEnabled || !this.authService.currentUserSignal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resetTimers();
|
||||
});
|
||||
}
|
||||
|
||||
private resetTimers(): void {
|
||||
if (!this.trackingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.clearTimers();
|
||||
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);
|
||||
|
||||
this.warningTimer = setTimeout(() => {
|
||||
this.showWarning.set(true);
|
||||
this.logoutDeadline = Date.now() + Math.max(safeLogoutDelay - safeWarningDelay, 0);
|
||||
this.updateRemainingSeconds();
|
||||
this.countdownTimer = setInterval(() => {
|
||||
this.updateRemainingSeconds();
|
||||
}, 1000);
|
||||
}, safeWarningDelay);
|
||||
|
||||
this.logoutTimer = setTimeout(() => {
|
||||
this.handleTimeoutLogout();
|
||||
}, safeLogoutDelay);
|
||||
}
|
||||
|
||||
private updateRemainingSeconds(): void {
|
||||
const remainingMs = Math.max(this.logoutDeadline - Date.now(), 0);
|
||||
this.remainingSeconds.set(Math.ceil(remainingMs / 1000));
|
||||
}
|
||||
|
||||
private handleTimeoutLogout(): void {
|
||||
const returnUrl = this.router.url.startsWith('/auth') ? '/' : this.router.url;
|
||||
this.stop();
|
||||
this.authService.logout();
|
||||
void this.router.navigate(['/auth/login'], {
|
||||
queryParams: returnUrl && returnUrl !== '/' ? { returnUrl } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private clearTimers(): void {
|
||||
if (this.warningTimer) {
|
||||
clearTimeout(this.warningTimer);
|
||||
this.warningTimer = null;
|
||||
}
|
||||
|
||||
if (this.logoutTimer) {
|
||||
clearTimeout(this.logoutTimer);
|
||||
this.logoutTimer = null;
|
||||
}
|
||||
|
||||
if (this.countdownTimer) {
|
||||
clearInterval(this.countdownTimer);
|
||||
this.countdownTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { Injectable, inject } from "@angular/core";
|
||||
import { STATE_ENDPOINTS } from "../../end-points/state/state.endpoints"
|
||||
import { Observable } from "rxjs";
|
||||
import { DataTableQuery, DataTableResult } from "../../../shared/components/data-table/data-table.types";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class StateService {
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
getStateDataTable(query: DataTableQuery, countryId: string): Observable<DataTableResult<any>> {
|
||||
return this.http.post<DataTableResult<any>>(`${STATE_ENDPOINTS.dataTable}`, { ...query, countryId });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { API_CONFIG } from '../config/api.config';
|
||||
import { TenantContext } from '../models/context.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class TenantContextService {
|
||||
private readonly http = inject(HttpClient);
|
||||
readonly tenantContext = signal<TenantContext | null>(null);
|
||||
|
||||
loadTenantContext(): Observable<TenantContext> {
|
||||
return this.http.get<TenantContext>(`${API_CONFIG.baseUrl}${API_CONFIG.endpoints.tenantContext}`).pipe(
|
||||
tap((context) => {
|
||||
this.tenantContext.set(context);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.tenantContext.set(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject, signal } from '@angular/core';
|
||||
import { map, Observable, tap } from 'rxjs';
|
||||
import { API_CONFIG } from '../config/api.config';
|
||||
import { CurrentUserContext } from '../models/context.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class UserContextService {
|
||||
private readonly http = inject(HttpClient);
|
||||
readonly currentUserContext = signal<CurrentUserContext | null>(null);
|
||||
|
||||
loadCurrentUserProfile(): Observable<CurrentUserContext> {
|
||||
return this.http.get<Partial<CurrentUserContext>>(`${API_CONFIG.baseUrl}${API_CONFIG.endpoints.currentUserProfile}`).pipe(
|
||||
map((response) => ({
|
||||
id: response.id?.trim() ?? '',
|
||||
email: response.email?.trim() ?? '',
|
||||
displayName: response.displayName?.trim() || undefined,
|
||||
fullName: response.fullName?.trim() || undefined,
|
||||
tenantId: response.tenantId?.trim() || undefined,
|
||||
roles: response.roles ?? [],
|
||||
defaultLandingPage: response.defaultLandingPage?.trim() || undefined,
|
||||
})),
|
||||
tap((profile) => {
|
||||
this.currentUserContext.set(profile);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.currentUserContext.set(null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user