diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index e4ba8e5b7..853085a83 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -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, diff --git a/client/src/app/+admin/users/index.ts b/client/src/app/+admin/users/index.ts index cef2c282c..efcd0d9cb 100644 --- a/client/src/app/+admin/users/index.ts +++ b/client/src/app/+admin/users/index.ts @@ -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' diff --git a/client/src/app/+admin/users/shared/user.service.ts b/client/src/app/+admin/users/shared/user.service.ts index ffd7ba7da..999013bcc 100644 --- a/client/src/app/+admin/users/shared/user.service.ts +++ b/client/src/app/+admin/users/shared/user.service.ts @@ -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)) } diff --git a/client/src/app/+admin/users/user-add/index.ts b/client/src/app/+admin/users/user-add/index.ts deleted file mode 100644 index 3a4654101..000000000 --- a/client/src/app/+admin/users/user-add/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user-add.component' diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts new file mode 100644 index 000000000..edec02fbb --- /dev/null +++ b/client/src/app/+admin/users/user-edit/index.ts @@ -0,0 +1,2 @@ +export * from './user-add.component' +export * from './user-update.component' diff --git a/client/src/app/+admin/users/user-add/user-add.component.ts b/client/src/app/+admin/users/user-edit/user-add.component.ts similarity index 85% rename from client/src/app/+admin/users/user-add/user-add.component.ts rename to client/src/app/+admin/users/user-edit/user-add.component.ts index 91377a933..40f649cff 100644 --- a/client/src/app/+admin/users/user-add/user-add.component.ts +++ b/client/src/app/+admin/users/user-edit/user-add.component.ts @@ -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' + } } diff --git a/client/src/app/+admin/users/user-add/user-add.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html similarity index 68% rename from client/src/app/+admin/users/user-add/user-add.component.html rename to client/src/app/+admin/users/user-edit/user-edit.component.html index f84d72c7c..0e23cb731 100644 --- a/client/src/app/+admin/users/user-add/user-add.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html @@ -1,12 +1,13 @@
-

Add user

+

Add user

+

Edit user {{ username }}

{{ error }}
-
-
+ +
-
+
- +
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts new file mode 100644 index 000000000..61db8a906 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-edit.ts @@ -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 +} diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts new file mode 100644 index 000000000..dbac5f974 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts @@ -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 + }) + } +} diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index bb4c99a3f..eb5bc9d4a 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html @@ -5,7 +5,7 @@ diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index dbb85cedd..7187a2008 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts @@ -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.') diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts index 723c5715d..a6a9c4c19 100644 --- a/client/src/app/+admin/users/users.routes.ts +++ b/client/src/app/+admin/users/users.routes.ts @@ -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' + } + } } ] } diff --git a/client/src/app/account/account-change-password/account-change-password.component.ts b/client/src/app/account/account-change-password/account-change-password.component.ts index ce786cfa3..dba141296 100644 --- a/client/src/app/account/account-change-password/account-change-password.component.ts +++ b/client/src/app/account/account-change-password/account-change-password.component.ts @@ -26,7 +26,6 @@ export class AccountChangePasswordComponent extends FormReactive implements OnIn constructor ( private formBuilder: FormBuilder, - private router: Router, private notificationsService: NotificationsService, private userService: UserService ) { diff --git a/client/src/app/account/account-details/account-details.component.ts b/client/src/app/account/account-details/account-details.component.ts index 547f045c4..8cbed5009 100644 --- a/client/src/app/account/account-details/account-details.component.ts +++ b/client/src/app/account/account-details/account-details.component.ts @@ -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() }, diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index b479ac034..35180be4d 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts @@ -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) diff --git a/client/src/app/shared/utils.ts b/client/src/app/shared/utils.ts index 832311f89..c3189a570 100644 --- a/client/src/app/shared/utils.ts +++ b/client/src/app/shared/utils.ts @@ -9,4 +9,8 @@ export class Utils { static getRowDeleteButton () { return '' } + + static getRowEditButton () { + return '' + } } diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 1b5b7f903..6922661ae 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -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() } diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index eeb0e3557..ebb343535 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -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) + }) } diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts index 414aaab08..45dbc7b8f 100644 --- a/shared/models/users/index.ts +++ b/shared/models/users/index.ts @@ -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' diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts new file mode 100644 index 000000000..0ee41a79b --- /dev/null +++ b/shared/models/users/user-update-me.model.ts @@ -0,0 +1,5 @@ +export interface UserUpdateMe { + displayNSFW?: boolean + email?: string + password?: string +} diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts index 895ec0681..e22166fdc 100644 --- a/shared/models/users/user-update.model.ts +++ b/shared/models/users/user-update.model.ts @@ -1,5 +1,4 @@ export interface UserUpdate { - displayNSFW?: boolean - password?: string + email?: string videoQuota?: number }