aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/package.json3
-rw-r--r--client/src/app/+admin/admin.module.ts3
-rw-r--r--client/src/app/+admin/users/user-edit/index.ts1
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html10
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss9
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.html25
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss21
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts100
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts24
-rw-r--r--client/src/app/+admin/users/users.routes.ts3
-rw-r--r--client/src/app/shared/users/user.service.ts5
-rw-r--r--server/controllers/api/users/index.ts1
-rw-r--r--server/lib/emailer.ts16
13 files changed, 217 insertions, 4 deletions
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 @@
165 "webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d", 165 "webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
166 "whatwg-fetch": "^3.0.0", 166 "whatwg-fetch": "^3.0.0",
167 "zone.js": "~0.8.5" 167 "zone.js": "~0.8.5"
168 },
169 "dependencies": {
170 "generate-password-browser": "^1.0.2"
168 } 171 }
169} 172}
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.
10import { JobsComponent } from './jobs/job.component' 10import { JobsComponent } from './jobs/job.component'
11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' 11import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
12import { JobService } from './jobs/shared/job.service' 12import { JobService } from './jobs/shared/job.service'
13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' 13import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' 14import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
15import { ModerationComponent } from '@app/+admin/moderation/moderation.component' 15import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' 16import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
@@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
36 UsersComponent, 36 UsersComponent,
37 UserCreateComponent, 37 UserCreateComponent,
38 UserUpdateComponent, 38 UserUpdateComponent,
39 UserPasswordComponent,
39 UserListComponent, 40 UserListComponent,
40 41
41 ModerationComponent, 42 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 @@
1export * from './user-create.component' 1export * from './user-create.component'
2export * from './user-update.component' 2export * from './user-update.component'
3export * 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 @@
81 81
82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
83</form> 83</form>
84
85<div *ngIf="isAdministration">
86 <div class="account-title" i18n>Danger Zone</div>
87
88 <p i18n>Send a link to reset the password by mail to the user.</p>
89 <button (click)="resetPassword()" i18n>Ask for new password</button>
90
91 <p class="mt-4" i18n>Manually set the user password</p>
92 <my-user-password></my-user-password>
93</div> \ 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]) {
14 @include peertube-select-container(340px); 14 @include peertube-select-container(340px);
15} 15}
16 16
17input[type=submit] { 17input[type=submit], button {
18 @include peertube-button; 18 @include peertube-button;
19 @include orange-button; 19 @include orange-button;
20 20
@@ -25,3 +25,10 @@ input[type=submit] {
25 margin-top: 5px; 25 margin-top: 5px;
26 font-size: 11px; 26 font-size: 11px;
27} 27}
28
29.account-title {
30 @include in-content-small-title;
31
32 margin-top: 55px;
33 margin-bottom: 30px;
34}
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 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group">
3
4 <div class="input-group mb-3">
5 <div class="input-group-prepend">
6 <div class="input-group-text">
7 <input type="checkbox" aria-label="Show password" (change)="togglePasswordVisibility()">
8 </div>
9 </div>
10 <input id="passwordField" #passwordField
11 [attr.type]="showPassword ? 'text' : 'password'" id="password"
12 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
13 >
14 <div class="input-group-append">
15 <button class="btn btn-sm btn-outline-secondary" (click)="generatePassword() "
16 type="button">Generate</button>
17 </div>
18 </div>
19 <div *ngIf="formErrors.password" class="form-error">
20 {{ formErrors.password }}
21 </div>
22 </div>
23
24 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
25</form> \ 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 @@
1@import '_variables';
2@import '_mixins';
3
4input:not([type=submit]):not([type=checkbox]) {
5 @include peertube-input-text(340px);
6 display: block;
7 border-top-right-radius: 0;
8 border-bottom-right-radius: 0;
9 border-right: none;
10}
11
12input[type=submit] {
13 @include peertube-button;
14 @include orange-button;
15
16 margin-top: 10px;
17}
18
19.input-group-append {
20 height: 30px;
21}
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 @@
1import { Component, OnDestroy, OnInit, Input } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs'
4import * as generator from 'generate-password-browser'
5import { NotificationsService } from 'angular2-notifications'
6import { UserService } from '@app/shared/users/user.service'
7import { ServerService } from '../../../core'
8import { User, UserUpdate } from '../../../../../../shared'
9import { I18n } from '@ngx-translate/i18n-polyfill'
10import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
11import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
12import { ConfigService } from '@app/+admin/config/shared/config.service'
13import { FormReactive } from '../../../shared'
14
15@Component({
16 selector: 'my-user-password',
17 templateUrl: './user-password.component.html',
18 styleUrls: [ './user-password.component.scss' ]
19})
20export class UserPasswordComponent extends FormReactive implements OnInit, OnDestroy {
21 error: string
22 userId: number
23 username: string
24 showPassword = false
25
26 private paramsSub: Subscription
27
28 constructor (
29 protected formValidatorService: FormValidatorService,
30 protected serverService: ServerService,
31 protected configService: ConfigService,
32 private userValidatorsService: UserValidatorsService,
33 private route: ActivatedRoute,
34 private router: Router,
35 private notificationsService: NotificationsService,
36 private userService: UserService,
37 private i18n: I18n
38 ) {
39 super()
40 }
41
42 ngOnInit () {
43 this.buildForm({
44 password: this.userValidatorsService.USER_PASSWORD
45 })
46
47 this.paramsSub = this.route.params.subscribe(routeParams => {
48 const userId = routeParams['id']
49 this.userService.getUser(userId).subscribe(
50 user => this.onUserFetched(user),
51
52 err => this.error = err.message
53 )
54 })
55 }
56
57 ngOnDestroy () {
58 this.paramsSub.unsubscribe()
59 }
60
61 formValidated () {
62 this.error = undefined
63
64 const userUpdate: UserUpdate = this.form.value
65
66 this.userService.updateUser(this.userId, userUpdate).subscribe(
67 () => {
68 this.notificationsService.success(
69 this.i18n('Success'),
70 this.i18n('Password changed for user {{username}}.', { username: this.username })
71 )
72 },
73
74 err => this.error = err.message
75 )
76 }
77
78 generatePassword () {
79 this.form.patchValue({
80 password: generator.generate({
81 length: 16,
82 excludeSimilarCharacters: true,
83 strict: true
84 })
85 })
86 }
87
88 togglePasswordVisibility () {
89 this.showPassword = !this.showPassword
90 }
91
92 getFormButtonTitle () {
93 return this.i18n('Update user password')
94 }
95
96 private onUserFetched (userJson: User) {
97 this.userId = userJson.id
98 this.username = userJson.username
99 }
100}
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 @@
1import { Component, OnDestroy, OnInit } from '@angular/core' 1import { Component, OnDestroy, OnInit, Input } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import { Subscription } from 'rxjs' 3import { Subscription } from 'rxjs'
4import { Notifier } from '@app/core' 4import { Notifier } from '@app/core'
@@ -19,9 +19,12 @@ import { UserService } from '@app/shared'
19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { 19export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
20 error: string 20 error: string
21 userId: number 21 userId: number
22 userEmail: string
22 username: string 23 username: string
24 isAdministration = false
23 25
24 private paramsSub: Subscription 26 private paramsSub: Subscription
27 private isAdministrationSub: Subscription
25 28
26 constructor ( 29 constructor (
27 protected formValidatorService: FormValidatorService, 30 protected formValidatorService: FormValidatorService,
@@ -56,10 +59,15 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
56 err => this.error = err.message 59 err => this.error = err.message
57 ) 60 )
58 }) 61 })
62
63 this.isAdministrationSub = this.route.data.subscribe(data => {
64 if (data.isAdministration) this.isAdministration = data.isAdministration
65 })
59 } 66 }
60 67
61 ngOnDestroy () { 68 ngOnDestroy () {
62 this.paramsSub.unsubscribe() 69 this.paramsSub.unsubscribe()
70 this.isAdministrationSub.unsubscribe()
63 } 71 }
64 72
65 formValidated () { 73 formValidated () {
@@ -89,9 +97,23 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
89 return this.i18n('Update user') 97 return this.i18n('Update user')
90 } 98 }
91 99
100 resetPassword () {
101 this.userService.askResetPassword(this.userEmail).subscribe(
102 () => {
103 this.notificationsService.success(
104 this.i18n('Success'),
105 this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
106 )
107 },
108
109 err => this.error = err.message
110 )
111 }
112
92 private onUserFetched (userJson: User) { 113 private onUserFetched (userJson: User) {
93 this.userId = userJson.id 114 this.userId = userJson.id
94 this.username = userJson.username 115 this.username = userJson.username
116 this.userEmail = userJson.email
95 117
96 this.form.patchValue({ 118 this.form.patchValue({
97 email: userJson.email, 119 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 = [
44 data: { 44 data: {
45 meta: { 45 meta: {
46 title: 'Update a user' 46 title: 'Update a user'
47 } 47 },
48 isAdministration: true
48 } 49 }
49 } 50 }
50 ] 51 ]
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 {
103 ) 103 )
104 } 104 }
105 105
106 resetUserPassword (userId: number) {
107 return this.authHttp.post(UserService.BASE_USERS_URL + userId + '/reset-password', {})
108 .pipe(catchError(err => this.restExtractor.handleError(err)))
109 }
110
106 verifyEmail (userId: number, verificationString: string) { 111 verifyEmail (userId: number, verificationString: string) {
107 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` 112 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
108 const body = { 113 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'
3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' 3import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared'
4import { logger } from '../../../helpers/logger' 4import { logger } from '../../../helpers/logger'
5import { getFormattedObjects } from '../../../helpers/utils' 5import { getFormattedObjects } from '../../../helpers/utils'
6import { pseudoRandomBytesPromise } from '../../../helpers/core-utils'
6import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' 7import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers'
7import { Emailer } from '../../../lib/emailer' 8import { Emailer } from '../../../lib/emailer'
8import { Redis } from '../../../lib/redis' 9import { 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 {
101 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 101 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
102 } 102 }
103 103
104 addForceResetPasswordEmailJob (to: string, resetPasswordUrl: string) {
105 const text = `Hi dear user,\n\n` +
106 `Your password has been reset on ${CONFIG.WEBSERVER.HOST}! ` +
107 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` +
108 `Cheers,\n` +
109 `PeerTube.`
110
111 const emailPayload: EmailPayload = {
112 to: [ to ],
113 subject: 'Reset of your PeerTube password',
114 text
115 }
116
117 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
118 }
119
104 addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { 120 addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') {
105 const followerName = actorFollow.ActorFollower.Account.getDisplayName() 121 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
106 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 122 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()