add tenant and user endpoints, services, models and guards

This commit is contained in:
Gagan7900
2026-07-18 16:38:41 +05:30
parent be86ab3564
commit 7c4e34e478
50 changed files with 3064 additions and 451 deletions
+12 -2
View File
@@ -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',
@@ -41,6 +39,18 @@ export const SAAS_MENU_DATA: MenuContext = {
{ 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',
@@ -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),
);
}
+16 -2
View File
@@ -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
}));
}
}
@@ -0,0 +1,47 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { TENANT_CURRENCIES_ENDPOINTS } from '../../end-points/tenant/tenant-currencies.endpoints';
import { DataTableQuery, DataTableResult } from '../../../shared/components/data-table/data-table.types';
import {
CreateTenantCurrencyRequest,
TenantCurrencyDto,
TenantCurrencyDto as TenantCurrencyDtoAlias,
TenantCurrencyLookupDto,
UpdateTenantCurrencyRequest
} from '../../models/tenant/tenant-currencies.model';
@Injectable({
providedIn: 'root'
})
export class TenantCurrenciesService {
private readonly http = inject(HttpClient);
getTenantDataTable(query: DataTableQuery): Observable<DataTableResult<TenantCurrencyDto>> {
return this.http.post<DataTableResult<TenantCurrencyDto>>(TENANT_CURRENCIES_ENDPOINTS.dataTable, query);
}
createTenant(request: CreateTenantCurrencyRequest): Observable<TenantCurrencyDto> {
return this.http.post<TenantCurrencyDto>(TENANT_CURRENCIES_ENDPOINTS.create, request);
}
updateTenant(id: string, request: UpdateTenantCurrencyRequest): Observable<TenantCurrencyDto> {
return this.http.put<TenantCurrencyDto>(TENANT_CURRENCIES_ENDPOINTS.update(id), request);
}
getTenantById(id: string): Observable<TenantCurrencyDto> {
return this.http.get<TenantCurrencyDto>(TENANT_CURRENCIES_ENDPOINTS.getById(id));
}
autocomplete(term?: string, limit = 10): Observable<readonly TenantCurrencyLookupDto[]> {
let params = new HttpParams().set('limit', limit);
const normalizedTerm = term?.trim();
if (normalizedTerm) {
params = params.set('term', normalizedTerm);
}
return this.http.get<readonly TenantCurrencyLookupDto[]>(TENANT_CURRENCIES_ENDPOINTS.autocomplete, { params });
}
}
@@ -0,0 +1,46 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { TENANT_ENDPOINTS } from '../../end-points/tenant/tenant.endpoints';
import { DataTableQuery, DataTableResult } from '../../../shared/components/data-table/data-table.types';
import {
CreateTenantRequest,
TenantDto,
TenantLookupDto,
UpdateTenantRequest
} from '../../models/tenant/tenant.model';
@Injectable({
providedIn: 'root'
})
export class TenantService {
private readonly http = inject(HttpClient);
getTenantDataTable(query: DataTableQuery): Observable<DataTableResult<TenantDto>> {
return this.http.post<DataTableResult<TenantDto>>(TENANT_ENDPOINTS.dataTable, query);
}
createTenant(request: CreateTenantRequest): Observable<TenantDto> {
return this.http.post<TenantDto>(TENANT_ENDPOINTS.create, request);
}
updateTenant(id: string, request: UpdateTenantRequest): Observable<TenantDto> {
return this.http.put<TenantDto>(TENANT_ENDPOINTS.update(id), request);
}
getTenantById(id: string): Observable<TenantDto> {
return this.http.get<TenantDto>(TENANT_ENDPOINTS.getById(id));
}
autocomplete(term?: string, limit = 10): Observable<readonly TenantLookupDto[]> {
let params = new HttpParams().set('limit', limit);
const normalizedTerm = term?.trim();
if (normalizedTerm) {
params = params.set('term', normalizedTerm);
}
return this.http.get<readonly TenantLookupDto[]>(TENANT_ENDPOINTS.autocomplete, { params });
}
}
@@ -0,0 +1,40 @@
import { inject, Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { CreateUserRequest, UpdateUserRequest, UserDto, UserLookupDto } from '../../models/user/user.model';
import { USER_ENDPOINTS } from '../../end-points/user/user.endpoints';
import { DataTableQuery, DataTableResult } from '../../../shared/components/data-table/data-table.types';
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly http = inject(HttpClient);
createUser(request: CreateUserRequest): Observable<UserDto> {
return this.http.post<UserDto>(USER_ENDPOINTS.create, request);
}
getById(id: string): Observable<UserDto> {
return this.http.get<UserDto>(USER_ENDPOINTS.getById(id));
}
getDataTable(query: DataTableQuery): Observable<DataTableResult<UserDto>> {
return this.http.post<DataTableResult<UserDto>>(USER_ENDPOINTS.dataTable, query);
}
update(id: string, request: UpdateUserRequest): Observable<UserDto> {
return this.http.put<UserDto>(USER_ENDPOINTS.update(id), request);
}
autocomplete(term: string | null, limit = 10): Observable<readonly UserLookupDto[]> {
const normalizedTerm = term?.trim() || null;
let params = new HttpParams().set('limit', limit);
if (normalizedTerm !== null) {
params = params.set('term', normalizedTerm);
}
return this.http.get<readonly UserLookupDto[]>(USER_ENDPOINTS.autocomplete, { params });
}
}