oip-common 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ng-package.json +19 -0
- package/package.json +19 -38
- package/src/api/FolderModule.ts +124 -0
- package/src/api/Menu.ts +134 -0
- package/src/api/Module.ts +92 -0
- package/src/api/Security.ts +40 -0
- package/src/api/Service.ts +57 -0
- package/src/api/data-contracts.ts +186 -0
- package/src/api/http-client.ts +276 -0
- package/src/components/app-configurator.component.ts +491 -0
- package/src/components/app-floating-configurator.component.ts +47 -0
- package/src/components/app-modules.component.ts +144 -0
- package/src/components/app.layout.component.ts +130 -0
- package/src/components/auth/access/access.component.ts +42 -0
- package/src/components/auth/error/error.component.ts +42 -0
- package/src/components/auth/login/login.component.ts +120 -0
- package/src/components/auth/unauthorized/unauthorized.component.ts +51 -0
- package/src/components/base-module.component.ts +258 -0
- package/src/components/config.component.ts +131 -0
- package/src/components/db-migration/db-migration.component.ts +164 -0
- package/src/components/db-migration.component.ts +156 -0
- package/src/components/footer.component.ts +17 -0
- package/src/components/logo.component.ts +34 -0
- package/src/components/menu/menu-item-create-dialog.component.ts +119 -0
- package/src/components/menu/menu-item-edit-dialog.component.ts +124 -0
- package/src/components/menu/menu-item.component.ts +295 -0
- package/src/components/menu/menu.component.ts +85 -0
- package/src/components/notfound.component.ts +31 -0
- package/src/components/profile.component.ts +44 -0
- package/src/components/security.component.ts +102 -0
- package/src/components/sidebar.component.ts +12 -0
- package/src/components/top-bar.component.ts +147 -0
- package/src/dtos/context-menu-item.dto.ts +23 -0
- package/src/dtos/edit-module-instance.dto.ts +8 -0
- package/src/dtos/no-settings.dto.ts +4 -0
- package/src/dtos/put-security.dto.ts +6 -0
- package/src/dtos/security.dto.ts +6 -0
- package/src/dtos/top-bar.dto.ts +13 -0
- package/src/events/menu-change.event.ts +23 -0
- package/src/helpers/date.helper.ts +94 -0
- package/src/intercepts/i18n-intercept.service.ts +13 -0
- package/src/modules/http-loader.factory.ts +40 -0
- package/src/modules/secure.pipe.ts +19 -0
- package/src/public-api.ts +42 -0
- package/src/services/app-title.service.ts +22 -0
- package/src/services/app.layout.service.ts +236 -0
- package/src/services/app.menu.service.ts +64 -0
- package/src/services/auth.service.ts +58 -0
- package/src/services/base-data.service.ts +74 -0
- package/src/services/l10n.service.ts +71 -0
- package/src/services/msg.service.ts +76 -0
- package/src/services/security-data.service.ts +19 -0
- package/src/services/security-storage.service.ts +21 -0
- package/src/services/security.service.ts +116 -0
- package/src/services/top-bar.service.ts +44 -0
- package/src/services/user.service.ts +77 -0
- package/src/test.ts +11 -0
- package/src/user-api/UserProfile.ts +85 -0
- package/src/user-api/data-contracts.ts +42 -0
- package/src/user-api/http-client.ts +253 -0
- package/templates/api.ejs +30 -0
- package/templates/data-contract-jsdoc.ejs +37 -0
- package/templates/data-contracts.ejs +52 -0
- package/templates/enum-data-contract.ejs +12 -0
- package/templates/http-client.ejs +245 -0
- package/templates/interface-data-contract.ejs +10 -0
- package/templates/object-field-jsdoc.ejs +28 -0
- package/templates/procedure-call.ejs +100 -0
- package/templates/route-docs.ejs +29 -0
- package/templates/route-name.ejs +42 -0
- package/templates/route-type.ejs +23 -0
- package/templates/route-types.ejs +18 -0
- package/templates/type-data-contract.ejs +15 -0
- package/tsconfig.lib.json +12 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +9 -0
- /package/{assets → src/assets}/demo/code.scss +0 -0
- /package/{assets → src/assets}/demo/demo.scss +0 -0
- /package/{assets → src/assets}/demo/flags/flags.scss +0 -0
- /package/{assets → src/assets}/demo/flags/flags_responsive.png +0 -0
- /package/{assets → src/assets}/demo/images/access/asset-access.svg +0 -0
- /package/{assets → src/assets}/demo/images/error/asset-error.svg +0 -0
- /package/{assets → src/assets}/demo/images/flag/flag_placeholder.png +0 -0
- /package/{assets → src/assets}/favicon.svg +0 -0
- /package/{assets → src/assets}/i18n/app-modules.en.json +0 -0
- /package/{assets → src/assets}/i18n/app-modules.ru.json +0 -0
- /package/{assets → src/assets}/i18n/config.en.json +0 -0
- /package/{assets → src/assets}/i18n/config.ru.json +0 -0
- /package/{assets → src/assets}/layout/_core.scss +0 -0
- /package/{assets → src/assets}/layout/_footer.scss +0 -0
- /package/{assets → src/assets}/layout/_logo.scss +0 -0
- /package/{assets → src/assets}/layout/_main.scss +0 -0
- /package/{assets → src/assets}/layout/_menu.scss +0 -0
- /package/{assets → src/assets}/layout/_mixins.scss +0 -0
- /package/{assets → src/assets}/layout/_preloading.scss +0 -0
- /package/{assets → src/assets}/layout/_responsive.scss +0 -0
- /package/{assets → src/assets}/layout/_topbar.scss +0 -0
- /package/{assets → src/assets}/layout/_typography.scss +0 -0
- /package/{assets → src/assets}/layout/_utils.scss +0 -0
- /package/{assets → src/assets}/layout/layout.scss +0 -0
- /package/{assets → src/assets}/layout/variables/_common.scss +0 -0
- /package/{assets → src/assets}/layout/variables/_dark.scss +0 -0
- /package/{assets → src/assets}/layout/variables/_light.scss +0 -0
- /package/{assets → src/assets}/oip-common.scss +0 -0
- /package/{assets → src/assets}/tailwind.css +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Injectable, effect, signal, computed } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { convertToPrimeNgDateFormat } from '../helpers/date.helper';
|
|
4
|
+
|
|
5
|
+
export interface AppConfig {
|
|
6
|
+
preset?: string;
|
|
7
|
+
primary?: string;
|
|
8
|
+
surface?: string | null;
|
|
9
|
+
darkTheme?: boolean;
|
|
10
|
+
menuMode?: string;
|
|
11
|
+
language?: string;
|
|
12
|
+
dateFormat: string;
|
|
13
|
+
timeFormat: string;
|
|
14
|
+
dateTimeFormat: string;
|
|
15
|
+
timeZone: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LayoutState {
|
|
19
|
+
staticMenuDesktopInactive?: boolean;
|
|
20
|
+
overlayMenuActive?: boolean;
|
|
21
|
+
configSidebarVisible?: boolean;
|
|
22
|
+
staticMenuMobileActive?: boolean;
|
|
23
|
+
menuHoverActive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface MenuChangeEvent {
|
|
27
|
+
key: string;
|
|
28
|
+
routeEvent?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Injectable({ providedIn: 'root' })
|
|
32
|
+
export class LayoutService {
|
|
33
|
+
_config: AppConfig = this.getAppConfigFromStorage();
|
|
34
|
+
|
|
35
|
+
_state: LayoutState = {
|
|
36
|
+
staticMenuDesktopInactive: false,
|
|
37
|
+
overlayMenuActive: false,
|
|
38
|
+
configSidebarVisible: false,
|
|
39
|
+
staticMenuMobileActive: false,
|
|
40
|
+
menuHoverActive: false
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
layoutConfig = signal<AppConfig>(this._config);
|
|
44
|
+
|
|
45
|
+
layoutState = signal<LayoutState>(this._state);
|
|
46
|
+
|
|
47
|
+
private readonly configUpdate = new Subject<AppConfig>();
|
|
48
|
+
|
|
49
|
+
private readonly overlayOpen = new Subject<any>();
|
|
50
|
+
|
|
51
|
+
private readonly menuSource = new Subject<MenuChangeEvent>();
|
|
52
|
+
|
|
53
|
+
private readonly resetSource = new Subject();
|
|
54
|
+
|
|
55
|
+
menuSource$ = this.menuSource.asObservable();
|
|
56
|
+
|
|
57
|
+
resetSource$ = this.resetSource.asObservable();
|
|
58
|
+
|
|
59
|
+
configUpdate$ = this.configUpdate.asObservable();
|
|
60
|
+
|
|
61
|
+
overlayOpen$ = this.overlayOpen.asObservable();
|
|
62
|
+
|
|
63
|
+
theme = computed(() => (this.layoutConfig()?.darkTheme ? 'light' : 'dark'));
|
|
64
|
+
|
|
65
|
+
isSidebarActive = computed(() => this.layoutState().overlayMenuActive || this.layoutState().staticMenuMobileActive);
|
|
66
|
+
|
|
67
|
+
isDarkTheme = computed(() => this.layoutConfig().darkTheme);
|
|
68
|
+
|
|
69
|
+
getPrimary = computed(() => this.layoutConfig().primary);
|
|
70
|
+
|
|
71
|
+
getSurface = computed(() => this.layoutConfig().surface);
|
|
72
|
+
|
|
73
|
+
isOverlay = computed(() => this.layoutConfig().menuMode === 'overlay');
|
|
74
|
+
|
|
75
|
+
language = computed(() => this.layoutConfig().language);
|
|
76
|
+
|
|
77
|
+
dateFormat = computed(() => this.layoutConfig().dateFormat);
|
|
78
|
+
|
|
79
|
+
primeNgDateFormat = computed(() => convertToPrimeNgDateFormat(this.layoutConfig().dateFormat));
|
|
80
|
+
|
|
81
|
+
timeFormat = computed(() => this.layoutConfig().timeFormat);
|
|
82
|
+
|
|
83
|
+
dateTimeFormat = computed(() => `${this.layoutConfig().dateFormat} ${this.layoutConfig().timeFormat}`);
|
|
84
|
+
|
|
85
|
+
monthFormat = computed(() => {
|
|
86
|
+
const reDay = /d+/i;
|
|
87
|
+
const reDelimeter = /^[^\w]|[^\w]$|([^\w])\1+/;
|
|
88
|
+
const ngDateFormat = convertToPrimeNgDateFormat(this.layoutConfig().dateFormat);
|
|
89
|
+
const ngDate = ngDateFormat.replace(reDay, '');
|
|
90
|
+
const dateGroups = ngDate.match(reDelimeter);
|
|
91
|
+
if (Array.isArray(dateGroups) && dateGroups.length > 1) {
|
|
92
|
+
return dateGroups[1] !== undefined
|
|
93
|
+
? ngDate.replace(dateGroups[0], '')
|
|
94
|
+
: ngDate.startsWith(dateGroups[0])
|
|
95
|
+
? ngDate.substring(1)
|
|
96
|
+
: ngDate.substring(0, ngDate.length - 1);
|
|
97
|
+
}
|
|
98
|
+
return ngDateFormat;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
timeZone = computed(() => this.layoutConfig().timeZone);
|
|
102
|
+
|
|
103
|
+
transitionComplete = signal<boolean>(false);
|
|
104
|
+
|
|
105
|
+
private initialized = false;
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
effect(() => {
|
|
109
|
+
const config = this.layoutConfig();
|
|
110
|
+
if (config) {
|
|
111
|
+
this.onConfigUpdate();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
effect(() => {
|
|
116
|
+
const config = this.layoutConfig();
|
|
117
|
+
|
|
118
|
+
if (!this.initialized || !config) {
|
|
119
|
+
this.initialized = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.handleDarkModeTransition(config);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get application settings from browser storage
|
|
129
|
+
* @returns AppConfig
|
|
130
|
+
*/
|
|
131
|
+
private getAppConfigFromStorage(): AppConfig {
|
|
132
|
+
const appConfigUiString = localStorage.getItem('layoutConfig');
|
|
133
|
+
if (appConfigUiString != null) {
|
|
134
|
+
const config = JSON.parse(appConfigUiString) as AppConfig;
|
|
135
|
+
config.timeZone ??= Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
136
|
+
return config;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
preset: 'Aura',
|
|
140
|
+
primary: 'emerald',
|
|
141
|
+
surface: null,
|
|
142
|
+
darkTheme: false,
|
|
143
|
+
menuMode: 'static',
|
|
144
|
+
language: 'ru',
|
|
145
|
+
dateFormat: 'yyyy-MM-dd',
|
|
146
|
+
timeFormat: 'HH:mm:ss',
|
|
147
|
+
dateTimeFormat: 'yyyy-MM-dd HH:mm:ss',
|
|
148
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleDarkModeTransition(config: AppConfig): void {
|
|
153
|
+
if ((document as any).startViewTransition) {
|
|
154
|
+
this.startViewTransition(config);
|
|
155
|
+
} else {
|
|
156
|
+
this.toggleDarkMode(config);
|
|
157
|
+
this.onTransitionEnd();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private startViewTransition(config: AppConfig): void {
|
|
162
|
+
const transition = (document as any).startViewTransition(() => {
|
|
163
|
+
this.toggleDarkMode(config);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
transition.ready
|
|
167
|
+
.then(() => {
|
|
168
|
+
this.onTransitionEnd();
|
|
169
|
+
})
|
|
170
|
+
.catch(() => {});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
toggleDarkMode(config?: AppConfig): void {
|
|
174
|
+
const _config = config || this.layoutConfig();
|
|
175
|
+
if (_config.darkTheme) {
|
|
176
|
+
document.documentElement.classList.add('app-dark');
|
|
177
|
+
} else {
|
|
178
|
+
document.documentElement.classList.remove('app-dark');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private onTransitionEnd() {
|
|
183
|
+
this.transitionComplete.set(true);
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
this.transitionComplete.set(false);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
onMenuToggle() {
|
|
190
|
+
if (this.isOverlay()) {
|
|
191
|
+
this.layoutState.update((prev) => ({ ...prev, overlayMenuActive: !this.layoutState().overlayMenuActive }));
|
|
192
|
+
|
|
193
|
+
if (this.layoutState().overlayMenuActive) {
|
|
194
|
+
this.overlayOpen.next(null);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (this.isDesktop()) {
|
|
199
|
+
this.layoutState.update((prev) => ({
|
|
200
|
+
...prev,
|
|
201
|
+
staticMenuDesktopInactive: !this.layoutState().staticMenuDesktopInactive
|
|
202
|
+
}));
|
|
203
|
+
} else {
|
|
204
|
+
this.layoutState.update((prev) => ({
|
|
205
|
+
...prev,
|
|
206
|
+
staticMenuMobileActive: !this.layoutState().staticMenuMobileActive
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
if (this.layoutState().staticMenuMobileActive) {
|
|
210
|
+
this.overlayOpen.next(null);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
isDesktop() {
|
|
216
|
+
return window.innerWidth > 991;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
isMobile() {
|
|
220
|
+
return !this.isDesktop();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
onConfigUpdate() {
|
|
224
|
+
this._config = { ...this.layoutConfig() };
|
|
225
|
+
this.configUpdate.next(this.layoutConfig());
|
|
226
|
+
localStorage.setItem('layoutConfig', JSON.stringify(this.layoutConfig()));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
onMenuStateChange(event: MenuChangeEvent) {
|
|
230
|
+
this.menuSource.next(event);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
reset() {
|
|
234
|
+
this.resetSource.next(true);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { BaseDataService } from './base-data.service';
|
|
4
|
+
import { MenuChangeEvent } from '../events/menu-change.event';
|
|
5
|
+
import { EditModuleInstanceDto } from '../dtos/edit-module-instance.dto';
|
|
6
|
+
import { Menu } from '../api/Menu';
|
|
7
|
+
import { AppTitleService } from './app-title.service';
|
|
8
|
+
import { AddModuleInstanceDto, ModuleInstanceDto } from '../api/data-contracts';
|
|
9
|
+
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class MenuService extends BaseDataService {
|
|
12
|
+
private readonly menuSource = new Subject<MenuChangeEvent>();
|
|
13
|
+
private readonly resetSource = new Subject();
|
|
14
|
+
private readonly titleService = inject(AppTitleService);
|
|
15
|
+
private readonly menuDataService = inject(Menu);
|
|
16
|
+
menuSource$ = this.menuSource.asObservable();
|
|
17
|
+
resetSource$ = this.resetSource.asObservable();
|
|
18
|
+
contextMenuItem: any;
|
|
19
|
+
|
|
20
|
+
public menu: ModuleInstanceDto[] = [];
|
|
21
|
+
public adminMode: boolean = false;
|
|
22
|
+
|
|
23
|
+
async loadMenu() {
|
|
24
|
+
this.menu = this.adminMode ? await this.getAdminMenu() : await this.getMenu();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Handles changes in the menu state.
|
|
29
|
+
* @param {MenuChangeEvent} event - The event containing information about the menu state change.
|
|
30
|
+
* @return {void}
|
|
31
|
+
*/
|
|
32
|
+
onMenuStateChange(event: MenuChangeEvent): void {
|
|
33
|
+
this.menuSource.next(event);
|
|
34
|
+
this.titleService.setTitle(event.item.label);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
reset() {
|
|
38
|
+
this.resetSource.next(true);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getMenu() {
|
|
42
|
+
return this.menuDataService.menuGet();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getAdminMenu() {
|
|
46
|
+
return this.menuDataService.menuGetAdminMenu();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getModules() {
|
|
50
|
+
return this.menuDataService.menuGetModules();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
addModuleInstance(addModuleInstance: AddModuleInstanceDto) {
|
|
54
|
+
return this.menuDataService.menuAddModuleInstance(addModuleInstance as AddModuleInstanceDto);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
deleteItem(moduleInstanceId: number) {
|
|
58
|
+
return this.sendRequest(this.baseUrl + 'api/menu/delete-module-instance?id=' + moduleInstanceId, 'DELETE');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
editModuleInstance(item: EditModuleInstanceDto) {
|
|
62
|
+
return this.sendRequest(this.baseUrl + 'api/menu/edit-module-instance', 'POST', item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { Router, UrlTree } from '@angular/router';
|
|
3
|
+
import { catchError, map, switchMap } from 'rxjs/operators';
|
|
4
|
+
import { Observable, combineLatest, of } from 'rxjs';
|
|
5
|
+
import { SecurityService } from './security.service';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A route guard that ensures the user is authenticated and has a valid access token.
|
|
9
|
+
* If the access token is expired, it attempts to refresh the session.
|
|
10
|
+
* If authentication fails or refresh is unsuccessful, redirects to the unauthorized page.
|
|
11
|
+
*/
|
|
12
|
+
@Injectable()
|
|
13
|
+
export class AuthGuardService {
|
|
14
|
+
private readonly oidcSecurityService = inject(SecurityService);
|
|
15
|
+
private readonly router = inject(Router);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Checks whether the route can be activated.
|
|
19
|
+
* - Returns `true` if the user is authenticated and the token is valid.
|
|
20
|
+
* - Attempts to refresh the token if expired.
|
|
21
|
+
* - Redirects to `/unauthorized` if not authenticated or refresh fails.
|
|
22
|
+
*
|
|
23
|
+
* @returns {Observable<boolean | UrlTree>} A stream resolving to true (allow), or UrlTree (redirect).
|
|
24
|
+
*/
|
|
25
|
+
canActivate(): Observable<boolean | UrlTree> {
|
|
26
|
+
return combineLatest([this.oidcSecurityService.isAuthenticated(), this.oidcSecurityService.isTokenExpired()]).pipe(
|
|
27
|
+
switchMap(([authenticated, tokenExpired]) => {
|
|
28
|
+
if (!authenticated) {
|
|
29
|
+
return of(this.router.parseUrl('/unauthorized'));
|
|
30
|
+
}
|
|
31
|
+
if (!tokenExpired) {
|
|
32
|
+
return of(true);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Token is expired; attempt to refresh
|
|
36
|
+
return this.tryRefreshToken();
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Attempts to refresh the session using the refresh token.
|
|
43
|
+
* If successful, allows route activation; otherwise, redirects to `/unauthorized`.
|
|
44
|
+
*
|
|
45
|
+
* @returns {boolean | UrlTree} A stream resolving to true or redirect UrlTree.
|
|
46
|
+
*/
|
|
47
|
+
tryRefreshToken(): Observable<boolean | UrlTree> {
|
|
48
|
+
return this.oidcSecurityService.forceRefreshSession().pipe(
|
|
49
|
+
map((refreshSuccess) => {
|
|
50
|
+
return refreshSuccess ? true : this.router.parseUrl('/unauthorized');
|
|
51
|
+
}),
|
|
52
|
+
catchError((err) => {
|
|
53
|
+
console.warn(err);
|
|
54
|
+
return of(this.router.parseUrl('/unauthorized'));
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
|
3
|
+
import { lastValueFrom, Observable } from 'rxjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* BaseDataService provides a unified interface for sending HTTP requests
|
|
7
|
+
* using Angular's HttpClient. It supports standard HTTP methods and automatic
|
|
8
|
+
* credential handling.
|
|
9
|
+
*/
|
|
10
|
+
@Injectable()
|
|
11
|
+
export class BaseDataService {
|
|
12
|
+
private readonly http = inject(HttpClient);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gets the base URL of the application from the HTML <base> tag.
|
|
16
|
+
*/
|
|
17
|
+
get baseUrl(): string {
|
|
18
|
+
return document.getElementsByTagName('base')[0].href;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sends an HTTP request with the specified method and data.
|
|
23
|
+
*
|
|
24
|
+
* @template TResponse - Expected response type.
|
|
25
|
+
* @param url - The target URL for the HTTP request.
|
|
26
|
+
* @param method - The HTTP method to use (GET, PUT, POST, DELETE). Default is 'GET'.
|
|
27
|
+
* @param data - An object containing request parameters or payload.
|
|
28
|
+
* @returns A promise that resolves to the response of type TResponse.
|
|
29
|
+
*/
|
|
30
|
+
sendRequest<TResponse>(
|
|
31
|
+
url: string,
|
|
32
|
+
method: 'GET' | 'PUT' | 'POST' | 'DELETE' = 'GET',
|
|
33
|
+
data: any = {}
|
|
34
|
+
): Promise<TResponse> {
|
|
35
|
+
const httpOptions = { withCredentials: true };
|
|
36
|
+
let result: Observable<TResponse>;
|
|
37
|
+
|
|
38
|
+
switch (method) {
|
|
39
|
+
case 'GET':
|
|
40
|
+
result = this.http.get<TResponse>(url, { params: data });
|
|
41
|
+
break;
|
|
42
|
+
case 'PUT':
|
|
43
|
+
result = this.http.put<TResponse>(url, data, httpOptions);
|
|
44
|
+
break;
|
|
45
|
+
case 'POST':
|
|
46
|
+
result = this.http.post<TResponse>(url, data, httpOptions);
|
|
47
|
+
break;
|
|
48
|
+
case 'DELETE':
|
|
49
|
+
result = this.http.request<TResponse>('DELETE', url, {
|
|
50
|
+
body: data,
|
|
51
|
+
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
|
|
52
|
+
withCredentials: true
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return lastValueFrom(result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Sends a GET request and retrieves a response as a Blob.
|
|
62
|
+
*
|
|
63
|
+
* @param url - The target URL for the GET request.
|
|
64
|
+
* @returns A promise that resolves to a Object response.
|
|
65
|
+
*/
|
|
66
|
+
getBlob(url: string): Promise<object> {
|
|
67
|
+
const httpOptions = {
|
|
68
|
+
responseType: 'blob' as 'json',
|
|
69
|
+
withCredentials: true
|
|
70
|
+
};
|
|
71
|
+
const result = this.http.get(url, httpOptions);
|
|
72
|
+
return lastValueFrom(result);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient } from '@angular/common/http';
|
|
3
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
4
|
+
import { lastValueFrom } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Service for managing translation loading in the application
|
|
8
|
+
*/
|
|
9
|
+
@Injectable({ providedIn: 'root' }) // Provided at root level for singleton usage
|
|
10
|
+
export class L10nService {
|
|
11
|
+
private loadedTranslations: Set<string> = new Set();
|
|
12
|
+
private httpClient = inject(HttpClient);
|
|
13
|
+
private translateService = inject(TranslateService);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Loads translations for a specific component
|
|
17
|
+
* @param component - Name of the component to load translations for
|
|
18
|
+
*/
|
|
19
|
+
async loadComponentTranslations(component: string): Promise<void> {
|
|
20
|
+
const lang = this.translateService.currentLang;
|
|
21
|
+
await this.loadTranslations(component, lang);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Gets the translated value of a key (or an array of keys)
|
|
26
|
+
* @returns the translated key, or an object of translated keys
|
|
27
|
+
*/
|
|
28
|
+
public async get(key: string) {
|
|
29
|
+
await this.loadComponentTranslations(key);
|
|
30
|
+
return this.translateService.get(key);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Internal method to load translations from JSON files
|
|
35
|
+
* @param component - Component or translation namespace
|
|
36
|
+
* @param lang - Language code to load translations for
|
|
37
|
+
*/
|
|
38
|
+
private async loadTranslations(component: string, lang: string): Promise<void> {
|
|
39
|
+
// Create unique key for this component-language combination
|
|
40
|
+
const key = `${component}.${lang}`;
|
|
41
|
+
|
|
42
|
+
// Skip if translations are already loaded
|
|
43
|
+
if (this.loadedTranslations.has(key)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Load translation file from assets
|
|
49
|
+
const translations = await lastValueFrom(this.httpClient.get(`./assets/i18n/${component}.${lang}.json`));
|
|
50
|
+
|
|
51
|
+
// Get existing translations for the language
|
|
52
|
+
const current = this.translateService.translations[lang] || {};
|
|
53
|
+
|
|
54
|
+
// Merge new translations with existing ones
|
|
55
|
+
this.translateService.setTranslation(lang, { ...current, ...translations }, true);
|
|
56
|
+
|
|
57
|
+
// Mark these translations as loaded
|
|
58
|
+
this.loadedTranslations.add(key);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn(`No translations found for ${component}.${lang}.json`);
|
|
61
|
+
console.error(e);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Changes the lang currently used
|
|
67
|
+
*/
|
|
68
|
+
use(selectedLanguage: string) {
|
|
69
|
+
this.translateService.use(selectedLanguage);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { inject, Injectable } from '@angular/core';
|
|
2
|
+
import { MessageService } from 'primeng/api';
|
|
3
|
+
import { HttpErrorResponse } from '@angular/common/http';
|
|
4
|
+
import { ToastMessageOptions } from 'primeng/api';
|
|
5
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
6
|
+
|
|
7
|
+
@Injectable({
|
|
8
|
+
providedIn: 'root'
|
|
9
|
+
})
|
|
10
|
+
export class MsgService {
|
|
11
|
+
protected readonly messageService: MessageService = inject(MessageService);
|
|
12
|
+
private readonly translate = inject(TranslateService);
|
|
13
|
+
private readonly lifetime: number = 2000;
|
|
14
|
+
|
|
15
|
+
add(msg: ToastMessageOptions) {
|
|
16
|
+
this.messageService.add(msg);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
success(detail: any, summary: any = this.translate.instant('msgService.success'), life: number = this.lifetime) {
|
|
20
|
+
this.messageService.add({
|
|
21
|
+
severity: 'success',
|
|
22
|
+
summary: summary,
|
|
23
|
+
detail: detail,
|
|
24
|
+
life: life
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
info(detail: any, summary: any = this.translate.instant('msgService.info'), life: number = this.lifetime) {
|
|
29
|
+
this.messageService.add({
|
|
30
|
+
severity: 'info',
|
|
31
|
+
summary: summary,
|
|
32
|
+
detail: detail,
|
|
33
|
+
life: life
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
warn(detail: any, summary: any = this.translate.instant('msgService.warn'), life: number = this.lifetime) {
|
|
38
|
+
this.messageService.add({
|
|
39
|
+
severity: 'warn',
|
|
40
|
+
summary: summary,
|
|
41
|
+
detail: detail,
|
|
42
|
+
life: life
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
error(detail: any, summary: any = this.translate.instant('msgService.error'), life: number = this.lifetime) {
|
|
47
|
+
if (detail instanceof HttpErrorResponse) {
|
|
48
|
+
summary = `Error: ${detail.status} ${detail.statusText}`;
|
|
49
|
+
detail = `${detail.name} \r\n ${detail.message}`;
|
|
50
|
+
}
|
|
51
|
+
this.messageService.add({
|
|
52
|
+
severity: 'error',
|
|
53
|
+
summary: summary,
|
|
54
|
+
detail: detail.toString(),
|
|
55
|
+
life: life
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
contrast(detail: any, summary: any = this.translate.instant('msgService.error'), life: number = this.lifetime) {
|
|
60
|
+
this.messageService.add({
|
|
61
|
+
severity: 'contrast',
|
|
62
|
+
summary: summary,
|
|
63
|
+
detail: detail,
|
|
64
|
+
life: life
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
secondary(detail: any, summary: any = this.translate.instant('msgService.secondary'), life: number = this.lifetime) {
|
|
69
|
+
this.messageService.add({
|
|
70
|
+
severity: 'secondary',
|
|
71
|
+
summary: summary,
|
|
72
|
+
detail: detail,
|
|
73
|
+
life: life
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { BaseDataService } from './base-data.service';
|
|
3
|
+
import { SecurityDto } from '../dtos/security.dto';
|
|
4
|
+
import { PutSecurityDto } from '../dtos/put-security.dto';
|
|
5
|
+
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class SecurityDataService extends BaseDataService {
|
|
8
|
+
getSecurity(controller: string, id: number) {
|
|
9
|
+
return this.sendRequest<SecurityDto[]>(this.baseUrl + `api/${controller}/get-security?id=${id}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
saveSecurity(controller: string, request: PutSecurityDto) {
|
|
13
|
+
return this.sendRequest<any>(this.baseUrl + `api/${controller}/put-security`, 'PUT', request);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getRealmRoles() {
|
|
17
|
+
return this.sendRequest<string[]>(this.baseUrl + `api/security/get-realm-roles`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AbstractSecurityStorage } from 'angular-auth-oidc-client';
|
|
2
|
+
import { Injectable } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class SecurityStorageService implements AbstractSecurityStorage {
|
|
6
|
+
read(key: string) {
|
|
7
|
+
return localStorage.getItem(key);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
write(key: string, value: any): void {
|
|
11
|
+
localStorage.setItem(key, value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
remove(key: string): void {
|
|
15
|
+
localStorage.removeItem(key);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
clear(): void {
|
|
19
|
+
localStorage.clear();
|
|
20
|
+
}
|
|
21
|
+
}
|