Add user update for admins

pull/87/head
Chocobozzz 2017-09-05 21:29:39 +02:00
parent 980246ea8f
commit 8094a89802
21 changed files with 290 additions and 51 deletions

View File

@ -4,7 +4,7 @@ import { AdminComponent } from './admin.component'
import { AdminRoutingModule } from './admin-routing.module'
import { FriendsComponent, FriendAddComponent, FriendListComponent, FriendService } from './friends'
import { RequestSchedulersComponent, RequestSchedulersStatsComponent, RequestSchedulersService } from './request-schedulers'
import { UsersComponent, UserAddComponent, UserListComponent, UserService } from './users'
import { UsersComponent, UserAddComponent, UserUpdateComponent, UserListComponent, UserService } from './users'
import { VideoAbusesComponent, VideoAbuseListComponent } from './video-abuses'
import { SharedModule } from '../shared'
@ -26,6 +26,7 @@ import { SharedModule } from '../shared'
UsersComponent,
UserAddComponent,
UserUpdateComponent,
UserListComponent,
VideoAbusesComponent,

View File

@ -1,5 +1,5 @@
export * from './shared'
export * from './user-add'
export * from './user-edit'
export * from './user-list'
export * from './users.component'
export * from './users.routes'

View File

@ -5,7 +5,7 @@ import 'rxjs/add/operator/map'
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared'
import { UserCreate } from '../../../../../../shared'
import { UserCreate, UserUpdate } from '../../../../../../shared'
@Injectable()
export class UserService {
@ -23,6 +23,18 @@ export class UserService {
.catch(this.restExtractor.handleError)
}
updateUser (userId: number, userUpdate: UserUpdate) {
return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
.map(this.restExtractor.extractDataBool)
.catch(this.restExtractor.handleError)
}
getUser (userId: number) {
return this.authHttp.get(UserService.BASE_USERS_URL + userId)
.map(this.restExtractor.extractDataGet)
.catch(this.restExtractor.handleError)
}
getDataSource () {
return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL, this.formatDataSource.bind(this))
}

View File

@ -1 +0,0 @@
export * from './user-add.component'

View File

@ -0,0 +1,2 @@
export * from './user-add.component'
export * from './user-update.component'

View File

@ -6,20 +6,20 @@ import { NotificationsService } from 'angular2-notifications'
import { UserService } from '../shared'
import {
FormReactive,
USER_USERNAME,
USER_EMAIL,
USER_PASSWORD,
USER_VIDEO_QUOTA
} from '../../../shared'
import { UserCreate } from '../../../../../../shared'
import { UserEdit } from './user-edit'
@Component({
selector: 'my-user-add',
templateUrl: './user-add.component.html'
templateUrl: './user-edit.component.html'
})
export class UserAddComponent extends FormReactive implements OnInit {
error: string = null
export class UserAddComponent extends UserEdit implements OnInit {
error: string
form: FormGroup
formErrors = {
@ -59,8 +59,8 @@ export class UserAddComponent extends FormReactive implements OnInit {
this.buildForm()
}
addUser () {
this.error = null
formValidated () {
this.error = undefined
const userCreate: UserCreate = this.form.value
@ -76,4 +76,12 @@ export class UserAddComponent extends FormReactive implements OnInit {
err => this.error = err.text
)
}
isCreation () {
return true
}
getFormButtonTitle () {
return 'Add user'
}
}

View File

@ -1,12 +1,13 @@
<div class="row">
<div class="content-padding">
<h3>Add user</h3>
<h3 *ngIf="isCreation() === true">Add user</h3>
<h3 *ngIf="isCreation() === false">Edit user {{ username }}</h3>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<form role="form" (ngSubmit)="addUser()" [formGroup]="form">
<div class="form-group">
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-group" *ngIf="isCreation()">
<label for="username">Username</label>
<input
type="text" class="form-control" id="username" placeholder="john"
@ -28,7 +29,7 @@
</div>
</div>
<div class="form-group">
<div class="form-group" *ngIf="isCreation()">
<label for="password">Password</label>
<input
type="password" class="form-control" id="password"
@ -42,17 +43,13 @@
<div class="form-group">
<label for="videoQuota">Video quota</label>
<select class="form-control" id="videoQuota" formControlName="videoQuota">
<option value="-1">Unlimited</option>
<option value="100000000">100MB</option>
<option value="500000000">500MB</option>
<option value="1000000000">1GB</option>
<option value="5000000000">5GB</option>
<option value="20000000000">20GB</option>
<option value="50000000000">50GB</option>
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
</div>
<input type="submit" value="Add user" class="btn btn-default" [disabled]="!form.valid">
<input type="submit" value="{{ getFormButtonTitle() }}" class="btn btn-default" [disabled]="!form.valid">
</form>
</div>
</div>

View File

@ -0,0 +1,16 @@
import { FormReactive } from '../../../shared'
export abstract class UserEdit extends FormReactive {
videoQuotaOptions = [
{ value: -1, label: 'Unlimited' },
{ value: 100 * 1024 * 1024, label: '100MB' },
{ value: 5 * 1024 * 1024, label: '500MB' },
{ value: 1024 * 1024 * 1024, label: '1GB' },
{ value: 5 * 1024 * 1024 * 1024, label: '5GB' },
{ value: 20 * 1024 * 1024 * 1024, label: '20GB' },
{ value: 50 * 1024 * 1024 * 1024, label: '50GB' }
]
abstract isCreation (): boolean
abstract getFormButtonTitle (): string
}

View File

@ -0,0 +1,106 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { FormBuilder, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { Subscription } from 'rxjs/Subscription'
import { NotificationsService } from 'angular2-notifications'
import { UserService } from '../shared'
import { USER_EMAIL, USER_VIDEO_QUOTA } from '../../../shared'
import { UserUpdate } from '../../../../../../shared/models/users/user-update.model'
import { User } from '../../../shared/users/user.model'
import { UserEdit } from './user-edit'
@Component({
selector: 'my-user-update',
templateUrl: './user-edit.component.html'
})
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
error: string
userId: number
username: string
form: FormGroup
formErrors = {
'email': '',
'videoQuota': ''
}
validationMessages = {
'email': USER_EMAIL.MESSAGES,
'videoQuota': USER_VIDEO_QUOTA.MESSAGES
}
private paramsSub: Subscription
constructor (
private formBuilder: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private notificationsService: NotificationsService,
private userService: UserService
) {
super()
}
buildForm () {
this.form = this.formBuilder.group({
email: [ '', USER_EMAIL.VALIDATORS ],
videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ]
})
this.form.valueChanges.subscribe(data => this.onValueChanged(data))
}
ngOnInit () {
this.buildForm()
this.paramsSub = this.route.params.subscribe(routeParams => {
const userId = routeParams['id']
this.userService.getUser(userId).subscribe(
user => this.onUserFetched(user),
err => this.error = err.text
)
})
}
ngOnDestroy () {
this.paramsSub.unsubscribe()
}
formValidated () {
this.error = undefined
const userUpdate: UserUpdate = this.form.value
// A select in HTML is always mapped as a string, we convert it to number
userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10)
this.userService.updateUser(this.userId, userUpdate).subscribe(
() => {
this.notificationsService.success('Success', `User ${this.username} updated.`)
this.router.navigate([ '/admin/users/list' ])
},
err => this.error = err.text
)
}
isCreation () {
return false
}
getFormButtonTitle () {
return 'Update user'
}
private onUserFetched (userJson: User) {
this.userId = userJson.id
this.username = userJson.username
this.form.patchValue({
email: userJson.email,
videoQuota: userJson.videoQuota
})
}
}

View File

@ -5,7 +5,7 @@
<ng2-smart-table
[settings]="tableSettings" [source]="usersSource"
(delete)="removeUser($event)"
(delete)="removeUser($event)" (edit)="editUser($event)"
></ng2-smart-table>
<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">

View File

@ -5,6 +5,7 @@ import { NotificationsService } from 'angular2-notifications'
import { ConfirmService } from '../../../core'
import { RestDataSource, User, Utils } from '../../../shared'
import { UserService } from '../shared'
import { Router } from '@angular/router'
@Component({
selector: 'my-user-list',
@ -22,15 +23,18 @@ export class UserListComponent {
actions: {
position: 'right',
add: false,
edit: false,
edit: true,
delete: true
},
delete: {
deleteButtonContent: Utils.getRowDeleteButton()
},
edit: {
editButtonContent: Utils.getRowEditButton()
},
pager: {
display: true,
perPage: 1
perPage: 10
},
columns: {
id: {
@ -58,6 +62,7 @@ export class UserListComponent {
}
constructor (
private router: Router,
private notificationsService: NotificationsService,
private confirmService: ConfirmService,
private userService: UserService
@ -65,8 +70,12 @@ export class UserListComponent {
this.usersSource = this.userService.getDataSource()
}
removeUser ({ data }) {
const user: User = data
editUser ({ data }: { data: User }) {
this.router.navigate([ '/admin', 'users', data.id, 'update' ])
}
removeUser ({ data }: { data: User }) {
const user = data
if (user.username === 'root') {
this.notificationsService.error('Error', 'You cannot delete root.')

View File

@ -1,7 +1,7 @@
import { Routes } from '@angular/router'
import { UsersComponent } from './users.component'
import { UserAddComponent } from './user-add'
import { UserAddComponent, UserUpdateComponent } from './user-edit'
import { UserListComponent } from './user-list'
export const UsersRoutes: Routes = [
@ -31,6 +31,15 @@ export const UsersRoutes: Routes = [
title: 'Add a user'
}
}
},
{
path: ':id/update',
component: UserUpdateComponent,
data: {
meta: {
title: 'Update a user'
}
}
}
]
}

View File

@ -26,7 +26,6 @@ export class AccountChangePasswordComponent extends FormReactive implements OnIn
constructor (
private formBuilder: FormBuilder,
private router: Router,
private notificationsService: NotificationsService,
private userService: UserService
) {

View File

@ -11,7 +11,7 @@ import {
UserService,
USER_PASSWORD
} from '../../shared'
import { UserUpdate } from '../../../../../shared'
import { UserUpdateMe } from '../../../../../shared'
@Component({
selector: 'my-account-details',
@ -30,7 +30,6 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
constructor (
private authService: AuthService,
private formBuilder: FormBuilder,
private router: Router,
private notificationsService: NotificationsService,
private userService: UserService
) {
@ -51,14 +50,14 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
updateDetails () {
const displayNSFW = this.form.value['displayNSFW']
const details: UserUpdate = {
const details: UserUpdateMe = {
displayNSFW
}
this.error = null
this.userService.updateDetails(details).subscribe(
this.userService.updateMyDetails(details).subscribe(
() => {
this.notificationsService.success('Success', 'Informations updated.')
this.notificationsService.success('Success', 'Information updated.')
this.authService.refreshUserInformations()
},

View File

@ -6,7 +6,7 @@ import 'rxjs/add/operator/map'
import { AuthService } from '../../core'
import { AuthHttp } from '../auth'
import { RestExtractor } from '../rest'
import { UserCreate, UserUpdate } from '../../../../../shared'
import { UserCreate, UserUpdateMe } from '../../../../../shared'
@Injectable()
export class UserService {
@ -22,13 +22,13 @@ export class UserService {
checkTokenValidity () {
const url = UserService.BASE_USERS_URL + 'me'
// AuthHttp will redirect us to the login page if the oken is not valid anymore
// AuthHttp will redirect us to the login page if the token is not valid anymore
this.authHttp.get(url).subscribe()
}
changePassword (newPassword: string) {
const url = UserService.BASE_USERS_URL + this.authService.getUser().id
const body: UserUpdate = {
const url = UserService.BASE_USERS_URL + 'me'
const body: UserUpdateMe = {
password: newPassword
}
@ -37,8 +37,8 @@ export class UserService {
.catch((res) => this.restExtractor.handleError(res))
}
updateDetails (details: UserUpdate) {
const url = UserService.BASE_USERS_URL + this.authService.getUser().id
updateMyDetails (details: UserUpdateMe) {
const url = UserService.BASE_USERS_URL + 'me'
return this.authHttp.put(url, details)
.map(this.restExtractor.extractDataBool)

View File

@ -9,4 +9,8 @@ export class Utils {
static getRowDeleteButton () {
return '<span class="glyphicon glyphicon-remove glyphicon-black"></span>'
}
static getRowEditButton () {
return '<span class="glyphicon glyphicon-pencil glyphicon-black"></span>'
}
}

View File

@ -9,15 +9,22 @@ import {
ensureUserRegistrationAllowed,
usersAddValidator,
usersUpdateValidator,
usersUpdateMeValidator,
usersRemoveValidator,
usersVideoRatingValidator,
usersGetValidator,
paginationValidator,
setPagination,
usersSortValidator,
setUsersSort,
token
} from '../../middlewares'
import { UserVideoRate as FormattedUserVideoRate, UserCreate, UserUpdate } from '../../../shared'
import {
UserVideoRate as FormattedUserVideoRate,
UserCreate,
UserUpdate,
UserUpdateMe
} from '../../../shared'
const usersRouter = express.Router()
@ -40,6 +47,11 @@ usersRouter.get('/',
listUsers
)
usersRouter.get('/:id',
usersGetValidator,
getUser
)
usersRouter.post('/',
authenticate,
ensureIsAdmin,
@ -53,8 +65,15 @@ usersRouter.post('/register',
createUser
)
usersRouter.put('/me',
authenticate,
usersUpdateMeValidator,
updateMe
)
usersRouter.put('/:id',
authenticate,
ensureIsAdmin,
usersUpdateValidator,
updateUser
)
@ -105,6 +124,10 @@ function getUserInformation (req: express.Request, res: express.Response, next:
.catch(err => next(err))
}
function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
return res.json(res.locals.user.toFormattedJSON())
}
function getUserVideoRating (req: express.Request, res: express.Response, next: express.NextFunction) {
const videoId = +req.params.videoId
const userId = +res.locals.oauth.token.User.id
@ -139,14 +162,15 @@ function removeUser (req: express.Request, res: express.Response, next: express.
})
}
function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) {
const body: UserUpdate = req.body
function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) {
const body: UserUpdateMe = req.body
// FIXME: user is not already a Sequelize instance?
db.User.loadByUsername(res.locals.oauth.token.user.username)
.then(user => {
if (body.password) user.password = body.password
if (body.password !== undefined) user.password = body.password
if (body.email !== undefined) user.email = body.email
if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW
if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
return user.save()
})
@ -154,6 +178,18 @@ function updateUser (req: express.Request, res: express.Response, next: express.
.catch(err => next(err))
}
function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) {
const body: UserUpdate = req.body
const user = res.locals.user
if (body.email !== undefined) user.email = body.email
if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
return user.save()
.then(() => res.sendStatus(204))
.catch(err => next(err))
}
function success (req: express.Request, res: express.Response, next: express.NextFunction) {
res.end()
}

View File

@ -53,16 +53,35 @@ function usersRemoveValidator (req: express.Request, res: express.Response, next
function usersUpdateValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
// Add old password verification
req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
req.checkBody('email', 'Should have a valid email attribute').optional().isEmail()
req.checkBody('videoQuota', 'Should have a valid user quota').optional().isUserVideoQuotaValid()
logger.debug('Checking usersUpdate parameters', { parameters: req.body })
checkErrors(req, res, () => {
checkUserExists(req.params.id, res, next)
})
}
function usersUpdateMeValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
// Add old password verification
req.checkBody('password', 'Should have a valid password').optional().isUserPasswordValid()
req.checkBody('email', 'Should have a valid email attribute').optional().isEmail()
req.checkBody('displayNSFW', 'Should have a valid display Not Safe For Work attribute').optional().isUserDisplayNSFWValid()
logger.debug('Checking usersUpdate parameters', { parameters: req.body })
checkErrors(req, res, next)
}
function usersGetValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
req.checkParams('id', 'Should have a valid id').notEmpty().isInt()
checkErrors(req, res, () => {
checkUserExists(req.params.id, res, next)
})
}
function usersVideoRatingValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
req.checkParams('videoId', 'Should have a valid video id').notEmpty().isVideoIdOrUUIDValid()
@ -106,6 +125,24 @@ export {
usersAddValidator,
usersRemoveValidator,
usersUpdateValidator,
usersUpdateMeValidator,
usersVideoRatingValidator,
ensureUserRegistrationAllowed
ensureUserRegistrationAllowed,
usersGetValidator
}
// ---------------------------------------------------------------------------
function checkUserExists (id: number, res: express.Response, callback: () => void) {
db.User.loadById(id)
.then(user => {
if (!user) return res.status(404).send('User not found')
res.locals.user = user
callback()
})
.catch(err => {
logger.error('Error in user request validator.', err)
return res.sendStatus(500)
})
}

View File

@ -1,4 +1,5 @@
export * from './user.model'
export * from './user-create.model'
export * from './user-update.model'
export * from './user-update-me.model'
export * from './user-role.type'

View File

@ -0,0 +1,5 @@
export interface UserUpdateMe {
displayNSFW?: boolean
email?: string
password?: string
}

View File

@ -1,5 +1,4 @@
export interface UserUpdate {
displayNSFW?: boolean
password?: string
email?: string
videoQuota?: number
}