mirror of https://github.com/Squidex/squidex.git
32 changed files with 657 additions and 58 deletions
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
) { |
||||
|
} |
||||
|
} |
||||
@ -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> |
||||
@ -0,0 +1,6 @@ |
|||||
|
@import '_vars'; |
||||
|
@import '_mixins'; |
||||
|
|
||||
|
.form-group-password { |
||||
|
margin-top: 2rem; |
||||
|
} |
||||
@ -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 |
||||
|
]] |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
@ -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(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue