aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2019-06-07 16:59:53 +0200
committerChocobozzz <me@florianbigard.com>2019-06-07 17:05:42 +0200
commit1f20622f2b087eaf8738d60fae00a44b9c558ca3 (patch)
tree1c8554623665ca96b8a1e6f2a6bcb8c1b5a83c2e
parent1a03bea0c42fa1064ce4770157b4fd2e3edd5565 (diff)
downloadPeerTube-1f20622f2b087eaf8738d60fae00a44b9c558ca3.tar.gz
PeerTube-1f20622f2b087eaf8738d60fae00a44b9c558ca3.tar.zst
PeerTube-1f20622f2b087eaf8738d60fae00a44b9c558ca3.zip
Improve registration
* Add ability to set the user display name * Use display name to guess the username/channel name * Add explanations about what is the purpose of a username/channel name * Add a loader at the "done" step
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts2
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.html34
-rw-r--r--client/src/app/+signup/+register/register-step-channel.component.ts30
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.html19
-rw-r--r--client/src/app/+signup/+register/register-step-user.component.ts19
-rw-r--r--client/src/app/+signup/+register/register.component.html6
-rw-r--r--client/src/app/+signup/+register/register.component.scss23
-rw-r--r--client/src/app/shared/forms/form-validators/user-validators.service.ts33
-rw-r--r--client/src/app/shared/misc/loader.component.html2
-rw-r--r--client/src/app/shared/misc/loader.component.scss14
-rw-r--r--client/src/app/shared/users/user.service.ts16
-rw-r--r--client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts1
-rw-r--r--server/controllers/api/users/index.ts8
-rw-r--r--server/initializers/installer.ts2
-rw-r--r--server/initializers/migrations/0100-activitypub.ts9
-rw-r--r--server/lib/user.ts41
-rw-r--r--server/middlewares/validators/users.ts12
-rw-r--r--server/tests/api/check-params/users.ts7
-rw-r--r--server/tests/api/users/users.ts15
-rw-r--r--server/tests/api/videos/video-playlists.ts1
-rw-r--r--shared/extra-utils/users/users.ts6
-rw-r--r--shared/models/users/user-register.model.ts2
-rw-r--r--support/doc/api/openapi.yaml13
23 files changed, 246 insertions, 69 deletions
diff --git a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
index a9503ed1b..fcad5a6c2 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-profile/my-account-profile.component.ts
@@ -30,7 +30,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
30 30
31 ngOnInit () { 31 ngOnInit () {
32 this.buildForm({ 32 this.buildForm({
33 'display-name': this.userValidatorsService.USER_DISPLAY_NAME, 33 'display-name': this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED,
34 description: this.userValidatorsService.USER_DESCRIPTION 34 description: this.userValidatorsService.USER_DESCRIPTION
35 }) 35 })
36 36
diff --git a/client/src/app/+signup/+register/register-step-channel.component.html b/client/src/app/+signup/+register/register-step-channel.component.html
index 68ea4473a..253374f87 100644
--- a/client/src/app/+signup/+register/register-step-channel.component.html
+++ b/client/src/app/+signup/+register/register-step-channel.component.html
@@ -12,6 +12,21 @@
12 </div> 12 </div>
13 13
14 <div class="form-group"> 14 <div class="form-group">
15 <label for="displayName" i18n>Channel display name</label>
16
17 <div class="input-group">
18 <input
19 type="text" id="displayName"
20 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
21 >
22 </div>
23
24 <div *ngIf="formErrors.displayName" class="form-error">
25 {{ formErrors.displayName }}
26 </div>
27 </div>
28
29 <div class="form-group">
15 <label for="name" i18n>Channel name</label> 30 <label for="name" i18n>Channel name</label>
16 31
17 <div class="input-group"> 32 <div class="input-group">
@@ -24,6 +39,10 @@
24 </div> 39 </div>
25 </div> 40 </div>
26 41
42 <div class="name-information" i18n>
43 The channel name is a unique identifier of your channel on this instance. It's like an address mail, so other people can find your channel.
44 </div>
45
27 <div *ngIf="formErrors.name" class="form-error"> 46 <div *ngIf="formErrors.name" class="form-error">
28 {{ formErrors.name }} 47 {{ formErrors.name }}
29 </div> 48 </div>
@@ -32,19 +51,4 @@
32 Channel name cannot be the same than your account name. You can click on the first step to update your account name. 51 Channel name cannot be the same than your account name. You can click on the first step to update your account name.
33 </div> 52 </div>
34 </div> 53 </div>
35
36 <div class="form-group">
37 <label for="displayName" i18n>Channel display name</label>
38
39 <div class="input-group">
40 <input
41 type="text" id="displayName"
42 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
43 >
44 </div>
45
46 <div *ngIf="formErrors.displayName" class="form-error">
47 {{ formErrors.displayName }}
48 </div>
49 </div>
50</form> 54</form>
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 9e13f75b3..e434b91a7 100644
--- a/client/src/app/+signup/+register/register-step-channel.component.ts
+++ b/client/src/app/+signup/+register/register-step-channel.component.ts
@@ -1,8 +1,10 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService } from '@app/core'
3import { FormReactive, VideoChannelValidatorsService } from '@app/shared' 3import { FormReactive, UserService, VideoChannelValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { FormGroup } from '@angular/forms' 5import { FormGroup } from '@angular/forms'
6import { pairwise } from 'rxjs/operators'
7import { concat, of } from 'rxjs'
6 8
7@Component({ 9@Component({
8 selector: 'my-register-step-channel', 10 selector: 'my-register-step-channel',
@@ -16,6 +18,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
16 constructor ( 18 constructor (
17 protected formValidatorService: FormValidatorService, 19 protected formValidatorService: FormValidatorService,
18 private authService: AuthService, 20 private authService: AuthService,
21 private userService: UserService,
19 private videoChannelValidatorsService: VideoChannelValidatorsService 22 private videoChannelValidatorsService: VideoChannelValidatorsService
20 ) { 23 ) {
21 super() 24 super()
@@ -25,16 +28,29 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
25 return window.location.host 28 return window.location.host
26 } 29 }
27 30
28 isSameThanUsername () {
29 return this.username && this.username === this.form.value['name']
30 }
31
32 ngOnInit () { 31 ngOnInit () {
33 this.buildForm({ 32 this.buildForm({
34 name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME, 33 displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME,
35 displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME 34 name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME
36 }) 35 })
37 36
38 setTimeout(() => this.formBuilt.emit(this.form)) 37 setTimeout(() => this.formBuilt.emit(this.form))
38
39 concat(
40 of(''),
41 this.form.get('displayName').valueChanges
42 ).pipe(pairwise())
43 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
44 }
45
46 isSameThanUsername () {
47 return this.username && this.username === this.form.value['name']
48 }
49
50 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
51 const name = this.form.value['name'] || ''
52
53 const newName = this.userService.getNewUsername(oldDisplayName, newDisplayName, name)
54 this.form.patchValue({ name: newName })
39 } 55 }
40} 56}
diff --git a/client/src/app/+signup/+register/register-step-user.component.html b/client/src/app/+signup/+register/register-step-user.component.html
index cd0c78bfa..47b3be8cc 100644
--- a/client/src/app/+signup/+register/register-step-user.component.html
+++ b/client/src/app/+signup/+register/register-step-user.component.html
@@ -1,6 +1,21 @@
1<form role="form" [formGroup]="form"> 1<form role="form" [formGroup]="form">
2 2
3 <div class="form-group"> 3 <div class="form-group">
4 <label for="displayName" i18n>Display name</label>
5
6 <div class="input-group">
7 <input
8 type="text" id="displayName" placeholder="John Doe"
9 formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
10 >
11 </div>
12
13 <div *ngIf="formErrors.displayName" class="form-error">
14 {{ formErrors.displayName }}
15 </div>
16 </div>
17
18 <div class="form-group">
4 <label for="username" i18n>Username</label> 19 <label for="username" i18n>Username</label>
5 20
6 <div class="input-group"> 21 <div class="input-group">
@@ -13,6 +28,10 @@
13 </div> 28 </div>
14 </div> 29 </div>
15 30
31 <div class="name-information" i18n>
32 The username is a unique identifier of your account on this instance. It's like an address mail, so other people can find you.
33 </div>
34
16 <div *ngIf="formErrors.username" class="form-error"> 35 <div *ngIf="formErrors.username" class="form-error">
17 {{ formErrors.username }} 36 {{ formErrors.username }}
18 </div> 37 </div>
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 3825ae371..3b71fd3c4 100644
--- a/client/src/app/+signup/+register/register-step-user.component.ts
+++ b/client/src/app/+signup/+register/register-step-user.component.ts
@@ -1,8 +1,10 @@
1import { Component, EventEmitter, OnInit, Output } from '@angular/core' 1import { Component, EventEmitter, OnInit, Output } from '@angular/core'
2import { AuthService } from '@app/core' 2import { AuthService } from '@app/core'
3import { FormReactive, UserValidatorsService } from '@app/shared' 3import { FormReactive, UserService, UserValidatorsService } from '@app/shared'
4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 4import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
5import { FormGroup } from '@angular/forms' 5import { FormGroup } from '@angular/forms'
6import { pairwise } from 'rxjs/operators'
7import { concat, of } from 'rxjs'
6 8
7@Component({ 9@Component({
8 selector: 'my-register-step-user', 10 selector: 'my-register-step-user',
@@ -15,6 +17,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
15 constructor ( 17 constructor (
16 protected formValidatorService: FormValidatorService, 18 protected formValidatorService: FormValidatorService,
17 private authService: AuthService, 19 private authService: AuthService,
20 private userService: UserService,
18 private userValidatorsService: UserValidatorsService 21 private userValidatorsService: UserValidatorsService
19 ) { 22 ) {
20 super() 23 super()
@@ -26,6 +29,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
26 29
27 ngOnInit () { 30 ngOnInit () {
28 this.buildForm({ 31 this.buildForm({
32 displayName: this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED,
29 username: this.userValidatorsService.USER_USERNAME, 33 username: this.userValidatorsService.USER_USERNAME,
30 password: this.userValidatorsService.USER_PASSWORD, 34 password: this.userValidatorsService.USER_PASSWORD,
31 email: this.userValidatorsService.USER_EMAIL, 35 email: this.userValidatorsService.USER_EMAIL,
@@ -33,5 +37,18 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
33 }) 37 })
34 38
35 setTimeout(() => this.formBuilt.emit(this.form)) 39 setTimeout(() => this.formBuilt.emit(this.form))
40
41 concat(
42 of(''),
43 this.form.get('displayName').valueChanges
44 ).pipe(pairwise())
45 .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
46 }
47
48 private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
49 const username = this.form.value['username'] || ''
50
51 const newUsername = this.userService.getNewUsername(oldDisplayName, newDisplayName, username)
52 this.form.patchValue({ username: newUsername })
36 } 53 }
37} 54}
diff --git a/client/src/app/+signup/+register/register.component.html b/client/src/app/+signup/+register/register.component.html
index 24def68c1..d7e47c1a8 100644
--- a/client/src/app/+signup/+register/register.component.html
+++ b/client/src/app/+signup/+register/register.component.html
@@ -27,6 +27,12 @@
27 </cdk-step> 27 </cdk-step>
28 28
29 <cdk-step i18n-label label="Done" editable="false"> 29 <cdk-step i18n-label label="Done" editable="false">
30 <div *ngIf="!signupDone && !error" class="done-loader">
31 <my-loader [loading]="true"></my-loader>
32
33 <div i18n>PeerTube is creating your account...</div>
34 </div>
35
30 <div *ngIf="error" class="alert alert-danger">{{ error }}</div> 36 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
31 </cdk-step> 37 </cdk-step>
32 </my-custom-stepper> 38 </my-custom-stepper>
diff --git a/client/src/app/+signup/+register/register.component.scss b/client/src/app/+signup/+register/register.component.scss
index 6f61b78f7..8d14992e7 100644
--- a/client/src/app/+signup/+register/register.component.scss
+++ b/client/src/app/+signup/+register/register.component.scss
@@ -56,3 +56,26 @@ button {
56 @include peertube-button; 56 @include peertube-button;
57 @include orange-button; 57 @include orange-button;
58} 58}
59
60.name-information {
61 margin-top: 10px;
62}
63
64.done-loader {
65 display: flex;
66 justify-content: center;
67 flex-direction: column;
68 align-items: center;
69
70 my-loader {
71 margin-bottom: 20px;
72
73 /deep/ .loader div {
74 border-color: var(--mainColor) transparent transparent transparent;
75 }
76
77 & + div {
78 font-size: 15px;
79 }
80 }
81}
diff --git a/client/src/app/shared/forms/form-validators/user-validators.service.ts b/client/src/app/shared/forms/form-validators/user-validators.service.ts
index 6589b2580..2dafb1816 100644
--- a/client/src/app/shared/forms/form-validators/user-validators.service.ts
+++ b/client/src/app/shared/forms/form-validators/user-validators.service.ts
@@ -12,7 +12,7 @@ export class UserValidatorsService {
12 readonly USER_VIDEO_QUOTA: BuildFormValidator 12 readonly USER_VIDEO_QUOTA: BuildFormValidator
13 readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator 13 readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator
14 readonly USER_ROLE: BuildFormValidator 14 readonly USER_ROLE: BuildFormValidator
15 readonly USER_DISPLAY_NAME: BuildFormValidator 15 readonly USER_DISPLAY_NAME_REQUIRED: BuildFormValidator
16 readonly USER_DESCRIPTION: BuildFormValidator 16 readonly USER_DESCRIPTION: BuildFormValidator
17 readonly USER_TERMS: BuildFormValidator 17 readonly USER_TERMS: BuildFormValidator
18 18
@@ -85,18 +85,7 @@ export class UserValidatorsService {
85 } 85 }
86 } 86 }
87 87
88 this.USER_DISPLAY_NAME = { 88 this.USER_DISPLAY_NAME_REQUIRED = this.getDisplayName(true)
89 VALIDATORS: [
90 Validators.required,
91 Validators.minLength(1),
92 Validators.maxLength(50)
93 ],
94 MESSAGES: {
95 'required': this.i18n('Display name is required.'),
96 'minlength': this.i18n('Display name must be at least 1 character long.'),
97 'maxlength': this.i18n('Display name cannot be more than 50 characters long.')
98 }
99 }
100 89
101 this.USER_DESCRIPTION = { 90 this.USER_DESCRIPTION = {
102 VALIDATORS: [ 91 VALIDATORS: [
@@ -129,4 +118,22 @@ export class UserValidatorsService {
129 } 118 }
130 } 119 }
131 } 120 }
121
122 private getDisplayName (required: boolean) {
123 const control = {
124 VALIDATORS: [
125 Validators.minLength(1),
126 Validators.maxLength(120)
127 ],
128 MESSAGES: {
129 'required': this.i18n('Display name is required.'),
130 'minlength': this.i18n('Display name must be at least 1 character long.'),
131 'maxlength': this.i18n('Display name cannot be more than 50 characters long.')
132 }
133 }
134
135 if (required) control.VALIDATORS.push(Validators.required)
136
137 return control
138 }
132} 139}
diff --git a/client/src/app/shared/misc/loader.component.html b/client/src/app/shared/misc/loader.component.html
index b8b7ad343..ca8ed063e 100644
--- a/client/src/app/shared/misc/loader.component.html
+++ b/client/src/app/shared/misc/loader.component.html
@@ -1,5 +1,5 @@
1<div *ngIf="loading"> 1<div *ngIf="loading">
2 <div class="lds-ring"> 2 <div class="loader">
3 <div></div> 3 <div></div>
4 <div></div> 4 <div></div>
5 <div></div> 5 <div></div>
diff --git a/client/src/app/shared/misc/loader.component.scss b/client/src/app/shared/misc/loader.component.scss
index ddb64f07a..ffac9c707 100644
--- a/client/src/app/shared/misc/loader.component.scss
+++ b/client/src/app/shared/misc/loader.component.scss
@@ -3,14 +3,14 @@
3 3
4// Thanks to https://loading.io/css/ (CC0 License) 4// Thanks to https://loading.io/css/ (CC0 License)
5 5
6.lds-ring { 6.loader {
7 display: inline-block; 7 display: inline-block;
8 position: relative; 8 position: relative;
9 width: 50px; 9 width: 50px;
10 height: 50px; 10 height: 50px;
11} 11}
12 12
13.lds-ring div { 13.loader div {
14 box-sizing: border-box; 14 box-sizing: border-box;
15 display: block; 15 display: block;
16 position: absolute; 16 position: absolute;
@@ -19,23 +19,23 @@
19 margin: 6px; 19 margin: 6px;
20 border: 4px solid; 20 border: 4px solid;
21 border-radius: 50%; 21 border-radius: 50%;
22 animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; 22 animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
23 border-color: #999999 transparent transparent transparent; 23 border-color: #999999 transparent transparent transparent;
24} 24}
25 25
26.lds-ring div:nth-child(1) { 26.loader div:nth-child(1) {
27 animation-delay: -0.45s; 27 animation-delay: -0.45s;
28} 28}
29 29
30.lds-ring div:nth-child(2) { 30.loader div:nth-child(2) {
31 animation-delay: -0.3s; 31 animation-delay: -0.3s;
32} 32}
33 33
34.lds-ring div:nth-child(3) { 34.loader div:nth-child(3) {
35 animation-delay: -0.15s; 35 animation-delay: -0.15s;
36} 36}
37 37
38@keyframes lds-ring { 38@keyframes loader {
39 0% { 39 0% {
40 transform: rotate(0deg); 40 transform: rotate(0deg);
41 } 41 }
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 20883456f..70ff9a058 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -136,6 +136,22 @@ export class UserService {
136 .pipe(catchError(res => this.restExtractor.handleError(res))) 136 .pipe(catchError(res => this.restExtractor.handleError(res)))
137 } 137 }
138 138
139 getNewUsername (oldDisplayName: string, newDisplayName: string, currentUsername: string) {
140 // Don't update display name, the user seems to have changed it
141 if (this.displayNameToUsername(oldDisplayName) !== currentUsername) return currentUsername
142
143 return this.displayNameToUsername(newDisplayName)
144 }
145
146 displayNameToUsername (displayName: string) {
147 if (!displayName) return ''
148
149 return displayName
150 .toLowerCase()
151 .replace(/\s/g, '_')
152 .replace(/[^a-z0-9_.]/g, '')
153 }
154
139 /* ###### Admin methods ###### */ 155 /* ###### Admin methods ###### */
140 156
141 addUser (userCreate: UserCreate) { 157 addUser (userCreate: UserCreate) {
diff --git a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
index ed9cb5840..e47624dd6 100644
--- a/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
+++ b/client/src/app/videos/+video-edit/video-add-components/video-import-torrent.component.ts
@@ -100,7 +100,6 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Ca
100 previewUrl: null 100 previewUrl: null
101 })) 101 }))
102 102
103
104 this.hydrateFormFromVideo() 103 this.hydrateFormFromVideo()
105 }, 104 },
106 105
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 48a6c63b8..99f51a648 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -184,7 +184,7 @@ async function createUser (req: express.Request, res: express.Response) {
184 adminFlags: body.adminFlags || UserAdminFlag.NONE 184 adminFlags: body.adminFlags || UserAdminFlag.NONE
185 }) 185 })
186 186
187 const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate) 187 const { user, account } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
188 188
189 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON())) 189 auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
190 logger.info('User %s with its channel and account created.', body.username) 190 logger.info('User %s with its channel and account created.', body.username)
@@ -214,7 +214,11 @@ async function registerUser (req: express.Request, res: express.Response) {
214 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null 214 emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
215 }) 215 })
216 216
217 const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate, body.channel) 217 const { user } = await createUserAccountAndChannelAndPlaylist({
218 userToCreate: userToCreate,
219 userDisplayName: body.displayName || undefined,
220 channelNames: body.channel
221 })
218 222
219 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON())) 223 auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
220 logger.info('User %s with its channel and account registered.', body.username) 224 logger.info('User %s with its channel and account registered.', body.username)
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index e14554ede..cb58454cb 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -146,7 +146,7 @@ async function createOAuthAdminIfNotExist () {
146 } 146 }
147 const user = new UserModel(userData) 147 const user = new UserModel(userData)
148 148
149 await createUserAccountAndChannelAndPlaylist(user, undefined, validatePassword) 149 await createUserAccountAndChannelAndPlaylist({ userToCreate: user, channelNames: undefined, validateUser: validatePassword })
150 logger.info('Username: ' + username) 150 logger.info('Username: ' + username)
151 logger.info('User password: ' + password) 151 logger.info('User password: ' + password)
152} 152}
diff --git a/server/initializers/migrations/0100-activitypub.ts b/server/initializers/migrations/0100-activitypub.ts
index 2880a97d9..96d44a7ce 100644
--- a/server/initializers/migrations/0100-activitypub.ts
+++ b/server/initializers/migrations/0100-activitypub.ts
@@ -65,7 +65,12 @@ async function up (utils: {
65 // Create application account 65 // Create application account
66 { 66 {
67 const applicationInstance = await ApplicationModel.findOne() 67 const applicationInstance = await ApplicationModel.findOne()
68 const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationInstance.id, undefined) 68 const accountCreated = await createLocalAccountWithoutKeys({
69 name: SERVER_ACTOR_NAME,
70 userId: null,
71 applicationId: applicationInstance.id,
72 t: undefined
73 })
69 74
70 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 75 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
71 accountCreated.Actor.publicKey = publicKey 76 accountCreated.Actor.publicKey = publicKey
@@ -83,7 +88,7 @@ async function up (utils: {
83 // Recreate accounts for each user 88 // Recreate accounts for each user
84 const users = await db.User.findAll() 89 const users = await db.User.findAll()
85 for (const user of users) { 90 for (const user of users) {
86 const account = await createLocalAccountWithoutKeys(user.username, user.id, null, undefined) 91 const account = await createLocalAccountWithoutKeys({ name: user.username, userId: user.id, applicationId: null, t: undefined })
87 92
88 const { publicKey, privateKey } = await createPrivateAndPublicKeys() 93 const { publicKey, privateKey } = await createPrivateAndPublicKeys()
89 account.Actor.publicKey = publicKey 94 account.Actor.publicKey = publicKey
diff --git a/server/lib/user.ts b/server/lib/user.ts
index d9fd89e15..b50b09d72 100644
--- a/server/lib/user.ts
+++ b/server/lib/user.ts
@@ -1,4 +1,3 @@
1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4' 1import * as uuidv4 from 'uuid/v4'
3import { ActivityPubActorType } from '../../shared/models/activitypub' 2import { ActivityPubActorType } from '../../shared/models/activitypub'
4import { SERVER_ACTOR_NAME } from '../initializers/constants' 3import { SERVER_ACTOR_NAME } from '../initializers/constants'
@@ -12,9 +11,17 @@ import { UserNotificationSettingModel } from '../models/account/user-notificatio
12import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users' 11import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
13import { createWatchLaterPlaylist } from './video-playlist' 12import { createWatchLaterPlaylist } from './video-playlist'
14import { sequelizeTypescript } from '../initializers/database' 13import { sequelizeTypescript } from '../initializers/database'
14import { Transaction } from 'sequelize/types'
15 15
16type ChannelNames = { name: string, displayName: string } 16type ChannelNames = { name: string, displayName: string }
17async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, channelNames?: ChannelNames, validateUser = true) { 17async function createUserAccountAndChannelAndPlaylist (parameters: {
18 userToCreate: UserModel,
19 userDisplayName?: string,
20 channelNames?: ChannelNames,
21 validateUser?: boolean
22}) {
23 const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
24
18 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => { 25 const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
19 const userOptions = { 26 const userOptions = {
20 transaction: t, 27 transaction: t,
@@ -24,7 +31,13 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
24 const userCreated = await userToCreate.save(userOptions) 31 const userCreated = await userToCreate.save(userOptions)
25 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t) 32 userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t)
26 33
27 const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t) 34 const accountCreated = await createLocalAccountWithoutKeys({
35 name: userCreated.username,
36 displayName: userDisplayName,
37 userId: userCreated.id,
38 applicationId: null,
39 t: t
40 })
28 userCreated.Account = accountCreated 41 userCreated.Account = accountCreated
29 42
30 const channelAttributes = await buildChannelAttributes(userCreated, channelNames) 43 const channelAttributes = await buildChannelAttributes(userCreated, channelNames)
@@ -46,20 +59,22 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
46 return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel } 59 return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel }
47} 60}
48 61
49async function createLocalAccountWithoutKeys ( 62async function createLocalAccountWithoutKeys (parameters: {
50 name: string, 63 name: string,
64 displayName?: string,
51 userId: number | null, 65 userId: number | null,
52 applicationId: number | null, 66 applicationId: number | null,
53 t: Sequelize.Transaction | undefined, 67 t: Transaction | undefined,
54 type: ActivityPubActorType= 'Person' 68 type?: ActivityPubActorType
55) { 69}) {
70 const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters
56 const url = getAccountActivityPubUrl(name) 71 const url = getAccountActivityPubUrl(name)
57 72
58 const actorInstance = buildActorInstance(type, url, name) 73 const actorInstance = buildActorInstance(type, url, name)
59 const actorInstanceCreated = await actorInstance.save({ transaction: t }) 74 const actorInstanceCreated = await actorInstance.save({ transaction: t })
60 75
61 const accountInstance = new AccountModel({ 76 const accountInstance = new AccountModel({
62 name, 77 name: displayName || name,
63 userId, 78 userId,
64 applicationId, 79 applicationId,
65 actorId: actorInstanceCreated.id 80 actorId: actorInstanceCreated.id
@@ -72,7 +87,13 @@ async function createLocalAccountWithoutKeys (
72} 87}
73 88
74async function createApplicationActor (applicationId: number) { 89async function createApplicationActor (applicationId: number) {
75 const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationId, undefined, 'Application') 90 const accountCreated = await createLocalAccountWithoutKeys({
91 name: SERVER_ACTOR_NAME,
92 userId: null,
93 applicationId: applicationId,
94 t: undefined,
95 type: 'Application'
96 })
76 97
77 accountCreated.Actor = await setAsyncActorKeys(accountCreated.Actor) 98 accountCreated.Actor = await setAsyncActorKeys(accountCreated.Actor)
78 99
@@ -89,7 +110,7 @@ export {
89 110
90// --------------------------------------------------------------------------- 111// ---------------------------------------------------------------------------
91 112
92function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) { 113function createDefaultUserNotificationSettings (user: UserModel, t: Transaction | undefined) {
93 const values: UserNotificationSetting & { userId: number } = { 114 const values: UserNotificationSetting & { userId: number } = {
94 userId: user.id, 115 userId: user.id,
95 newVideoFromSubscription: UserNotificationSettingValue.WEB, 116 newVideoFromSubscription: UserNotificationSettingValue.WEB,
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 7a081af33..b4e09c9b7 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -53,8 +53,16 @@ const usersRegisterValidator = [
53 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'), 53 body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
54 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), 54 body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
55 body('email').isEmail().withMessage('Should have a valid email'), 55 body('email').isEmail().withMessage('Should have a valid email'),
56 body('channel.name').optional().custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'), 56 body('displayName')
57 body('channel.displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'), 57 .optional()
58 .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
59
60 body('channel.name')
61 .optional()
62 .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
63 body('channel.displayName')
64 .optional()
65 .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
58 66
59 async (req: express.Request, res: express.Response, next: express.NextFunction) => { 67 async (req: express.Request, res: express.Response, next: express.NextFunction) => {
60 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') }) 68 logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index 95097817b..3268f8c90 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -643,6 +643,7 @@ describe('Test users API validators', function () {
643 const registrationPath = path + '/register' 643 const registrationPath = path + '/register'
644 const baseCorrectParams = { 644 const baseCorrectParams = {
645 username: 'user3', 645 username: 'user3',
646 displayName: 'super user',
646 email: 'test3@example.com', 647 email: 'test3@example.com',
647 password: 'my super password' 648 password: 'my super password'
648 } 649 }
@@ -725,6 +726,12 @@ describe('Test users API validators', function () {
725 }) 726 })
726 }) 727 })
727 728
729 it('Should fail with a bad display name', async function () {
730 const fields = immutableAssign(baseCorrectParams, { displayName: 'a'.repeat(150) })
731
732 await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
733 })
734
728 it('Should fail with a bad channel name', async function () { 735 it('Should fail with a bad channel name', async function () {
729 const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } }) 736 const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } })
730 737
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index 9d2ef786f..b1f214fe2 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -17,11 +17,12 @@ import {
17 getUserInformation, 17 getUserInformation,
18 getUsersList, 18 getUsersList,
19 getUsersListPaginationAndSort, 19 getUsersListPaginationAndSort,
20 getVideoChannel,
20 getVideosList, 21 getVideosList,
21 login, 22 login,
22 makePutBodyRequest, 23 makePutBodyRequest,
23 rateVideo, 24 rateVideo,
24 registerUser, 25 registerUserWithChannel,
25 removeUser, 26 removeUser,
26 removeVideo, 27 removeVideo,
27 ServerInfo, 28 ServerInfo,
@@ -31,8 +32,7 @@ import {
31 updateMyUser, 32 updateMyUser,
32 updateUser, 33 updateUser,
33 uploadVideo, 34 uploadVideo,
34 userLogin, 35 userLogin
35 registerUserWithChannel, getVideoChannel
36} from '../../../../shared/extra-utils' 36} from '../../../../shared/extra-utils'
37import { follow } from '../../../../shared/extra-utils/server/follows' 37import { follow } from '../../../../shared/extra-utils/server/follows'
38import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login' 38import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
@@ -618,7 +618,7 @@ describe('Test users', function () {
618 618
619 describe('Registering a new user', function () { 619 describe('Registering a new user', function () {
620 it('Should register a new user', async function () { 620 it('Should register a new user', async function () {
621 const user = { username: 'user_15', password: 'my super password' } 621 const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
622 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' } 622 const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
623 623
624 await registerUserWithChannel({ url: server.url, user, channel }) 624 await registerUserWithChannel({ url: server.url, user, channel })
@@ -633,6 +633,13 @@ describe('Test users', function () {
633 accessToken = await userLogin(server, user15) 633 accessToken = await userLogin(server, user15)
634 }) 634 })
635 635
636 it('Should have the correct display name', async function () {
637 const res = await getMyUserInformation(server.url, accessToken)
638 const user: User = res.body
639
640 expect(user.account.displayName).to.equal('super user 15')
641 })
642
636 it('Should have the correct video quota', async function () { 643 it('Should have the correct video quota', async function () {
637 const res = await getMyUserInformation(server.url, accessToken) 644 const res = await getMyUserInformation(server.url, accessToken)
638 const user = res.body 645 const user = res.body
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts
index 8690327c4..f82c8cbce 100644
--- a/server/tests/api/videos/video-playlists.ts
+++ b/server/tests/api/videos/video-playlists.ts
@@ -754,7 +754,6 @@ describe('Test video playlists', function () {
754 } 754 }
755 }) 755 })
756 756
757
758 it('Should be able to create a public playlist, and set it to private', async function () { 757 it('Should be able to create a public playlist, and set it to private', async function () {
759 this.timeout(30000) 758 this.timeout(30000)
760 759
diff --git a/shared/extra-utils/users/users.ts b/shared/extra-utils/users/users.ts
index 0f2f0ae15..c09211b71 100644
--- a/shared/extra-utils/users/users.ts
+++ b/shared/extra-utils/users/users.ts
@@ -73,7 +73,7 @@ function registerUser (url: string, username: string, password: string, specialS
73 73
74function registerUserWithChannel (options: { 74function registerUserWithChannel (options: {
75 url: string, 75 url: string,
76 user: { username: string, password: string }, 76 user: { username: string, password: string, displayName?: string },
77 channel: { name: string, displayName: string } 77 channel: { name: string, displayName: string }
78}) { 78}) {
79 const path = '/api/v1/users/register' 79 const path = '/api/v1/users/register'
@@ -84,6 +84,10 @@ function registerUserWithChannel (options: {
84 channel: options.channel 84 channel: options.channel
85 } 85 }
86 86
87 if (options.user.displayName) {
88 Object.assign(body, { displayName: options.user.displayName })
89 }
90
87 return makePostBodyRequest({ 91 return makePostBodyRequest({
88 url: options.url, 92 url: options.url,
89 path, 93 path,
diff --git a/shared/models/users/user-register.model.ts b/shared/models/users/user-register.model.ts
index ce5c9c3d2..cf9a43a67 100644
--- a/shared/models/users/user-register.model.ts
+++ b/shared/models/users/user-register.model.ts
@@ -3,6 +3,8 @@ export interface UserRegister {
3 password: string 3 password: string
4 email: string 4 email: string
5 5
6 displayName?: string
7
6 channel?: { 8 channel?: {
7 name: string 9 name: string
8 displayName: string 10 displayName: string
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 5a4f6fcb2..332c0050c 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -2290,6 +2290,19 @@ components:
2290 email: 2290 email:
2291 type: string 2291 type: string
2292 description: 'The email of the user ' 2292 description: 'The email of the user '
2293 displayName:
2294 type: string
2295 description: 'The user display name'
2296 channel:
2297 type: object
2298 properties:
2299 name:
2300 type: string
2301 description: 'The default channel name'
2302 displayName:
2303 type: string
2304 description: 'The default channel display name'
2305
2293 required: 2306 required:
2294 - username 2307 - username
2295 - password 2308 - password