From 00004f7f6b966a975498612117212b5373f4103c Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 27 Oct 2021 09:36:37 +0200 Subject: Put admin users in overview tab --- client/src/app/+admin/admin-routing.module.ts | 5 +- client/src/app/+admin/admin.component.ts | 15 +- client/src/app/+admin/admin.module.ts | 3 +- client/src/app/+admin/overview/index.ts | 2 + client/src/app/+admin/overview/overview.routes.ts | 6 + client/src/app/+admin/overview/users/index.ts | 3 + .../app/+admin/overview/users/user-edit/index.ts | 3 + .../users/user-edit/user-create.component.ts | 98 ++++++++ .../users/user-edit/user-edit.component.html | 237 ++++++++++++++++++++ .../users/user-edit/user-edit.component.scss | 76 +++++++ .../+admin/overview/users/user-edit/user-edit.ts | 102 +++++++++ .../users/user-edit/user-password.component.html | 21 ++ .../users/user-edit/user-password.component.scss | 23 ++ .../users/user-edit/user-password.component.ts | 55 +++++ .../users/user-edit/user-update.component.ts | 139 ++++++++++++ .../app/+admin/overview/users/user-list/index.ts | 1 + .../users/user-list/user-list.component.html | 168 ++++++++++++++ .../users/user-list/user-list.component.scss | 65 ++++++ .../users/user-list/user-list.component.ts | 246 +++++++++++++++++++++ .../src/app/+admin/overview/users/users.routes.ts | 49 ++++ client/src/app/+admin/users/index.ts | 4 - client/src/app/+admin/users/user-edit/index.ts | 3 - .../users/user-edit/user-create.component.ts | 98 -------- .../users/user-edit/user-edit.component.html | 237 -------------------- .../users/user-edit/user-edit.component.scss | 76 ------- client/src/app/+admin/users/user-edit/user-edit.ts | 102 --------- .../users/user-edit/user-password.component.html | 21 -- .../users/user-edit/user-password.component.scss | 23 -- .../users/user-edit/user-password.component.ts | 55 ----- .../users/user-edit/user-update.component.ts | 139 ------------ client/src/app/+admin/users/user-list/index.ts | 1 - .../users/user-list/user-list.component.html | 163 -------------- .../users/user-list/user-list.component.scss | 65 ------ .../+admin/users/user-list/user-list.component.ts | 246 --------------------- client/src/app/+admin/users/users.component.ts | 7 - client/src/app/+admin/users/users.routes.ts | 51 ----- 36 files changed, 1312 insertions(+), 1296 deletions(-) create mode 100644 client/src/app/+admin/overview/index.ts create mode 100644 client/src/app/+admin/overview/overview.routes.ts create mode 100644 client/src/app/+admin/overview/users/index.ts create mode 100644 client/src/app/+admin/overview/users/user-edit/index.ts create mode 100644 client/src/app/+admin/overview/users/user-edit/user-create.component.ts create mode 100644 client/src/app/+admin/overview/users/user-edit/user-edit.component.html create mode 100644 client/src/app/+admin/overview/users/user-edit/user-edit.component.scss create mode 100644 client/src/app/+admin/overview/users/user-edit/user-edit.ts create mode 100644 client/src/app/+admin/overview/users/user-edit/user-password.component.html create mode 100644 client/src/app/+admin/overview/users/user-edit/user-password.component.scss create mode 100644 client/src/app/+admin/overview/users/user-edit/user-password.component.ts create mode 100644 client/src/app/+admin/overview/users/user-edit/user-update.component.ts create mode 100644 client/src/app/+admin/overview/users/user-list/index.ts create mode 100644 client/src/app/+admin/overview/users/user-list/user-list.component.html create mode 100644 client/src/app/+admin/overview/users/user-list/user-list.component.scss create mode 100644 client/src/app/+admin/overview/users/user-list/user-list.component.ts create mode 100644 client/src/app/+admin/overview/users/users.routes.ts delete mode 100644 client/src/app/+admin/users/index.ts delete mode 100644 client/src/app/+admin/users/user-edit/index.ts delete mode 100644 client/src/app/+admin/users/user-edit/user-create.component.ts delete mode 100644 client/src/app/+admin/users/user-edit/user-edit.component.html delete mode 100644 client/src/app/+admin/users/user-edit/user-edit.component.scss delete mode 100644 client/src/app/+admin/users/user-edit/user-edit.ts delete mode 100644 client/src/app/+admin/users/user-edit/user-password.component.html delete mode 100644 client/src/app/+admin/users/user-edit/user-password.component.scss delete mode 100644 client/src/app/+admin/users/user-edit/user-password.component.ts delete mode 100644 client/src/app/+admin/users/user-edit/user-update.component.ts delete mode 100644 client/src/app/+admin/users/user-list/index.ts delete mode 100644 client/src/app/+admin/users/user-list/user-list.component.html delete mode 100644 client/src/app/+admin/users/user-list/user-list.component.scss delete mode 100644 client/src/app/+admin/users/user-list/user-list.component.ts delete mode 100644 client/src/app/+admin/users/users.component.ts delete mode 100644 client/src/app/+admin/users/users.routes.ts (limited to 'client/src/app/+admin') diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts index d029661d3..bd08b8287 100644 --- a/client/src/app/+admin/admin-routing.module.ts +++ b/client/src/app/+admin/admin-routing.module.ts @@ -6,7 +6,7 @@ import { PluginsRoutes } from '@app/+admin/plugins/plugins.routes' import { SystemRoutes } from '@app/+admin/system' import { AdminComponent } from './admin.component' import { FollowsRoutes } from './follows' -import { UsersRoutes } from './users' +import { OverviewRoutes } from './overview' const adminRoutes: Routes = [ { @@ -18,8 +18,9 @@ const adminRoutes: Routes = [ redirectTo: 'users', pathMatch: 'full' }, + ...FollowsRoutes, - ...UsersRoutes, + ...OverviewRoutes, ...ModerationRoutes, ...SystemRoutes, ...ConfigRoutes, diff --git a/client/src/app/+admin/admin.component.ts b/client/src/app/+admin/admin.component.ts index 15739f8d3..27d5e0a10 100644 --- a/client/src/app/+admin/admin.component.ts +++ b/client/src/app/+admin/admin.component.ts @@ -31,8 +31,21 @@ export class AdminComponent implements OnInit { } private buildOverviewItems () { + const overviewItems: TopMenuDropdownParam = { + label: $localize`Overview`, + children: [] + } + if (this.hasUsersRight()) { - this.menuEntries.push({ label: $localize`Users`, routerLink: '/admin/users' }) + overviewItems.children.push({ + label: $localize`Users`, + routerLink: '/admin/users', + iconName: 'user' + }) + } + + if (overviewItems.children.length !== 0) { + this.menuEntries.push(overviewItems) } } diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 1ea7b9784..a2bd88880 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -33,6 +33,7 @@ import { AbuseListComponent, VideoBlockListComponent } from './moderation' import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from './moderation/instance-blocklist' import { ModerationComponent } from './moderation/moderation.component' import { VideoCommentListComponent } from './moderation/video-comment-list' +import { UserCreateComponent, UserListComponent, UserPasswordComponent, UserUpdateComponent } from './overview' import { PluginListInstalledComponent } from './plugins/plugin-list-installed/plugin-list-installed.component' import { PluginSearchComponent } from './plugins/plugin-search/plugin-search.component' import { PluginShowInstalledComponent } from './plugins/plugin-show-installed/plugin-show-installed.component' @@ -41,7 +42,6 @@ import { PluginApiService } from './plugins/shared/plugin-api.service' import { JobService, LogsComponent, LogsService, SystemComponent } from './system' import { DebugComponent, DebugService } from './system/debug' import { JobsComponent } from './system/jobs/jobs.component' -import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersComponent, UserUpdateComponent } from './users' @NgModule({ imports: [ @@ -73,7 +73,6 @@ import { UserCreateComponent, UserListComponent, UserPasswordComponent, UsersCom VideoRedundanciesListComponent, VideoRedundancyInformationComponent, - UsersComponent, UserCreateComponent, UserUpdateComponent, UserPasswordComponent, diff --git a/client/src/app/+admin/overview/index.ts b/client/src/app/+admin/overview/index.ts new file mode 100644 index 000000000..b71a6a45f --- /dev/null +++ b/client/src/app/+admin/overview/index.ts @@ -0,0 +1,2 @@ +export * from './users' +export * from './overview.routes' diff --git a/client/src/app/+admin/overview/overview.routes.ts b/client/src/app/+admin/overview/overview.routes.ts new file mode 100644 index 000000000..cb5986072 --- /dev/null +++ b/client/src/app/+admin/overview/overview.routes.ts @@ -0,0 +1,6 @@ +import { Routes } from '@angular/router' +import { UsersRoutes } from './users' + +export const OverviewRoutes: Routes = [ + ...UsersRoutes +] diff --git a/client/src/app/+admin/overview/users/index.ts b/client/src/app/+admin/overview/users/index.ts new file mode 100644 index 000000000..6f15ab578 --- /dev/null +++ b/client/src/app/+admin/overview/users/index.ts @@ -0,0 +1,3 @@ +export * from './user-edit' +export * from './user-list' +export * from './users.routes' diff --git a/client/src/app/+admin/overview/users/user-edit/index.ts b/client/src/app/+admin/overview/users/user-edit/index.ts new file mode 100644 index 000000000..ec734ef92 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/index.ts @@ -0,0 +1,3 @@ +export * from './user-create.component' +export * from './user-update.component' +export * from './user-password.component' diff --git a/client/src/app/+admin/overview/users/user-edit/user-create.component.ts b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts new file mode 100644 index 000000000..b61b22fd0 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-create.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit } from '@angular/core' +import { Router } from '@angular/router' +import { ConfigService } from '@app/+admin/config/shared/config.service' +import { AuthService, Notifier, ScreenService, ServerService, UserService } from '@app/core' +import { + USER_CHANNEL_NAME_VALIDATOR, + USER_EMAIL_VALIDATOR, + USER_PASSWORD_OPTIONAL_VALIDATOR, + USER_PASSWORD_VALIDATOR, + USER_ROLE_VALIDATOR, + USER_USERNAME_VALIDATOR, + USER_VIDEO_QUOTA_DAILY_VALIDATOR, + USER_VIDEO_QUOTA_VALIDATOR +} from '@app/shared/form-validators/user-validators' +import { FormValidatorService } from '@app/shared/shared-forms' +import { UserCreate, UserRole } from '@shared/models' +import { UserEdit } from './user-edit' + +@Component({ + selector: 'my-user-create', + templateUrl: './user-edit.component.html', + styleUrls: [ './user-edit.component.scss' ] +}) +export class UserCreateComponent extends UserEdit implements OnInit { + error: string + + constructor ( + protected serverService: ServerService, + protected formValidatorService: FormValidatorService, + protected configService: ConfigService, + protected screenService: ScreenService, + protected auth: AuthService, + private router: Router, + private notifier: Notifier, + private userService: UserService + ) { + super() + + this.buildQuotaOptions() + } + + ngOnInit () { + super.ngOnInit() + + const defaultValues = { + role: UserRole.USER.toString(), + videoQuota: -1, + videoQuotaDaily: -1 + } + + this.buildForm({ + username: USER_USERNAME_VALIDATOR, + channelName: USER_CHANNEL_NAME_VALIDATOR, + email: USER_EMAIL_VALIDATOR, + password: this.isPasswordOptional() ? USER_PASSWORD_OPTIONAL_VALIDATOR : USER_PASSWORD_VALIDATOR, + role: USER_ROLE_VALIDATOR, + videoQuota: USER_VIDEO_QUOTA_VALIDATOR, + videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR, + byPassAutoBlock: null + }, defaultValues) + } + + formValidated () { + this.error = undefined + + const userCreate: UserCreate = this.form.value + + userCreate.adminFlags = this.buildAdminFlags(this.form.value) + + // A select in HTML is always mapped as a string, we convert it to number + userCreate.videoQuota = parseInt(this.form.value['videoQuota'], 10) + userCreate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) + + this.userService.addUser(userCreate) + .subscribe({ + next: () => { + this.notifier.success($localize`User ${userCreate.username} created.`) + this.router.navigate([ '/admin/users/list' ]) + }, + + error: err => { + this.error = err.message + } + }) + } + + isCreation () { + return true + } + + isPasswordOptional () { + return this.serverConfig.email.enabled + } + + getFormButtonTitle () { + return $localize`Create user` + } +} diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.html b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html new file mode 100644 index 000000000..772ebf272 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.html @@ -0,0 +1,237 @@ + + + + + + +
+
+ +
+ +
+
+ +
{{ error }}
+ +
+
+
+ + +
+ +
+ +
+
+ + +
+ {{ formErrors.username }} +
+
+ +
+ + +
+ {{ formErrors.channelName }} +
+
+ +
+ + +
+ {{ formErrors.email }} +
+
+ +
+ + + + + If you leave the password empty, an email will be sent to the user. + + + + + + +
+ {{ formErrors.password }} +
+
+ +
+ +
+ +
+ +
+ {{ formErrors.role }} +
+
+ +
+ + + + +
+ Transcoding is enabled. The video quota only takes into account original video size.
+ At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}. +
+ +
+ {{ formErrors.videoQuota }} +
+
+ +
+ + + + +
+ {{ formErrors.videoQuotaDaily }} +
+
+ +
+ + +
+ +
+
+ +
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+
+
+ +
+ +
+ +
+
+ + +
+ +
+ + +
+
+ +
+
diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss new file mode 100644 index 000000000..d7932154b --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.component.scss @@ -0,0 +1,76 @@ +@use 'sass:math'; +@use '_variables' as *; +@use '_mixins' as *; + +$form-base-input-width: 340px; + +label { + font-weight: $font-regular; + font-size: 100%; +} + +.account-title { + @include settings-big-title; + + &.account-title-danger { + color: lighten($color: #c54130, $amount: 10); + } +} + +input:not([type=submit]) { + @include peertube-input-text($form-base-input-width); + display: block; +} + +my-input-toggle-hidden { + @include responsive-width($form-base-input-width); + + display: block; +} + +.peertube-select-container { + @include peertube-select-container($form-base-input-width); +} + +my-select-custom-value { + @include responsive-width($form-base-input-width); + + display: block; +} + +input[type=submit], +button { + @include peertube-button; + @include orange-button; + + margin-top: 10px; +} + +.transcoding-information { + margin-top: 5px; + font-size: 11px; +} + +.danger-zone { + .reset-password-email { + margin-bottom: 30px; + + button { + @include peertube-button; + @include danger-button; + @include disable-outline; + + display: block; + margin-top: 0; + } + } +} + +.breadcrumb { + @include breadcrumb; +} + +.dashboard { + @include dashboard; + max-width: 900px; +} diff --git a/client/src/app/+admin/overview/users/user-edit/user-edit.ts b/client/src/app/+admin/overview/users/user-edit/user-edit.ts new file mode 100644 index 000000000..069b62a53 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-edit.ts @@ -0,0 +1,102 @@ +import { Directive, OnInit } from '@angular/core' +import { ConfigService } from '@app/+admin/config/shared/config.service' +import { AuthService, ScreenService, ServerService, User } from '@app/core' +import { FormReactive } from '@app/shared/shared-forms' +import { USER_ROLE_LABELS } from '@shared/core-utils/users' +import { HTMLServerConfig, UserAdminFlag, UserRole, VideoResolution } from '@shared/models' +import { SelectOptionsItem } from '../../../../../types/select-options-item.model' + +@Directive() +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export abstract class UserEdit extends FormReactive implements OnInit { + videoQuotaOptions: SelectOptionsItem[] = [] + videoQuotaDailyOptions: SelectOptionsItem[] = [] + username: string + user: User + + roles: { value: string, label: string }[] = [] + + protected serverConfig: HTMLServerConfig + + protected abstract serverService: ServerService + protected abstract configService: ConfigService + protected abstract screenService: ScreenService + protected abstract auth: AuthService + abstract isCreation (): boolean + abstract getFormButtonTitle (): string + + ngOnInit (): void { + this.serverConfig = this.serverService.getHTMLConfig() + + this.buildRoles() + } + + get subscribersCount () { + const forAccount = this.user + ? this.user.account.followersCount + : 0 + const forChannels = this.user + ? this.user.videoChannels.map(c => c.followersCount).reduce((a, b) => a + b, 0) + : 0 + return forAccount + forChannels + } + + getAuthPlugins () { + return this.serverConfig.plugin.registeredIdAndPassAuths.map(p => p.npmName) + .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName)) + } + + isInBigView () { + return this.screenService.getWindowInnerWidth() > 1600 + } + + buildRoles () { + const authUser = this.auth.getUser() + + if (authUser.role === UserRole.ADMINISTRATOR) { + this.roles = Object.keys(USER_ROLE_LABELS) + .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) + return + } + + this.roles = [ + { value: UserRole.USER.toString(), label: USER_ROLE_LABELS[UserRole.USER] } + ] + } + + isTranscodingInformationDisplayed () { + const formVideoQuota = parseInt(this.form.value['videoQuota'], 10) + + return this.serverConfig.transcoding.enabledResolutions.length !== 0 && + formVideoQuota > 0 + } + + computeQuotaWithTranscoding () { + const transcodingConfig = this.serverConfig.transcoding + + const resolutions = transcodingConfig.enabledResolutions + const higherResolution = VideoResolution.H_4K + let multiplier = 0 + + for (const resolution of resolutions) { + multiplier += resolution / higherResolution + } + + if (transcodingConfig.hls.enabled) multiplier *= 2 + + return multiplier * parseInt(this.form.value['videoQuota'], 10) + } + + resetPassword () { + return + } + + protected buildAdminFlags (formValue: any) { + return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE + } + + protected buildQuotaOptions () { + this.videoQuotaOptions = this.configService.videoQuotaOptions + this.videoQuotaDailyOptions = this.configService.videoQuotaDailyOptions + } +} diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.html b/client/src/app/+admin/overview/users/user-edit/user-password.component.html new file mode 100644 index 000000000..1238d1839 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.html @@ -0,0 +1,21 @@ +
+
+ +
+ +
+ +
+
+
+ {{ formErrors.password }} +
+
+ + +
diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.scss b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss new file mode 100644 index 000000000..acb680682 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.scss @@ -0,0 +1,23 @@ +@use '_variables' as *; +@use '_mixins' as *; + +input:not([type=submit]):not([type=checkbox]) { + @include peertube-input-text(340px); + + display: block; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 0; +} + +input[type=submit] { + @include peertube-button; + @include danger-button; + @include disable-outline; + + margin-top: 10px; +} + +.input-group-append { + height: 30px; +} diff --git a/client/src/app/+admin/overview/users/user-edit/user-password.component.ts b/client/src/app/+admin/overview/users/user-edit/user-password.component.ts new file mode 100644 index 000000000..42bf20de1 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-password.component.ts @@ -0,0 +1,55 @@ +import { Component, Input, OnInit } from '@angular/core' +import { Notifier, UserService } from '@app/core' +import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' +import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { UserUpdate } from '@shared/models' + +@Component({ + selector: 'my-user-password', + templateUrl: './user-password.component.html', + styleUrls: [ './user-password.component.scss' ] +}) +export class UserPasswordComponent extends FormReactive implements OnInit { + error: string + username: string + showPassword = false + + @Input() userId: number + + constructor ( + protected formValidatorService: FormValidatorService, + private notifier: Notifier, + private userService: UserService + ) { + super() + } + + ngOnInit () { + this.buildForm({ + password: USER_PASSWORD_VALIDATOR + }) + } + + formValidated () { + this.error = undefined + + const userUpdate: UserUpdate = this.form.value + + this.userService.updateUser(this.userId, userUpdate) + .subscribe({ + next: () => this.notifier.success($localize`Password changed for user ${this.username}.`), + + error: err => { + this.error = err.message + } + }) + } + + togglePasswordVisibility () { + this.showPassword = !this.showPassword + } + + getFormButtonTitle () { + return $localize`Update user password` + } +} diff --git a/client/src/app/+admin/overview/users/user-edit/user-update.component.ts b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts new file mode 100644 index 000000000..42599a17e --- /dev/null +++ b/client/src/app/+admin/overview/users/user-edit/user-update.component.ts @@ -0,0 +1,139 @@ +import { Subscription } from 'rxjs' +import { Component, OnDestroy, OnInit } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { ConfigService } from '@app/+admin/config/shared/config.service' +import { AuthService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core' +import { + USER_EMAIL_VALIDATOR, + USER_ROLE_VALIDATOR, + USER_VIDEO_QUOTA_DAILY_VALIDATOR, + USER_VIDEO_QUOTA_VALIDATOR +} from '@app/shared/form-validators/user-validators' +import { FormValidatorService } from '@app/shared/shared-forms' +import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' +import { UserEdit } from './user-edit' + +@Component({ + selector: 'my-user-update', + templateUrl: './user-edit.component.html', + styleUrls: [ './user-edit.component.scss' ] +}) +export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { + error: string + + private paramsSub: Subscription + + constructor ( + protected formValidatorService: FormValidatorService, + protected serverService: ServerService, + protected configService: ConfigService, + protected screenService: ScreenService, + protected auth: AuthService, + private route: ActivatedRoute, + private router: Router, + private notifier: Notifier, + private userService: UserService + ) { + super() + + this.buildQuotaOptions() + } + + ngOnInit () { + super.ngOnInit() + + const defaultValues = { + role: UserRole.USER.toString(), + videoQuota: '-1', + videoQuotaDaily: '-1' + } + + this.buildForm({ + email: USER_EMAIL_VALIDATOR, + role: USER_ROLE_VALIDATOR, + videoQuota: USER_VIDEO_QUOTA_VALIDATOR, + videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR, + byPassAutoBlock: null, + pluginAuth: null + }, defaultValues) + + this.paramsSub = this.route.params.subscribe(routeParams => { + const userId = routeParams['id'] + this.userService.getUser(userId, true) + .subscribe({ + next: user => this.onUserFetched(user), + + error: err => { + this.error = err.message + } + }) + }) + } + + ngOnDestroy () { + this.paramsSub.unsubscribe() + } + + formValidated () { + this.error = undefined + + const userUpdate: UserUpdate = this.form.value + userUpdate.adminFlags = this.buildAdminFlags(this.form.value) + + // A select in HTML is always mapped as a string, we convert it to number + userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) + userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) + + if (userUpdate.pluginAuth === 'null') userUpdate.pluginAuth = null + + this.userService.updateUser(this.user.id, userUpdate) + .subscribe({ + next: () => { + this.notifier.success($localize`User ${this.user.username} updated.`) + this.router.navigate([ '/admin/users/list' ]) + }, + + error: err => { + this.error = err.message + } + }) + } + + isCreation () { + return false + } + + isPasswordOptional () { + return false + } + + getFormButtonTitle () { + return $localize`Update user` + } + + resetPassword () { + this.userService.askResetPassword(this.user.email) + .subscribe({ + next: () => { + this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`) + }, + + error: err => { + this.error = err.message + } + }) + } + + private onUserFetched (userJson: UserType) { + this.user = new User(userJson) + + this.form.patchValue({ + email: userJson.email, + role: userJson.role.toString(), + videoQuota: userJson.videoQuota, + videoQuotaDaily: userJson.videoQuotaDaily, + pluginAuth: userJson.pluginAuth, + byPassAutoBlock: userJson.adminFlags & UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST + }) + } +} diff --git a/client/src/app/+admin/overview/users/user-list/index.ts b/client/src/app/+admin/overview/users/user-list/index.ts new file mode 100644 index 000000000..1826a4abe --- /dev/null +++ b/client/src/app/+admin/overview/users/user-list/index.ts @@ -0,0 +1 @@ +export * from './user-list.component' diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.html b/client/src/app/+admin/overview/users/user-list/user-list.component.html new file mode 100644 index 000000000..a96dbd7f8 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.html @@ -0,0 +1,168 @@ +

+ + Users +

+ + + +
+ + +
+ +
+ +
+
+ + + + + + + + +
+ + + +
+ + {{ getColumn('username').label }} + {{ getColumn('email').label }} + {{ getColumn('quota').label }} + {{ getColumn('quotaDaily').label }} + {{ getColumn('role').label }} + {{ getColumn('pluginAuth').label }} + {{ getColumn('createdAt').label }} + {{ getColumn('lastLoginDate').label }} + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +
+ {{ user.account.displayName }} + {{ user.username }} +
+
+
+ + + + + {{ user.email }} + + + + + + ? {{ user.email }} + + + + ✓ {{ user.email }} + + + + + +
+
+
+ {{ user.videoQuotaUsed }} + {{ user.videoQuota }} +
+ + + +
+
+
+ {{ user.videoQuotaUsedDaily }} + {{ user.videoQuotaDaily }} +
+ + + + {{ user.roleLabel }} + {{ user.roleLabel }} + + + + {{ user.pluginAuth }} + + + {{ user.createdAt | date: 'short' }} + + {{ user.lastLoginDate | date: 'short' }} + +
+ + + + + Ban reason: + {{ user.blockedReason }} + + + +
+ + diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.scss b/client/src/app/+admin/overview/users/user-list/user-list.component.scss new file mode 100644 index 000000000..e425306b5 --- /dev/null +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.scss @@ -0,0 +1,65 @@ +@use '_variables' as *; +@use '_mixins' as *; + +.add-button { + @include create-button; +} + +tr.banned > td { + background-color: lighten($color: $red, $amount: 40) !important; +} + +.table-email { + @include disable-default-a-behaviour; + + color: pvar(--mainForegroundColor); +} + +.banned-info { + font-style: italic; +} + +.ban-reason-label { + font-weight: $font-semibold; +} + +.user-table-primary-text .glyphicon { + @include margin-left(0.1rem); + + font-size: 80%; + color: #808080; +} + +p-tableCheckbox { + position: relative; + top: -2.5px; +} + +my-global-icon { + width: 18px; +} + +.chip { + @include chip; +} + +.badge { + @include table-badge; +} + +.progress { + @include progressbar($small: true); + + width: auto; + max-width: 100%; +} + +@media screen and (max-width: $primeng-breakpoint) { + .progress { + width: 100%; + } + + .empty-cell { + padding: 0; + } +} diff --git a/client/src/app/+admin/overview/users/user-list/user-list.component.ts b/client/src/app/+admin/overview/users/user-list/user-list.component.ts new file mode 100644 index 000000000..548e6e80f --- /dev/null +++ b/client/src/app/+admin/overview/users/user-list/user-list.component.ts @@ -0,0 +1,246 @@ +import { SortMeta } from 'primeng/api' +import { Component, OnInit, ViewChild } from '@angular/core' +import { ActivatedRoute, Router } from '@angular/router' +import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core' +import { AdvancedInputFilter } from '@app/shared/shared-forms' +import { DropdownAction } from '@app/shared/shared-main' +import { UserBanModalComponent } from '@app/shared/shared-moderation' +import { User, UserRole } from '@shared/models' + +type UserForList = User & { + rawVideoQuota: number + rawVideoQuotaUsed: number + rawVideoQuotaDaily: number + rawVideoQuotaUsedDaily: number +} + +@Component({ + selector: 'my-user-list', + templateUrl: './user-list.component.html', + styleUrls: [ './user-list.component.scss' ] +}) +export class UserListComponent extends RestTable implements OnInit { + @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent + + users: User[] = [] + + totalRecords = 0 + sort: SortMeta = { field: 'createdAt', order: 1 } + pagination: RestPagination = { count: this.rowsPerPage, start: 0 } + + highlightBannedUsers = false + + selectedUsers: User[] = [] + bulkUserActions: DropdownAction[][] = [] + columns: { id: string, label: string }[] + + inputFilters: AdvancedInputFilter[] = [ + { + title: $localize`Advanced filters`, + children: [ + { + queryParams: { search: 'banned:true' }, + label: $localize`Banned users` + } + ] + } + ] + + requiresEmailVerification = false + + private _selectedColumns: string[] + + constructor ( + protected route: ActivatedRoute, + protected router: Router, + private notifier: Notifier, + private confirmService: ConfirmService, + private serverService: ServerService, + private auth: AuthService, + private userService: UserService + ) { + super() + } + + get authUser () { + return this.auth.getUser() + } + + get selectedColumns () { + return this._selectedColumns + } + + set selectedColumns (val: string[]) { + this._selectedColumns = val + } + + ngOnInit () { + this.serverService.getConfig() + .subscribe(config => this.requiresEmailVerification = config.signup.requiresEmailVerification) + + this.initialize() + + this.bulkUserActions = [ + [ + { + label: $localize`Delete`, + description: $localize`Videos will be deleted, comments will be tombstoned.`, + handler: users => this.removeUsers(users), + isDisplayed: users => users.every(u => this.authUser.canManage(u)) + }, + { + label: $localize`Ban`, + description: $localize`User won't be able to login anymore, but videos and comments will be kept as is.`, + handler: users => this.openBanUserModal(users), + isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === false) + }, + { + label: $localize`Unban`, + handler: users => this.unbanUsers(users), + isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === true) + } + ], + [ + { + label: $localize`Set Email as Verified`, + handler: users => this.setEmailsAsVerified(users), + isDisplayed: users => { + return this.requiresEmailVerification && + users.every(u => this.authUser.canManage(u) && !u.blocked && u.emailVerified === false) + } + } + ] + ] + + this.columns = [ + { id: 'username', label: $localize`Username` }, + { id: 'email', label: $localize`Email` }, + { id: 'quota', label: $localize`Video quota` }, + { id: 'role', label: $localize`Role` }, + { id: 'createdAt', label: $localize`Created` } + ] + + this.selectedColumns = this.columns.map(c => c.id) + + this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` }) + this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` }) + this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` }) + } + + getIdentifier () { + return 'UserListComponent' + } + + getRoleClass (role: UserRole) { + switch (role) { + case UserRole.ADMINISTRATOR: + return 'badge-purple' + case UserRole.MODERATOR: + return 'badge-blue' + default: + return 'badge-yellow' + } + } + + isSelected (id: string) { + return this.selectedColumns.find(c => c === id) + } + + getColumn (id: string) { + return this.columns.find(c => c.id === id) + } + + getUserVideoQuotaPercentage (user: UserForList) { + return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota + } + + getUserVideoQuotaDailyPercentage (user: UserForList) { + return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily + } + + openBanUserModal (users: User[]) { + for (const user of users) { + if (user.username === 'root') { + this.notifier.error($localize`You cannot ban root.`) + return + } + } + + this.userBanModal.openModal(users) + } + + onUserChanged () { + this.reloadData() + } + + async unbanUsers (users: User[]) { + const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) + if (res === false) return + + this.userService.unbanUsers(users) + .subscribe({ + next: () => { + this.notifier.success($localize`${users.length} users unbanned.`) + this.reloadData() + }, + + error: err => this.notifier.error(err.message) + }) + } + + async removeUsers (users: User[]) { + for (const user of users) { + if (user.username === 'root') { + this.notifier.error($localize`You cannot delete root.`) + return + } + } + + const message = $localize`If you remove these users, you will not be able to create others with the same username!` + const res = await this.confirmService.confirm(message, $localize`Delete`) + if (res === false) return + + this.userService.removeUser(users) + .subscribe({ + next: () => { + this.notifier.success($localize`${users.length} users deleted.`) + this.reloadData() + }, + + error: err => this.notifier.error(err.message) + }) + } + + setEmailsAsVerified (users: User[]) { + this.userService.updateUsers(users, { emailVerified: true }) + .subscribe({ + next: () => { + this.notifier.success($localize`${users.length} users email set as verified.`) + this.reloadData() + }, + + error: err => this.notifier.error(err.message) + }) + } + + isInSelectionMode () { + return this.selectedUsers.length !== 0 + } + + protected reloadData () { + this.selectedUsers = [] + + this.userService.getUsers({ + pagination: this.pagination, + sort: this.sort, + search: this.search + }).subscribe({ + next: resultList => { + this.users = resultList.data + this.totalRecords = resultList.total + }, + + error: err => this.notifier.error(err.message) + }) + } +} diff --git a/client/src/app/+admin/overview/users/users.routes.ts b/client/src/app/+admin/overview/users/users.routes.ts new file mode 100644 index 000000000..8b63f5bc7 --- /dev/null +++ b/client/src/app/+admin/overview/users/users.routes.ts @@ -0,0 +1,49 @@ +import { Routes } from '@angular/router' +import { UserRightGuard } from '@app/core' +import { UserRight } from '@shared/models' +import { UserCreateComponent, UserUpdateComponent } from './user-edit' +import { UserListComponent } from './user-list' + +export const UsersRoutes: Routes = [ + { + path: 'users', + canActivate: [ UserRightGuard ], + data: { + userRight: UserRight.MANAGE_USERS + }, + children: [ + { + path: '', + redirectTo: 'list', + pathMatch: 'full' + }, + { + path: 'list', + component: UserListComponent, + data: { + meta: { + title: $localize`Users list` + } + } + }, + { + path: 'create', + component: UserCreateComponent, + data: { + meta: { + title: $localize`Create a user` + } + } + }, + { + path: 'update/:id', + component: UserUpdateComponent, + data: { + meta: { + title: $localize`Update a user` + } + } + } + ] + } +] diff --git a/client/src/app/+admin/users/index.ts b/client/src/app/+admin/users/index.ts deleted file mode 100644 index 156e54d89..000000000 --- a/client/src/app/+admin/users/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './user-edit' -export * from './user-list' -export * from './users.component' -export * from './users.routes' diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts deleted file mode 100644 index ec734ef92..000000000 --- a/client/src/app/+admin/users/user-edit/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './user-create.component' -export * from './user-update.component' -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 deleted file mode 100644 index b61b22fd0..000000000 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Component, OnInit } from '@angular/core' -import { Router } from '@angular/router' -import { ConfigService } from '@app/+admin/config/shared/config.service' -import { AuthService, Notifier, ScreenService, ServerService, UserService } from '@app/core' -import { - USER_CHANNEL_NAME_VALIDATOR, - USER_EMAIL_VALIDATOR, - USER_PASSWORD_OPTIONAL_VALIDATOR, - USER_PASSWORD_VALIDATOR, - USER_ROLE_VALIDATOR, - USER_USERNAME_VALIDATOR, - USER_VIDEO_QUOTA_DAILY_VALIDATOR, - USER_VIDEO_QUOTA_VALIDATOR -} from '@app/shared/form-validators/user-validators' -import { FormValidatorService } from '@app/shared/shared-forms' -import { UserCreate, UserRole } from '@shared/models' -import { UserEdit } from './user-edit' - -@Component({ - selector: 'my-user-create', - templateUrl: './user-edit.component.html', - styleUrls: [ './user-edit.component.scss' ] -}) -export class UserCreateComponent extends UserEdit implements OnInit { - error: string - - constructor ( - protected serverService: ServerService, - protected formValidatorService: FormValidatorService, - protected configService: ConfigService, - protected screenService: ScreenService, - protected auth: AuthService, - private router: Router, - private notifier: Notifier, - private userService: UserService - ) { - super() - - this.buildQuotaOptions() - } - - ngOnInit () { - super.ngOnInit() - - const defaultValues = { - role: UserRole.USER.toString(), - videoQuota: -1, - videoQuotaDaily: -1 - } - - this.buildForm({ - username: USER_USERNAME_VALIDATOR, - channelName: USER_CHANNEL_NAME_VALIDATOR, - email: USER_EMAIL_VALIDATOR, - password: this.isPasswordOptional() ? USER_PASSWORD_OPTIONAL_VALIDATOR : USER_PASSWORD_VALIDATOR, - role: USER_ROLE_VALIDATOR, - videoQuota: USER_VIDEO_QUOTA_VALIDATOR, - videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR, - byPassAutoBlock: null - }, defaultValues) - } - - formValidated () { - this.error = undefined - - const userCreate: UserCreate = this.form.value - - userCreate.adminFlags = this.buildAdminFlags(this.form.value) - - // A select in HTML is always mapped as a string, we convert it to number - userCreate.videoQuota = parseInt(this.form.value['videoQuota'], 10) - userCreate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) - - this.userService.addUser(userCreate) - .subscribe({ - next: () => { - this.notifier.success($localize`User ${userCreate.username} created.`) - this.router.navigate([ '/admin/users/list' ]) - }, - - error: err => { - this.error = err.message - } - }) - } - - isCreation () { - return true - } - - isPasswordOptional () { - return this.serverConfig.email.enabled - } - - getFormButtonTitle () { - return $localize`Create user` - } -} 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 deleted file mode 100644 index 772ebf272..000000000 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - -
-
- -
- -
-
- -
{{ error }}
- -
-
-
- - -
- -
- -
-
- - -
- {{ formErrors.username }} -
-
- -
- - -
- {{ formErrors.channelName }} -
-
- -
- - -
- {{ formErrors.email }} -
-
- -
- - - - - If you leave the password empty, an email will be sent to the user. - - - - - - -
- {{ formErrors.password }} -
-
- -
- -
- -
- -
- {{ formErrors.role }} -
-
- -
- - - - -
- Transcoding is enabled. The video quota only takes into account original video size.
- At most, this user could upload ~ {{ computeQuotaWithTranscoding() | bytes: 0 }}. -
- -
- {{ formErrors.videoQuota }} -
-
- -
- - - - -
- {{ formErrors.videoQuotaDaily }} -
-
- -
- - -
- -
-
- -
- -
- - -
- -
- -
- -
-
- - -
-
-
- -
- -
- -
-
- - -
- -
- - -
-
- -
-
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 deleted file mode 100644 index d7932154b..000000000 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ /dev/null @@ -1,76 +0,0 @@ -@use 'sass:math'; -@use '_variables' as *; -@use '_mixins' as *; - -$form-base-input-width: 340px; - -label { - font-weight: $font-regular; - font-size: 100%; -} - -.account-title { - @include settings-big-title; - - &.account-title-danger { - color: lighten($color: #c54130, $amount: 10); - } -} - -input:not([type=submit]) { - @include peertube-input-text($form-base-input-width); - display: block; -} - -my-input-toggle-hidden { - @include responsive-width($form-base-input-width); - - display: block; -} - -.peertube-select-container { - @include peertube-select-container($form-base-input-width); -} - -my-select-custom-value { - @include responsive-width($form-base-input-width); - - display: block; -} - -input[type=submit], -button { - @include peertube-button; - @include orange-button; - - margin-top: 10px; -} - -.transcoding-information { - margin-top: 5px; - font-size: 11px; -} - -.danger-zone { - .reset-password-email { - margin-bottom: 30px; - - button { - @include peertube-button; - @include danger-button; - @include disable-outline; - - display: block; - margin-top: 0; - } - } -} - -.breadcrumb { - @include breadcrumb; -} - -.dashboard { - @include dashboard; - max-width: 900px; -} diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts deleted file mode 100644 index af5e674a7..000000000 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Directive, OnInit } from '@angular/core' -import { ConfigService } from '@app/+admin/config/shared/config.service' -import { AuthService, ScreenService, ServerService, User } from '@app/core' -import { FormReactive } from '@app/shared/shared-forms' -import { USER_ROLE_LABELS } from '@shared/core-utils/users' -import { HTMLServerConfig, UserAdminFlag, UserRole, VideoResolution } from '@shared/models' -import { SelectOptionsItem } from '../../../../types/select-options-item.model' - -@Directive() -// eslint-disable-next-line @angular-eslint/directive-class-suffix -export abstract class UserEdit extends FormReactive implements OnInit { - videoQuotaOptions: SelectOptionsItem[] = [] - videoQuotaDailyOptions: SelectOptionsItem[] = [] - username: string - user: User - - roles: { value: string, label: string }[] = [] - - protected serverConfig: HTMLServerConfig - - protected abstract serverService: ServerService - protected abstract configService: ConfigService - protected abstract screenService: ScreenService - protected abstract auth: AuthService - abstract isCreation (): boolean - abstract getFormButtonTitle (): string - - ngOnInit (): void { - this.serverConfig = this.serverService.getHTMLConfig() - - this.buildRoles() - } - - get subscribersCount () { - const forAccount = this.user - ? this.user.account.followersCount - : 0 - const forChannels = this.user - ? this.user.videoChannels.map(c => c.followersCount).reduce((a, b) => a + b, 0) - : 0 - return forAccount + forChannels - } - - getAuthPlugins () { - return this.serverConfig.plugin.registeredIdAndPassAuths.map(p => p.npmName) - .concat(this.serverConfig.plugin.registeredExternalAuths.map(p => p.npmName)) - } - - isInBigView () { - return this.screenService.getWindowInnerWidth() > 1600 - } - - buildRoles () { - const authUser = this.auth.getUser() - - if (authUser.role === UserRole.ADMINISTRATOR) { - this.roles = Object.keys(USER_ROLE_LABELS) - .map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] })) - return - } - - this.roles = [ - { value: UserRole.USER.toString(), label: USER_ROLE_LABELS[UserRole.USER] } - ] - } - - isTranscodingInformationDisplayed () { - const formVideoQuota = parseInt(this.form.value['videoQuota'], 10) - - return this.serverConfig.transcoding.enabledResolutions.length !== 0 && - formVideoQuota > 0 - } - - computeQuotaWithTranscoding () { - const transcodingConfig = this.serverConfig.transcoding - - const resolutions = transcodingConfig.enabledResolutions - const higherResolution = VideoResolution.H_4K - let multiplier = 0 - - for (const resolution of resolutions) { - multiplier += resolution / higherResolution - } - - if (transcodingConfig.hls.enabled) multiplier *= 2 - - return multiplier * parseInt(this.form.value['videoQuota'], 10) - } - - resetPassword () { - return - } - - protected buildAdminFlags (formValue: any) { - return formValue.byPassAutoBlock ? UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST : UserAdminFlag.NONE - } - - protected buildQuotaOptions () { - this.videoQuotaOptions = this.configService.videoQuotaOptions - this.videoQuotaDailyOptions = this.configService.videoQuotaDailyOptions - } -} 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 deleted file mode 100644 index 1238d1839..000000000 --- a/client/src/app/+admin/users/user-edit/user-password.component.html +++ /dev/null @@ -1,21 +0,0 @@ -
-
- -
- -
- -
-
-
- {{ formErrors.password }} -
-
- - -
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 deleted file mode 100644 index acb680682..000000000 --- a/client/src/app/+admin/users/user-edit/user-password.component.scss +++ /dev/null @@ -1,23 +0,0 @@ -@use '_variables' as *; -@use '_mixins' as *; - -input:not([type=submit]):not([type=checkbox]) { - @include peertube-input-text(340px); - - display: block; - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: 0; -} - -input[type=submit] { - @include peertube-button; - @include danger-button; - @include disable-outline; - - margin-top: 10px; -} - -.input-group-append { - height: 30px; -} 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 deleted file mode 100644 index 42bf20de1..000000000 --- a/client/src/app/+admin/users/user-edit/user-password.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core' -import { Notifier, UserService } from '@app/core' -import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' -import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' -import { UserUpdate } from '@shared/models' - -@Component({ - selector: 'my-user-password', - templateUrl: './user-password.component.html', - styleUrls: [ './user-password.component.scss' ] -}) -export class UserPasswordComponent extends FormReactive implements OnInit { - error: string - username: string - showPassword = false - - @Input() userId: number - - constructor ( - protected formValidatorService: FormValidatorService, - private notifier: Notifier, - private userService: UserService - ) { - super() - } - - ngOnInit () { - this.buildForm({ - password: USER_PASSWORD_VALIDATOR - }) - } - - formValidated () { - this.error = undefined - - const userUpdate: UserUpdate = this.form.value - - this.userService.updateUser(this.userId, userUpdate) - .subscribe({ - next: () => this.notifier.success($localize`Password changed for user ${this.username}.`), - - error: err => { - this.error = err.message - } - }) - } - - togglePasswordVisibility () { - this.showPassword = !this.showPassword - } - - getFormButtonTitle () { - return $localize`Update user password` - } -} 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 deleted file mode 100644 index 42599a17e..000000000 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Subscription } from 'rxjs' -import { Component, OnDestroy, OnInit } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { ConfigService } from '@app/+admin/config/shared/config.service' -import { AuthService, Notifier, ScreenService, ServerService, User, UserService } from '@app/core' -import { - USER_EMAIL_VALIDATOR, - USER_ROLE_VALIDATOR, - USER_VIDEO_QUOTA_DAILY_VALIDATOR, - USER_VIDEO_QUOTA_VALIDATOR -} from '@app/shared/form-validators/user-validators' -import { FormValidatorService } from '@app/shared/shared-forms' -import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' -import { UserEdit } from './user-edit' - -@Component({ - selector: 'my-user-update', - templateUrl: './user-edit.component.html', - styleUrls: [ './user-edit.component.scss' ] -}) -export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { - error: string - - private paramsSub: Subscription - - constructor ( - protected formValidatorService: FormValidatorService, - protected serverService: ServerService, - protected configService: ConfigService, - protected screenService: ScreenService, - protected auth: AuthService, - private route: ActivatedRoute, - private router: Router, - private notifier: Notifier, - private userService: UserService - ) { - super() - - this.buildQuotaOptions() - } - - ngOnInit () { - super.ngOnInit() - - const defaultValues = { - role: UserRole.USER.toString(), - videoQuota: '-1', - videoQuotaDaily: '-1' - } - - this.buildForm({ - email: USER_EMAIL_VALIDATOR, - role: USER_ROLE_VALIDATOR, - videoQuota: USER_VIDEO_QUOTA_VALIDATOR, - videoQuotaDaily: USER_VIDEO_QUOTA_DAILY_VALIDATOR, - byPassAutoBlock: null, - pluginAuth: null - }, defaultValues) - - this.paramsSub = this.route.params.subscribe(routeParams => { - const userId = routeParams['id'] - this.userService.getUser(userId, true) - .subscribe({ - next: user => this.onUserFetched(user), - - error: err => { - this.error = err.message - } - }) - }) - } - - ngOnDestroy () { - this.paramsSub.unsubscribe() - } - - formValidated () { - this.error = undefined - - const userUpdate: UserUpdate = this.form.value - userUpdate.adminFlags = this.buildAdminFlags(this.form.value) - - // A select in HTML is always mapped as a string, we convert it to number - userUpdate.videoQuota = parseInt(this.form.value['videoQuota'], 10) - userUpdate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) - - if (userUpdate.pluginAuth === 'null') userUpdate.pluginAuth = null - - this.userService.updateUser(this.user.id, userUpdate) - .subscribe({ - next: () => { - this.notifier.success($localize`User ${this.user.username} updated.`) - this.router.navigate([ '/admin/users/list' ]) - }, - - error: err => { - this.error = err.message - } - }) - } - - isCreation () { - return false - } - - isPasswordOptional () { - return false - } - - getFormButtonTitle () { - return $localize`Update user` - } - - resetPassword () { - this.userService.askResetPassword(this.user.email) - .subscribe({ - next: () => { - this.notifier.success($localize`An email asking for password reset has been sent to ${this.user.username}.`) - }, - - error: err => { - this.error = err.message - } - }) - } - - private onUserFetched (userJson: UserType) { - this.user = new User(userJson) - - this.form.patchValue({ - email: userJson.email, - role: userJson.role.toString(), - videoQuota: userJson.videoQuota, - videoQuotaDaily: userJson.videoQuotaDaily, - pluginAuth: userJson.pluginAuth, - byPassAutoBlock: userJson.adminFlags & UserAdminFlag.BYPASS_VIDEO_AUTO_BLACKLIST - }) - } -} diff --git a/client/src/app/+admin/users/user-list/index.ts b/client/src/app/+admin/users/user-list/index.ts deleted file mode 100644 index 1826a4abe..000000000 --- a/client/src/app/+admin/users/user-list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './user-list.component' 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 deleted file mode 100644 index c82f3c06f..000000000 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ /dev/null @@ -1,163 +0,0 @@ - - -
- - -
- -
- -
-
- - - - - - - - -
- - - -
- - {{ getColumn('username').label }} - {{ getColumn('email').label }} - {{ getColumn('quota').label }} - {{ getColumn('quotaDaily').label }} - {{ getColumn('role').label }} - {{ getColumn('pluginAuth').label }} - {{ getColumn('createdAt').label }} - {{ getColumn('lastLoginDate').label }} - -
- - - - - - - - - - - - - - - - - - - - - -
- -
- {{ user.account.displayName }} - {{ user.username }} -
-
-
- - - - - {{ user.email }} - - - - - - ? {{ user.email }} - - - - ✓ {{ user.email }} - - - - - -
-
-
- {{ user.videoQuotaUsed }} - {{ user.videoQuota }} -
- - - -
-
-
- {{ user.videoQuotaUsedDaily }} - {{ user.videoQuotaDaily }} -
- - - - {{ user.roleLabel }} - {{ user.roleLabel }} - - - - {{ user.pluginAuth }} - - - {{ user.createdAt | date: 'short' }} - - {{ user.lastLoginDate | date: 'short' }} - -
- - - - - Ban reason: - {{ user.blockedReason }} - - - -
- - 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 deleted file mode 100644 index e425306b5..000000000 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ /dev/null @@ -1,65 +0,0 @@ -@use '_variables' as *; -@use '_mixins' as *; - -.add-button { - @include create-button; -} - -tr.banned > td { - background-color: lighten($color: $red, $amount: 40) !important; -} - -.table-email { - @include disable-default-a-behaviour; - - color: pvar(--mainForegroundColor); -} - -.banned-info { - font-style: italic; -} - -.ban-reason-label { - font-weight: $font-semibold; -} - -.user-table-primary-text .glyphicon { - @include margin-left(0.1rem); - - font-size: 80%; - color: #808080; -} - -p-tableCheckbox { - position: relative; - top: -2.5px; -} - -my-global-icon { - width: 18px; -} - -.chip { - @include chip; -} - -.badge { - @include table-badge; -} - -.progress { - @include progressbar($small: true); - - width: auto; - max-width: 100%; -} - -@media screen and (max-width: $primeng-breakpoint) { - .progress { - width: 100%; - } - - .empty-cell { - padding: 0; - } -} 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 deleted file mode 100644 index 548e6e80f..000000000 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { SortMeta } from 'primeng/api' -import { Component, OnInit, ViewChild } from '@angular/core' -import { ActivatedRoute, Router } from '@angular/router' -import { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService, UserService } from '@app/core' -import { AdvancedInputFilter } from '@app/shared/shared-forms' -import { DropdownAction } from '@app/shared/shared-main' -import { UserBanModalComponent } from '@app/shared/shared-moderation' -import { User, UserRole } from '@shared/models' - -type UserForList = User & { - rawVideoQuota: number - rawVideoQuotaUsed: number - rawVideoQuotaDaily: number - rawVideoQuotaUsedDaily: number -} - -@Component({ - selector: 'my-user-list', - templateUrl: './user-list.component.html', - styleUrls: [ './user-list.component.scss' ] -}) -export class UserListComponent extends RestTable implements OnInit { - @ViewChild('userBanModal', { static: true }) userBanModal: UserBanModalComponent - - users: User[] = [] - - totalRecords = 0 - sort: SortMeta = { field: 'createdAt', order: 1 } - pagination: RestPagination = { count: this.rowsPerPage, start: 0 } - - highlightBannedUsers = false - - selectedUsers: User[] = [] - bulkUserActions: DropdownAction[][] = [] - columns: { id: string, label: string }[] - - inputFilters: AdvancedInputFilter[] = [ - { - title: $localize`Advanced filters`, - children: [ - { - queryParams: { search: 'banned:true' }, - label: $localize`Banned users` - } - ] - } - ] - - requiresEmailVerification = false - - private _selectedColumns: string[] - - constructor ( - protected route: ActivatedRoute, - protected router: Router, - private notifier: Notifier, - private confirmService: ConfirmService, - private serverService: ServerService, - private auth: AuthService, - private userService: UserService - ) { - super() - } - - get authUser () { - return this.auth.getUser() - } - - get selectedColumns () { - return this._selectedColumns - } - - set selectedColumns (val: string[]) { - this._selectedColumns = val - } - - ngOnInit () { - this.serverService.getConfig() - .subscribe(config => this.requiresEmailVerification = config.signup.requiresEmailVerification) - - this.initialize() - - this.bulkUserActions = [ - [ - { - label: $localize`Delete`, - description: $localize`Videos will be deleted, comments will be tombstoned.`, - handler: users => this.removeUsers(users), - isDisplayed: users => users.every(u => this.authUser.canManage(u)) - }, - { - label: $localize`Ban`, - description: $localize`User won't be able to login anymore, but videos and comments will be kept as is.`, - handler: users => this.openBanUserModal(users), - isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === false) - }, - { - label: $localize`Unban`, - handler: users => this.unbanUsers(users), - isDisplayed: users => users.every(u => this.authUser.canManage(u) && u.blocked === true) - } - ], - [ - { - label: $localize`Set Email as Verified`, - handler: users => this.setEmailsAsVerified(users), - isDisplayed: users => { - return this.requiresEmailVerification && - users.every(u => this.authUser.canManage(u) && !u.blocked && u.emailVerified === false) - } - } - ] - ] - - this.columns = [ - { id: 'username', label: $localize`Username` }, - { id: 'email', label: $localize`Email` }, - { id: 'quota', label: $localize`Video quota` }, - { id: 'role', label: $localize`Role` }, - { id: 'createdAt', label: $localize`Created` } - ] - - this.selectedColumns = this.columns.map(c => c.id) - - this.columns.push({ id: 'quotaDaily', label: $localize`Daily quota` }) - this.columns.push({ id: 'pluginAuth', label: $localize`Auth plugin` }) - this.columns.push({ id: 'lastLoginDate', label: $localize`Last login` }) - } - - getIdentifier () { - return 'UserListComponent' - } - - getRoleClass (role: UserRole) { - switch (role) { - case UserRole.ADMINISTRATOR: - return 'badge-purple' - case UserRole.MODERATOR: - return 'badge-blue' - default: - return 'badge-yellow' - } - } - - isSelected (id: string) { - return this.selectedColumns.find(c => c === id) - } - - getColumn (id: string) { - return this.columns.find(c => c.id === id) - } - - getUserVideoQuotaPercentage (user: UserForList) { - return user.rawVideoQuotaUsed * 100 / user.rawVideoQuota - } - - getUserVideoQuotaDailyPercentage (user: UserForList) { - return user.rawVideoQuotaUsedDaily * 100 / user.rawVideoQuotaDaily - } - - openBanUserModal (users: User[]) { - for (const user of users) { - if (user.username === 'root') { - this.notifier.error($localize`You cannot ban root.`) - return - } - } - - this.userBanModal.openModal(users) - } - - onUserChanged () { - this.reloadData() - } - - async unbanUsers (users: User[]) { - const res = await this.confirmService.confirm($localize`Do you really want to unban ${users.length} users?`, $localize`Unban`) - if (res === false) return - - this.userService.unbanUsers(users) - .subscribe({ - next: () => { - this.notifier.success($localize`${users.length} users unbanned.`) - this.reloadData() - }, - - error: err => this.notifier.error(err.message) - }) - } - - async removeUsers (users: User[]) { - for (const user of users) { - if (user.username === 'root') { - this.notifier.error($localize`You cannot delete root.`) - return - } - } - - const message = $localize`If you remove these users, you will not be able to create others with the same username!` - const res = await this.confirmService.confirm(message, $localize`Delete`) - if (res === false) return - - this.userService.removeUser(users) - .subscribe({ - next: () => { - this.notifier.success($localize`${users.length} users deleted.`) - this.reloadData() - }, - - error: err => this.notifier.error(err.message) - }) - } - - setEmailsAsVerified (users: User[]) { - this.userService.updateUsers(users, { emailVerified: true }) - .subscribe({ - next: () => { - this.notifier.success($localize`${users.length} users email set as verified.`) - this.reloadData() - }, - - error: err => this.notifier.error(err.message) - }) - } - - isInSelectionMode () { - return this.selectedUsers.length !== 0 - } - - protected reloadData () { - this.selectedUsers = [] - - this.userService.getUsers({ - pagination: this.pagination, - sort: this.sort, - search: this.search - }).subscribe({ - next: resultList => { - this.users = resultList.data - this.totalRecords = resultList.total - }, - - error: err => this.notifier.error(err.message) - }) - } -} diff --git a/client/src/app/+admin/users/users.component.ts b/client/src/app/+admin/users/users.component.ts deleted file mode 100644 index e9c8f6b0d..000000000 --- a/client/src/app/+admin/users/users.component.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - template: '' -}) -export class UsersComponent { -} diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts deleted file mode 100644 index 9175be067..000000000 --- a/client/src/app/+admin/users/users.routes.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Routes } from '@angular/router' -import { UserRightGuard } from '@app/core' -import { UserRight } from '@shared/models' -import { UserCreateComponent, UserUpdateComponent } from './user-edit' -import { UserListComponent } from './user-list' -import { UsersComponent } from './users.component' - -export const UsersRoutes: Routes = [ - { - path: 'users', - component: UsersComponent, - canActivate: [ UserRightGuard ], - data: { - userRight: UserRight.MANAGE_USERS - }, - children: [ - { - path: '', - redirectTo: 'list', - pathMatch: 'full' - }, - { - path: 'list', - component: UserListComponent, - data: { - meta: { - title: $localize`Users list` - } - } - }, - { - path: 'create', - component: UserCreateComponent, - data: { - meta: { - title: $localize`Create a user` - } - } - }, - { - path: 'update/:id', - component: UserUpdateComponent, - data: { - meta: { - title: $localize`Update a user` - } - } - } - ] - } -] -- cgit v1.2.3