Browse Source

Setting to restrict app creation in UI.

pull/351/head
Sebastian Stehle 7 years ago
parent
commit
0c1fb192c2
  1. 2
      src/Squidex.Shared/Permissions.cs
  2. 4
      src/Squidex/Areas/Api/Controllers/UI/Models/UISettingsDto.cs
  3. 2
      src/Squidex/Areas/Api/Controllers/UI/MyUIOptions.cs
  4. 35
      src/Squidex/Areas/Api/Controllers/UI/UIController.cs
  5. 2
      src/Squidex/app/features/apps/pages/apps-page.component.html
  6. 4
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  7. 46
      src/Squidex/app/shared/services/ui.service.spec.ts
  8. 17
      src/Squidex/app/shared/services/ui.service.ts
  9. 41
      src/Squidex/app/shared/state/ui.state.spec.ts
  10. 45
      src/Squidex/app/shared/state/ui.state.ts
  11. 7
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  12. 6
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  13. 5
      src/Squidex/appsettings.json

2
src/Squidex.Shared/Permissions.cs

@ -34,6 +34,8 @@ namespace Squidex.Shared
public const string Admin = "squidex.admin.*"; public const string Admin = "squidex.admin.*";
public const string AdminOrleans = "squidex.admin.orleans"; public const string AdminOrleans = "squidex.admin.orleans";
public const string AdminAppCreate = "squidex.admin.apps.create";
public const string AdminRestore = "squidex.admin.restore"; public const string AdminRestore = "squidex.admin.restore";
public const string AdminRestoreRead = "squidex.admin.restore.read"; public const string AdminRestoreRead = "squidex.admin.restore.read";
public const string AdminRestoreCreate = "squidex.admin.restore.create"; public const string AdminRestoreCreate = "squidex.admin.restore.create";

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

@ -24,8 +24,8 @@ namespace Squidex.Areas.Api.Controllers.UI.Models
public string MapKey { get; set; } public string MapKey { get; set; }
/// <summary> /// <summary>
/// Indicates whether twitter actions are supported. /// True when the user can create apps.
/// </summary> /// </summary>
public bool SupportsTwitterActions { get; set; } public bool CanCreateApps { get; set; }
} }
} }

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

@ -15,6 +15,8 @@ namespace Squidex.Areas.Api.Controllers.UI
public MapOptions Map { get; set; } public MapOptions Map { get; set; }
public bool OnlyAdminsCanCreateApps { get; set; }
public sealed class MapOptions public sealed class MapOptions
{ {
public string Type { get; set; } public string Type { get; set; }

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

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -13,12 +14,16 @@ using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Domain.Apps.Entities.Apps; using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Web; using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.UI namespace Squidex.Areas.Api.Controllers.UI
{ {
public sealed class UIController : ApiController public sealed class UIController : ApiController
{ {
private static readonly Permission CreateAppPermission = new Permission(Permissions.AdminAppCreate);
private readonly MyUIOptions uiOptions; private readonly MyUIOptions uiOptions;
private readonly IGrainFactory grainFactory; private readonly IGrainFactory grainFactory;
@ -32,6 +37,31 @@ namespace Squidex.Areas.Api.Controllers.UI
this.grainFactory = grainFactory; this.grainFactory = grainFactory;
} }
/// <summary>
/// Get ui settings.
/// </summary>
/// <returns>
/// 200 => UI settings returned.
/// </returns>
[HttpGet]
[Route("ui/settings/")]
[ProducesResponseType(typeof(UISettingsDto), 200)]
[ApiPermission]
public IActionResult GetSettings()
{
var result = new UISettingsDto
{
MapType = uiOptions.Map?.Type ?? "OSM",
MapKey = uiOptions.Map?.GoogleMaps?.Key
};
var canCreateApps = !uiOptions.OnlyAdminsCanCreateApps || User.Permissions().Includes(CreateAppPermission);
result.CanCreateApps = canCreateApps;
return Ok(result);
}
/// <summary> /// <summary>
/// Get ui settings. /// Get ui settings.
/// </summary> /// </summary>
@ -42,15 +72,12 @@ namespace Squidex.Areas.Api.Controllers.UI
/// </returns> /// </returns>
[HttpGet] [HttpGet]
[Route("apps/{app}/ui/settings/")] [Route("apps/{app}/ui/settings/")]
[ProducesResponseType(typeof(UISettingsDto), 200)] [ProducesResponseType(typeof(Dictionary<string, string>), 200)]
[ApiPermission] [ApiPermission]
public async Task<IActionResult> GetSettings(string app) public async Task<IActionResult> GetSettings(string app)
{ {
var result = await grainFactory.GetGrain<IAppUISettingsGrain>(AppId).GetAsync(); var result = await grainFactory.GetGrain<IAppUISettingsGrain>(AppId).GetAsync();
result.Value.Add("mapType", uiOptions.Map?.Type ?? "OSM");
result.Value.Add("mapKey", uiOptions.Map?.GoogleMaps?.Key);
return Ok(result.Value); return Ok(result.Value);
} }

2
src/Squidex/app/features/apps/pages/apps-page.component.html

@ -26,7 +26,7 @@
</div> </div>
</ng-container> </ng-container>
<div class="apps-section"> <div class="apps-section" *ngIf="(uiState.settings | async).canCreateApps">
<div class="card card-template card-href" (click)="createNewApp('')"> <div class="card card-template card-href" (click)="createNewApp('')">
<div class="card-body"> <div class="card-body">
<div class="card-image"> <div class="card-image">

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

@ -15,7 +15,8 @@ import {
FeatureDto, FeatureDto,
LocalStoreService, LocalStoreService,
NewsService, NewsService,
OnboardingService OnboardingService,
UIState
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -35,6 +36,7 @@ export class AppsPageComponent implements OnInit {
constructor( constructor(
public readonly appsState: AppsState, public readonly appsState: AppsState,
public readonly authState: AuthService, public readonly authState: AuthService,
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

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

@ -31,16 +31,56 @@ describe('UIService', () => {
httpMock.verify(); httpMock.verify();
})); }));
it('should make get request to get settings', it('should make get request to get common settings',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => { inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: UISettingsDto; let settings: UISettingsDto;
uiService.getCommonSettings().subscribe(result => {
settings = result;
});
const response: UISettingsDto = { mapType: 'OSM', mapKey: '', canCreateApps: true };
const req = httpMock.expectOne('http://service/p/api/ui/settings');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush(response);
expect(settings!).toEqual(response);
}));
it('should return default common settings when error occurs',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: object;
uiService.getCommonSettings().subscribe(result => {
settings = result;
});
const req = httpMock.expectOne('http://service/p/api/ui/settings');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.error(new ErrorEvent('500'));
expect(settings!).toEqual({ mapType: 'OSM', mapKey: '', canCreateApps: true });
}));
it('should make get request to get settings',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: object;
uiService.getSettings('my-app').subscribe(result => { uiService.getSettings('my-app').subscribe(result => {
settings = result; settings = result;
}); });
const response: UISettingsDto = { mapType: 'OSM', mapKey: '' }; const response = { mapType: 'OSM', mapKey: '' };
const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings'); const req = httpMock.expectOne('http://service/p/api/apps/my-app/ui/settings');
@ -55,7 +95,7 @@ describe('UIService', () => {
it('should return default settings when error occurs', it('should return default settings when error occurs',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => { inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: UISettingsDto; let settings: object;
uiService.getSettings('my-app').subscribe(result => { uiService.getSettings('my-app').subscribe(result => {
settings = result; settings = result;

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

@ -15,6 +15,8 @@ import { ApiUrlConfig } from '@app/framework';
export interface UISettingsDto { export interface UISettingsDto {
mapType: string; mapType: string;
mapKey?: string; mapKey?: string;
canCreateApps: boolean;
} }
@Injectable() @Injectable()
@ -25,12 +27,21 @@ export class UIService {
) { ) {
} }
public getSettings(appName: string): Observable<UISettingsDto & object> { public getCommonSettings(): Observable<UISettingsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings`); const url = this.apiUrl.buildUrl(`api/ui/settings`);
return this.http.get<UISettingsDto>(url).pipe( return this.http.get<UISettingsDto>(url).pipe(
catchError(_ => { catchError(_ => {
return of({ regexSuggestions: [], mapType: 'OSM', mapKey: '' }); return of({ mapType: 'OSM', mapKey: '', canCreateApps: true });
}));
}
public getSettings(appName: string): Observable<object> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings`);
return this.http.get<object>(url).pipe(
catchError(_ => {
return of({ });
})); }));
} }

41
src/Squidex/app/shared/state/ui.state.spec.ts

@ -16,8 +16,16 @@ import { UIState } from './ui.state';
describe('UIState', () => { describe('UIState', () => {
const app = 'my-app'; const app = 'my-app';
const oldSettings = { const appSettings = {
mapType: 'OSM' mapType: 'GM',
mapSize: 1024,
canCreateApps: true
};
const commonSettings = {
mapType: 'OSM',
mapKey: 'Key',
canCreateApps: true
}; };
let appsState: IMock<AppsState>; let appsState: IMock<AppsState>;
@ -30,10 +38,16 @@ describe('UIState', () => {
appsState.setup(x => x.appName) appsState.setup(x => x.appName)
.returns(() => app); .returns(() => app);
appsState.setup(x => x.selectedApp)
.returns(() => of(<any>{ name: app }));
uiService = Mock.ofType<UIService>(); uiService = Mock.ofType<UIService>();
uiService.setup(x => x.getSettings(app)) uiService.setup(x => x.getSettings(app))
.returns(() => of(oldSettings)); .returns(() => of(appSettings));
uiService.setup(x => x.getCommonSettings())
.returns(() => of(commonSettings));
uiService.setup(x => x.putSetting(app, It.isAnyString(), It.isAny())) uiService.setup(x => x.putSetting(app, It.isAnyString(), It.isAny()))
.returns(() => of({})); .returns(() => of({}));
@ -45,17 +59,25 @@ describe('UIState', () => {
}); });
it('should load settings', () => { it('should load settings', () => {
expect(uiState.snapshot.settings).toEqual(oldSettings); expect(uiState.snapshot.settings).toEqual({
mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
canCreateApps: true
});
}); });
it('should add value to snapshot when set', () => { it('should add value to snapshot when set', () => {
uiState.set('root.nested', 123); uiState.set('root.nested', 123);
expect(uiState.snapshot.settings).toEqual({ expect(uiState.snapshot.settings).toEqual({
mapType: 'OSM', mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
root: { root: {
nested: 123 nested: 123
} },
canCreateApps: true
}); });
uiState.get('root', {}).subscribe(x => { uiState.get('root', {}).subscribe(x => {
@ -79,10 +101,13 @@ describe('UIState', () => {
uiState.remove('root.nested1'); uiState.remove('root.nested1');
expect(uiState.snapshot.settings).toEqual({ expect(uiState.snapshot.settings).toEqual({
mapType: 'OSM', mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
root: { root: {
nested2: 123 nested2: 123
} },
canCreateApps: true
}); });
uiState.get('root', {}).subscribe(x => { uiState.get('root', {}).subscribe(x => {

45
src/Squidex/app/shared/state/ui.state.ts

@ -15,6 +15,8 @@ import { AppsState } from './apps.state';
import { UIService, UISettingsDto } from './../services/ui.service'; import { UIService, UISettingsDto } from './../services/ui.service';
interface Snapshot { interface Snapshot {
settingsCommon: object & any;
settingsApp: object & any;
settings: object & any; settings: object & any;
} }
@ -33,27 +35,30 @@ export class UIState extends State<Snapshot> {
private readonly appsState: AppsState, private readonly appsState: AppsState,
private readonly uiService: UIService private readonly uiService: UIService
) { ) {
super({ settings: { mapType: 'OSM' } }); super({ settings: { }, settingsCommon: { }, settingsApp: { } });
this.loadCommon();
if (appsState.selectedApp && Types.isFunction(appsState.selectedApp.subscribe)) {
appsState.selectedApp.subscribe(app => { appsState.selectedApp.subscribe(app => {
if (app) { if (app) {
this.load(true); this.load();
} }
}); });
} else {
this.load(true);
}
} }
public load(reset = false) { private load() {
if (!reset) { this.next(s => updateAppSettings(s, {}));
this.resetState();
}
this.uiService.getSettings(this.appName) this.uiService.getSettings(this.appName)
.subscribe(dtos => { .subscribe(dtos => {
return this.next(s => ({ ...s, settings: dtos })); this.next(s => updateAppSettings(s, dtos));
});
}
private loadCommon() {
this.uiService.getCommonSettings()
.subscribe(dtos => {
this.next(s => updateCommonSettings(s, dtos));
}); });
} }
@ -65,7 +70,7 @@ export class UIState extends State<Snapshot> {
current[key] = value; current[key] = value;
this.next(s => ({ ...s, settings: root })); this.next(s => updateAppSettings(s, root));
} }
} }
@ -77,14 +82,14 @@ export class UIState extends State<Snapshot> {
delete current[key]; delete current[key];
this.next(s => ({ ...s, settings: root })); this.next(s => updateAppSettings(s, root));
} }
} }
private getContainer(path: string) { private getContainer(path: string) {
const segments = path.split('.'); const segments = path.split('.');
let current = { ...this.snapshot.settings }; let current = { ...this.snapshot.settingsApp };
const root = current; const root = current;
@ -137,3 +142,15 @@ export class UIState extends State<Snapshot> {
return this.appsState.appName; return this.appsState.appName;
} }
} }
function updateAppSettings(state: Snapshot, settingsApp: object & any) {
const { settingsCommon } = state;
return { settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
}
function updateCommonSettings(state: Snapshot, settingsCommon: object & any) {
const { settingsApp } = state;
return { settings: { ...settingsCommon, ...settingsApp }, settingsApp, settingsCommon };
}

7
src/Squidex/app/shell/pages/internal/apps-menu.component.html

@ -17,21 +17,24 @@
<span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span> <span class="all-apps-pill badge badge-pill badge-primary">{{apps.length}}</span>
</a> </a>
<div class="dropdown-divider"></div>
<ng-container *ngIf="apps.length > 0"> <ng-container *ngIf="apps.length > 0">
<div class="dropdown-divider"></div>
<div class="apps-list"> <div class="apps-list">
<a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a> <a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a>
</div> </div>
</ng-container>
<ng-container *ngIf="(uiState.settings | async).canCreateApps">
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</ng-container>
<div class="dropdown-button"> <div class="dropdown-button">
<button type="button" class="btn btn-block btn-success" (click)="createApp()"> <button type="button" class="btn btn-block btn-success" (click)="createApp()">
<i class="icon-plus"></i> Create New App <i class="icon-plus"></i> Create New App
</button> </button>
</div> </div>
</ng-container>
</div> </div>
</ng-container> </ng-container>
</li> </li>

6
src/Squidex/app/shell/pages/internal/apps-menu.component.ts

@ -11,7 +11,8 @@ import {
AppsState, AppsState,
DialogModel, DialogModel,
fadeAnimation, fadeAnimation,
ModalModel ModalModel,
UIState
} from '@app/shared'; } from '@app/shared';
@Component({ @Component({
@ -29,7 +30,8 @@ export class AppsMenuComponent {
public appsMenu = new ModalModel(); public appsMenu = new ModalModel();
constructor( constructor(
public readonly appsState: AppsState public readonly appsState: AppsState,
public readonly uiState: UIState
) { ) {
} }

5
src/Squidex/appsettings.json

@ -47,6 +47,11 @@
"Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:\\/?#%[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$" "Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:\\/?#%[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$"
}, },
/*
* True if only admins should be able to create apps.
*/
"onlyAdminsCanCreateApps": false,
"map": { "map": {
/* /*
* Define the type of the geolocation service. * Define the type of the geolocation service.

Loading…
Cancel
Save