diff --git a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs index 96c282131..396c673ea 100644 --- a/src/Squidex.Write/Schemas/SchemaCommandHandler.cs +++ b/src/Squidex.Write/Schemas/SchemaCommandHandler.cs @@ -52,32 +52,32 @@ namespace Squidex.Write.Schemas public Task On(DeleteSchema command, CommandContext context) { - return UpdateAsync(command, s => s.Delete()); + return UpdateAsync(command, s => s.Delete(command)); } public Task On(DeleteField command, CommandContext context) { - return UpdateAsync(command, s => s.DeleteField(command.FieldId)); + return UpdateAsync(command, s => s.DeleteField(command)); } public Task On(DisableField command, CommandContext context) { - return UpdateAsync(command, s => s.DisableField(command.FieldId)); + return UpdateAsync(command, s => s.DisableField(command)); } public Task On(EnableField command, CommandContext context) { - return UpdateAsync(command, s => s.EnableField(command.FieldId)); + return UpdateAsync(command, s => s.EnableField(command)); } public Task On(HideField command, CommandContext context) { - return UpdateAsync(command, s => s.HideField(command.FieldId)); + return UpdateAsync(command, s => s.HideField(command)); } public Task On(ShowField command, CommandContext context) { - return UpdateAsync(command, s => s.ShowField(command.FieldId)); + return UpdateAsync(command, s => s.ShowField(command)); } public Task On(UpdateSchema command, CommandContext context) diff --git a/src/Squidex.Write/Schemas/SchemaDomainObject.cs b/src/Squidex.Write/Schemas/SchemaDomainObject.cs index e8f27651d..1ee9bea03 100644 --- a/src/Squidex.Write/Schemas/SchemaDomainObject.cs +++ b/src/Squidex.Write/Schemas/SchemaDomainObject.cs @@ -116,7 +116,7 @@ namespace Squidex.Write.Schemas public SchemaDomainObject UpdateField(UpdateField command) { - Guard.Valid(command, nameof(command), () => $"Cannot update schema '{schema.Name} ({Id})'"); + Guard.Valid(command, nameof(command), () => $"Cannot update schema '{Id}'"); VerifyCreatedAndNotDeleted(); @@ -138,7 +138,7 @@ namespace Squidex.Write.Schemas public SchemaDomainObject Update(UpdateSchema command) { - Guard.Valid(command, nameof(command), () => $"Cannot update schema '{schema.Name} ({Id})'"); + Guard.Valid(command, nameof(command), () => $"Cannot update schema '{Id}'"); VerifyCreatedAndNotDeleted(); @@ -147,52 +147,62 @@ namespace Squidex.Write.Schemas return this; } - public SchemaDomainObject HideField(long fieldId) + public SchemaDomainObject HideField(HideField command) { + Guard.NotNull(command, nameof(command)); + VerifyCreatedAndNotDeleted(); - RaiseEvent(new FieldHidden { FieldId = fieldId }); + RaiseEvent(new FieldHidden { FieldId = command.FieldId }); return this; } - public SchemaDomainObject ShowField(long fieldId) + public SchemaDomainObject ShowField(ShowField command) { + Guard.NotNull(command, nameof(command)); + VerifyCreatedAndNotDeleted(); - RaiseEvent(new FieldShown { FieldId = fieldId }); + RaiseEvent(new FieldShown { FieldId = command.FieldId }); return this; } - public SchemaDomainObject DisableField(long fieldId) + public SchemaDomainObject DisableField(DisableField command) { + Guard.NotNull(command, nameof(command)); + VerifyCreatedAndNotDeleted(); - RaiseEvent(new FieldDisabled { FieldId = fieldId }); + RaiseEvent(new FieldDisabled { FieldId = command.FieldId }); return this; } - public SchemaDomainObject EnableField(long fieldId) + public SchemaDomainObject EnableField(EnableField command) { + Guard.NotNull(command, nameof(command)); + VerifyCreatedAndNotDeleted(); - RaiseEvent(new FieldEnabled { FieldId = fieldId }); + RaiseEvent(new FieldEnabled { FieldId = command.FieldId }); return this; } - public SchemaDomainObject DeleteField(long fieldId) + public SchemaDomainObject DeleteField(DeleteField command) { + Guard.NotNull(command, nameof(command)); + VerifyCreatedAndNotDeleted(); - RaiseEvent(new FieldDeleted { FieldId = fieldId }); + RaiseEvent(new FieldDeleted { FieldId = command.FieldId }); return this; } - public SchemaDomainObject Delete() + public SchemaDomainObject Delete(DeleteSchema command) { VerifyCreatedAndNotDeleted(); diff --git a/src/Squidex/Config/Identity/LazyClientStore.cs b/src/Squidex/Config/Identity/LazyClientStore.cs index 757395bde..fd827a852 100644 --- a/src/Squidex/Config/Identity/LazyClientStore.cs +++ b/src/Squidex/Config/Identity/LazyClientStore.cs @@ -38,38 +38,40 @@ namespace Squidex.Config.Identity { var client = staticClients.GetOrDefault(clientId); - if (client == null) + if (client != null) { - return null; + return client; } - var app = await appProvider.FindAppByNameAsync(clientId); + var token = clientId.Split(':'); - if (app != null) + if (token.Length != 2) { - client = CreateClientFromApp(app); + return null; } - return client; - } + var app = await appProvider.FindAppByNameAsync(token[0]); - private void CreateStaticClients(IOptions urlsOptions) - { - foreach (var client in CreateStaticClients(urlsOptions.Value)) + var appClient = app?.Clients.FirstOrDefault(x => x.ClientName == token[1]); + + if (appClient == null) { - staticClients[client.ClientId] = client; + return null; + } + + client = CreateClientFromApp(clientId, appClient); + + return client; } - private static Client CreateClientFromApp(IAppEntity app) + private static Client CreateClientFromApp(string id, IAppClientEntity appClient) { - var id = app.Name; - return new Client { ClientId = id, ClientName = id, - ClientSecrets = app.Clients.Select(x => new Secret(x.ClientName, x.ExpiresUtc)).ToList(), + ClientSecrets = new List { new Secret(appClient.ClientSecret.Sha512(), appClient.ExpiresUtc) }, AccessTokenLifetime = (int)TimeSpan.FromDays(30).TotalSeconds, AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = new List @@ -79,6 +81,14 @@ namespace Squidex.Config.Identity }; } + private void CreateStaticClients(IOptions urlsOptions) + { + foreach (var client in CreateStaticClients(urlsOptions.Value)) + { + staticClients[client.ClientId] = client; + } + } + private static IEnumerable CreateStaticClients(MyUrlsOptions urlsOptions) { const string id = Constants.FrontendClient; diff --git a/src/Squidex/Pipeline/RandomErrorAttribute.cs b/src/Squidex/Pipeline/RandomErrorAttribute.cs new file mode 100644 index 000000000..444461413 --- /dev/null +++ b/src/Squidex/Pipeline/RandomErrorAttribute.cs @@ -0,0 +1,27 @@ +// ========================================================================== +// RandomErrorFilter.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Squidex.Pipeline +{ + public class RandomErrorAttribute : ActionFilterAttribute + { + private static readonly Random random = new Random(); + + public override void OnActionExecuted(ActionExecutedContext context) + { + if (random.Next(10) < 5) + { + context.Result = new StatusCodeResult(500); + } + } + } +} diff --git a/src/Squidex/Startup.cs b/src/Squidex/Startup.cs index 9889a2cea..fed70d24d 100644 --- a/src/Squidex/Startup.cs +++ b/src/Squidex/Startup.cs @@ -94,7 +94,7 @@ namespace Squidex public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { - loggerFactory.AddConsole(); + loggerFactory.AddConsole(LogLevel.Debug); loggerFactory.AddDebug(); if (!Environment.IsDevelopment()) diff --git a/src/Squidex/app/app.module.ts b/src/Squidex/app/app.module.ts index 258eee0dc..f24e73983 100644 --- a/src/Squidex/app/app.module.ts +++ b/src/Squidex/app/app.module.ts @@ -27,6 +27,7 @@ import { DecimalSeparatorConfig, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, + NotificationService, LanguageService, LocalStoreService, SqxFrameworkModule, @@ -88,6 +89,7 @@ export function configCurrency() { LocalStoreService, MustBeAuthenticatedGuard, MustBeNotAuthenticatedGuard, + NotificationService, TitleService, UsersProviderService, UsersService, diff --git a/src/Squidex/app/components/internal/app/settings/clients-page.component.html b/src/Squidex/app/components/internal/app/settings/clients-page.component.html index e91a93cef..41fcb7aea 100644 --- a/src/Squidex/app/components/internal/app/settings/clients-page.component.html +++ b/src/Squidex/app/components/internal/app/settings/clients-page.component.html @@ -51,7 +51,7 @@ - + diff --git a/src/Squidex/app/components/internal/app/settings/clients-page.component.ts b/src/Squidex/app/components/internal/app/settings/clients-page.component.ts index eace94e51..74a24fb32 100644 --- a/src/Squidex/app/components/internal/app/settings/clients-page.component.ts +++ b/src/Squidex/app/components/internal/app/settings/clients-page.component.ts @@ -14,6 +14,8 @@ import { AppClientCreateDto, AppClientsService, fadeAnimation, + Notification, + NotificationService, TitleService } from 'shared'; @@ -46,7 +48,8 @@ export class ClientsPageComponent implements Ng2.OnInit { private readonly titles: TitleService, private readonly appsStore: AppsStoreService, private readonly appClientsService: AppClientsService, - private readonly formBuilder: Ng2Forms.FormBuilder + private readonly formBuilder: Ng2Forms.FormBuilder, + private readonly notifications: NotificationService ) { } @@ -58,9 +61,13 @@ export class ClientsPageComponent implements Ng2.OnInit { this.titles.setTitle('{appName} | Settings | Clients', { appName: app.name }); - this.appClientsService.getClients(app.name).subscribe(clients => { - this.appClients = clients; - }); + this.appClientsService.getClients(app.name) + .subscribe(clients => { + this.appClients = clients; + }, () => { + this.notifications.notify(Notification.error('Failed to load clients. Please reload squidex portal.')); + this.appClients = []; + }); } }); } @@ -74,9 +81,16 @@ export class ClientsPageComponent implements Ng2.OnInit { } public revokeClient(client: AppClientDto) { - this.appClientsService.deleteClient(this.appName, client.clientName).subscribe(); + this.appClientsService.deleteClient(this.appName, client.clientName) + .subscribe(() => { + this.appClients.splice(this.appClients.indexOf(client), 1); + }, error => { + this.notifications.notify(Notification.error('Failed to revoke client. Please retry.')); + }); + } - this.appClients.splice(this.appClients.indexOf(client), 1); + public createToken(client: AppClientDto) { + this.appClientsService.createToken(this.appName, client).subscribe(); } public attachClient() { diff --git a/src/Squidex/app/components/internal/app/settings/contributors-page.component.ts b/src/Squidex/app/components/internal/app/settings/contributors-page.component.ts index f6901eb3e..9c1c540c2 100644 --- a/src/Squidex/app/components/internal/app/settings/contributors-page.component.ts +++ b/src/Squidex/app/components/internal/app/settings/contributors-page.component.ts @@ -16,6 +16,8 @@ import { AppContributorsService, AppsStoreService, AuthService, + Notification, + NotificationService, TitleService, UserDto, UsersService, @@ -27,7 +29,7 @@ class UsersDataSource extends CompleterBaseData { constructor( private readonly usersService: UsersService, - private readonly component: ContributorsPageComponent, + private readonly component: ContributorsPageComponent ) { super(); } @@ -93,7 +95,8 @@ export class ContributorsPageComponent implements Ng2.OnInit { private readonly appsStore: AppsStoreService, private readonly appContributorsService: AppContributorsService, private readonly usersProvider: UsersProviderService, - private readonly usersService: UsersService + private readonly usersService: UsersService, + private readonly notifications: NotificationService ) { this.usersDataSource = new UsersDataSource(usersService, this); } @@ -108,9 +111,12 @@ export class ContributorsPageComponent implements Ng2.OnInit { this.titles.setTitle('{appName} | Settings | Contributors', { appName: app.name }); - this.appContributorsService.getContributors(app.name).subscribe(contributors => { - this.appContributors = contributors; - }); + this.appContributorsService.getContributors(app.name).retry(2) + .subscribe(contributors => { + this.appContributors = contributors; + }, error => { + this.notifications.notify(Notification.error('Failed to load app contributors. Please reload squidex portal.')); + }); } }); } @@ -126,7 +132,13 @@ export class ContributorsPageComponent implements Ng2.OnInit { const contributor = new AppContributorDto(this.selectedUser.id, 'Editor'); - this.appContributorsService.postContributor(this.appName, contributor).subscribe(); + this.appContributorsService.postContributor(this.appName, contributor) + .catch(error => { + this.notifications.notify(Notification.error('Failed to assign contributors. Please retry.')); + + return Observable.of(true); + }).subscribe(); + this.appContributors.push(contributor); this.selectedUser = null; @@ -134,13 +146,23 @@ export class ContributorsPageComponent implements Ng2.OnInit { } public removeContributor(contributor: AppContributorDto) { - this.appContributorsService.deleteContributor(this.appName, contributor.contributorId).subscribe(); + this.appContributorsService.deleteContributor(this.appName, contributor.contributorId) + .catch(error => { + this.notifications.notify(Notification.error('Failed to remove contributors. Please retry.')); + + return Observable.of(true); + }).subscribe(); this.appContributors.splice(this.appContributors.indexOf(contributor), 1); } public saveContributor(contributor: AppContributorDto) { - this.appContributorsService.postContributor(this.appName, contributor).subscribe(); + this.appContributorsService.postContributor(this.appName, contributor) + .catch(error => { + this.notifications.notify(Notification.error('Failed to update contributors. Please retry.')); + + return Observable.of(true); + }).subscribe(); } public selectUser(selection: CompleterItem | null) { diff --git a/src/Squidex/app/components/internal/app/settings/languages-page.component.html b/src/Squidex/app/components/internal/app/settings/languages-page.component.html index 02fd9b9e8..b348ba656 100644 --- a/src/Squidex/app/components/internal/app/settings/languages-page.component.html +++ b/src/Squidex/app/components/internal/app/settings/languages-page.component.html @@ -4,7 +4,7 @@
-
+
diff --git a/src/Squidex/app/components/internal/app/settings/languages-page.component.ts b/src/Squidex/app/components/internal/app/settings/languages-page.component.ts index 400560bb0..23c92c0b8 100644 --- a/src/Squidex/app/components/internal/app/settings/languages-page.component.ts +++ b/src/Squidex/app/components/internal/app/settings/languages-page.component.ts @@ -12,6 +12,8 @@ import { AppsStoreService, LanguageDto, LanguageService, + Notification, + NotificationService, TitleService } from 'shared'; @@ -38,14 +40,18 @@ export class LanguagesPageComponent implements Ng2.OnInit { private readonly titles: TitleService, private readonly appsStore: AppsStoreService, private readonly appLanguagesService: AppLanguagesService, - private readonly languagesService: LanguageService + private readonly languagesService: LanguageService, + private readonly notifications: NotificationService ) { } public ngOnInit() { - this.languagesService.getLanguages().subscribe(languages => { - this.allLanguages = languages; - }); + this.languagesService.getLanguages().retry(2) + .subscribe(languages => { + this.allLanguages = languages; + }, error => { + this.notifications.notify(Notification.error('Failed to load languages. Please reload squidex portal.')); + }); this.appSubscription = this.appsStore.selectedApp.subscribe(app => { @@ -54,9 +60,12 @@ export class LanguagesPageComponent implements Ng2.OnInit { this.titles.setTitle('{appName} | Settings | Languages', { appName: app.name }); - this.appLanguagesService.getLanguages(app.name).subscribe(appLanguages => { - this.appLanguages = appLanguages; - }); + this.appLanguagesService.getLanguages(app.name).retry(2) + .subscribe(appLanguages => { + this.appLanguages = appLanguages; + }, error => { + this.notifications.notify(Notification.error('Failed to load app languages. Please reload squidex portal.')); + }); } }); } @@ -80,10 +89,13 @@ export class LanguagesPageComponent implements Ng2.OnInit { this.appLanguagesService.postLanguages(this.appName, this.appLanguages.map(l => l.iso2Code)) .delay(500) - .finally(() => { - this.isSaving = false; - }) - .subscribe(); + .subscribe(() => { + this.isSaving = false; + }, error => { + this.isSaving = false; + + this.notifications.notify(Notification.error('Failed to save app languages. Please retry.')); + }); } } diff --git a/src/Squidex/app/components/internal/internal-area.component.html b/src/Squidex/app/components/internal/internal-area.component.html index 3972fbddf..cfdecad51 100644 --- a/src/Squidex/app/components/internal/internal-area.component.html +++ b/src/Squidex/app/components/internal/internal-area.component.html @@ -17,3 +17,9 @@ + +
+
+ {{notification.message}} +
+
diff --git a/src/Squidex/app/components/internal/internal-area.component.scss b/src/Squidex/app/components/internal/internal-area.component.scss index e545b17df..b1c02a400 100644 --- a/src/Squidex/app/components/internal/internal-area.component.scss +++ b/src/Squidex/app/components/internal/internal-area.component.scss @@ -15,4 +15,26 @@ .search-form { margin-left: 15px; +} + +.notification { + &-container { + @include fixed(auto, 10px, 10px, auto); + width: 260px; + } + + &-item { + @include border-radius; + @include box-shadow; + padding: 10px; + margin-top: 10px; + font-size: .8rem; + font-weight: normal; + color: $color-accent-dark; + cursor: pointer; + } + + &-error { + background: $color-theme-error; + } } \ No newline at end of file diff --git a/src/Squidex/app/components/internal/internal-area.component.ts b/src/Squidex/app/components/internal/internal-area.component.ts index a16d8886e..446a39705 100644 --- a/src/Squidex/app/components/internal/internal-area.component.ts +++ b/src/Squidex/app/components/internal/internal-area.component.ts @@ -7,9 +7,48 @@ import * as Ng2 from '@angular/core'; +import { + fadeAnimation, + Notification, + NotificationService +} from 'shared'; + @Ng2.Component({ selector: 'sqx-internal-area', styles, - template + template, + animations: [ + fadeAnimation + ] }) -export class InternalAreaComponent { } \ No newline at end of file +export class InternalAreaComponent implements Ng2.OnInit, Ng2.OnDestroy { + private notificationsSubscription: any; + + public notifications: Notification[] = []; + + constructor( + private readonly notificationService: NotificationService + ) { + } + + public ngOnInit() { + this.notificationsSubscription = + this.notificationService.notifications.subscribe(notification => { + this.notifications.push(notification); + + if (notification.displayTime > 0) { + setTimeout(() => { + this.close(notification); + }, notification.displayTime); + } + }); + } + + public ngOnDestroy() { + this.notificationsSubscription.unsubscribe(); + } + + public close(notification: Notification) { + this.notifications.splice(this.notifications.indexOf(notification), 1); + } + } \ No newline at end of file diff --git a/src/Squidex/app/components/layout/app-form.component.html b/src/Squidex/app/components/layout/app-form.component.html index 630678701..c0c259158 100644 --- a/src/Squidex/app/components/layout/app-form.component.html +++ b/src/Squidex/app/components/layout/app-form.component.html @@ -32,7 +32,7 @@
- - + +
\ No newline at end of file diff --git a/src/Squidex/app/framework/declarations.ts b/src/Squidex/app/framework/declarations.ts index 207d46e55..60f874435 100644 --- a/src/Squidex/app/framework/declarations.ts +++ b/src/Squidex/app/framework/declarations.ts @@ -25,6 +25,7 @@ export * from './configurations'; export * from './services/clipboard.service'; export * from './services/drag.service'; export * from './services/local-store.service'; +export * from './services/notification.service'; export * from './services/shortcut.service'; export * from './services/title.service'; diff --git a/src/Squidex/app/framework/services/clipboard.service.spec.ts b/src/Squidex/app/framework/services/clipboard.service.spec.ts index 3c80cfaf5..1c4356751 100644 --- a/src/Squidex/app/framework/services/clipboard.service.spec.ts +++ b/src/Squidex/app/framework/services/clipboard.service.spec.ts @@ -8,7 +8,6 @@ import { ClipboardService, ClipboardServiceFactory } from './../'; describe('ShortcutService', () => { - it('should instantiate from factory', () => { const clipboardService = ClipboardServiceFactory(); diff --git a/src/Squidex/app/framework/services/notification.service.spec.ts b/src/Squidex/app/framework/services/notification.service.spec.ts new file mode 100644 index 000000000..c768d81d7 --- /dev/null +++ b/src/Squidex/app/framework/services/notification.service.spec.ts @@ -0,0 +1,41 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import { + Notification, + NotificationService, + NotificationServiceFactory +} from './../'; + +describe('NotificationService', () => { + it('should instantiate from factory', () => { + const notificationService = NotificationServiceFactory(); + + expect(notificationService).toBeDefined(); + }); + + it('should instantiate', () => { + const notificationService = new NotificationService(); + + expect(notificationService).toBeDefined(); + }); + + it('should publish notification', () => { + const notificationService = new NotificationService(); + const notification = Notification.error('Message'); + + let publishedNotification: Notification; + + notificationService.notifications.subscribe(result => { + publishedNotification = result; + }); + + notificationService.notify(notification); + + expect(publishedNotification).toBe(notification); + }); +}); diff --git a/src/Squidex/app/framework/services/notification.service.ts b/src/Squidex/app/framework/services/notification.service.ts new file mode 100644 index 000000000..10b6f8523 --- /dev/null +++ b/src/Squidex/app/framework/services/notification.service.ts @@ -0,0 +1,44 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Sebastian Stehle. All rights reserved + */ + +import * as Ng2 from '@angular/core'; + +import { Observable, Subject } from 'rxjs'; + +export const NotificationServiceFactory = () => { + return new NotificationService(); +}; + +export class Notification { + constructor( + public readonly message: string, + public readonly messageType: string, + public readonly displayTime: number = 10000 + ) { + } + + public static error(message: string) { + return new Notification(message, 'error'); + } + + public static info(message: string) { + return new Notification(message, 'info'); + } +} + +@Ng2.Injectable() +export class NotificationService { + private readonly notificationsStream$ = new Subject(); + + public get notifications(): Observable { + return this.notificationsStream$; + } + + public notify(notification: Notification) { + this.notificationsStream$.next(notification); + } +} \ No newline at end of file diff --git a/src/Squidex/app/shared/services/app-clients.service.spec.ts b/src/Squidex/app/shared/services/app-clients.service.spec.ts index 52fbdc8b7..9128d6c06 100644 --- a/src/Squidex/app/shared/services/app-clients.service.spec.ts +++ b/src/Squidex/app/shared/services/app-clients.service.spec.ts @@ -25,7 +25,7 @@ describe('AppClientsService', () => { beforeEach(() => { authService = TypeMoq.Mock.ofType(AuthService); - appClientsService = new AppClientsService(authService.object, new ApiUrlConfig('http://service/p/')); + appClientsService = new AppClientsService(authService.object, new ApiUrlConfig('http://service/p/'), null); }); it('should make get request with auth service to get app clients', () => { diff --git a/src/Squidex/app/shared/services/app-clients.service.ts b/src/Squidex/app/shared/services/app-clients.service.ts index 631c0db38..53a2ce913 100644 --- a/src/Squidex/app/shared/services/app-clients.service.ts +++ b/src/Squidex/app/shared/services/app-clients.service.ts @@ -6,6 +6,7 @@ */ import * as Ng2 from '@angular/core'; +import * as Ng2Http from '@angular/http'; import { Observable } from 'rxjs'; @@ -32,7 +33,8 @@ export class AppClientCreateDto { export class AppClientsService { constructor( private readonly authService: AuthService, - private readonly apiUrl: ApiUrlConfig + private readonly apiUrl: ApiUrlConfig, + private readonly http: Ng2Http.Http ) { } @@ -64,4 +66,16 @@ export class AppClientsService { public deleteClient(appName: string, name: string): Observable { return this.authService.authDelete(this.apiUrl.buildUrl(`api/apps/${appName}/clients/${name}`)); } + + public createToken(appName: string, client: AppClientDto): Observable { + const options = new Ng2Http.RequestOptions({ + headers: new Ng2Http.Headers({ + 'Content-Type': 'application/x-www-form-urlencoded' + }) + }); + + const body = `grant_type=client_credentials&scope=squidex-api&client_id=${appName}:${client.clientName}&client_secret=${client.clientSecret}`; + + return this.http.post(this.apiUrl.buildUrl('identity-server/connect/token'), body, options); + } } \ No newline at end of file diff --git a/src/Squidex/app/shared/services/users-provider.service.ts b/src/Squidex/app/shared/services/users-provider.service.ts index 5580b823d..ca6e64e67 100644 --- a/src/Squidex/app/shared/services/users-provider.service.ts +++ b/src/Squidex/app/shared/services/users-provider.service.ts @@ -28,7 +28,7 @@ export class UsersProviderService { if (!result) { const request = - this.usersService.getUser(id) + this.usersService.getUser(id).retry(2) .map(u => { if (this.authService.user && u.id === this.authService.user.id) { return new UserDto(u.id, u.email, 'Me', u.pictureUrl); diff --git a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs index a49ecba30..a1973aa2d 100644 --- a/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Apps/AppDomainObjectTests.cs @@ -46,7 +46,7 @@ namespace Squidex.Write.Tests.Apps } [Fact] - public void Create_should_throw_if_command_is_invalid() + public void Create_should_throw_if_command_is_not_valid() { Assert.Throws(() => sut.Create(new CreateApp())); } @@ -76,9 +76,9 @@ namespace Squidex.Write.Tests.Apps } [Fact] - public void AssignContributor_should_throw_if_contributor_id_not_valid() + public void AssignContributor_should_throw_if_command_is_not_valid() { - Assert.Throws(() => sut.AssignContributor(new AssignContributor())); + Assert.Throws(() => sut.AssignContributor(new AssignContributor())); } [Fact] @@ -113,9 +113,9 @@ namespace Squidex.Write.Tests.Apps } [Fact] - public void RemoveContributor_should_throw_if_contributor_id_not_valid() + public void RemoveContributor_should_throw_if_command_is_not_valid() { - Assert.Throws(() => sut.RemoveContributor(new RemoveContributor())); + Assert.Throws(() => sut.RemoveContributor(new RemoveContributor())); } [Fact] @@ -161,7 +161,7 @@ namespace Squidex.Write.Tests.Apps } [Fact] - public void ConfigureLanguages_should_throw_if_languages_are_null_or_empty() + public void ConfigureLanguages_should_throw_if_command_is_not_valid() { CreateApp(); @@ -193,7 +193,7 @@ namespace Squidex.Write.Tests.Apps } [Fact] - public void AttachClient_should_throw_if_name_not_valid() + public void AttachClient_should_throw_if_command_is_not_valid() { CreateApp(); @@ -237,7 +237,7 @@ namespace Squidex.Write.Tests.Apps } [Fact] - public void RevokeClient_should_throw_if_client_key_is_null_or_empty() + public void RevokeClient_should_throw_if_command_is_not_valid() { CreateApp(); diff --git a/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs b/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs index 58d1bc448..94bf85a27 100644 --- a/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Schemas/SchemaDomainObjectTests.cs @@ -12,18 +12,21 @@ using FluentAssertions; using Squidex.Core.Schemas; using Squidex.Events.Schemas; using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS; using Squidex.Infrastructure.CQRS.Events; using Squidex.Write.Schemas; using Squidex.Write.Schemas.Commands; using Xunit; +// ReSharper disable ConvertToConstant.Local namespace Squidex.Write.Tests.Schemas { [Collection("Schema")] public class SchemaDomainObjectTests { - private const string TestName = "schema"; private readonly Guid appId = Guid.NewGuid(); + private readonly string fieldName = "age"; + private readonly string appName = "schema"; private readonly FieldRegistry registry = new FieldRegistry(); private readonly SchemaDomainObject sut; @@ -35,23 +38,23 @@ namespace Squidex.Write.Tests.Schemas [Fact] public void Create_should_throw_if_created() { - sut.Create(new CreateSchema { Name = TestName }); + sut.Create(new CreateSchema { Name = appName }); - Assert.Throws(() => sut.Create(new CreateSchema { Name = TestName })); + Assert.Throws(() => sut.Create(new CreateSchema { Name = appName })); } [Fact] - public void Create_should_throw_if_command_is_invalid() + public void Create_should_throw_if_command_is_not_valid() { Assert.Throws(() => sut.Create(new CreateSchema())); } [Fact] - public void Create_should_create_schema() + public void Create_should_create_schema_and_create_events() { var properties = new SchemaProperties(); - sut.Create(new CreateSchema { Name = TestName, AppId = appId, Properties = properties }); + sut.Create(new CreateSchema { Name = appName, AppId = appId, Properties = properties }); Assert.Equal("schema", sut.Schema.Name); @@ -59,7 +62,7 @@ namespace Squidex.Write.Tests.Schemas .ShouldBeEquivalentTo( new IEvent[] { - new SchemaCreated { Name = TestName, AppId = appId, Properties = properties } + new SchemaCreated { Name = appName, AppId = appId, Properties = properties } }); } @@ -72,31 +75,32 @@ namespace Squidex.Write.Tests.Schemas [Fact] public void Update_should_throw_if_schema_is_deleted() { - sut.Create(new CreateSchema { Name = TestName }); - sut.Delete(); + CreateSchema(); + DeleteSchema(); Assert.Throws(() => sut.Update(new UpdateSchema())); } [Fact] - public void Update_should_throw_if_command_is_invalid() + public void Update_should_throw_if_command_is_not_valid() { - sut.Create(new CreateSchema { Name = TestName }); + CreateSchema(); Assert.Throws(() => sut.Update(new UpdateSchema())); } [Fact] - public void Update_should_refresh_properties() + public void Update_should_refresh_properties_and_create_events() { var properties = new SchemaProperties(); - sut.Create(new CreateSchema { Name = TestName, AppId = appId }); + CreateSchema(); + sut.Update(new UpdateSchema { Properties = properties }); Assert.Equal(properties, sut.Schema.Properties); - sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray() + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() .ShouldBeEquivalentTo( new IEvent[] { @@ -107,32 +111,342 @@ namespace Squidex.Write.Tests.Schemas [Fact] public void Delete_should_throw_if_not_created() { - Assert.Throws(() => sut.Delete()); + Assert.Throws(() => sut.Delete(new DeleteSchema())); } [Fact] public void Delete_should_throw_if_already_deleted() { - sut.Create(new CreateSchema { Name = TestName }); - sut.Delete(); + CreateSchema(); + DeleteSchema(); - Assert.Throws(() => sut.Delete()); + Assert.Throws(() => sut.Delete(new DeleteSchema())); } [Fact] - public void Delete_should_refresh_properties() + public void Delete_should_refresh_properties_and_create_events() { - sut.Create(new CreateSchema { Name = TestName, AppId = appId }); - sut.Delete(); + CreateSchema(); + + sut.Delete(new DeleteSchema()); Assert.True(sut.IsDeleted); - sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray() + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() .ShouldBeEquivalentTo( new IEvent[] { new SchemaDeleted() }); } + + [Fact] + public void AddField_should_throw_if_not_created() + { + Assert.Throws(() => sut.AddField(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); + } + + [Fact] + public void AddField_should_throw_if_command_is_not_valid() + { + Assert.Throws(() => sut.AddField(new AddField())); + } + + [Fact] + public void AddField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.AddField(new AddField { Name = fieldName, Properties = new NumberFieldProperties() })); + } + + [Fact] + public void AddField_should_update_schema_and_create_events() + { + var properties = new NumberFieldProperties(); + + CreateSchema(); + + sut.AddField(new AddField { Name = fieldName, Properties = properties }); + + Assert.Equal(properties, sut.Schema.Fields[1].RawProperties); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldAdded { Name = fieldName, FieldId = 1, Properties = properties } + }); + } + + [Fact] + public void UpdateField_should_throw_if_not_created() + { + Assert.Throws(() => sut.UpdateField(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); + } + + [Fact] + public void UpdateField_should_throw_if_command_is_not_valid() + { + Assert.Throws(() => sut.UpdateField(new UpdateField())); + } + + [Fact] + public void UpdateField_should_throw_if_field_is_not_found() + { + CreateSchema(); + + Assert.Throws(() => sut.UpdateField(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); + } + + [Fact] + public void UpdateField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.UpdateField(new UpdateField { FieldId = 1, Properties = new NumberFieldProperties() })); + } + + [Fact] + public void UpdateField_should_update_schema_and_create_events() + { + var properties = new NumberFieldProperties(); + + CreateSchema(); + CreateField(); + + sut.UpdateField(new UpdateField { FieldId = 1, Properties = properties }); + + Assert.Equal(properties, sut.Schema.Fields[1].RawProperties); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldUpdated { FieldId = 1, Properties = properties } + }); + } + + [Fact] + public void HideField_should_throw_if_not_created() + { + Assert.Throws(() => sut.HideField(new HideField { FieldId = 1 })); + } + + [Fact] + public void HideField_should_throw_if_field_is_not_found() + { + CreateSchema(); + + Assert.Throws(() => sut.HideField(new HideField { FieldId = 2 })); + } + + [Fact] + public void HideField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.HideField(new HideField { FieldId = 1 })); + } + + [Fact] + public void HideField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.HideField(new HideField { FieldId = 1 }); + + Assert.True(sut.Schema.Fields[1].IsHidden); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldHidden { FieldId = 1 } + }); + } + + [Fact] + public void ShowField_should_throw_if_not_created() + { + Assert.Throws(() => sut.ShowField(new ShowField { FieldId = 1 })); + } + + [Fact] + public void ShowField_should_throw_if_field_is_not_found() + { + CreateSchema(); + + Assert.Throws(() => sut.ShowField(new ShowField { FieldId = 2 })); + } + + [Fact] + public void ShowField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.ShowField(new ShowField { FieldId = 1 })); + } + + [Fact] + public void ShowField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.HideField(new HideField { FieldId = 1 }); + sut.ShowField(new ShowField { FieldId = 1 }); + + Assert.False(sut.Schema.Fields[1].IsHidden); + + sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldShown { FieldId = 1 } + }); + } + + [Fact] + public void DisableField_should_throw_if_not_created() + { + Assert.Throws(() => sut.DisableField(new DisableField { FieldId = 1 })); + } + + [Fact] + public void DisableField_should_throw_if_field_is_not_found() + { + CreateSchema(); + + Assert.Throws(() => sut.DisableField(new DisableField { FieldId = 2 })); + } + + [Fact] + public void DisableField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.DisableField(new DisableField { FieldId = 1 })); + } + + [Fact] + public void DisableField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.DisableField(new DisableField { FieldId = 1 }); + + Assert.True(sut.Schema.Fields[1].IsDisabled); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldDisabled { FieldId = 1 } + }); + } + + [Fact] + public void EnableField_should_throw_if_not_created() + { + Assert.Throws(() => sut.EnableField(new EnableField { FieldId = 1 })); + } + + [Fact] + public void EnableField_should_throw_if_field_is_not_found() + { + CreateSchema(); + + Assert.Throws(() => sut.EnableField(new EnableField { FieldId = 2 })); + } + + [Fact] + public void EnableField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.EnableField(new EnableField { FieldId = 1 })); + } + + [Fact] + public void EnableField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.DisableField(new DisableField { FieldId = 1 }); + sut.EnableField(new EnableField { FieldId = 1 }); + + Assert.False(sut.Schema.Fields[1].IsDisabled); + + sut.GetUncomittedEvents().Select(x => x.Payload).Skip(1).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldEnabled { FieldId = 1 } + }); + } + + [Fact] + public void DeleteField_should_throw_if_not_created() + { + Assert.Throws(() => sut.DeleteField(new DeleteField { FieldId = 1 })); + } + + [Fact] + public void DeleteField_should_throw_if_schema_is_deleted() + { + CreateSchema(); + DeleteSchema(); + + Assert.Throws(() => sut.DeleteField(new DeleteField { FieldId = 1 })); + } + + [Fact] + public void DeleteField_should_update_schema_and_create_events() + { + CreateSchema(); + CreateField(); + + sut.DeleteField(new DeleteField { FieldId = 1 }); + + Assert.False(sut.Schema.Fields.ContainsKey(1)); + + sut.GetUncomittedEvents().Select(x => x.Payload).ToArray() + .ShouldBeEquivalentTo( + new IEvent[] + { + new FieldDeleted { FieldId = 1 } + }); + } + + private void CreateField() + { + sut.AddField(new AddField { Name = fieldName, Properties = new NumberFieldProperties() }); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void CreateSchema() + { + sut.Create(new CreateSchema { Name = appName, AppId = appId }); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + + private void DeleteSchema() + { + sut.Delete(new DeleteSchema()); + + ((IAggregate)sut).ClearUncommittedEvents(); + } } }