Browse Source

Merge pull request #384 from Squidex/feature/ui-options

Feature/ui options
pull/381/head
Sebastian Stehle 7 years ago
committed by GitHub
parent
commit
749f13f72b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  2. 6
      src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
  3. 7
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  4. 27
      src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs
  5. 15
      src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs
  6. 16
      src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs
  7. 10
      src/Squidex/app/app.module.ts
  8. 6
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  9. 1
      src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts
  10. 29
      src/Squidex/app/framework/configurations.ts
  11. 28
      src/Squidex/app/framework/services/onboarding.service.spec.ts
  12. 13
      src/Squidex/app/framework/services/onboarding.service.ts
  13. 38
      src/Squidex/app/shared/components/geolocation-editor.component.ts
  14. 28
      src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
  15. 13
      src/Squidex/app/shared/guards/must-be-authenticated.guard.ts
  16. 30
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  17. 13
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts
  18. 2
      src/Squidex/app/shared/services/ui.service.spec.ts
  19. 3
      src/Squidex/app/shared/services/ui.service.ts
  20. 2
      src/Squidex/app/shared/services/workflows.service.spec.ts
  21. 39
      src/Squidex/appsettings.json

14
src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs

@ -5,24 +5,10 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Areas.Api.Controllers.UI.Models namespace Squidex.Areas.Api.Controllers.UI.Models
{ {
public sealed class UISettingsDto public sealed class UISettingsDto
{ {
/// <summary>
/// The type of the map control.
/// </summary>
[Required]
public string MapType { get; set; }
/// <summary>
/// The key for the map control.
/// </summary>
[Required]
public string MapKey { get; set; }
/// <summary> /// <summary>
/// True when the user can create apps. /// True when the user can create apps.
/// </summary> /// </summary>

6
src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs

@ -15,6 +15,12 @@ namespace Squidex.Areas.Api.Controllers.UI
public MapOptions Map { get; set; } public MapOptions Map { get; set; }
public bool HideNews { get; set; }
public bool HideOnboarding { get; set; }
public bool RedirectToLogin { get; set; }
public bool OnlyAdminsCanCreateApps { get; set; } public bool OnlyAdminsCanCreateApps { get; set; }
public sealed class MapOptions public sealed class MapOptions

7
src/Squidex/Areas/Api/Controllers/UI/UIController.cs

@ -48,14 +48,9 @@ namespace Squidex.Areas.Api.Controllers.UI
{ {
var result = new UISettingsDto var result = new UISettingsDto
{ {
MapType = uiOptions.Map?.Type ?? "OSM", CanCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.Permissions.Includes(CreateAppPermission)
MapKey = uiOptions.Map?.GoogleMaps?.Key
}; };
var canCreateApps = !uiOptions.OnlyAdminsCanCreateApps || Context.Permissions.Includes(CreateAppPermission);
result.CanCreateApps = canCreateApps;
return Ok(result); return Ok(result);
} }

27
src/Squidex/Areas/Frontend/Middlewares/IndexExtensions.cs

@ -7,6 +7,11 @@
using System; using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Squidex.Areas.Api.Controllers.UI;
using Squidex.Infrastructure.Json;
namespace Squidex.Areas.Frontend.Middlewares namespace Squidex.Areas.Frontend.Middlewares
{ {
@ -26,5 +31,27 @@ namespace Squidex.Areas.Frontend.Middlewares
{ {
return context.Response.ContentType?.ToLower().Contains("text/html") == true; return context.Response.ContentType?.ToLower().Contains("text/html") == true;
} }
public static string AdjustHtml(this string html, HttpContext httpContext)
{
var result = html;
if (httpContext.Request.PathBase.HasValue)
{
result = result.Replace("<base href=\"/\">", $"<base href=\"{httpContext.Request.PathBase}/\">");
}
var uiOptions = httpContext.RequestServices.GetService<IOptions<MyUIOptions>>()?.Value;
if (uiOptions != null)
{
var jsonSerializer = httpContext.RequestServices.GetRequiredService<IJsonSerializer>();
var jsonOptions = jsonSerializer.Serialize(uiOptions, false);
result = result.Replace("<body>", $"<body><script>var options = {jsonOptions};</script>");
}
return result;
}
} }
} }

15
src/Squidex/Areas/Frontend/Middlewares/IndexMiddleware.cs

@ -25,7 +25,7 @@ namespace Squidex.Areas.Frontend.Middlewares
{ {
var basePath = context.Request.PathBase; var basePath = context.Request.PathBase;
if (context.IsHtmlPath() && basePath.HasValue) if (context.IsHtmlPath() && context.Response.StatusCode != 304)
{ {
var responseBuffer = new MemoryStream(); var responseBuffer = new MemoryStream();
var responseBody = context.Response.Body; var responseBody = context.Response.Body;
@ -36,24 +36,19 @@ namespace Squidex.Areas.Frontend.Middlewares
context.Response.Body = responseBody; context.Response.Body = responseBody;
var response = Encoding.UTF8.GetString(responseBuffer.ToArray()); var html = Encoding.UTF8.GetString(responseBuffer.ToArray());
response = AdjustBase(response, basePath); html = html.AdjustHtml(context);
context.Response.ContentLength = Encoding.UTF8.GetByteCount(response); context.Response.ContentLength = Encoding.UTF8.GetByteCount(html);
context.Response.Body = responseBody; context.Response.Body = responseBody;
await context.Response.WriteAsync(response); await context.Response.WriteAsync(html);
} }
else else
{ {
await next(context); await next(context);
} }
} }
private static string AdjustBase(string response, string baseUrl)
{
return response.Replace("<base href=\"/\">", $"<base href=\"{baseUrl}/\">");
}
} }
} }

16
src/Squidex/Areas/Frontend/Middlewares/WebpackMiddleware.cs

@ -37,7 +37,7 @@ namespace Squidex.Areas.Frontend.Middlewares
{ {
var html = await result.Content.ReadAsStringAsync(); var html = await result.Content.ReadAsStringAsync();
html = AdjustBase(html, context.Request.PathBase); html = html.AdjustHtml(context);
await context.Response.WriteHtmlAsync(html); await context.Response.WriteHtmlAsync(html);
} }
@ -58,7 +58,7 @@ namespace Squidex.Areas.Frontend.Middlewares
var html = Encoding.UTF8.GetString(responseBuffer.ToArray()); var html = Encoding.UTF8.GetString(responseBuffer.ToArray());
html = AdjustBase(html, context.Request.PathBase); html = html.AdjustHtml(context);
context.Response.ContentLength = Encoding.UTF8.GetByteCount(html); context.Response.ContentLength = Encoding.UTF8.GetByteCount(html);
context.Response.Body = responseBody; context.Response.Body = responseBody;
@ -71,17 +71,5 @@ namespace Squidex.Areas.Frontend.Middlewares
await next(context); await next(context);
} }
} }
private static string AdjustBase(string html, PathString baseUrl)
{
if (baseUrl.HasValue)
{
return html.Replace("<base href=\"/\">", $"<base href=\"{baseUrl}/\">");
}
else
{
return html;
}
}
} }
} }

10
src/Squidex/app/app.module.ts

@ -23,7 +23,8 @@ import {
DecimalSeparatorConfig, DecimalSeparatorConfig,
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule, SqxSharedModule,
TitlesConfig TitlesConfig,
UIOptions
} from './shared'; } from './shared';
import { SqxShellModule } from './shell'; import { SqxShellModule } from './shell';
@ -49,6 +50,10 @@ export function configApiUrl() {
} }
} }
export function configUIOptions() {
return new UIOptions(window['options']);
}
export function configTitles() { export function configTitles() {
return new TitlesConfig({}, undefined, 'Squidex Headless CMS'); return new TitlesConfig({}, undefined, 'Squidex Headless CMS');
} }
@ -88,7 +93,8 @@ export function configCurrency() {
{ provide: ApiUrlConfig, useFactory: configApiUrl }, { provide: ApiUrlConfig, useFactory: configApiUrl },
{ provide: CurrencyConfig, useFactory: configCurrency }, { provide: CurrencyConfig, useFactory: configCurrency },
{ provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator }, { provide: DecimalSeparatorConfig, useFactory: configDecimalSeparator },
{ provide: TitlesConfig, useFactory: configTitles } { provide: TitlesConfig, useFactory: configTitles },
{ provide: UIOptions, useFactory: configUIOptions }
], ],
entryComponents: [AppComponent] entryComponents: [AppComponent]
}) })

6
src/Squidex/app/features/apps/pages/apps-page.component.ts

@ -17,6 +17,7 @@ import {
LocalStoreService, LocalStoreService,
NewsService, NewsService,
OnboardingService, OnboardingService,
UIOptions,
UIState UIState
} from '@app/shared'; } from '@app/shared';
@ -40,7 +41,8 @@ export class AppsPageComponent implements OnInit {
public readonly uiState: UIState, public readonly uiState: UIState,
private readonly localStore: LocalStoreService, private readonly localStore: LocalStoreService,
private readonly newsService: NewsService, private readonly newsService: NewsService,
private readonly onboardingService: OnboardingService private readonly onboardingService: OnboardingService,
private readonly uiOptions: UIOptions
) { ) {
} }
@ -52,7 +54,7 @@ export class AppsPageComponent implements OnInit {
if (shouldShowOnboarding && apps.length === 0) { if (shouldShowOnboarding && apps.length === 0) {
this.onboardingService.disable('dialog'); this.onboardingService.disable('dialog');
this.onboardingDialog.show(); this.onboardingDialog.show();
} else { } else if (!this.uiOptions.get('hideNews')) {
const newsVersion = this.localStore.getInt('squidex.news.version'); const newsVersion = this.localStore.getInt('squidex.news.version');
this.newsService.getFeatures(newsVersion) this.newsService.getFeatures(newsVersion)

1
src/Squidex/app/framework/angular/modals/onboarding-tooltip.component.ts

@ -45,6 +45,7 @@ export class OnboardingTooltipComponent extends StatefulComponent implements OnD
private readonly renderer: Renderer2 private readonly renderer: Renderer2
) { ) {
super(changeDetector, {}); super(changeDetector, {});
} }
public ngOnDestroy() { public ngOnDestroy() {

29
src/Squidex/app/framework/configurations.ts

@ -5,6 +5,35 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
export class UIOptions {
constructor(
private readonly value: any
) {
}
public get(path: string) {
if (!path) {
return undefined;
}
let value = this.value;
if (value) {
const parts = path.split('.');
for (let part of parts) {
value = value[part];
if (!value) {
break;
}
}
}
return value;
}
}
export class ApiUrlConfig { export class ApiUrlConfig {
public readonly value: string; public readonly value: string;

28
src/Squidex/app/framework/services/onboarding.service.spec.ts

@ -7,6 +7,8 @@
import { OnboardingService, OnboardingServiceFactory } from './onboarding.service'; import { OnboardingService, OnboardingServiceFactory } from './onboarding.service';
import { UIOptions } from './../configurations';
class LocalStoreMock { class LocalStoreMock {
private store = {}; private store = {};
@ -27,46 +29,52 @@ describe('OnboardingService', () => {
}); });
it('should instantiate from factory', () => { it('should instantiate from factory', () => {
const onboardingService = OnboardingServiceFactory(<any>localStore); const onboardingService = OnboardingServiceFactory(new UIOptions({}), <any>localStore);
expect(onboardingService).toBeDefined(); expect(onboardingService).toBeDefined();
}); });
it('should instantiate', () => { it('should instantiate', () => {
const onboardingService = new OnboardingService(<any>localStore); const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
expect(onboardingService).toBeDefined(); expect(onboardingService).toBeDefined();
}); });
it('should return true when value not in store', () => { it('should show when value not in store', () => {
localStore.set('squidex.onboarding.disable.feature-a1', '0'); localStore.set('squidex.onboarding.disable.feature-a1', '0');
const onboardingService = new OnboardingService(<any>localStore); const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
expect(onboardingService.shouldShow('feature-a2')).toBeTruthy(); expect(onboardingService.shouldShow('feature-a2')).toBeTruthy();
}); });
it('should return false when value in store', () => { it('should not show when value in store', () => {
localStore.set('squidex.onboarding.disable.feature-b1', '1'); localStore.set('squidex.onboarding.disable.feature-b1', '1');
const onboardingService = new OnboardingService(<any>localStore); const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
expect(onboardingService.shouldShow('feature-b1')).toBeFalsy(); expect(onboardingService.shouldShow('feature-b1')).toBeFalsy();
}); });
it('should return false when disabled', () => { it('should not show when disabled', () => {
const onboardingService = new OnboardingService(<any>localStore); const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
onboardingService.disable('feature-c1'); onboardingService.disable('feature-c1');
expect(onboardingService.shouldShow('feature-c1')).toBeFalsy(); expect(onboardingService.shouldShow('feature-c1')).toBeFalsy();
}); });
it('should return false when all disabled', () => { it('should not show when all disabled', () => {
const onboardingService = new OnboardingService(<any>localStore); const onboardingService = new OnboardingService(new UIOptions({}), <any>localStore);
onboardingService.disableAll(); onboardingService.disableAll();
expect(onboardingService.shouldShow('feature-d1')).toBeFalsy(); expect(onboardingService.shouldShow('feature-d1')).toBeFalsy();
}); });
it('should not show when disabled by setting', () => {
const onboardingService = new OnboardingService(new UIOptions({ hideOnboarding: true }), <any>localStore);
expect(onboardingService.shouldShow('feature-d1')).toBeFalsy();
});
}); });

13
src/Squidex/app/framework/services/onboarding.service.ts

@ -12,15 +12,20 @@ import { Injectable } from '@angular/core';
import { LocalStoreService } from './local-store.service'; import { LocalStoreService } from './local-store.service';
export const OnboardingServiceFactory = (localStore: LocalStoreService) => { import { UIOptions } from './../configurations';
return new OnboardingService(localStore);
export const OnboardingServiceFactory = (uiOptions: UIOptions, localStore: LocalStoreService) => {
return new OnboardingService(uiOptions, localStore);
}; };
@Injectable() @Injectable()
export class OnboardingService { export class OnboardingService {
constructor( private readonly disabled: boolean;
constructor(uiOptions: UIOptions,
private readonly localStore: LocalStoreService private readonly localStore: LocalStoreService
) { ) {
this.disabled = uiOptions.get('hideOnboarding');
} }
public disableAll() { public disableAll() {
@ -32,7 +37,7 @@ export class OnboardingService {
} }
public shouldShow(key: string) { public shouldShow(key: string) {
return this.shouldShowKey(key) && this.shouldShowKey('all'); return !this.disabled && this.shouldShowKey(key) && this.shouldShowKey('all');
} }
private shouldShowKey(key: string) { private shouldShowKey(key: string) {

38
src/Squidex/app/shared/components/geolocation-editor.component.ts

@ -12,7 +12,7 @@ import {
ResourceLoaderService, ResourceLoaderService,
StatefulControlComponent, StatefulControlComponent,
Types, Types,
UIState, UIOptions,
ValidatorsEx ValidatorsEx
} from '@app/shared/internal'; } from '@app/shared/internal';
@ -28,10 +28,6 @@ interface Geolocation {
longitude: number; longitude: number;
} }
interface State {
isGoogleMaps: boolean;
}
@Component({ @Component({
selector: 'sqx-geolocation-editor', selector: 'sqx-geolocation-editor',
styleUrls: ['./geolocation-editor.component.scss'], styleUrls: ['./geolocation-editor.component.scss'],
@ -39,7 +35,8 @@ interface State {
providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR], providers: [SQX_GEOLOCATION_EDITOR_CONTROL_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class GeolocationEditorComponent extends StatefulControlComponent<State, Geolocation> implements AfterViewInit { export class GeolocationEditorComponent extends StatefulControlComponent<any, Geolocation> implements AfterViewInit {
private readonly isGoogleMaps: boolean;
private marker: any; private marker: any;
private map: any; private map: any;
private value: Geolocation | null = null; private value: Geolocation | null = null;
@ -73,11 +70,11 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
constructor(changeDetector: ChangeDetectorRef, constructor(changeDetector: ChangeDetectorRef,
private readonly resourceLoader: ResourceLoaderService, private readonly resourceLoader: ResourceLoaderService,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly uiState: UIState private readonly uiOptions: UIOptions
) { ) {
super(changeDetector, { super(changeDetector, {});
isGoogleMaps: false
}); this.isGoogleMaps = uiOptions.get('map.type');
} }
public writeValue(obj: any) { public writeValue(obj: any) {
@ -154,18 +151,11 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
} }
public ngAfterViewInit() { public ngAfterViewInit() {
this.uiState.settings if (!this.isGoogleMaps) {
.subscribe(settings => { this.ngAfterViewInitOSM();
const isGoogleMaps = settings.mapType === 'GoogleMaps'; } else {
this.ngAfterViewInitGoogle(this.uiOptions.get('map.googleMaps.key'));
this.next(s => ({ ...s, isGoogleMaps })); }
if (!this.snapshot.isGoogleMaps) {
this.ngAfterViewInitOSM();
} else {
this.ngAfterViewInitGoogle(settings.mapKey);
}
});
} }
private ngAfterViewInitOSM() { private ngAfterViewInitOSM() {
@ -224,11 +214,11 @@ export class GeolocationEditorComponent extends StatefulControlComponent<State,
} }
}); });
this.map.addListener('bounds_changed', (event: any) => { this.map.addListener('bounds_changed', () => {
searchBox.setBounds(this.map.getBounds()); searchBox.setBounds(this.map.getBounds());
}); });
searchBox.addListener('places_changed', (event: any) => { searchBox.addListener('places_changed', () => {
let places = searchBox.getPlaces(); let places = searchBox.getPlaces();
if (places.length === 1) { if (places.length === 1) {

28
src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts

@ -9,24 +9,25 @@ import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared'; import { AuthService, UIOptions } from '@app/shared';
import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard'; import { MustBeAuthenticatedGuard } from './must-be-authenticated.guard';
describe('MustBeAuthenticatedGuard', () => { describe('MustBeAuthenticatedGuard', () => {
let router: IMock<Router>; let router: IMock<Router>;
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let authGuard: MustBeAuthenticatedGuard; let uiOptions = new UIOptions({ map: { type: 'OSM' } });
let uiOptionsRedirect = new UIOptions({ map: { type: 'OSM' }, redirectToLogin: true });
beforeEach(() => { beforeEach(() => {
router = Mock.ofType<Router>(); router = Mock.ofType<Router>();
authService = Mock.ofType<AuthService>(); authService = Mock.ofType<AuthService>();
authGuard = new MustBeAuthenticatedGuard(authService.object, router.object);
}); });
it('should navigate to default page if not authenticated', () => { it('should navigate to default page if not authenticated', () => {
const authGuard = new MustBeAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges) authService.setup(x => x.userChanges)
.returns(() => of(null)); .returns(() => of(null));
@ -42,6 +43,8 @@ describe('MustBeAuthenticatedGuard', () => {
}); });
it('should return true if authenticated', () => { it('should return true if authenticated', () => {
const authGuard = new MustBeAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges) authService.setup(x => x.userChanges)
.returns(() => of(<any>{})); .returns(() => of(<any>{}));
@ -55,4 +58,21 @@ describe('MustBeAuthenticatedGuard', () => {
router.verify(x => x.navigate(It.isAny()), Times.never()); router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
it('should login redirect if redirect enabled', () => {
const authGuard = new MustBeAuthenticatedGuard(uiOptionsRedirect, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(null));
let result: boolean;
authGuard.canActivate().subscribe(x => {
result = x;
});
expect(result!).toBeFalsy();
authService.verify(x => x.loginRedirect(), Times.once());
});
}); });

13
src/Squidex/app/shared/guards/must-be-authenticated.guard.ts

@ -10,14 +10,19 @@ import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators'; import { map, take, tap } from 'rxjs/operators';
import { UIOptions } from '@app/framework';
import { AuthService } from './../services/auth.service'; import { AuthService } from './../services/auth.service';
@Injectable() @Injectable()
export class MustBeAuthenticatedGuard implements CanActivate { export class MustBeAuthenticatedGuard implements CanActivate {
constructor( private readonly redirect: boolean;
constructor(uiOptions: UIOptions,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly router: Router private readonly router: Router
) { ) {
this.redirect = uiOptions.get('redirectToLogin');
} }
public canActivate(): Observable<boolean> { public canActivate(): Observable<boolean> {
@ -25,7 +30,11 @@ export class MustBeAuthenticatedGuard implements CanActivate {
take(1), take(1),
tap(user => { tap(user => {
if (!user) { if (!user) {
this.router.navigate(['']); if (this.redirect) {
this.authService.loginRedirect();
} else {
this.router.navigate(['']);
}
} }
}), }),
map(user => !!user)); map(user => !!user));

30
src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts

@ -9,24 +9,25 @@ import { Router } from '@angular/router';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { IMock, It, Mock, Times } from 'typemoq'; import { IMock, It, Mock, Times } from 'typemoq';
import { AuthService } from '@app/shared'; import { AuthService, UIOptions } from '@app/shared';
import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard'; import { MustBeNotAuthenticatedGuard } from './must-be-not-authenticated.guard';
describe('MustNotBeAuthenticatedGuard', () => { describe('MustBeNotAuthenticatedGuard', () => {
let router: IMock<Router>; let router: IMock<Router>;
let authService: IMock<AuthService>; let authService: IMock<AuthService>;
let authGuard: MustBeNotAuthenticatedGuard; let uiOptions = new UIOptions({ map: { type: 'OSM' } });
let uiOptionsRedirect = new UIOptions({ map: { type: 'OSM' }, redirectToLogin: true });
beforeEach(() => { beforeEach(() => {
router = Mock.ofType<Router>(); router = Mock.ofType<Router>();
authService = Mock.ofType<AuthService>(); authService = Mock.ofType<AuthService>();
authGuard = new MustBeNotAuthenticatedGuard(authService.object, router.object);
}); });
it('should navigate to app page if authenticated', () => { it('should navigate to app page if authenticated', () => {
const authGuard = new MustBeNotAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges) authService.setup(x => x.userChanges)
.returns(() => of(<any>{})); .returns(() => of(<any>{}));
@ -42,6 +43,8 @@ describe('MustNotBeAuthenticatedGuard', () => {
}); });
it('should return true if not authenticated', () => { it('should return true if not authenticated', () => {
const authGuard = new MustBeNotAuthenticatedGuard(uiOptions, authService.object, router.object);
authService.setup(x => x.userChanges) authService.setup(x => x.userChanges)
.returns(() => of(null)); .returns(() => of(null));
@ -55,4 +58,21 @@ describe('MustNotBeAuthenticatedGuard', () => {
router.verify(x => x.navigate(It.isAny()), Times.never()); router.verify(x => x.navigate(It.isAny()), Times.never());
}); });
it('should login redirect and return false if redirect enabled', () => {
const authGuard = new MustBeNotAuthenticatedGuard(uiOptionsRedirect, authService.object, router.object);
authService.setup(x => x.userChanges)
.returns(() => of(null));
let result: boolean;
authGuard.canActivate().subscribe(x => {
result = x;
});
expect(result!).toBeFalsy();
authService.verify(x => x.loginRedirect(), Times.once());
});
}); });

13
src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts

@ -10,24 +10,31 @@ import { CanActivate, Router } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, take, tap } from 'rxjs/operators'; import { map, take, tap } from 'rxjs/operators';
import { UIOptions } from '@app/framework';
import { AuthService } from './../services/auth.service'; import { AuthService } from './../services/auth.service';
@Injectable() @Injectable()
export class MustBeNotAuthenticatedGuard implements CanActivate { export class MustBeNotAuthenticatedGuard implements CanActivate {
constructor( private readonly redirect: boolean;
constructor(uiOptions: UIOptions,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly router: Router private readonly router: Router
) { ) {
this.redirect = uiOptions.get('redirectToLogin');
} }
public canActivate(): Observable<boolean> { public canActivate(): Observable<boolean> {
return this.authService.userChanges.pipe( return this.authService.userChanges.pipe(
take(1), take(1),
tap(user => { tap(user => {
if (user) { if (this.redirect) {
this.authService.loginRedirect();
} else if (user) {
this.router.navigate(['app']); this.router.navigate(['app']);
} }
}), }),
map(user => !user)); map(user => !user && !this.redirect));
} }
} }

2
src/Squidex/app/shared/services/ui.service.spec.ts

@ -40,7 +40,7 @@ describe('UIService', () => {
settings = result; settings = result;
}); });
const response: UISettingsDto = { mapType: 'OSM', mapKey: '', canCreateApps: true }; const response: UISettingsDto = { canCreateApps: true };
const req = httpMock.expectOne('http://service/p/api/ui/settings'); const req = httpMock.expectOne('http://service/p/api/ui/settings');

3
src/Squidex/app/shared/services/ui.service.ts

@ -13,9 +13,6 @@ import { catchError } from 'rxjs/operators';
import { ApiUrlConfig } from '@app/framework'; import { ApiUrlConfig } from '@app/framework';
export interface UISettingsDto { export interface UISettingsDto {
readonly mapType: string;
readonly mapKey?: string;
readonly canCreateApps: boolean; readonly canCreateApps: boolean;
} }

2
src/Squidex/app/shared/services/workflows.service.spec.ts

@ -147,7 +147,7 @@ describe('Workflow', () => {
it('should create empty workflow', () => { it('should create empty workflow', () => {
const workflow = new WorkflowDto(); const workflow = new WorkflowDto();
expect(workflow.initial); expect(workflow.initial).not.toBeDefined();
}); });
it('should add step to workflow', () => { it('should add step to workflow', () => {

39
src/Squidex/appsettings.json

@ -65,7 +65,20 @@
*/ */
"key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg" "key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg"
} }
} },
/*
* Redirect to login automatically.
*/
"redirectToLogin": false,
/*
* Hide the news dialog.
*/
"hideNews": false,
/*
* Hide all onboarding tooltips and dialogs.
*/
"hideOnboarding": false
}, },
"email": { "email": {
@ -73,29 +86,29 @@
/* /*
* The host name to your email server. * The host name to your email server.
*/ */
"server": "", "server": "",
/* /*
* The sender email address. * The sender email address.
*/ */
"sender": "hello@squidex.io", "sender": "hello@squidex.io",
/* /*
* The username to authenticate to your email server. * The username to authenticate to your email server.
*/ */
"username": "", "username": "",
/* /*
* The password to authenticate to your email server. * The password to authenticate to your email server.
*/ */
"password": "", "password": "",
/* /*
* Always use SSL if possible. * Always use SSL if possible.
*/ */
"enableSsl": true, "enableSsl": true,
/* /*
* The port to your email server. * The port to your email server.
*/ */
"port": 465 "port": 465
}, },
"notifications": { "notifications": {
/* /*
* The email subject when a new user is added as contributor. * The email subject when a new user is added as contributor.

Loading…
Cancel
Save