diff options
-rw-r--r-- | client/package.json | 3 | ||||
-rw-r--r-- | client/src/app/+admin/admin.module.ts | 3 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/index.ts | 1 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-edit.component.html | 10 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-edit.component.scss | 9 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-password.component.html | 25 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-password.component.scss | 21 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-password.component.ts | 100 | ||||
-rw-r--r-- | client/src/app/+admin/users/user-edit/user-update.component.ts | 24 | ||||
-rw-r--r-- | client/src/app/+admin/users/users.routes.ts | 3 | ||||
-rw-r--r-- | client/src/app/shared/users/user.service.ts | 5 | ||||
-rw-r--r-- | server/controllers/api/users/index.ts | 1 | ||||
-rw-r--r-- | server/lib/emailer.ts | 16 |
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. | |||
10 | import { JobsComponent } from './jobs/job.component' | 10 | import { JobsComponent } from './jobs/job.component' |
11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' | 11 | import { JobsListComponent } from './jobs/jobs-list/jobs-list.component' |
12 | import { JobService } from './jobs/shared/job.service' | 12 | import { JobService } from './jobs/shared/job.service' |
13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users' | 13 | import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users' |
14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' | 14 | import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation' |
15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' | 15 | import { ModerationComponent } from '@app/+admin/moderation/moderation.component' |
16 | import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component' | 16 | import { 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 @@ | |||
1 | export * from './user-create.component' | 1 | export * from './user-create.component' |
2 | export * from './user-update.component' | 2 | export * from './user-update.component' |
3 | 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 @@ | |||
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 | ||
17 | input[type=submit] { | 17 | input[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 | |||
4 | input: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 | |||
12 | input[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 @@ | |||
1 | import { Component, OnDestroy, OnInit, Input } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { Subscription } from 'rxjs' | ||
4 | import * as generator from 'generate-password-browser' | ||
5 | import { NotificationsService } from 'angular2-notifications' | ||
6 | import { UserService } from '@app/shared/users/user.service' | ||
7 | import { ServerService } from '../../../core' | ||
8 | import { User, UserUpdate } from '../../../../../../shared' | ||
9 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
10 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
11 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
12 | import { ConfigService } from '@app/+admin/config/shared/config.service' | ||
13 | import { FormReactive } from '../../../shared' | ||
14 | |||
15 | @Component({ | ||
16 | selector: 'my-user-password', | ||
17 | templateUrl: './user-password.component.html', | ||
18 | styleUrls: [ './user-password.component.scss' ] | ||
19 | }) | ||
20 | export 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 @@ | |||
1 | import { Component, OnDestroy, OnInit } from '@angular/core' | 1 | import { Component, OnDestroy, OnInit, Input } 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' |
@@ -19,9 +19,12 @@ import { UserService } from '@app/shared' | |||
19 | export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | 19 | export 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' | |||
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' | ||
6 | import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' | 7 | import { CONFIG, RATES_LIMIT, sequelizeTypescript } from '../../../initializers' |
7 | import { Emailer } from '../../../lib/emailer' | 8 | import { Emailer } from '../../../lib/emailer' |
8 | import { Redis } from '../../../lib/redis' | 9 | 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 { | |||
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() |