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