diff --git a/CHANGELOG.md b/CHANGELOG.md index 62498385cf..4d83fdad5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,94 @@ Full Changelog: [https://github.com/netgrif/components/commits/v6.5.0](https://g - [NAE-2019] Bugs after merge with 6.4.0 ### Changed -- [NAE- 1940] Update to new Angular +- [NAE-1889] Process list is not refreshed after uploading process file +- [NAE-1885] Side panel creates a new case when pressing Enter on date field +- [NAE-1687] Factory class as providers +- [NAE-1911] Autosave on text area in cooperation with button +- [NAE-1497] Frontend actions +- [NAE-1915] TaskRef behaviour handling for multiple level +- [NAE-1904] Case ref as multichoice and enumeration +- [NAE-1908] NAE-1906 Improvements +- [NAE-1918] Tags on process resources +- [NAE-1921] User field value cannot be cleared +- [NAE-1914] revertToPreviousValue - change is always false +- [NAE-1925] Panel is not opening +- [NAE-1924] Neziadane volanie setData z FE +- [NAE-1923] DateTime doesn't have locale and rework validation to isoWeekday +- [NAE-1928] Refresh tabs on change +- [NAE-1926] Can't close Tab in Tab view +- [NAE-1929] Data field type list of strings +- [NAE-1933] UserList deleteAll button +- [NAE-1939] Problem with Tests +- [NAE-1936] Disable create case button using menu items +- [NAE-1935] Improved breadcrumbs from menu items +- [NAE-1876] Process URI v2 +- [NAE-1882] Filter folder process +- [NAE-1890] Data field component register +- [NAE-1901] Taskref rendering update +- [NAE-1900] New component design +- [NAE-1920] Injection token NAE_USER_ASSIGN_COMPONENT breaks delegate +- [NAE-1879] Language register +- [NAE-1905] Add bold on i18n text plainText field +- [NAE-1873] Seperator for number field +- [NAE-1922] Signature Pad Field +- [NAE-1949] Allowed Types for Filefield +- [NAE-1813] Field id as path variable in TaskController +- [NAE-1957] Allow filter to caseRef field and variants +- [NAE-1960] Enumeration Map does not propagate changes when selecting +- [NAE-1958] Make component properties changeable +- [NAE- 1940] Update to new Angular +- [NAE-1983] Public view file handling +- [NAE-1999] Broken pagination on paged case view +- [NAE-1949] Allowed Types for Filefield +- [NAE-2005] Field behavior change does not work correctly with multiple references using taskRef +- [NAE-2013] Autocomplete options are set to the first dropdown +- [NAE-2013] Autocomplete options are set to the first dropdown +- [NAE-2016] Global roles for menu items permissions +- [NAE-2018] User list input is not showing dialog +- [NAE-2020] Create case error when allowed net blocks are present +- [NAE-2021] Outputs for navigation components +- [NAE-2022] UI Design Fixes and Improvements +- [NAE-2034] Open first view +- [NAE-2038] Public View +- [NAE-2033] Welcome dashboard +- [NAE-2035] Implement Single-Task-View +- [NAE-2036] Task-List-View to open Case by link +- [NAE-2041] Implementing Ticket View +- [NAE-2040] Search in role management +- [NAE-2052] Integrate ticket view with menu items +- [NAE-2039] Search in workflow view +- [NAE-2063] Action API 6.5.0 +- [NAE-2115] Task search on Search Node #314 +- [NAE-2119] Fix menuItem +- [NAE-2125] Remove URI service usage from admin and menu items #318 +- [NAE-2116] Frontend remote configuration +- [NAE-2085] Refactor User +- [NAE-2122] Implement Structured and Efficient Pagination in gRPC +- [NAE-2146] Broken hidden menu on frontend +- [NAE-2085] Refactor User +- [NAE-2118] Implement OpenID Connector Auth for Admin node +- [NAE-2165] Broken task list pagination +- [NAE-2174] Vanishing menu after few clicks +- Refactor case ID usage to replace `stringId` with `id` +- [NAE-2188] Wrong remote configuration loading order +- [NAE-2197] Wrong elastic sort +- [NAE-2205] DefaultCaseRefListViewComponent - headers not displayed when processes exceed single view +- [NAE-2202] Post test fixes +- [NAE-2218] Misaligned header on case view when displayed below 1920px +- [NAE-2224] Title in optional text in case creation button as I18nString +- [NAE-2226] Cannot switch between menu items +- [NAE-2232] Frontend Actions Task events resolver +- [NAE-2233] Fix dashboard menu issue +- [NAE-2227] MenuItem default headers not working +- [NAE-2234] Fix dashboard menu +- [NAE-2217] Single Task View +- [NAE-2251] Map field options are not translated +- [NAE-2285] Group as a value of userlists +- [NAE-2354] Include assignee userRealmId in task response +- [NAE-2263] Copy/selection prevention is set to to entire application not just for side menu +- [NAE-2435] Enumeration field with no choices does not properly handle validation +- [NAE-2416] AbstractFileDefaultFieldComponent does not push upload event ## [6.5.0](https://github.com/netgrif/components/releases/tag/v6.5.0) (2025-02-18) ### Added diff --git a/nae.json b/nae.json index 16dc91b4ba..86d7b49aa4 100644 --- a/nae.json +++ b/nae.json @@ -20,11 +20,21 @@ "sessionTimeoutEnabled": false, "sessionTimeout": 900, "jwtBearer": "X-Jwt-Token", + "apiToken": { + "enabled": true, + "queryParameter": "token", + "realmQueryParameter": "realmId", + "removeFromUrl": true, + "allowedPaths": [ + "/tabbed-views", + "/tabbed-views/**" + ] + }, "sso": { "enable": false, "clientId": "dev-cluster-worker", "redirectUrl": "http://localhost:8081/realms/netgrif-cloud-testing/protocol/openid-connect/auth", - "refreshUrl": "http://localhost:8800/api/auth/login", + "refreshUrl": "http://localhost:8080/api/auth/login", "scopes": ["openid","email","profile","roles"] } }, diff --git a/package.json b/package.json index f2bed4cbfc..2469bcb43b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@netgrif/components-project", - "version": "7.0.0-rc.19", + "version": "7.0.2", "description": "Netgrif Application Engine Frontend project. Project includes angular libraries as base for NAE applications.", "homepage": "https://components.netgrif.com", "license": "SEE LICENSE IN LICENSE", diff --git a/projects/nae-example-app/src/app/app.component.ts b/projects/nae-example-app/src/app/app.component.ts index f63663c479..b8dd1cd273 100644 --- a/projects/nae-example-app/src/app/app.component.ts +++ b/projects/nae-example-app/src/app/app.component.ts @@ -28,7 +28,7 @@ export class AppComponent { translate.setTranslation('en', en, true); translate.setTranslation('sk', sk, true); - this.userService.user$.pipe(filter(u => !!u && u.id !== ''), take(1)).subscribe(() => { + this.userService.user$.pipe(filter(u => !!u && u.id !== '' && !u.isAnonymous()), take(1)).subscribe(() => { const allNets = allowedNetsFactory.createWithAllNets(); allNets.allowedNetsIdentifiers$.pipe(take(1)).subscribe(nets => { if (this.baseAllowedNets.allowedNets.length !== 0) { diff --git a/projects/netgrif-components-core/package.json b/projects/netgrif-components-core/package.json index bba6174408..3524daef04 100644 --- a/projects/netgrif-components-core/package.json +++ b/projects/netgrif-components-core/package.json @@ -1,6 +1,6 @@ { "name": "@netgrif/components-core", - "version": "7.0.0-rc.19", + "version": "7.0.2", "description": "Netgrif Application engine frontend core Angular library", "homepage": "https://components.netgrif.com", "license": "SEE LICENSE IN LICENSE", diff --git a/projects/netgrif-components-core/schematics/_commons/schema.d.ts b/projects/netgrif-components-core/schematics/_commons/schema.d.ts index 7b07799968..9f6b4956ec 100644 --- a/projects/netgrif-components-core/schematics/_commons/schema.d.ts +++ b/projects/netgrif-components-core/schematics/_commons/schema.d.ts @@ -47,11 +47,20 @@ export interface Auth { sessionBearer?: string; jwtEnabled?: boolean; jwtBearer?: string; + apiToken?: ApiTokenAuthentication; endpoints?: string | { [k: string]: string }; [k: string]: any; } +export interface ApiTokenAuthentication { + enabled?: boolean; + queryParameter?: string; + realmQueryParameter?: string; + removeFromUrl?: boolean; + allowedPaths?: Array; +} + export interface Resource { name: string; address: string; diff --git a/projects/netgrif-components-core/src/commons/schema.ts b/projects/netgrif-components-core/src/commons/schema.ts index 7a740b1b35..76afc0ce9b 100644 --- a/projects/netgrif-components-core/src/commons/schema.ts +++ b/projects/netgrif-components-core/src/commons/schema.ts @@ -45,12 +45,21 @@ export interface Auth { address: string; authentication: string; sessionBearer?: string; + apiToken?: ApiTokenAuthentication; endpoints?: string | { [k: string]: string }; sso?: Sso; [k: string]: any; } +export interface ApiTokenAuthentication { + enabled?: boolean; + queryParameter?: string; + realmQueryParameter?: string; + removeFromUrl?: boolean; + allowedPaths?: Array; +} + export interface Sso { enable: boolean; redirectUrl: string; diff --git a/projects/netgrif-components-core/src/lib/authentication/anonymous/anonymous.service.spec.ts b/projects/netgrif-components-core/src/lib/authentication/anonymous/anonymous.service.spec.ts deleted file mode 100644 index 93ef00b802..0000000000 --- a/projects/netgrif-components-core/src/lib/authentication/anonymous/anonymous.service.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import {TestBed} from '@angular/core/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {HttpClientTestingModule} from '@angular/common/http/testing'; -import {RouterTestingModule} from '@angular/router/testing'; -import {AnonymousService} from './anonymous.service'; -import {ConfigurationService} from '../../configuration/configuration.service'; -import {TestConfigurationService} from '../../utility/tests/test-config'; - -describe('AnonymousService', () => { - let service: AnonymousService; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, HttpClientTestingModule, RouterTestingModule.withRoutes([])], - providers: [{provide: ConfigurationService, useClass: TestConfigurationService}] - }); - service = TestBed.inject(AnonymousService); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - afterEach(() => { - TestBed.resetTestingModule(); - }); -}); - diff --git a/projects/netgrif-components-core/src/lib/authentication/anonymous/anonymous.service.ts b/projects/netgrif-components-core/src/lib/authentication/anonymous/anonymous.service.ts deleted file mode 100644 index 2ed898ec8e..0000000000 --- a/projects/netgrif-components-core/src/lib/authentication/anonymous/anonymous.service.ts +++ /dev/null @@ -1,64 +0,0 @@ -import {Injectable, OnDestroy} from '@angular/core'; -import {ConfigurationService} from '../../configuration/configuration.service'; -import {NullStorage} from '../session/null-storage'; -import {BehaviorSubject, Observable} from 'rxjs'; - - -@Injectable({ - providedIn: 'root' -}) -export class AnonymousService implements OnDestroy { - - public static readonly JWT_BEARER_HEADER_DEFAULT = 'X-Jwt-Token'; - protected readonly _jwtHeader: string; - protected _storage: Storage; - protected _tokenSet: BehaviorSubject; - - constructor(protected _config: ConfigurationService) { - this._jwtHeader = this._config.get().providers.auth.jwtBearer ? - this._config.get().providers.auth.jwtBearer : AnonymousService.JWT_BEARER_HEADER_DEFAULT; - this._storage = this.resolveStorage(this._config.get().providers.auth['local']); - this._tokenSet = new BehaviorSubject(false); - } - - get jwtHeader(): string { - return this._jwtHeader; - } - - get tokenSet(): Observable { - return this._tokenSet.asObservable(); - } - - public getToken(): string { - return this._storage.getItem(this._jwtHeader); - } - - public setToken(token: string): void { - this._storage.setItem(this._jwtHeader, token); - if (!this._tokenSet.getValue()) - this._tokenSet.next(true); - } - - public removeToken(): void { - this._storage.removeItem(this._jwtHeader); - this._tokenSet.next(false); - } - - ngOnDestroy(): void { - localStorage.removeItem(this._jwtHeader); - this._tokenSet.complete(); - } - - protected resolveStorage(storage: string): any { - switch (storage) { - case 'local': - return localStorage; - case 'session': - return sessionStorage; - case 'null': - return new NullStorage(); - default: - return localStorage; - } - } -} diff --git a/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts b/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts index b6e1f2c204..771ad7eef8 100644 --- a/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts +++ b/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts @@ -6,7 +6,7 @@ import {ProxyAuthenticationService} from './proxyAuthentication.service'; import {AuthenticationMethodService} from './services/authentication-method.service'; import {OverlayModule} from '@angular/cdk/overlay'; import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; -import {AnonymousAuthenticationInterceptor} from './services/anonymous-authentication-interceptor'; +// import {AnonymousAuthenticationInterceptor} from './services/anonymous-authentication-interceptor'; @NgModule({ @@ -22,7 +22,7 @@ import {AnonymousAuthenticationInterceptor} from './services/anonymous-authentic ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }, - { provide: HTTP_INTERCEPTORS, useClass: AnonymousAuthenticationInterceptor, multi: true }, + // { provide: HTTP_INTERCEPTORS, useClass: AnonymousAuthenticationInterceptor, multi: true }, { provide: AuthenticationMethodService, useClass: ProxyAuthenticationService}, // AuthenticationEffects ] diff --git a/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.spec.ts b/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.spec.ts index 5696e62298..a7fd123142 100644 --- a/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.spec.ts +++ b/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.spec.ts @@ -17,7 +17,8 @@ describe('ProxyAuthenticationService', () => { auth: { authentication: 'basic', address: 'http://localhost:8080', - endpoints: {login: '/api/auth/login'} + endpoints: {login: '/api/auth/login'}, + apiToken: {} } } } as any, @@ -59,4 +60,19 @@ describe('ProxyAuthenticationService', () => { expect(response).toBeTruthy(); })); + + it('authenticates an API token with bearer and realm headers', fakeAsync(() => { + let response: any; + + service.loginWithApiToken('user-id.secret', 'Admin').subscribe(res => response = res); + + const req = httpMock.expectOne('http://localhost:8080/api/auth/login'); + expect(req.request.method).toBe('GET'); + expect(req.request.headers.get('Authorization')).toBe('Bearer user-id.secret'); + expect(req.request.headers.get('X-Realm-ID')).toBe('Admin'); + req.flush({id: '1', name: 'User'}); + + tick(); + expect(response).toBeTruthy(); + })); }); diff --git a/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.ts b/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.ts index 666676abd4..368f26406c 100644 --- a/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.ts +++ b/projects/netgrif-components-core/src/lib/authentication/proxyAuthentication.service.ts @@ -1,12 +1,12 @@ import {ConfigurationService} from '../configuration/configuration.service'; import {NullAuthenticationService} from './services/methods/null-authentication/null-authentication.service'; import {BasicAuthenticationService} from './services/methods/basic-authentication/basic-authentication.service'; -import {HttpClient} from '@angular/common/http'; +import {HttpClient, HttpHeaders} from '@angular/common/http'; import {BasicWithRealmAuthenticationService} from "./services/methods/basic-authentication/basic-with-realm-authentication.service"; import {filter, take} from "rxjs/operators"; import {UserResource} from "../resources/interface/user-resource"; import {Credentials} from "./models/credentials"; -import {Observable} from 'rxjs'; +import {Observable, throwError} from 'rxjs'; import {Injectable} from "@angular/core"; import {AuthenticationMethodService} from "./services/authentication-method.service"; @@ -46,6 +46,24 @@ export class ProxyAuthenticationService extends AuthenticationMethodService { return this._proxyAuthMethod.login(credentials); } + loginWithApiToken(token: string, realmId?: string): Observable { + const auth = this._config.get().providers.auth; + const loginEndpoint = typeof auth.endpoints === 'object' ? auth.endpoints['login'] : undefined; + const url = auth.address + (loginEndpoint ?? ''); + if (!loginEndpoint) { + return throwError(new Error('Login URL is not defined in the config [nae.providers.auth.endpoints.login]')); + } + if (!token?.trim()) { + return throwError(new Error('API token is empty')); + } + + let headers = new HttpHeaders().set('Authorization', `Bearer ${token.trim()}`); + if (realmId?.trim()) { + headers = headers.set('X-Realm-ID', realmId.trim()); + } + return this._http.get(url, {headers}); + } + logout(): Observable { return this._proxyAuthMethod.logout(); } diff --git a/projects/netgrif-components-core/src/lib/authentication/public-api.ts b/projects/netgrif-components-core/src/lib/authentication/public-api.ts index ecd362b905..f5ff8e8865 100644 --- a/projects/netgrif-components-core/src/lib/authentication/public-api.ts +++ b/projects/netgrif-components-core/src/lib/authentication/public-api.ts @@ -8,9 +8,7 @@ export * from './sign-up/public-api'; /* MODULES */ export * from './authentication.module'; -/* SERVICES */ -export * from './anonymous/anonymous.service'; -export * from './services/anonymous-authentication-interceptor' +// export * from './services/anonymous-authentication-interceptor' export * from './services/authentication-interceptor' export * from './proxyAuthentication.service' diff --git a/projects/netgrif-components-core/src/lib/authentication/services/anonymous-authentication-interceptor.spec.ts b/projects/netgrif-components-core/src/lib/authentication/services/anonymous-authentication-interceptor.spec.ts deleted file mode 100644 index 594f185f1d..0000000000 --- a/projects/netgrif-components-core/src/lib/authentication/services/anonymous-authentication-interceptor.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {inject, TestBed} from '@angular/core/testing'; -import {ConfigurationService} from '../../configuration/configuration.service'; -import {TestConfigurationService} from '../../utility/tests/test-config'; -import {HTTP_INTERCEPTORS, HttpClient, HttpHeaders} from '@angular/common/http'; -import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; -import {RouterTestingModule} from '@angular/router/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {LoggerService} from '../../logger/services/logger.service'; -import {AnonymousService} from '../anonymous/anonymous.service'; -import {AnonymousAuthenticationInterceptor} from './anonymous-authentication-interceptor'; - -describe('AnonymousAuthenticationInterceptor', () => { - let service: AnonymousService; - let warnSpy: jasmine.Spy; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule, NoopAnimationsModule, RouterTestingModule.withRoutes([])], - providers: [ - {provide: ConfigurationService, useClass: TestConfigurationService}, - AnonymousService, - { - provide: HTTP_INTERCEPTORS, - useClass: AnonymousAuthenticationInterceptor, - multi: true - } - ] - }); - service = TestBed.inject(AnonymousService); - warnSpy = spyOn(TestBed.inject(LoggerService), 'warn'); - }); - - describe('intercept HTTP request', () => { - it('should add JWT bearer to Headers', (done) => { - inject([HttpClient, HttpTestingController], - (http: HttpClient, mock: HttpTestingController) => { - - service.setToken('jwt-token'); - http.get('/api').subscribe(response => { - expect(response).toBeTruthy(); - done(); - }); - const request = mock.expectOne(req => (req.headers.has('X-Jwt-Token'))); - - request.flush({data: 'test'}, {headers: new HttpHeaders({'X-Jwt-Token': 'tokenos'})}); - mock.verify(); - })(); - }); - afterEach(inject([HttpTestingController], (mock: HttpTestingController) => { - mock.verify(); - TestBed.resetTestingModule(); - })); - }); - - afterEach(() => { - TestBed.resetTestingModule(); - }); -}); diff --git a/projects/netgrif-components-core/src/lib/authentication/services/anonymous-authentication-interceptor.ts b/projects/netgrif-components-core/src/lib/authentication/services/anonymous-authentication-interceptor.ts deleted file mode 100644 index f02ce95dc8..0000000000 --- a/projects/netgrif-components-core/src/lib/authentication/services/anonymous-authentication-interceptor.ts +++ /dev/null @@ -1,41 +0,0 @@ -import {Injectable} from '@angular/core'; -import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '@angular/common/http'; -import {Observable, throwError} from 'rxjs'; -import {catchError, tap} from 'rxjs/operators'; -import {AnonymousService} from '../anonymous/anonymous.service'; - -@Injectable() -export class AnonymousAuthenticationInterceptor implements HttpInterceptor { - - constructor(protected _anonymousService: AnonymousService) {} - - intercept(req: HttpRequest, next: HttpHandler): Observable> { - const jwtAuthToken = this._anonymousService.getToken(); - - if (!this._anonymousService) { - next.handle(req); - } - - if (!!jwtAuthToken) { - req = req.clone({ - headers: req.headers.set(this._anonymousService.jwtHeader, jwtAuthToken) - }); - } - return next.handle(req).pipe( - tap(event => { - if (event instanceof HttpResponse) { - if (event.headers.has(this._anonymousService.jwtHeader)) { - this._anonymousService.setToken(event.headers.get(this._anonymousService.jwtHeader)); - } - } - }), - catchError(errorEvent => { - if (errorEvent instanceof HttpErrorResponse && errorEvent.status === 401) { - console.debug('Authentication token is invalid. Clearing session token'); - this._anonymousService.removeToken(); - } - return throwError(errorEvent); - }) - ); - } -} diff --git a/projects/netgrif-components-core/src/lib/authentication/services/authentication-interceptor.ts b/projects/netgrif-components-core/src/lib/authentication/services/authentication-interceptor.ts index 7e56051d92..abc768a716 100644 --- a/projects/netgrif-components-core/src/lib/authentication/services/authentication-interceptor.ts +++ b/projects/netgrif-components-core/src/lib/authentication/services/authentication-interceptor.ts @@ -11,7 +11,7 @@ import {Observable, throwError} from 'rxjs'; import {catchError, tap} from 'rxjs/operators'; import {SessionService} from '../session/services/session.service'; import {RedirectService} from '../../routing/redirect-service/redirect.service'; -import {AnonymousService} from '../anonymous/anonymous.service'; +// import {AnonymousService} from '../anonymous/anonymous.service'; import {SessionIdleTimerService} from "../session/services/session-idle-timer.service"; @Injectable() @@ -19,7 +19,7 @@ export class AuthenticationInterceptor implements HttpInterceptor { constructor(private _session: SessionService, private _redirect: RedirectService, - private _anonymousService: AnonymousService, + // private _anonymousService: AnonymousService, private idleTimerService: SessionIdleTimerService) { } @@ -28,25 +28,31 @@ export class AuthenticationInterceptor implements HttpInterceptor { return next.handle(req); } - if (this._session && !!this._session.sessionToken) { + const sessionHeader = this._session.sessionHeader; + const sessionToken = req.headers.get(sessionHeader) || this._session.sessionToken || ''; + if (sessionToken && !req.headers.has('Authorization') && !req.headers.has(sessionHeader)) { req = req.clone({ - headers: req.headers.set(this._session.sessionHeader, this._session.sessionToken) + headers: req.headers.set(sessionHeader, sessionToken) }); this.idleTimerService.resetTimer(); } return next.handle(req).pipe( tap(event => { - if (event instanceof HttpResponse) { - if (event.headers.has(this._session.sessionHeader) && !event.headers.has(this._anonymousService.jwtHeader)) { - this._session.setVerifiedToken(event.headers.get(this._session.sessionHeader)); + if (event instanceof HttpResponse && (this._session.sessionToken || '') === sessionToken) { + const responseToken = event.headers.get(sessionHeader); + if (responseToken) { + this._session.setVerifiedToken(responseToken); } } }), catchError(errorEvent => { - if (errorEvent instanceof HttpErrorResponse && errorEvent.status === 401) { + if (errorEvent instanceof HttpErrorResponse && errorEvent.status === 401 + && (this._session.sessionToken || '') === sessionToken) { console.debug('Authentication token is invalid. Clearing session token'); this._session.clear(); - this._redirect.redirect(this._redirect.resolveLoginPath()); + if (this._session.isInitialized && !req.headers.has('Authorization')) { + this._redirect.redirect(this._redirect.resolveLoginPath()); + } } return throwError(errorEvent); }) diff --git a/projects/netgrif-components-core/src/lib/authentication/services/authentication-method.service.ts b/projects/netgrif-components-core/src/lib/authentication/services/authentication-method.service.ts index 240a4d00d9..49e71cce4d 100644 --- a/projects/netgrif-components-core/src/lib/authentication/services/authentication-method.service.ts +++ b/projects/netgrif-components-core/src/lib/authentication/services/authentication-method.service.ts @@ -1,4 +1,4 @@ -import {Observable} from 'rxjs'; +import {Observable, throwError} from 'rxjs'; import {Credentials} from '../models/credentials'; import {UserResource} from '../../resources/interface/user-resource'; @@ -9,5 +9,9 @@ export abstract class AuthenticationMethodService { abstract login(credentials: Credentials): Observable; + loginWithApiToken(_token: string, _realmId?: string): Observable { + return throwError(new Error('API token authentication is not supported by this authentication method')); + } + abstract logout(): Observable; } diff --git a/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.spec.ts b/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.spec.ts index 1705b1927b..da260e4df1 100644 --- a/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.spec.ts +++ b/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.spec.ts @@ -40,6 +40,14 @@ describe('AuthenticationService', () => { }); }); + it('should login with an API token', (done) => { + service.loginWithApiToken('user-id.secret', 'Admin').subscribe(res => { + expect(res.id).toEqual('id'); + expect(service.isAuthenticated).toBe(true); + done(); + }); + }); + afterEach(() => { TestBed.resetTestingModule(); }); diff --git a/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.ts b/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.ts index f10233c2eb..a9feece659 100644 --- a/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.ts +++ b/projects/netgrif-components-core/src/lib/authentication/services/authentication/authentication.service.ts @@ -49,6 +49,19 @@ export class AuthenticationService implements OnDestroy { ); } + loginWithApiToken(token: string, realmId?: string): Observable { + return this._auth.loginWithApiToken(token, realmId).pipe( + tap((user: UserResource) => { + this._authenticated$.next(!!user[AuthenticationService.IDENTIFICATION_ATTRIBUTE]); + }), + map((user: UserResource) => this._userTransformer.transform(user)), + catchError(error => { + console.error(error); + return of(null); + }) + ); + } + logout(): Observable { return this._auth.logout().pipe( tap(() => { diff --git a/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.spec.ts b/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.spec.ts index 45b5aa9283..4d0425f426 100644 --- a/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.spec.ts +++ b/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.spec.ts @@ -1,19 +1,28 @@ -import {TestBed} from '@angular/core/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {ActivatedRouteSnapshot, convertToParamMap, Router, RouterStateSnapshot, UrlTree} from '@angular/router'; import {ConfigurationService} from '../../../configuration/configuration.service'; import {AuthenticationMethodService} from '../authentication-method.service'; import {AuthenticationGuardService} from './authentication-guard.service'; import {AuthenticationService} from '../authentication/authentication.service'; import {RouterTestingModule} from '@angular/router/testing'; -import {TestConfigurationService} from '../../../utility/tests/test-config'; +import {ApiTokenTestConfigurationService} from '../../../utility/tests/test-config'; import {NoopAnimationsModule} from '@angular/platform-browser/animations'; import {MockAuthenticationMethodService} from '../../../utility/tests/mocks/mock-authentication-method-service'; import {MockAuthenticationService} from '../../../utility/tests/mocks/mock-authentication.service'; import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {SessionService} from '../../session/services/session.service'; +import {Observable, firstValueFrom, of} from 'rxjs'; +import {UserService} from '../../../user/services/user.service'; +import {User} from '../../../user/models/user'; describe('AuthenticationGuardService', () => { let service: AuthenticationGuardService; + let session: SessionService; + let userService: UserService; - beforeEach(() => { + beforeEach(fakeAsync(() => { + localStorage.removeItem(SessionService.SESSION_TOKEN_STORAGE_KEY); + const configuration = new ApiTokenTestConfigurationService(); TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, @@ -22,19 +31,74 @@ describe('AuthenticationGuardService', () => { RouterTestingModule.withRoutes([]) ], providers: [ - {provide: ConfigurationService, useClass: TestConfigurationService}, + {provide: ConfigurationService, useValue: configuration}, {provide: AuthenticationMethodService, useClass: MockAuthenticationMethodService}, {provide: AuthenticationService, useClass: MockAuthenticationService}, AuthenticationGuardService ]}); service = TestBed.inject(AuthenticationGuardService); - }); + session = TestBed.inject(SessionService); + userService = TestBed.inject(UserService); + tick(); + })); it('should be created', () => { expect(service).toBeTruthy(); }); + it('logs in from an allowed API-token URL and removes credentials from the URL', async () => { + const loginSpy = spyOn(userService, 'loginWithApiToken').and.callFake(() => { + session.setVerifiedToken('session-token'); + return of(user()); + }); + + const result = service.canActivate( + routeWithQuery({token: 'user-id.secret', realmId: 'Admin', caseId: 'case-1'}), + {url: '/tabbed-views?token=user-id.secret&realmId=Admin&caseId=case-1'} as RouterStateSnapshot + ) as Observable; + const decision = await firstValueFrom(result); + + expect(loginSpy).toHaveBeenCalledWith('user-id.secret', 'Admin'); + expect(decision instanceof UrlTree).toBeTrue(); + expect(TestBed.inject(Router).serializeUrl(decision as UrlTree)).toBe('/tabbed-views?caseId=case-1'); + }); + + it('does not consume an API token on a path outside the allow-list', () => { + const loginSpy = spyOn(userService, 'loginWithApiToken'); + const login = service.canActivate( + routeWithQuery({token: 'user-id.secret', realmId: 'Admin'}), + {url: '/login?token=user-id.secret&realmId=Admin'} as RouterStateSnapshot + ) as UrlTree; + + expect(loginSpy).not.toHaveBeenCalled(); + expect(TestBed.inject(Router).serializeUrl(login)).toBe('/login'); + }); + + it('keeps token query parameters when API-token login is disabled for an existing session', () => { + const configuration = TestBed.inject(ConfigurationService); + const value = configuration.get(); + value.providers.auth.apiToken = {...value.providers.auth.apiToken, enabled: false}; + spyOn(configuration, 'get').and.returnValue(value); + session.setVerifiedToken('session-token'); + + const result = service.canActivate( + routeWithQuery({token: 'application-value', realmId: 'application-realm'}), + {url: '/tabbed-views?token=application-value&realmId=application-realm'} as RouterStateSnapshot + ); + + expect(result).toBeTrue(); + }); + afterEach(() => { TestBed.resetTestingModule(); + localStorage.removeItem(SessionService.SESSION_TOKEN_STORAGE_KEY); }); }); + +function routeWithQuery(query: Record): ActivatedRouteSnapshot { + return {queryParamMap: convertToParamMap(query)} as ActivatedRouteSnapshot; +} + +function user(): User { + return new User('id', 'username', 'mail', 'Admin', 'name', 'surname', [], [], [], []); +} diff --git a/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.ts b/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.ts index bbb6491f22..476fff83a5 100644 --- a/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.ts +++ b/projects/netgrif-components-core/src/lib/authentication/services/guard/authentication-guard.service.ts @@ -3,6 +3,11 @@ import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTre import {AuthenticationModule} from '../../authentication.module'; import {SessionService} from '../../session/services/session.service'; import {RedirectService} from '../../../routing/redirect-service/redirect.service'; +import {ConfigurationService} from '../../../configuration/configuration.service'; +import {UserService} from '../../../user/services/user.service'; +import {isObservable, Observable, of} from 'rxjs'; +import {catchError, filter, map, switchMap, take} from 'rxjs/operators'; +import {ApiTokenAuthentication} from '../../../../commons/schema'; @Injectable({ providedIn: AuthenticationModule @@ -13,12 +18,83 @@ export class AuthenticationGuardService implements CanActivate { constructor(private _session: SessionService, private _redirectService: RedirectService, + private _configuration: ConfigurationService, + private _userService: UserService, private _router: Router) { this._loginUrl = this._redirectService.resolveLoginPath(); } - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree { + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable { this._redirectService.intendedRoute = route; - return this._session.sessionToken && this._session.verified ? true : this._router.parseUrl(this._loginUrl); + if (this._session.isInitialized) { + return this.authorize(route, state); + } + return this._session.initializing.pipe( + filter(initialized => initialized), + take(1), + switchMap(() => { + const decision = this.authorize(route, state); + return isObservable(decision) ? decision : of(decision); + }) + ); + } + + private authorize(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable { + const config = this.apiTokenConfiguration(); + const tokenParameter = config?.queryParameter || 'token'; + const realmParameter = config?.realmQueryParameter || 'realmId'; + const token = route.queryParamMap.get(tokenParameter)?.trim(); + + if (this._session.sessionToken && this._session.verified) { + return config?.enabled && token && config.removeFromUrl !== false + ? this.sanitizedUrl(state.url, tokenParameter, realmParameter) + : true; + } + if (!config?.enabled || !token || !this.isAllowedPath(state.url, config.allowedPaths)) { + return this._router.parseUrl(this._loginUrl); + } + + const realmId = route.queryParamMap.get(realmParameter)?.trim(); + return this._userService.loginWithApiToken(token, realmId).pipe( + take(1), + map(user => { + if (!user || !this._session.sessionToken || !this._session.verified) { + return this._router.parseUrl(this._loginUrl); + } + return config.removeFromUrl === false + ? true + : this.sanitizedUrl(state.url, tokenParameter, realmParameter); + }), + catchError(() => of(this._router.parseUrl(this._loginUrl))) + ); + } + + private apiTokenConfiguration(): ApiTokenAuthentication | undefined { + return this._configuration.get().providers.auth.apiToken; + } + + private isAllowedPath(url: string, allowedPaths: Array | undefined): boolean { + if (!allowedPaths?.length) { + return false; + } + const path = this._router.parseUrl(url).root.children['primary']?.segments + .map(segment => segment.path) + .join('/') || ''; + const normalizedPath = `/${path}`.replace(/\/$/, '') || '/'; + return allowedPaths.some(allowedPath => { + const normalizedAllowedPath = `/${allowedPath}`.replace(/\/+/g, '/').replace(/\/$/, '') || '/'; + if (normalizedAllowedPath.endsWith('/**')) { + const prefix = normalizedAllowedPath.slice(0, -3); + return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`); + } + return normalizedPath === normalizedAllowedPath; + }); + } + + private sanitizedUrl(url: string, tokenParameter: string, realmParameter: string): UrlTree { + const tree = this._router.parseUrl(url); + delete tree.queryParams[tokenParameter]; + delete tree.queryParams[realmParameter]; + return tree; } } diff --git a/projects/netgrif-components-core/src/lib/authentication/session/services/session.service.ts b/projects/netgrif-components-core/src/lib/authentication/session/services/session.service.ts index 61374d20bf..5ddd9c8c63 100644 --- a/projects/netgrif-components-core/src/lib/authentication/session/services/session.service.ts +++ b/projects/netgrif-components-core/src/lib/authentication/session/services/session.service.ts @@ -4,7 +4,7 @@ import {ConfigurationService} from '../../../configuration/configuration.service import {NullStorage} from '../null-storage'; import {HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http'; import {LoggerService} from '../../../logger/services/logger.service'; -import {catchError, filter, map, take, tap} from 'rxjs/operators'; +import {catchError, filter, finalize, map, take} from 'rxjs/operators'; import {MessageResource} from '../../../resources/interface/message-resource'; import {LoadingEmitter} from '../../../utility/loading-emitter'; import {SessionIdleTimerService} from "./session-idle-timer.service"; @@ -54,6 +54,7 @@ export class SessionService implements OnDestroy { ngOnDestroy(): void { this._session$.complete(); this._verifying.complete(); + this._initialized.complete(); } get session$(): Observable { @@ -61,6 +62,7 @@ export class SessionService implements OnDestroy { } set sessionToken(sessionToken: string) { + this.ensureConfigInitialized(); this._session$.next(sessionToken); this._storage.setItem(SessionService.SESSION_TOKEN_STORAGE_KEY, btoa(SessionService.SESSION_TOKEN_STORAGE_KEY + ':' + sessionToken)); @@ -131,41 +133,48 @@ export class SessionService implements OnDestroy { observe: 'response' }).pipe( catchError(error => { - if (error instanceof HttpErrorResponse && error.status === 401) { + if (error instanceof HttpErrorResponse && error.status === 401 && this.sessionToken === token) { this._log.warn('Authentication token is invalid. Clearing session token'); this.clear(); } - this._verifying.off(); - this.idleTimerService.stopTimer(); - this._initialized.on(); return throwError(error); }), map(response => { + if (this.sessionToken !== token) { + return this.verified; + } this._log.debug(response.body.success); this._verified = true; this.idleTimerService.resetTimer(); - this._initialized.on(); - this.sessionToken = token; + this.sessionToken = response.headers.get(this.sessionHeader) || token; return true; }), - tap(_ => this._verifying.off()) + finalize(() => { + this._verifying.off(); + this._initialized.on(); + }) ); } } protected load(): string { this.ensureConfigInitialized(); + if (this.verified && this.sessionToken) { + this._initialized.on(); + return this.sessionToken; + } - let token = this._storage.getItem(SessionService.SESSION_TOKEN_STORAGE_KEY); + const token = this.resolveToken(this._storage.getItem(SessionService.SESSION_TOKEN_STORAGE_KEY)); this._verified = false; this.idleTimerService.stopTimer(); if (token) { - token = this.resolveToken(token); this.sessionToken = token; - this.verify(token).pipe(take(1)).subscribe(ver => { - this._log.debug('Token ' + token + ' verified status: ' + ver); + this.verify(token).pipe(take(1)).subscribe({ + next: verified => this._log.debug('Stored session verified: ' + verified), + error: () => this._log.warn('Stored session could not be restored') }); } else { + this.clear(); this._initialized.on(); } return ''; @@ -184,7 +193,12 @@ export class SessionService implements OnDestroy { } private resolveToken(raw: string): string { - return raw ? atob(raw).split(':')[1] : ''; + try { + const [key, token] = raw ? atob(raw).split(':') : []; + return key === SessionService.SESSION_TOKEN_STORAGE_KEY ? token || '' : ''; + } catch { + return ''; + } } private resolveStorage(storage: string): any { diff --git a/projects/netgrif-components-core/src/lib/authorization/permission/access.service.ts b/projects/netgrif-components-core/src/lib/authorization/permission/access.service.ts index d02b79728b..c96b47250c 100644 --- a/projects/netgrif-components-core/src/lib/authorization/permission/access.service.ts +++ b/projects/netgrif-components-core/src/lib/authorization/permission/access.service.ts @@ -37,7 +37,7 @@ export class AccessService { if (view.access !== 'private') { throw new Error(`Unknown access option '${view.access}'. Only 'public' or 'private' is allowed.`); } - return !this._userService.user.isEmpty(); + return !this._userService.user.isEmpty() && !this._userService.user.isAnonymous(); } if (!url) { diff --git a/projects/netgrif-components-core/src/lib/data-fields/button-field/button-default-field/abstract-button-default-field.component.spec.ts b/projects/netgrif-components-core/src/lib/data-fields/button-field/button-default-field/abstract-button-default-field.component.spec.ts index da7d5d798d..cb4e3a0e42 100644 --- a/projects/netgrif-components-core/src/lib/data-fields/button-field/button-default-field/abstract-button-default-field.component.spec.ts +++ b/projects/netgrif-components-core/src/lib/data-fields/button-field/button-default-field/abstract-button-default-field.component.spec.ts @@ -57,7 +57,8 @@ describe('AbstractButtonDefaultFieldComponent', () => { }).compileComponents(); fixture = TestBed.createComponent(TestWrapperComponent); - const initializeLanguage = TestBed.inject(LanguageService); + TestBed.inject(LanguageService); + TestBed.inject(TranslateService).use('en'); component = fixture.debugElement.children[0].componentInstance; fixture.detectChanges(); })); diff --git a/projects/netgrif-components-core/src/lib/data-fields/number-field/abstract-number-errors.component.spec.ts b/projects/netgrif-components-core/src/lib/data-fields/number-field/abstract-number-errors.component.spec.ts index 54dfedb66b..d91f2a80cd 100644 --- a/projects/netgrif-components-core/src/lib/data-fields/number-field/abstract-number-errors.component.spec.ts +++ b/projects/netgrif-components-core/src/lib/data-fields/number-field/abstract-number-errors.component.spec.ts @@ -70,6 +70,7 @@ describe('AbstractNumberErrorsComponent', () => { fixture = TestBed.createComponent(TestWrapperComponent); component = fixture.debugElement.children[0].componentInstance; TestBed.inject(LanguageService); + TestBed.inject(TranslateService).use('en'); fixture.detectChanges(); })); diff --git a/projects/netgrif-components-core/src/lib/filter/models/filter.ts b/projects/netgrif-components-core/src/lib/filter/models/filter.ts index 9d26bdf1f8..74c4a722aa 100644 --- a/projects/netgrif-components-core/src/lib/filter/models/filter.ts +++ b/projects/netgrif-components-core/src/lib/filter/models/filter.ts @@ -71,6 +71,16 @@ export abstract class Filter { */ public abstract bodyContainsQuery(): boolean; + + /** + * Checks whether any of the filter bodies contains the `caseId` attribute. + * + * This method analyzes the body of the filter to determine if at least one of its parts includes a `caseId` field. + * + * @returns `true` if the `caseId` attribute exists in any of the filter bodies; otherwise, `false`. + */ + public abstract bodyContainsCaseId(): boolean; + /** * Returns the necessary request params for the filter. Default implementation returns an empty object. * The params are added on top of the request when sending it to the backend by the respective service methods. diff --git a/projects/netgrif-components-core/src/lib/filter/models/merged-filter.ts b/projects/netgrif-components-core/src/lib/filter/models/merged-filter.ts index 23a952c801..6118f49586 100644 --- a/projects/netgrif-components-core/src/lib/filter/models/merged-filter.ts +++ b/projects/netgrif-components-core/src/lib/filter/models/merged-filter.ts @@ -121,6 +121,13 @@ export class MergedFilter extends Filter { return this._filters.some(f => f.query !== undefined && f.query !== null); } + /** + * See [Filter.bodyContainsCaseId()]{@link Filter#bodyContainsCaseId} + */ + bodyContainsCaseId(): boolean { + return false; + } + /** * Returns the necessary request params for the filter. * @returns params with `operation` set to either `AND` or `OR` based on this object's `_operator` property. diff --git a/projects/netgrif-components-core/src/lib/filter/models/simple-filter.ts b/projects/netgrif-components-core/src/lib/filter/models/simple-filter.ts index 557d3d9e97..5d3e9cb447 100644 --- a/projects/netgrif-components-core/src/lib/filter/models/simple-filter.ts +++ b/projects/netgrif-components-core/src/lib/filter/models/simple-filter.ts @@ -102,6 +102,20 @@ export class SimpleFilter extends Filter { return this._filter.query !== undefined && this._filter.query !== null; } + /** + * See [Filter.bodyContainsCaseId()]{@link Filter#bodyContainsCaseId} + */ + bodyContainsCaseId(): boolean { + return !!this._filter + && !!this._filter['case'] + && ((!Array.isArray(this._filter['case']) + && !!this._filter['case'].id + && this._filter['case'].id.length > 0) + || (Array.isArray(this._filter['case']) + && this._filter['case'].length > 0)); + } + + /** * See [Filter.getRequestBody()]{@link Filter#getRequestBody} */ diff --git a/projects/netgrif-components-core/src/lib/groups/services/next-group.service.ts b/projects/netgrif-components-core/src/lib/groups/services/next-group.service.ts index 759a934fda..798da2387d 100644 --- a/projects/netgrif-components-core/src/lib/groups/services/next-group.service.ts +++ b/projects/netgrif-components-core/src/lib/groups/services/next-group.service.ts @@ -25,35 +25,14 @@ export class NextGroupService implements OnDestroy { protected _memberGroups$: BehaviorSubject>; protected _ownerGroups$: BehaviorSubject>; - private _userSub: Subscription; constructor(protected _userService: UserService, protected _caseResourceService: CaseResourceService) { this._ownerGroups$ = new BehaviorSubject>([]); this._memberGroups$ = new BehaviorSubject>([]); - this._userSub = this._userService.user$.pipe( - switchMap(user => { - if (!user || user.id === '') { - return of([]); - } - - const params = new HttpParams().set(PaginationParams.PAGE_SIZE, `${(user as any).nextGroups.length}`); - - return this._caseResourceService.searchCases(SimpleFilter.fromCaseQuery({id: (user as any).nextGroups}), params) - .pipe( - map(page => page.content ? page.content : []), - map(groups => groups.filter(group => group.author.fullName !== 'application engine')) - ); - }) - ).subscribe(groups => { - const ownerGroups = groups.filter(g => g.author.email === this._userService.user.email); - this._ownerGroups$.next(ownerGroups); - this._memberGroups$.next(groups); - }); } ngOnDestroy(): void { - this._userSub.unsubscribe(); this._memberGroups$.complete(); this._ownerGroups$.complete(); } diff --git a/projects/netgrif-components-core/src/lib/navigation/navigation-tree/abstract-navigation-tree.component.spec.ts b/projects/netgrif-components-core/src/lib/navigation/navigation-tree/abstract-navigation-tree.component.spec.ts index 1ed4b54e37..b56d68ead2 100644 --- a/projects/netgrif-components-core/src/lib/navigation/navigation-tree/abstract-navigation-tree.component.spec.ts +++ b/projects/netgrif-components-core/src/lib/navigation/navigation-tree/abstract-navigation-tree.component.spec.ts @@ -22,7 +22,6 @@ import {UserResourceService} from '../../resources/engine-endpoint/user-resource import {UserTransformer} from '../../authentication/models/user.transformer'; import {SessionService} from '../../authentication/session/services/session.service'; import {User} from '../../user/models/user'; -import {AnonymousService} from '../../authentication/anonymous/anonymous.service'; import {ActiveGroupService} from '../../groups/services/active-group.service'; import {TaskResourceService} from '../../resources/engine-endpoint/task-resource.service'; import {LanguageService} from '../../translate/language.service'; @@ -396,9 +395,9 @@ class TestUserService extends UserService { userTransform: UserTransformer, log: LoggerService, session: SessionService, - anonymousService: AnonymousService, + // anonymousService: AnonymousService, config: ConfigurationService) { - super(authService, userResource, userTransform, log, session, anonymousService, config); + super(authService, userResource, userTransform, log, session, config); } public setUser(user: User) { diff --git a/projects/netgrif-components-core/src/lib/public/factories/public-factory-resolver.ts b/projects/netgrif-components-core/src/lib/public/factories/public-factory-resolver.ts index f343dd8e0d..a2790c8b9d 100644 --- a/projects/netgrif-components-core/src/lib/public/factories/public-factory-resolver.ts +++ b/projects/netgrif-components-core/src/lib/public/factories/public-factory-resolver.ts @@ -15,7 +15,7 @@ export const publicFactoryResolver = (userService: UserService, sessionService: } else { router.navigate([url], {queryParams: redirectService.queryParams}); } - } else if (authService.isAuthenticated && userService.user.id !== '' && userService.user.email !== 'anonymous@netgrif.com') { + } else if (authService.isAuthenticated && !!userService.user && !userService.user.isAnonymous()) { return privateService; } else { return publicService; diff --git a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-case-resource.service.ts b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-case-resource.service.ts index 508dc2b606..07dad17ee5 100644 --- a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-case-resource.service.ts +++ b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-case-resource.service.ts @@ -16,23 +16,13 @@ export class PublicCaseResourceService extends CaseResourceService { super(provider, configService); } - /** - * Get all case data - * GET - * {{baseUrl}}/api/public/case/:id/data - */ - public getCaseData(caseID: string): Observable { - return this._resourceProvider.get$('public/case/' + caseID + '/data', this.SERVER_URL) - .pipe(map(r => this.changeType(r, undefined))); - } - /** * Create new case * POST * {{baseUrl}}/api/workflow/case */ public createCase(body: object): Observable { - return this._resourceProvider.post$('public/case/', this.SERVER_URL, body) + return this._resourceProvider.post$('workflow/public/case/', this.SERVER_URL, body) .pipe(map(r => this.changeType(r, undefined))); } } diff --git a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-petri-net-resource.service.ts b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-petri-net-resource.service.ts index 9b5f3b2e14..e2a07efe69 100644 --- a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-petri-net-resource.service.ts +++ b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-petri-net-resource.service.ts @@ -27,10 +27,10 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * * **Request Type:** GET * - * **Request URL:** {{baseUrl}}/api/public/petrinet/{id} + * **Request URL:** {{baseUrl}}/api/petrinet/public/{id} */ public getOneById(netId: string, params?: Params): Observable { - return this.provider.get$('public/petrinet/' + netId, this.SERVER_URL, params) + return this.provider.get$('petrinet/public/' + netId, this.SERVER_URL, params) .pipe(map(r => this.changeType(r, undefined))); } @@ -39,10 +39,10 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * * **Request Type:** GET * - * **Request URL:** {{baseUrl}}/api/public/petrinet/{identifier}/{version} + * **Request URL:** {{baseUrl}}/api/petrinet/public/{identifier}/{version} */ public getOne(identifier: string, version: string, params?: Params): Observable { - return this.provider.get$('public/petrinet/' + btoa(identifier) + '/' + version, this.SERVER_URL, params) + return this.provider.get$('petrinet/public/' + btoa(identifier) + '/' + version, this.SERVER_URL, params) .pipe(map(r => this.changeType(r, 'petriNetReferences'))); } @@ -54,7 +54,7 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * **Request URL:** {{baseUrl}}/api/petrinet/search */ public searchPetriNets(body: PetriNetRequestBody, params?: Params): Observable> { - return this._resourceProvider.post$('public/petrinet/search', this.SERVER_URL, body, params) + return this._resourceProvider.post$('petrinet/public/search', this.SERVER_URL, body, params) // .pipe(map(r => this.getResourcePage(r, 'petriNetReferences'))); .pipe(map(r => this.mapToPage(r))); } @@ -67,7 +67,7 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * **Request URL:** {{baseUrl}}/api/petrinet/{id}/roles */ public getPetriNetRoles(netId: string, params?: Params): Observable { - return this._resourceProvider.get$('public/petrinet/' + netId + '/roles', this.SERVER_URL, params) + return this._resourceProvider.get$('petrinet/public/' + netId + '/roles', this.SERVER_URL, params) .pipe(map(r => this.changeType(r, 'processRoles'))); } @@ -79,7 +79,7 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * **Request URL:** {{baseUrl}}/api/petrinet/{id}/transactions */ public getPetriNetTransactions(netId: string, params?: Params): Observable> { - return this._resourceProvider.get$('public/petrinet/' + netId + '/transactions', this.SERVER_URL, params) + return this._resourceProvider.get$('petrinet/public/' + netId + '/transactions', this.SERVER_URL, params) .pipe(map(r => this.changeType(r, 'transactions'))); } @@ -91,7 +91,7 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * **Request URL:** {{baseUrl}}/api/petrinet/data */ public getDataPetriNet(body: object): Observable { // TODO: response - return this._resourceProvider.post$('public/petrinet/data', this.SERVER_URL, body) + return this._resourceProvider.post$('petrinet/public/data', this.SERVER_URL, body) .pipe(map(r => this.changeType(r, undefined))); } @@ -103,7 +103,7 @@ export class PublicPetriNetResourceService extends PetriNetResourceService { * **Request URL:** {{baseUrl}}/api/petrinet/transitions */ public getPetriNetTransitions(netId: string): Observable> { - return this._resourceProvider.get$('public/petrinet/transitions', this.SERVER_URL, new HttpParams().set('ids', netId)) + return this._resourceProvider.get$('petrinet/public/transitions', this.SERVER_URL, new HttpParams().set('ids', netId)) .pipe(map(r => this.changeType(r, 'transitionReferences'))); } } diff --git a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-task-resource.service.ts b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-task-resource.service.ts index 9809009a72..6ba4e37d60 100644 --- a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-task-resource.service.ts +++ b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/public/public-task-resource.service.ts @@ -32,9 +32,9 @@ export class PublicTaskResourceService extends TaskResourceService { * Assign task * GET */ - // {{baseUrl}}/api/public/task/assign/:id + // {{baseUrl}}/api/task/public/assign/:id public assignTask(taskId: string): Observable { - return this._provider.get$('public/task/assign/' + taskId, this.SERVER_URL) + return this._provider.get$('task/public/assign/' + taskId, this.SERVER_URL) .pipe(map(r => this.changeType(r, undefined))); } @@ -42,9 +42,9 @@ export class PublicTaskResourceService extends TaskResourceService { * Cancel task * GET */ - // {{baseUrl}}/api/public/task/cancel/:id + // {{baseUrl}}/api/task/public/cancel/:id public cancelTask(taskId: string): Observable { - return this._provider.get$('public/task/cancel/' + taskId, this.SERVER_URL) + return this._provider.get$('task/public/cancel/' + taskId, this.SERVER_URL) .pipe(map(r => this.changeType(r, undefined))); } @@ -52,9 +52,9 @@ export class PublicTaskResourceService extends TaskResourceService { * Finish task * GET */ - // {{baseUrl}}/api/public/task/finish/:id + // {{baseUrl}}/api/task/public/finish/:id public finishTask(taskId: string): Observable { - return this._provider.get$('public/task/finish/' + taskId, this.SERVER_URL) + return this._provider.get$('task/public/finish/' + taskId, this.SERVER_URL) .pipe(map(r => this.changeType(r, undefined))); } @@ -62,9 +62,18 @@ export class PublicTaskResourceService extends TaskResourceService { * Get tasks of the case * GET */ - // {{baseUrl}}/api/public/task/case/:id + public getAllTasksByCases(caseIds: string[]): Observable> { + return this._resourceProvider.post$('task/public/case', this.SERVER_URL, caseIds) + .pipe(map(r => this.getResourcePage(r, 'tasks'))); + } + + /** + * Get tasks of the case + * GET + */ + // {{baseUrl}}/api/task/public/case/:id public getAllTasksByCase(caseId: string): Observable> { - return this._provider.get$('public/task/case/' + caseId, this.SERVER_URL) + return this._provider.get$('task/public/case/' + caseId, this.SERVER_URL) .pipe(map(r => this.changeType(r, undefined))); } @@ -77,9 +86,9 @@ export class PublicTaskResourceService extends TaskResourceService { * * @returns the raw backend response without any additional processing */ - // {{baseUrl}}/api/public/task/:id/data + // {{baseUrl}}/api/task/public/:id/data public rawGetData(taskId: string): Observable { - return this._provider.get$('public/task/' + taskId + '/data', this.SERVER_URL) + return this._provider.get$('task/public/' + taskId + '/data', this.SERVER_URL) .pipe(map(r => this.changeType(r, 'dataGroups'))); } @@ -87,9 +96,9 @@ export class PublicTaskResourceService extends TaskResourceService { * Set task data * POST */ - // {{baseUrl}}/api/public/task/:id/data + // {{baseUrl}}/api/task/public/:id/data public setData(taskId: string, body: TaskSetDataRequestBody): Observable { - return this._provider.post$('public/task/' + taskId + '/data', this.SERVER_URL, body) + return this._provider.post$('task/public/' + taskId + '/data', this.SERVER_URL, body) .pipe(map(r => this.changeType(r, undefined))); } @@ -101,7 +110,7 @@ export class PublicTaskResourceService extends TaskResourceService { * Attempting to use it will display a warning and remove the attribute from the request. * @param params Additional request parameters */ - // {{baseUrl}}/api/public/task/search + // {{baseUrl}}/api/task/public/search public getTasks(filterParam: Filter, params?: Params): Observable> { if (filterParam.type !== FilterType.TASK) { throw new Error('Provided filter doesn\'t have type TASK'); @@ -112,7 +121,7 @@ export class PublicTaskResourceService extends TaskResourceService { } params = ResourceProvider.combineParams(filterParam.getRequestParams(), params); - return this._provider.post$('public/task/search', this.SERVER_URL, filterParam.getRequestBody(), params) + return this._provider.post$('task/public/search', this.SERVER_URL, filterParam.getRequestBody(), params) .pipe(map(r => this.getResourcePage(r, 'tasks'))); } @@ -121,7 +130,7 @@ export class PublicTaskResourceService extends TaskResourceService { * GET */ public downloadFile(taskId: string, params: HttpParams): Observable { - const url = `public/task/${taskId}/file${params?.has("fileName") ? '/named' : ''}`; + const url = `task/public/${taskId}/file${params?.has("fileName") ? '/named' : ''}`; return this._resourceProvider.getBlob$(url, this.SERVER_URL, params).pipe( map(event => { switch (event.type) { @@ -143,7 +152,7 @@ export class PublicTaskResourceService extends TaskResourceService { */ public uploadFile(taskId: string, body: object, multipleFiles: boolean): Observable { - const url = `public/task/${taskId}/${multipleFiles ? 'files' : 'file'}`; + const url = `task/public/${taskId}/${multipleFiles ? 'files' : 'file'}`; return this._resourceProvider.postWithEvent$(url, this.SERVER_URL, body).pipe( map(event => { switch (event.type) { @@ -164,7 +173,7 @@ export class PublicTaskResourceService extends TaskResourceService { * DELETE */ public deleteFile(taskId: string, body: FileFieldRequest): Observable { - const url = `public/task/${taskId}/file${body.fileName ? '/named' : ''}`; + const url = `task/public/${taskId}/file${body.fileName ? '/named' : ''}`; return this._resourceProvider.delete$(url, this.SERVER_URL, {}, {}, 'json', body).pipe( map(r => this.changeType(r, undefined)) ); @@ -175,7 +184,7 @@ export class PublicTaskResourceService extends TaskResourceService { * GET */ public downloadFilePreview(taskId: string, params: HttpParams): Observable { - const url = `public/task/${taskId}/file_preview`; + const url = `task/public/${taskId}/file_preview`; return this._resourceProvider.getBlob$(url, this.SERVER_URL, params).pipe( map(event => { switch (event.type) { diff --git a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.spec.ts b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.spec.ts index 6e55219f34..99aa7e1888 100644 --- a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.spec.ts +++ b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.spec.ts @@ -153,8 +153,8 @@ describe('TaskResourceService', () => { it('should getAllTasksByCases', (done) => { inject([HttpTestingController], (httpMock: HttpTestingController) => { - service.getAllTasksByCases({}).subscribe(res => { - expect(res.length).toEqual(0); + service.getAllTasksByCases([]).subscribe(res => { + expect(res.content.length).toEqual(0); done(); }); diff --git a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.ts b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.ts index ce3918d8d3..0ca953b97f 100644 --- a/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.ts +++ b/projects/netgrif-components-core/src/lib/resources/engine-endpoint/task-resource.service.ts @@ -141,9 +141,9 @@ export class TaskResourceService extends AbstractResourceService implements Coun * POST */ // {{baseUrl}}/api/task/case - public getAllTasksByCases(body: object): Observable> { // TODO: ?? - return this._resourceProvider.post$('task/case', this.SERVER_URL, body) - .pipe(map(r => this.changeType(r, 'tasks'))); + public getAllTasksByCases(caseIds: string[]): Observable> { + return this._resourceProvider.post$('task/case', this.SERVER_URL, caseIds) + .pipe(map(r => this.getResourcePage(r, 'tasks'))); } /** @@ -312,7 +312,7 @@ export class TaskResourceService extends AbstractResourceService implements Coun */ public deleteFile(taskId: string, body?: FileFieldRequest): Observable { const url = `task/${taskId}/file${body?.fileName ? '/named' : ''}`; - return this._resourceProvider.delete$(url, this.SERVER_URL, {}, {}, 'json', body).pipe( + return this._resourceProvider.delete$(url , this.SERVER_URL, {}, {}, 'json', body).pipe( map(r => this.changeType(r, undefined)) ); } diff --git a/projects/netgrif-components-core/src/lib/search/models/category/case/case-string-id.ts b/projects/netgrif-components-core/src/lib/search/models/category/case/case-string-id.ts index 7b1f6ef09b..895ec157e6 100644 --- a/projects/netgrif-components-core/src/lib/search/models/category/case/case-string-id.ts +++ b/projects/netgrif-components-core/src/lib/search/models/category/case/case-string-id.ts @@ -41,7 +41,7 @@ export class CaseStringId extends NoConfigurationCategory { protected get elasticKeywords(): Array { if (!!this._optionalDependencies) { const resolver = this._optionalDependencies.searchIndexResolver; - return [resolver.getCoreIndex(CaseSearch.STRING_ID, this.isSelectedOperator(Substring))]; + return [resolver.getCoreIndex(CaseSearch.STRING_ID, this.isSelectedOperator(Substring) || this.isSelectedOperator(Equals))]; } else { return this._elasticKeywords; } diff --git a/projects/netgrif-components-core/src/lib/search/models/category/case/case-visual-id.ts b/projects/netgrif-components-core/src/lib/search/models/category/case/case-visual-id.ts index d009b9fed9..09efdca346 100644 --- a/projects/netgrif-components-core/src/lib/search/models/category/case/case-visual-id.ts +++ b/projects/netgrif-components-core/src/lib/search/models/category/case/case-visual-id.ts @@ -37,7 +37,7 @@ export class CaseVisualId extends NoConfigurationCategory { protected get elasticKeywords(): Array { if (!!this._optionalDependencies) { const resolver = this._optionalDependencies.searchIndexResolver; - return [resolver.getCoreIndex(CaseSearch.VISUAL_ID, this.isSelectedOperator(Substring))]; + return [resolver.getCoreIndex(CaseSearch.VISUAL_ID, this.isSelectedOperator(Substring) || this.isSelectedOperator(Equals))]; } else { return this._elasticKeywords; } diff --git a/projects/netgrif-components-core/src/lib/tabs/classes/tab-view.ts b/projects/netgrif-components-core/src/lib/tabs/classes/tab-view.ts index bc2feec17e..a81e28193d 100644 --- a/projects/netgrif-components-core/src/lib/tabs/classes/tab-view.ts +++ b/projects/netgrif-components-core/src/lib/tabs/classes/tab-view.ts @@ -33,6 +33,11 @@ export class TabView implements TabViewInterface { public selectedIndex: FormControl; private uniqueIdCounter = new IncrementingCounter(); + + private _switching = false; + + private _pendingTabUniqueId: string | undefined; + /** * @ignore * Holds a reference to an object that hides some public attributes and methods from tabs. @@ -274,17 +279,40 @@ export class TabView implements TabViewInterface { } public tabChange(event: MatTabChangeEvent) { - if (event.index !== this.selectedIndex.value) { + const pendingTabUniqueId = this.openedTabs[event.index]?.uniqueId; + if (pendingTabUniqueId === undefined) { + return; + } + this._pendingTabUniqueId = pendingTabUniqueId; + if (this._switching) { + return; // an update is already being processed; the pending value will be picked up after + } + this._processSwitch(); + } + + private _processSwitch() { + this._switching = true; + const uniqueId = this._pendingTabUniqueId; + const index = this.openedTabs.findIndex(tab => tab.uniqueId === uniqueId); + + if (index !== -1 && index !== this.selectedIndex.value) { let tab = this.openedTabs[this.selectedIndex.value]; if (tab) { tab.tabSelected$.next(false); } - tab = this.openedTabs[event.index]; + tab = this.openedTabs[index]; if (tab) { tab.tabSelected$.next(true); } - this.selectedIndex.setValue(event.index); + this.selectedIndex.setValue(index); } + + setTimeout(() => { + this._switching = false; + if (this._pendingTabUniqueId !== uniqueId) { + this._processSwitch(); + } + }, 150); } /** diff --git a/projects/netgrif-components-core/src/lib/task/services/assign-policy.service.ts b/projects/netgrif-components-core/src/lib/task/services/assign-policy.service.ts index d2ec6e715e..db703f3774 100644 --- a/projects/netgrif-components-core/src/lib/task/services/assign-policy.service.ts +++ b/projects/netgrif-components-core/src/lib/task/services/assign-policy.service.ts @@ -55,11 +55,8 @@ export class AssignPolicyService extends TaskHandlingService { if (!this._userService.isCurrentUserEmpty()) { this.performAssign(taskOpened, afterAction); } else { - race([ - this._userService.anonymousUser$, - this._userService.user$ - ]) - .pipe(filter(user => !this._userService.isUserEmpty(user))) + // this._userService.anonymousUser$, + this._userService.user$.pipe(filter(user => !this._userService.isUserEmpty(user))) .pipe(take(1)) .subscribe(user => this.performAssign(taskOpened, afterAction)); } diff --git a/projects/netgrif-components-core/src/lib/user/models/user.ts b/projects/netgrif-components-core/src/lib/user/models/user.ts index 86786fc2ca..bbc6c71fe3 100644 --- a/projects/netgrif-components-core/src/lib/user/models/user.ts +++ b/projects/netgrif-components-core/src/lib/user/models/user.ts @@ -60,4 +60,8 @@ export class User implements IUser { return !!this.impersonated; } + public isAnonymous(): boolean { + return this.authorities.length === 1 && this.authorities[0].includes('ANONYMOUS'); + } + } diff --git a/projects/netgrif-components-core/src/lib/user/services/user-comparator.service.ts b/projects/netgrif-components-core/src/lib/user/services/user-comparator.service.ts index f2b12b586b..a96bbeb7a1 100644 --- a/projects/netgrif-components-core/src/lib/user/services/user-comparator.service.ts +++ b/projects/netgrif-components-core/src/lib/user/services/user-comparator.service.ts @@ -11,6 +11,6 @@ export class UserComparatorService { public compareUsers(userId: string, acceptImpersonator: boolean = true): boolean { const loggedUser = acceptImpersonator ? this._userService.user.getSelfOrImpersonated() : this._userService.user; - return userId === loggedUser.id || this._userService.user.email.includes('anonymous'); + return userId === loggedUser.id || this._userService.user.isAnonymous(); } } diff --git a/projects/netgrif-components-core/src/lib/user/services/user-preference.service.ts b/projects/netgrif-components-core/src/lib/user/services/user-preference.service.ts index c2b9f8201e..f05bcd1012 100644 --- a/projects/netgrif-components-core/src/lib/user/services/user-preference.service.ts +++ b/projects/netgrif-components-core/src/lib/user/services/user-preference.service.ts @@ -19,9 +19,7 @@ export class UserPreferenceService implements OnDestroy { protected _preferences: Preferences; protected _preferencesChanged$: Subject; protected _sub: Subscription; - protected _subAnonym: Subscription; public _drawerWidthChanged$: Subject; - protected _anonym: boolean; constructor(protected _userService: UserService, protected _userResourceService: UserResourceService, @@ -31,7 +29,6 @@ export class UserPreferenceService implements OnDestroy { this._preferences = this._emptyPreferences(); this._preferencesChanged$ = new Subject(); this._drawerWidthChanged$ = new Subject(); - this._anonym = false; this._sub = this._userService.user$.subscribe(loggedUser => { if (loggedUser && loggedUser.id !== '') { @@ -47,22 +44,6 @@ export class UserPreferenceService implements OnDestroy { } }); - this._subAnonym = this._userService.anonymousUser$.subscribe(loggedUser => { - if (loggedUser && loggedUser.id !== '') { - this._userResourceService.getPublicPreferences().subscribe(prefs => { - this._preferences = this._emptyPreferences(); - Object.assign(this._preferences, prefs); - this._preferencesChanged$.next(); - this._anonym = true; - } - ); - } else { - this._preferences = this._emptyPreferences(); - this._preferencesChanged$.next(); - this._anonym = false; - } - }); - this._drawerWidthChanged$.asObservable().pipe( debounceTime(DRAWER_DEBOUNCE) ).subscribe(newWidth => { @@ -125,15 +106,9 @@ export class UserPreferenceService implements OnDestroy { } protected _savePreferences(): void { - if (!this._anonym) { - this._userResourceService.setPreferences(this._preferences).subscribe(resultMessage => { - this.resultMessage(resultMessage); - }); - } else { - this._userResourceService.setPublicPreferences(this._preferences).subscribe(resultMessage => { - this.resultMessage(resultMessage); - }); - } + this._userResourceService.setPreferences(this._preferences).subscribe(resultMessage => { + this.resultMessage(resultMessage); + }); } protected resultMessage(resultMessage): void { diff --git a/projects/netgrif-components-core/src/lib/user/services/user.service.spec.ts b/projects/netgrif-components-core/src/lib/user/services/user.service.spec.ts index e79ef9385b..ab3422f05c 100644 --- a/projects/netgrif-components-core/src/lib/user/services/user.service.spec.ts +++ b/projects/netgrif-components-core/src/lib/user/services/user.service.spec.ts @@ -40,6 +40,15 @@ describe('UserService', () => { }); }); + it('should login with an API token', (done) => { + service.loginWithApiToken('user-id.secret', 'Admin').subscribe(res => { + expect(res.id).toEqual('id'); + expect(service.user.id).toEqual('id'); + expect(service.hasAuthority('ADMIN')).toBeTrue(); + done(); + }); + }); + it('should logout', (done) => { service.logout().subscribe(res => { expect(res).toEqual(undefined); diff --git a/projects/netgrif-components-core/src/lib/user/services/user.service.ts b/projects/netgrif-components-core/src/lib/user/services/user.service.ts index 2ef8a10529..97ab49e020 100644 --- a/projects/netgrif-components-core/src/lib/user/services/user.service.ts +++ b/projects/netgrif-components-core/src/lib/user/services/user.service.ts @@ -12,7 +12,6 @@ import {LoggerService} from '../../logger/services/logger.service'; import {HttpErrorResponse} from '@angular/common/http'; import {SessionService} from '../../authentication/session/services/session.service'; import {UserResource} from '../../resources/interface/user-resource'; -import {AnonymousService} from '../../authentication/anonymous/anonymous.service'; @Injectable({ @@ -22,11 +21,8 @@ export class UserService implements OnDestroy { protected _user: User; protected _userChange$: ReplaySubject; - protected _anonymousUserChange$: ReplaySubject; protected _loginCalled: boolean; protected _subAuth: Subscription; - protected _subAnonym: Subscription; - private _publicLoadCalled: boolean; public readonly GLOBAL_ROLE_PREFIX = 'global_'; @@ -35,12 +31,10 @@ export class UserService implements OnDestroy { protected _userTransform: UserTransformer, protected _log: LoggerService, protected _session: SessionService, - protected _anonymousService: AnonymousService, protected _config: ConfigurationService) { this._user = this.emptyUser(); this._loginCalled = false; this._userChange$ = new ReplaySubject(1); - this._anonymousUserChange$ = new ReplaySubject(1); this._config.loaded$ .pipe( filter(loaded => loaded), @@ -56,14 +50,6 @@ export class UserService implements OnDestroy { } }); }); - this._subAnonym = this._anonymousService.tokenSet.subscribe(token => { - if (token) { - this.loadPublicUser(); - } else { - this.clearUser(); - this.publishAnonymousUserChange(); - } - }); }); } @@ -79,15 +65,10 @@ export class UserService implements OnDestroy { return this.anonymousUser; } - get anonymousUser$(): Observable { - return this._anonymousUserChange$.asObservable(); - } ngOnDestroy(): void { this._userChange$.complete(); - this._anonymousUserChange$.complete(); this._subAuth.unsubscribe(); - this._subAnonym.unsubscribe(); } /** @@ -172,6 +153,17 @@ export class UserService implements OnDestroy { ); } + public loginWithApiToken(token: string, realmId?: string): Observable { + this._loginCalled = true; + return this._authService.loginWithApiToken(token, realmId).pipe( + tap((authUser: User) => { + this._user = authUser; + this._loginCalled = false; + this.publishUserChange(); + }) + ); + } + public logout(): Observable { return this._authService.logout().pipe( tap(() => { @@ -186,7 +178,7 @@ export class UserService implements OnDestroy { } protected emptyUser() { - return new User('', '', '', '', '', '', [], [], [], []); + return new User('', '', '', '', '', '', ['ANONYMOUS'], [], [], []); } protected loadUser(): void { @@ -206,19 +198,6 @@ export class UserService implements OnDestroy { }); } - public loadPublicUser(): void { - this._userResource.getPublicLoggedUser().pipe(take(1)).subscribe((user: UserResource) => { - if (user) { - const backendUser = {...user, id: user.id.toString()}; - this._user = this._userTransform.transform(backendUser); - this.publishAnonymousUserChange(); - } - }, error => { - this._log.error('Loading logged user has failed! Initialisation has not be completed successfully!', error); - this._publicLoadCalled = false; - }); - } - public clearUser() { this._user = this.emptyUser(); } @@ -234,8 +213,4 @@ export class UserService implements OnDestroy { protected publishUserChange(): void { this._userChange$.next(this.user); } - - protected publishAnonymousUserChange(): void { - this._anonymousUserChange$.next(this.user); - } } diff --git a/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication-method-service.ts b/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication-method-service.ts index 3bf1ab5798..02e8eb804b 100644 --- a/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication-method-service.ts +++ b/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication-method-service.ts @@ -9,6 +9,10 @@ export class MockAuthenticationMethodService extends AuthenticationMethodService groups: [], authorities: [], nextGroups: [], processRoles: []}); } + loginWithApiToken(_token: string, _realmId?: string): Observable { + return this.login({username: '', password: ''}); + } + logout(): Observable { return of(undefined); } diff --git a/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication.service.ts b/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication.service.ts index e97d420640..aca54ed193 100644 --- a/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication.service.ts +++ b/projects/netgrif-components-core/src/lib/utility/tests/mocks/mock-authentication.service.ts @@ -23,6 +23,10 @@ export class MockAuthenticationService extends AuthenticationService { return of(new User('id', 'username', 'mail', 'realmId', 'name', 'surname', ['ADMIN'], [{stringId: 'id', name: 'id', importId: 'id'}])); } + loginWithApiToken(_token: string, _realmId?: string): Observable { + return this.login({username: '', password: ''}); + } + logout(): Observable { return of(undefined); } diff --git a/projects/netgrif-components-core/src/lib/utility/tests/test-config.ts b/projects/netgrif-components-core/src/lib/utility/tests/test-config.ts index 18923cb419..00cf12cb3d 100644 --- a/projects/netgrif-components-core/src/lib/utility/tests/test-config.ts +++ b/projects/netgrif-components-core/src/lib/utility/tests/test-config.ts @@ -447,6 +447,19 @@ export class TestConfigurationService extends ConfigurationService { } } +export class ApiTokenTestConfigurationService extends TestConfigurationService { + constructor() { + super(); + this.configuration.providers.auth.apiToken = { + enabled: true, + queryParameter: 'token', + realmQueryParameter: 'realmId', + removeFromUrl: true, + allowedPaths: ['/tabbed-views', '/tabbed-views/**'] + }; + } +} + class TestHttp extends HttpHandler { handle(req: HttpRequest): Observable> { return undefined; diff --git a/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.spec.ts b/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.spec.ts index 89198b8369..83732a92aa 100644 --- a/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.spec.ts +++ b/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.spec.ts @@ -89,7 +89,7 @@ describe('TaskViewService', () => { // NAE-968 it('should process second filter change before first filter call returns', fakeAsync(() => { let tasks: Array; - + console.log(service); service.tasks$.subscribe(receivedTasks => { tasks = receivedTasks; }); @@ -173,4 +173,8 @@ class MyResources { getTasks(): Observable> { return this.returnResponse(); } + + public getAllTasksByCases(caseIds: string[]): Observable> { + return this.returnResponse(); + } } diff --git a/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.ts b/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.ts index 8737f62175..04e94cb4f0 100644 --- a/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.ts +++ b/projects/netgrif-components-core/src/lib/view/task-view/service/task-view.service.ts @@ -222,7 +222,10 @@ export class TaskViewService extends AbstractSortableViewComponent implements On this._loading$.on(requestContext.filter); let request: Observable>; - if (requestContext.filter.bodyContainsQuery() || this._preferredEndpoint === TaskEndpoint.ELASTIC) { + if (this._userService.user.isAnonymous()) { + const caseIds = Array.isArray(requestContext.filter.getRequestBody()['case']) ? requestContext.filter.getRequestBody()['case'][0].id : [requestContext.filter.getRequestBody()['case']?.id ?? '']; + request = this._taskService.getAllTasksByCases(caseIds).pipe(take(1)); + } else if (requestContext.filter.bodyContainsQuery() || this._preferredEndpoint === TaskEndpoint.ELASTIC) { request = timer(200).pipe( switchMap(() => this._taskService.searchTask(requestContext.filter, params).pipe(take(1))) ); diff --git a/projects/netgrif-components-core/src/schema/nae-schema.json b/projects/netgrif-components-core/src/schema/nae-schema.json index ac45d84f42..06eae8593c 100644 --- a/projects/netgrif-components-core/src/schema/nae-schema.json +++ b/projects/netgrif-components-core/src/schema/nae-schema.json @@ -35,6 +35,9 @@ "authentication": { "type": "string" }, + "apiToken": { + "$ref": "#/definitions/ApiTokenAuthentication" + }, "endpoints": { "anyOf": [ { @@ -62,6 +65,34 @@ }, "type": "object" }, + "ApiTokenAuthentication": { + "additionalProperties": false, + "properties": { + "allowedPaths": { + "items": { + "type": "string" + }, + "type": "array" + }, + "enabled": { + "default": false, + "type": "boolean" + }, + "queryParameter": { + "default": "token", + "type": "string" + }, + "realmQueryParameter": { + "default": "realmId", + "type": "string" + }, + "removeFromUrl": { + "default": true, + "type": "boolean" + } + }, + "type": "object" + }, "CaseLayout": { "properties": { "name": { diff --git a/projects/netgrif-components/package.json b/projects/netgrif-components/package.json index 2c1be258e1..3e3c62c3f8 100644 --- a/projects/netgrif-components/package.json +++ b/projects/netgrif-components/package.json @@ -1,6 +1,6 @@ { "name": "@netgrif/components", - "version": "7.0.0-rc.19", + "version": "7.0.2", "description": "Netgrif Application Engine frontend Angular components", "homepage": "https://components.netgrif.com", "license": "SEE LICENSE IN LICENSE", @@ -29,7 +29,7 @@ "nae frontend" ], "peerDependencies": { - "@netgrif/components-core": "7.0.0-rc.19", + "@netgrif/components-core": "7.0.2", "@angular-material-components/datetime-picker": "~16.0.0", "@angular-material-components/moment-adapter": "~16.0.0", "@angular/animations": "~17.1.0",