aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-02-11 09:30:29 +0100
committerChocobozzz <me@florianbigard.com>2019-02-11 10:37:27 +0100
commitb426edd4854adc6e65844d8c54b8998e792b5778 (patch)
treeb9ef4da0cdb2ab14c0aa1d67a883303f3ed0de14
parent67b1d3fed765278bdc876cce393ef56d56942df0 (diff)
downloadPeerTube-b426edd4854adc6e65844d8c54b8998e792b5778.tar.gz
PeerTube-b426edd4854adc6e65844d8c54b8998e792b5778.tar.zst
PeerTube-b426edd4854adc6e65844d8c54b8998e792b5778.zip
Cleanup reset user password by admin
And add some tests
-rw-r--r--client/package.json3
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html16
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.scss13
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts5
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.html16
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.scss1
-rw-r--r--client/src/app/+admin/users/user-edit/user-password.component.ts35
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts5
-rw-r--r--client/src/app/shared/users/user.service.ts5
-rw-r--r--server/controllers/api/users/index.ts20
-rw-r--r--server/controllers/api/users/me.ts2
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/emailer.ts20
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/tests/api/check-params/users.ts18
-rw-r--r--server/tests/api/users/users.ts16
-rw-r--r--shared/models/users/user-update.model.ts1
-rw-r--r--shared/utils/users/users.ts2
18 files changed, 97 insertions, 85 deletions
diff --git a/client/package.json b/client/package.json
index 9e5e87d4a..3eea661f1 100644
--- a/client/package.json
+++ b/client/package.json
@@ -164,7 +164,6 @@
164 "webpack-cli": "^3.0.8", 164 "webpack-cli": "^3.0.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 "generate-password-browser": "^1.0.2"
169 } 168 }
170} 169}
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 3ce246771..c6566da24 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
@@ -82,12 +82,16 @@
82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> 82 <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
83</form> 83</form>
84 84
85<div *ngIf="!isCreation()"> 85<div *ngIf="!isCreation()" class="danger-zone">
86 <div class="account-title" i18n>Danger Zone</div> 86 <div class="account-title" i18n>Danger Zone</div>
87 87
88 <p i18n>Send a link to reset the password by mail to the user.</p> 88 <div class="form-group reset-password-email">
89 <button style="margin-top:0;" (click)="resetPassword()" i18n>Ask for new password</button> 89 <label i18n>Send a link to reset the password by email to the user</label>
90 <button (click)="resetPassword()" i18n>Ask for new password</button>
91 </div>
90 92
91 <p class="mt-4" i18n>Manually set the user password</p> 93 <div class="form-group">
92 <my-user-password userId="userId"></my-user-password> 94 <label i18n>Manually set the user password</label>
93</div> \ No newline at end of file 95 <my-user-password [userId]="userId"></my-user-password>
96 </div>
97</div>
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 2b4aae83c..c1cc4ca45 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
@@ -32,3 +32,16 @@ input[type=submit], button {
32 margin-top: 55px; 32 margin-top: 55px;
33 margin-bottom: 30px; 33 margin-bottom: 30px;
34} 34}
35
36.danger-zone {
37 .reset-password-email {
38 margin-bottom: 30px;
39 padding-bottom: 30px;
40 border-bottom: 1px solid rgba(0, 0, 0, 0.1);
41
42 button {
43 display: block;
44 margin-top: 0;
45 }
46 }
47}
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts
index 021b1feb4..649b35b0c 100644
--- a/client/src/app/+admin/users/user-edit/user-edit.ts
+++ b/client/src/app/+admin/users/user-edit/user-edit.ts
@@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive {
8 videoQuotaDailyOptions: { value: string, label: string }[] = [] 8 videoQuotaDailyOptions: { value: string, label: string }[] = []
9 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) 9 roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
10 username: string 10 username: string
11 userId: number
11 12
12 protected abstract serverService: ServerService 13 protected abstract serverService: ServerService
13 protected abstract configService: ConfigService 14 protected abstract configService: ConfigService
@@ -37,6 +38,10 @@ export abstract class UserEdit extends FormReactive {
37 return multiplier * parseInt(this.form.value['videoQuota'], 10) 38 return multiplier * parseInt(this.form.value['videoQuota'], 10)
38 } 39 }
39 40
41 resetPassword () {
42 return
43 }
44
40 protected buildQuotaOptions () { 45 protected buildQuotaOptions () {
41 // These are used by a HTML select, so convert key into strings 46 // These are used by a HTML select, so convert key into strings
42 this.videoQuotaOptions = this.configService 47 this.videoQuotaOptions = this.configService
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
index 822e4688e..a1e1f6216 100644
--- a/client/src/app/+admin/users/user-edit/user-password.component.html
+++ b/client/src/app/+admin/users/user-edit/user-password.component.html
@@ -1,19 +1,15 @@
1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> 1<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
2 <div class="form-group"> 2 <div class="form-group">
3 3
4 <div class="input-group mb-3"> 4 <div class="input-group">
5 <div class="input-group-prepend"> 5 <input id="password" [attr.type]="showPassword ? 'text' : 'password'"
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'] }" 6 formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
13 > 7 >
14 <div class="input-group-append"> 8 <div class="input-group-append">
15 <button class="btn btn-sm btn-outline-secondary" (click)="generatePassword() " 9 <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
16 type="button">Generate</button> 10 <ng-container *ngIf="!showPassword" i18n>Show</ng-container>
11 <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
12 </button>
17 </div> 13 </div>
18 </div> 14 </div>
19 <div *ngIf="formErrors.password" class="form-error"> 15 <div *ngIf="formErrors.password" class="form-error">
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
index 9185e787c..217d585af 100644
--- a/client/src/app/+admin/users/user-edit/user-password.component.scss
+++ b/client/src/app/+admin/users/user-edit/user-password.component.scss
@@ -3,6 +3,7 @@
3 3
4input:not([type=submit]):not([type=checkbox]) { 4input:not([type=submit]):not([type=checkbox]) {
5 @include peertube-input-text(340px); 5 @include peertube-input-text(340px);
6
6 display: block; 7 display: block;
7 border-top-right-radius: 0; 8 border-top-right-radius: 0;
8 border-bottom-right-radius: 0; 9 border-bottom-right-radius: 0;
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
index 30cd21ccd..5b3040440 100644
--- a/client/src/app/+admin/users/user-edit/user-password.component.ts
+++ b/client/src/app/+admin/users/user-edit/user-password.component.ts
@@ -1,14 +1,11 @@
1import { Component, OnDestroy, OnInit, Input } from '@angular/core' 1import { Component, Input, OnInit } from '@angular/core'
2import { ActivatedRoute, Router } from '@angular/router' 2import { ActivatedRoute, Router } from '@angular/router'
3import * as generator from 'generate-password-browser'
4import { NotificationsService } from 'angular2-notifications'
5import { UserService } from '@app/shared/users/user.service' 3import { UserService } from '@app/shared/users/user.service'
6import { ServerService } from '../../../core' 4import { Notifier } from '../../../core'
7import { User, UserUpdate } from '../../../../../../shared' 5import { User, UserUpdate } from '../../../../../../shared'
8import { I18n } from '@ngx-translate/i18n-polyfill' 6import { I18n } from '@ngx-translate/i18n-polyfill'
9import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 7import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
10import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' 8import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
11import { ConfigService } from '@app/+admin/config/shared/config.service'
12import { FormReactive } from '../../../shared' 9import { FormReactive } from '../../../shared'
13 10
14@Component({ 11@Component({
@@ -16,7 +13,7 @@ import { FormReactive } from '../../../shared'
16 templateUrl: './user-password.component.html', 13 templateUrl: './user-password.component.html',
17 styleUrls: [ './user-password.component.scss' ] 14 styleUrls: [ './user-password.component.scss' ]
18}) 15})
19export class UserPasswordComponent extends FormReactive implements OnInit, OnDestroy { 16export class UserPasswordComponent extends FormReactive implements OnInit {
20 error: string 17 error: string
21 username: string 18 username: string
22 showPassword = false 19 showPassword = false
@@ -25,12 +22,10 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
25 22
26 constructor ( 23 constructor (
27 protected formValidatorService: FormValidatorService, 24 protected formValidatorService: FormValidatorService,
28 protected serverService: ServerService,
29 protected configService: ConfigService,
30 private userValidatorsService: UserValidatorsService, 25 private userValidatorsService: UserValidatorsService,
31 private route: ActivatedRoute, 26 private route: ActivatedRoute,
32 private router: Router, 27 private router: Router,
33 private notificationsService: NotificationsService, 28 private notifier: Notifier,
34 private userService: UserService, 29 private userService: UserService,
35 private i18n: I18n 30 private i18n: I18n
36 ) { 31 ) {
@@ -43,10 +38,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
43 }) 38 })
44 } 39 }
45 40
46 ngOnDestroy () {
47 //
48 }
49
50 formValidated () { 41 formValidated () {
51 this.error = undefined 42 this.error = undefined
52 43
@@ -54,8 +45,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
54 45
55 this.userService.updateUser(this.userId, userUpdate).subscribe( 46 this.userService.updateUser(this.userId, userUpdate).subscribe(
56 () => { 47 () => {
57 this.notificationsService.success( 48 this.notifier.success(
58 this.i18n('Success'),
59 this.i18n('Password changed for user {{username}}.', { username: this.username }) 49 this.i18n('Password changed for user {{username}}.', { username: this.username })
60 ) 50 )
61 }, 51 },
@@ -64,16 +54,6 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
64 ) 54 )
65 } 55 }
66 56
67 generatePassword () {
68 this.form.patchValue({
69 password: generator.generate({
70 length: 16,
71 excludeSimilarCharacters: true,
72 strict: true
73 })
74 })
75 }
76
77 togglePasswordVisibility () { 57 togglePasswordVisibility () {
78 this.showPassword = !this.showPassword 58 this.showPassword = !this.showPassword
79 } 59 }
@@ -81,9 +61,4 @@ export class UserPasswordComponent extends FormReactive implements OnInit, OnDes
81 getFormButtonTitle () { 61 getFormButtonTitle () {
82 return this.i18n('Update user password') 62 return this.i18n('Update user password')
83 } 63 }
84
85 private onUserFetched (userJson: User) {
86 this.userId = userJson.id
87 this.username = userJson.username
88 }
89} 64}
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 4e4002a73..94ef87b08 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, Input } from '@angular/core' 1import { Component, OnDestroy, OnInit } 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'
@@ -93,8 +93,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
93 resetPassword () { 93 resetPassword () {
94 this.userService.askResetPassword(this.userEmail).subscribe( 94 this.userService.askResetPassword(this.userEmail).subscribe(
95 () => { 95 () => {
96 this.notificationsService.success( 96 this.notifier.success(
97 this.i18n('Success'),
98 this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) 97 this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
99 ) 98 )
100 }, 99 },
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index d0abc7def..cc5c051f1 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -103,11 +103,6 @@ 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
111 verifyEmail (userId: number, verificationString: string) { 106 verifyEmail (userId: number, verificationString: string) {
112 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` 107 const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email`
113 const body = { 108 const body = {
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index beac6d8b1..e3533a7f6 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -3,7 +3,6 @@ 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'
7import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' 6import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers'
8import { Emailer } from '../../../lib/emailer' 7import { Emailer } from '../../../lib/emailer'
9import { Redis } from '../../../lib/redis' 8import { Redis } from '../../../lib/redis'
@@ -230,7 +229,7 @@ async function unblockUser (req: express.Request, res: express.Response, next: e
230 return res.status(204).end() 229 return res.status(204).end()
231} 230}
232 231
233async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { 232async function blockUser (req: express.Request, res: express.Response) {
234 const user: UserModel = res.locals.user 233 const user: UserModel = res.locals.user
235 const reason = req.body.reason 234 const reason = req.body.reason
236 235
@@ -239,23 +238,23 @@ async function blockUser (req: express.Request, res: express.Response, next: exp
239 return res.status(204).end() 238 return res.status(204).end()
240} 239}
241 240
242function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { 241function getUser (req: express.Request, res: express.Response) {
243 return res.json((res.locals.user as UserModel).toFormattedJSON()) 242 return res.json((res.locals.user as UserModel).toFormattedJSON())
244} 243}
245 244
246async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 245async function autocompleteUsers (req: express.Request, res: express.Response) {
247 const resultList = await UserModel.autoComplete(req.query.search as string) 246 const resultList = await UserModel.autoComplete(req.query.search as string)
248 247
249 return res.json(resultList) 248 return res.json(resultList)
250} 249}
251 250
252async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { 251async function listUsers (req: express.Request, res: express.Response) {
253 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search) 252 const resultList = await UserModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.search)
254 253
255 return res.json(getFormattedObjects(resultList.data, resultList.total)) 254 return res.json(getFormattedObjects(resultList.data, resultList.total))
256} 255}
257 256
258async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { 257async function removeUser (req: express.Request, res: express.Response) {
259 const user: UserModel = res.locals.user 258 const user: UserModel = res.locals.user
260 259
261 await user.destroy() 260 await user.destroy()
@@ -265,12 +264,13 @@ async function removeUser (req: express.Request, res: express.Response, next: ex
265 return res.sendStatus(204) 264 return res.sendStatus(204)
266} 265}
267 266
268async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { 267async function updateUser (req: express.Request, res: express.Response) {
269 const body: UserUpdate = req.body 268 const body: UserUpdate = req.body
270 const userToUpdate = res.locals.user as UserModel 269 const userToUpdate = res.locals.user as UserModel
271 const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON()) 270 const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
272 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role 271 const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
273 272
273 if (body.password !== undefined) userToUpdate.password = body.password
274 if (body.email !== undefined) userToUpdate.email = body.email 274 if (body.email !== undefined) userToUpdate.email = body.email
275 if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified 275 if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
276 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota 276 if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
@@ -280,11 +280,11 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
280 const user = await userToUpdate.save() 280 const user = await userToUpdate.save()
281 281
282 // Destroy user token to refresh rights 282 // Destroy user token to refresh rights
283 if (roleChanged) await deleteUserToken(userToUpdate.id) 283 if (roleChanged || body.password !== undefined) await deleteUserToken(userToUpdate.id)
284 284
285 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView) 285 auditLogger.update(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()), oldUserAuditView)
286 286
287 // Don't need to send this update to followers, these attributes are not propagated 287 // Don't need to send this update to followers, these attributes are not federated
288 288
289 return res.sendStatus(204) 289 return res.sendStatus(204)
290} 290}
@@ -294,7 +294,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response
294 294
295 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) 295 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
296 const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString 296 const url = CONFIG.WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
297 await Emailer.Instance.addForgetPasswordEmailJob(user.email, url) 297 await Emailer.Instance.addPasswordResetEmailJob(user.email, url)
298 298
299 return res.status(204).end() 299 return res.status(204).end()
300} 300}
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 94a2b8732..d5e154869 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -167,7 +167,7 @@ async function deleteMe (req: express.Request, res: express.Response) {
167 return res.sendStatus(204) 167 return res.sendStatus(204)
168} 168}
169 169
170async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { 170async function updateMe (req: express.Request, res: express.Response) {
171 const body: UserUpdateMe = req.body 171 const body: UserUpdateMe = req.body
172 172
173 const user: UserModel = res.locals.oauth.token.user 173 const user: UserModel = res.locals.oauth.token.user
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 98f8f8694..e5c4c4e63 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -711,6 +711,8 @@ if (isTestInstance() === true) {
711 CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000 711 CACHE.VIDEO_CAPTIONS.MAX_AGE = 3000
712 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1 712 MEMOIZE_TTL.OVERVIEWS_SAMPLE = 1
713 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms' 713 ROUTE_CACHE_LIFETIME.OVERVIEWS.VIDEOS = '0ms'
714
715 RATES_LIMIT.LOGIN.MAX = 20
714} 716}
715 717
716updateWebserverUrls() 718updateWebserverUrls()
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index 7681164b3..672414cc0 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -101,22 +101,6 @@ 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
120 addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') { 104 addNewFollowNotification (to: string[], actorFollow: ActorFollowModel, followType: 'account' | 'channel') {
121 const followerName = actorFollow.ActorFollower.Account.getDisplayName() 105 const followerName = actorFollow.ActorFollower.Account.getDisplayName()
122 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName() 106 const followingName = (actorFollow.ActorFollowing.VideoChannel || actorFollow.ActorFollowing.Account).getDisplayName()
@@ -312,9 +296,9 @@ class Emailer {
312 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload }) 296 return JobQueue.Instance.createJob({ type: 'email', payload: emailPayload })
313 } 297 }
314 298
315 addForgetPasswordEmailJob (to: string, resetPasswordUrl: string) { 299 addPasswordResetEmailJob (to: string, resetPasswordUrl: string) {
316 const text = `Hi dear user,\n\n` + 300 const text = `Hi dear user,\n\n` +
317 `It seems you forgot your password on ${CONFIG.WEBSERVER.HOST}! ` + 301 `A reset password procedure for your account ${to} has been requested on ${CONFIG.WEBSERVER.HOST} ` +
318 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` + 302 `Please follow this link to reset it: ${resetPasswordUrl}\n\n` +
319 `If you are not the person who initiated this request, please ignore this email.\n\n` + 303 `If you are not the person who initiated this request, please ignore this email.\n\n` +
320 `Cheers,\n` + 304 `Cheers,\n` +
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 1bb0bfb1b..a52e3060a 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -113,6 +113,7 @@ const deleteMeValidator = [
113 113
114const usersUpdateValidator = [ 114const usersUpdateValidator = [
115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), 115 param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
116 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
116 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 117 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
117 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'), 118 body('emailVerified').optional().isBoolean().withMessage('Should have a valid email verified attribute'),
118 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), 119 body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
@@ -233,6 +234,7 @@ const usersAskResetPasswordValidator = [
233 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body }) 234 logger.debug('Checking usersAskResetPassword parameters', { parameters: req.body })
234 235
235 if (areValidationErrors(req, res)) return 236 if (areValidationErrors(req, res)) return
237
236 const exists = await checkUserEmailExist(req.body.email, res, false) 238 const exists = await checkUserEmailExist(req.body.email, res, false)
237 if (!exists) { 239 if (!exists) {
238 logger.debug('User with email %s does not exist (asking reset password).', req.body.email) 240 logger.debug('User with email %s does not exist (asking reset password).', req.body.email)
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index a3e8e2e9c..13be8b460 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -464,6 +464,24 @@ describe('Test users API validators', function () {
464 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields }) 464 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
465 }) 465 })
466 466
467 it('Should fail with a too small password', async function () {
468 const fields = {
469 currentPassword: 'my super password',
470 password: 'bla'
471 }
472
473 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
474 })
475
476 it('Should fail with a too long password', async function () {
477 const fields = {
478 currentPassword: 'my super password',
479 password: 'super'.repeat(61)
480 }
481
482 await makePutBodyRequest({ url: server.url, path: path + userId, token: server.accessToken, fields })
483 })
484
467 it('Should fail with an non authenticated user', async function () { 485 it('Should fail with an non authenticated user', async function () {
468 const fields = { 486 const fields = {
469 videoQuota: 42 487 videoQuota: 42
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index ad98ab1c7..c4465d541 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -501,6 +501,22 @@ describe('Test users', function () {
501 accessTokenUser = await userLogin(server, user) 501 accessTokenUser = await userLogin(server, user)
502 }) 502 })
503 503
504 it('Should be able to update another user password', async function () {
505 await updateUser({
506 url: server.url,
507 userId,
508 accessToken,
509 password: 'password updated'
510 })
511
512 await getMyUserVideoQuotaUsed(server.url, accessTokenUser, 401)
513
514 await userLogin(server, user, 400)
515
516 user.password = 'password updated'
517 accessTokenUser = await userLogin(server, user)
518 })
519
504 it('Should be able to list video blacklist by a moderator', async function () { 520 it('Should be able to list video blacklist by a moderator', async function () {
505 await getBlacklistedVideosList(server.url, accessTokenUser) 521 await getBlacklistedVideosList(server.url, accessTokenUser)
506 }) 522 })
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts
index abde51321..cd215bab3 100644
--- a/shared/models/users/user-update.model.ts
+++ b/shared/models/users/user-update.model.ts
@@ -1,6 +1,7 @@
1import { UserRole } from './user-role' 1import { UserRole } from './user-role'
2 2
3export interface UserUpdate { 3export interface UserUpdate {
4 password?: string
4 email?: string 5 email?: string
5 emailVerified?: boolean 6 emailVerified?: boolean
6 videoQuota?: number 7 videoQuota?: number
diff --git a/shared/utils/users/users.ts b/shared/utils/users/users.ts
index 61a7e3757..7191b263e 100644
--- a/shared/utils/users/users.ts
+++ b/shared/utils/users/users.ts
@@ -213,11 +213,13 @@ function updateUser (options: {
213 emailVerified?: boolean, 213 emailVerified?: boolean,
214 videoQuota?: number, 214 videoQuota?: number,
215 videoQuotaDaily?: number, 215 videoQuotaDaily?: number,
216 password?: string,
216 role?: UserRole 217 role?: UserRole
217}) { 218}) {
218 const path = '/api/v1/users/' + options.userId 219 const path = '/api/v1/users/' + options.userId
219 220
220 const toSend = {} 221 const toSend = {}
222 if (options.password !== undefined && options.password !== null) toSend['password'] = options.password
221 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email 223 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
222 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified 224 if (options.emailVerified !== undefined && options.emailVerified !== null) toSend['emailVerified'] = options.emailVerified
223 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota 225 if (options.videoQuota !== undefined && options.videoQuota !== null) toSend['videoQuota'] = options.videoQuota