diff options
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 | ||
4 | input:not([type=submit]):not([type=checkbox]) { | 4 | input: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 @@ | |||
1 | import { Component, OnDestroy, OnInit, Input } from '@angular/core' | 1 | import { Component, Input, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import * as generator from 'generate-password-browser' | ||
4 | import { NotificationsService } from 'angular2-notifications' | ||
5 | import { UserService } from '@app/shared/users/user.service' | 3 | import { UserService } from '@app/shared/users/user.service' |
6 | import { ServerService } from '../../../core' | 4 | import { Notifier } from '../../../core' |
7 | import { User, UserUpdate } from '../../../../../../shared' | 5 | import { User, UserUpdate } from '../../../../../../shared' |
8 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
9 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | 7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' |
10 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | 8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' |
11 | import { ConfigService } from '@app/+admin/config/shared/config.service' | ||
12 | import { FormReactive } from '../../../shared' | 9 | import { 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 | }) |
19 | export class UserPasswordComponent extends FormReactive implements OnInit, OnDestroy { | 16 | export 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 @@ | |||
1 | import { Component, OnDestroy, OnInit, Input } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit } from '@angular/core' |
2 | import { ActivatedRoute, Router } from '@angular/router' | 2 | import { ActivatedRoute, Router } from '@angular/router' |
3 | import { Subscription } from 'rxjs' | 3 | import { Subscription } from 'rxjs' |
4 | import { Notifier } from '@app/core' | 4 | import { 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' | |||
3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' | 3 | import { UserCreate, UserRight, UserRole, UserUpdate } from '../../../../shared' |
4 | import { logger } from '../../../helpers/logger' | 4 | import { logger } from '../../../helpers/logger' |
5 | import { getFormattedObjects } from '../../../helpers/utils' | 5 | import { getFormattedObjects } from '../../../helpers/utils' |
6 | import { pseudoRandomBytesPromise } from '../../../helpers/core-utils' | ||
7 | import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' | 6 | import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' |
8 | import { Emailer } from '../../../lib/emailer' | 7 | import { Emailer } from '../../../lib/emailer' |
9 | import { Redis } from '../../../lib/redis' | 8 | import { 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 | ||
233 | async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 232 | async 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 | ||
242 | function getUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 241 | function 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 | ||
246 | async function autocompleteUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 245 | async 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 | ||
252 | async function listUsers (req: express.Request, res: express.Response, next: express.NextFunction) { | 251 | async 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 | ||
258 | async function removeUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 257 | async 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 | ||
268 | async function updateUser (req: express.Request, res: express.Response, next: express.NextFunction) { | 267 | async 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 | ||
170 | async function updateMe (req: express.Request, res: express.Response, next: express.NextFunction) { | 170 | async 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 | ||
716 | updateWebserverUrls() | 718 | updateWebserverUrls() |
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 | ||
114 | const usersUpdateValidator = [ | 114 | const 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 @@ | |||
1 | import { UserRole } from './user-role' | 1 | import { UserRole } from './user-role' |
2 | 2 | ||
3 | export interface UserUpdate { | 3 | export 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 |