diff --git a/package.json b/package.json index a426fe7..9544738 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "ng test", "lint": "ng lint", "e2e": "ng e2e", - "generate-client": "openapi --input http://127.0.0.1:5000/openapi.json --output ./src/app/openapi --client angular --useOptions --useUnionTypes", + "generate-client": "openapi --input http://127.0.0.1:5000/openapi.json --output ./src/app/openapi --client angular --useUnionTypes", "source-map-explorer": "source-map-explorer dist/*.js" }, "licenses": [ diff --git a/src/app/_helpers/http.interceptor.ts b/src/app/_helpers/http.interceptor.ts new file mode 100644 index 0000000..4087254 --- /dev/null +++ b/src/app/_helpers/http.interceptor.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core' +import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HTTP_INTERCEPTORS } from '@angular/common/http' +import { Observable } from 'rxjs' + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + intercept(req: HttpRequest, next: HttpHandler): Observable> { + const token = localStorage.token + + if (!token) { + return next.handle(req) + } + + req = req.clone({ + headers: req.headers.set('Authorization', `Bearer ${token}`), + }) + + return next.handle(req) + } +} + +export const httpInterceptorProviders = [ + { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }, +] \ No newline at end of file diff --git a/src/app/_services/authentication.service.ts b/src/app/_services/authentication.service.ts index e66fc88..c11ee0e 100644 --- a/src/app/_services/authentication.service.ts +++ b/src/app/_services/authentication.service.ts @@ -1,16 +1,18 @@ import { Injectable } from '@angular/core' import { HttpClient, HttpHeaders } from '@angular/common/http' -import { Observable, BehaviorSubject, from, throwError } from 'rxjs' +import { Observable, BehaviorSubject, from, throwError, of } from 'rxjs' import { map, catchError } from 'rxjs/operators' import { User } from '../_models/user' -import { RoleReadNoUsers } from '../openapi' +import { RoleReadNoUsers, ApiService, Token } from '../openapi' +import { BootstrapService } from './bootstrap.service' +import { ConfigService } from './config.service' -interface AuthResponse { - access_token: string, - roles: string[] -} +// interface AuthResponse { +// access_token: string, +// roles: string[] +// } @Injectable() export class AuthenticationService { @@ -20,6 +22,9 @@ export class AuthenticationService { constructor( private _http: HttpClient, + public api: ApiService, + public bootstrapService: BootstrapService, + public configService: ConfigService, ) { // set token if saved in local storage this.user.next(JSON.parse(localStorage.getItem('user'))) @@ -54,43 +59,44 @@ export class AuthenticationService { ) } - login(userName: string, password: string): Observable { + login(username: string, password: string): Observable { const headers = new HttpHeaders({'Content-Type': 'application/x-www-form-urlencoded'}) - var formData: any = new URLSearchParams() - formData.set('username', userName) - formData.set('password', password) - return this._http.post( - '/api/token', formData, {headers: headers} - ).pipe(map( - (response: AuthResponse) => { - // login successful if there's a jwt token in the response - let token = response.access_token - if (token) { - //const decodedToken = this.helper.decodeToken(token) - // store userName and jwt token in local storage to keep user logged in between page refreshes - localStorage.setItem('user', - JSON.stringify({ - userName: userName, - token: token, - roles: response.roles, - }) - ) + // var formData: any = new URLSearchParams() + // formData.set('username', userName) + // formData.set('password', password) + return this.api.loginForAccessTokenApiTokenPost({ + username: username, + password: password + }).pipe(map( + token => { + localStorage.setItem('token', token.access_token) + // store jwt token in local storage to keep user logged in between page refreshes + // localStorage.setItem('user', + // JSON.stringify({ + // userName: username, + // token: token, + // roles: response.roles, + // }) + // ) - console.log('TODO: AuthenticationService roles to be set by refreshing bootstrap') - // this.roles = response.roles + this.bootstrapService.get().subscribe( + bsData => this.configService.setConf(bsData) + ) + return token + // this.roles = response.roles - // Notify - this.user.next(new User(userName, token)) + // Notify + // this.user.next(new User(userName, token)) - // return true to indicate successful login - // return true - } else { - this.user.next(undefined) - this.roles = [] - // return false to indicate failed login - // return false - } - return response + // return true to indicate successful login + // return true + // } else { + // this.user.next(undefined) + // this.roles = [] + // // return false to indicate failed login + // // return false + // } + // return response } )) } @@ -99,24 +105,32 @@ export class AuthenticationService { // XXX: not completly safe: the server might be down: // We should actually *check* that the logout response is OK and display message // clear token remove user from local storage to log user out - let has_token: boolean = this.user.value && !!this.user.value.token - localStorage.removeItem('user') - this.user.next(undefined) - this.roles = [] + // let has_token: boolean = this.user.value && !!this.user.value.token + const has_token: boolean = !!localStorage.getItem('token') + // localStorage.removeItem('user') + // this.user.next(undefined) + // this.roles = [] // Tell server that the user has logged out if (has_token) { - this._http.get('/auth/logout').subscribe(response => {}) + this._http.get('/api/logout').subscribe(response => {}) + localStorage.removeItem('token') } + this.bootstrapService.get().subscribe( + bsData => this.configService.setConf(bsData) + ) return has_token } logoutAdmin(): void { } - isAuthorized(roles: string[]) { + isAuthorized(roles: string[]): Observable { // Return true if at least one role in given list matches one role of the authenticated user - if (roles.length == 0) return true - return this.roles.filter(value => -1 !== roles.indexOf(value.name)).length > 0 + if (roles.length == 0) return of(true) + // return this.roles.filter(value => -1 !== roles.indexOf(value.name)).length > 0 + return this.configService.conf.pipe(map( + conf => conf.bsData?.user.roles.filter(value => -1 !== roles.indexOf(value.name)).length > 0 + )) } } diff --git a/src/app/_services/bootstrap.service.ts b/src/app/_services/bootstrap.service.ts index 47ab738..b34b419 100644 --- a/src/app/_services/bootstrap.service.ts +++ b/src/app/_services/bootstrap.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Observable } from 'rxjs' +import { Observable, map } from 'rxjs' import { ApiService, BootstrapData } from '../openapi' diff --git a/src/app/config.service.ts b/src/app/_services/config.service.ts similarity index 63% rename from src/app/config.service.ts rename to src/app/_services/config.service.ts index 7c4cd4b..f1d3ba7 100644 --- a/src/app/config.service.ts +++ b/src/app/_services/config.service.ts @@ -2,23 +2,25 @@ import { inject, Injectable } from "@angular/core" import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot } from "@angular/router" import { Subject, ReplaySubject, BehaviorSubject, Observable, map } from "rxjs" +import { BootstrapData } from "../openapi" +import { BootstrapService } from "./bootstrap.service" + export class Config { - map = {} - proj = {} - measures = {} - geo = {} + constructor( + public bsData?: BootstrapData + ) {} } @Injectable({ providedIn: 'root' }) export class ConfigService { - defaultConf: Config = { - 'map': {}, - 'proj': {}, - 'measures': {}, - 'geo': {} - } + // defaultConf: Config = { + // 'map': {}, + // 'proj': {}, + // 'measures': {}, + // 'geo': {} + // } hasConf = new ReplaySubject() public conf: BehaviorSubject = new BehaviorSubject(new Config()) @@ -36,12 +38,14 @@ export class ConfigService { } */ - setConf(c: Object) { - this.conf.value.map = c['map'] - this.conf.value.proj = c['proj'] - this.conf.value.geo = c['geo'] - this.conf.value.measures = c['measures'] - this.conf.next(this.conf.value) + setConf(bsData: BootstrapData) { + this.conf.next(new Config(bsData)) + // this.conf.value.map = c. + // this.conf.value.proj = c['proj'] + // this.conf.value.geo = c['geo'] + // this.conf.value.measures = c['measures'] + // this.conf.next(this.conf.value) + localStorage.setItem('bsData', JSON.stringify(bsData)) this.hasConf.next(undefined) } } diff --git a/src/app/_services/data.service.ts b/src/app/_services/data.service.ts index 80bbc73..87e2a32 100644 --- a/src/app/_services/data.service.ts +++ b/src/app/_services/data.service.ts @@ -36,7 +36,7 @@ export class DataService { } getList(store: string): Observable { - return this.api.getModelListApiDataProviderStoreGet({store: store}) + return this.api.getModelListApiDataProviderStoreGet(store) } getValues( @@ -61,11 +61,8 @@ export class DataService { // .set('resample', sampling) // .set('format', format) // FIXME: add the name of the value to fetch - return this.api.getModelValuesApiStoreNameValuesValueGet({ - storeName: store, - value: value, - where: JSON.stringify(p), - resample: sampling - }) + return this.api.getModelValuesApiStoreNameValuesValueGet( + store, value, JSON.stringify(p), sampling + ) } } diff --git a/src/app/_services/geojson.service.ts b/src/app/_services/geojson.service.ts index a89f541..6f0c373 100644 --- a/src/app/_services/geojson.service.ts +++ b/src/app/_services/geojson.service.ts @@ -34,9 +34,7 @@ export class GeoJsonService { } getStyle(store: string): Observable { - return this.mapService.getLayerStyleApiMapLayerStyleStoreGet( - {store: store} - ) + return this.mapService.getLayerStyleApiMapLayerStyleStoreGet(store) } getAll(url: string, store: string, params?: object): Observable { diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 6b5b0f9..24ad21a 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { configResolver } from './config.service' +import { configResolver } from './_services/config.service' import { LoginComponent } from './login/login.component' import { PageNotFoundComponent } from './pageNotFound.component' diff --git a/src/app/app.component.html b/src/app/app.component.html index 582e14d..4f591f1 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,7 +2,7 @@ @@ -25,12 +25,12 @@ - + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5ddce37..eee9b12 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core' import { Title } from '@angular/platform-browser' import { BootstrapService } from './_services/bootstrap.service' -import { ConfigService } from './config.service' +import { ConfigService } from './_services/config.service' import { MatSnackBar } from '@angular/material/snack-bar' import { AuthenticationService } from './_services/authentication.service' @@ -52,7 +52,6 @@ export class AppComponent implements OnInit { this.title = res.title || this.title this.titleService.setTitle(res.windowTitle || this.title) this.configService.setConf(res) - this.authenticationService.roles = res.user?.roles || [] if (res.redirect && (window != window.top)) { // Refusing to be embedded in an iframe let loc = res.redirect + window.location.pathname @@ -69,10 +68,5 @@ export class AppComponent implements OnInit { ) } }) - - this.authenticationService.isLoggedIn().subscribe({ - next: resp => resp, - error: (err: string) => this.snackBar.open(err, 'OK', {duration: 3000}) - }) } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index d41335f..13529ec 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,26 +1,17 @@ import { BrowserModule } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { Injector, NgModule, LOCALE_ID } from '@angular/core' +import { NgModule, LOCALE_ID } from '@angular/core' import { FormsModule } from '@angular/forms' import { HttpClientModule } from '@angular/common/http' import { MatButtonModule } from '@angular/material/button' import { MatIconModule } from '@angular/material/icon' import { MatSnackBarModule } from '@angular/material/snack-bar' -import { MatSnackBar } from '@angular/material/snack-bar' import { MatToolbarModule } from '@angular/material/toolbar' import { MatTooltipModule } from '@angular/material/tooltip' import { FlexLayoutModule } from 'ngx-flexible-layout' -// import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular' -// import { HttpLink } from 'apollo-angular/http' -// import { onError } from '@apollo/client/link/error' -// import { split, from, ApolloLink, InMemoryCache, ApolloClientOptions } from '@apollo/client/core' -// import { WebSocketLink } from '@apollo/client/link/ws' -// import { getMainDefinition, getOperationName } from '@apollo/client/utilities' -// import { DefinitionNode } from 'graphql' - import { AppComponent } from './app.component' import { PageNotFoundComponent } from './pageNotFound.component' import { LoginModule } from './login/login.module' @@ -28,18 +19,18 @@ import { AuthenticationService } from './_services/authentication.service' import { BootstrapService } from './_services/bootstrap.service' import { ModelDataService } from './_services/apollo.service' import { ActionsService } from './_services/actions.service' -import { ConfigService } from './config.service' +import { ConfigService } from './_services/config.service' import { ApiService } from './openapi/services/ApiService' import { AdminService } from './openapi/services/AdminService' import { DashboardService } from './openapi/services/DashboardService' import { GeoapiService } from './openapi/services/GeoapiService' import { MapService } from './openapi/services/MapService' +import { httpInterceptorProviders } from './_helpers/http.interceptor' import { HtmlSnackbarComponent } from './custom-snackbar/custom-snackbar.component' import { AppRoutingModule } from './app-routing.module' -import { environment } from '../environments/environment' @NgModule({ declarations: [ @@ -79,95 +70,11 @@ import { environment } from '../environments/environment' MapService, ConfigService, ModelDataService, + httpInterceptorProviders, { provide: LOCALE_ID, useValue: "en-IN" }, - // { - // provide: APOLLO_OPTIONS, - // useFactory(httpLink: HttpLink, snackBar: MatSnackBar) { - // const definitionIsMutation = (d: DefinitionNode) => { - // return d.kind === 'OperationDefinition' && d.operation === 'mutation' - // } - - // // See https://github.com/apollographql/apollo-angular/issues/1013 - // const linkQueries = httpLink.create({ - // uri: '/graphql', - // method: 'GET', - // }) - - // const linkMutations = httpLink.create({ - // uri: '/graphql', - // }) - - // const splittedLink = split( - // ({ query }) => query.definitions.some(definitionIsMutation), - // linkMutations, - // linkQueries, - // ) - - // const schedulerQueriesLink = httpLink.create({ - // uri: '/graphql_sched', - // method: 'GET', - // }) - - // const proxyLink = split( - // ({ query }) => { - // let res = query.definitions[0]['name']['value'].startsWith('scheduler_') - // return res - // }, - // schedulerQueriesLink, - // splittedLink - // ) - - // const errorLink = onError(({ graphQLErrors, networkError }) => { - // if (graphQLErrors) - // graphQLErrors.map(({ message, locations, path }) => { - // snackBar.open(`Error: ${message}`, 'close') - // console.error( - // `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, - // graphQLErrors - // ) - // } - // ) - // if (networkError) { - // snackBar.open( - // `Network error: ${networkError['statusText']}`, - // 'close', - // ) - // console.error(networkError) - // } - // }) - - // const httpLinkWithErrorHandling = from([ - // errorLink, - // proxyLink, - // ]) - - // return { - // link: httpLinkWithErrorHandling, - // cache: new InMemoryCache(), - // defaultOptions: { - // watchQuery: { - // fetchPolicy: 'network-only', - // errorPolicy: 'ignore', - // }, - // query: { - // fetchPolicy: 'network-only', - // errorPolicy: 'all', - // }, - // subscription: { - // fetchPolicy: 'network-only', - // errorPolicy: 'all', - // } - // }, - // } - // }, - // deps: [ - // HttpLink, - // MatSnackBar - // ], - // } ], bootstrap: [ AppComponent diff --git a/src/app/info/info-data.service.ts b/src/app/info/info-data.service.ts index 8e3efe5..13db5e9 100644 --- a/src/app/info/info-data.service.ts +++ b/src/app/info/info-data.service.ts @@ -10,7 +10,7 @@ import { MapControlService } from '../map/map-control.service' import { LayerNode } from '../map/models' import { Tag, TagAction } from './info-tags/tags.service' -import { ConfigService } from '../config.service' +import { ConfigService } from '../_services/config.service' import { DataService } from '../_services/data.service' import { ApiService, ModelInfo, FeatureInfo, PlotParams } from '../openapi' @@ -466,7 +466,7 @@ export class InfoDataService { public taggedLayers$ = this.taggedLayers.asObservable() getModelInfo(store: string): Observable { - return this.api.getModelInfoApiModelInfoStoreGet({store: store}) + return this.api.getModelInfoApiModelInfoStoreGet(store) console.warn('Migrate Graphql') return observableOf() // return this.apollo.query({ @@ -555,11 +555,7 @@ export class InfoDataService { } getPlotParams(store: string, id: string, value: string): Observable { - return this.api.getPlotParamsApiPlotParamsStoreGet({ - 'store': store, - 'id': id, - 'value': value - }) + return this.api.getPlotParamsApiPlotParamsStoreGet(store, id, value) console.warn('Migrate Graphql') return observableOf() // return this.apollo.query({ @@ -592,10 +588,7 @@ export class InfoDataService { } getFeatureInfo(store: string, id: string): Observable { - return this.api.getFeatureInfoApiFeatureInfoStoreIdGet({ - store: store, - id: id - }) + return this.api.getFeatureInfoApiFeatureInfoStoreIdGet(store, id) // return this.apollo.query({ // query: getFeatureInfoQuery, // variables: { @@ -710,7 +703,7 @@ export class InfoDataService { } public getTagKeys(): Observable { - return observableOf(this.configService.conf.value.map['tagKeys']) + return observableOf(this.configService.conf.value.bsData?.map['tagKeys']) // This could be fetched from the server /* return this.apollo.query({ diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 05eaf94..351a959 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -6,7 +6,7 @@ import { Router } from '@angular/router' import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms' -import { ConfigService } from '../config.service' +import { ConfigService } from '../_services/config.service' import { AuthenticationService } from '../_services/authentication.service' @Component({ @@ -41,15 +41,8 @@ export class LoginComponent implements OnInit { this.loading = true this.authenticationService.login(this.formGroup.value.userName, this.formGroup.value.password).subscribe({ next: result => { - if (result.access_token) { - // login successful - this.router.navigate(['/']) - } else { - // login failed - this.error = 'User name or password is incorrect' - this.loading = false - this.cdr.markForCheck() - } + // login successful + this.router.navigate(['/']) }, error: error => { console.error(error) diff --git a/src/app/map/controls/map-controls.component.html b/src/app/map/controls/map-controls.component.html index 0dc3fe5..5f3e320 100644 --- a/src/app/map/controls/map-controls.component.html +++ b/src/app/map/controls/map-controls.component.html @@ -34,7 +34,7 @@ step=0.01 matTooltip="Opacity of the background layer" matTooltipPosition="right" - #ngSlider> + #ngSlider> @@ -85,7 +85,7 @@ matTooltip='Filter features from survey categories by status' matTooltipPosition="right" > - {{ _status }} + {{ _status }} @@ -147,22 +147,22 @@ -