aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+accounts/accounts.component.html5
-rw-r--r--client/src/app/+accounts/accounts.component.scss12
-rw-r--r--client/src/app/+accounts/accounts.component.ts48
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html3
-rw-r--r--client/src/app/shared/account/account.model.ts3
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.html2
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.scss6
-rw-r--r--client/src/app/shared/buttons/action-dropdown.component.ts1
-rw-r--r--client/src/app/shared/moderation/index.ts2
-rw-r--r--client/src/app/shared/moderation/user-ban-modal.component.ts3
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.html6
-rw-r--r--client/src/app/shared/moderation/user-moderation-dropdown.component.ts69
-rw-r--r--client/src/app/shared/users/user.service.ts6
-rw-r--r--server/models/account/account.ts3
-rw-r--r--server/models/video/video-format-utils.ts2
-rw-r--r--server/models/video/video.ts1
-rw-r--r--server/tests/api/users/users-multiple-servers.ts6
-rw-r--r--shared/models/actors/account.model.ts2
18 files changed, 132 insertions, 48 deletions
diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html
index 69f648269..036e794d2 100644
--- a/client/src/app/+accounts/accounts.component.html
+++ b/client/src/app/+accounts/accounts.component.html
@@ -8,6 +8,11 @@
8 <div class="actor-names"> 8 <div class="actor-names">
9 <div class="actor-display-name">{{ account.displayName }}</div> 9 <div class="actor-display-name">{{ account.displayName }}</div>
10 <div class="actor-name">{{ account.nameWithHost }}</div> 10 <div class="actor-name">{{ account.nameWithHost }}</div>
11
12 <span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
13
14 <my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()">
15 </my-user-moderation-dropdown>
11 </div> 16 </div>
12 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div> 17 <div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
13 </div> 18 </div>
diff --git a/client/src/app/+accounts/accounts.component.scss b/client/src/app/+accounts/accounts.component.scss
index 909b65bc7..3cedda889 100644
--- a/client/src/app/+accounts/accounts.component.scss
+++ b/client/src/app/+accounts/accounts.component.scss
@@ -3,4 +3,16 @@
3 3
4.sub-menu { 4.sub-menu {
5 @include sub-menu-with-actor; 5 @include sub-menu-with-actor;
6}
7
8my-user-moderation-dropdown,
9.badge {
10 margin-left: 10px;
11
12 position: relative;
13 top: 3px;
14}
15
16.badge {
17 font-size: 13px;
6} \ No newline at end of file 18} \ No newline at end of file
diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts
index af0451e91..e19927d6b 100644
--- a/client/src/app/+accounts/accounts.component.ts
+++ b/client/src/app/+accounts/accounts.component.ts
@@ -1,10 +1,14 @@
1import { Component, OnInit, OnDestroy } from '@angular/core' 1import { Component, OnDestroy, OnInit } from '@angular/core'
2import { ActivatedRoute } from '@angular/router' 2import { ActivatedRoute } from '@angular/router'
3import { AccountService } from '@app/shared/account/account.service' 3import { AccountService } from '@app/shared/account/account.service'
4import { Account } from '@app/shared/account/account.model' 4import { Account } from '@app/shared/account/account.model'
5import { RestExtractor } from '@app/shared' 5import { RestExtractor, UserService } from '@app/shared'
6import { catchError, switchMap, distinctUntilChanged, map } from 'rxjs/operators' 6import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
7import { Subscription } from 'rxjs' 7import { Subscription } from 'rxjs'
8import { NotificationsService } from 'angular2-notifications'
9import { User, UserRight } from '../../../../shared'
10import { I18n } from '@ngx-translate/i18n-polyfill'
11import { AuthService, RedirectService } from '@app/core'
8 12
9@Component({ 13@Component({
10 templateUrl: './accounts.component.html', 14 templateUrl: './accounts.component.html',
@@ -12,13 +16,19 @@ import { Subscription } from 'rxjs'
12}) 16})
13export class AccountsComponent implements OnInit, OnDestroy { 17export class AccountsComponent implements OnInit, OnDestroy {
14 account: Account 18 account: Account
19 user: User
15 20
16 private routeSub: Subscription 21 private routeSub: Subscription
17 22
18 constructor ( 23 constructor (
19 private route: ActivatedRoute, 24 private route: ActivatedRoute,
25 private userService: UserService,
20 private accountService: AccountService, 26 private accountService: AccountService,
21 private restExtractor: RestExtractor 27 private notificationsService: NotificationsService,
28 private restExtractor: RestExtractor,
29 private redirectService: RedirectService,
30 private authService: AuthService,
31 private i18n: I18n
22 ) {} 32 ) {}
23 33
24 ngOnInit () { 34 ngOnInit () {
@@ -27,12 +37,40 @@ export class AccountsComponent implements OnInit, OnDestroy {
27 map(params => params[ 'accountId' ]), 37 map(params => params[ 'accountId' ]),
28 distinctUntilChanged(), 38 distinctUntilChanged(),
29 switchMap(accountId => this.accountService.getAccount(accountId)), 39 switchMap(accountId => this.accountService.getAccount(accountId)),
40 tap(account => this.getUserIfNeeded(account)),
30 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) 41 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
31 ) 42 )
32 .subscribe(account => this.account = account) 43 .subscribe(
44 account => this.account = account,
45
46 err => this.notificationsService.error(this.i18n('Error'), err.message)
47 )
33 } 48 }
34 49
35 ngOnDestroy () { 50 ngOnDestroy () {
36 if (this.routeSub) this.routeSub.unsubscribe() 51 if (this.routeSub) this.routeSub.unsubscribe()
37 } 52 }
53
54 onUserChanged () {
55 this.getUserIfNeeded(this.account)
56 }
57
58 onUserDeleted () {
59 this.redirectService.redirectToHomepage()
60 }
61
62 private getUserIfNeeded (account: Account) {
63 if (!account.userId) return
64 if (!this.authService.isLoggedIn()) return
65
66 const user = this.authService.getUser()
67 if (user.hasRight(UserRight.MANAGE_USERS)) {
68 this.userService.getUser(account.userId)
69 .subscribe(
70 user => this.user = user,
71
72 err => this.notificationsService.error(this.i18n('Error'), err.message)
73 )
74 }
75 }
38} 76}
diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html
index 2479ce9e4..cca057ba1 100644
--- a/client/src/app/+admin/users/user-list/user-list.component.html
+++ b/client/src/app/+admin/users/user-list/user-list.component.html
@@ -40,7 +40,8 @@
40 <td>{{ user.roleLabel }}</td> 40 <td>{{ user.roleLabel }}</td>
41 <td>{{ user.createdAt }}</td> 41 <td>{{ user.createdAt }}</td>
42 <td class="action-cell"> 42 <td class="action-cell">
43 <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()"></my-user-moderation-dropdown> 43 <my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
44 </my-user-moderation-dropdown>
44 </td> 45 </td>
45 </tr> 46 </tr>
46 </ng-template> 47 </ng-template>
diff --git a/client/src/app/shared/account/account.model.ts b/client/src/app/shared/account/account.model.ts
index 5058e372f..42f2cfeaf 100644
--- a/client/src/app/shared/account/account.model.ts
+++ b/client/src/app/shared/account/account.model.ts
@@ -6,11 +6,14 @@ export class Account extends Actor implements ServerAccount {
6 description: string 6 description: string
7 nameWithHost: string 7 nameWithHost: string
8 8
9 userId?: number
10
9 constructor (hash: ServerAccount) { 11 constructor (hash: ServerAccount) {
10 super(hash) 12 super(hash)
11 13
12 this.displayName = hash.displayName 14 this.displayName = hash.displayName
13 this.description = hash.description 15 this.description = hash.description
16 this.userId = hash.userId
14 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host) 17 this.nameWithHost = Actor.CREATE_BY_STRING(this.name, this.host)
15 } 18 }
16} 19}
diff --git a/client/src/app/shared/buttons/action-dropdown.component.html b/client/src/app/shared/buttons/action-dropdown.component.html
index 8b7241379..8110e2515 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.html
+++ b/client/src/app/shared/buttons/action-dropdown.component.html
@@ -1,5 +1,5 @@
1<div class="dropdown-root" ngbDropdown [placement]="placement"> 1<div class="dropdown-root" ngbDropdown [placement]="placement">
2 <div class="action-button" ngbDropdownToggle role="button"> 2 <div class="action-button" [ngClass]="{ small: buttonSize === 'small' }" ngbDropdownToggle role="button">
3 <span class="icon icon-action"></span> 3 <span class="icon icon-action"></span>
4 </div> 4 </div>
5 5
diff --git a/client/src/app/shared/buttons/action-dropdown.component.scss b/client/src/app/shared/buttons/action-dropdown.component.scss
index 615511093..00f120fb8 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.scss
+++ b/client/src/app/shared/buttons/action-dropdown.component.scss
@@ -22,6 +22,12 @@
22 background-image: url('../../../assets/images/video/more.svg'); 22 background-image: url('../../../assets/images/video/more.svg');
23 top: -1px; 23 top: -1px;
24 } 24 }
25
26 &.small {
27 font-size: 14px;
28 height: 20px;
29 line-height: 20px;
30 }
25} 31}
26 32
27.dropdown-menu { 33.dropdown-menu {
diff --git a/client/src/app/shared/buttons/action-dropdown.component.ts b/client/src/app/shared/buttons/action-dropdown.component.ts
index 17f9cc618..1838ff697 100644
--- a/client/src/app/shared/buttons/action-dropdown.component.ts
+++ b/client/src/app/shared/buttons/action-dropdown.component.ts
@@ -17,4 +17,5 @@ export class ActionDropdownComponent<T> {
17 @Input() actions: DropdownAction<T>[] = [] 17 @Input() actions: DropdownAction<T>[] = []
18 @Input() entry: T 18 @Input() entry: T
19 @Input() placement = 'left' 19 @Input() placement = 'left'
20 @Input() buttonSize: 'normal' | 'small' = 'normal'
20} 21}
diff --git a/client/src/app/shared/moderation/index.ts b/client/src/app/shared/moderation/index.ts
index 2245294c5..9a77c64c0 100644
--- a/client/src/app/shared/moderation/index.ts
+++ b/client/src/app/shared/moderation/index.ts
@@ -1,2 +1,2 @@
1export * from './user-ban-modal.component' 1export * from './user-ban-modal.component'
2export * from './user-moderation-dropdown.component' \ No newline at end of file 2export * from './user-moderation-dropdown.component'
diff --git a/client/src/app/shared/moderation/user-ban-modal.component.ts b/client/src/app/shared/moderation/user-ban-modal.component.ts
index d49783cd2..67ae38e48 100644
--- a/client/src/app/shared/moderation/user-ban-modal.component.ts
+++ b/client/src/app/shared/moderation/user-ban-modal.component.ts
@@ -5,7 +5,8 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' 6import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
7import { FormReactive, UserValidatorsService } from '@app/shared/forms' 7import { FormReactive, UserValidatorsService } from '@app/shared/forms'
8import { User, UserService } from '@app/shared/users' 8import { UserService } from '@app/shared/users'
9import { User } from '../../../../../shared'
9 10
10@Component({ 11@Component({
11 selector: 'my-user-ban-modal', 12 selector: 'my-user-ban-modal',
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.html b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
index ed8a4dc66..ed1a4c863 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.html
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.html
@@ -1,3 +1,5 @@
1<my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal> 1<ng-container *ngIf="user && userActions.length !== 0">
2 <my-user-ban-modal #userBanModal (userBanned)="onUserBanned()"></my-user-ban-modal>
2 3
3<my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user"></my-action-dropdown> \ No newline at end of file 4 <my-action-dropdown i18n-label label="Actions" [actions]="userActions" [entry]="user" [buttonSize]="buttonSize"></my-action-dropdown>
5</ng-container> \ No newline at end of file
diff --git a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
index d92423476..4f88456de 100644
--- a/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
+++ b/client/src/app/shared/moderation/user-moderation-dropdown.component.ts
@@ -4,9 +4,9 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 4import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref' 5import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
6import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component' 6import { UserBanModalComponent } from '@app/shared/moderation/user-ban-modal.component'
7import { User, UserService } from '@app/shared/users' 7import { UserService } from '@app/shared/users'
8import { AuthService, ConfirmService } from '@app/core' 8import { AuthService, ConfirmService } from '@app/core'
9import { UserRight } from '../../../../../shared/models/users' 9import { User, UserRight } from '../../../../../shared/models/users'
10 10
11@Component({ 11@Component({
12 selector: 'my-user-moderation-dropdown', 12 selector: 'my-user-moderation-dropdown',
@@ -17,7 +17,10 @@ export class UserModerationDropdownComponent implements OnInit {
17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent 17 @ViewChild('userBanModal') userBanModal: UserBanModalComponent
18 18
19 @Input() user: User 19 @Input() user: User
20 @Input() buttonSize: 'normal' | 'small' = 'normal'
21
20 @Output() userChanged = new EventEmitter() 22 @Output() userChanged = new EventEmitter()
23 @Output() userDeleted = new EventEmitter()
21 24
22 userActions: DropdownAction<User>[] = [] 25 userActions: DropdownAction<User>[] = []
23 26
@@ -32,34 +35,7 @@ export class UserModerationDropdownComponent implements OnInit {
32 ) { } 35 ) { }
33 36
34 ngOnInit () { 37 ngOnInit () {
35 this.userActions = [] 38 this.buildActions()
36
37 if (this.authService.isLoggedIn()) {
38 const authUser = this.authService.getUser()
39
40 if (authUser.hasRight(UserRight.MANAGE_USERS)) {
41 this.userActions = this.userActions.concat([
42 {
43 label: this.i18n('Edit'),
44 linkBuilder: this.getRouterUserEditLink
45 },
46 {
47 label: this.i18n('Delete'),
48 handler: user => this.removeUser(user)
49 },
50 {
51 label: this.i18n('Ban'),
52 handler: user => this.openBanUserModal(user),
53 isDisplayed: user => !user.blocked
54 },
55 {
56 label: this.i18n('Unban'),
57 handler: user => this.unbanUser(user),
58 isDisplayed: user => user.blocked
59 }
60 ])
61 }
62 }
63 } 39 }
64 40
65 hideBanUserModal () { 41 hideBanUserModal () {
@@ -115,7 +91,7 @@ export class UserModerationDropdownComponent implements OnInit {
115 this.i18n('Success'), 91 this.i18n('Success'),
116 this.i18n('User {{username}} deleted.', { username: user.username }) 92 this.i18n('User {{username}} deleted.', { username: user.username })
117 ) 93 )
118 this.userChanged.emit() 94 this.userDeleted.emit()
119 }, 95 },
120 96
121 err => this.notificationsService.error(this.i18n('Error'), err.message) 97 err => this.notificationsService.error(this.i18n('Error'), err.message)
@@ -125,4 +101,35 @@ export class UserModerationDropdownComponent implements OnInit {
125 getRouterUserEditLink (user: User) { 101 getRouterUserEditLink (user: User) {
126 return [ '/admin', 'users', 'update', user.id ] 102 return [ '/admin', 'users', 'update', user.id ]
127 } 103 }
104
105 private buildActions () {
106 this.userActions = []
107
108 if (this.authService.isLoggedIn()) {
109 const authUser = this.authService.getUser()
110
111 if (authUser.hasRight(UserRight.MANAGE_USERS)) {
112 this.userActions = this.userActions.concat([
113 {
114 label: this.i18n('Edit'),
115 linkBuilder: this.getRouterUserEditLink
116 },
117 {
118 label: this.i18n('Delete'),
119 handler: user => this.removeUser(user)
120 },
121 {
122 label: this.i18n('Ban'),
123 handler: user => this.openBanUserModal(user),
124 isDisplayed: user => !user.blocked
125 },
126 {
127 label: this.i18n('Unban'),
128 handler: user => this.unbanUser(user),
129 isDisplayed: user => user.blocked
130 }
131 ])
132 }
133 }
134 }
128} 135}
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 5ab290a59..d9b81c181 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -170,19 +170,19 @@ export class UserService {
170 ) 170 )
171 } 171 }
172 172
173 removeUser (user: User) { 173 removeUser (user: { id: number }) {
174 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) 174 return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
175 .pipe(catchError(err => this.restExtractor.handleError(err))) 175 .pipe(catchError(err => this.restExtractor.handleError(err)))
176 } 176 }
177 177
178 banUser (user: User, reason?: string) { 178 banUser (user: { id: number }, reason?: string) {
179 const body = reason ? { reason } : {} 179 const body = reason ? { reason } : {}
180 180
181 return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body) 181 return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/block', body)
182 .pipe(catchError(err => this.restExtractor.handleError(err))) 182 .pipe(catchError(err => this.restExtractor.handleError(err)))
183 } 183 }
184 184
185 unbanUser (user: User) { 185 unbanUser (user: { id: number }) {
186 return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {}) 186 return this.authHttp.post(UserService.BASE_USERS_URL + user.id + '/unblock', {})
187 .pipe(catchError(err => this.restExtractor.handleError(err))) 187 .pipe(catchError(err => this.restExtractor.handleError(err)))
188 } 188 }
diff --git a/server/models/account/account.ts b/server/models/account/account.ts
index 27c75d886..5a237d733 100644
--- a/server/models/account/account.ts
+++ b/server/models/account/account.ts
@@ -248,7 +248,8 @@ export class AccountModel extends Model<AccountModel> {
248 displayName: this.getDisplayName(), 248 displayName: this.getDisplayName(),
249 description: this.description, 249 description: this.description,
250 createdAt: this.createdAt, 250 createdAt: this.createdAt,
251 updatedAt: this.updatedAt 251 updatedAt: this.updatedAt,
252 userId: this.userId ? this.userId : undefined
252 } 253 }
253 254
254 return Object.assign(actor, account) 255 return Object.assign(actor, account)
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts
index 78972b199..905e84449 100644
--- a/server/models/video/video-format-utils.ts
+++ b/server/models/video/video-format-utils.ts
@@ -10,7 +10,7 @@ import {
10 getVideoLikesActivityPubUrl, 10 getVideoLikesActivityPubUrl,
11 getVideoSharesActivityPubUrl 11 getVideoSharesActivityPubUrl
12} from '../../lib/activitypub' 12} from '../../lib/activitypub'
13import { isArray } from 'util' 13import { isArray } from '../../helpers/custom-validators/misc'
14 14
15export type VideoFormattingJSONOptions = { 15export type VideoFormattingJSONOptions = {
16 completeDescription?: boolean 16 completeDescription?: boolean
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 0a2d7e6de..46d823240 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -94,7 +94,6 @@ import {
94import * as validator from 'validator' 94import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96 96
97
98// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 97// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
99const indexes: Sequelize.DefineIndexesOptions[] = [ 98const indexes: Sequelize.DefineIndexesOptions[] = [
100 buildTrigramSearchIndex('video_name_trigram', 'name'), 99 buildTrigramSearchIndex('video_name_trigram', 'name'),
diff --git a/server/tests/api/users/users-multiple-servers.ts b/server/tests/api/users/users-multiple-servers.ts
index b67072851..d8699db17 100644
--- a/server/tests/api/users/users-multiple-servers.ts
+++ b/server/tests/api/users/users-multiple-servers.ts
@@ -148,6 +148,12 @@ describe('Test users with multiple servers', function () {
148 expect(rootServer1Get.displayName).to.equal('my super display name') 148 expect(rootServer1Get.displayName).to.equal('my super display name')
149 expect(rootServer1Get.description).to.equal('my super description updated') 149 expect(rootServer1Get.description).to.equal('my super description updated')
150 150
151 if (server.serverNumber === 1) {
152 expect(rootServer1Get.userId).to.be.a('number')
153 } else {
154 expect(rootServer1Get.userId).to.be.undefined
155 }
156
151 await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png') 157 await testImage(server.url, 'avatar2-resized', rootServer1Get.avatar.path, '.png')
152 } 158 }
153 }) 159 })
diff --git a/shared/models/actors/account.model.ts b/shared/models/actors/account.model.ts
index e1117486d..7f1dbbc37 100644
--- a/shared/models/actors/account.model.ts
+++ b/shared/models/actors/account.model.ts
@@ -3,4 +3,6 @@ import { Actor } from './actor.model'
3export interface Account extends Actor { 3export interface Account extends Actor {
4 displayName: string 4 displayName: string
5 description: string 5 description: string
6
7 userId?: number
6} 8}