Browse Source

Merge remote-tracking branch 'Squidex/master'

pull/130/head
pushrbx 9 years ago
parent
commit
8af8a647e5
  1. 4
      README.md
  2. 33
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  3. 2
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs
  4. 72
      src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs
  5. 2
      src/Squidex.Domain.Users/UserExtensions.cs
  6. 21
      src/Squidex.Domain.Users/UserManagerExtensions.cs
  7. 119
      src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs
  8. 3
      src/Squidex.Infrastructure/Dispatching/Helper.cs
  9. 3
      src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs
  10. 2
      src/Squidex/Controllers/UI/Extensions.cs
  11. 8
      src/Squidex/Pipeline/Swagger/SwaggerHelper.cs
  12. 18
      src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts
  13. 2
      src/Squidex/app/features/apps/pages/apps-page.component.ts
  14. 2
      src/Squidex/app/features/content/pages/content/content-field.component.html
  15. 8
      src/Squidex/app/features/content/pages/content/content-field.component.ts
  16. 4
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  17. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  18. 28
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  19. 5
      src/Squidex/app/features/content/shared/content-item.component.ts
  20. 2
      src/Squidex/app/features/content/shared/references-editor.component.html
  21. 8
      src/Squidex/app/features/content/shared/references-editor.component.ts
  22. 22
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  23. 14
      src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html
  24. 6
      src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts
  25. 2
      src/Squidex/app/framework/angular/can-deactivate.guard.ts
  26. 32
      src/Squidex/app/framework/angular/cloak.directive.spec.ts
  27. 23
      src/Squidex/app/framework/angular/cloak.directive.ts
  28. 48
      src/Squidex/app/framework/angular/date-time-editor.component.ts
  29. 53
      src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts
  30. 35
      src/Squidex/app/framework/angular/focus-on-change.directive.ts
  31. 4
      src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts
  32. 2
      src/Squidex/app/framework/angular/root-view.directive.spec.ts
  33. 2
      src/Squidex/app/framework/declarations.ts
  34. 6
      src/Squidex/app/framework/module.ts
  35. 4
      src/Squidex/app/shared/components/app-form.component.html
  36. 2
      src/Squidex/app/shared/components/app.component-base.ts
  37. 1
      src/Squidex/app/shared/declarations-base.ts
  38. 13
      src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts
  39. 14
      src/Squidex/app/shared/guards/app-must-exist.guard.ts
  40. 13
      src/Squidex/app/shared/guards/must-be-authenticated.guard.spec.ts
  41. 18
      src/Squidex/app/shared/guards/must-be-authenticated.guard.ts
  42. 13
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.spec.ts
  43. 18
      src/Squidex/app/shared/guards/must-be-not-authenticated.guard.ts
  44. 6
      src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts
  45. 16
      src/Squidex/app/shared/guards/resolve-app-languages.guard.ts
  46. 6
      src/Squidex/app/shared/guards/resolve-content.guard.spec.ts
  47. 16
      src/Squidex/app/shared/guards/resolve-content.guard.ts
  48. 8
      src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts
  49. 18
      src/Squidex/app/shared/guards/resolve-published-schema.guard.ts
  50. 6
      src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts
  51. 16
      src/Squidex/app/shared/guards/resolve-schema.guard.ts
  52. 6
      src/Squidex/app/shared/guards/resolve-user.guard.spec.ts
  53. 16
      src/Squidex/app/shared/guards/resolve-user.guard.ts
  54. 36
      src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts
  55. 50
      src/Squidex/app/shared/interceptors/auth.interceptor.ts
  56. 2
      src/Squidex/app/shared/module.ts
  57. 79
      src/Squidex/app/shared/services/apps-store.service.spec.ts
  58. 79
      src/Squidex/app/shared/services/apps-store.service.ts
  59. 95
      src/Squidex/app/shared/services/auth.service.ts
  60. 48
      src/Squidex/app/shared/services/graphql.service.spec.ts
  61. 27
      src/Squidex/app/shared/services/graphql.service.ts
  62. 6
      src/Squidex/app/shell/pages/home/home-page.component.ts
  63. 4
      src/Squidex/app/shell/pages/internal/apps-menu.component.html
  64. 2
      src/Squidex/app/shell/pages/internal/apps-menu.component.ts
  65. 18
      src/Squidex/app/shell/pages/internal/profile-menu.component.ts
  66. 4
      src/Squidex/app/shell/pages/login/login-page.component.ts
  67. 4
      src/Squidex/app/shell/pages/logout/logout-page.component.ts
  68. 27
      tests/Squidex.Domain.Apps.Read.Tests/Schemas/ODataQueryTests.cs
  69. 3
      tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj

4
README.md

@ -8,6 +8,10 @@ Squidex is an open source headless CMS and content management hub. In contrast t
Read the docs at [https://docs.squidex.io/](https://docs.squidex.io/) (work in progress) or just check out the code and play around.
## Status
Current Version 1.0-beta1
## Prerequisites
* [Visual Studio Code](https://code.visualstudio.com/) or [Visual Studio 2017](https://www.visualstudio.com/vs/visual-studio-2017-rc/)

33
src/Squidex.Domain.Apps.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -9,9 +9,9 @@
using System;
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Domain.Apps.Read.MongoDb.Utils;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching;
@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
public string EventsFilter
{
get { return "^(content-)|(schema-)|(asset-)"; }
get { return "^(content-)|(app-)|(asset-)"; }
}
public async Task ClearAsync()
@ -58,10 +58,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
return this.DispatchActionAsync(@event.Payload, @event.Headers);
}
protected Task On(SchemaCreated @event, EnvelopeHeaders headers)
protected Task On(AppCreated @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, async collection =>
{
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.SchemaId).Descending(x => x.LastModified));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.ReferencedIds));
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsPublished));
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
@ -116,18 +117,6 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
});
}
protected Task On(AssetDeleted @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, collection =>
{
return collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, @event.AssetId),
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId));
});
}
protected Task On(ContentDeleted @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, async collection =>
@ -141,7 +130,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
await collection.DeleteOneAsync(x => x.Id == headers.AggregateId());
});
}
protected Task On(AssetDeleted @event, EnvelopeHeaders headers)
{
return ForAppIdAsync(@event.AppId.Id, collection =>
{
return collection.UpdateManyAsync(
Filter.And(
Filter.AnyEq(x => x.ReferencedIds, @event.AssetId),
Filter.AnyNe(x => x.ReferencedIdsDeleted, @event.AssetId)),
Update.AddToSet(x => x.ReferencedIdsDeleted, @event.AssetId));
});
}
private Task ForAppIdAsync(Guid appId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{
var collection = GetCollection(appId);

2
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/EdmModelExtensions.cs

@ -17,6 +17,8 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
{
public static ODataUriParser ParseQuery(this IEdmModel model, string query)
{
query = query ?? string.Empty;
var path = model.EntityContainer.EntitySets().First().Path.Path.Split('.').Last();
if (query.StartsWith("?"))

72
src/Squidex.Domain.Apps.Read.MongoDb/Contents/Visitors/FilterVisitor.cs

@ -36,6 +36,11 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
return node.Accept(visitor);
}
public override FilterDefinition<MongoContentEntity> Visit(ConvertNode nodeIn)
{
return nodeIn.Source.Accept(this);
}
public override FilterDefinition<MongoContentEntity> Visit(UnaryOperatorNode nodeIn)
{
if (nodeIn.OperatorKind == UnaryOperatorKind.Not)
@ -51,19 +56,19 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
var fieldNode = nodeIn.Parameters.ElementAt(0);
var valueNode = nodeIn.Parameters.ElementAt(1);
if (nodeIn.Name == "endswith")
if (string.Equals(nodeIn.Name, "endswith", StringComparison.OrdinalIgnoreCase))
{
var value = BuildRegex(valueNode, v => v + "$");
return Filter.Regex(BuildFieldDefinition(fieldNode), value);
}
if (nodeIn.Name == "startswith")
if (string.Equals(nodeIn.Name, "startswith", StringComparison.OrdinalIgnoreCase))
{
var value = BuildRegex(valueNode, v => "^" + v);
return Filter.Regex(BuildFieldDefinition(fieldNode), value);
}
if (nodeIn.Name == "contains")
if (string.Equals(nodeIn.Name, "contains", StringComparison.OrdinalIgnoreCase))
{
var value = BuildRegex(valueNode, v => v);
@ -83,29 +88,50 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents.Visitors
{
return Filter.Or(nodeIn.Left.Accept(this), nodeIn.Right.Accept(this));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
return Filter.Ne(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
return Filter.Eq(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
return Filter.Lt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
return Filter.Lte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
if (nodeIn.Left is SingleValueFunctionCallNode functionNode)
{
return Filter.Gt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
var regexFilter = Visit(functionNode);
var value = BuildValue(nodeIn.Right);
if (value is bool booleanRight)
{
if ((nodeIn.OperatorKind == BinaryOperatorKind.Equal && !booleanRight) ||
(nodeIn.OperatorKind == BinaryOperatorKind.NotEqual && booleanRight))
{
regexFilter = Filter.Not(regexFilter);
}
return regexFilter;
}
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
else
{
return Filter.Gte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
if (nodeIn.OperatorKind == BinaryOperatorKind.NotEqual)
{
return Filter.Ne(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.Equal)
{
return Filter.Eq(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThan)
{
return Filter.Lt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.LessThanOrEqual)
{
return Filter.Lte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThan)
{
return Filter.Gt(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
if (nodeIn.OperatorKind == BinaryOperatorKind.GreaterThanOrEqual)
{
return Filter.Gte(BuildFieldDefinition(nodeIn.Left), BuildValue(nodeIn.Right));
}
}
throw new NotSupportedException();

2
src/Squidex.Domain.Users/UserExtensions.cs

@ -18,7 +18,7 @@ namespace Squidex.Domain.Users
{
public static class UserExtensions
{
public static void UpdateDisplayName(this IUser user, string displayName)
public static void SetDisplayName(this IUser user, string displayName)
{
user.SetClaim(SquidexClaimTypes.SquidexDisplayName, displayName);
}

21
src/Squidex.Domain.Users/UserManagerExtensions.cs

@ -54,14 +54,23 @@ namespace Squidex.Domain.Users
{
var user = factory.Create(email);
user.UpdateDisplayName(displayName);
user.SetPictureUrlFromGravatar(email);
try
{
user.SetDisplayName(displayName);
user.SetPictureUrlFromGravatar(email);
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
await DoChecked(() => userManager.CreateAsync(user), "Cannot create user.");
if (!string.IsNullOrWhiteSpace(password))
if (!string.IsNullOrWhiteSpace(password))
{
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user.");
}
}
catch
{
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user.");
await userManager.DeleteAsync(user);
throw;
}
return user;
@ -83,7 +92,7 @@ namespace Squidex.Domain.Users
if (!string.IsNullOrWhiteSpace(displayName))
{
user.UpdateDisplayName(displayName);
user.SetDisplayName(displayName);
}
await DoChecked(() => userManager.UpdateAsync(user), "Cannot update user.");

119
src/Squidex.Infrastructure.MongoDb/EventStore/MongoEventStore.cs

@ -9,9 +9,7 @@
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Timers;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
@ -19,12 +17,13 @@ using System.Threading;
using System.Threading.Tasks;
using Squidex.Infrastructure.Tasks;
// ReSharper disable RedundantIfElseBlock
// ReSharper disable InvertIf
// ReSharper disable ConvertIfStatementToConditionalTernaryExpression
namespace Squidex.Infrastructure.MongoDb.EventStore
{
public class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore, IDisposable
public class MongoEventStore : MongoRepositoryBase<MongoEventCommit>, IEventStore
{
private static readonly BsonTimestamp EmptyTimestamp = new BsonTimestamp(0);
private static readonly FieldDefinition<MongoEventCommit, BsonTimestamp> TimestampField = Fields.Build(x => x.Timestamp);
@ -32,9 +31,6 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
private static readonly FieldDefinition<MongoEventCommit, long> EventStreamOffsetField = Fields.Build(x => x.EventStreamOffset);
private static readonly FieldDefinition<MongoEventCommit, string> EventStreamField = Fields.Build(x => x.EventStream);
private readonly IEventNotifier notifier;
private readonly CompletionTimer timer;
private readonly ConcurrentQueue<(BsonDocument Document, TaskCompletionSource<bool> Completion)> pendingCommits = new ConcurrentQueue<(BsonDocument Document, TaskCompletionSource<bool> Completion)>();
private readonly Lazy<IMongoCollection<BsonDocument>> plainCollection;
public MongoEventStore(IMongoDatabase database, IEventNotifier notifier)
: base(database)
@ -42,15 +38,6 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
Guard.NotNull(notifier, nameof(notifier));
this.notifier = notifier;
timer = new CompletionTimer(50, ct => WriteAsync());
plainCollection = new Lazy<IMongoCollection<BsonDocument>>(() => Database.GetCollection<BsonDocument>(CollectionName()));
}
public void Dispose()
{
timer.Dispose();
}
protected override string CollectionName()
@ -120,68 +107,6 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
}, cancellationToken);
}
private async Task WriteAsync()
{
while (true)
{
var commitsToInsert = new List<(BsonDocument Document, TaskCompletionSource<bool> Completion)>();
while (pendingCommits.TryDequeue(out var commit))
{
commitsToInsert.Add(commit);
}
var numCommits = commitsToInsert.Count;
if (numCommits == 0)
{
return;
}
try
{
await plainCollection.Value.InsertManyAsync(commitsToInsert.Select(x => x.Document), new InsertManyOptions { IsOrdered = false });
notifier.NotifyEventsStored();
foreach (var commit in commitsToInsert)
{
commit.Completion.SetResult(true);
}
}
catch (MongoBulkWriteException ex)
{
foreach (var error in ex.WriteErrors)
{
var commit = commitsToInsert[error.Index];
if (error.Category == ServerErrorCategory.DuplicateKey)
{
var streamName = commit.Document[nameof(MongoEventCommit.EventStream)].AsString;
var streamOffset = commit.Document[nameof(MongoEventCommit.EventStreamOffset)].AsInt64;
var currentVersion = await GetEventStreamOffset(streamName);
var exception = new WrongEventVersionException(currentVersion, streamOffset);
commit.Completion.SetException(exception);
}
else
{
commit.Completion.SetException(new MongoWriteException(ex.ConnectionId, error, ex.WriteConcernError, ex));
}
}
}
catch (Exception ex)
{
foreach (var commit in commitsToInsert)
{
commit.Completion.SetException(ex);
}
}
}
}
public async Task AppendEventsAsync(Guid commitId, string streamName, int expectedVersion, ICollection<EventData> events)
{
Guard.NotNullOrEmpty(streamName, nameof(streamName));
@ -202,23 +127,33 @@ namespace Squidex.Infrastructure.MongoDb.EventStore
commitEvents[i++] = mongoEvent;
}
var cts = new TaskCompletionSource<bool>();
var document = new MongoEventCommit
try
{
Id = commitId,
Events = commitEvents,
EventsCount = eventsCount,
EventStream = streamName,
EventStreamOffset = expectedVersion,
Timestamp = EmptyTimestamp
}.ToBsonDocument();
pendingCommits.Enqueue((document, cts));
timer.Wakeup();
var document = new MongoEventCommit
{
Id = commitId,
Events = commitEvents,
EventsCount = eventsCount,
EventStream = streamName,
EventStreamOffset = expectedVersion,
Timestamp = EmptyTimestamp
};
await Collection.InsertOneAsync(document);
}
catch (MongoWriteException ex)
{
if (ex.WriteError?.Category == ServerErrorCategory.DuplicateKey)
{
var currentVersion = await GetEventStreamOffset(streamName);
await cts.Task;
throw new WrongEventVersionException(currentVersion, expectedVersion);
}
else
{
throw;
}
}
}
}

3
src/Squidex.Infrastructure/Dispatching/Helper.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Reflection;
namespace Squidex.Infrastructure.Dispatching
@ -14,7 +15,7 @@ namespace Squidex.Infrastructure.Dispatching
{
public static bool HasRightName(MethodInfo method)
{
return method.Name == "On";
return string.Equals(method.Name, "On", StringComparison.OrdinalIgnoreCase);
}
public static bool HasRightReturnType<TOut>(MethodInfo method)

3
src/Squidex/Config/Swagger/XmlResponseTypesProcessor.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using NJsonSchema.Infrastructure;
@ -43,7 +44,7 @@ namespace Squidex.Config.Swagger
response.Description = match.Groups["Description"].Value;
if (statusCode == "200")
if (string.Equals(statusCode, "200", StringComparison.OrdinalIgnoreCase))
{
hasOkResponse = true;
}

2
src/Squidex/Controllers/UI/Extensions.cs

@ -19,7 +19,7 @@ namespace Squidex.Controllers.UI
public static Task<IdentityResult> UpdateAsync(this UserManager<IUser> userManager, IUser user, string email, string displayName)
{
user.UpdateEmail(email);
user.UpdateDisplayName(displayName);
user.SetDisplayName(displayName);
return userManager.UpdateAsync(user);
}

8
src/Squidex/Pipeline/Swagger/SwaggerHelper.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
@ -36,12 +37,17 @@ namespace Squidex.Pipeline.Swagger
public static SwaggerDocument CreateApiDocument(HttpContext context, MyUrlsOptions urlOptions, string appName)
{
var scheme =
string.Equals(context.Request.Scheme, "http", StringComparison.OrdinalIgnoreCase) ?
SwaggerSchema.Http :
SwaggerSchema.Https;
var document = new SwaggerDocument
{
Tags = new List<SwaggerTag>(),
Schemes = new List<SwaggerSchema>
{
context.Request.Scheme == "http" ? SwaggerSchema.Http : SwaggerSchema.Https
scheme
},
Consumes = new List<string>
{

18
src/Squidex/app/features/api/pages/graphql/graphql-page.component.ts

@ -6,7 +6,6 @@
*/
import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
@ -16,9 +15,10 @@ const GraphiQL = require('graphiql');
/* tslint:disable:use-view-encapsulation */
import {
ApiUrlConfig,
AppComponentBase,
AppsStoreService,
GraphQlService,
LocalStoreService,
NotificationService
} from 'shared';
@ -33,8 +33,8 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit {
public graphiQLContainer: ElementRef;
constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly apiUrl: ApiUrlConfig,
private readonly http: HttpClient
private readonly graphQlService: GraphQlService,
private readonly localStoreService: LocalStoreService
) {
super(notifications, apps);
}
@ -42,7 +42,13 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit {
public ngOnInit() {
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: (params: any) => this.request(params)
fetcher: (params: any) => {
return this.request(params);
},
onEditQuery: (query: string) => {
this.localStoreService.set('graphiQlQuery', query);
},
query: this.localStoreService.get('graphiQlQuery')
}),
this.graphiQLContainer.nativeElement
);
@ -50,7 +56,7 @@ export class GraphQLPageComponent extends AppComponentBase implements OnInit {
private request(params: any) {
return this.appNameOnce()
.switchMap(app => this.http.post(this.apiUrl.buildUrl(`api/content/${app}/graphql`), params))
.switchMap(app => this.graphQlService.query(app, params))
.toPromise();
}
}

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

@ -24,7 +24,7 @@ import {
export class AppsPageComponent implements OnInit {
public addAppDialog = new ModalView();
public apps = this.appsStore.apps.map(a => a || []);
public apps = this.appsStore.apps;
constructor(
private readonly appsStore: AppsStoreService

2
src/Squidex/app/features/content/pages/content/content-field.component.html

@ -93,7 +93,7 @@
<sqx-assets-editor [formControlName]="partition"></sqx-assets-editor>
</div>
<div *ngSwitchCase="'References'">
<sqx-references-editor [formControlName]="partition" [languageCode]="partition" [schemaId]="field.properties.schemaId"></sqx-references-editor>
<sqx-references-editor [formControlName]="partition" [languageCode]="selectFieldLanguage(partition)" [schemaId]="field.properties.schemaId"></sqx-references-editor>
</div>
</div>
</div>

8
src/Squidex/app/features/content/pages/content/content-field.component.ts

@ -16,6 +16,8 @@ import { AppLanguageDto, FieldDto } from 'shared';
templateUrl: './content-field.component.html'
})
export class ContentFieldComponent implements OnInit {
private masterLanguageCode: string;
@Input()
public field: FieldDto;
@ -36,6 +38,8 @@ export class ContentFieldComponent implements OnInit {
}
public ngOnInit() {
this.masterLanguageCode = this.languages.find(l => l.isMaster).iso2Code;
if (this.field.isDisabled) {
this.fieldForm.disable();
}
@ -48,5 +52,9 @@ export class ContentFieldComponent implements OnInit {
this.fieldPartition = 'iv';
}
}
public selectFieldLanguage(partition: string) {
return partition === 'iv' ? this.masterLanguageCode : partition;
}
}

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

@ -91,9 +91,9 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
});
}
public canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
public canDeactivate(): Observable<boolean> {
if (!this.contentForm.dirty) {
return true;
return Observable.of(true);
} else {
this.cancelDialog.show();

4
src/Squidex/app/features/content/pages/contents/contents-page.component.html

@ -69,7 +69,7 @@
<tbody *ngIf="!isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" [routerLink]="[content.id]" routerLinkActive="active"
[language]="languageSelected"
[languageCode]="languageSelected.iso2Code"
[schemaFields]="contentFields"
[schema]="schema"
(unpublishing)="unpublishContent(content)"
@ -82,7 +82,7 @@
<tbody *ngIf="isReadOnly">
<ng-template ngFor let-content [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-draggable [dragData]="dropData(content)"
[language]="languageSelected"
[languageCode]="languageSelected.iso2Code"
[schemaFields]="contentFields"
[schema]="schema"
isReadOnly="true"></tr>

28
src/Squidex/app/features/content/pages/contents/contents-page.component.ts

@ -163,26 +163,30 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
private loadFields() {
this.contentFields = this.schema.fields.filter(x => x.properties.isListField);
this.columnWidth = 100 / this.contentFields.length;
if (this.contentFields.length === 0 && this.schema.fields.length > 0) {
this.contentFields = [this.schema.fields[0]];
}
if (this.contentFields.length > 0) {
this.columnWidth = 100 / this.contentFields.length;
} else {
this.columnWidth = 100;
}
}
public load(showInfo = false) {
this.appNameOnce()
.switchMap(app => this.contentsService.getContents(app, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery))
.subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total);
if (showInfo) {
this.notifyInfo('Contents reloaded.');
}
}, error => {
this.notifyError(error);
});
.subscribe(dtos => {
this.contentItems = ImmutableArray.of(dtos.items);
this.contentsPager = this.contentsPager.setCount(dtos.total);
if (showInfo) {
this.notifyInfo('Contents reloaded.');
}
}, error => {
this.notifyError(error);
});
}
public dropData(content: ContentDto) {

5
src/Squidex/app/features/content/shared/content-item.component.ts

@ -9,7 +9,6 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angu
import {
AppComponentBase,
AppLanguageDto,
AppsStoreService,
ContentDto,
fadeAnimation,
@ -42,7 +41,7 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
public deleting = new EventEmitter<ContentDto>();
@Input()
public language: AppLanguageDto;
public languageCode: string;
@Input()
public schemaFields: FieldDto[];
@ -88,7 +87,7 @@ export class ContentItemComponent extends AppComponentBase implements OnInit, On
if (contentField) {
if (field.partitioning === 'language') {
return field.formatValue(contentField[this.language.iso2Code]);
return field.formatValue(contentField[this.languageCode]);
} else {
return field.formatValue(contentField['iv']);
}

2
src/Squidex/app/features/content/shared/references-editor.component.html

@ -20,7 +20,7 @@
<tbody dnd-sortable-container [sortableData]="contentItems.mutableValues">
<ng-template ngFor let-content let-i="index" [ngForOf]="contentItems">
<tr [sqxContent]="content" dnd-sortable [sortableIndex]="i" (sqxSorted)="onContentsSorted($event)"
[language]="languageSelected"
[languageCode]="languageCode"
[schemaFields]="contentFields"
[schema]="schema"
(deleting)="onContentRemoving(content)"

8
src/Squidex/app/features/content/shared/references-editor.component.ts

@ -145,10 +145,14 @@ export class ReferencesEditorComponent extends AppComponentBase implements Contr
private loadFields() {
this.contentFields = this.schema.fields.filter(x => x.properties.isListField);
this.columnWidth = 100 / this.contentFields.length;
if (this.contentFields.length === 0 && this.schema.fields.length > 0) {
this.contentFields = [this.schema.fields[0]];
}
if (this.contentFields.length > 0) {
this.columnWidth = 100 / this.contentFields.length;
} else {
this.columnWidth = 100;
}
}
}

22
src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts

@ -5,8 +5,7 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription} from 'rxjs';
import { Component, OnInit } from '@angular/core';
import {
AppComponentBase,
@ -29,9 +28,7 @@ declare var _urq: any;
fadeAnimation
]
})
export class DashboardPageComponent extends AppComponentBase implements OnInit, OnDestroy {
private authenticationSubscription: Subscription;
export class DashboardPageComponent extends AppComponentBase implements OnInit {
public profileDisplayName = '';
public chartStorageCount: any;
@ -65,16 +62,12 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit,
public callsMax: string | null = null;
constructor(apps: AppsStoreService, notifications: NotificationService,
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly usagesService: UsagesService
) {
super(notifications, apps);
}
public ngOnDestroy() {
this.authenticationSubscription.unsubscribe();
}
public ngOnInit() {
this.appName()
.switchMap(app => this.usagesService.getTodayStorage(app))
@ -154,14 +147,7 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit,
};
});
this.authenticationSubscription =
this.auth.isAuthenticated.subscribe(() => {
const user = this.auth.user;
if (user) {
this.profileDisplayName = user.displayName;
}
});
this.profileDisplayName = this.authService.user.displayName;
}
public showForum() {

14
src/Squidex/app/features/schemas/pages/schemas/schema-form.component.html

@ -8,9 +8,9 @@
<div class="form-group">
<label for="schema-name">Name</label>
<sqx-control-errors for="name" [submitted]="createFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="name" submitOnly="true" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="schema-name" formControlName="name" autocomplete="off" sqxLowerCaseInput />
<input type="text" class="form-control" id="schema-name" formControlName="name" autocomplete="off" sqxLowerCaseInput sqxFocusOnInit />
<small class="form-text text-muted">
The schema name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}{{appName}}/<b>{{schemaName | async}}</b>/.
@ -20,6 +20,11 @@
</small>
</div>
<div class="form-group clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-success">Create</button>
</div>
<div>
<button class="btn btn-sm btn-link" (click)="toggleImport()" [class.hidden]="showImport">
Import schema
@ -30,9 +35,4 @@
<sqx-json-editor *ngIf="showImport" formControlName="import"></sqx-json-editor>
</div>
<div class="form-group clearfix">
<button type="reset" class="float-left btn btn-secondary" (click)="cancel()">Cancel</button>
<button type="submit" class="float-right btn btn-success">Create</button>
</div>
</form>

6
src/Squidex/app/framework/angular/can-deactivate.guard.spec.ts

@ -5,6 +5,8 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Observable } from 'rxjs';
import { CanDeactivateGuard } from './can-deactivate.guard';
describe('CanDeactivateGuard', () => {
@ -15,13 +17,13 @@ describe('CanDeactivateGuard', () => {
canDeactivate: () => {
called = true;
return true;
return Observable.of(true);
}
};
const result = new CanDeactivateGuard().canDeactivate(component);
expect(result).toBeTruthy();
expect(result).toBeDefined();
expect(called).toBeTruthy();
});
});

2
src/Squidex/app/framework/angular/can-deactivate.guard.ts

@ -10,7 +10,7 @@ import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs/Observable';
export interface CanComponentDeactivate {
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
canDeactivate(): Observable<boolean>;
}
@Injectable()

32
src/Squidex/app/framework/angular/cloak.directive.spec.ts

@ -1,32 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { CloakDirective } from './cloak.directive';
describe('CloakDirective', () => {
it('should remove class from element on ngOnInit', () => {
let called = false;
const element = {
nativeElement: {}
};
const renderer = {
setElementClass: (target: any, className: string, isAdd: boolean) => {
called = true;
expect(target).toBe(element.nativeElement);
expect(className).toBe('sqx-cloak');
expect(isAdd).toBeFalsy();
}
};
new CloakDirective(<any>element, <any>renderer).ngOnInit();
expect(called).toBeTruthy();
});
});

23
src/Squidex/app/framework/angular/cloak.directive.ts

@ -1,23 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Directive, ElementRef, OnInit, Renderer } from '@angular/core';
@Directive({
selector: '[sqxCloak]'
})
export class CloakDirective implements OnInit {
constructor(
private readonly element: ElementRef,
private readonly renderer: Renderer
) {
}
public ngOnInit() {
this.renderer.setElementClass(this.element.nativeElement, 'sqx-cloak', false);
}
}

48
src/Squidex/app/framework/angular/date-time-editor.component.ts

@ -5,8 +5,9 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { AfterViewInit, Component, forwardRef, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, Component, forwardRef, ElementRef, Input, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Subscription } from 'rxjs';
import * as moment from 'moment';
let Pikaday = require('pikaday/pikaday');
@ -23,7 +24,9 @@ export const SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR: any = {
templateUrl: './date-time-editor.component.html',
providers: [SQX_DATE_TIME_EDITOR_CONTROL_VALUE_ACCESSOR]
})
export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, AfterViewInit {
export class DateTimeEditorComponent implements ControlValueAccessor, OnDestroy, OnInit, AfterViewInit {
private timeSubscription: Subscription;
private dateSubscription: Subscription;
private picker: any;
private timeValue: any | null = null;
private dateValue: any | null = null;
@ -54,26 +57,33 @@ export class DateTimeEditorComponent implements ControlValueAccessor, OnInit, Af
public isDisabled = false;
public ngOnInit() {
this.timeControl.valueChanges.subscribe(value => {
if (!value || value.length === 0) {
this.timeValue = null;
} else {
this.timeValue = moment(value, 'HH:mm:ss');
}
public ngOnDestroy() {
this.dateSubscription.unsubscribe();
this.timeSubscription.unsubscribe();
}
this.updateValue();
});
public ngOnInit() {
this.timeSubscription =
this.timeControl.valueChanges.subscribe(value => {
if (!value || value.length === 0) {
this.timeValue = null;
} else {
this.timeValue = moment(value, 'HH:mm:ss');
}
this.dateControl.valueChanges.subscribe(value => {
if (!value || value.length === 0) {
this.dateValue = null;
} else {
this.dateValue = moment(value, 'YYYY-MM-DD');
}
this.updateValue();
});
this.dateSubscription =
this.dateControl.valueChanges.subscribe(value => {
if (!value || value.length === 0) {
this.dateValue = null;
} else {
this.dateValue = moment(value, 'YYYY-MM-DD');
}
this.updateValue();
});
this.updateValue();
});
}
public writeValue(value: any) {

53
src/Squidex/app/framework/angular/focus-on-change.directive.spec.ts

@ -1,53 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { ElementRef, Renderer } from '@angular/core';
import { FocusOnChangeDirective } from './focus-on-change.directive';
describe('FocusOnChangeDirective', () => {
let originalTimeout = 0;
beforeEach(() => {
originalTimeout = jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmine.DEFAULT_TIMEOUT_INTERVAL = 800;
});
it('should call focus on element when value changes', (done: any) => {
const calledMethods: string[] = [];
const calledElements: any[] = [];
const renderer = {
invokeElementMethod: (element: any, method: any, args: any) => {
calledElements.push(element);
calledMethods.push(method);
}
};
const element: ElementRef = {
nativeElement: {}
};
const directive = new FocusOnChangeDirective(element, renderer as Renderer);
directive.select = true;
directive.ngOnChanges();
expect(calledMethods).toEqual([]);
setTimeout(() => {
expect(calledMethods).toEqual(['focus', 'select']);
expect(calledElements).toEqual([element.nativeElement, element.nativeElement]);
done();
}, 400);
});
afterEach(() => {
jasmine.DEFAULT_TIMEOUT_INTERVAL = originalTimeout;
});
});

35
src/Squidex/app/framework/angular/focus-on-change.directive.ts

@ -1,35 +0,0 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Directive, ElementRef, Input, OnChanges, Renderer }from '@angular/core';
@Directive({
selector: '[sqxFocusOnChange]'
})
export class FocusOnChangeDirective implements OnChanges {
@Input()
public sqxFocusOnChange: any;
@Input()
public select: boolean;
constructor(
private readonly element: ElementRef,
private readonly renderer: Renderer
) {
}
public ngOnChanges() {
setTimeout(() => {
this.renderer.invokeElementMethod(this.element.nativeElement, 'focus', []);
if (this.select) {
this.renderer.invokeElementMethod(this.element.nativeElement, 'select', []);
}
}, 100);
}
}

4
src/Squidex/app/framework/angular/focus-on-init.directive.spec.ts

@ -23,8 +23,8 @@ describe('FocusOnInitDirective', () => {
const calledElements: any[] = [];
const renderer = {
invokeElementMethod: (element: any, method: any, args: any) => {
calledElements.push(element);
invokeElementMethod: (elem: any, method: any, args: any) => {
calledElements.push(elem);
calledMethods.push(method);
}
};

2
src/Squidex/app/framework/angular/root-view.directive.spec.ts

@ -7,6 +7,8 @@
import { RootViewDirective } from './../';
/* tslint:disable:no-unused-expression */
describe('RootViewDirective', () => {
it('should call init of service in ctor', () => {
let viewRef = {};

2
src/Squidex/app/framework/declarations.ts

@ -8,14 +8,12 @@
export * from './angular/animations';
export * from './angular/autocomplete.component';
export * from './angular/can-deactivate.guard';
export * from './angular/cloak.directive';
export * from './angular/control-errors.component';
export * from './angular/copy.directive';
export * from './angular/date-time-editor.component';
export * from './angular/date-time.pipes';
export * from './angular/dropdown.component';
export * from './angular/file-drop.directive';
export * from './angular/focus-on-change.directive';
export * from './angular/focus-on-init.directive';
export * from './angular/geolocation-editor.component';
export * from './angular/http-extensions-impl';

6
src/Squidex/app/framework/module.ts

@ -15,7 +15,6 @@ import {
AutocompleteComponent,
CanDeactivateGuard,
ClipboardService,
CloakDirective,
ControlErrorsComponent,
CopyDirective,
DateTimeEditorComponent,
@ -25,7 +24,6 @@ import {
DropdownComponent,
DurationPipe,
FileDropDirective,
FocusOnChangeDirective,
FocusOnInitDirective,
FromNowPipe,
GeolocationEditorComponent,
@ -76,7 +74,6 @@ import {
],
declarations: [
AutocompleteComponent,
CloakDirective,
ControlErrorsComponent,
CopyDirective,
DateTimeEditorComponent,
@ -86,7 +83,6 @@ import {
DropdownComponent,
DurationPipe,
FileDropDirective,
FocusOnChangeDirective,
FocusOnInitDirective,
FromNowPipe,
GeolocationEditorComponent,
@ -121,7 +117,6 @@ import {
],
exports: [
AutocompleteComponent,
CloakDirective,
ControlErrorsComponent,
CopyDirective,
DateTimeEditorComponent,
@ -131,7 +126,6 @@ import {
DropdownComponent,
DurationPipe,
FileDropDirective,
FocusOnChangeDirective,
FocusOnInitDirective,
FromNowPipe,
GeolocationEditorComponent,

4
src/Squidex/app/shared/components/app-form.component.html

@ -8,9 +8,9 @@
<div class="form-group">
<label for="app-name">Name</label>
<sqx-control-errors for="name" [submitted]="createFormSubmitted"></sqx-control-errors>
<sqx-control-errors for="name" submitOnly="true" [submitted]="createFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="app-name" formControlName="name" autocomplete="off" sqxLowerCaseInput />
<input type="text" class="form-control" id="app-name" formControlName="name" autocomplete="off" sqxLowerCaseInput sqxFocusOnInit />
<small class="form-text text-muted">
The app name becomes part of the api url,<br /> e.g {{apiUrl.buildUrl("api/content/")}}<b>{{appName | async}}</b>/.

2
src/Squidex/app/shared/components/app.component-base.ts

@ -27,7 +27,7 @@ export abstract class AppComponentBase extends ComponentBase {
}
public appNameOnce(): Observable<string> {
return this.appName$.take(1);
return this.appName$.first();
}
}

1
src/Squidex/app/shared/declarations-base.ts

@ -25,6 +25,7 @@ export * from './services/assets.service';
export * from './services/auth.service';
export * from './services/contents.service';
export * from './services/event-consumers.service';
export * from './services/graphql.service';
export * from './services/help.service';
export * from './services/history.service';
export * from './services/languages.service';

13
src/Squidex/app/shared/guards/app-must-exist.guard.spec.ts

@ -6,6 +6,7 @@
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { AppsStoreService } from 'shared';
@ -21,14 +22,14 @@ describe('AppMustExistGuard', () => {
it('should navigate to 404 page if app is not found', (done) => {
appsStore.setup(x => x.selectApp('my-app'))
.returns(() => Promise.resolve(false));
.returns(() => Observable.of(false));
const router = new RouterMockup();
const route = <any> { params: { appName: 'my-app' } };
const guard = new AppMustExistGuard(appsStore.object, <any>router);
guard.canActivate(route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -38,14 +39,14 @@ describe('AppMustExistGuard', () => {
it('should navigate to 404 page if app loading fails', (done) => {
appsStore.setup(x => x.selectApp('my-app'))
.returns(() => Promise.reject<boolean>('error'));
.returns(() => Observable.throw('error'));
const router = new RouterMockup();
const route = <any> { params: { appName: 'my-app' } };
const guard = new AppMustExistGuard(appsStore.object, <any>router);
guard.canActivate(route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -55,14 +56,14 @@ describe('AppMustExistGuard', () => {
it('should return true if app is found', (done) => {
appsStore.setup(x => x.selectApp('my-app'))
.returns(() => Promise.resolve(true));
.returns(() => Observable.of(true));
const router = new RouterMockup();
const route = <any> { params: { appName: 'my-app' } };
const guard = new AppMustExistGuard(appsStore.object, <any>router);
guard.canActivate(route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeTruthy();
expect(router.lastNavigation).toBeUndefined();

14
src/Squidex/app/shared/guards/app-must-exist.guard.ts

@ -7,6 +7,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AppsStoreService } from './../services/apps-store.service';
@ -18,21 +19,20 @@ export class AppMustExistGuard implements CanActivate {
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const appName = route.params['appName'];
const result =
this.appsStore.selectApp(appName)
.then(hasApp => {
if (!hasApp) {
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
return hasApp;
}, () => {
})
.catch(error => {
this.router.navigate(['/404']);
return false;
return Observable.of(false);
});
return result;

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

@ -6,6 +6,7 @@
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { AuthService } from 'shared';
@ -20,14 +21,14 @@ describe('MustBeAuthenticatedGuard', () => {
});
it('should navigate to default page if not authenticated', (done) => {
authService.setup(x => x.checkLogin())
.returns(() => Promise.resolve(false));
authService.setup(x => x.userChanges)
.returns(() => Observable.of(null));
const router = new RouterMockup();
const guard = new MustBeAuthenticatedGuard(authService.object, <any>router);
guard.canActivate(<any>{}, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['']);
@ -36,14 +37,14 @@ describe('MustBeAuthenticatedGuard', () => {
});
it('should return true if authenticated', (done) => {
authService.setup(x => x.checkLogin())
.returns(() => Promise.resolve(true));
authService.setup(x => x.userChanges)
.returns(() => Observable.of(<any>{}));
const router = new RouterMockup();
const guard = new MustBeAuthenticatedGuard(authService.object, <any>router);
guard.canActivate(<any>{}, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeTruthy();
expect(router.lastNavigation).toBeUndefined();

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

@ -7,23 +7,25 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './../services/auth.service';
@Injectable()
export class MustBeAuthenticatedGuard implements CanActivate {
constructor(
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return this.auth.checkLogin().then(isAuthenticated => {
if (!isAuthenticated) {
this.router.navigate(['']);
}
return isAuthenticated;
});
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.authService.userChanges.first()
.do(user => {
if (!user) {
this.router.navigate(['']);
}
})
.map(user => !!user);
}
}

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

@ -6,6 +6,7 @@
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { AuthService } from 'shared';
@ -20,14 +21,14 @@ describe('MustBeNotAuthenticatedGuard', () => {
});
it('should navigate to app page if authenticated', (done) => {
authService.setup(x => x.checkLogin())
.returns(() => Promise.resolve(true));
authService.setup(x => x.userChanges)
.returns(() => Observable.of(<any>{}));
const router = new RouterMockup();
const guard = new MustBeNotAuthenticatedGuard(authService.object, <any>router);
guard.canActivate(<any>{}, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['app']);
@ -36,14 +37,14 @@ describe('MustBeNotAuthenticatedGuard', () => {
});
it('should return true if not authenticated', (done) => {
authService.setup(x => x.checkLogin())
.returns(() => Promise.resolve(false));
authService.setup(x => x.userChanges)
.returns(() => Observable.of(null));
const router = new RouterMockup();
const guard = new MustBeNotAuthenticatedGuard(authService.object, <any>router);
guard.canActivate(<any>{}, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeTruthy();
expect(router.lastNavigation).toBeUndefined();

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

@ -7,23 +7,25 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './../services/auth.service';
@Injectable()
export class MustBeNotAuthenticatedGuard implements CanActivate {
constructor(
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> {
return this.auth.checkLogin().then(isAuthenticated => {
if (isAuthenticated) {
this.router.navigate(['app']);
}
return !isAuthenticated;
});
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.authService.userChanges.first()
.do(user => {
if (user) {
this.router.navigate(['app']);
}
})
.map(user => !user);
}
}

6
src/Squidex/app/shared/guards/resolve-app-languages.guard.spec.ts

@ -43,7 +43,7 @@ describe('ResolveAppLanguagesGuard', () => {
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -59,7 +59,7 @@ describe('ResolveAppLanguagesGuard', () => {
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -77,7 +77,7 @@ describe('ResolveAppLanguagesGuard', () => {
const guard = new ResolveAppLanguagesGuard(appLanguagesService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBe(languages);
done();

16
src/Squidex/app/shared/guards/resolve-app-languages.guard.ts

@ -7,6 +7,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
@ -20,7 +21,7 @@ export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<AppLanguageDto[]> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<AppLanguageDto[]> {
const params = allParams(route);
const appName = params['appName'];
@ -30,19 +31,16 @@ export class ResolveAppLanguagesGuard implements Resolve<AppLanguageDto[]> {
}
const result =
this.appLanguagesService.getLanguages(appName).toPromise()
.then(dto => {
this.appLanguagesService.getLanguages(appName)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
})
.catch(error => {
this.router.navigate(['/404']);
return null;
return Observable.of(null);
});
return result;

6
src/Squidex/app/shared/guards/resolve-content.guard.spec.ts

@ -62,7 +62,7 @@ describe('ResolveContentGuard', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -78,7 +78,7 @@ describe('ResolveContentGuard', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -96,7 +96,7 @@ describe('ResolveContentGuard', () => {
const guard = new ResolveContentGuard(appsStore.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBe(content);
done();

16
src/Squidex/app/shared/guards/resolve-content.guard.ts

@ -7,6 +7,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
@ -20,7 +21,7 @@ export class ResolveContentGuard implements Resolve<ContentDto> {
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<ContentDto> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<ContentDto> {
const params = allParams(route);
const appName = params['appName'];
@ -42,19 +43,16 @@ export class ResolveContentGuard implements Resolve<ContentDto> {
}
const result =
this.contentsService.getContent(appName, schemaName, contentId).toPromise()
.then(dto => {
this.contentsService.getContent(appName, schemaName, contentId)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
})
.catch(error => {
this.router.navigate(['/404']);
return null;
return Observable.of(null);
});
return result;

8
src/Squidex/app/shared/guards/resolve-published-schema.guard.spec.ts

@ -51,7 +51,7 @@ describe('ResolvePublishedSchemaGuard', () => {
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -67,7 +67,7 @@ describe('ResolvePublishedSchemaGuard', () => {
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -85,7 +85,7 @@ describe('ResolvePublishedSchemaGuard', () => {
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -103,7 +103,7 @@ describe('ResolvePublishedSchemaGuard', () => {
const guard = new ResolvePublishedSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBe(schema);
done();

18
src/Squidex/app/shared/guards/resolve-published-schema.guard.ts

@ -7,6 +7,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
@ -20,7 +21,7 @@ export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> {
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SchemaDetailsDto> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<SchemaDetailsDto> {
const params = allParams(route);
const appName = params['appName'];
@ -36,19 +37,16 @@ export class ResolvePublishedSchemaGuard implements Resolve<SchemaDetailsDto> {
}
const result =
this.schemasService.getSchema(appName, schemaName).toPromise()
.then(dto => {
if (!dto || !dto.isPublished) {
this.schemasService.getSchema(appName, schemaName).map(dto => dto && dto.isPublished ? dto : null)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
})
.catch(error => {
this.router.navigate(['/404']);
return null;
return Observable.of(error);
});
return result;

6
src/Squidex/app/shared/guards/resolve-schema.guard.spec.ts

@ -51,7 +51,7 @@ describe('ResolveSchemaGuard', () => {
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -67,7 +67,7 @@ describe('ResolveSchemaGuard', () => {
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -85,7 +85,7 @@ describe('ResolveSchemaGuard', () => {
const guard = new ResolveSchemaGuard(schemasService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBe(schema);
done();

16
src/Squidex/app/shared/guards/resolve-schema.guard.ts

@ -7,6 +7,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
@ -20,7 +21,7 @@ export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> {
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<SchemaDetailsDto> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<SchemaDetailsDto> {
const params = allParams(route);
const appName = params['appName'];
@ -36,19 +37,16 @@ export class ResolveSchemaGuard implements Resolve<SchemaDetailsDto> {
}
const result =
this.schemasService.getSchema(appName, schemaName).toPromise()
.then(dto => {
this.schemasService.getSchema(appName, schemaName)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
})
.catch(error => {
this.router.navigate(['/404']);
return null;
return Observable.of(error);
});
return result;

6
src/Squidex/app/shared/guards/resolve-user.guard.spec.ts

@ -43,7 +43,7 @@ describe('ResolveUserGuard', () => {
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -59,7 +59,7 @@ describe('ResolveUserGuard', () => {
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
@ -77,7 +77,7 @@ describe('ResolveUserGuard', () => {
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
.subscribe(result => {
expect(result).toBe(user);
done();

16
src/Squidex/app/shared/guards/resolve-user.guard.ts

@ -7,6 +7,7 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
@ -20,7 +21,7 @@ export class ResolveUserGuard implements Resolve<UserDto> {
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<UserDto> {
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<UserDto> {
const params = allParams(route);
const userId = params['userId'];
@ -30,19 +31,16 @@ export class ResolveUserGuard implements Resolve<UserDto> {
}
const result =
this.userManagementService.getUser(userId).toPromise()
.then(dto => {
this.userManagementService.getUser(userId)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
})
.catch(error => {
this.router.navigate(['/404']);
return null;
return Observable.of(null);
});
return result;

36
src/Squidex/app/shared/interceptors/auth.interceptor.spec.ts

@ -8,6 +8,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { HttpClient, HttpHeaders, HTTP_INTERCEPTORS } from '@angular/common/http';
import { inject, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs';
import { IMock, Mock, Times } from 'typemoq';
import {
@ -16,12 +17,11 @@ import {
AuthInterceptor
} from './../';
describe('AppClientsService', () => {
describe('AuthInterceptor', () => {
let authService: IMock<AuthService> = null;
beforeEach(() => {
authService = Mock.ofType(AuthService);
authService.setup(x => x.user).returns(() => { return <any>{ authToken: 'letmein' }; });
TestBed.configureTestingModule({
imports: [
@ -46,6 +46,8 @@ describe('AppClientsService', () => {
it('should append headers to request',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein' }); });
http.get('http://service/p/apps').subscribe();
const req = httpMock.expectOne('http://service/p/apps');
@ -59,6 +61,8 @@ describe('AppClientsService', () => {
it('should not append headers for no auth headers',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein' }); });
http.get('http://service/p/apps', { headers: new HttpHeaders().set('NoAuth', '') }).subscribe();
const req = httpMock.expectOne('http://service/p/apps');
@ -72,6 +76,8 @@ describe('AppClientsService', () => {
it('should not append headers for other requests',
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein' }); });
http.get('http://cloud/p/apps').subscribe();
const req = httpMock.expectOne('http://cloud/p/apps');
@ -85,7 +91,7 @@ describe('AppClientsService', () => {
it(`should logout for 404 status code when user is expired.`,
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.user).returns(() => { return <any>{ authToken: 'letmein', isExpired: true }; });
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein', isExpired: true }); });
http.get('http://service/p/apps').subscribe(
_ => { /* NOOP */ },
@ -98,10 +104,30 @@ describe('AppClientsService', () => {
authService.verify(x => x.logoutRedirect(), Times.once());
}));
[401, 403].forEach(statusCode => {
it(`should logout for 401 status code`,
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein' }); });
authService.setup(x => x.loginSilent()).returns(() => { return Observable.of(<any>{ authToken: 'letmereallyin' }); });
http.get('http://service/p/apps').subscribe(
_ => { /* NOOP */ },
_ => { /* NOOP */ });
// const req = httpMock.expectOne('http://service/p/apps');
httpMock.expectOne('http://service/p/apps').error(<any>{}, { status: 401 });
httpMock.expectOne('http://service/p/apps').error(<any>{}, { status: 401 });
authService.verify(x => x.logoutRedirect(), Times.once());
}));
[403].forEach(statusCode => {
it(`should logout for ${statusCode} status code`,
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein' }); });
http.get('http://service/p/apps').subscribe(
_ => { /* NOOP */ },
_ => { /* NOOP */ });
@ -118,6 +144,8 @@ describe('AppClientsService', () => {
it(`should not logout for ${statusCode} status code`,
inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
authService.setup(x => x.userChanges).returns(() => { return Observable.of(<any>{ authToken: 'letmein' }); });
http.get('http://service/p/apps').subscribe(
_ => { /* NOOP */ },
_ => { /* NOOP */ });

50
src/Squidex/app/shared/interceptors/auth.interceptor.ts

@ -9,7 +9,7 @@ import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpErrorResponse
import { Injectable} from '@angular/core';
import { Observable } from 'rxjs';
import { AuthService } from './../services/auth.service';
import { AuthService, Profile } from './../services/auth.service';
import { ApiUrlConfig } from 'framework';
@Injectable()
@ -24,28 +24,38 @@ export class AuthInterceptor implements HttpInterceptor {
public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.url.indexOf(this.baseUrl) === 0 && !req.headers.has('NoAuth')) {
const authReq = req.clone({
headers: req.headers
.set('Authorization', this.authService.user ? this.authService.user.authToken : '')
.set('Accept-Language', '*')
.set('Pragma', 'no-cache')
return this.authService.userChanges.first().switchMap(user => {
return this.makeRequest(req, next, user, true);
});
return next.handle(authReq)
.catch((error: HttpErrorResponse) => {
if (error.status === 404 && (!this.authService.user || this.authService.user.isExpired)) {
this.authService.logoutRedirect();
return Observable.empty<Response>();
} else if (error.status === 401 || error.status === 403) {
this.authService.logoutRedirect();
return Observable.empty<Response>();
}
return Observable.throw(error);
});
} else {
return next.handle(req);
}
}
private makeRequest(req: HttpRequest<any>, next: HttpHandler, user: Profile, renew = false): Observable<HttpEvent<any>> {
const token = user ? user.authToken : '';
const authReq = req.clone({
headers: req.headers
.set('Authorization', token)
.set('Accept-Language', '*')
.set('Pragma', 'no-cache')
});
return next.handle(authReq)
.catch((error: HttpErrorResponse) => {
if (error.status === 401 && renew) {
return this.authService.loginSilent().switchMap(u => this.makeRequest(req, next, u));
} else if (error.status === 404 && (!user || user.isExpired)) {
this.authService.logoutRedirect();
return Observable.empty<Response>();
}else if (error.status === 401 || error.status === 403) {
this.authService.logoutRedirect();
return Observable.empty<Response>();
}
return Observable.throw(error);
});
}
}

2
src/Squidex/app/shared/module.ts

@ -26,6 +26,7 @@ import {
ContentsService,
EventConsumersService,
HelpComponent,
GraphQlService,
HelpService,
HistoryComponent,
HistoryService,
@ -106,6 +107,7 @@ export class SqxSharedModule {
AuthService,
ContentsService,
EventConsumersService,
GraphQlService,
HelpService,
HistoryService,
LanguagesService,

79
src/Squidex/app/shared/services/apps-store.service.spec.ts

@ -12,7 +12,6 @@ import {
AppDto,
AppsService,
AppsStoreService,
AuthService,
CreateAppDto,
DateTime
} from './../';
@ -24,23 +23,17 @@ describe('AppsStoreService', () => {
const newApp = new AppDto('id', 'new-name', 'Owner', now, now);
let appsService: IMock<AppsService>;
let authService: IMock<AuthService>;
beforeEach(() => {
appsService = Mock.ofType(AppsService);
authService = Mock.ofType(AuthService);
});
it('should load when authenticated once', () => {
authService.setup(x => x.isAuthenticated)
.returns(() => Observable.of(true))
.verifiable(Times.once());
it('should load automatically', () => {
appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps))
.verifiable(Times.once());
const store = new AppsStoreService(authService.object, appsService.object);
const store = new AppsStoreService(appsService.object);
let result1: AppDto[] | null = null;
let result2: AppDto[] | null = null;
@ -59,41 +52,7 @@ describe('AppsStoreService', () => {
appsService.verifyAll();
});
it('should reload value from apps-service when called', () => {
authService.setup(x => x.isAuthenticated)
.returns(() => Observable.of(true))
.verifiable(Times.once());
appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps))
.verifiable(Times.exactly(2));
const store = new AppsStoreService(authService.object, appsService.object);
let result1: AppDto[] | null = null;
let result2: AppDto[] | null = null;
store.apps.subscribe(x => {
result1 = x;
}).unsubscribe();
store.reload();
store.apps.subscribe(x => {
result2 = x;
}).unsubscribe();
expect(result1).toEqual(oldApps);
expect(result2).toEqual(oldApps);
appsService.verifyAll();
});
it('should add app to cache when created', () => {
authService.setup(x => x.isAuthenticated)
.returns(() => Observable.of(true))
.verifiable(Times.once());
appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps))
.verifiable(Times.once());
@ -102,7 +61,7 @@ describe('AppsStoreService', () => {
.returns(() => Observable.of(newApp))
.verifiable(Times.once());
const store = new AppsStoreService(authService.object, appsService.object);
const store = new AppsStoreService(appsService.object);
let result1: AppDto[] | null = null;
let result2: AppDto[] | null = null;
@ -123,42 +82,14 @@ describe('AppsStoreService', () => {
appsService.verifyAll();
});
it('should not add app to cache when cache is null', () => {
authService.setup(x => x.isAuthenticated)
.returns(() => Observable.of(false))
.verifiable(Times.once());
appsService.setup(x => x.postApp(It.isAny()))
.returns(() => Observable.of(newApp))
.verifiable(Times.once());
const store = new AppsStoreService(authService.object, appsService.object);
let result: AppDto[] | null = null;
store.createApp(new CreateAppDto('new-name'), now).subscribe(x => { /* Do Nothing */ });
store.apps.subscribe(x => {
result = x;
}).unsubscribe();
expect(result).toBeNull();
appsService.verifyAll();
});
it('should select app', (done) => {
authService.setup(x => x.isAuthenticated)
.returns(() => Observable.of(true))
.verifiable(Times.once());
appsService.setup(x => x.getApps())
.returns(() => Observable.of(oldApps))
.verifiable(Times.once());
const store = new AppsStoreService(authService.object, appsService.object);
const store = new AppsStoreService(appsService.object);
store.selectApp('old-name').then((isSelected) => {
store.selectApp('old-name').subscribe(isSelected => {
expect(isSelected).toBeTruthy();
appsService.verifyAll();

79
src/Squidex/app/shared/services/apps-store.service.ts

@ -6,7 +6,7 @@
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { BehaviorSubject, Observable, Observer, ReplaySubject } from 'rxjs';
import { DateTime } from 'framework';
@ -16,74 +16,47 @@ import {
CreateAppDto
} from './apps.service';
import { AuthService } from './auth.service';
@Injectable()
export class AppsStoreService {
private readonly apps$ = new Subject<AppDto[]>();
private readonly appName$ = new BehaviorSubject<string | null>(null);
private lastApps: AppDto[] | null = null;
private isAuthenticated = false;
private readonly appsPublished$ =
this.apps$
.distinctUntilChanged()
.publishReplay(1);
private readonly selectedApp$ =
this.appsPublished$.combineLatest(this.appName$, (apps, name) => apps && name ? apps.find(x => x.name === name) || null : null)
.distinctUntilChanged()
.publishReplay(1);
private readonly apps$ = new ReplaySubject<AppDto[]>(1);
private readonly app$ = new BehaviorSubject<AppDto | null>(null);
public get apps(): Observable<AppDto[]> {
return this.appsPublished$;
return this.apps$;
}
public get selectedApp(): Observable<AppDto | null> {
return this.selectedApp$;
return this.app$;
}
constructor(
private readonly auth: AuthService,
private readonly appsService: AppsService
) {
if (!auth || !appsService) {
if (!appsService) {
return;
}
this.selectedApp$.connect();
this.appsPublished$.connect();
this.appsPublished$.subscribe(apps => {
this.lastApps = apps;
});
this.auth.isAuthenticated.subscribe(isAuthenticated => {
this.isAuthenticated = isAuthenticated;
if (isAuthenticated) {
this.load();
}
});
}
public reload() {
if (this.isAuthenticated) {
this.load();
}
}
private load() {
this.appsService.getApps()
this.appsService.getApps()
.subscribe(apps => {
this.apps$.next(apps);
}, error => {
this.apps$.next([]);
});
}
public selectApp(name: string | null): Promise<boolean> {
this.appName$.next(name);
public selectApp(name: string | null): Observable<boolean> {
return Observable.create((observer: Observer<boolean>) => {
this.apps$.subscribe(apps => {
const app = apps.find(x => x.name === name) || null;
return this.selectedApp.take(1).map(app => app !== null).toPromise();
this.app$.next(app);
observer.next(app !== null);
observer.complete();
}, error => {
observer.error(error);
});
});
}
public createApp(dto: CreateAppDto, now?: DateTime): Observable<AppDto> {
@ -91,14 +64,12 @@ export class AppsStoreService {
.map(created => {
now = now || DateTime.now();
const app = new AppDto(created.id, dto.name, 'Owner', now, now);
return app;
return new AppDto(created.id, dto.name, 'Owner', now, now);
})
.do(app => {
if (this.lastApps && app) {
this.apps$.next(this.lastApps.concat([app]));
}
this.apps$.first().subscribe(apps => {
this.apps$.next(apps.concat([app]));
});
});
}
}

95
src/Squidex/app/shared/services/auth.service.ts

@ -6,12 +6,13 @@
*/
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Observable, ReplaySubject } from 'rxjs';
import {
Log,
User,
UserManager
UserManager,
WebStorageStateStore
} from 'oidc-client';
import { ApiUrlConfig } from 'framework';
@ -54,22 +55,15 @@ export class Profile {
@Injectable()
export class AuthService {
private readonly userManager: UserManager;
private readonly isAuthenticatedChanged$ = new Subject<boolean>();
private loginCompleted: boolean | null = false;
private loginCache: Promise<boolean> | null = null;
private currentUser: Profile | null = null;
private readonly isAuthenticatedChangedPublished$ =
this.isAuthenticatedChanged$
.distinctUntilChanged()
.publishReplay(1);
private readonly user$ = new ReplaySubject<Profile | null>(1);
private currentUser: Profile = null;
public get user(): Profile | null {
return this.currentUser;
}
public get isAuthenticated(): Observable<boolean> {
return this.isAuthenticatedChangedPublished$;
public get userChanges(): Observable<Profile | null> {
return this.user$;
}
constructor(apiUrl: ApiUrlConfig) {
@ -88,32 +82,23 @@ export class AuthService {
silent_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-silent/'),
popup_redirect_uri: apiUrl.buildUrl('identity-server/client-callback-popup/'),
authority: apiUrl.buildUrl('identity-server/'),
userStore: new WebStorageStateStore({ store: window.localStorage || window.sessionStorage }),
automaticSilentRenew: true
});
this.userManager.events.addUserLoaded(user => {
this.onAuthenticated(user);
this.user$.next(new Profile(user));
});
this.userManager.events.addUserUnloaded(() => {
this.onDeauthenticated();
this.user$.next(null);
});
this.checkLogin();
this.user$.subscribe(user => {
this.currentUser = user;
});
this.isAuthenticatedChangedPublished$.connect();
}
public checkLogin(): Promise<boolean> {
if (this.loginCompleted) {
return Promise.resolve(this.currentUser !== null);
} else if (this.loginCache) {
return this.loginCache;
} else {
this.loginCache = this.checkState(this.userManager.signinSilent());
return this.loginCache;
}
this.checkState(this.userManager.getUser());
}
public logoutRedirect(): Observable<any> {
@ -124,51 +109,35 @@ export class AuthService {
return Observable.fromPromise(this.userManager.signoutRedirectCallback());
}
public loginRedirect(): Observable<any> {
return Observable.fromPromise(this.userManager.signinRedirect());
public loginPopup(): Observable<Profile> {
return Observable.fromPromise(this.userManager.signinPopup()).map(u => this.createProfile(u));
}
public loginRedirectComplete(): Observable<any> {
return Observable.fromPromise(this.userManager.signinRedirectCallback());
public loginSilent(): Observable<any> {
return Observable.fromPromise(this.userManager.signinSilent()).map(u => this.createProfile(u));
}
public loginPopup(): Observable<boolean> {
const promise = this.checkState(this.userManager.signinPopup());
return Observable.fromPromise(promise);
public loginRedirect(): Observable<any> {
return Observable.fromPromise(this.userManager.signinRedirect());
}
private onAuthenticated(user: User) {
this.currentUser = new Profile(user);
this.isAuthenticatedChanged$.next(true);
public loginRedirectComplete(): Observable<Profile> {
return Observable.fromPromise(this.userManager.signinRedirectCallback()).map(u => this.createProfile(u));
}
private onDeauthenticated() {
this.currentUser = null;
this.isAuthenticatedChanged$.next(false);
private createProfile(user: User) {
return user ? new Profile(user) : null;
}
private checkState(promise: Promise<User>): Promise<boolean> {
const resultPromise =
promise
.then(user => {
this.loginCache = null;
this.loginCompleted = null;
private checkState(promise: Promise<User>) {
promise.then(user => {
this.user$.next(this.createProfile(user));
this.onAuthenticated(user);
return true;
}, err => {
this.user$.next(null);
return !!this.currentUser;
}).catch((err) => {
this.loginCache = null;
this.loginCompleted = null;
this.onDeauthenticated();
return false;
});
return resultPromise;
return false;
});
}
}

48
src/Squidex/app/shared/services/graphql.service.spec.ts

@ -0,0 +1,48 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig, GraphQlService } from './../';
describe('GraphQlService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
GraphQlService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get history events',
inject([GraphQlService, HttpTestingController], (graphQlService: GraphQlService, httpMock: HttpTestingController) => {
let graphQlResult = null;
graphQlService.query('my-app', { }).subscribe(result => {
graphQlResult = result;
});
const req = httpMock.expectOne('http://service/p/api/content/my-app/graphql');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ result: true });
expect(graphQlResult).toEqual({ result: true });
}));
});

27
src/Squidex/app/shared/services/graphql.service.ts

@ -0,0 +1,27 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiUrlConfig } from 'framework';
@Injectable()
export class GraphQlService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public query(appName: string, params: any): Observable<any> {
const url = this.apiUrl.buildUrl(`api/content/${appName}/graphql`)
return this.http.post(url, params);
}
}

6
src/Squidex/app/shell/pages/home/home-page.component.ts

@ -19,16 +19,16 @@ export class HomePageComponent {
public showLoginError = false;
constructor(
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly router: Router
) {
}
public login() {
if (this.isIE()) {
this.auth.loginRedirect();
this.authService.loginRedirect();
} else {
this.auth.loginPopup()
this.authService.loginPopup()
.subscribe(() => {
this.router.navigate(['/app']);
}, ex => {

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

@ -5,12 +5,12 @@
<div class="dropdown-menu" *sqxModalView="modalMenu" closeAlways="true" [@fade]>
<a class="dropdown-item all-apps" routerLink="/app">
<span class="all-apps-text">All Apps</span>
<span class="all-apps-pill tag tag-pill tag-default">{{apps.length || 0}}</span>
<span class="all-apps-pill tag tag-pill tag-default">{{apps.length}}</span>
</a>
<div class="dropdown-divider"></div>
<div *ngIf="apps && apps.length > 0">
<div *ngIf="apps.length > 0">
<a class="dropdown-item" *ngFor="let app of apps" [routerLink]="['/app', app.name]" routerLinkActive="active">{{app.name}}</a>
<div class="dropdown-divider"></div>

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

@ -49,7 +49,7 @@ export class AppsMenuComponent implements OnInit, OnDestroy {
public ngOnInit() {
this.appsSubscription =
this.appsStore.apps.subscribe(apps => {
this.apps = apps || [];
this.apps = apps;
});
this.appSubscription =

18
src/Squidex/app/shell/pages/internal/profile-menu.component.ts

@ -36,7 +36,7 @@ export class ProfileMenuComponent implements OnInit, OnDestroy {
public profileUrl = this.apiUrl.buildUrl('/identity-server/account/profile');
constructor(
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly apiUrl: ApiUrlConfig
) {
}
@ -47,20 +47,16 @@ export class ProfileMenuComponent implements OnInit, OnDestroy {
public ngOnInit() {
this.authenticationSubscription =
this.auth.isAuthenticated.take(1)
.subscribe(() => {
const user = this.auth.user;
this.authService.userChanges.filter(user => !!user)
.subscribe(user => {
this.profileId = user.id;
this.profileDisplayName = user.displayName;
if (user) {
this.profileId = user.id;
this.profileDisplayName = user.displayName;
this.isAdmin = user.isAdmin;
}
this.isAdmin = user.isAdmin;
});
}
public logout() {
this.auth.logoutRedirect();
this.authService.logoutRedirect();
}
}

4
src/Squidex/app/shell/pages/login/login-page.component.ts

@ -16,13 +16,13 @@ import { AuthService } from 'shared';
})
export class LoginPageComponent implements OnInit {
constructor(
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly router: Router
) {
}
public ngOnInit() {
this.auth.loginRedirectComplete()
this.authService.loginRedirectComplete()
.subscribe(
() => {
this.router.navigate(['/app'], { replaceUrl: true });

4
src/Squidex/app/shell/pages/logout/logout-page.component.ts

@ -16,13 +16,13 @@ import { AuthService } from 'shared';
})
export class LogoutPageComponent implements OnInit {
constructor(
private readonly auth: AuthService,
private readonly authService: AuthService,
private readonly router: Router
) {
}
public ngOnInit() {
this.auth.logoutRedirectComplete()
this.authService.logoutRedirectComplete()
.subscribe(
() => {
this.router.navigate(['/'], { replaceUrl: true });

27
tests/Squidex.Domain.Apps.Read.Tests/MongoDb/Contents/ODataQueryTests.cs → tests/Squidex.Domain.Apps.Read.Tests/Schemas/ODataQueryTests.cs

@ -114,6 +114,33 @@ namespace Squidex.Domain.Apps.Read.MongoDb.Contents
Assert.Equal(o, i);
}
[Fact]
public void Should_create_contains_query_with_equals()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq true");
var o = C("{ 'do.1.de' : /Sebastian/i }");
Assert.Equal(o, i);
}
[Fact]
public void Should_create_negated_contains_query_with_equals()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i } }");
Assert.Equal(o, i);
}
[Fact]
public void Should_create_negated_contains_query_and_other()
{
var i = F("$filter=contains(data/firstName/de, 'Sebastian') eq false and data/isAdmin/iv eq true");
var o = C("{ 'do.1.de' : { '$not' : /Sebastian/i }, 'do.3.iv' : true }");
Assert.Equal(o, i);
}
[Fact]
public void Should_create_string_equals_query()
{

3
tests/Squidex.Domain.Apps.Read.Tests/Squidex.Domain.Apps.Read.Tests.csproj

@ -26,4 +26,7 @@
<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<ItemGroup>
<Folder Include="MongoDb\Contents\" />
</ItemGroup>
</Project>
Loading…
Cancel
Save