diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index e0dea3ba5..c672fa280 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -10,6 +10,7 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons' import { SharedMainModule } from '@app/shared/shared-main' import { SharedModerationModule } from '@app/shared/shared-moderation' import { SharedTablesModule } from '@app/shared/shared-tables' +import { SharedUsersModule } from '@app/shared/shared-users' import { SharedVideoCommentModule } from '@app/shared/shared-video-comment' import { SharedVideoMiniatureModule } from '@app/shared/shared-video-miniature' import { AdminRoutingModule } from './admin-routing.module' @@ -67,6 +68,7 @@ import { JobsComponent } from './system/jobs/jobs.component' SharedCustomMarkupModule, SharedVideoMiniatureModule, SharedTablesModule, + SharedUsersModule, TableModule, ChartModule 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 index b61b22fd0..1713e06ce 100644 --- 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 @@ -1,7 +1,7 @@ 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 { AuthService, Notifier, ScreenService, ServerService } from '@app/core' import { USER_CHANNEL_NAME_VALIDATOR, USER_EMAIL_VALIDATOR, @@ -13,6 +13,7 @@ import { USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormValidatorService } from '@app/shared/shared-forms' +import { UserAdminService } from '@app/shared/shared-users' import { UserCreate, UserRole } from '@shared/models' import { UserEdit } from './user-edit' @@ -32,7 +33,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { protected auth: AuthService, private router: Router, private notifier: Notifier, - private userService: UserService + private userAdminService: UserAdminService ) { super() @@ -71,7 +72,7 @@ export class UserCreateComponent extends UserEdit implements OnInit { userCreate.videoQuota = parseInt(this.form.value['videoQuota'], 10) userCreate.videoQuotaDaily = parseInt(this.form.value['videoQuotaDaily'], 10) - this.userService.addUser(userCreate) + this.userAdminService.addUser(userCreate) .subscribe({ next: () => { this.notifier.success($localize`User ${userCreate.username} created.`) 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 index 42bf20de1..8999d1f00 100644 --- 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 @@ -1,7 +1,8 @@ import { Component, Input, OnInit } from '@angular/core' -import { Notifier, UserService } from '@app/core' +import { Notifier } from '@app/core' import { USER_PASSWORD_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { UserAdminService } from '@app/shared/shared-users' import { UserUpdate } from '@shared/models' @Component({ @@ -19,7 +20,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit { constructor ( protected formValidatorService: FormValidatorService, private notifier: Notifier, - private userService: UserService + private userAdminService: UserAdminService ) { super() } @@ -35,7 +36,7 @@ export class UserPasswordComponent extends FormReactive implements OnInit { const userUpdate: UserUpdate = this.form.value - this.userService.updateUser(this.userId, userUpdate) + this.userAdminService.updateUser(this.userId, userUpdate) .subscribe({ next: () => this.notifier.success($localize`Password changed for user ${this.username}.`), 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 index 42599a17e..bab288a67 100644 --- 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 @@ -10,6 +10,7 @@ import { USER_VIDEO_QUOTA_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormValidatorService } from '@app/shared/shared-forms' +import { UserAdminService } from '@app/shared/shared-users' import { User as UserType, UserAdminFlag, UserRole, UserUpdate } from '@shared/models' import { UserEdit } from './user-edit' @@ -32,7 +33,8 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { private route: ActivatedRoute, private router: Router, private notifier: Notifier, - private userService: UserService + private userService: UserService, + private userAdminService: UserAdminService ) { super() @@ -86,7 +88,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { if (userUpdate.pluginAuth === 'null') userUpdate.pluginAuth = null - this.userService.updateUser(this.user.id, userUpdate) + this.userAdminService.updateUser(this.user.id, userUpdate) .subscribe({ next: () => { this.notifier.success($localize`User ${this.user.username} updated.`) 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 index 9fba11cbd..9a9d0f5c6 100644 --- 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 @@ -1,10 +1,11 @@ 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 { AuthService, ConfirmService, Notifier, RestPagination, RestTable, ServerService } 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 { UserAdminService } from '@app/shared/shared-users' import { User, UserRole } from '@shared/models' type UserForList = User & { @@ -57,7 +58,7 @@ export class UserListComponent extends RestTable implements OnInit { private confirmService: ConfirmService, private serverService: ServerService, private auth: AuthService, - private userService: UserService + private userAdminService: UserAdminService ) { super() } @@ -177,7 +178,7 @@ export class UserListComponent extends RestTable implements OnInit { 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) + this.userAdminService.unbanUsers(users) .subscribe({ next: () => { this.notifier.success($localize`${users.length} users unbanned.`) @@ -200,7 +201,7 @@ export class UserListComponent extends RestTable implements OnInit { const res = await this.confirmService.confirm(message, $localize`Delete`) if (res === false) return - this.userService.removeUser(users) + this.userAdminService.removeUser(users) .subscribe({ next: () => { this.notifier.success($localize`${users.length} users deleted.`) @@ -212,7 +213,7 @@ export class UserListComponent extends RestTable implements OnInit { } setEmailsAsVerified (users: User[]) { - this.userService.updateUsers(users, { emailVerified: true }) + this.userAdminService.updateUsers(users, { emailVerified: true }) .subscribe({ next: () => { this.notifier.success($localize`${users.length} users email set as verified.`) @@ -230,7 +231,7 @@ export class UserListComponent extends RestTable implements OnInit { protected reloadData () { this.selectedUsers = [] - this.userService.getUsers({ + this.userAdminService.getUsers({ pagination: this.pagination, sort: this.sort, search: this.search diff --git a/client/src/app/+signup/+register/register-step-channel.component.ts b/client/src/app/+signup/+register/register-step-channel.component.ts index d965a7865..1bc0ccfd3 100644 --- a/client/src/app/+signup/+register/register-step-channel.component.ts +++ b/client/src/app/+signup/+register/register-step-channel.component.ts @@ -2,9 +2,9 @@ import { concat, of } from 'rxjs' import { pairwise } from 'rxjs/operators' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { FormGroup } from '@angular/forms' -import { UserService } from '@app/core' import { VIDEO_CHANNEL_DISPLAY_NAME_VALIDATOR, VIDEO_CHANNEL_NAME_VALIDATOR } from '@app/shared/form-validators/video-channel-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-register-step-channel', @@ -17,7 +17,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit constructor ( protected formValidatorService: FormValidatorService, - private userService: UserService + private userSignupService: UserSignupService ) { super() } @@ -48,7 +48,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { const name = this.form.value['name'] || '' - const newName = this.userService.getNewUsername(oldDisplayName, newDisplayName, name) + const newName = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, name) this.form.patchValue({ name: newName }) } } diff --git a/client/src/app/+signup/+register/register-step-user.component.ts b/client/src/app/+signup/+register/register-step-user.component.ts index 716cd8c78..92ddfca2e 100644 --- a/client/src/app/+signup/+register/register-step-user.component.ts +++ b/client/src/app/+signup/+register/register-step-user.component.ts @@ -2,7 +2,6 @@ import { concat, of } from 'rxjs' import { pairwise } from 'rxjs/operators' import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' import { FormGroup } from '@angular/forms' -import { UserService } from '@app/core' import { USER_DISPLAY_NAME_REQUIRED_VALIDATOR, USER_EMAIL_VALIDATOR, @@ -10,6 +9,7 @@ import { USER_USERNAME_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-register-step-user', @@ -23,7 +23,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { constructor ( protected formValidatorService: FormValidatorService, - private userService: UserService + private userSignupService: UserSignupService ) { super() } @@ -52,7 +52,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit { private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) { const username = this.form.value['username'] || '' - const newUsername = this.userService.getNewUsername(oldDisplayName, newDisplayName, username) + const newUsername = this.userSignupService.getNewUsername(oldDisplayName, newDisplayName, username) this.form.patchValue({ username: newUsername }) } } diff --git a/client/src/app/+signup/+register/register.component.ts b/client/src/app/+signup/+register/register.component.ts index bb7276459..b4a7c0d0e 100644 --- a/client/src/app/+signup/+register/register.component.ts +++ b/client/src/app/+signup/+register/register.component.ts @@ -1,12 +1,13 @@ import { Component, OnInit } from '@angular/core' import { FormGroup } from '@angular/forms' import { ActivatedRoute } from '@angular/router' -import { AuthService, UserService } from '@app/core' +import { AuthService } from '@app/core' import { HooksService } from '@app/core/plugins/hooks.service' +import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' +import { UserSignupService } from '@app/shared/shared-users' import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap' import { UserRegister } from '@shared/models' import { ServerConfig } from '@shared/models/server' -import { InstanceAboutAccordionComponent } from '@app/shared/shared-instance' @Component({ selector: 'my-register', @@ -49,7 +50,7 @@ export class RegisterComponent implements OnInit { constructor ( private route: ActivatedRoute, private authService: AuthService, - private userService: UserService, + private userSignupService: UserSignupService, private hooks: HooksService ) { } @@ -128,7 +129,7 @@ export class RegisterComponent implements OnInit { 'filter:api.signup.registration.create.params' ) - this.userService.signup(body).subscribe({ + this.userSignupService.signup(body).subscribe({ next: () => { this.signupDone = true diff --git a/client/src/app/+signup/+register/register.module.ts b/client/src/app/+signup/+register/register.module.ts index c36da53d5..52cdb33bc 100644 --- a/client/src/app/+signup/+register/register.module.ts +++ b/client/src/app/+signup/+register/register.module.ts @@ -1,6 +1,6 @@ import { CdkStepperModule } from '@angular/cdk/stepper' import { NgModule } from '@angular/core' -import { SignupSharedModule } from '@app/+signup/shared/signup-shared.module' +import { SharedSignupModule } from '@app/+signup/shared/shared-signup.module' import { SharedInstanceModule } from '@app/shared/shared-instance' import { CustomStepperComponent } from './custom-stepper.component' import { RegisterRoutingModule } from './register-routing.module' @@ -15,7 +15,7 @@ import { RegisterComponent } from './register.component' CdkStepperModule, - SignupSharedModule, + SharedSignupModule, SharedInstanceModule ], diff --git a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index 83c24a251..a0ed66a3a 100644 --- a/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts @@ -1,7 +1,8 @@ import { Component, OnInit } from '@angular/core' -import { Notifier, RedirectService, ServerService, UserService } from '@app/core' +import { Notifier, RedirectService, ServerService } from '@app/core' import { USER_EMAIL_VALIDATOR } from '@app/shared/form-validators/user-validators' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' +import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-verify-account-ask-send-email', @@ -14,7 +15,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements constructor ( protected formValidatorService: FormValidatorService, - private userService: UserService, + private userSignupService: UserSignupService, private serverService: ServerService, private notifier: Notifier, private redirectService: RedirectService @@ -33,7 +34,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements askSendVerifyEmail () { const email = this.form.value['verify-email-email'] - this.userService.askSendVerifyEmail(email) + this.userSignupService.askSendVerifyEmail(email) .subscribe({ next: () => { this.notifier.success($localize`An email with verification link will be sent to ${email}.`) diff --git a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts index 827ec7652..88efce4a1 100644 --- a/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+signup/+verify-account/verify-account-email/verify-account-email.component.ts @@ -1,6 +1,7 @@ import { Component, OnInit } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { AuthService, Notifier, UserService } from '@app/core' +import { AuthService, Notifier } from '@app/core' +import { UserSignupService } from '@app/shared/shared-users' @Component({ selector: 'my-verify-account-email', @@ -16,7 +17,7 @@ export class VerifyAccountEmailComponent implements OnInit { private verificationString: string constructor ( - private userService: UserService, + private userSignupService: UserSignupService, private authService: AuthService, private notifier: Notifier, private route: ActivatedRoute @@ -37,7 +38,7 @@ export class VerifyAccountEmailComponent implements OnInit { } verifyEmail () { - this.userService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) + this.userSignupService.verifyEmail(this.userId, this.verificationString, this.isPendingEmail) .subscribe({ next: () => { if (this.authService.isLoggedIn()) { diff --git a/client/src/app/+signup/+verify-account/verify-account.module.ts b/client/src/app/+signup/+verify-account/verify-account.module.ts index 7255605d4..ec342df8d 100644 --- a/client/src/app/+signup/+verify-account/verify-account.module.ts +++ b/client/src/app/+signup/+verify-account/verify-account.module.ts @@ -1,5 +1,5 @@ import { NgModule } from '@angular/core' -import { SignupSharedModule } from '../shared/signup-shared.module' +import { SharedSignupModule } from '../shared/shared-signup.module' import { VerifyAccountAskSendEmailComponent } from './verify-account-ask-send-email/verify-account-ask-send-email.component' import { VerifyAccountEmailComponent } from './verify-account-email/verify-account-email.component' import { VerifyAccountRoutingModule } from './verify-account-routing.module' @@ -8,7 +8,7 @@ import { VerifyAccountRoutingModule } from './verify-account-routing.module' imports: [ VerifyAccountRoutingModule, - SignupSharedModule + SharedSignupModule ], declarations: [ diff --git a/client/src/app/+signup/shared/signup-shared.module.ts b/client/src/app/+signup/shared/shared-signup.module.ts similarity index 79% rename from client/src/app/+signup/shared/signup-shared.module.ts rename to client/src/app/+signup/shared/shared-signup.module.ts index 56b0b3bae..f8b224c71 100644 --- a/client/src/app/+signup/shared/signup-shared.module.ts +++ b/client/src/app/+signup/shared/shared-signup.module.ts @@ -1,14 +1,16 @@ import { NgModule } from '@angular/core' -import { SharedMainModule } from '@app/shared/shared-main' -import { SignupSuccessComponent } from './signup-success.component' import { SharedFormModule } from '@app/shared/shared-forms' import { SharedGlobalIconModule } from '@app/shared/shared-icons' +import { SharedMainModule } from '@app/shared/shared-main' +import { SharedUsersModule } from '@app/shared/shared-users' +import { SignupSuccessComponent } from './signup-success.component' @NgModule({ imports: [ SharedMainModule, SharedFormModule, - SharedGlobalIconModule + SharedGlobalIconModule, + SharedUsersModule ], declarations: [ @@ -26,4 +28,4 @@ import { SharedGlobalIconModule } from '@app/shared/shared-icons' providers: [ ] }) -export class SignupSharedModule { } +export class SharedSignupModule { } diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts index f173c356a..b14bff193 100644 --- a/client/src/app/core/users/user.service.ts +++ b/client/src/app/core/users/user.service.ts @@ -1,24 +1,12 @@ -import { SortMeta } from 'primeng/api' -import { from, Observable, of } from 'rxjs' -import { catchError, concatMap, first, map, shareReplay, tap, toArray } from 'rxjs/operators' +import { Observable, of } from 'rxjs' +import { catchError, first, map, shareReplay } from 'rxjs/operators' import { HttpClient, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { AuthService } from '@app/core/auth' -import { getBytes } from '@root-helpers/bytes' -import { - ActorImage, - ResultList, - User as UserServerModel, - UserCreate, - UserRegister, - UserRole, - UserUpdate, - UserUpdateMe, - UserVideoQuota -} from '@shared/models' +import { ActorImage, User as UserServerModel, UserUpdateMe, UserVideoQuota } from '@shared/models' import { environment } from '../../../environments/environment' -import { RestExtractor, RestPagination, RestService } from '../rest' -import { UserLocalStorageService } from './' +import { RestExtractor } from '../rest' +import { UserLocalStorageService } from './user-local-storage.service' import { User } from './user.model' @Injectable() @@ -26,21 +14,71 @@ export class UserService { static BASE_USERS_URL = environment.apiUrl + '/api/v1/users/' private userCache: { [ id: number ]: Observable } = {} - private signupInThisSession = false constructor ( private authHttp: HttpClient, private authService: AuthService, private restExtractor: RestExtractor, - private restService: RestService, private userLocalStorageService: UserLocalStorageService ) { } + // --------------------------------------------------------------------------- + + getUserWithCache (userId: number) { + if (!this.userCache[userId]) { + this.userCache[userId] = this.getUser(userId).pipe(shareReplay()) + } + + return this.userCache[userId] + } + + getUser (userId: number, withStats = false) { + const params = new HttpParams().append('withStats', withStats + '') + + return this.authHttp.get(UserService.BASE_USERS_URL + userId, { params }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + // --------------------------------------------------------------------------- + + setSignupInThisSession (value: boolean) { + this.signupInThisSession = value + } + hasSignupInThisSession () { return this.signupInThisSession } + // --------------------------------------------------------------------------- + + updateMyAnonymousProfile (profile: UserUpdateMe) { + this.userLocalStorageService.setUserInfo(profile) + } + + listenAnonymousUpdate () { + return this.userLocalStorageService.listenUserInfoChange() + .pipe(map(() => this.getAnonymousUser())) + } + + getAnonymousUser () { + return new User(this.userLocalStorageService.getUserInfo()) + } + + getAnonymousOrLoggedUser () { + if (!this.authService.isLoggedIn()) { + return of(this.getAnonymousUser()) + } + + return this.authService.userInformationLoaded + .pipe( + first(), + map(() => this.authService.getUser()) + ) + } + + // --------------------------------------------------------------------------- + changePassword (currentPassword: string, newPassword: string) { const url = UserService.BASE_USERS_URL + 'me' const body: UserUpdateMe = { @@ -63,23 +101,6 @@ export class UserService { .pipe(catchError(err => this.restExtractor.handleError(err))) } - // --------------------------------------------------------------------------- - - updateMyAnonymousProfile (profile: UserUpdateMe) { - this.userLocalStorageService.setUserInfo(profile) - } - - listenAnonymousUpdate () { - return this.userLocalStorageService.listenUserInfoChange() - .pipe(map(() => this.getAnonymousUser())) - } - - getAnonymousUser () { - return new User(this.userLocalStorageService.getUserInfo()) - } - - // --------------------------------------------------------------------------- - updateMyProfile (profile: UserUpdateMe) { const url = UserService.BASE_USERS_URL + 'me' @@ -108,14 +129,6 @@ export class UserService { .pipe(catchError(err => this.restExtractor.handleError(err))) } - signup (userCreate: UserRegister) { - return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) - .pipe( - tap(() => this.signupInThisSession = true), - catchError(err => this.restExtractor.handleError(err)) - ) - } - getMyVideoQuotaUsed () { const url = UserService.BASE_USERS_URL + 'me/video-quota-used' @@ -141,24 +154,6 @@ export class UserService { .pipe(catchError(res => this.restExtractor.handleError(res))) } - verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { - const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` - const body = { - verificationString, - isPendingEmail - } - - return this.authHttp.post(url, body) - .pipe(catchError(res => this.restExtractor.handleError(res))) - } - - askSendVerifyEmail (email: string) { - const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' - - return this.authHttp.post(url, { email }) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - autocomplete (search: string): Observable { const url = UserService.BASE_USERS_URL + 'autocomplete' const params = new HttpParams().append('search', search) @@ -167,169 +162,4 @@ export class UserService { .get(url, { params }) .pipe(catchError(res => this.restExtractor.handleError(res))) } - - getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { - // Don't update display name, the user seems to have changed it - if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername - - return this.displayNameToUsername(newDisplayName) - } - - displayNameToUsername (displayName: string) { - if (!displayName) return '' - - return displayName - .toLowerCase() - .replace(/\s/g, '_') - .replace(/[^a-z0-9_.]/g, '') - } - - /* ###### Admin methods ###### */ - - addUser (userCreate: UserCreate) { - return this.authHttp.post(UserService.BASE_USERS_URL, userCreate) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - updateUser (userId: number, userUpdate: UserUpdate) { - return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - updateUsers (users: UserServerModel[], userUpdate: UserUpdate) { - return from(users) - .pipe( - concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)), - toArray(), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getUserWithCache (userId: number) { - if (!this.userCache[userId]) { - this.userCache[userId] = this.getUser(userId).pipe(shareReplay()) - } - - return this.userCache[userId] - } - - getUser (userId: number, withStats = false) { - const params = new HttpParams().append('withStats', withStats + '') - return this.authHttp.get(UserService.BASE_USERS_URL + userId, { params }) - .pipe(catchError(err => this.restExtractor.handleError(err))) - } - - getUsers (parameters: { - pagination: RestPagination - sort: SortMeta - search?: string - }): Observable> { - const { pagination, sort, search } = parameters - - let params = new HttpParams() - params = this.restService.addRestGetParams(params, pagination, sort) - - if (search) { - const filters = this.restService.parseQueryStringFilter(search, { - blocked: { - prefix: 'banned:', - isBoolean: true - } - }) - - params = this.restService.addObjectParams(params, filters) - } - - return this.authHttp.get>(UserService.BASE_USERS_URL, { params }) - .pipe( - map(res => this.restExtractor.convertResultListDateToHuman(res)), - map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - removeUser (usersArg: UserServerModel | UserServerModel[]) { - const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] - - return from(users) - .pipe( - concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)), - toArray(), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) { - const body = reason ? { reason } : {} - const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] - - return from(users) - .pipe( - concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)), - toArray(), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - unbanUsers (usersArg: UserServerModel | UserServerModel[]) { - const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] - - return from(users) - .pipe( - concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})), - toArray(), - catchError(err => this.restExtractor.handleError(err)) - ) - } - - getAnonymousOrLoggedUser () { - if (!this.authService.isLoggedIn()) { - return of(this.getAnonymousUser()) - } - - return this.authService.userInformationLoaded - .pipe( - first(), - map(() => this.authService.getUser()) - ) - } - - private formatUser (user: UserServerModel) { - let videoQuota - if (user.videoQuota === -1) { - videoQuota = '∞' - } else { - videoQuota = getBytes(user.videoQuota, 0) - } - - const videoQuotaUsed = getBytes(user.videoQuotaUsed, 0) - - let videoQuotaDaily: string - let videoQuotaUsedDaily: string - if (user.videoQuotaDaily === -1) { - videoQuotaDaily = '∞' - videoQuotaUsedDaily = getBytes(0, 0) + '' - } else { - videoQuotaDaily = getBytes(user.videoQuotaDaily, 0) + '' - videoQuotaUsedDaily = getBytes(user.videoQuotaUsedDaily || 0, 0) + '' - } - - const roleLabels: { [ id in UserRole ]: string } = { - [UserRole.USER]: $localize`User`, - [UserRole.ADMINISTRATOR]: $localize`Administrator`, - [UserRole.MODERATOR]: $localize`Moderator` - } - - return Object.assign(user, { - roleLabel: roleLabels[user.role], - videoQuota, - videoQuotaUsed, - rawVideoQuota: user.videoQuota, - rawVideoQuotaUsed: user.videoQuotaUsed, - videoQuotaDaily, - videoQuotaUsedDaily, - rawVideoQuotaDaily: user.videoQuotaDaily, - rawVideoQuotaUsedDaily: user.videoQuotaUsedDaily - }) - } } diff --git a/client/src/app/shared/shared-moderation/shared-moderation.module.ts b/client/src/app/shared/shared-moderation/shared-moderation.module.ts index 7cadda67c..dac4ae951 100644 --- a/client/src/app/shared/shared-moderation/shared-moderation.module.ts +++ b/client/src/app/shared/shared-moderation/shared-moderation.module.ts @@ -1,10 +1,13 @@ import { NgModule } from '@angular/core' +import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' import { SharedFormModule } from '../shared-forms/shared-form.module' import { SharedGlobalIconModule } from '../shared-icons' import { SharedMainModule } from '../shared-main/shared-main.module' +import { SharedUsersModule } from '../shared-users' import { SharedVideoCommentModule } from '../shared-video-comment' import { AbuseService } from './abuse.service' +import { AccountBlockBadgesComponent } from './account-block-badges.component' import { BatchDomainsModalComponent } from './batch-domains-modal.component' import { BlocklistService } from './blocklist.service' import { BulkService } from './bulk.service' @@ -13,8 +16,6 @@ import { UserBanModalComponent } from './user-ban-modal.component' import { UserModerationDropdownComponent } from './user-moderation-dropdown.component' import { VideoBlockComponent } from './video-block.component' import { VideoBlockService } from './video-block.service' -import { AccountBlockBadgesComponent } from './account-block-badges.component' -import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image.module' @NgModule({ imports: [ @@ -22,7 +23,8 @@ import { SharedActorImageModule } from '../shared-actor-image/shared-actor-image SharedFormModule, SharedGlobalIconModule, SharedVideoCommentModule, - SharedActorImageModule + SharedActorImageModule, + SharedUsersModule ], declarations: [ diff --git a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts index b2ce019c5..17cad18ec 100644 --- a/client/src/app/shared/shared-moderation/user-ban-modal.component.ts +++ b/client/src/app/shared/shared-moderation/user-ban-modal.component.ts @@ -1,10 +1,11 @@ import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' -import { Notifier, UserService } from '@app/core' +import { Notifier } from '@app/core' import { FormReactive, FormValidatorService } from '@app/shared/shared-forms' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' import { User } from '@shared/models' import { USER_BAN_REASON_VALIDATOR } from '../form-validators/user-validators' +import { UserAdminService } from '../shared-users' @Component({ selector: 'my-user-ban-modal', @@ -23,7 +24,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { protected formValidatorService: FormValidatorService, private modalService: NgbModal, private notifier: Notifier, - private userService: UserService + private userAdminService: UserAdminService ) { super() } @@ -50,7 +51,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit { banUser () { const reason = this.form.value['reason'] || undefined - this.userService.banUsers(this.usersToBan, reason) + this.userAdminService.banUsers(this.usersToBan, reason) .subscribe({ next: () => { const message = Array.isArray(this.usersToBan) diff --git a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts index e2cd2cdc1..0d19565ef 100644 --- a/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts +++ b/client/src/app/shared/shared-moderation/user-moderation-dropdown.component.ts @@ -1,7 +1,8 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, ViewChild } from '@angular/core' -import { AuthService, ConfirmService, Notifier, ServerService, UserService } from '@app/core' +import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' import { Account, DropdownAction } from '@app/shared/shared-main' import { BulkRemoveCommentsOfBody, User, UserRight } from '@shared/models' +import { UserAdminService } from '../shared-users' import { BlocklistService } from './blocklist.service' import { BulkService } from './bulk.service' import { UserBanModalComponent } from './user-ban-modal.component' @@ -35,7 +36,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { private notifier: Notifier, private confirmService: ConfirmService, private serverService: ServerService, - private userService: UserService, + private userAdminService: UserAdminService, private blocklistService: BlocklistService, private bulkService: BulkService ) { } @@ -66,7 +67,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { const res = await this.confirmService.confirm($localize`Do you really want to unban ${user.username}?`, $localize`Unban`) if (res === false) return - this.userService.unbanUsers(user) + this.userAdminService.unbanUsers(user) .subscribe({ next: () => { this.notifier.success($localize`User ${user.username} unbanned.`) @@ -87,7 +88,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { const res = await this.confirmService.confirm(message, $localize`Delete ${user.username}`) if (res === false) return - this.userService.removeUser(user) + this.userAdminService.removeUser(user) .subscribe({ next: () => { this.notifier.success($localize`User ${user.username} deleted.`) @@ -99,7 +100,7 @@ export class UserModerationDropdownComponent implements OnInit, OnChanges { } setEmailAsVerified (user: User) { - this.userService.updateUser(user.id, { emailVerified: true }) + this.userAdminService.updateUser(user.id, { emailVerified: true }) .subscribe({ next: () => { this.notifier.success($localize`User ${user.username} email set as verified`) diff --git a/client/src/app/shared/shared-users/index.ts b/client/src/app/shared/shared-users/index.ts new file mode 100644 index 000000000..8f90f2515 --- /dev/null +++ b/client/src/app/shared/shared-users/index.ts @@ -0,0 +1,4 @@ +export * from './user-admin.service' +export * from './user-signup.service' + +export * from './shared-users.module' diff --git a/client/src/app/shared/shared-users/shared-users.module.ts b/client/src/app/shared/shared-users/shared-users.module.ts new file mode 100644 index 000000000..2a1dadf20 --- /dev/null +++ b/client/src/app/shared/shared-users/shared-users.module.ts @@ -0,0 +1,21 @@ + +import { NgModule } from '@angular/core' +import { SharedMainModule } from '../shared-main/shared-main.module' +import { UserAdminService } from './user-admin.service' +import { UserSignupService } from './user-signup.service' + +@NgModule({ + imports: [ + SharedMainModule + ], + + declarations: [ ], + + exports: [], + + providers: [ + UserSignupService, + UserAdminService + ] +}) +export class SharedUsersModule { } diff --git a/client/src/app/shared/shared-users/user-admin.service.ts b/client/src/app/shared/shared-users/user-admin.service.ts new file mode 100644 index 000000000..3db271c4a --- /dev/null +++ b/client/src/app/shared/shared-users/user-admin.service.ts @@ -0,0 +1,139 @@ +import { SortMeta } from 'primeng/api' +import { from, Observable } from 'rxjs' +import { catchError, concatMap, map, toArray } from 'rxjs/operators' +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, RestPagination, RestService, UserService } from '@app/core' +import { getBytes } from '@root-helpers/bytes' +import { ResultList, User as UserServerModel, UserCreate, UserRole, UserUpdate } from '@shared/models' + +@Injectable() +export class UserAdminService { + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService + ) { } + + addUser (userCreate: UserCreate) { + return this.authHttp.post(UserService.BASE_USERS_URL, userCreate) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + updateUser (userId: number, userUpdate: UserUpdate) { + return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + updateUsers (users: UserServerModel[], userUpdate: UserUpdate) { + return from(users) + .pipe( + concatMap(u => this.authHttp.put(UserService.BASE_USERS_URL + u.id, userUpdate)), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + getUsers (parameters: { + pagination: RestPagination + sort: SortMeta + search?: string + }): Observable> { + const { pagination, sort, search } = parameters + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + if (search) { + const filters = this.restService.parseQueryStringFilter(search, { + blocked: { + prefix: 'banned:', + isBoolean: true + } + }) + + params = this.restService.addObjectParams(params, filters) + } + + return this.authHttp.get>(UserService.BASE_USERS_URL, { params }) + .pipe( + map(res => this.restExtractor.convertResultListDateToHuman(res)), + map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this))), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + removeUser (usersArg: UserServerModel | UserServerModel[]) { + const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] + + return from(users) + .pipe( + concatMap(u => this.authHttp.delete(UserService.BASE_USERS_URL + u.id)), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + banUsers (usersArg: UserServerModel | UserServerModel[], reason?: string) { + const body = reason ? { reason } : {} + const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] + + return from(users) + .pipe( + concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/block', body)), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + unbanUsers (usersArg: UserServerModel | UserServerModel[]) { + const users = Array.isArray(usersArg) ? usersArg : [ usersArg ] + + return from(users) + .pipe( + concatMap(u => this.authHttp.post(UserService.BASE_USERS_URL + u.id + '/unblock', {})), + toArray(), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + private formatUser (user: UserServerModel) { + let videoQuota + if (user.videoQuota === -1) { + videoQuota = '∞' + } else { + videoQuota = getBytes(user.videoQuota, 0) + } + + const videoQuotaUsed = getBytes(user.videoQuotaUsed, 0) + + let videoQuotaDaily: string + let videoQuotaUsedDaily: string + if (user.videoQuotaDaily === -1) { + videoQuotaDaily = '∞' + videoQuotaUsedDaily = getBytes(0, 0) + '' + } else { + videoQuotaDaily = getBytes(user.videoQuotaDaily, 0) + '' + videoQuotaUsedDaily = getBytes(user.videoQuotaUsedDaily || 0, 0) + '' + } + + const roleLabels: { [ id in UserRole ]: string } = { + [UserRole.USER]: $localize`User`, + [UserRole.ADMINISTRATOR]: $localize`Administrator`, + [UserRole.MODERATOR]: $localize`Moderator` + } + + return Object.assign(user, { + roleLabel: roleLabels[user.role], + videoQuota, + videoQuotaUsed, + rawVideoQuota: user.videoQuota, + rawVideoQuotaUsed: user.videoQuotaUsed, + videoQuotaDaily, + videoQuotaUsedDaily, + rawVideoQuotaDaily: user.videoQuotaDaily, + rawVideoQuotaUsedDaily: user.videoQuotaUsedDaily + }) + } +} diff --git a/client/src/app/shared/shared-users/user-signup.service.ts b/client/src/app/shared/shared-users/user-signup.service.ts new file mode 100644 index 000000000..46fe34af1 --- /dev/null +++ b/client/src/app/shared/shared-users/user-signup.service.ts @@ -0,0 +1,56 @@ +import { catchError, tap } from 'rxjs/operators' +import { HttpClient } from '@angular/common/http' +import { Injectable } from '@angular/core' +import { RestExtractor, UserService } from '@app/core' +import { UserRegister } from '@shared/models' + +@Injectable() +export class UserSignupService { + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private userService: UserService + ) { } + + signup (userCreate: UserRegister) { + return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) + .pipe( + tap(() => this.userService.setSignupInThisSession(true)), + catchError(err => this.restExtractor.handleError(err)) + ) + } + + verifyEmail (userId: number, verificationString: string, isPendingEmail: boolean) { + const url = `${UserService.BASE_USERS_URL}/${userId}/verify-email` + const body = { + verificationString, + isPendingEmail + } + + return this.authHttp.post(url, body) + .pipe(catchError(res => this.restExtractor.handleError(res))) + } + + askSendVerifyEmail (email: string) { + const url = UserService.BASE_USERS_URL + '/ask-send-verify-email' + + return this.authHttp.post(url, { email }) + .pipe(catchError(err => this.restExtractor.handleError(err))) + } + + getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) { + // Don't update display name, the user seems to have changed it + if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername + + return this.displayNameToUsername(newDisplayName) + } + + private displayNameToUsername (displayName: string) { + if (!displayName) return '' + + return displayName + .toLowerCase() + .replace(/\s/g, '_') + .replace(/[^a-z0-9_.]/g, '') + } +}