From 328c78bc4a570a9aceaaa1a2124bacd4a0e8d295 Mon Sep 17 00:00:00 2001 From: Rigel Kent Date: Sat, 6 Oct 2018 13:54:00 +0200 Subject: [PATCH] allow administration to change/reset a user's password --- client/package.json | 3 + client/src/app/+admin/admin.module.ts | 3 +- .../src/app/+admin/users/user-edit/index.ts | 1 + .../users/user-edit/user-edit.component.html | 10 ++ .../users/user-edit/user-edit.component.scss | 9 +- .../user-edit/user-password.component.html | 25 +++++ .../user-edit/user-password.component.scss | 21 ++++ .../user-edit/user-password.component.ts | 100 ++++++++++++++++++ .../users/user-edit/user-update.component.ts | 24 ++++- client/src/app/+admin/users/users.routes.ts | 3 +- client/src/app/shared/users/user.service.ts | 5 + server/controllers/api/users/index.ts | 1 + server/lib/emailer.ts | 16 +++ 13 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 client/src/app/+admin/users/user-edit/user-password.component.html create mode 100644 client/src/app/+admin/users/user-edit/user-password.component.scss create mode 100644 client/src/app/+admin/users/user-edit/user-password.component.ts diff --git a/client/package.json b/client/package.json index 3eea661f1..5f957bf75 100644 --- a/client/package.json +++ b/client/package.json @@ -165,5 +165,8 @@ "webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d", "whatwg-fetch": "^3.0.0", "zone.js": "~0.8.5" + }, + "dependencies": { + "generate-password-browser": "^1.0.2" } } diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index c06ae1d60..f7f347105 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list. import { JobsComponent } from './jobs/job.component' import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' import { JobService } from './jobs/shared/job.service' -import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' +import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' import { ModerationComponent } from '@app/+admin/moderation/moderation.component' import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' @@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f UsersComponent, UserCreateComponent, UserUpdateComponent, + UserPasswordComponent, UserListComponent, ModerationComponent, diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts index fd80a02e0..ec734ef92 100644 --- a/client/src/app/+admin/users/user-edit/index.ts +++ b/client/src/app/+admin/users/user-edit/index.ts @@ -1,2 +1,3 @@ export * from './user-create.component' export * from './user-update.component' +export * from './user-password.component' diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 56cf7d17d..cbc06c157 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html @@ -81,3 +81,13 @@ + +
+ + +

Send a link to reset the password by mail to the user.

+ + +

Manually set the user password

+ +
\ No newline at end of file diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index 6675f65cc..2b4aae83c 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss @@ -14,7 +14,7 @@ input:not([type=submit]) { @include peertube-select-container(340px); } -input[type=submit] { +input[type=submit], button { @include peertube-button; @include orange-button; @@ -25,3 +25,10 @@ input[type=submit] { margin-top: 5px; font-size: 11px; } + +.account-title { + @include in-content-small-title; + + margin-top: 55px; + margin-bottom: 30px; +} diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html new file mode 100644 index 000000000..ee7d8dff5 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.html @@ -0,0 +1,25 @@ +
+
+ +
+
+
+ +
+
+ +
+ +
+
+
+ {{ formErrors.password }} +
+
+ + +
\ No newline at end of file diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss new file mode 100644 index 000000000..9185e787c --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss @@ -0,0 +1,21 @@ +@import '_variables'; +@import '_mixins'; + +input:not([type=submit]):not([type=checkbox]) { + @include peertube-input-text(340px); + display: block; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; +} + +input[type=submit] { + @include peertube-button; + @include orange-button; + + margin-top: 10px; +} + +.input-group-append { + height: 30px; +} diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts new file mode 100644 index 000000000..1f9ccb4e8 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts @@ -0,0 +1,100 @@ +import { Component, OnDestroy, OnInit, Input } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { Subscription } from 'rxjs' +import * as generator from 'generate-password-browser' +import { NotificationsService } from 'angular2-notifications' +import { UserService } from '@app/shared/users/user.service' +import { ServerService } from '../../../core' +import { User, UserUpdate } from '../../../../../../shared' +import { I18n } from '@ngx-translate/i18n-polyfill' +import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' +import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' +import { ConfigService } from '@app/+admin/config/shared/config.service' +import { FormReactive } from '../../../shared' + +@Component({ + selector: 'my-user-password', + templateUrl: './user-password.component.html', + styleUrls: [ './user-password.component.scss' ] +}) +export class UserPasswordComponent extends FormReactive implements OnInit, OnDestroy { + error: string + userId: number + username: string + showPassword = false + + private paramsSub: Subscription + + constructor ( + protected formValidatorService: FormValidatorService, + protected serverService: ServerService, + protected configService: ConfigService, + private userValidatorsService: UserValidatorsService, + private route: ActivatedRoute, + private router: Router, + private notificationsService: NotificationsService, + private userService: UserService, + private i18n: I18n + ) { + super() + } + + ngOnInit () { + this.buildForm({ + password: this.userValidatorsService.USER_PASSWORD + }) + + this.paramsSub = this.route.params.subscribe(routeParams => { + const userId = routeParams['id'] + this.userService.getUser(userId).subscribe( + user => this.onUserFetched(user), + + err => this.error = err.message + ) + }) + } + + ngOnDestroy () { + this.paramsSub.unsubscribe() + } + + formValidated () { + this.error = undefined + + const userUpdate: UserUpdate = this.form.value + + this.userService.updateUser(this.userId, userUpdate).subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('Password changed for user {{username}}.', { username: this.username }) + ) + }, + + err => this.error = err.message + ) + } + + generatePassword () { + this.form.patchValue({ + password: generator.generate({ + length: 16, + excludeSimilarCharacters: true, + strict: true + }) + }) + } + + togglePasswordVisibility () { + this.showPassword = !this.showPassword + } + + getFormButtonTitle () { + return this.i18n('Update user password') + } + + private onUserFetched (userJson: User) { + this.userId = userJson.id + this.username = userJson.username + } +} 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 index 61e641823..cb74897d0 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' +import { Component, OnDestroy, OnInit, Input } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { Subscription } from 'rxjs' import { Notifier } from '@app/core' @@ -19,9 +19,12 @@ import { UserService } from '@app/shared' export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { error: string userId: number + userEmail: string username: string + isAdministration = false private paramsSub: Subscription + private isAdministrationSub: Subscription constructor ( protected formValidatorService: FormValidatorService, @@ -56,10 +59,15 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { err => this.error = err.message ) }) + + this.isAdministrationSub = this.route.data.subscribe(data => { + if (data.isAdministration) this.isAdministration = data.isAdministration + }) } ngOnDestroy () { this.paramsSub.unsubscribe() + this.isAdministrationSub.unsubscribe() } formValidated () { @@ -89,9 +97,23 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { return this.i18n('Update user') } + resetPassword () { + this.userService.askResetPassword(this.userEmail).subscribe( + () => { + this.notificationsService.success( + this.i18n('Success'), + this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) + ) + }, + + err => this.error = err.message + ) + } + private onUserFetched (userJson: User) { this.userId = userJson.id this.username = userJson.username + this.userEmail = userJson.email this.form.patchValue({ email: userJson.email, diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts index 8b3791bd3..460ebd89e 100644 --- a/client/src/app/+admin/users/users.routes.ts +++ b/client/src/app/+admin/users/users.routes.ts @@ -44,7 +44,8 @@ export const UsersRoutes: Routes = [ data: { meta: { title: 'Update a user' - } + }, + isAdministration: true } } ] diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts index cc5c051f1..d0abc7def 100644 --- a/client/src/app/shared/users/user.service.ts +++ b/client/src/app/shared/users/user.service.ts @@ -103,6 +103,11 @@ export class UserService { ) } + resetUserPassword (userId: number) { + return this.authHttp.post(UserService.BASE_USERS_URL + userId + '/reset-password', {}) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + verifyEmail (userId: number, verificationString: string) { const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` const body = { diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts index dbe0718d4..beac6d8b1 100644 --- a/server/controllers/api/users/index.ts +++ b/server/controllers/api/users/index.ts @@ -3,6 +3,7 @@ import * as RateLimit from 'express-rate-limit' import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' import { logger } from '../../../helpers/logger' import { getFormattedObjects } from '../../../helpers/utils' +import { pseudoRandomBytesPromise } from '../../../helpers/core-utils' import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' import { Emailer } from '../../../lib/emailer' import { Redis } from '../../../lib/redis' diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts index f384a254e..7681164b3 100644 --- a/server/lib/emailer.ts +++ b/server/lib/emailer.ts @@ -101,6 +101,22 @@ class Emailer { return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) } + addForceResetPasswordEmailJob (to: string, resetPasswordUrl: string) { + const text = `Hi dear user,\n\n` + + `Your password has been reset on ${CONFIG.WEBSERVER.HOST}! ` + + `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + + `Cheers,\n` + + `PeerTube.` + + const emailPayload: EmailPayload = { + to: [ to ], + subject: 'Reset of your PeerTube password', + text + } + + return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) + } + addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { const followerName = actorFollow.ActorFollower.Account.getDisplayName() const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() -- 2.41.0