Split admin services from authorization and add session checks
This commit is contained in:
parent
5f405b69a0
commit
52333b8601
|
|
@ -1,7 +1,8 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { adminGuard } from './admin-guard';
|
||||
import { firstValueFrom, Observable, of } from 'rxjs';
|
||||
|
||||
describe('adminGuard', () => {
|
||||
let authorizationSpy: jasmine.SpyObj<Authorization>;
|
||||
|
|
@ -14,8 +15,9 @@ describe('adminGuard', () => {
|
|||
const dummyState = {} as RouterStateSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
authorizationSpy = jasmine.createSpyObj('Authorization', ['isAdmin']);
|
||||
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
authorizationSpy = jasmine.createSpyObj('Authorization', ['validateSession']);
|
||||
routerSpy = jasmine.createSpyObj('Router', ['parseUrl']);
|
||||
routerSpy.parseUrl.and.returnValue({} as UrlTree);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
|
@ -25,17 +27,27 @@ describe('adminGuard', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should allow access when user is admin', () => {
|
||||
authorizationSpy.isAdmin.and.returnValue(true);
|
||||
const result = executeGuard(dummyRoute, dummyState);
|
||||
it('should allow access when user is admin', async () => {
|
||||
authorizationSpy.validateSession.and.returnValue(of({
|
||||
username: 'adminuser',
|
||||
email: 'admin@example.com',
|
||||
role: 'ADMIN'
|
||||
}));
|
||||
const result = await firstValueFrom(executeGuard(dummyRoute, dummyState) as Observable<boolean | UrlTree>);
|
||||
expect(result).toBeTrue();
|
||||
expect(routerSpy.navigate).not.toHaveBeenCalled();
|
||||
expect(routerSpy.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny access and redirect to / when user is not admin', () => {
|
||||
authorizationSpy.isAdmin.and.returnValue(false);
|
||||
const result = executeGuard(dummyRoute, dummyState);
|
||||
expect(result).toBeFalse();
|
||||
expect(routerSpy.navigate).toHaveBeenCalledWith(['/']);
|
||||
it('should deny access and redirect to / when user is not admin', async () => {
|
||||
const homeUrlTree = {} as UrlTree;
|
||||
authorizationSpy.validateSession.and.returnValue(of({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
role: 'USER'
|
||||
}));
|
||||
routerSpy.parseUrl.and.returnValue(homeUrlTree);
|
||||
const result = await firstValueFrom(executeGuard(dummyRoute, dummyState) as Observable<boolean | UrlTree>);
|
||||
expect(result).toBe(homeUrlTree);
|
||||
expect(routerSpy.parseUrl).toHaveBeenCalledWith('/');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,20 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
export const adminGuard: CanActivateFn = (_route, state) => {
|
||||
const authorization = inject(Authorization);
|
||||
const router = inject(Router);
|
||||
|
||||
if (authorization.isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
return authorization.validateSession().pipe(
|
||||
map((user) => {
|
||||
if (user?.role === 'ADMIN') {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`[adminGuard] Access denied to ${state.url} — user is not admin, redirecting to /`);
|
||||
router.navigate(['/']);
|
||||
return false;
|
||||
console.warn(`[adminGuard] Access denied to ${state.url} — user is not admin, redirecting to /`);
|
||||
return router.parseUrl('/');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { CanActivateFn, Router, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { authGuard } from './auth-guard';
|
||||
import { firstValueFrom, Observable, of } from 'rxjs';
|
||||
|
||||
describe('authGuard', () => {
|
||||
let authorizationSpy: jasmine.SpyObj<Authorization>;
|
||||
|
|
@ -14,8 +15,9 @@ describe('authGuard', () => {
|
|||
const dummyState = {} as RouterStateSnapshot;
|
||||
|
||||
beforeEach(() => {
|
||||
authorizationSpy = jasmine.createSpyObj('Authorization', ['isLoggedIn']);
|
||||
routerSpy = jasmine.createSpyObj('Router', ['navigate']);
|
||||
authorizationSpy = jasmine.createSpyObj('Authorization', ['validateSession']);
|
||||
routerSpy = jasmine.createSpyObj('Router', ['parseUrl']);
|
||||
routerSpy.parseUrl.and.returnValue({} as UrlTree);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
|
@ -25,17 +27,23 @@ describe('authGuard', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should allow access when logged in', () => {
|
||||
authorizationSpy.isLoggedIn.and.returnValue(true);
|
||||
const result = executeGuard(dummyRoute, dummyState);
|
||||
it('should allow access when logged in', async () => {
|
||||
authorizationSpy.validateSession.and.returnValue(of({
|
||||
username: 'testuser',
|
||||
email: 'test@example.com',
|
||||
role: 'USER'
|
||||
}));
|
||||
const result = await firstValueFrom(executeGuard(dummyRoute, dummyState) as Observable<boolean | UrlTree>);
|
||||
expect(result).toBeTrue();
|
||||
expect(routerSpy.navigate).not.toHaveBeenCalled();
|
||||
expect(routerSpy.parseUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should deny access and redirect to /login when not logged in', () => {
|
||||
authorizationSpy.isLoggedIn.and.returnValue(false);
|
||||
const result = executeGuard(dummyRoute, dummyState);
|
||||
expect(result).toBeFalse();
|
||||
expect(routerSpy.navigate).toHaveBeenCalledWith(['/login']);
|
||||
it('should deny access and redirect to /login when not logged in', async () => {
|
||||
const loginUrlTree = {} as UrlTree;
|
||||
authorizationSpy.validateSession.and.returnValue(of(null));
|
||||
routerSpy.parseUrl.and.returnValue(loginUrlTree);
|
||||
const result = await firstValueFrom(executeGuard(dummyRoute, dummyState) as Observable<boolean | UrlTree>);
|
||||
expect(result).toBe(loginUrlTree);
|
||||
expect(routerSpy.parseUrl).toHaveBeenCalledWith('/login');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn, Router } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { map } from 'rxjs';
|
||||
|
||||
export const authGuard: CanActivateFn = (_route, state) => {
|
||||
const authService = inject(Authorization);
|
||||
const router = inject(Router);
|
||||
if (authService.isLoggedIn()) {
|
||||
return true;
|
||||
} else {
|
||||
|
||||
return authService.validateSession().pipe(
|
||||
map((user) => {
|
||||
if (user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.warn(`[authGuard] Access denied to ${state.url} — user not logged in, redirecting to /login`);
|
||||
router.navigate(['/login']);
|
||||
return false;
|
||||
}
|
||||
return router.parseUrl('/login');
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ export const authTokenInterceptor: HttpInterceptorFn = (req, next) => {
|
|||
const requestPath = req.url.split('?')[0];
|
||||
const isAuthEndpoint =
|
||||
requestPath.endsWith('/auth/login') ||
|
||||
requestPath.endsWith('/auth/register');
|
||||
requestPath.endsWith('/auth/register') ||
|
||||
requestPath.endsWith('/auth/me');
|
||||
|
||||
return next(req).pipe(
|
||||
catchError((error: unknown) => {
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ export type UserStatistics = {
|
|||
executionsSucceeded: number;
|
||||
executionsFailed: number;
|
||||
simulationsStarted: number;
|
||||
loginCount: number;
|
||||
avgSessionDurationSeconds: number;
|
||||
lastLoginAt: string | null;
|
||||
lastFlowUpdateAt: string | null;
|
||||
lastExecutionAt: number | null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { MatInputModule } from '@angular/material/input';
|
|||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { AdminCreateUserRequest, UserRole } from '@models/user';
|
||||
import { Router } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { AdminService } from '@services/admin/admin';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChecks } from '@utilities/password-validation';
|
||||
|
||||
|
|
@ -32,7 +32,7 @@ import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChec
|
|||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminCreateUserPage extends FormUtility {
|
||||
private authorization = inject(Authorization);
|
||||
private adminService = inject(AdminService);
|
||||
private router = inject(Router);
|
||||
|
||||
readonly createError = signal<string | null>(null);
|
||||
|
|
@ -92,7 +92,7 @@ export class AdminCreateUserPage extends FormUtility {
|
|||
this.createEmailError.set(null);
|
||||
this.createPasswordError.set(null);
|
||||
|
||||
this.authorization.createAdminUser(this.createModel()).subscribe({
|
||||
this.adminService.createAdminUser(this.createModel()).subscribe({
|
||||
next: () => {
|
||||
this.createSaving.set(false);
|
||||
this.successMessage.set('User created successfully.');
|
||||
|
|
|
|||
|
|
@ -69,6 +69,14 @@
|
|||
<div class="admin-stats-metric__value">{{ currentStats.simulationsStarted }}</div>
|
||||
<div class="admin-stats-metric__meta">Started in simulation mode</div>
|
||||
</mat-card>
|
||||
|
||||
@if (selectedUsername() && selectedUserStats(); as userStats) {
|
||||
<mat-card class="admin-stats-metric">
|
||||
<div class="admin-stats-metric__label">Authentications</div>
|
||||
<div class="admin-stats-metric__value">{{ userStats.loginCount }}</div>
|
||||
<div class="admin-stats-metric__meta">Average session {{ formatDuration(userStats.avgSessionDurationSeconds) }}</div>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-card class="admin-stats-card">
|
||||
|
|
@ -83,6 +91,11 @@
|
|||
@if (!selectedUsername() && operationsStats(); as overviewStats) {
|
||||
<div><span class="admin-stats-details__label">Users count:</span> {{ overviewStats.usersCount }}</div>
|
||||
}
|
||||
@if (selectedUsername() && selectedUserStats(); as userStats) {
|
||||
<div><span class="admin-stats-details__label">Login count:</span> {{ userStats.loginCount }}</div>
|
||||
<div><span class="admin-stats-details__label">Average session duration:</span> {{ formatDuration(userStats.avgSessionDurationSeconds) }}</div>
|
||||
<div><span class="admin-stats-details__label">Last login:</span> {{ userStats.lastLoginAt ? (userStats.lastLoginAt | date:'dd/MM/yyyy HH:mm') : 'N/A' }}</div>
|
||||
}
|
||||
<div><span class="admin-stats-details__label">Last flow update:</span> {{ currentStats.lastFlowUpdateAt ? (currentStats.lastFlowUpdateAt | date:'dd/MM/yyyy HH:mm') : 'N/A' }}</div>
|
||||
<div><span class="admin-stats-details__label">Last execution:</span> {{ currentStats.lastExecutionAt ? (currentStats.lastExecutionAt | date:'dd/MM/yyyy HH:mm') : 'N/A' }}</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
|||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { OperationsStatistics, UserStatistics } from '@models/user';
|
||||
import { AdminService } from '@services/admin/admin';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
|
||||
@Component({
|
||||
|
|
@ -25,6 +26,7 @@ import { Authorization } from '@services/authorization/authorization';
|
|||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminStatsPage {
|
||||
private adminService = inject(AdminService);
|
||||
private authorization = inject(Authorization);
|
||||
|
||||
readonly users = signal<string[]>([]);
|
||||
|
|
@ -47,6 +49,23 @@ export class AdminStatsPage {
|
|||
this.selectedUsername() ? this.selectedUserStats() : this.operationsStats()
|
||||
);
|
||||
|
||||
formatDuration(totalSeconds: number | null | undefined): string {
|
||||
const seconds = Math.max(0, Math.trunc(Number(totalSeconds ?? 0)));
|
||||
if (!seconds) return '0m';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
return `${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.loadUsers();
|
||||
this.loadOperationsStats();
|
||||
|
|
@ -56,7 +75,7 @@ export class AdminStatsPage {
|
|||
if (!this.authorization.isAdmin()) return;
|
||||
this.loadingUsers.set(true);
|
||||
this.pageError.set(null);
|
||||
this.authorization.listStatisticsUsers().subscribe({
|
||||
this.adminService.listStatisticsUsers().subscribe({
|
||||
next: (users) => {
|
||||
this.users.set(users);
|
||||
this.loadingUsers.set(false);
|
||||
|
|
@ -73,7 +92,7 @@ export class AdminStatsPage {
|
|||
if (!this.authorization.isAdmin()) return;
|
||||
this.loadingOverview.set(true);
|
||||
this.statsError.set(null);
|
||||
this.authorization.getOperationsStatistics().subscribe({
|
||||
this.adminService.getOperationsStatistics().subscribe({
|
||||
next: (stats) => {
|
||||
this.operationsStats.set(stats);
|
||||
this.loadingOverview.set(false);
|
||||
|
|
@ -104,7 +123,7 @@ export class AdminStatsPage {
|
|||
|
||||
this.loadingUserStats.set(true);
|
||||
this.statsError.set(null);
|
||||
this.authorization.getUserStatistics(username).subscribe({
|
||||
this.adminService.getUserStatistics(username).subscribe({
|
||||
next: (stats) => {
|
||||
this.selectedUserStats.set(stats);
|
||||
this.loadingUserStats.set(false);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { MatIconModule } from '@angular/material/icon';
|
|||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { AdminUser, UserRole } from '@models/user';
|
||||
import { Router } from '@angular/router';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { AdminService } from '@services/admin/admin';
|
||||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
import { AdminResetPasswordDialogComponent } from '@shared/admin-reset-password-dialog/admin-reset-password-dialog';
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ import { AdminResetPasswordDialogComponent } from '@shared/admin-reset-password-
|
|||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminUsersListPage {
|
||||
private authorization = inject(Authorization);
|
||||
private adminService = inject(AdminService);
|
||||
private confirmDialog = inject(ConfirmDialogService);
|
||||
private router = inject(Router);
|
||||
|
||||
|
|
@ -52,7 +52,7 @@ export class AdminUsersListPage {
|
|||
loadUsers() {
|
||||
this.loading.set(true);
|
||||
this.pageError.set(null);
|
||||
this.authorization.listAdminUsers().subscribe({
|
||||
this.adminService.listAdminUsers().subscribe({
|
||||
next: (users) => {
|
||||
this.users.set(users);
|
||||
this.roleDraftByUser.set(
|
||||
|
|
@ -86,7 +86,7 @@ export class AdminUsersListPage {
|
|||
|
||||
this.roleSavingByUser.update((current) => ({ ...current, [user.username]: true }));
|
||||
this.roleErrorByUser.update((current) => ({ ...current, [user.username]: null }));
|
||||
this.authorization.changeAdminUserRole(user.username, { role: nextRole }).subscribe({
|
||||
this.adminService.changeAdminUserRole(user.username, { role: nextRole }).subscribe({
|
||||
next: () => {
|
||||
this.roleSavingByUser.update((current) => ({ ...current, [user.username]: false }));
|
||||
this.successMessage.set(`Role updated for ${user.username}.`);
|
||||
|
|
@ -118,7 +118,7 @@ export class AdminUsersListPage {
|
|||
submitResetPassword(event: { username: string; newPassword: string }) {
|
||||
this.resetPasswordSaving.set(true);
|
||||
this.resetPasswordError.set(null);
|
||||
this.authorization.changeAdminUserPassword(event.username, { newPassword: event.newPassword }).subscribe({
|
||||
this.adminService.changeAdminUserPassword(event.username, { newPassword: event.newPassword }).subscribe({
|
||||
next: () => {
|
||||
this.resetPasswordSaving.set(false);
|
||||
this.resetPasswordDialogUser.set(null);
|
||||
|
|
@ -138,7 +138,7 @@ export class AdminUsersListPage {
|
|||
if (!confirmed) return;
|
||||
|
||||
this.deleteBusyByUser.update((current) => ({ ...current, [user.username]: true }));
|
||||
this.authorization.deleteAdminUser(user.username).subscribe({
|
||||
this.adminService.deleteAdminUser(user.username).subscribe({
|
||||
next: () => {
|
||||
this.deleteBusyByUser.update((current) => ({ ...current, [user.username]: false }));
|
||||
this.successMessage.set(`User ${user.username} deleted.`);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
AdminUser,
|
||||
UserRole
|
||||
} from '@models/user';
|
||||
import { Authorization } from '@services/authorization/authorization';
|
||||
import { AdminService } from '@services/admin/admin';
|
||||
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
|
||||
import { AdminResetPasswordDialogComponent } from '@shared/admin-reset-password-dialog/admin-reset-password-dialog';
|
||||
import { FormUtility } from '@utilities/form-utility';
|
||||
|
|
@ -38,7 +38,7 @@ import { hasValidPasswordComplexity, evaluatePasswordChecks, initialPasswordChec
|
|||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class AdminUsersPage extends FormUtility {
|
||||
private authorization = inject(Authorization);
|
||||
private adminService = inject(AdminService);
|
||||
private confirmDialog = inject(ConfirmDialogService);
|
||||
|
||||
readonly users = signal<AdminUser[]>([]);
|
||||
|
|
@ -106,7 +106,7 @@ export class AdminUsersPage extends FormUtility {
|
|||
loadUsers() {
|
||||
this.loading.set(true);
|
||||
this.pageError.set(null);
|
||||
this.authorization.listAdminUsers().subscribe({
|
||||
this.adminService.listAdminUsers().subscribe({
|
||||
next: (users) => {
|
||||
this.users.set(users);
|
||||
this.roleDraftByUser.set(
|
||||
|
|
@ -132,7 +132,7 @@ export class AdminUsersPage extends FormUtility {
|
|||
this.createEmailError.set(null);
|
||||
this.createPasswordError.set(null);
|
||||
|
||||
this.authorization.createAdminUser(this.createModel()).subscribe({
|
||||
this.adminService.createAdminUser(this.createModel()).subscribe({
|
||||
next: () => {
|
||||
this.createSaving.set(false);
|
||||
this.successMessage.set('User created successfully.');
|
||||
|
|
@ -189,7 +189,7 @@ export class AdminUsersPage extends FormUtility {
|
|||
|
||||
this.roleSavingByUser.update((current) => ({ ...current, [user.username]: true }));
|
||||
this.roleErrorByUser.update((current) => ({ ...current, [user.username]: null }));
|
||||
this.authorization.changeAdminUserRole(user.username, { role: nextRole }).subscribe({
|
||||
this.adminService.changeAdminUserRole(user.username, { role: nextRole }).subscribe({
|
||||
next: () => {
|
||||
this.roleSavingByUser.update((current) => ({ ...current, [user.username]: false }));
|
||||
this.successMessage.set(`Role updated for ${user.username}.`);
|
||||
|
|
@ -220,7 +220,7 @@ export class AdminUsersPage extends FormUtility {
|
|||
submitResetPassword(event: { username: string; newPassword: string }) {
|
||||
this.resetPasswordSaving.set(true);
|
||||
this.resetPasswordError.set(null);
|
||||
this.authorization.changeAdminUserPassword(event.username, { newPassword: event.newPassword }).subscribe({
|
||||
this.adminService.changeAdminUserPassword(event.username, { newPassword: event.newPassword }).subscribe({
|
||||
next: () => {
|
||||
this.resetPasswordSaving.set(false);
|
||||
this.resetPasswordDialogUser.set(null);
|
||||
|
|
@ -239,7 +239,7 @@ export class AdminUsersPage extends FormUtility {
|
|||
if (!confirmed) return;
|
||||
|
||||
this.deleteBusyByUser.update((current) => ({ ...current, [user.username]: true }));
|
||||
this.authorization.deleteAdminUser(user.username).subscribe({
|
||||
this.adminService.deleteAdminUser(user.username).subscribe({
|
||||
next: () => {
|
||||
this.deleteBusyByUser.update((current) => ({ ...current, [user.username]: false }));
|
||||
this.successMessage.set(`User ${user.username} deleted.`);
|
||||
|
|
|
|||
|
|
@ -51,6 +51,13 @@ export class Login extends FormUtility {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.authService.validateSession().subscribe({
|
||||
next: (user) => {
|
||||
if (user) {
|
||||
void this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
});
|
||||
const registered = history.state?.registered;
|
||||
if (registered) {
|
||||
this.registeredUser.set(registered);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
AdminUser,
|
||||
OperationsStatistics,
|
||||
UserStatistics
|
||||
} from "@models/user";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
export abstract class AdminCallServiceBase {
|
||||
abstract listAdminUsers(): Observable<AdminUser[]>;
|
||||
|
||||
abstract createAdminUser(request: AdminCreateUserRequest): Observable<void>;
|
||||
|
||||
abstract changeAdminUserPassword(username: string, request: AdminResetPasswordRequest): Observable<void>;
|
||||
|
||||
abstract changeAdminUserRole(username: string, request: AdminChangeRoleRequest): Observable<void>;
|
||||
|
||||
abstract deleteAdminUser(username: string): Observable<void>;
|
||||
|
||||
abstract getOperationsStatistics(): Observable<OperationsStatistics>;
|
||||
|
||||
abstract listStatisticsUsers(): Observable<string[]>;
|
||||
|
||||
abstract getUserStatistics(username: string): Observable<UserStatistics>;
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
import { Observable } from "rxjs";
|
||||
import { AdminCallServiceBase } from "./admin-call.base";
|
||||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
AdminUser,
|
||||
OperationsStatistics,
|
||||
UserRegistration,
|
||||
UserRole,
|
||||
UserStatistics
|
||||
} from "@models/user";
|
||||
|
||||
export class AdminCallFakeService extends AdminCallServiceBase {
|
||||
private users: Array<UserRegistration & { role: UserRole }> = [
|
||||
{ username: 'testuser', email: 'testuser@example.com', password: 'Password123!', role: 'USER' },
|
||||
{ username: 'adminuser', email: 'admin@example.com', password: 'Adminpass1!', role: 'ADMIN' },
|
||||
];
|
||||
|
||||
override listAdminUsers(): Observable<AdminUser[]> {
|
||||
return new Observable<AdminUser[]>((observer) => {
|
||||
observer.next(this.users.map((user) => ({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
})));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override createAdminUser(request: AdminCreateUserRequest): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
if (!request.username || !request.password || !request.email) {
|
||||
observer.error(new Error('username, password and email are required'));
|
||||
return;
|
||||
}
|
||||
if (this.users.find((user) => user.username === request.username)) {
|
||||
observer.error(new Error('the user already exists'));
|
||||
return;
|
||||
}
|
||||
this.users.push({
|
||||
username: request.username,
|
||||
password: request.password,
|
||||
email: request.email,
|
||||
role: request.role === 'ADMIN' ? 'ADMIN' : 'USER'
|
||||
});
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override changeAdminUserPassword(username: string, request: AdminResetPasswordRequest): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
const user = this.users.find((candidate) => candidate.username === username);
|
||||
if (!user) {
|
||||
observer.error(new Error(`User ${username} not found`));
|
||||
return;
|
||||
}
|
||||
if (!request.newPassword) {
|
||||
observer.error(new Error('username and newPassword are required'));
|
||||
return;
|
||||
}
|
||||
user.password = request.newPassword;
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override changeAdminUserRole(username: string, request: AdminChangeRoleRequest): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
const user = this.users.find((candidate) => candidate.username === username);
|
||||
if (!user) {
|
||||
observer.error(new Error(`User ${username} not found`));
|
||||
return;
|
||||
}
|
||||
const nextRole: UserRole = request.role === 'ADMIN' ? 'ADMIN' : request.role === 'USER' ? 'USER' : null as never;
|
||||
if (!nextRole) {
|
||||
observer.error(new Error('INVALID_ROLE'));
|
||||
return;
|
||||
}
|
||||
if (user.role === 'ADMIN' && nextRole !== 'ADMIN' && this.users.filter((candidate) => candidate.role === 'ADMIN').length === 1) {
|
||||
observer.error(new Error('LAST_ADMIN'));
|
||||
return;
|
||||
}
|
||||
user.role = nextRole;
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override deleteAdminUser(username: string): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
const userIndex = this.users.findIndex((candidate) => candidate.username === username);
|
||||
if (userIndex < 0) {
|
||||
observer.error(new Error(`User ${username} not found`));
|
||||
return;
|
||||
}
|
||||
if (this.users[userIndex].role === 'ADMIN' && this.users.filter((candidate) => candidate.role === 'ADMIN').length === 1) {
|
||||
observer.error(new Error('LAST_ADMIN'));
|
||||
return;
|
||||
}
|
||||
this.users.splice(userIndex, 1);
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override getOperationsStatistics(): Observable<OperationsStatistics> {
|
||||
return new Observable<OperationsStatistics>((observer) => {
|
||||
observer.next({
|
||||
usersCount: this.users.length,
|
||||
flowsCreated: 7,
|
||||
flowsPublished: 3,
|
||||
flowsFinalized: 0,
|
||||
executionsCreated: 0,
|
||||
executionsRunning: 0,
|
||||
executionsSucceeded: 0,
|
||||
executionsFailed: 0,
|
||||
simulationsStarted: 0,
|
||||
lastFlowUpdateAt: '2026-03-26T12:18:06.100822',
|
||||
lastExecutionAt: null
|
||||
});
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override listStatisticsUsers(): Observable<string[]> {
|
||||
return new Observable<string[]>((observer) => {
|
||||
observer.next(this.users.map((user) => user.username));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
override getUserStatistics(username: string): Observable<UserStatistics> {
|
||||
return new Observable<UserStatistics>((observer) => {
|
||||
const user = this.users.find((candidate) => candidate.username === username);
|
||||
if (!user) {
|
||||
observer.error(new Error('User not found'));
|
||||
return;
|
||||
}
|
||||
observer.next(this.buildStatistics(username));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
private buildStatistics(username: string): UserStatistics {
|
||||
const base = username.length;
|
||||
return {
|
||||
username,
|
||||
flowsCreated: base + 2,
|
||||
flowsPublished: Math.max(0, base - 2),
|
||||
flowsFinalized: Math.max(0, base - 4),
|
||||
executionsCreated: base * 3,
|
||||
executionsRunning: base % 3,
|
||||
executionsSucceeded: base * 2,
|
||||
executionsFailed: base % 5,
|
||||
simulationsStarted: Math.max(0, base - 5),
|
||||
loginCount: base + 1,
|
||||
avgSessionDurationSeconds: base * 180,
|
||||
lastLoginAt: '2026-04-15T08:10:00Z',
|
||||
lastFlowUpdateAt: '2026-03-26T10:15:30',
|
||||
lastExecutionAt: 1774520966139
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
AdminUser,
|
||||
OperationsStatistics,
|
||||
UserStatistics,
|
||||
UserRole
|
||||
} from "@models/user";
|
||||
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
|
||||
import { inject } from "@angular/core";
|
||||
import { environment } from "@environment";
|
||||
import { catchError, map, Observable, throwError } from "rxjs";
|
||||
import { AdminCallServiceBase } from "./admin-call.base";
|
||||
|
||||
export class AdminCallService extends AdminCallServiceBase {
|
||||
private readonly http = inject(HttpClient);
|
||||
|
||||
override listAdminUsers(): Observable<AdminUser[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/auth/admin/users`)
|
||||
.pipe(
|
||||
map((raw) => Array.isArray(raw) ? raw.map((item) => this.adminUserFromApi(item)) : []),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
403: 'Admin access required.'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override createAdminUser(request: AdminCreateUserRequest): Observable<void> {
|
||||
return this.http
|
||||
.post<void>(`${environment.apiUrl}/auth/admin/users`, request)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
400: 'Unable to create user.',
|
||||
403: 'Admin access required.'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override changeAdminUserPassword(username: string, request: AdminResetPasswordRequest): Observable<void> {
|
||||
return this.http
|
||||
.put<void>(`${environment.apiUrl}/auth/admin/users/${encodeURIComponent(username)}/password`, request)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
400: 'Unable to update password.',
|
||||
403: 'Admin access required.',
|
||||
404: `User ${username} not found`
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override changeAdminUserRole(username: string, request: AdminChangeRoleRequest): Observable<void> {
|
||||
return this.http
|
||||
.put<void>(`${environment.apiUrl}/auth/admin/users/${encodeURIComponent(username)}/role`, request)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
400: 'Unable to update role.',
|
||||
403: 'Admin access required.',
|
||||
404: `User ${username} not found`,
|
||||
409: 'LAST_ADMIN'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override deleteAdminUser(username: string): Observable<void> {
|
||||
return this.http
|
||||
.delete<void>(`${environment.apiUrl}/auth/admin/users/${encodeURIComponent(username)}`)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
403: 'Admin access required.',
|
||||
404: `User ${username} not found`,
|
||||
409: 'LAST_ADMIN'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override getOperationsStatistics(): Observable<OperationsStatistics> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/stats`)
|
||||
.pipe(
|
||||
map((raw) => this.operationsStatisticsFromApi(raw)),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated',
|
||||
403: 'You are not allowed to view user statistics'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override listStatisticsUsers(): Observable<string[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/stats/users`)
|
||||
.pipe(
|
||||
map((raw) => Array.isArray(raw)
|
||||
? raw.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter((item) => item.length > 0)
|
||||
: []),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated',
|
||||
403: 'You are not allowed to view user statistics'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override getUserStatistics(username: string): Observable<UserStatistics> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/stats/users/${encodeURIComponent(username)}`)
|
||||
.pipe(
|
||||
map((raw) => this.userStatisticsFromApi(raw, username)),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated',
|
||||
403: 'You are not allowed to view user statistics',
|
||||
404: 'User not found'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
private extractHttpErrorMessage(error: HttpErrorResponse): string | null {
|
||||
const payload = error.error;
|
||||
if (typeof payload === 'string' && payload.trim().length > 0) {
|
||||
return payload.trim();
|
||||
}
|
||||
if (payload && typeof payload === 'object') {
|
||||
const record = payload as Record<string, unknown>;
|
||||
const directMessage = record['message'];
|
||||
if (typeof directMessage === 'string' && directMessage.trim().length > 0) {
|
||||
return directMessage.trim();
|
||||
}
|
||||
const errorMessage = record['error'];
|
||||
if (typeof errorMessage === 'string' && errorMessage.trim().length > 0) {
|
||||
return errorMessage.trim();
|
||||
}
|
||||
const details = record['details'];
|
||||
if (typeof details === 'string' && details.trim().length > 0) {
|
||||
return details.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private normalizeRole(value: unknown): UserRole {
|
||||
return String(value ?? '').toUpperCase() === 'ADMIN' ? 'ADMIN' : 'USER';
|
||||
}
|
||||
|
||||
private adminUserFromApi(raw: unknown): AdminUser {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
username: String(value['username'] ?? ''),
|
||||
email: typeof value['email'] === 'string' ? value['email'] : null,
|
||||
role: this.normalizeRole(value['role'])
|
||||
};
|
||||
}
|
||||
|
||||
private userStatisticsFromApi(raw: unknown, username: string): UserStatistics {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
username: String(value['username'] ?? username),
|
||||
flowsCreated: Number(value['flowsCreated'] ?? 0),
|
||||
flowsPublished: Number(value['flowsPublished'] ?? 0),
|
||||
flowsFinalized: Number(value['flowsFinalized'] ?? 0),
|
||||
executionsCreated: Number(value['executionsCreated'] ?? 0),
|
||||
executionsRunning: Number(value['executionsRunning'] ?? 0),
|
||||
executionsSucceeded: Number(value['executionsSucceeded'] ?? 0),
|
||||
executionsFailed: Number(value['executionsFailed'] ?? 0),
|
||||
simulationsStarted: Number(value['simulationsStarted'] ?? 0),
|
||||
loginCount: Number(value['loginCount'] ?? 0),
|
||||
avgSessionDurationSeconds: Number(value['avgSessionDurationSeconds'] ?? 0),
|
||||
lastLoginAt: typeof value['lastLoginAt'] === 'string' ? value['lastLoginAt'] : null,
|
||||
lastFlowUpdateAt: typeof value['lastFlowUpdateAt'] === 'string' ? value['lastFlowUpdateAt'] : null,
|
||||
lastExecutionAt: typeof value['lastExecutionAt'] === 'number' ? value['lastExecutionAt'] : null
|
||||
};
|
||||
}
|
||||
|
||||
private operationsStatisticsFromApi(raw: unknown): OperationsStatistics {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
usersCount: Number(value['usersCount'] ?? 0),
|
||||
flowsCreated: Number(value['flowsCreated'] ?? 0),
|
||||
flowsPublished: Number(value['flowsPublished'] ?? 0),
|
||||
flowsFinalized: Number(value['flowsFinalized'] ?? 0),
|
||||
executionsCreated: Number(value['executionsCreated'] ?? 0),
|
||||
executionsRunning: Number(value['executionsRunning'] ?? 0),
|
||||
executionsSucceeded: Number(value['executionsSucceeded'] ?? 0),
|
||||
executionsFailed: Number(value['executionsFailed'] ?? 0),
|
||||
simulationsStarted: Number(value['simulationsStarted'] ?? 0),
|
||||
lastFlowUpdateAt: typeof value['lastFlowUpdateAt'] === 'string' ? value['lastFlowUpdateAt'] : null,
|
||||
lastExecutionAt: typeof value['lastExecutionAt'] === 'number' ? value['lastExecutionAt'] : null
|
||||
};
|
||||
}
|
||||
|
||||
private toHttpError(error: unknown, fallbackByStatus: Record<number, string>): Observable<never> {
|
||||
if (error instanceof HttpErrorResponse) {
|
||||
const message = this.extractHttpErrorMessage(error)
|
||||
?? fallbackByStatus[error.status]
|
||||
?? 'Request failed.';
|
||||
return throwError(() => new Error(message));
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return throwError(() => error);
|
||||
}
|
||||
return throwError(() => new Error('Request failed.'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
OperationsStatistics,
|
||||
UserStatistics
|
||||
} from '@models/user';
|
||||
import { environment } from '@environment';
|
||||
import { take } from 'rxjs';
|
||||
import { AdminCallServiceBase } from './admin-call.base';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AdminService {
|
||||
adminCall: AdminCallServiceBase = new environment.adminCallService();
|
||||
|
||||
listAdminUsers() {
|
||||
return this.adminCall.listAdminUsers().pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
createAdminUser(request: AdminCreateUserRequest) {
|
||||
return this.adminCall.createAdminUser(request).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
changeAdminUserPassword(username: string, request: AdminResetPasswordRequest) {
|
||||
return this.adminCall.changeAdminUserPassword(username, request).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
changeAdminUserRole(username: string, request: AdminChangeRoleRequest) {
|
||||
return this.adminCall.changeAdminUserRole(username, request).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
deleteAdminUser(username: string) {
|
||||
return this.adminCall.deleteAdminUser(username).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
getOperationsStatistics() {
|
||||
return this.adminCall.getOperationsStatistics().pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
listStatisticsUsers() {
|
||||
return this.adminCall.listStatisticsUsers().pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
getUserStatistics(username: string) {
|
||||
return this.adminCall.getUserStatistics(username).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,7 @@
|
|||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
AdminUser,
|
||||
ChangePasswordRequest,
|
||||
OperationsStatistics,
|
||||
User,
|
||||
UserRegistration,
|
||||
UserStatistics
|
||||
} from "@models/user";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
|
|
@ -15,26 +9,12 @@ export abstract class AuthorizationCallServiceBase {
|
|||
|
||||
abstract login(username: string, password: string): Observable<User>;
|
||||
|
||||
abstract currentUser(): Observable<User>;
|
||||
|
||||
abstract register(userRegistration: UserRegistration): Observable<void>;
|
||||
|
||||
abstract changePassword(request: ChangePasswordRequest): Observable<void>;
|
||||
|
||||
abstract listAdminUsers(): Observable<AdminUser[]>;
|
||||
|
||||
abstract createAdminUser(request: AdminCreateUserRequest): Observable<void>;
|
||||
|
||||
abstract changeAdminUserPassword(username: string, request: AdminResetPasswordRequest): Observable<void>;
|
||||
|
||||
abstract changeAdminUserRole(username: string, request: AdminChangeRoleRequest): Observable<void>;
|
||||
|
||||
abstract deleteAdminUser(username: string): Observable<void>;
|
||||
|
||||
abstract getOperationsStatistics(): Observable<OperationsStatistics>;
|
||||
|
||||
abstract listStatisticsUsers(): Observable<string[]>;
|
||||
|
||||
abstract getUserStatistics(username: string): Observable<UserStatistics>;
|
||||
|
||||
abstract logout(): Observable<void>;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,9 @@
|
|||
import { Observable, of } from "rxjs";
|
||||
import { Observable, of, throwError } from "rxjs";
|
||||
import { AuthorizationCallServiceBase } from "./authorization-call.base";
|
||||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
AdminUser,
|
||||
ChangePasswordRequest,
|
||||
OperationsStatistics,
|
||||
User,
|
||||
UserRegistration,
|
||||
UserStatistics,
|
||||
UserRole
|
||||
} from "@models/user";
|
||||
|
||||
|
|
@ -20,6 +14,7 @@ export class AuthorizationCallFakeService extends AuthorizationCallServiceBase {
|
|||
{ username: 'testuser', email: 'testuser@example.com', password: 'Password123!', role: 'USER' },
|
||||
{ username: 'adminuser', email: 'admin@example.com', password: 'Adminpass1!', role: 'ADMIN' },
|
||||
];
|
||||
private currentUsername: string | null = null;
|
||||
|
||||
login(username: string, password: string): Observable<User> {
|
||||
return new Observable<User>((observer) => {
|
||||
|
|
@ -37,10 +32,25 @@ export class AuthorizationCallFakeService extends AuthorizationCallServiceBase {
|
|||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
this.currentUsername = user.username;
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
currentUser(): Observable<User> {
|
||||
const user = this.currentUsername
|
||||
? this.users.find((candidate) => candidate.username === this.currentUsername)
|
||||
: null;
|
||||
if (!user) {
|
||||
return throwError(() => new Error('Unauthenticated'));
|
||||
}
|
||||
return of({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
});
|
||||
}
|
||||
|
||||
register(userRegistration: UserRegistration): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
setTimeout(() => {
|
||||
|
|
@ -77,150 +87,8 @@ export class AuthorizationCallFakeService extends AuthorizationCallServiceBase {
|
|||
});
|
||||
}
|
||||
|
||||
listAdminUsers(): Observable<AdminUser[]> {
|
||||
return new Observable<AdminUser[]>((observer) => {
|
||||
observer.next(this.users.map((user) => ({
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
role: user.role
|
||||
})));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
createAdminUser(request: AdminCreateUserRequest): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
if (!request.username || !request.password || !request.email) {
|
||||
observer.error(new Error('username, password and email are required'));
|
||||
return;
|
||||
}
|
||||
if (this.users.find((user) => user.username === request.username)) {
|
||||
observer.error(new Error('the user already exists'));
|
||||
return;
|
||||
}
|
||||
this.users.push({
|
||||
username: request.username,
|
||||
password: request.password,
|
||||
email: request.email,
|
||||
role: request.role === 'ADMIN' ? 'ADMIN' : 'USER'
|
||||
});
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
changeAdminUserPassword(username: string, request: AdminResetPasswordRequest): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
const user = this.users.find((candidate) => candidate.username === username);
|
||||
if (!user) {
|
||||
observer.error(new Error(`User ${username} not found`));
|
||||
return;
|
||||
}
|
||||
if (!request.newPassword) {
|
||||
observer.error(new Error('username and newPassword are required'));
|
||||
return;
|
||||
}
|
||||
user.password = request.newPassword;
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
changeAdminUserRole(username: string, request: AdminChangeRoleRequest): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
const user = this.users.find((candidate) => candidate.username === username);
|
||||
if (!user) {
|
||||
observer.error(new Error(`User ${username} not found`));
|
||||
return;
|
||||
}
|
||||
const nextRole: UserRole = request.role === 'ADMIN' ? 'ADMIN' : request.role === 'USER' ? 'USER' : null as never;
|
||||
if (!nextRole) {
|
||||
observer.error(new Error('INVALID_ROLE'));
|
||||
return;
|
||||
}
|
||||
if (user.role === 'ADMIN' && nextRole !== 'ADMIN' && this.users.filter((candidate) => candidate.role === 'ADMIN').length === 1) {
|
||||
observer.error(new Error('LAST_ADMIN'));
|
||||
return;
|
||||
}
|
||||
user.role = nextRole;
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
deleteAdminUser(username: string): Observable<void> {
|
||||
return new Observable<void>((observer) => {
|
||||
const userIndex = this.users.findIndex((candidate) => candidate.username === username);
|
||||
if (userIndex < 0) {
|
||||
observer.error(new Error(`User ${username} not found`));
|
||||
return;
|
||||
}
|
||||
if (this.users[userIndex].role === 'ADMIN' && this.users.filter((candidate) => candidate.role === 'ADMIN').length === 1) {
|
||||
observer.error(new Error('LAST_ADMIN'));
|
||||
return;
|
||||
}
|
||||
this.users.splice(userIndex, 1);
|
||||
observer.next();
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getOperationsStatistics(): Observable<OperationsStatistics> {
|
||||
return new Observable<OperationsStatistics>((observer) => {
|
||||
observer.next({
|
||||
usersCount: this.users.length,
|
||||
flowsCreated: 7,
|
||||
flowsPublished: 3,
|
||||
flowsFinalized: 0,
|
||||
executionsCreated: 0,
|
||||
executionsRunning: 0,
|
||||
executionsSucceeded: 0,
|
||||
executionsFailed: 0,
|
||||
simulationsStarted: 0,
|
||||
lastFlowUpdateAt: '2026-03-26T12:18:06.100822',
|
||||
lastExecutionAt: null
|
||||
});
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
listStatisticsUsers(): Observable<string[]> {
|
||||
return new Observable<string[]>((observer) => {
|
||||
observer.next(this.users.map((user) => user.username));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
getUserStatistics(username: string): Observable<UserStatistics> {
|
||||
return new Observable<UserStatistics>((observer) => {
|
||||
const user = this.users.find((candidate) => candidate.username === username);
|
||||
if (!user) {
|
||||
observer.error(new Error('User not found'));
|
||||
return;
|
||||
}
|
||||
observer.next(this.buildStatistics(username));
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
private buildStatistics(username: string): UserStatistics {
|
||||
const base = username.length;
|
||||
return {
|
||||
username,
|
||||
flowsCreated: base + 2,
|
||||
flowsPublished: Math.max(0, base - 2),
|
||||
flowsFinalized: Math.max(0, base - 4),
|
||||
executionsCreated: base * 3,
|
||||
executionsRunning: base % 3,
|
||||
executionsSucceeded: base * 2,
|
||||
executionsFailed: base % 5,
|
||||
simulationsStarted: Math.max(0, base - 5),
|
||||
lastFlowUpdateAt: '2026-03-26T10:15:30',
|
||||
lastExecutionAt: 1774520966139
|
||||
};
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
this.currentUsername = null;
|
||||
return of(undefined);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
import { AuthorizationCallServiceBase } from "./authorization-call.base";
|
||||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
AdminUser,
|
||||
ChangePasswordRequest,
|
||||
OperationsStatistics,
|
||||
User,
|
||||
UserRegistration,
|
||||
UserStatistics,
|
||||
UserRole
|
||||
} from "@models/user";
|
||||
import { catchError, map, Observable, of, throwError } from "rxjs";
|
||||
|
|
@ -23,19 +17,7 @@ export class AuthorizationCallService extends AuthorizationCallServiceBase {
|
|||
return this.http
|
||||
.post<unknown>(`${environment.apiUrl}/auth/login`, { username, password })
|
||||
.pipe(
|
||||
map((raw) => {
|
||||
const payload = (raw ?? {}) as Record<string, unknown>;
|
||||
const userSource =
|
||||
(payload["user"] as Record<string, unknown> | undefined)
|
||||
?? (payload["profile"] as Record<string, unknown> | undefined)
|
||||
?? payload;
|
||||
|
||||
return {
|
||||
username: String(userSource["username"] ?? username),
|
||||
email: typeof userSource["email"] === "string" ? String(userSource["email"]) : null,
|
||||
role: this.normalizeRole(userSource["role"])
|
||||
} satisfies User;
|
||||
}),
|
||||
map((raw) => this.userFromApi(raw, username)),
|
||||
catchError((error: unknown) => {
|
||||
if (error instanceof HttpErrorResponse && error.status === 404) {
|
||||
return throwError(() => new Error('User not found'));
|
||||
|
|
@ -47,6 +29,18 @@ export class AuthorizationCallService extends AuthorizationCallServiceBase {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
override currentUser(): Observable<User> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/auth/me`)
|
||||
.pipe(
|
||||
map((raw) => this.userFromApi(raw)),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override register(userRegistration: UserRegistration): Observable<void> {
|
||||
return this.http.post<void>(`${environment.apiUrl}/auth/register`, userRegistration)
|
||||
.pipe(
|
||||
|
|
@ -75,104 +69,6 @@ export class AuthorizationCallService extends AuthorizationCallServiceBase {
|
|||
);
|
||||
}
|
||||
|
||||
override listAdminUsers(): Observable<AdminUser[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/auth/admin/users`)
|
||||
.pipe(
|
||||
map((raw) => Array.isArray(raw) ? raw.map((item) => this.adminUserFromApi(item)) : []),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
403: 'Admin access required.'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override createAdminUser(request: AdminCreateUserRequest): Observable<void> {
|
||||
return this.http
|
||||
.post<void>(`${environment.apiUrl}/auth/admin/users`, request)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
400: 'Unable to create user.',
|
||||
403: 'Admin access required.'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override changeAdminUserPassword(username: string, request: AdminResetPasswordRequest): Observable<void> {
|
||||
return this.http
|
||||
.put<void>(`${environment.apiUrl}/auth/admin/users/${encodeURIComponent(username)}/password`, request)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
400: 'Unable to update password.',
|
||||
403: 'Admin access required.',
|
||||
404: `User ${username} not found`
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override changeAdminUserRole(username: string, request: AdminChangeRoleRequest): Observable<void> {
|
||||
return this.http
|
||||
.put<void>(`${environment.apiUrl}/auth/admin/users/${encodeURIComponent(username)}/role`, request)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
400: 'Unable to update role.',
|
||||
403: 'Admin access required.',
|
||||
404: `User ${username} not found`,
|
||||
409: 'LAST_ADMIN'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override deleteAdminUser(username: string): Observable<void> {
|
||||
return this.http
|
||||
.delete<void>(`${environment.apiUrl}/auth/admin/users/${encodeURIComponent(username)}`)
|
||||
.pipe(
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
403: 'Admin access required.',
|
||||
404: `User ${username} not found`,
|
||||
409: 'LAST_ADMIN'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override getOperationsStatistics(): Observable<OperationsStatistics> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/stats`)
|
||||
.pipe(
|
||||
map((raw) => this.operationsStatisticsFromApi(raw)),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated',
|
||||
403: 'You are not allowed to view user statistics'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override listStatisticsUsers(): Observable<string[]> {
|
||||
return this.http
|
||||
.get<unknown[]>(`${environment.apiUrl}/stats/users`)
|
||||
.pipe(
|
||||
map((raw) => Array.isArray(raw)
|
||||
? raw.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter((item) => item.length > 0)
|
||||
: []),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated',
|
||||
403: 'You are not allowed to view user statistics'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
override getUserStatistics(username: string): Observable<UserStatistics> {
|
||||
return this.http
|
||||
.get<unknown>(`${environment.apiUrl}/stats/users/${encodeURIComponent(username)}`)
|
||||
.pipe(
|
||||
map((raw) => this.userStatisticsFromApi(raw, username)),
|
||||
catchError((error: unknown) => this.toHttpError(error, {
|
||||
401: 'Unauthenticated',
|
||||
403: 'You are not allowed to view user statistics',
|
||||
404: 'User not found'
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
private extractHttpErrorMessage(error: HttpErrorResponse): string | null {
|
||||
const payload = error.error;
|
||||
if (typeof payload === 'string' && payload.trim().length > 0) {
|
||||
|
|
@ -196,53 +92,24 @@ export class AuthorizationCallService extends AuthorizationCallServiceBase {
|
|||
return null;
|
||||
}
|
||||
|
||||
private userFromApi(raw: unknown, fallbackUsername?: string): User {
|
||||
const payload = (raw ?? {}) as Record<string, unknown>;
|
||||
const userSource =
|
||||
(payload["user"] as Record<string, unknown> | undefined)
|
||||
?? (payload["profile"] as Record<string, unknown> | undefined)
|
||||
?? payload;
|
||||
|
||||
return {
|
||||
username: String(userSource["username"] ?? fallbackUsername ?? ''),
|
||||
email: typeof userSource["email"] === "string" ? String(userSource["email"]) : null,
|
||||
role: this.normalizeRole(userSource["role"])
|
||||
} satisfies User;
|
||||
}
|
||||
|
||||
private normalizeRole(value: unknown): UserRole {
|
||||
return String(value ?? '').toUpperCase() === 'ADMIN' ? 'ADMIN' : 'USER';
|
||||
}
|
||||
|
||||
private adminUserFromApi(raw: unknown): AdminUser {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
username: String(value['username'] ?? ''),
|
||||
email: typeof value['email'] === 'string' ? value['email'] : null,
|
||||
role: this.normalizeRole(value['role'])
|
||||
};
|
||||
}
|
||||
|
||||
private userStatisticsFromApi(raw: unknown, username: string): UserStatistics {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
username: String(value['username'] ?? username),
|
||||
flowsCreated: Number(value['flowsCreated'] ?? 0),
|
||||
flowsPublished: Number(value['flowsPublished'] ?? 0),
|
||||
flowsFinalized: Number(value['flowsFinalized'] ?? 0),
|
||||
executionsCreated: Number(value['executionsCreated'] ?? 0),
|
||||
executionsRunning: Number(value['executionsRunning'] ?? 0),
|
||||
executionsSucceeded: Number(value['executionsSucceeded'] ?? 0),
|
||||
executionsFailed: Number(value['executionsFailed'] ?? 0),
|
||||
simulationsStarted: Number(value['simulationsStarted'] ?? 0),
|
||||
lastFlowUpdateAt: typeof value['lastFlowUpdateAt'] === 'string' ? value['lastFlowUpdateAt'] : null,
|
||||
lastExecutionAt: typeof value['lastExecutionAt'] === 'number' ? value['lastExecutionAt'] : null
|
||||
};
|
||||
}
|
||||
|
||||
private operationsStatisticsFromApi(raw: unknown): OperationsStatistics {
|
||||
const value = (raw ?? {}) as Record<string, unknown>;
|
||||
return {
|
||||
usersCount: Number(value['usersCount'] ?? 0),
|
||||
flowsCreated: Number(value['flowsCreated'] ?? 0),
|
||||
flowsPublished: Number(value['flowsPublished'] ?? 0),
|
||||
flowsFinalized: Number(value['flowsFinalized'] ?? 0),
|
||||
executionsCreated: Number(value['executionsCreated'] ?? 0),
|
||||
executionsRunning: Number(value['executionsRunning'] ?? 0),
|
||||
executionsSucceeded: Number(value['executionsSucceeded'] ?? 0),
|
||||
executionsFailed: Number(value['executionsFailed'] ?? 0),
|
||||
simulationsStarted: Number(value['simulationsStarted'] ?? 0),
|
||||
lastFlowUpdateAt: typeof value['lastFlowUpdateAt'] === 'string' ? value['lastFlowUpdateAt'] : null,
|
||||
lastExecutionAt: typeof value['lastExecutionAt'] === 'number' ? value['lastExecutionAt'] : null
|
||||
};
|
||||
}
|
||||
|
||||
override logout(): Observable<void> {
|
||||
return this.http
|
||||
.post<void>(`${environment.apiUrl}/auth/logout`, {})
|
||||
|
|
|
|||
|
|
@ -1,17 +1,12 @@
|
|||
import { Injectable, signal } from '@angular/core';
|
||||
import {
|
||||
AdminChangeRoleRequest,
|
||||
AdminCreateUserRequest,
|
||||
AdminResetPasswordRequest,
|
||||
ChangePasswordRequest,
|
||||
OperationsStatistics,
|
||||
User,
|
||||
UserRegistration,
|
||||
UserStatistics
|
||||
UserRegistration
|
||||
} from '@models/user';
|
||||
import { AuthorizationCallServiceBase } from './authorization-call.base';
|
||||
import { environment } from '@environment';
|
||||
import { Observable, catchError, take, tap, throwError } from 'rxjs';
|
||||
import { Observable, catchError, finalize, map, of, shareReplay, take, tap, throwError } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
|
@ -20,24 +15,21 @@ export class Authorization {
|
|||
static readonly USER_STORAGE_KEY = 'loggedInUser';
|
||||
|
||||
private user = signal<User | null>(null);
|
||||
private sessionValidation$: Observable<User | null> | null = null;
|
||||
|
||||
authCall: AuthorizationCallServiceBase = new environment.authorizationCallService();
|
||||
|
||||
loggedInUser = this.user.asReadonly();
|
||||
|
||||
constructor() {
|
||||
this.restoreUserFromStorage();
|
||||
}
|
||||
|
||||
login(username: string, password: string) {
|
||||
return this.authCall.login(username, password).pipe(
|
||||
take(1),
|
||||
tap(res => {
|
||||
const normalizedUser = this.normalizeUser(res);
|
||||
this.user.set(normalizedUser);
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem(Authorization.USER_STORAGE_KEY, JSON.stringify(normalizedUser));
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors (e.g. quota exceeded, private browsing).
|
||||
}
|
||||
this.persistUserState(res);
|
||||
}),
|
||||
catchError((err) => {
|
||||
console.error('Login failed', err);
|
||||
|
|
@ -58,81 +50,41 @@ export class Authorization {
|
|||
);
|
||||
}
|
||||
|
||||
listAdminUsers() {
|
||||
return this.authCall.listAdminUsers().pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
createAdminUser(request: AdminCreateUserRequest) {
|
||||
return this.authCall.createAdminUser(request).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
changeAdminUserPassword(username: string, request: AdminResetPasswordRequest) {
|
||||
return this.authCall.changeAdminUserPassword(username, request).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
changeAdminUserRole(username: string, request: AdminChangeRoleRequest) {
|
||||
return this.authCall.changeAdminUserRole(username, request).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
deleteAdminUser(username: string) {
|
||||
return this.authCall.deleteAdminUser(username).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
getOperationsStatistics() {
|
||||
return this.authCall.getOperationsStatistics().pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
listStatisticsUsers() {
|
||||
return this.authCall.listStatisticsUsers().pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
getUserStatistics(username: string) {
|
||||
return this.authCall.getUserStatistics(username).pipe(
|
||||
take(1)
|
||||
);
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
return this.authCall.logout().pipe(
|
||||
take(1),
|
||||
tap(() => {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(Authorization.USER_STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
this.user.set(null);
|
||||
})
|
||||
tap(() => this.clearUserState())
|
||||
);
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
if (typeof localStorage === 'undefined') return false;
|
||||
const storedUser = localStorage.getItem(Authorization.USER_STORAGE_KEY);
|
||||
const normalizedUser = storedUser != null ? this.normalizeUser(JSON.parse(storedUser) as User) : null;
|
||||
this.user.set(normalizedUser);
|
||||
return storedUser != null;
|
||||
return this.loggedInUser() != null;
|
||||
}
|
||||
|
||||
isAdmin(): boolean {
|
||||
const currentUser = this.loggedInUser() ?? (this.isLoggedIn() ? this.loggedInUser() : null);
|
||||
return currentUser?.role === 'ADMIN';
|
||||
return this.loggedInUser()?.role === 'ADMIN';
|
||||
}
|
||||
|
||||
validateSession(): Observable<User | null> {
|
||||
if (this.sessionValidation$) {
|
||||
return this.sessionValidation$;
|
||||
}
|
||||
|
||||
this.sessionValidation$ = this.authCall.currentUser().pipe(
|
||||
take(1),
|
||||
map((user) => this.normalizeUser(user)),
|
||||
tap((user) => this.persistUserState(user)),
|
||||
catchError(() => {
|
||||
this.clearUserState();
|
||||
return of(null);
|
||||
}),
|
||||
finalize(() => {
|
||||
this.sessionValidation$ = null;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
return this.sessionValidation$;
|
||||
}
|
||||
|
||||
private normalizeUser(user: User | null): User | null {
|
||||
|
|
@ -144,4 +96,36 @@ export class Authorization {
|
|||
};
|
||||
}
|
||||
|
||||
private restoreUserFromStorage() {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
|
||||
try {
|
||||
const storedUser = localStorage.getItem(Authorization.USER_STORAGE_KEY);
|
||||
if (!storedUser) return;
|
||||
this.user.set(this.normalizeUser(JSON.parse(storedUser) as User));
|
||||
} catch {
|
||||
this.clearUserState();
|
||||
}
|
||||
}
|
||||
|
||||
private persistUserState(user: User | null) {
|
||||
const normalizedUser = this.normalizeUser(user);
|
||||
this.user.set(normalizedUser);
|
||||
|
||||
try {
|
||||
if (typeof localStorage === 'undefined') return;
|
||||
if (normalizedUser) {
|
||||
localStorage.setItem(Authorization.USER_STORAGE_KEY, JSON.stringify(normalizedUser));
|
||||
} else {
|
||||
localStorage.removeItem(Authorization.USER_STORAGE_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors (e.g. quota exceeded, private browsing).
|
||||
}
|
||||
}
|
||||
|
||||
private clearUserState() {
|
||||
this.persistUserState(null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AdminCallFakeService } from "@services/admin/admin-call.fake";
|
||||
import { AssistantCallServiceFake } from "@services/assistant/assistant-call.fake";
|
||||
import { AuthorizationCallFakeService } from "@services/authorization/authorization-call.fake";
|
||||
import { BlocksCallServiceFake } from "@services/blocks/blocks-call.fake";
|
||||
|
|
@ -13,6 +14,7 @@ export const environment = {
|
|||
tourModeAlwaysOn: true,
|
||||
turnstileEnabled: false,
|
||||
authorizationCallService: AuthorizationCallFakeService, // Assign the appropriate service here
|
||||
adminCallService: AdminCallFakeService,
|
||||
assistantCallService: AssistantCallServiceFake,
|
||||
flowsCallService: FlowsCallServiceFake,
|
||||
blocksCallService: BlocksCallServiceFake,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AdminCallService } from "@services/admin/admin-call";
|
||||
import { AssistantCallService } from "@services/assistant/assistant-call";
|
||||
import { AuthorizationCallService } from "@services/authorization/authorization-call";
|
||||
import { BlocksCallService } from "@services/blocks/blocks-call";
|
||||
|
|
@ -14,6 +15,7 @@ export const environment = {
|
|||
turnstileEnabled: false,
|
||||
assistantCallService: AssistantCallService,
|
||||
authorizationCallService: AuthorizationCallService,
|
||||
adminCallService: AdminCallService,
|
||||
flowsCallService: FlowsCallService,
|
||||
blocksCallService: BlocksCallService,
|
||||
containersCallService: ContainersCallService,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AdminCallService } from "@services/admin/admin-call";
|
||||
import { AssistantCallService } from "@services/assistant/assistant-call";
|
||||
import { AuthorizationCallService } from "@services/authorization/authorization-call";
|
||||
import { BlocksCallService } from "@services/blocks/blocks-call";
|
||||
|
|
@ -13,6 +14,7 @@ export const environment = {
|
|||
tourModeAlwaysOn: window.__runtimeConfig?.tourModeAlwaysOn ?? false,
|
||||
turnstileEnabled: window.__runtimeConfig?.turnstileEnabled ?? true,
|
||||
authorizationCallService: AuthorizationCallService,
|
||||
adminCallService: AdminCallService,
|
||||
assistantCallService: AssistantCallService,
|
||||
flowsCallService: FlowsCallService,
|
||||
blocksCallService: BlocksCallService,
|
||||
|
|
|
|||
Loading…
Reference in New Issue