Browse Source

User Management improved

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
eb4b40b1bc
  1. 6
      src/Squidex.Read.MongoDb/Users/MongoUserStore.cs
  2. 2
      src/Squidex.Read/Users/IUserResolver.cs
  3. 4
      src/Squidex.Read/Users/UserManagerExtensions.cs
  4. 2
      src/Squidex.Write/Apps/AppCommandHandler.cs
  5. 1
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  6. 21
      src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs
  7. 29
      src/Squidex/Controllers/Api/Users/UserManagementController.cs
  8. 1
      src/Squidex/app/features/administration/declarations.ts
  9. 18
      src/Squidex/app/features/administration/module.ts
  10. 25
      src/Squidex/app/features/administration/pages/users/messages.ts
  11. 65
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  12. 6
      src/Squidex/app/features/administration/pages/users/user-page.component.scss
  13. 157
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  14. 10
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  15. 37
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  16. 20
      src/Squidex/app/features/content/pages/content/content-page.component.ts
  17. 2
      src/Squidex/app/features/content/pages/contents/contents-page.component.html
  18. 7
      src/Squidex/app/features/content/pages/contents/contents-page.component.ts
  19. 8
      src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts
  20. 4
      src/Squidex/app/features/schemas/pages/schema/schema-page.component.ts
  21. 12
      src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts
  22. 4
      src/Squidex/app/features/settings/pages/languages/languages-page.component.ts
  23. 7
      src/Squidex/app/shared/components/app-form.component.ts
  24. 7
      src/Squidex/app/shared/components/asset.component.ts
  25. 1
      src/Squidex/app/shared/declarations-base.ts
  26. 85
      src/Squidex/app/shared/guards/resolve-user.guard.spec.ts
  27. 62
      src/Squidex/app/shared/guards/resolve-user.guard.ts
  28. 3
      src/Squidex/app/shared/module.ts
  29. 28
      src/Squidex/app/shared/services/users.service.spec.ts
  30. 58
      src/Squidex/app/shared/services/users.service.ts
  31. 15
      src/Squidex/app/theme/_lists.scss
  32. 8
      tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

6
src/Squidex.Read.MongoDb/Users/MongoUserStore.cs

@ -31,6 +31,7 @@ namespace Squidex.Read.MongoDb.Users
IUserLockoutStore<IUser>,
IUserAuthenticationTokenStore<IUser>,
IUserFactory,
IUserResolver,
IQueryableUserStore<IUser>
{
private readonly UserStore<WrappedIdentityUser> innerStore;
@ -60,6 +61,11 @@ namespace Squidex.Read.MongoDb.Users
return new WrappedIdentityUser { Email = email, UserName = email };
}
public async Task<IUser> FindByIdAsync(string userId)
{
return await innerStore.FindByIdAsync(userId, CancellationToken.None);
}
public async Task<IUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
{
return await innerStore.FindByIdAsync(userId, cancellationToken);

2
src/Squidex.Read/Users/IUserResolver.cs

@ -12,6 +12,6 @@ namespace Squidex.Read.Users
{
public interface IUserResolver
{
Task<IUser> FindById(string id);
Task<IUser> FindByIdAsync(string id);
}
}

4
src/Squidex.Read/Users/UserManagerExtensions.cs

@ -50,7 +50,7 @@ namespace Squidex.Read.Users
return result;
}
public static async Task<string> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password)
public static async Task<IUser> CreateAsync(this UserManager<IUser> userManager, IUserFactory factory, string email, string displayName, string password)
{
var user = factory.Create(email);
@ -64,7 +64,7 @@ namespace Squidex.Read.Users
await DoChecked(() => userManager.AddPasswordAsync(user, password), "Cannot create user.");
}
return user.Id;
return user;
}
public static async Task UpdateAsync(this UserManager<IUser> userManager, string id, string email, string displayName, string password)

2
src/Squidex.Write/Apps/AppCommandHandler.cs

@ -69,7 +69,7 @@ namespace Squidex.Write.Apps
protected async Task On(AssignContributor command, CommandContext context)
{
if (await userResolver.FindById(command.ContributorId) == null)
if (await userResolver.FindByIdAsync(command.ContributorId) == null)
{
var error =
new ValidationError("Cannot find contributor the contributor",

1
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -88,6 +88,7 @@ namespace Squidex.Config.Domain
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseRegistration))
.As<IUserStore<IUser>>()
.As<IUserFactory>()
.As<IUserResolver>()
.SingleInstance();
builder.RegisterType<MongoRoleStore>()

21
src/Squidex/Controllers/Api/Users/Models/UserCreatedDto.cs

@ -0,0 +1,21 @@
// ==========================================================================
// UserCreatedDto.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.ComponentModel.DataAnnotations;
namespace Squidex.Controllers.Api.Users.Models
{
public class UserCreatedDto
{
[Required]
public string Id { get; set; }
[Required]
public string PictureUrl { get; set; }
}
}

29
src/Squidex/Controllers/Api/Users/UserManagementController.cs

@ -54,14 +54,31 @@ namespace Squidex.Controllers.Api.Users
return Ok(response);
}
[HttpGet]
[Route("user-management/{id}")]
[ApiCosts(0)]
public async Task<IActionResult> GetUser(string id)
{
var entity = await userManager.FindByIdAsync(id);
if (entity == null)
{
return NotFound();
}
var response = SimpleMapper.Map(entity, new UserDto { DisplayName = entity.DisplayName(), PictureUrl = entity.PictureUrl() });
return Ok(response);
}
[HttpPost]
[Route("user-management")]
[ApiCosts(0)]
public async Task<IActionResult> Create([FromBody] CreateUserDto request)
public async Task<IActionResult> PostUser([FromBody] CreateUserDto request)
{
var id = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password);
var user = await userManager.CreateAsync(userFactory, request.Email, request.DisplayName, request.Password);
var response = new EntityCreatedDto { Id = id };
var response = new UserCreatedDto { Id = user.Id, PictureUrl = user.PictureUrl() };
return Ok(response);
}
@ -69,7 +86,7 @@ namespace Squidex.Controllers.Api.Users
[HttpPut]
[Route("user-management/{id}")]
[ApiCosts(0)]
public async Task<IActionResult> Update(string id, [FromBody] UpdateUserDto request)
public async Task<IActionResult> PutUser(string id, [FromBody] UpdateUserDto request)
{
await userManager.UpdateAsync(id, request.Email, request.DisplayName, request.Password);
@ -79,7 +96,7 @@ namespace Squidex.Controllers.Api.Users
[HttpPut]
[Route("user-management/{id}/lock/")]
[ApiCosts(0)]
public async Task<IActionResult> Lock(string id)
public async Task<IActionResult> LockUser(string id)
{
if (IsSelf(id))
{
@ -94,7 +111,7 @@ namespace Squidex.Controllers.Api.Users
[HttpPut]
[Route("user-management/{id}/unlock/")]
[ApiCosts(0)]
public async Task<IActionResult> Unlock(string id)
public async Task<IActionResult> UnlockUser(string id)
{
if (IsSelf(id))
{

1
src/Squidex/app/features/administration/declarations.ts

@ -6,6 +6,7 @@
*/
export * from './pages/event-consumers/event-consumers-page.component';
export * from './pages/users/user-page.component';
export * from './pages/users/users-page.component';
export * from './administration-area.component';

18
src/Squidex/app/features/administration/module.ts

@ -9,6 +9,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import {
ResolveUserGuard,
SqxFrameworkModule,
SqxSharedModule
} from 'shared';
@ -16,6 +17,7 @@ import {
import {
AdministrationAreaComponent,
EventConsumersPageComponent,
UserPageComponent,
UsersPageComponent
} from './declarations';
@ -33,7 +35,20 @@ const routes: Routes = [
},
{
path: 'users',
component: UsersPageComponent
component: UsersPageComponent,
children: [
{
path: 'new',
component: UserPageComponent
},
{
path: ':userId',
component: UserPageComponent,
resolve: {
user: ResolveUserGuard
}
}
]
}
]
}
@ -50,6 +65,7 @@ const routes: Routes = [
declarations: [
AdministrationAreaComponent,
EventConsumersPageComponent,
UserPageComponent,
UsersPageComponent
]
})

25
src/Squidex/app/features/administration/pages/users/messages.ts

@ -0,0 +1,25 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
export class UserCreated {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly pictureUrl: string
) {
}
}
export class UserUpdated {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string
) {
}
}

65
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -0,0 +1,65 @@
<sqx-title message="User Management"></sqx-title>
<form [formGroup]="userForm" (ngSubmit)="save()">
<sqx-panel panelWidth="26rem">
<div class="panel-header">
<div class="panel-title-row">
<div class="float-right">
<button type="submit" class="btn btn-primary" title="CTRL + S">
Save
</button>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPusaveblish()"></sqx-shortcut>
<h3 class="panel-title" *ngIf="isNewMode">
New User
</h3>
<h3 class="panel-title" *ngIf="!isNewMode">
Edit User
</h3>
</div>
<sqx-shortcut keys="ctrl+s" (trigger)="saveAndPublish()"></sqx-shortcut>
<a class="panel-close" sqxParentLink>
<i class="icon-close"></i>
</a>
</div>
<div class="panel-main">
<div class="panel-content panel-content-blank">
<div class="form-group">
<label for="email">Email</label>
<sqx-control-errors for="email" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="email" class="form-control" id="email" maxlength="100" formControlName="email" />
</div>
<div class="form-group">
<label for="displayName">Display Name</label>
<sqx-control-errors for="displayName" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" />
</div>
<div class="form-group form-group-password">
<label for="password">Password</label>
<sqx-control-errors for="password" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="password" class="form-control" id="password" maxlength="100" formControlName="password" />
</div>
<div class="form-group">
<label for="passwordConfirm">Confirm Password</label>
<sqx-control-errors for="passwordConfirm" [submitted]="userFormSubmitted"></sqx-control-errors>
<input type="password" class="form-control" id="passwordConfirm" maxlength="100" formControlName="passwordConfirm" />
</div>
</div>
</div>
</sqx-panel>
</form>

6
src/Squidex/app/features/administration/pages/users/user-page.component.scss

@ -0,0 +1,6 @@
@import '_vars';
@import '_mixins';
.form-group-password {
margin-top: 2rem;
}

157
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -0,0 +1,157 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import {
ComponentBase,
MessageBus,
NotificationService,
UserDto,
UserManagementService
} from 'shared';
import { UserCreated, UserUpdated } from './messages';
@Component({
selector: 'sqx-user-page',
styleUrls: ['./user-page.component.scss'],
templateUrl: './user-page.component.html'
})
export class UserPageComponent extends ComponentBase implements OnInit {
public userFormSubmitted = false;
public userForm: FormGroup;
public userId: string;
public userFormError: string;
public isNewMode = false;
constructor(notifications: NotificationService,
private readonly formBuilder: FormBuilder,
private readonly messageBus: MessageBus,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly userManagementService: UserManagementService
) {
super(notifications);
}
public ngOnInit() {
this.route.data.map(p => p['user'])
.subscribe((user: UserDto) => {
this.populateForm(user);
});
}
public save(publish: boolean) {
this.userFormSubmitted = true;
if (this.userForm.valid) {
this.userForm.disable();
const requestDto = this.userForm.value;
const enable = (message: string) => {
this.userForm.enable();
this.userForm.controls['password'].reset();
this.userForm.controls['passwordConfirm'].reset();
this.userFormSubmitted = false;
this.userFormError = message;
};
const back = () => {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
};
if (this.isNewMode) {
this.userManagementService.postUser(requestDto)
.subscribe(created => {
this.messageBus.publish(
new UserCreated(
created.id,
requestDto.email,
requestDto.displayName,
created.pictureUrl));
this.notifyInfo('User created successfully.');
back();
}, error => {
this.notifyError(error);
enable(error.displayMessage);
});
} else {
this.userManagementService.putUser(this.userId, requestDto)
.subscribe(() => {
this.messageBus.publish(
new UserUpdated(
this.userId,
requestDto.email,
requestDto.displayName));
this.notifyInfo('User saved successfully.');
enable(null);
}, error => {
this.notifyError(error);
enable(error.displayMessage);
});
}
} else {
this.notifyError('Content element not valid, please check the field with the red bar on the left in all languages (if localizable).');
}
}
private populateForm(user: UserDto) {
this.userFormError = '';
this.userFormSubmitted = false;
if (user) {
this.isNewMode = false;
this.userId = user.id;
this.userForm =
this.formBuilder.group({
email: [user.email,
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]],
displayName: [user.displayName,
[
Validators.required,
Validators.maxLength(100)
]],
password: ['', []],
passwordConfirm: ['', []]
});
} else {
this.isNewMode = true;
this.userForm =
this.formBuilder.group({
displayName: ['',
[
Validators.required,
Validators.maxLength(100)
]],
email: ['',
[
Validators.email,
Validators.required,
Validators.maxLength(100)
]],
password: ['', [
Validators.required
]],
passwordConfirm: ['', [
Validators.required
]]
});
}
}
}

10
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -13,6 +13,10 @@
<form class="form-inline" (ngSubmit)="search()">
<input class="form-control" [formControl]="usersFilter" placeholder="Search for user" />
</form>
<button class="btn btn-success" [routerLink]="['new']">
<i class="icon-plus"></i> New
</button>
</div>
<h3 class="panel-title">Users</h3>
@ -52,7 +56,7 @@
<tbody>
<ng-template ngFor let-user [ngForOf]="usersItems">
<tr>
<tr [routerLink]="[user.id]" routerLinkActive="active">
<td>
<img class="user-picture" [attr.title]="user.name" [attr.src]="user.pictureUrl" />
</td>
@ -92,4 +96,6 @@
</div>
</div>
</div>
</sqx-panel>
</sqx-panel>
<router-outlet></router-outlet>

37
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -5,25 +5,32 @@
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Component, OnInit } from '@angular/core';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import {
AuthService,
ComponentBase,
ImmutableArray,
MessageBus,
NotificationService,
Pager,
UserDto,
UserManagementService
} from 'shared';
import { UserCreated, UserUpdated } from './messages';
@Component({
selector: 'sqx-users-page',
styleUrls: ['./users-page.component.scss'],
templateUrl: './users-page.component.html'
})
export class UsersPageComponent extends ComponentBase implements OnInit {
export class UsersPageComponent extends ComponentBase implements OnDestroy, OnInit {
private userCreatedSubscription: Subscription;
private userUpdatedSubscription: Subscription;
public currentUserId: string;
public usersItems = ImmutableArray.empty<UserDto>();
@ -33,12 +40,36 @@ export class UsersPageComponent extends ComponentBase implements OnInit {
constructor(notifications: NotificationService,
private readonly userManagementService: UserManagementService,
private readonly authService: AuthService
private readonly authService: AuthService,
private readonly messageBus: MessageBus
) {
super(notifications);
}
public ngOnDestroy() {
this.userCreatedSubscription.unsubscribe();
this.userUpdatedSubscription.unsubscribe();
}
public ngOnInit() {
this.userCreatedSubscription =
this.messageBus.of(UserCreated)
.subscribe(message => {
const user = new UserDto(message.id, message.email, message.displayName, message.pictureUrl, false);
this.usersItems = this.usersItems.pushFront(user);
this.usersPager = this.usersPager.incrementCount();
});
this.userUpdatedSubscription =
this.messageBus.of(UserUpdated)
.subscribe(message => {
this.usersItems =
this.usersItems.replaceAll(
u => u.id === message.id,
u => new UserDto(u.id, message.email, message.displayName, u.pictureUrl, u.isLocked));
});
this.currentUserId = this.authService.user!.id;
this.load();

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

@ -139,27 +139,29 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
if (this.contentForm.valid) {
this.disable();
const data = this.contentForm.value;
const requestDto = this.contentForm.value;
const back = () => {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
};
if (this.isNewMode) {
this.appNameOnce()
.switchMap(app => this.contentsService.postContent(app, this.schema.name, data, publish, this.version))
.switchMap(app => this.contentsService.postContent(app, this.schema.name, requestDto, publish, this.version))
.subscribe(created => {
this.contentId = created.id;
this.messageBus.publish(new ContentCreated(created.id, created.data, this.version.value, publish));
this.notifyInfo('Content created successfully.');
this.finish();
back();
}, error => {
this.notifyError(error);
this.enable();
});
} else {
this.appNameOnce()
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, data, this.version))
.switchMap(app => this.contentsService.putContent(app, this.schema.name, this.contentId!, requestDto, this.version))
.subscribe(() => {
this.messageBus.publish(new ContentUpdated(this.contentId!, data, this.version.value));
this.messageBus.publish(new ContentUpdated(this.contentId!, requestDto, this.version.value));
this.notifyInfo('Content saved successfully.');
this.enable();
@ -173,10 +175,6 @@ export class ContentPageComponent extends AppComponentBase implements CanCompone
}
}
private finish() {
this.router.navigate(['../'], { relativeTo: this.route, replaceUrl: true });
}
private enable() {
this.contentForm.markAsPristine();

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

@ -18,7 +18,7 @@
<sqx-language-selector class="languages-buttons" (selectedLanguageChanged)="selectLanguage($event)" [languages]="languages"></sqx-language-selector>
</span>
<button class="btn btn-success" (click)="gotoNew()">
<button class="btn btn-success" [routerLink]="['new']">
<i class="icon-plus"></i> New
</button>
</div>

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

@ -7,7 +7,7 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import {
@ -59,7 +59,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
private readonly authService: AuthService,
private readonly contentsService: ContentsService,
private readonly route: ActivatedRoute,
private readonly router: Router,
private readonly messageBus: MessageBus
) {
super(notifications, apps);
@ -188,10 +187,6 @@ export class ContentsPageComponent extends AppComponentBase implements OnDestroy
this.load();
}
public gotoNew() {
this.router.navigate(['./new'], { relativeTo: this.route });
}
private updateContents(id: string, p: boolean | undefined, data: any, version: string) {
this.contentItems = this.contentItems.replaceAll(x => x.id === id, c => this.updateContent(c, p === undefined ? c.isPublished : p, data, version));
}

8
src/Squidex/app/features/schemas/pages/schema/schema-edit-form.component.ts

@ -75,6 +75,11 @@ export class SchemaEditFormComponent implements OnInit {
if (this.editForm.valid) {
this.editForm.disable();
const enable = () => {
this.editForm.enable();
this.editFormSubmitted = false;
};
const requestDto = this.editForm.value;
this.schemas.putSchema(this.appName, this.name, requestDto, this.version)
@ -82,7 +87,7 @@ export class SchemaEditFormComponent implements OnInit {
this.reset();
this.saved.emit(new SchemaPropertiesDto(requestDto.label, requestDto.hints));
}, error => {
this.editForm.enable();
enable();
this.notifications.notify(Notification.error(error.displayMessage));
});
}
@ -91,5 +96,6 @@ export class SchemaEditFormComponent implements OnInit {
private reset() {
this.editFormSubmitted = false;
this.editForm.reset();
this.editForm.enable();
}
}

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

@ -195,10 +195,10 @@ export class SchemaPageComponent extends AppComponentBase implements OnInit {
}
public saveField(field: FieldDto, newField: FieldDto) {
const request = new UpdateFieldDto(newField.properties);
const requestDto = new UpdateFieldDto(newField.properties);
this.appNameOnce()
.switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, request, this.schemaVersion)).retry(2)
.switchMap(app => this.schemasService.putField(app, this.schemaName, field.fieldId, requestDto, this.schemaVersion)).retry(2)
.subscribe(() => {
this.updateField(field, new FieldDto(field.fieldId, field.name, newField.isHidden, field.isDisabled, field.partitioning, newField.properties));
}, error => {

12
src/Squidex/app/features/settings/pages/contributors/contributors-page.component.ts

@ -125,12 +125,12 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
}
public assignContributor() {
const newContributor = new AppContributorDto(this.addContributorForm.get('user')!.value.model.id, 'Editor');
const requestDto = new AppContributorDto(this.addContributorForm.get('user')!.value.model.id, 'Editor');
this.appNameOnce()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version))
.switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version))
.subscribe(() => {
this.updateContributors(this.appContributors.push(newContributor));
this.updateContributors(this.appContributors.push(requestDto));
}, error => {
this.notifyError(error);
});
@ -139,12 +139,12 @@ export class ContributorsPageComponent extends AppComponentBase implements OnIni
}
public changePermission(contributor: AppContributorDto, permission: string) {
const newContributor = changePermission(contributor, permission);
const requestDto = changePermission(contributor, permission);
this.appNameOnce()
.switchMap(app => this.appContributorsService.postContributor(app, newContributor, this.version))
.switchMap(app => this.appContributorsService.postContributor(app, requestDto, this.version))
.subscribe(() => {
this.updateContributors(this.appContributors.replace(contributor, newContributor));
this.updateContributors(this.appContributors.replace(contributor, requestDto));
}, error => {
this.notifyError(error);
});

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

@ -77,10 +77,10 @@ export class LanguagesPageComponent extends AppComponentBase implements OnInit {
}
public addLanguage() {
const request = new AddAppLanguageDto(this.addLanguageForm.get('language')!.value.iso2Code);
const requestDto = new AddAppLanguageDto(this.addLanguageForm.get('language')!.value.iso2Code);
this.appNameOnce()
.switchMap(app => this.appLanguagesService.postLanguages(app, request, this.version))
.switchMap(app => this.appLanguagesService.postLanguages(app, requestDto, this.version))
.subscribe(dto => {
this.updateLanguages(this.appLanguages.push(dto));
}, error => {

7
src/Squidex/app/shared/components/app-form.component.ts

@ -66,12 +66,17 @@ export class AppFormComponent {
const request = new CreateAppDto(this.createForm.get('name')!.value);
const enable = () => {
this.createForm.enable();
this.createFormSubmitted = false;
};
this.appsStore.createApp(request)
.subscribe(dto => {
this.reset();
this.created.emit(dto);
}, error => {
this.createForm.enable();
enable();
this.creationError = error.displayMessage;
});
}

7
src/Squidex/app/shared/components/asset.component.ts

@ -164,18 +164,17 @@ export class AssetComponent extends AppComponentBase implements OnInit {
if (this.renameForm.valid) {
this.renameForm.disable();
const dto = new UpdateAssetDto(this.renameForm.controls['name'].value);
const requestDto = new UpdateAssetDto(this.renameForm.controls['name'].value);
this.appNameOnce()
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, dto, this.version))
.switchMap(app => this.assetsService.putAsset(app, this.asset.id, requestDto, this.version))
.subscribe(_ => {
const me = `subject:${this.authService.user!.id}`;
const asset = new AssetDto(
this.asset.id,
this.asset.createdBy, me,
this.asset.created, DateTime.now(),
dto.fileName,
this.asset.created, DateTime.now(), requestDto.fileName,
this.asset.fileSize,
this.asset.fileVersion,
this.asset.mimeType,

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

@ -12,6 +12,7 @@ export * from './guards/resolve-app-languages.guard';
export * from './guards/resolve-content.guard';
export * from './guards/resolve-published-schema.guard';
export * from './guards/resolve-schema.guard';
export * from './guards/resolve-user.guard';
export * from './services/app-contributors.service';
export * from './services/app-clients.service';

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

@ -0,0 +1,85 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { UserManagementService } from 'shared';
import { ResolveUserGuard } from './resolve-user.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveUserGuard', () => {
const route = {
parent: {
params: {
userId: 'my-user'
}
}
};
let usersService: IMock<UserManagementService>;
beforeEach(() => {
usersService = Mock.ofType(UserManagementService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveUserGuard(usersService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain app and user name.');
});
it('should navigate to 404 page if user is not found', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should navigate to 404 page if user loading fails', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should return user if loading succeeded', (done) => {
const user = {};
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(user));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.then(result => {
expect(result).toBe(user);
done();
});
});
});

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

@ -0,0 +1,62 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Sebastian Stehle. All rights reserved
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { UserDto, UserManagementService } from './../services/users.service';
@Injectable()
export class ResolveUserGuard implements Resolve<UserDto> {
constructor(
private readonly userManagementService: UserManagementService,
private readonly router: Router
) {
}
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<UserDto> {
const userId = this.findParameter(route, 'userId');
if (!userId) {
throw 'Route must contain user id.';
}
const result =
this.userManagementService.getUser(userId).toPromise()
.then(dto => {
if (!dto) {
this.router.navigate(['/404']);
return null;
}
return dto;
}).catch(() => {
this.router.navigate(['/404']);
return null;
});
return result;
}
private findParameter(route: ActivatedRouteSnapshot, name: string): string | null {
let result: string | null = null;
while (route) {
result = route.params[name];
if (result || !route.parent) {
break;
}
route = route.parent;
}
return result;
}
}

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

@ -7,7 +7,6 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { DndModule } from 'ng2-dnd';
import { ProgressHttpModule } from 'angular-progress-http';
import { SqxFrameworkModule } from 'framework';
@ -39,6 +38,7 @@ import {
ResolvePublishedSchemaGuard,
ResolveSchemaGuard,
SchemasService,
ResolveUserGuard,
UsagesService,
UserEmailPipe,
UserEmailRefPipe,
@ -110,6 +110,7 @@ export class SqxSharedModule {
ResolveContentGuard,
ResolvePublishedSchemaGuard,
ResolveSchemaGuard,
ResolveUserGuard,
SchemasService,
UsagesService,
UserManagementService,

28
src/Squidex/app/shared/services/users.service.spec.ts

@ -235,6 +235,34 @@ describe('UserManagementService', () => {
authService.verifyAll();
});
it('should make get request to get single user', () => {
authService.setup(x => x.authGet('http://service/p/api/users/123'))
.returns(() => Observable.of(
new Response(
new ResponseOptions({
body: {
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
}
})
)
))
.verifiable(Times.once());
let user: UserDto | null = null;
userManagementService.getUser('123').subscribe(result => {
user = result;
}).unsubscribe();
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
authService.verifyAll();
});
it('should make put request to lock user', () => {
authService.setup(x => x.authPut('http://service/p/api/user-management/123/lock', It.isAny()))
.returns(() => Observable.of(

58
src/Squidex/app/shared/services/users.service.ts

@ -21,6 +21,32 @@ export class UsersDto {
}
}
export class UserCreatedDto {
constructor(
public readonly id: string,
public readonly pictureUrl: string
) {
}
}
export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UserDto {
constructor(
public readonly id: string,
@ -107,6 +133,38 @@ export class UserManagementService {
.catchError('Failed to load users. Please reload.');
}
public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return this.authService.authGet(url)
.map(response => response.json())
.map(response => {
return new UserDto(
response.id,
response.email,
response.displayName,
response.pictureUrl,
response.isLocked);
})
.catchError('Failed to load user. Please reload.');
}
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/`);
return this.authService.authPost(url, dto)
.map(response => response.json())
.map(response => new UserCreatedDto(response.id, response.pictureUrl))
.catchError('Failed to create user. Please reload.');
}
public putUser(id: string, dto: UpdateUserDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return this.authService.authPut(url, dto)
.catchError('Failed to update user. Please reload.');
}
public lockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`);

15
src/Squidex/app/theme/_lists.scss

@ -60,9 +60,18 @@
}
&.active {
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-dark-foreground;
& {
background: $color-theme-blue;
border-color: $color-theme-blue;
color: $color-dark-foreground;
}
.btn-link {
&,
&:hover {
color: $color-dark-foreground;
}
}
}
}
}

8
tests/Squidex.Write.Tests/Apps/AppCommandHandlerTests.cs

@ -87,7 +87,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(contributorId)).Returns(Task.FromResult<IUser>(null));
userResolver.Setup(x => x.FindByIdAsync(contributorId)).Returns(Task.FromResult<IUser>(null));
await TestUpdate(app, async _ =>
{
@ -106,7 +106,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(It.IsAny<string>())).Returns(Task.FromResult(new Mock<IUser>().Object));
userResolver.Setup(x => x.FindByIdAsync(It.IsAny<string>())).Returns(Task.FromResult(new Mock<IUser>().Object));
await TestUpdate(app, async _ =>
{
@ -121,7 +121,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(contributorId)).Returns(Task.FromResult<IUser>(null));
userResolver.Setup(x => x.FindByIdAsync(contributorId)).Returns(Task.FromResult<IUser>(null));
await TestUpdate(app, async _ =>
{
@ -138,7 +138,7 @@ namespace Squidex.Write.Apps
var context = CreateContextForCommand(new AssignContributor { ContributorId = contributorId });
userResolver.Setup(x => x.FindById(contributorId)).Returns(Task.FromResult(new Mock<IUser>().Object));
userResolver.Setup(x => x.FindByIdAsync(contributorId)).Returns(Task.FromResult(new Mock<IUser>().Object));
await TestUpdate(app, async _ =>
{

Loading…
Cancel
Save