Skip to content

Commit a96d518

Browse files
committed
Add support to get scicat token for an OIDC user. Add a scicat dataset viewer widget
1 parent 5fb8e00 commit a96d518

24 files changed

+429
-41
lines changed

scilog/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,5 @@ typings/
6363

6464
# Cache used by TypeScript's incremental build
6565
*.tsbuildinfo
66+
67+
.vscode

scilog/package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scilog/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"@angular-devkit/build-angular": "^15.2.11",
5555
"@angular/cli": "^15.2.11",
5656
"@angular/compiler-cli": "^15.2.10",
57+
"@scicatproject/scicat-sdk-ts-fetch": "^4.13.0",
5758
"@types/jasmine": "~3.6.0",
5859
"@types/jasminewd2": "~2.0.3",
5960
"@types/node": "^12.11.1",

scilog/src/app/app-config.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface AppConfig {
1212
lbBaseURL?: string;
1313
oAuth2Endpoint?: Oauth2Endpoint;
1414
help?: string;
15+
scicatLbBaseURL?: string;
16+
scicatFrontendBaseURL?: string;
1517
}
1618

1719
@Injectable()
@@ -24,7 +26,7 @@ export class AppConfigService {
2426
try {
2527
this.appConfig = await this.http.get("/assets/config.json").toPromise();
2628
} catch (err) {
27-
console.error("No config provided, applying defaults");
29+
console.error("No config provided, applying defaults", err);
2830
}
2931
}
3032

scilog/src/app/app-routing.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ import { ViewSettingsComponent } from '@shared/settings/view-settings/view-setti
1515
import { ProfileSettingsComponent } from '@shared/settings/profile-settings/profile-settings.component';
1616
import { DownloadComponent } from '@shared/download/download.component';
1717
import { NavigationGuardService } from './logbook/core/navigation-guard-service';
18+
import { AuthCallbackComponent } from './auth-callback/auth-callback.component';
1819

1920
const routes: Routes = [
2021
{ path: '', redirectTo: '/login', pathMatch: 'full' },
2122
{ path: 'login', component: LoginComponent },
23+
{ path: 'auth-callback', component: AuthCallbackComponent },
2224
{ path: 'overview', component: OverviewComponent },
2325
{ path: 'download/:fileId', component: DownloadComponent },
2426
{ path: 'logbooks/:logbookId', component: LogbookComponent,

scilog/src/app/app.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ import { MatPaginatorModule } from '@angular/material/paginator';
8888
import { MatSortModule } from '@angular/material/sort';
8989
import { OverviewScrollComponent } from './overview/overview-scroll/overview-scroll.component';
9090
import { ActionsMenuComponent } from './overview/actions-menu/actions-menu.component';
91+
import { ScicatViewerComponent } from './logbook/widgets/scicat-viewer/scicat-viewer.component';
92+
import { AuthCallbackComponent } from './auth-callback/auth-callback.component';
9193

9294
const appConfigInitializerFn = (appConfig: AppConfigService) => {
9395
return () => appConfig.loadAppConfig();
@@ -135,7 +137,9 @@ const appConfigInitializerFn = (appConfig: AppConfigService) => {
135137
ResizedDirective,
136138
OverviewTableComponent,
137139
OverviewScrollComponent,
138-
ActionsMenuComponent
140+
ActionsMenuComponent,
141+
ScicatViewerComponent,
142+
AuthCallbackComponent
139143
],
140144
imports: [
141145
BrowserModule,

scilog/src/app/auth-callback/auth-callback.component.css

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<p>auth-callback works!</p>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { AuthCallbackComponent } from './auth-callback.component';
4+
5+
describe('AuthCallbackComponent', () => {
6+
let component: AuthCallbackComponent;
7+
let fixture: ComponentFixture<AuthCallbackComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
declarations: [ AuthCallbackComponent ]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(AuthCallbackComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Component, OnDestroy, OnInit } from '@angular/core';
2+
import { ActivatedRoute, Router } from '@angular/router';
3+
import { Subscription } from 'rxjs';
4+
5+
@Component({
6+
selector: 'app-auth-callback',
7+
template: '',
8+
styles: [],
9+
})
10+
export class AuthCallbackComponent implements OnInit, OnDestroy {
11+
constructor(private route: ActivatedRoute, private router: Router) {}
12+
13+
subscriptions: Subscription[] = [];
14+
15+
ngOnInit(): void {
16+
const sub = this.route.queryParams.subscribe((params) => {
17+
const token = params['access-token'];
18+
const returnUrl = params['returnUrl'];
19+
20+
if (token) {
21+
localStorage.setItem('scicat_token', token);
22+
}
23+
24+
if (returnUrl) {
25+
this.router.navigateByUrl(returnUrl);
26+
} else {
27+
this.router.navigate(['/overview']);
28+
}
29+
});
30+
this.subscriptions.push(sub);
31+
}
32+
33+
ngOnDestroy(): void {
34+
this.subscriptions.forEach((sub) => sub.unsubscribe());
35+
}
36+
}
Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,63 @@
11
import { TestBed } from '@angular/core/testing';
22

33
import { AuthInterceptor } from './auth.interceptor';
4+
import { ServerSettingsService } from '@shared/config/server-settings.service';
5+
import { HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http';
6+
import { of } from 'rxjs';
47

58
describe('AuthInterceptor', () => {
6-
beforeEach(() => TestBed.configureTestingModule({
7-
providers: [
8-
AuthInterceptor
9-
]
10-
}));
9+
const serverSettingsService = jasmine.createSpyObj('ServerSettingsService', [
10+
'getSciCatServerAddress',
11+
]);
12+
serverSettingsService.getSciCatServerAddress.and.returnValue(
13+
'https://scicat-backend.psi.ch'
14+
);
15+
beforeEach(() =>
16+
TestBed.configureTestingModule({
17+
providers: [
18+
AuthInterceptor,
19+
{ provide: ServerSettingsService, useValue: serverSettingsService },
20+
],
21+
})
22+
);
1123

1224
it('should be created', () => {
1325
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
1426
expect(interceptor).toBeTruthy();
1527
});
28+
29+
it('should append scicat token if request to scicat backend', (done: DoneFn) => {
30+
localStorage.setItem('scicat_token', 'test_scicat_token');
31+
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
32+
const req = new HttpRequest("GET", 'https://scicat-backend.psi.ch/api/v3/datasets');
33+
const next = jasmine.createSpyObj<HttpHandler>('HttpHandler', ['handle']);
34+
next.handle.and.returnValue(of({} as HttpEvent<any>));
35+
interceptor.intercept(req, next).subscribe({
36+
next: (_httpEvent: HttpEvent<any>) => {
37+
const reqArg = next.handle.calls.mostRecent().args[0];
38+
expect(reqArg.headers.get('Authorization')).toBe('Bearer test_scicat_token');
39+
localStorage.clear();
40+
done();
41+
},
42+
error: done.fail,
43+
});
44+
});
45+
46+
it('should append scilog token if request is not to scicat backend', (done: DoneFn) => {
47+
localStorage.setItem('id_token', 'test_scilog_token');
48+
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
49+
const req = new HttpRequest("GET", 'https://scilog-backend.psi.ch/api/v1');
50+
const next = jasmine.createSpyObj<HttpHandler>('HttpHandler', ['handle']);
51+
next.handle.and.returnValue(of({} as HttpEvent<any>));
52+
interceptor.intercept(req, next).subscribe({
53+
next: (_httpEvent: HttpEvent<any>) => {
54+
const reqArg = next.handle.calls.mostRecent().args[0];
55+
expect(reqArg.headers.get('Authorization')).toBe('Bearer test_scilog_token');
56+
localStorage.clear();
57+
done();
58+
},
59+
error: done.fail,
60+
});
61+
});
1662
});
63+
Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,85 @@
1-
import { Injectable } from '@angular/core';
1+
import { inject, Injectable } from '@angular/core';
22
import {
33
HttpRequest,
44
HttpHandler,
55
HttpEvent,
66
HttpInterceptor,
77
HttpResponse,
8-
HttpErrorResponse
8+
HttpErrorResponse,
99
} from '@angular/common/http';
1010
import { Observable } from 'rxjs';
1111
import { tap } from 'rxjs/operators';
12+
import { ServerSettingsService } from '@shared/config/server-settings.service';
1213

1314
function logout() {
14-
localStorage.removeItem("id_token");
15+
localStorage.removeItem('id_token');
1516
localStorage.removeItem('id_session');
17+
localStorage.removeItem('scicat_token');
1618
sessionStorage.removeItem('scilog-auto-selection-logbook');
1719
location.href = '/login';
18-
};
19-
20-
function handle_request(handler: HttpHandler, req: HttpRequest<any>) {
21-
return handler.handle(req).pipe(tap((event: HttpEvent<any>) => {
22-
if (event instanceof HttpResponse) {
23-
// console.log(cloned);
24-
// console.log("Service Response thr Interceptor");
25-
}
26-
}, (err: any) => {
27-
if (err instanceof HttpErrorResponse) {
28-
console.log("err.status", err);
29-
if (err.status === 401) {
30-
logout();
31-
}
32-
}
33-
}));
3420
}
3521

3622
@Injectable()
3723
export class AuthInterceptor implements HttpInterceptor {
24+
private serverSettingsService = inject(ServerSettingsService);
3825

39-
intercept(req: HttpRequest<any>,
40-
next: HttpHandler): Observable<HttpEvent<any>> {
26+
private isRequestToSciCatBackend(req_url: string): boolean {
27+
try {
28+
const origin = new URL(req_url).origin;
29+
return (
30+
origin ===
31+
new URL(this.serverSettingsService.getSciCatServerAddress()).origin
32+
);
33+
} catch (err) {
34+
// new URL(...) fails for request to static assets (e.g. /assets/config.json)
35+
return false;
36+
}
37+
}
4138

42-
const idToken = localStorage.getItem("id_token");
39+
private handle_request(handler: HttpHandler, req: HttpRequest<any>) {
40+
return handler.handle(req).pipe(
41+
tap({
42+
next: (event: HttpEvent<any>) => {
43+
if (event instanceof HttpResponse) {
44+
// console.log(cloned);
45+
// console.log("Service Response thr Interceptor");
46+
}
47+
},
48+
error: (err: any) => {
49+
if (err instanceof HttpErrorResponse) {
50+
console.log('err.status', err);
51+
if (err.status === 401) {
52+
if (!this.isRequestToSciCatBackend(err.url)) {
53+
logout();
54+
} else {
55+
window.location.href = `${this.serverSettingsService.getSciCatServerAddress()}/api/v3/auth/oidc?client=scilog&returnURL=${
56+
window.location.pathname + window.location.search
57+
}`;
58+
}
59+
}
60+
}
61+
},
62+
})
63+
);
64+
}
4365

66+
intercept(
67+
req: HttpRequest<any>,
68+
next: HttpHandler
69+
): Observable<HttpEvent<any>> {
70+
let idToken = '';
71+
if (this.isRequestToSciCatBackend(req.url)) {
72+
idToken = localStorage.getItem('scicat_token');
73+
} else {
74+
idToken = localStorage.getItem('id_token');
75+
}
4476
if (idToken) {
4577
const cloned = req.clone({
46-
headers: req.headers.set("Authorization",
47-
"Bearer " + idToken)
78+
headers: req.headers.set('Authorization', 'Bearer ' + idToken),
4879
});
49-
50-
return handle_request(next, cloned);
51-
52-
}
53-
else {
54-
return handle_request(next, req);
80+
return this.handle_request(next, cloned);
81+
} else {
82+
return this.handle_request(next, req);
5583
}
5684
}
5785
}

scilog/src/app/core/auth-services/auth.service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class AuthService {
3131
logout() {
3232
localStorage.removeItem("id_token");
3333
localStorage.removeItem('id_session');
34+
localStorage.removeItem('scicat_token');
3435
sessionStorage.removeItem('scilog-auto-selection-logbook');
3536
this.forceReload=true;
3637
}

scilog/src/app/core/config/server-settings.service.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ export class ServerSettingsService {
1414
return this.appConfigService.getConfig().lbBaseURL ?? 'http://[::1]:3000/';
1515
}
1616

17+
getSciCatServerAddress() : string | undefined {
18+
return this.appConfigService.getConfig().scicatLbBaseURL;
19+
}
20+
21+
getScicatFrontendBaseUrl() : string | undefined {
22+
return this.appConfigService.getConfig().scicatFrontendBaseURL;
23+
}
24+
1725
getSocketAddress(){
1826
const lbBaseURL = this.appConfigService.getConfig().lbBaseURL ?? 'http://localhost:3000/';
1927
if (!lbBaseURL.startsWith('http')) throw new Error('BaseURL must use the http or https protocol');
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { DatasetService } from './dataset.service';
4+
5+
describe('DatasetService', () => {
6+
let service: DatasetService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(DatasetService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});

0 commit comments

Comments
 (0)