Browse Source

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
Chocobozzz 5 years ago
parent
commit
1f20622f2b

+ 1 - 1
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 {
 
   ngOnInit () {
     this.buildForm({
-      'display-name': this.userValidatorsService.USER_DISPLAY_NAME,
+      'display-name': this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED,
       description: this.userValidatorsService.USER_DESCRIPTION
     })
 

+ 19 - 15
client/src/app/+signup/+register/register-step-channel.component.html

@@ -11,6 +11,21 @@
     </p>
   </div>
 
+  <div class="form-group">
+    <label for="displayName" i18n>Channel display name</label>
+
+    <div class="input-group">
+      <input
+        type="text" id="displayName"
+        formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
+      >
+    </div>
+
+    <div *ngIf="formErrors.displayName" class="form-error">
+      {{ formErrors.displayName }}
+    </div>
+  </div>
+
   <div class="form-group">
     <label for="name" i18n>Channel name</label>
 
@@ -24,6 +39,10 @@
       </div>
     </div>
 
+    <div class="name-information" i18n>
+      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.
+    </div>
+
     <div *ngIf="formErrors.name" class="form-error">
       {{ formErrors.name }}
     </div>
@@ -32,19 +51,4 @@
       Channel name cannot be the same than your account name. You can click on the first step to update your account name.
     </div>
   </div>
-
-  <div class="form-group">
-    <label for="displayName" i18n>Channel display name</label>
-
-    <div class="input-group">
-      <input
-        type="text" id="displayName"
-        formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
-      >
-    </div>
-
-    <div *ngIf="formErrors.displayName" class="form-error">
-      {{ formErrors.displayName }}
-    </div>
-  </div>
 </form>

+ 23 - 7
client/src/app/+signup/+register/register-step-channel.component.ts

@@ -1,8 +1,10 @@
 import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
 import { AuthService } from '@app/core'
-import { FormReactive, VideoChannelValidatorsService } from '@app/shared'
+import { FormReactive, UserService, VideoChannelValidatorsService } from '@app/shared'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 import { FormGroup } from '@angular/forms'
+import { pairwise } from 'rxjs/operators'
+import { concat, of } from 'rxjs'
 
 @Component({
   selector: 'my-register-step-channel',
@@ -16,6 +18,7 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
   constructor (
     protected formValidatorService: FormValidatorService,
     private authService: AuthService,
+    private userService: UserService,
     private videoChannelValidatorsService: VideoChannelValidatorsService
   ) {
     super()
@@ -25,16 +28,29 @@ export class RegisterStepChannelComponent extends FormReactive implements OnInit
     return window.location.host
   }
 
-  isSameThanUsername () {
-    return this.username && this.username === this.form.value['name']
-  }
-
   ngOnInit () {
     this.buildForm({
-      name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME,
-      displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME
+      displayName: this.videoChannelValidatorsService.VIDEO_CHANNEL_DISPLAY_NAME,
+      name: this.videoChannelValidatorsService.VIDEO_CHANNEL_NAME
     })
 
     setTimeout(() => this.formBuilt.emit(this.form))
+
+    concat(
+      of(''),
+      this.form.get('displayName').valueChanges
+    ).pipe(pairwise())
+     .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
+  }
+
+  isSameThanUsername () {
+    return this.username && this.username === this.form.value['name']
+  }
+
+  private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
+    const name = this.form.value['name'] || ''
+
+    const newName = this.userService.getNewUsername(oldDisplayName, newDisplayName, name)
+    this.form.patchValue({ name: newName })
   }
 }

+ 19 - 0
client/src/app/+signup/+register/register-step-user.component.html

@@ -1,5 +1,20 @@
 <form role="form" [formGroup]="form">
 
+  <div class="form-group">
+    <label for="displayName" i18n>Display name</label>
+
+    <div class="input-group">
+      <input
+        type="text" id="displayName" placeholder="John Doe"
+        formControlName="displayName" [ngClass]="{ 'input-error': formErrors['displayName'] }"
+      >
+    </div>
+
+    <div *ngIf="formErrors.displayName" class="form-error">
+      {{ formErrors.displayName }}
+    </div>
+  </div>
+
   <div class="form-group">
     <label for="username" i18n>Username</label>
 
@@ -13,6 +28,10 @@
       </div>
     </div>
 
+    <div class="name-information" i18n>
+      The username is a unique identifier of your account on this instance. It's like an address mail, so other people can find you.
+    </div>
+
     <div *ngIf="formErrors.username" class="form-error">
       {{ formErrors.username }}
     </div>

+ 18 - 1
client/src/app/+signup/+register/register-step-user.component.ts

@@ -1,8 +1,10 @@
 import { Component, EventEmitter, OnInit, Output } from '@angular/core'
 import { AuthService } from '@app/core'
-import { FormReactive, UserValidatorsService } from '@app/shared'
+import { FormReactive, UserService, UserValidatorsService } from '@app/shared'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 import { FormGroup } from '@angular/forms'
+import { pairwise } from 'rxjs/operators'
+import { concat, of } from 'rxjs'
 
 @Component({
   selector: 'my-register-step-user',
@@ -15,6 +17,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
   constructor (
     protected formValidatorService: FormValidatorService,
     private authService: AuthService,
+    private userService: UserService,
     private userValidatorsService: UserValidatorsService
   ) {
     super()
@@ -26,6 +29,7 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
 
   ngOnInit () {
     this.buildForm({
+      displayName: this.userValidatorsService.USER_DISPLAY_NAME_REQUIRED,
       username: this.userValidatorsService.USER_USERNAME,
       password: this.userValidatorsService.USER_PASSWORD,
       email: this.userValidatorsService.USER_EMAIL,
@@ -33,5 +37,18 @@ export class RegisterStepUserComponent extends FormReactive implements OnInit {
     })
 
     setTimeout(() => this.formBuilt.emit(this.form))
+
+    concat(
+      of(''),
+      this.form.get('displayName').valueChanges
+    ).pipe(pairwise())
+     .subscribe(([ oldValue, newValue ]) => this.onDisplayNameChange(oldValue, newValue))
+  }
+
+  private onDisplayNameChange (oldDisplayName: string, newDisplayName: string) {
+    const username = this.form.value['username'] || ''
+
+    const newUsername = this.userService.getNewUsername(oldDisplayName, newDisplayName, username)
+    this.form.patchValue({ username: newUsername })
   }
 }

+ 6 - 0
client/src/app/+signup/+register/register.component.html

@@ -27,6 +27,12 @@
         </cdk-step>
 
         <cdk-step i18n-label label="Done" editable="false">
+          <div *ngIf="!signupDone && !error" class="done-loader">
+            <my-loader [loading]="true"></my-loader>
+
+            <div i18n>PeerTube is creating your account...</div>
+          </div>
+
           <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
         </cdk-step>
       </my-custom-stepper>

+ 23 - 0
client/src/app/+signup/+register/register.component.scss

@@ -56,3 +56,26 @@ button {
   @include peertube-button;
   @include orange-button;
 }
+
+.name-information {
+  margin-top: 10px;
+}
+
+.done-loader {
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  align-items: center;
+
+  my-loader {
+    margin-bottom: 20px;
+
+    /deep/ .loader div {
+      border-color: var(--mainColor) transparent transparent transparent;
+    }
+
+    & + div {
+      font-size: 15px;
+    }
+  }
+}

+ 20 - 13
client/src/app/shared/forms/form-validators/user-validators.service.ts

@@ -12,7 +12,7 @@ export class UserValidatorsService {
   readonly USER_VIDEO_QUOTA: BuildFormValidator
   readonly USER_VIDEO_QUOTA_DAILY: BuildFormValidator
   readonly USER_ROLE: BuildFormValidator
-  readonly USER_DISPLAY_NAME: BuildFormValidator
+  readonly USER_DISPLAY_NAME_REQUIRED: BuildFormValidator
   readonly USER_DESCRIPTION: BuildFormValidator
   readonly USER_TERMS: BuildFormValidator
 
@@ -85,18 +85,7 @@ export class UserValidatorsService {
       }
     }
 
-    this.USER_DISPLAY_NAME = {
-      VALIDATORS: [
-        Validators.required,
-        Validators.minLength(1),
-        Validators.maxLength(50)
-      ],
-      MESSAGES: {
-        'required': this.i18n('Display name is required.'),
-        'minlength': this.i18n('Display name must be at least 1 character long.'),
-        'maxlength': this.i18n('Display name cannot be more than 50 characters long.')
-      }
-    }
+    this.USER_DISPLAY_NAME_REQUIRED = this.getDisplayName(true)
 
     this.USER_DESCRIPTION = {
       VALIDATORS: [
@@ -129,4 +118,22 @@ export class UserValidatorsService {
       }
     }
   }
+
+  private getDisplayName (required: boolean) {
+    const control = {
+      VALIDATORS: [
+        Validators.minLength(1),
+        Validators.maxLength(120)
+      ],
+      MESSAGES: {
+        'required': this.i18n('Display name is required.'),
+        'minlength': this.i18n('Display name must be at least 1 character long.'),
+        'maxlength': this.i18n('Display name cannot be more than 50 characters long.')
+      }
+    }
+
+    if (required) control.VALIDATORS.push(Validators.required)
+
+    return control
+  }
 }

+ 1 - 1
client/src/app/shared/misc/loader.component.html

@@ -1,5 +1,5 @@
 <div *ngIf="loading">
-  <div class="lds-ring">
+  <div class="loader">
     <div></div>
     <div></div>
     <div></div>

+ 7 - 7
client/src/app/shared/misc/loader.component.scss

@@ -3,14 +3,14 @@
 
 // Thanks to https://loading.io/css/ (CC0 License)
 
-.lds-ring {
+.loader {
   display: inline-block;
   position: relative;
   width: 50px;
   height: 50px;
 }
 
-.lds-ring div {
+.loader div {
   box-sizing: border-box;
   display: block;
   position: absolute;
@@ -19,23 +19,23 @@
   margin: 6px;
   border: 4px solid;
   border-radius: 50%;
-  animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+  animation: loader 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
   border-color: #999999 transparent transparent transparent;
 }
 
-.lds-ring div:nth-child(1) {
+.loader div:nth-child(1) {
   animation-delay: -0.45s;
 }
 
-.lds-ring div:nth-child(2) {
+.loader div:nth-child(2) {
   animation-delay: -0.3s;
 }
 
-.lds-ring div:nth-child(3) {
+.loader div:nth-child(3) {
   animation-delay: -0.15s;
 }
 
-@keyframes lds-ring {
+@keyframes loader {
   0% {
     transform: rotate(0deg);
   }

+ 16 - 0
client/src/app/shared/users/user.service.ts

@@ -136,6 +136,22 @@ export class UserService {
       .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) {

+ 0 - 1
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
           previewUrl: null
         }))
 
-
         this.hydrateFormFromVideo()
       },
 

+ 6 - 2
server/controllers/api/users/index.ts

@@ -184,7 +184,7 @@ async function createUser (req: express.Request, res: express.Response) {
     adminFlags: body.adminFlags || UserAdminFlag.NONE
   })
 
-  const { user, account } = await createUserAccountAndChannelAndPlaylist(userToCreate)
+  const { user, account } = await createUserAccountAndChannelAndPlaylist({ userToCreate: userToCreate })
 
   auditLogger.create(getAuditIdFromRes(res), new UserAuditView(user.toFormattedJSON()))
   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) {
     emailVerified: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION ? false : null
   })
 
-  const { user } = await createUserAccountAndChannelAndPlaylist(userToCreate, body.channel)
+  const { user } = await createUserAccountAndChannelAndPlaylist({
+    userToCreate: userToCreate,
+    userDisplayName: body.displayName || undefined,
+    channelNames: body.channel
+  })
 
   auditLogger.create(body.username, new UserAuditView(user.toFormattedJSON()))
   logger.info('User %s with its channel and account registered.', body.username)

+ 1 - 1
server/initializers/installer.ts

@@ -146,7 +146,7 @@ async function createOAuthAdminIfNotExist () {
   }
   const user = new UserModel(userData)
 
-  await createUserAccountAndChannelAndPlaylist(user, undefined, validatePassword)
+  await createUserAccountAndChannelAndPlaylist({ userToCreate: user, channelNames: undefined, validateUser: validatePassword })
   logger.info('Username: ' + username)
   logger.info('User password: ' + password)
 }

+ 7 - 2
server/initializers/migrations/0100-activitypub.ts

@@ -65,7 +65,12 @@ async function up (utils: {
   // Create application account
   {
     const applicationInstance = await ApplicationModel.findOne()
-    const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationInstance.id, undefined)
+    const accountCreated = await createLocalAccountWithoutKeys({
+      name: SERVER_ACTOR_NAME,
+      userId: null,
+      applicationId: applicationInstance.id,
+      t: undefined
+    })
 
     const { publicKey, privateKey } = await createPrivateAndPublicKeys()
     accountCreated.Actor.publicKey = publicKey
@@ -83,7 +88,7 @@ async function up (utils: {
   // Recreate accounts for each user
   const users = await db.User.findAll()
   for (const user of users) {
-    const account = await createLocalAccountWithoutKeys(user.username, user.id, null, undefined)
+    const account = await createLocalAccountWithoutKeys({ name: user.username, userId: user.id, applicationId: null, t: undefined })
 
     const { publicKey, privateKey } = await createPrivateAndPublicKeys()
     account.Actor.publicKey = publicKey

+ 31 - 10
server/lib/user.ts

@@ -1,4 +1,3 @@
-import * as Sequelize from 'sequelize'
 import * as uuidv4 from 'uuid/v4'
 import { ActivityPubActorType } from '../../shared/models/activitypub'
 import { SERVER_ACTOR_NAME } from '../initializers/constants'
@@ -12,9 +11,17 @@ import { UserNotificationSettingModel } from '../models/account/user-notificatio
 import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
 import { createWatchLaterPlaylist } from './video-playlist'
 import { sequelizeTypescript } from '../initializers/database'
+import { Transaction } from 'sequelize/types'
 
 type ChannelNames = { name: string, displayName: string }
-async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel, channelNames?: ChannelNames, validateUser = true) {
+async function createUserAccountAndChannelAndPlaylist (parameters: {
+  userToCreate: UserModel,
+  userDisplayName?: string,
+  channelNames?: ChannelNames,
+  validateUser?: boolean
+}) {
+  const { userToCreate, userDisplayName, channelNames, validateUser = true } = parameters
+
   const { user, account, videoChannel } = await sequelizeTypescript.transaction(async t => {
     const userOptions = {
       transaction: t,
@@ -24,7 +31,13 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
     const userCreated = await userToCreate.save(userOptions)
     userCreated.NotificationSetting = await createDefaultUserNotificationSettings(userCreated, t)
 
-    const accountCreated = await createLocalAccountWithoutKeys(userCreated.username, userCreated.id, null, t)
+    const accountCreated = await createLocalAccountWithoutKeys({
+      name: userCreated.username,
+      displayName: userDisplayName,
+      userId: userCreated.id,
+      applicationId: null,
+      t: t
+    })
     userCreated.Account = accountCreated
 
     const channelAttributes = await buildChannelAttributes(userCreated, channelNames)
@@ -46,20 +59,22 @@ async function createUserAccountAndChannelAndPlaylist (userToCreate: UserModel,
   return { user, account, videoChannel } as { user: UserModel, account: AccountModel, videoChannel: VideoChannelModel }
 }
 
-async function createLocalAccountWithoutKeys (
+async function createLocalAccountWithoutKeys (parameters: {
   name: string,
+  displayName?: string,
   userId: number | null,
   applicationId: number | null,
-  t: Sequelize.Transaction | undefined,
-  type: ActivityPubActorType= 'Person'
-) {
+  t: Transaction | undefined,
+  type?: ActivityPubActorType
+}) {
+  const { name, displayName, userId, applicationId, t, type = 'Person' } = parameters
   const url = getAccountActivityPubUrl(name)
 
   const actorInstance = buildActorInstance(type, url, name)
   const actorInstanceCreated = await actorInstance.save({ transaction: t })
 
   const accountInstance = new AccountModel({
-    name,
+    name: displayName || name,
     userId,
     applicationId,
     actorId: actorInstanceCreated.id
@@ -72,7 +87,13 @@ async function createLocalAccountWithoutKeys (
 }
 
 async function createApplicationActor (applicationId: number) {
-  const accountCreated = await createLocalAccountWithoutKeys(SERVER_ACTOR_NAME, null, applicationId, undefined, 'Application')
+  const accountCreated = await createLocalAccountWithoutKeys({
+    name: SERVER_ACTOR_NAME,
+    userId: null,
+    applicationId: applicationId,
+    t: undefined,
+    type: 'Application'
+  })
 
   accountCreated.Actor = await setAsyncActorKeys(accountCreated.Actor)
 
@@ -89,7 +110,7 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function createDefaultUserNotificationSettings (user: UserModel, t: Sequelize.Transaction | undefined) {
+function createDefaultUserNotificationSettings (user: UserModel, t: Transaction | undefined) {
   const values: UserNotificationSetting & { userId: number } = {
     userId: user.id,
     newVideoFromSubscription: UserNotificationSettingValue.WEB,

+ 10 - 2
server/middlewares/validators/users.ts

@@ -53,8 +53,16 @@ const usersRegisterValidator = [
   body('username').custom(isUserUsernameValid).withMessage('Should have a valid username'),
   body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
   body('email').isEmail().withMessage('Should have a valid email'),
-  body('channel.name').optional().custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
-  body('channel.displayName').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
+  body('displayName')
+    .optional()
+    .custom(isUserDisplayNameValid).withMessage('Should have a valid display name'),
+
+  body('channel.name')
+    .optional()
+    .custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
+  body('channel.displayName')
+    .optional()
+    .custom(isVideoChannelNameValid).withMessage('Should have a valid display name'),
 
   async (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersRegister parameters', { parameters: omit(req.body, 'password') })

+ 7 - 0
server/tests/api/check-params/users.ts

@@ -643,6 +643,7 @@ describe('Test users API validators', function () {
     const registrationPath = path + '/register'
     const baseCorrectParams = {
       username: 'user3',
+      displayName: 'super user',
       email: 'test3@example.com',
       password: 'my super password'
     }
@@ -725,6 +726,12 @@ describe('Test users API validators', function () {
       })
     })
 
+    it('Should fail with a bad display name', async function () {
+      const fields = immutableAssign(baseCorrectParams, { displayName: 'a'.repeat(150) })
+
+      await makePostBodyRequest({ url: server.url, path: registrationPath, token: server.accessToken, fields })
+    })
+
     it('Should fail with a bad channel name', async function () {
       const fields = immutableAssign(baseCorrectParams, { channel: { name: '[]azf', displayName: 'toto' } })
 

+ 11 - 4
server/tests/api/users/users.ts

@@ -17,11 +17,12 @@ import {
   getUserInformation,
   getUsersList,
   getUsersListPaginationAndSort,
+  getVideoChannel,
   getVideosList,
   login,
   makePutBodyRequest,
   rateVideo,
-  registerUser,
+  registerUserWithChannel,
   removeUser,
   removeVideo,
   ServerInfo,
@@ -31,8 +32,7 @@ import {
   updateMyUser,
   updateUser,
   uploadVideo,
-  userLogin,
-  registerUserWithChannel, getVideoChannel
+  userLogin
 } from '../../../../shared/extra-utils'
 import { follow } from '../../../../shared/extra-utils/server/follows'
 import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
@@ -618,7 +618,7 @@ describe('Test users', function () {
 
   describe('Registering a new user', function () {
     it('Should register a new user', async function () {
-      const user = { username: 'user_15', password: 'my super password' }
+      const user = { displayName: 'super user 15', username: 'user_15', password: 'my super password' }
       const channel = { name: 'my_user_15_channel', displayName: 'my channel rocks' }
 
       await registerUserWithChannel({ url: server.url, user, channel })
@@ -633,6 +633,13 @@ describe('Test users', function () {
       accessToken = await userLogin(server, user15)
     })
 
+    it('Should have the correct display name', async function () {
+      const res = await getMyUserInformation(server.url, accessToken)
+      const user: User = res.body
+
+      expect(user.account.displayName).to.equal('super user 15')
+    })
+
     it('Should have the correct video quota', async function () {
       const res = await getMyUserInformation(server.url, accessToken)
       const user = res.body

+ 0 - 1
server/tests/api/videos/video-playlists.ts

@@ -754,7 +754,6 @@ describe('Test video playlists', function () {
     }
   })
 
-
   it('Should be able to create a public playlist, and set it to private', async function () {
     this.timeout(30000)
 

+ 5 - 1
shared/extra-utils/users/users.ts

@@ -73,7 +73,7 @@ function registerUser (url: string, username: string, password: string, specialS
 
 function registerUserWithChannel (options: {
   url: string,
-  user: { username: string, password: string },
+  user: { username: string, password: string, displayName?: string },
   channel: { name: string, displayName: string }
 }) {
   const path = '/api/v1/users/register'
@@ -84,6 +84,10 @@ function registerUserWithChannel (options: {
     channel: options.channel
   }
 
+  if (options.user.displayName) {
+    Object.assign(body, { displayName: options.user.displayName })
+  }
+
   return makePostBodyRequest({
     url: options.url,
     path,

+ 2 - 0
shared/models/users/user-register.model.ts

@@ -3,6 +3,8 @@ export interface UserRegister {
   password: string
   email: string
 
+  displayName?: string
+
   channel?: {
     name: string
     displayName: string

+ 13 - 0
support/doc/api/openapi.yaml

@@ -2290,6 +2290,19 @@ components:
         email:
           type: string
           description: 'The email of the user '
+        displayName:
+          type: string
+          description: 'The user display name'
+        channel:
+          type: object
+          properties:
+            name:
+              type: string
+              description: 'The default channel name'
+            displayName:
+              type: string
+              description: 'The default channel display name'
+
       required:
         - username
         - password