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 @@
+
+
+
Danger Zone
+
+
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 @@
+
\ 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()