Skip to content

Commit

Permalink
Merged in bugfix/SHIBUI-1711 (pull request #459)
Browse files Browse the repository at this point in the history
Bugfix/SHIBUI-1711 - Allow app to be deployed under any configurable context in both embedded and external Tomcat

Approved-by: Ryan Mathis <rmathis@unicon.net>
Approved-by: Dmitriy Kopylenko <dkopylenko@unicon.net>
  • Loading branch information
dima767 authored and rmathis committed Jan 29, 2020
2 parents f2833c8 + e8224f4 commit 299f074
Show file tree
Hide file tree
Showing 35 changed files with 200 additions and 64 deletions.
13 changes: 8 additions & 5 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ bootWar {
)
}
from(tasks.findByPath(':ui:npm_run_buildProd').outputs) {
// into '/'
into '/public'
//Copying into this particular classpath location due too
//deployment to external Tomcat would not work with /public location
//This way, it works with both embedded and extarnal Tomcat
into 'WEB-INF/classes/resources'
}
archiveName = "${baseName}-${version}.war"
}
Expand All @@ -107,8 +109,10 @@ bootJar {
)
}
from(tasks.findByPath(':ui:npm_run_buildProd').outputs) {
// into '/'
into '/public'
//Copying into this particular classpath location due too
//deployment to external Tomcat would not work with /public location
//This way, it works with both embedded and external Tomcat
into 'WEB-INF/classes/resources'
}
archiveName = "${baseName}-${version}.jar"
}
Expand Down Expand Up @@ -166,7 +170,6 @@ dependencies {
//So it works on Java 9 without explicitly requiring to load that module (needed by Hibernate)
runtimeOnly 'javax.xml.bind:jaxb-api:2.3.0'

// TODO: these will likely only be runtimeOnly or test scope, unless we want to ship the libraries with the final product
compile "com.h2database:h2"
runtimeOnly "org.postgresql:postgresql"
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client:2.2.0'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
package edu.internet2.tier.shibboleth.admin.ui.controller;

import com.google.common.io.ByteStreams;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Controller;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.stream.Collectors;

@Controller
public class RootUiViewController {
Expand All @@ -10,4 +25,20 @@ public class RootUiViewController {
public String index() {
return "redirect:/index.html";
}

@RequestMapping(value = {"**/index.html", "/dashboard/**", "/metadata/**"})
public void indexHtml(HttpServletRequest request, HttpServletResponse response) throws IOException, URISyntaxException {
//This method is necessary in order for Angular framework to honor dynamic ServletContext
//under which shib ui application is deployed, both during initial index.html load and subsequest page refreshes
String content = new BufferedReader(new InputStreamReader(request.getServletContext()
.getResourceAsStream("/WEB-INF/classes/resources/index.html")))
.lines()
.collect(Collectors.joining("\n"));

content = content.replaceFirst("<base.+>", "<base href=\"" + request.getContextPath() + "/\">");
response.setContentType("text/html");
try (OutputStream writer = response.getOutputStream()) {
writer.write(content.getBytes());
}
}
}
24 changes: 18 additions & 6 deletions ui/proxy.conf.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
{
"/api": {
"/shibui/api": {
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug"
"logLevel": "debug",
"pathRewrite": {
"^/shibui": ""
}
},
"/actuator": {
"/shibui/actuator": {
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug"
"logLevel": "debug",
"pathRewrite": {
"^/shibui": ""
}
},
"/login": {
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug"
"logLevel": "debug",
"pathRewrite": {
"^/shibui": ""
}
},
"/logout": {
"target": "http://localhost:8080",
"secure": false,
"logLevel": "debug"
"logLevel": "debug",
"pathRewrite": {
"^/shibui": ""
}
}
}
7 changes: 4 additions & 3 deletions ui/src/app/admin/service/admin.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AdminService } from './admin.service';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';
import { HttpRequest, HttpClientModule } from '@angular/common/http';
import { Admin } from '../model/admin';
import API_BASE_PATH from '../../app.constant';

let users = <Admin[]>[
{
Expand Down Expand Up @@ -43,7 +44,7 @@ describe('Admin Service', () => {
service.query().subscribe();

backend.expectOne((req: HttpRequest<any>) => {
return req.url === '/api/admin/users'
return req.url === `${API_BASE_PATH}/admin/users`
&& req.method === 'GET';
}, `GET admin collection`);
}
Expand All @@ -55,7 +56,7 @@ describe('Admin Service', () => {
service.update({...users[0]}).subscribe();

backend.expectOne((req: HttpRequest<any>) => {
return req.url === '/api/admin/users/abc'
return req.url === `${API_BASE_PATH}/admin/users/abc`
&& req.method === 'PATCH';
}, `PATCH admin user`);
}
Expand All @@ -67,7 +68,7 @@ describe('Admin Service', () => {
service.remove(users[0].username).subscribe();

backend.expectOne((req: HttpRequest<any>) => {
return req.url === '/api/admin/users/abc'
return req.url === `${API_BASE_PATH}/admin/users/abc`
&& req.method === 'DELETE';
}, `DELETE admin user`);
}
Expand Down
4 changes: 3 additions & 1 deletion ui/src/app/admin/service/admin.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { Admin } from '../model/admin';
import { HttpClient } from '@angular/common/http';
import { map, catchError } from 'rxjs/operators';

import API_BASE_PATH from '../../app.constant';

@Injectable()
export class AdminService {

private endpoint = '/admin/users';
private base = '/api';
private base = API_BASE_PATH;

constructor(
private http: HttpClient
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/app.brand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ export const brand: Brand = {
title: 'brand.header.title'
},
logo: {
default: '/assets/shibboleth_logowordmark_color.png',
small: '/assets/shibboleth_icon_color_130x130.png',
large: '/assets/shibboleth_logowordmark_color.png',
default: 'assets/shibboleth_logowordmark_color.png',
small: 'assets/shibboleth_icon_color_130x130.png',
large: 'assets/shibboleth_logowordmark_color.png',
alt: 'brand.logo-alt',
link: {
label: 'brand.logo-link-label', // shibboleth
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@
<div class="d-flex align-items-end justify-content-end p-2">
<translate-i18n key="brand.in-partnership-with" class="flex-item mr-2">In partnership with</translate-i18n>&nbsp;
<a href="https://www.unicon.net" target="_blank" title="Unicon" class="flex-item">
<img src="/assets/logo_unicon.png" class="img-fluid float-right" alt="Unicon Logo">
<img src="assets/logo_unicon.png" class="img-fluid float-right" alt="Unicon Logo">
</a>
&nbsp;<translate-i18n key="brand.and" class="flex-item mx-2">and</translate-i18n>&nbsp;
<a href="https://www.internet2.edu/" target="_blank" title="Internet 2" class="flex-item">
<img src="/assets/logo_internet2.png" class="img-fluid float-right" alt="Internet 2 Logo">
<img src="assets/logo_internet2.png" class="img-fluid float-right" alt="Internet 2 Logo">
</a>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions ui/src/app/app.constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const API_BASE_PATH = 'api';
export default API_BASE_PATH;
21 changes: 19 additions & 2 deletions ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule, Store } from '@ngrx/store';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { StoreRouterConnectingModule, RouterStateSerializer } from '@ngrx/router-store';
Expand All @@ -24,6 +24,8 @@ import { WizardModule } from './wizard/wizard.module';
import { FormModule } from './schema-form/schema-form.module';
import { environment } from '../environments/environment.prod';
import { I18nModule } from './i18n/i18n.module';
import { ApiPathInterceptor } from './core/service/api-path.interceptor';
import { APP_BASE_HREF } from '@angular/common';

@NgModule({
declarations: [
Expand Down Expand Up @@ -63,7 +65,17 @@ import { I18nModule } from './i18n/i18n.module';
AppRoutingModule
],
providers: [
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer },
{
provide: APP_BASE_HREF,
useFactory: () => {
const url = new URL(document.getElementsByTagName('base')[0].href);
return url.pathname;
}
},
{
provide: RouterStateSerializer,
useClass: CustomRouterStateSerializer
},
{
provide: HTTP_INTERCEPTORS,
useClass: AuthorizedInterceptor,
Expand All @@ -73,6 +85,11 @@ import { I18nModule } from './i18n/i18n.module';
provide: HTTP_INTERCEPTORS,
useClass: ErrorInterceptor,
multi: true
},
{
provide: HTTP_INTERCEPTORS,
useClass: ApiPathInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/core/effect/version.effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { VersionInfo } from '../model/version';
export class VersionEffects {

private endpoint = '/info';
private base = '/actuator';
private base = 'actuator';

@Effect()
loadVersionInfo$ = this.actions$
Expand Down
41 changes: 41 additions & 0 deletions ui/src/app/core/service/api-path.interceptor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { TestBed, async, inject } from '@angular/core/testing';
import { HTTP_INTERCEPTORS, HttpClientModule, HttpClient, HttpRequest } from '@angular/common/http';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';

import { ApiPathInterceptor } from './api-path.interceptor';
import { APP_BASE_HREF } from '@angular/common';

describe('API Path Interceptor Service', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientModule,
HttpClientTestingModule
],
providers: [
{
provide: APP_BASE_HREF,
useValue: '/shibui/'
},
{
provide: HTTP_INTERCEPTORS,
useClass: ApiPathInterceptor,
multi: true
}
]
});
});

describe('query', () => {
it(`should send an expected query request`, async(inject([HttpClient, HttpTestingController],
(service: HttpClient, backend: HttpTestingController) => {
service.get('foo').subscribe();

backend.expectOne((req: HttpRequest<any>) => {
return req.url === '/shibui/foo'
&& req.method === 'GET';
}, `GET collection`);
}
)));
});
});
15 changes: 15 additions & 0 deletions ui/src/app/core/service/api-path.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Injectable, Inject } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { APP_BASE_HREF } from '@angular/common';

@Injectable()
export class ApiPathInterceptor implements HttpInterceptor {
constructor(
@Inject(APP_BASE_HREF) private baseHref: string
) {}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const apiReq = req.clone({ url: `${this.baseHref}${req.url}` });
return next.handle(apiReq);
}
}
4 changes: 2 additions & 2 deletions ui/src/app/core/service/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { User } from '../model/user';
import { HttpClient } from '@angular/common/http';
import { catchError, map } from 'rxjs/operators';
import { API_BASE_PATH } from '../../app.constant';

@Injectable()
export class UserService {

readonly base = `/api`;
readonly base = API_BASE_PATH;

constructor(
private http: HttpClient
Expand Down
3 changes: 2 additions & 1 deletion ui/src/app/i18n/service/i18n.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { HttpClient, HttpParams } from '@angular/common/http';
import { NavigatorService } from '../../core/service/navigator.service';
import { getCurrentLanguage, getCurrentLocale } from '../../shared/util';
import { Messages } from '../model/Messages';
import API_BASE_PATH from '../../app.constant';

@Injectable()
export class I18nService {

readonly path = '/messages';
readonly base = '/api';
readonly base = API_BASE_PATH;

constructor(
private http: HttpClient,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ export class ConfigurationComponent implements OnDestroy {
private store: Store<fromConfiguration.ConfigurationState>,
private routerState: ActivatedRoute
) {

this.routerState.params.pipe(
takeUntil(this.ngUnsubscribe),
map(({ id, type, version }) => new SetMetadata({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe(`Attributes Service`, () => {
const type = 'resource';
service.getVersion(resourceId, type).subscribe();
backend.expectOne((req: HttpRequest<any>) => {
return req.url === `/${service.base}/${PATHS[type]}/${resourceId}`
return req.url === `${service.base}/${PATHS[type]}/${resourceId}`
&& req.method === 'GET';
}, `GET schema by path`);
}
Expand All @@ -59,7 +59,7 @@ describe(`Attributes Service`, () => {
const versionId = '1';
service.getVersion(resourceId, type, versionId).subscribe();
backend.expectOne((req: HttpRequest<any>) => {
return req.url === `/${service.base}/${PATHS[type]}/${resourceId}/${service.path}/${versionId}`
return req.url === `${service.base}/${PATHS[type]}/${resourceId}/${service.path}/${versionId}`
&& req.method === 'GET';
}, `GET schema by path`);
}
Expand All @@ -74,7 +74,7 @@ describe(`Attributes Service`, () => {
const versionId = '1';
service.updateVersion(resourceId, type, {} as Metadata).subscribe();
backend.expectOne((req: HttpRequest<any>) => {
return req.url === `/${service.base}/${PATHS[type]}/${resourceId}`
return req.url === `${service.base}/${PATHS[type]}/${resourceId}`
&& req.method === 'PUT';
}, `PUT schema by path`);
}
Expand Down
Loading

0 comments on commit 299f074

Please sign in to comment.