Skip to content
This repository was archived by the owner on May 14, 2025. It is now read-only.

Commit f0f0820

Browse files
klopfdrehonobc
authored andcommitted
Add client-side reverse proxy feature
When the UI is accessed over reverse-proxy the url looks like `https://reverse-proxy-url:443/scdf/` with the actual backend url like `http://somedomain.de:8080/scdf/`. If you access `_links` the backend is rendering them with the backend url rather than the reverse proxy url. This commit exchanges the protocol / host / port for the rendered links. For example, `http://somedomain.de:8080/scdf/` is replaced with `https://reverse-proxy-url:443/scdf/`. User's have to opt-in to this behavior (checkbox in settings page). If the server does not run behind a reverse proxy then nothing is changed. Resolves #1994
1 parent e53eb89 commit f0f0820

13 files changed

+103
-23
lines changed

ui/src/app/settings/settings.service.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {SettingModel} from '../shared/model/setting.model';
88
import {LocalStorageService} from 'angular-2-local-storage';
99

1010
const DEFAULT_LANG = 'en';
11+
const DEFAULT_REVERSE_PROXY_FIX_ACTIVE = 'false';
1112

1213
@Injectable({
1314
providedIn: 'root'
@@ -20,9 +21,11 @@ export class SettingsService {
2021
load(languages: Array<string>): Observable<SettingModel[]> {
2122
this.language = languages;
2223
const isDarkConfig = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
23-
let activeThemeValue: string = isDarkConfig ? 'dark' : 'default';
2424

25+
let activeThemeValue: string = isDarkConfig ? 'dark' : 'default';
2526
let activeLanguageValue: string = DEFAULT_LANG;
27+
let reverseProxyFixActiveValue: string = DEFAULT_REVERSE_PROXY_FIX_ACTIVE;
28+
2629
if (navigator?.language) {
2730
activeLanguageValue = navigator.language.split('-')[0];
2831
}
@@ -40,10 +43,15 @@ export class SettingsService {
4043
}
4144
}
4245

46+
if (this.localStorageService.get('reverseProxyFixActiveValue')) {
47+
reverseProxyFixActiveValue = this.localStorageService.get('reverseProxyFixActiveValue');
48+
}
49+
4350
const settings: SettingModel[] = [
4451
{name: 'language-active', value: activeLanguageValue},
4552
{name: 'theme-active', value: activeThemeValue},
46-
{name: 'results-per-page', value: '20'}
53+
{name: 'results-per-page', value: '20'},
54+
{name: 'reverse-proxy-fix-active', value: reverseProxyFixActiveValue}
4755
];
4856
return of(settings).pipe(tap(sett => this.store.dispatch(loaded({settings: sett}))));
4957
}
@@ -55,6 +63,9 @@ export class SettingsService {
5563
if (setting.name === 'language-active') {
5664
this.localStorageService.set('languageActiveValue', setting.value);
5765
}
66+
if (setting.name === 'reverse-proxy-fix-active') {
67+
this.localStorageService.set('reverseProxyFixActiveValue', setting.value);
68+
}
5869
return from(new Promise<void>(resolve => resolve()));
5970
}
6071

ui/src/app/settings/settings/settings.component.html

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
<h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
44
<div class="modal-body">
55
<clr-select-container *ngIf="languageActive && languages?.length > 1">
6-
<label class="clr-col-md-3">{{ 'settings.language' | translate }}</label>
6+
<label class="clr-col-md-4">{{ 'settings.language' | translate }}</label>
77
<select
88
clrSelect
99
name="language"
10-
class="clr-col-md-9"
10+
class="clr-col-md-8"
1111
[value]="languageActive.value"
1212
(change)="languageActiveSettingOnChange($event.target.value)"
1313
>
@@ -18,11 +18,11 @@ <h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
1818
<clr-control-helper>{{ 'settings.languageDescription' | translate }}</clr-control-helper>
1919
</clr-select-container>
2020
<clr-select-container *ngIf="themeActive">
21-
<label class="clr-col-md-3">{{ 'settings.theme' | translate }}</label>
21+
<label class="clr-col-md-4">{{ 'settings.theme' | translate }}</label>
2222
<select
2323
clrSelect
2424
name="theme"
25-
class="clr-col-md-9"
25+
class="clr-col-md-8"
2626
[value]="themeActive.value"
2727
(change)="themeActiveSettingOnChange($event.target.value)"
2828
>
@@ -33,11 +33,11 @@ <h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
3333
</clr-select-container>
3434

3535
<clr-select-container *ngIf="resultsPerPage">
36-
<label class="clr-col-md-3">{{ 'settings.results' | translate }}</label>
36+
<label class="clr-col-md-4">{{ 'settings.results' | translate }}</label>
3737
<select
3838
clrSelect
3939
name="theme"
40-
class="clr-col-md-9"
40+
class="clr-col-md-8"
4141
[value]="resultsPerPage.value"
4242
(change)="resultPerPageSettingOnChange($event.target.value)"
4343
>
@@ -50,6 +50,23 @@ <h3 class="modal-title">{{ 'settings.title' | translate }}</h3>
5050
{{ 'settings.resultsDescription' | translate }}
5151
</clr-control-helper>
5252
</clr-select-container>
53+
<clr-toggle-container>
54+
<label class="clr-col-md-4">{{ 'settings.reverseProxyFix' | translate }}</label>
55+
<clr-toggle-wrapper>
56+
<input
57+
type="checkbox"
58+
clrToggle
59+
name="reverseProxyFix"
60+
class="clr-col-md-8"
61+
[checked]="reverseProxyFix.value === 'true'"
62+
(change)="reverseProxyFixOnChange($event.target.checked + '')"
63+
/>
64+
<label>{{ 'settings.reverseProxyFixActive' | translate }}</label>
65+
</clr-toggle-wrapper>
66+
<clr-control-helper>
67+
{{ 'settings.reverseProxyFixDescription' | translate }}
68+
</clr-control-helper>
69+
</clr-toggle-container>
5370
</div>
5471
<div class="modal-footer">
5572
<button type="button" class="btn btn-outline" (click)="isOpen = false" translate>settings.close</button>

ui/src/app/settings/settings/settings.component.spec.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ describe('SettingsComponent', () => {
1616

1717
const initialState = {
1818
[fromSettings.settingsFeatureKey]: {
19-
settings: [{name: fromSettings.themeActiveKey, value: 'default'}]
19+
settings: [
20+
{name: fromSettings.themeActiveKey, value: 'default'},
21+
{name: fromSettings.reverseProxyFixKey, value: 'false'}
22+
]
2023
}
2124
};
2225

ui/src/app/settings/settings/settings.component.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export class SettingsComponent extends ModalDialog implements OnInit {
1111
themeActive: SettingModel;
1212
resultsPerPage: SettingModel;
1313
languageActive: SettingModel;
14+
reverseProxyFix: SettingModel;
1415
languages: Array<string>;
1516

1617
constructor(private settingsService: SettingsService) {
@@ -23,6 +24,7 @@ export class SettingsComponent extends ModalDialog implements OnInit {
2324
this.themeActive = settings.find(st => st.name === 'theme-active');
2425
this.resultsPerPage = settings.find(st => st.name === 'results-per-page');
2526
this.languageActive = settings.find(st => st.name === 'language-active');
27+
this.reverseProxyFix = settings.find(st => st.name === 'reverse-proxy-fix-active');
2628
});
2729
}
2830

@@ -37,4 +39,8 @@ export class SettingsComponent extends ModalDialog implements OnInit {
3739
languageActiveSettingOnChange(language: string): void {
3840
this.settingsService.dispatch({name: 'language-active', value: language});
3941
}
42+
43+
reverseProxyFixOnChange(active: string): void {
44+
this.settingsService.dispatch({name: 'reverse-proxy-fix-active', value: active});
45+
}
4046
}

ui/src/app/settings/store/settings.reducer.spec.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,26 @@ describe('settings/store/settings.reducer.ts', () => {
1212
let expectedState: fromSettings.SettingsState = {
1313
settings: [
1414
{name: fromSettings.themeActiveKey, value: 'value1'},
15-
{name: fromSettings.languageActiveKey, value: 'value1'}
15+
{name: fromSettings.languageActiveKey, value: 'value1'},
16+
{name: fromSettings.reverseProxyFixKey, value: 'value1'}
1617
]
1718
};
1819
let newState = fromSettings.reducer(
1920
undefined,
2021
SettingsActions.loaded({
2122
settings: [
2223
{name: fromSettings.themeActiveKey, value: 'value1'},
23-
{name: fromSettings.languageActiveKey, value: 'value1'}
24+
{name: fromSettings.languageActiveKey, value: 'value1'},
25+
{name: fromSettings.reverseProxyFixKey, value: 'value1'}
2426
]
2527
})
2628
);
2729
expect(newState).toEqual(expectedState);
2830
expectedState = {
2931
settings: [
3032
{name: fromSettings.themeActiveKey, value: 'value2'},
31-
{name: fromSettings.languageActiveKey, value: 'value1'}
33+
{name: fromSettings.languageActiveKey, value: 'value1'},
34+
{name: fromSettings.reverseProxyFixKey, value: 'value1'}
3235
]
3336
};
3437
newState = fromSettings.reducer(

ui/src/app/settings/store/settings.reducer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {SettingModel} from '../../shared/model/setting.model';
66
export const settingsFeatureKey = 'settings';
77
export const themeActiveKey = 'theme-active';
88
export const languageActiveKey = 'language-active';
9+
export const reverseProxyFixKey = 'reverse-proxy-fix-active';
910

1011
export interface SettingsState {
1112
settings: SettingModel[];
@@ -23,7 +24,8 @@ export const getSetting = (settings: SettingModel[], name: string): string =>
2324
export const initialState: SettingsState = {
2425
settings: [
2526
{name: themeActiveKey, value: 'default'},
26-
{name: languageActiveKey, value: 'en'}
27+
{name: languageActiveKey, value: 'en'},
28+
{name: reverseProxyFixKey, value: 'false'}
2729
]
2830
};
2931

ui/src/app/shared/api/schedule.service.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {TaskService} from './task.service';
44

55
describe('shared/api/schedule.service.ts', () => {
66
let mockHttp;
7+
let mockLocalStorageService;
78
let scheduleService;
89
let taskService;
910
let jsonData = {};
@@ -15,8 +16,11 @@ describe('shared/api/schedule.service.ts', () => {
1516
post: jasmine.createSpy('post'),
1617
put: jasmine.createSpy('put')
1718
};
19+
mockLocalStorageService = {
20+
get: jasmine.createSpy('get')
21+
};
1822
jsonData = {};
19-
taskService = new TaskService(mockHttp);
23+
taskService = new TaskService(mockHttp, mockLocalStorageService);
2024
scheduleService = new ScheduleService(mockHttp, taskService);
2125
});
2226

ui/src/app/shared/api/task.service.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {TaskExecution} from '../model/task-execution.model';
55

66
describe('shared/api/task.service.ts', () => {
77
let mockHttp;
8+
let mockLocalStorageService;
89
let taskService;
910
let jsonData = {};
1011
beforeEach(() => {
@@ -14,8 +15,11 @@ describe('shared/api/task.service.ts', () => {
1415
post: jasmine.createSpy('post'),
1516
put: jasmine.createSpy('put')
1617
};
18+
mockLocalStorageService = {
19+
get: jasmine.createSpy('get')
20+
};
1721
jsonData = {};
18-
taskService = new TaskService(mockHttp);
22+
taskService = new TaskService(mockHttp, mockLocalStorageService);
1923
});
2024

2125
it('getTasks', () => {
@@ -144,6 +148,7 @@ describe('shared/api/task.service.ts', () => {
144148

145149
it('getExecutionLogs', () => {
146150
mockHttp.get.and.returnValue(of(jsonData));
151+
mockLocalStorageService.get.and.returnValue(of('false'));
147152
taskService.getExecutionLogs(
148153
TaskExecution.parse({
149154
externalExecutionId: 'foo',

ui/src/app/shared/api/task.service.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import {
1313
ValuedConfigurationMetadataPropertyList
1414
} from '../model/detailed-app.model';
1515
import {UrlUtilities} from '../../url-utilities.service';
16+
import {LocalStorageService} from 'angular-2-local-storage';
1617

1718
@Injectable({
1819
providedIn: 'root'
1920
})
2021
export class TaskService {
21-
constructor(protected httpClient: HttpClient) {}
22+
constructor(protected httpClient: HttpClient, private localStorageService: LocalStorageService) {}
2223

2324
getTasks(
2425
page: number,
@@ -264,10 +265,13 @@ export class TaskService {
264265
`tasks/logs/${taskExecution.externalExecutionId}?platformName=${platformName}&schemaTarget=${taskExecution.schemaTarget}`;
265266
const params = new HttpParams({encoder: new DataflowEncoder()});
266267
return this.httpClient
267-
.get<any>(url, {
268-
headers,
269-
params
270-
})
268+
.get<any>(
269+
UrlUtilities.fixReverseProxyUrl(url, this.localStorageService.get('reverseProxyFixActiveValue') === true),
270+
{
271+
headers,
272+
params
273+
}
274+
)
271275
.pipe(catchError(ErrorUtils.catchError));
272276
}
273277

ui/src/app/url-utilities.service.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,20 @@ export class UrlUtilities {
2424
path += path.endsWith('/') ? '' : '/';
2525
return path;
2626
}
27+
28+
public static fixReverseProxyUrl(url: string, active: boolean) {
29+
if (!active) {
30+
return url;
31+
}
32+
try {
33+
const urlToFix: URL = new URL(url);
34+
const baseUrl: URL = new URL(window.location.href);
35+
urlToFix.host = baseUrl.host;
36+
urlToFix.protocol = baseUrl.protocol;
37+
urlToFix.port = baseUrl.port;
38+
return urlToFix.href;
39+
} catch (_) {
40+
return url;
41+
}
42+
}
2743
}

ui/src/assets/i18n/de.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@
244244
"darkTheme": "Dunkel",
245245
"defaultTheme": "Standard",
246246
"results": "Ergebnisse",
247-
"resultsDescription": "Sie können die Anzahl pro Seite wählen."
247+
"resultsDescription": "Sie können die Anzahl pro Seite wählen.",
248+
"reverseProxyFix": "Reverse-Proxy-Feature",
249+
"reverseProxyFixActive": "Aktivieren",
250+
"reverseProxyFixDescription": "Sie können das clientseitige Reverse-Proxy-Feature aktivieren."
248251
},
249252
"about": {
250253
"user": {

ui/src/assets/i18n/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,10 @@
248248
"darkTheme": "dark",
249249
"defaultTheme": "default",
250250
"results": "Results",
251-
"resultsDescription": "You can choose the number of results per page."
251+
"resultsDescription": "You can choose the number of results per page.",
252+
"reverseProxyFix": "Reverse-Proxy-Feature",
253+
"reverseProxyFixActive": "Activate",
254+
"reverseProxyFixDescription": "You can activate the client-side reverse proxy feature."
252255
},
253256
"about": {
254257
"user": {

ui/src/assets/i18n/ru.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,10 @@
244244
"darkTheme": "темная",
245245
"defaultTheme": "По умолчанию",
246246
"results": "Резултат",
247-
"resultsDescription": "Вы можете выбрать количество результатов на странице."
247+
"resultsDescription": "Вы можете выбрать количество результатов на странице.",
248+
"reverseProxyFix": "Функция обратного прокси",
249+
"reverseProxyFixActive": "Активировать",
250+
"reverseProxyFixDescription": "Вы можете активировать функцию обратного прокси-сервера на стороне клиента."
248251
},
249252
"about": {
250253
"user": {

0 commit comments

Comments
 (0)