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. 53
      src/Squidex/app/shared/state/ui.state.ts
  11. 19
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  12. 6
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  13. 15
      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 AdminOrleans = "squidex.admin.orleans";
public const string AdminAppCreate = "squidex.admin.apps.create";
public const string AdminRestore = "squidex.admin.restore";
public const string AdminRestoreRead = "squidex.admin.restore.read";
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; }
/// <summary>
/// Indicates whether twitter actions are supported.
/// True when the user can create apps.
/// </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 bool OnlyAdminsCanCreateApps { get; set; }
public sealed class MapOptions
{
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.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@ -13,12 +14,16 @@ using Squidex.Areas.Api.Controllers.UI.Models;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
using Squidex.Shared;
using Squidex.Shared.Identity;
using Squidex.Web;
namespace Squidex.Areas.Api.Controllers.UI
{
public sealed class UIController : ApiController
{
private static readonly Permission CreateAppPermission = new Permission(Permissions.AdminAppCreate);
private readonly MyUIOptions uiOptions;
private readonly IGrainFactory grainFactory;
@ -32,6 +37,31 @@ namespace Squidex.Areas.Api.Controllers.UI
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>
/// Get ui settings.
/// </summary>
@ -42,15 +72,12 @@ namespace Squidex.Areas.Api.Controllers.UI
/// </returns>
[HttpGet]
[Route("apps/{app}/ui/settings/")]
[ProducesResponseType(typeof(UISettingsDto), 200)]
[ProducesResponseType(typeof(Dictionary<string, string>), 200)]
[ApiPermission]
public async Task<IActionResult> GetSettings(string app)
{
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);
}

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

@ -26,7 +26,7 @@
</div>
</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-body">
<div class="card-image">

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

@ -15,7 +15,8 @@ import {
FeatureDto,
LocalStoreService,
NewsService,
OnboardingService
OnboardingService,
UIState
} from '@app/shared';
@Component({
@ -35,6 +36,7 @@ export class AppsPageComponent implements OnInit {
constructor(
public readonly appsState: AppsState,
public readonly authState: AuthService,
public readonly uiState: UIState,
private readonly localStore: LocalStoreService,
private readonly newsService: NewsService,
private readonly onboardingService: OnboardingService

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

@ -31,16 +31,56 @@ describe('UIService', () => {
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) => {
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 => {
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');
@ -55,7 +95,7 @@ describe('UIService', () => {
it('should return default settings when error occurs',
inject([UIService, HttpTestingController], (uiService: UIService, httpMock: HttpTestingController) => {
let settings: UISettingsDto;
let settings: object;
uiService.getSettings('my-app').subscribe(result => {
settings = result;

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

@ -15,6 +15,8 @@ import { ApiUrlConfig } from '@app/framework';
export interface UISettingsDto {
mapType: string;
mapKey?: string;
canCreateApps: boolean;
}
@Injectable()
@ -25,12 +27,21 @@ export class UIService {
) {
}
public getSettings(appName: string): Observable<UISettingsDto & object> {
const url = this.apiUrl.buildUrl(`api/apps/${appName}/ui/settings`);
public getCommonSettings(): Observable<UISettingsDto> {
const url = this.apiUrl.buildUrl(`api/ui/settings`);
return this.http.get<UISettingsDto>(url).pipe(
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', () => {
const app = 'my-app';
const oldSettings = {
mapType: 'OSM'
const appSettings = {
mapType: 'GM',
mapSize: 1024,
canCreateApps: true
};
const commonSettings = {
mapType: 'OSM',
mapKey: 'Key',
canCreateApps: true
};
let appsState: IMock<AppsState>;
@ -30,10 +38,16 @@ describe('UIState', () => {
appsState.setup(x => x.appName)
.returns(() => app);
appsState.setup(x => x.selectedApp)
.returns(() => of(<any>{ name: app }));
uiService = Mock.ofType<UIService>();
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()))
.returns(() => of({}));
@ -45,17 +59,25 @@ describe('UIState', () => {
});
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', () => {
uiState.set('root.nested', 123);
expect(uiState.snapshot.settings).toEqual({
mapType: 'OSM',
mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
root: {
nested: 123
}
},
canCreateApps: true
});
uiState.get('root', {}).subscribe(x => {
@ -79,10 +101,13 @@ describe('UIState', () => {
uiState.remove('root.nested1');
expect(uiState.snapshot.settings).toEqual({
mapType: 'OSM',
mapType: 'GM',
mapKey: 'Key',
mapSize: 1024,
root: {
nested2: 123
}
},
canCreateApps: true
});
uiState.get('root', {}).subscribe(x => {

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

@ -15,6 +15,8 @@ import { AppsState } from './apps.state';
import { UIService, UISettingsDto } from './../services/ui.service';
interface Snapshot {
settingsCommon: object & any;
settingsApp: object & any;
settings: object & any;
}
@ -33,27 +35,30 @@ export class UIState extends State<Snapshot> {
private readonly appsState: AppsState,
private readonly uiService: UIService
) {
super({ settings: { mapType: 'OSM' } });
super({ settings: { }, settingsCommon: { }, settingsApp: { } });
if (appsState.selectedApp && Types.isFunction(appsState.selectedApp.subscribe)) {
appsState.selectedApp.subscribe(app => {
if (app) {
this.load(true);
}
});
} else {
this.load(true);
}
this.loadCommon();
appsState.selectedApp.subscribe(app => {
if (app) {
this.load();
}
});
}
public load(reset = false) {
if (!reset) {
this.resetState();
}
private load() {
this.next(s => updateAppSettings(s, {}));
this.uiService.getSettings(this.appName)
.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;
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];
this.next(s => ({ ...s, settings: root }));
this.next(s => updateAppSettings(s, root));
}
}
private getContainer(path: string) {
const segments = path.split('.');
let current = { ...this.snapshot.settings };
let current = { ...this.snapshot.settingsApp };
const root = current;
@ -136,4 +141,16 @@ export class UIState extends State<Snapshot> {
private get 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 };
}

19
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>
</a>
<div class="dropdown-divider"></div>
<ng-container *ngIf="apps.length > 0">
<div class="dropdown-divider"></div>
<div class="apps-list">
<a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a>
</div>
<div class="dropdown-divider"></div>
</ng-container>
<ng-container *ngIf="(uiState.settings | async).canCreateApps">
<div class="dropdown-divider"></div>
<div class="dropdown-button">
<button type="button" class="btn btn-block btn-success" (click)="createApp()">
<i class="icon-plus"></i> Create New App
</button>
</div>
<div class="dropdown-button">
<button type="button" class="btn btn-block btn-success" (click)="createApp()">
<i class="icon-plus"></i> Create New App
</button>
</div>
</ng-container>
</div>
</ng-container>
</li>

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

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

15
src/Squidex/appsettings.json

@ -47,20 +47,25 @@
"Url": "^(?:http(s)?:\\/\\/)?[\\w.-]+(?:\\.[\\w\\.-]+)+[\\w\\-\\._~:\\/?#%[\\]@!\\$&'\\(\\)\\*\\+,;=.]+$"
},
/*
* True if only admins should be able to create apps.
*/
"onlyAdminsCanCreateApps": false,
"map": {
/*
* Define the type of the geolocation service.
*
* Supported: GoogleMaps, OSM
*/
"type": "OSM",
"googleMaps": {
/*
"type": "OSM",
"googleMaps": {
/*
* The optional google maps API key. CREATE YOUR OWN PLEASE.
*/
"key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg"
}
"key": "AIzaSyB_Z8l3nwUxZhMJykiDUJy6bSHXXlwcYMg"
}
}
},
"robots": {

Loading…
Cancel
Save