diff options
Diffstat (limited to 'client/src/app/+admin/users')
12 files changed, 351 insertions, 39 deletions
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-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts index dd8e4efd5..137ecfcbd 100644 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/users/user-edit/user-create.component.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit } from '@angular/core' |
2 | import { Router } from '@angular/router' | 2 | import { Router } from '@angular/router' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { Notifier, ServerService } from '@app/core' |
4 | import { ServerService } from '../../../core' | ||
5 | import { UserCreate, UserRole } from '../../../../../../shared' | 4 | import { UserCreate, UserRole } from '../../../../../../shared' |
6 | import { UserEdit } from './user-edit' | 5 | import { UserEdit } from './user-edit' |
7 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -24,7 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
24 | protected configService: ConfigService, | 23 | protected configService: ConfigService, |
25 | private userValidatorsService: UserValidatorsService, | 24 | private userValidatorsService: UserValidatorsService, |
26 | private router: Router, | 25 | private router: Router, |
27 | private notificationsService: NotificationsService, | 26 | private notifier: Notifier, |
28 | private userService: UserService, | 27 | private userService: UserService, |
29 | private i18n: I18n | 28 | private i18n: I18n |
30 | ) { | 29 | ) { |
@@ -60,10 +59,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { | |||
60 | 59 | ||
61 | this.userService.addUser(userCreate).subscribe( | 60 | this.userService.addUser(userCreate).subscribe( |
62 | () => { | 61 | () => { |
63 | this.notificationsService.success( | 62 | this.notifier.success(this.i18n('User {{username}} created.', { username: userCreate.username })) |
64 | this.i18n('Success'), | ||
65 | this.i18n('User {{username}} created.', { username: userCreate.username }) | ||
66 | ) | ||
67 | this.router.navigate([ '/admin/users/list' ]) | 63 | this.router.navigate([ '/admin/users/list' ]) |
68 | }, | 64 | }, |
69 | 65 | ||
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..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 | |||
@@ -81,3 +81,17 @@ | |||
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="!isCreation()" class="danger-zone"> | ||
86 | <div class="account-title" i18n>Danger Zone</div> | ||
87 | |||
88 | <div class="form-group reset-password-email"> | ||
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> | ||
92 | |||
93 | <div class="form-group"> | ||
94 | <label i18n>Manually set the user password</label> | ||
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 6675f65cc..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 | |||
@@ -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,23 @@ 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 | } | ||
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 07b087b5b..649b35b0c 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -1,14 +1,14 @@ | |||
1 | import { ServerService } from '../../../core' | 1 | import { ServerService } from '../../../core' |
2 | import { FormReactive } from '../../../shared' | 2 | import { FormReactive } from '../../../shared' |
3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' | 3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' |
4 | import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/' | ||
5 | import { ConfigService } from '@app/+admin/config/shared/config.service' | 4 | import { ConfigService } from '@app/+admin/config/shared/config.service' |
6 | 5 | ||
7 | export abstract class UserEdit extends FormReactive { | 6 | export abstract class UserEdit extends FormReactive { |
8 | |||
9 | videoQuotaOptions: { value: string, label: string }[] = [] | 7 | videoQuotaOptions: { value: string, label: string }[] = [] |
10 | videoQuotaDailyOptions: { value: string, label: string }[] = [] | 8 | videoQuotaDailyOptions: { value: string, label: string }[] = [] |
11 | 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 | ||
11 | userId: number | ||
12 | 12 | ||
13 | protected abstract serverService: ServerService | 13 | protected abstract serverService: ServerService |
14 | protected abstract configService: ConfigService | 14 | protected abstract configService: ConfigService |
@@ -23,7 +23,9 @@ export abstract class UserEdit extends FormReactive { | |||
23 | } | 23 | } |
24 | 24 | ||
25 | computeQuotaWithTranscoding () { | 25 | computeQuotaWithTranscoding () { |
26 | const resolutions = this.serverService.getConfig().transcoding.enabledResolutions | 26 | const transcodingConfig = this.serverService.getConfig().transcoding |
27 | |||
28 | const resolutions = transcodingConfig.enabledResolutions | ||
27 | const higherResolution = VideoResolution.H_1080P | 29 | const higherResolution = VideoResolution.H_1080P |
28 | let multiplier = 0 | 30 | let multiplier = 0 |
29 | 31 | ||
@@ -31,9 +33,15 @@ export abstract class UserEdit extends FormReactive { | |||
31 | multiplier += resolution / higherResolution | 33 | multiplier += resolution / higherResolution |
32 | } | 34 | } |
33 | 35 | ||
36 | if (transcodingConfig.hls.enabled) multiplier *= 2 | ||
37 | |||
34 | return multiplier * parseInt(this.form.value['videoQuota'], 10) | 38 | return multiplier * parseInt(this.form.value['videoQuota'], 10) |
35 | } | 39 | } |
36 | 40 | ||
41 | resetPassword () { | ||
42 | return | ||
43 | } | ||
44 | |||
37 | protected buildQuotaOptions () { | 45 | protected buildQuotaOptions () { |
38 | // 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 |
39 | 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 new file mode 100644 index 000000000..a1e1f6216 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.html | |||
@@ -0,0 +1,21 @@ | |||
1 | <form role="form" (ngSubmit)="formValidated()" [formGroup]="form"> | ||
2 | <div class="form-group"> | ||
3 | |||
4 | <div class="input-group"> | ||
5 | <input id="password" [attr.type]="showPassword ? 'text' : 'password'" | ||
6 | formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }" | ||
7 | > | ||
8 | <div class="input-group-append"> | ||
9 | <button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button"> | ||
10 | <ng-container *ngIf="!showPassword" i18n>Show</ng-container> | ||
11 | <ng-container *ngIf="!!showPassword" i18n>Hide</ng-container> | ||
12 | </button> | ||
13 | </div> | ||
14 | </div> | ||
15 | <div *ngIf="formErrors.password" class="form-error"> | ||
16 | {{ formErrors.password }} | ||
17 | </div> | ||
18 | </div> | ||
19 | |||
20 | <input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid"> | ||
21 | </form> | ||
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..217d585af --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss | |||
@@ -0,0 +1,22 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | input:not([type=submit]):not([type=checkbox]) { | ||
5 | @include peertube-input-text(340px); | ||
6 | |||
7 | display: block; | ||
8 | border-top-right-radius: 0; | ||
9 | border-bottom-right-radius: 0; | ||
10 | border-right: none; | ||
11 | } | ||
12 | |||
13 | input[type=submit] { | ||
14 | @include peertube-button; | ||
15 | @include orange-button; | ||
16 | |||
17 | margin-top: 10px; | ||
18 | } | ||
19 | |||
20 | .input-group-append { | ||
21 | height: 30px; | ||
22 | } | ||
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..5b3040440 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts | |||
@@ -0,0 +1,64 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { ActivatedRoute, Router } from '@angular/router' | ||
3 | import { UserService } from '@app/shared/users/user.service' | ||
4 | import { Notifier } from '../../../core' | ||
5 | import { User, UserUpdate } from '../../../../../../shared' | ||
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | ||
7 | import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' | ||
8 | import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' | ||
9 | import { FormReactive } from '../../../shared' | ||
10 | |||
11 | @Component({ | ||
12 | selector: 'my-user-password', | ||
13 | templateUrl: './user-password.component.html', | ||
14 | styleUrls: [ './user-password.component.scss' ] | ||
15 | }) | ||
16 | export class UserPasswordComponent extends FormReactive implements OnInit { | ||
17 | error: string | ||
18 | username: string | ||
19 | showPassword = false | ||
20 | |||
21 | @Input() userId: number | ||
22 | |||
23 | constructor ( | ||
24 | protected formValidatorService: FormValidatorService, | ||
25 | private userValidatorsService: UserValidatorsService, | ||
26 | private route: ActivatedRoute, | ||
27 | private router: Router, | ||
28 | private notifier: Notifier, | ||
29 | private userService: UserService, | ||
30 | private i18n: I18n | ||
31 | ) { | ||
32 | super() | ||
33 | } | ||
34 | |||
35 | ngOnInit () { | ||
36 | this.buildForm({ | ||
37 | password: this.userValidatorsService.USER_PASSWORD | ||
38 | }) | ||
39 | } | ||
40 | |||
41 | formValidated () { | ||
42 | this.error = undefined | ||
43 | |||
44 | const userUpdate: UserUpdate = this.form.value | ||
45 | |||
46 | this.userService.updateUser(this.userId, userUpdate).subscribe( | ||
47 | () => { | ||
48 | this.notifier.success( | ||
49 | this.i18n('Password changed for user {{username}}.', { username: this.username }) | ||
50 | ) | ||
51 | }, | ||
52 | |||
53 | err => this.error = err.message | ||
54 | ) | ||
55 | } | ||
56 | |||
57 | togglePasswordVisibility () { | ||
58 | this.showPassword = !this.showPassword | ||
59 | } | ||
60 | |||
61 | getFormButtonTitle () { | ||
62 | return this.i18n('Update user password') | ||
63 | } | ||
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 cd3885a99..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,7 +1,7 @@ | |||
1 | import { Component, OnDestroy, OnInit } 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 { NotificationsService } from 'angular2-notifications' | 4 | import { Notifier } from '@app/core' |
5 | import { ServerService } from '../../../core' | 5 | import { ServerService } from '../../../core' |
6 | import { UserEdit } from './user-edit' | 6 | import { UserEdit } from './user-edit' |
7 | import { User, UserUpdate } from '../../../../../../shared' | 7 | import { User, UserUpdate } from '../../../../../../shared' |
@@ -19,6 +19,7 @@ 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 |
23 | 24 | ||
24 | private paramsSub: Subscription | 25 | private paramsSub: Subscription |
@@ -30,7 +31,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
30 | private userValidatorsService: UserValidatorsService, | 31 | private userValidatorsService: UserValidatorsService, |
31 | private route: ActivatedRoute, | 32 | private route: ActivatedRoute, |
32 | private router: Router, | 33 | private router: Router, |
33 | private notificationsService: NotificationsService, | 34 | private notifier: Notifier, |
34 | private userService: UserService, | 35 | private userService: UserService, |
35 | private i18n: I18n | 36 | private i18n: I18n |
36 | ) { | 37 | ) { |
@@ -73,10 +74,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
73 | 74 | ||
74 | this.userService.updateUser(this.userId, userUpdate).subscribe( | 75 | this.userService.updateUser(this.userId, userUpdate).subscribe( |
75 | () => { | 76 | () => { |
76 | this.notificationsService.success( | 77 | this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username })) |
77 | this.i18n('Success'), | ||
78 | this.i18n('User {{username}} updated.', { username: this.username }) | ||
79 | ) | ||
80 | this.router.navigate([ '/admin/users/list' ]) | 78 | this.router.navigate([ '/admin/users/list' ]) |
81 | }, | 79 | }, |
82 | 80 | ||
@@ -92,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
92 | return this.i18n('Update user') | 90 | return this.i18n('Update user') |
93 | } | 91 | } |
94 | 92 | ||
93 | resetPassword () { | ||
94 | this.userService.askResetPassword(this.userEmail).subscribe( | ||
95 | () => { | ||
96 | this.notifier.success( | ||
97 | this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username }) | ||
98 | ) | ||
99 | }, | ||
100 | |||
101 | err => this.error = err.message | ||
102 | ) | ||
103 | } | ||
104 | |||
95 | private onUserFetched (userJson: User) { | 105 | private onUserFetched (userJson: User) { |
96 | this.userId = userJson.id | 106 | this.userId = userJson.id |
97 | this.username = userJson.username | 107 | this.username = userJson.username |
108 | this.userEmail = userJson.email | ||
98 | 109 | ||
99 | this.form.patchValue({ | 110 | this.form.patchValue({ |
100 | email: userJson.email, | 111 | email: userJson.email, |
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index cca057ba1..69a4616a3 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html | |||
@@ -2,7 +2,7 @@ | |||
2 | <div i18n class="form-sub-title">Users list</div> | 2 | <div i18n class="form-sub-title">Users list</div> |
3 | 3 | ||
4 | <a class="add-button" routerLink="/admin/users/create"> | 4 | <a class="add-button" routerLink="/admin/users/create"> |
5 | <span class="icon icon-add"></span> | 5 | <my-global-icon iconName="add"></my-global-icon> |
6 | <ng-container i18n>Create user</ng-container> | 6 | <ng-container i18n>Create user</ng-container> |
7 | </a> | 7 | </a> |
8 | </div> | 8 | </div> |
@@ -10,9 +10,32 @@ | |||
10 | <p-table | 10 | <p-table |
11 | [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" | 11 | [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" |
12 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" | 12 | [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id" |
13 | [(selection)]="selectedUsers" | ||
13 | > | 14 | > |
15 | <ng-template pTemplate="caption"> | ||
16 | <div class="caption"> | ||
17 | <div> | ||
18 | <my-action-dropdown | ||
19 | *ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange" | ||
20 | [actions]="bulkUserActions" [entry]="selectedUsers" | ||
21 | > | ||
22 | </my-action-dropdown> | ||
23 | </div> | ||
24 | |||
25 | <div> | ||
26 | <input | ||
27 | type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..." | ||
28 | (keyup)="onSearch($event.target.value)" | ||
29 | > | ||
30 | </div> | ||
31 | </div> | ||
32 | </ng-template> | ||
33 | |||
14 | <ng-template pTemplate="header"> | 34 | <ng-template pTemplate="header"> |
15 | <tr> | 35 | <tr> |
36 | <th style="width: 40px"> | ||
37 | <p-tableHeaderCheckbox></p-tableHeaderCheckbox> | ||
38 | </th> | ||
16 | <th style="width: 40px"></th> | 39 | <th style="width: 40px"></th> |
17 | <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> | 40 | <th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th> |
18 | <th i18n>Email</th> | 41 | <th i18n>Email</th> |
@@ -25,22 +48,42 @@ | |||
25 | 48 | ||
26 | <ng-template pTemplate="body" let-expanded="expanded" let-user> | 49 | <ng-template pTemplate="body" let-expanded="expanded" let-user> |
27 | 50 | ||
28 | <tr [ngClass]="{ banned: user.blocked }"> | 51 | <tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }"> |
52 | <td> | ||
53 | <p-tableCheckbox [value]="user"></p-tableCheckbox> | ||
54 | </td> | ||
55 | |||
29 | <td> | 56 | <td> |
30 | <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> | 57 | <span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user"> |
31 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> | 58 | <i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i> |
32 | </span> | 59 | </span> |
33 | </td> | 60 | </td> |
61 | |||
34 | <td> | 62 | <td> |
35 | {{ user.username }} | 63 | <a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]"> |
36 | <span *ngIf="user.blocked" class="banned-info">(banned)</span> | 64 | {{ user.username }} |
65 | <span i18n *ngIf="user.blocked" class="banned-info">(banned)</span> | ||
66 | </a> | ||
37 | </td> | 67 | </td> |
38 | <td>{{ user.email }}</td> | 68 | |
69 | <td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td> | ||
70 | |||
71 | <ng-template #emailWithVerificationStatus> | ||
72 | <td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login"> | ||
73 | <em>? {{ user.email }}</em> | ||
74 | </td> | ||
75 | <ng-template #emailVerifiedNotFalse> | ||
76 | <td i18n-title title="User's email is verified / User can login without email verification"> | ||
77 | ✓ {{ user.email }} | ||
78 | </td> | ||
79 | </ng-template> | ||
80 | </ng-template> | ||
81 | |||
39 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> | 82 | <td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td> |
40 | <td>{{ user.roleLabel }}</td> | 83 | <td>{{ user.roleLabel }}</td> |
41 | <td>{{ user.createdAt }}</td> | 84 | <td>{{ user.createdAt }}</td> |
42 | <td class="action-cell"> | 85 | <td class="action-cell"> |
43 | <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> | 86 | <my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()"> |
44 | </my-user-moderation-dropdown> | 87 | </my-user-moderation-dropdown> |
45 | </td> | 88 | </td> |
46 | </tr> | 89 | </tr> |
@@ -56,3 +99,4 @@ | |||
56 | </ng-template> | 99 | </ng-template> |
57 | </p-table> | 100 | </p-table> |
58 | 101 | ||
102 | <my-user-ban-modal #userBanModal (userBanned)="onUserChanged()"></my-user-ban-modal> | ||
diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index 47291918d..5274be01c 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss | |||
@@ -2,7 +2,7 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | 3 | ||
4 | .add-button { | 4 | .add-button { |
5 | @include create-button('../../../../assets/images/global/add.svg'); | 5 | @include create-button; |
6 | } | 6 | } |
7 | 7 | ||
8 | tr.banned { | 8 | tr.banned { |
@@ -15,4 +15,12 @@ tr.banned { | |||
15 | 15 | ||
16 | .ban-reason-label { | 16 | .ban-reason-label { |
17 | font-weight: $font-semibold; | 17 | font-weight: $font-semibold; |
18 | } \ No newline at end of file | 18 | } |
19 | |||
20 | .caption { | ||
21 | justify-content: space-between; | ||
22 | |||
23 | input { | ||
24 | @include peertube-input-text(250px); | ||
25 | } | ||
26 | } | ||
diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index dee3ed643..66ab796f9 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts | |||
@@ -1,10 +1,12 @@ | |||
1 | import { Component, OnInit } from '@angular/core' | 1 | import { Component, OnInit, ViewChild } from '@angular/core' |
2 | import { NotificationsService } from 'angular2-notifications' | 2 | import { Notifier } from '@app/core' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { ConfirmService } from '../../../core' | 4 | import { ConfirmService, ServerService } from '../../../core' |
5 | import { RestPagination, RestTable, UserService } from '../../../shared' | 5 | import { RestPagination, RestTable, UserService } from '../../../shared' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
7 | import { User } from '../../../../../../shared' | 7 | import { User } from '../../../../../../shared' |
8 | import { UserBanModalComponent } from '@app/shared/moderation' | ||
9 | import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' | ||
8 | 10 | ||
9 | @Component({ | 11 | @Component({ |
10 | selector: 'my-user-list', | 12 | selector: 'my-user-list', |
@@ -12,38 +14,139 @@ import { User } from '../../../../../../shared' | |||
12 | styleUrls: [ './user-list.component.scss' ] | 14 | styleUrls: [ './user-list.component.scss' ] |
13 | }) | 15 | }) |
14 | export class UserListComponent extends RestTable implements OnInit { | 16 | export class UserListComponent extends RestTable implements OnInit { |
17 | @ViewChild('userBanModal') userBanModal: UserBanModalComponent | ||
18 | |||
15 | users: User[] = [] | 19 | users: User[] = [] |
16 | totalRecords = 0 | 20 | totalRecords = 0 |
17 | rowsPerPage = 10 | 21 | rowsPerPage = 10 |
18 | sort: SortMeta = { field: 'createdAt', order: 1 } | 22 | sort: SortMeta = { field: 'createdAt', order: 1 } |
19 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } | 23 | pagination: RestPagination = { count: this.rowsPerPage, start: 0 } |
20 | 24 | ||
25 | selectedUsers: User[] = [] | ||
26 | bulkUserActions: DropdownAction<User[]>[] = [] | ||
27 | |||
21 | constructor ( | 28 | constructor ( |
22 | private notificationsService: NotificationsService, | 29 | private notifier: Notifier, |
23 | private confirmService: ConfirmService, | 30 | private confirmService: ConfirmService, |
31 | private serverService: ServerService, | ||
24 | private userService: UserService, | 32 | private userService: UserService, |
25 | private i18n: I18n | 33 | private i18n: I18n |
26 | ) { | 34 | ) { |
27 | super() | 35 | super() |
28 | } | 36 | } |
29 | 37 | ||
38 | get requiresEmailVerification () { | ||
39 | return this.serverService.getConfig().signup.requiresEmailVerification | ||
40 | } | ||
41 | |||
30 | ngOnInit () { | 42 | ngOnInit () { |
31 | this.loadSort() | 43 | this.initialize() |
44 | |||
45 | this.bulkUserActions = [ | ||
46 | { | ||
47 | label: this.i18n('Delete'), | ||
48 | handler: users => this.removeUsers(users) | ||
49 | }, | ||
50 | { | ||
51 | label: this.i18n('Ban'), | ||
52 | handler: users => this.openBanUserModal(users), | ||
53 | isDisplayed: users => users.every(u => u.blocked === false) | ||
54 | }, | ||
55 | { | ||
56 | label: this.i18n('Unban'), | ||
57 | handler: users => this.unbanUsers(users), | ||
58 | isDisplayed: users => users.every(u => u.blocked === true) | ||
59 | }, | ||
60 | { | ||
61 | label: this.i18n('Set Email as Verified'), | ||
62 | handler: users => this.setEmailsAsVerified(users), | ||
63 | isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false) | ||
64 | } | ||
65 | ] | ||
66 | } | ||
67 | |||
68 | openBanUserModal (users: User[]) { | ||
69 | for (const user of users) { | ||
70 | if (user.username === 'root') { | ||
71 | this.notifier.error(this.i18n('You cannot ban root.')) | ||
72 | return | ||
73 | } | ||
74 | } | ||
75 | |||
76 | this.userBanModal.openModal(users) | ||
32 | } | 77 | } |
33 | 78 | ||
34 | onUserChanged () { | 79 | onUserChanged () { |
35 | this.loadData() | 80 | this.loadData() |
36 | } | 81 | } |
37 | 82 | ||
83 | async unbanUsers (users: User[]) { | ||
84 | const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length }) | ||
85 | |||
86 | const res = await this.confirmService.confirm(message, this.i18n('Unban')) | ||
87 | if (res === false) return | ||
88 | |||
89 | this.userService.unbanUsers(users) | ||
90 | .subscribe( | ||
91 | () => { | ||
92 | const message = this.i18n('{{num}} users unbanned.', { num: users.length }) | ||
93 | |||
94 | this.notifier.success(message) | ||
95 | this.loadData() | ||
96 | }, | ||
97 | |||
98 | err => this.notifier.error(err.message) | ||
99 | ) | ||
100 | } | ||
101 | |||
102 | async removeUsers (users: User[]) { | ||
103 | for (const user of users) { | ||
104 | if (user.username === 'root') { | ||
105 | this.notifier.error(this.i18n('You cannot delete root.')) | ||
106 | return | ||
107 | } | ||
108 | } | ||
109 | |||
110 | const message = this.i18n('If you remove these users, you will not be able to create others with the same username!') | ||
111 | const res = await this.confirmService.confirm(message, this.i18n('Delete')) | ||
112 | if (res === false) return | ||
113 | |||
114 | this.userService.removeUser(users).subscribe( | ||
115 | () => { | ||
116 | this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length })) | ||
117 | this.loadData() | ||
118 | }, | ||
119 | |||
120 | err => this.notifier.error(err.message) | ||
121 | ) | ||
122 | } | ||
123 | |||
124 | async setEmailsAsVerified (users: User[]) { | ||
125 | this.userService.updateUsers(users, { emailVerified: true }).subscribe( | ||
126 | () => { | ||
127 | this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length })) | ||
128 | this.loadData() | ||
129 | }, | ||
130 | |||
131 | err => this.notifier.error(err.message) | ||
132 | ) | ||
133 | } | ||
134 | |||
135 | isInSelectionMode () { | ||
136 | return this.selectedUsers.length !== 0 | ||
137 | } | ||
138 | |||
38 | protected loadData () { | 139 | protected loadData () { |
39 | this.userService.getUsers(this.pagination, this.sort) | 140 | this.selectedUsers = [] |
40 | .subscribe( | 141 | |
41 | resultList => { | 142 | this.userService.getUsers(this.pagination, this.sort, this.search) |
42 | this.users = resultList.data | 143 | .subscribe( |
43 | this.totalRecords = resultList.total | 144 | resultList => { |
44 | }, | 145 | this.users = resultList.data |
45 | 146 | this.totalRecords = resultList.total | |
46 | err => this.notificationsService.error(this.i18n('Error'), err.message) | 147 | }, |
47 | ) | 148 | |
149 | err => this.notifier.error(err.message) | ||
150 | ) | ||
48 | } | 151 | } |
49 | } | 152 | } |