diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2017-10-27 16:55:03 +0200 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2017-10-27 16:55:03 +0200 |
commit | 954605a804da399317ca62afa2fb9244afa11ebf (patch) | |
tree | de6ee69280bfb928bc01c29430e13d5b820e921a | |
parent | e02573ad67626210ed279bad321ee139094921a1 (diff) | |
download | PeerTube-954605a804da399317ca62afa2fb9244afa11ebf.tar.gz PeerTube-954605a804da399317ca62afa2fb9244afa11ebf.tar.zst PeerTube-954605a804da399317ca62afa2fb9244afa11ebf.zip |
Support roles with rights and add moderator role
51 files changed, 378 insertions, 139 deletions
diff --git a/client/src/app/+admin/admin-routing.module.ts b/client/src/app/+admin/admin-routing.module.ts index c3e4895ac..7262768fe 100644 --- a/client/src/app/+admin/admin-routing.module.ts +++ b/client/src/app/+admin/admin-routing.module.ts | |||
@@ -8,15 +8,14 @@ import { FriendsRoutes } from './friends' | |||
8 | import { RequestSchedulersRoutes } from './request-schedulers' | 8 | import { RequestSchedulersRoutes } from './request-schedulers' |
9 | import { UsersRoutes } from './users' | 9 | import { UsersRoutes } from './users' |
10 | import { VideoAbusesRoutes } from './video-abuses' | 10 | import { VideoAbusesRoutes } from './video-abuses' |
11 | import { AdminGuard } from './admin-guard.service' | ||
12 | import { VideoBlacklistRoutes } from './video-blacklist' | 11 | import { VideoBlacklistRoutes } from './video-blacklist' |
13 | 12 | ||
14 | const adminRoutes: Routes = [ | 13 | const adminRoutes: Routes = [ |
15 | { | 14 | { |
16 | path: '', | 15 | path: '', |
17 | component: AdminComponent, | 16 | component: AdminComponent, |
18 | canActivate: [ MetaGuard, AdminGuard ], | 17 | canActivate: [ MetaGuard ], |
19 | canActivateChild: [ MetaGuard, AdminGuard ], | 18 | canActivateChild: [ MetaGuard ], |
20 | children: [ | 19 | children: [ |
21 | { | 20 | { |
22 | path: '', | 21 | path: '', |
diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index f29c501b0..6c216e5d8 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts | |||
@@ -8,7 +8,6 @@ import { UsersComponent, UserAddComponent, UserUpdateComponent, UserListComponen | |||
8 | import { VideoAbusesComponent, VideoAbuseListComponent } from './video-abuses' | 8 | import { VideoAbusesComponent, VideoAbuseListComponent } from './video-abuses' |
9 | import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist' | 9 | import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist' |
10 | import { SharedModule } from '../shared' | 10 | import { SharedModule } from '../shared' |
11 | import { AdminGuard } from './admin-guard.service' | ||
12 | 11 | ||
13 | @NgModule({ | 12 | @NgModule({ |
14 | imports: [ | 13 | imports: [ |
@@ -45,8 +44,7 @@ import { AdminGuard } from './admin-guard.service' | |||
45 | providers: [ | 44 | providers: [ |
46 | FriendService, | 45 | FriendService, |
47 | RequestSchedulersService, | 46 | RequestSchedulersService, |
48 | UserService, | 47 | UserService |
49 | AdminGuard | ||
50 | ] | 48 | ] |
51 | }) | 49 | }) |
52 | export class AdminModule { } | 50 | export class AdminModule { } |
diff --git a/client/src/app/+admin/friends/friends.routes.ts b/client/src/app/+admin/friends/friends.routes.ts index 615b6f4f7..61cfcae19 100644 --- a/client/src/app/+admin/friends/friends.routes.ts +++ b/client/src/app/+admin/friends/friends.routes.ts | |||
@@ -1,13 +1,19 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | 2 | ||
3 | import { UserRightGuard } from '../../core' | ||
3 | import { FriendsComponent } from './friends.component' | 4 | import { FriendsComponent } from './friends.component' |
4 | import { FriendAddComponent } from './friend-add' | 5 | import { FriendAddComponent } from './friend-add' |
5 | import { FriendListComponent } from './friend-list' | 6 | import { FriendListComponent } from './friend-list' |
7 | import { UserRight } from '../../../../../shared' | ||
6 | 8 | ||
7 | export const FriendsRoutes: Routes = [ | 9 | export const FriendsRoutes: Routes = [ |
8 | { | 10 | { |
9 | path: 'friends', | 11 | path: 'friends', |
10 | component: FriendsComponent, | 12 | component: FriendsComponent, |
13 | canActivate: [ UserRightGuard ], | ||
14 | data: { | ||
15 | userRight: UserRight.MANAGE_PODS | ||
16 | }, | ||
11 | children: [ | 17 | children: [ |
12 | { | 18 | { |
13 | path: '', | 19 | path: '', |
diff --git a/client/src/app/+admin/request-schedulers/request-schedulers.routes.ts b/client/src/app/+admin/request-schedulers/request-schedulers.routes.ts index 4961c646b..c2564de15 100644 --- a/client/src/app/+admin/request-schedulers/request-schedulers.routes.ts +++ b/client/src/app/+admin/request-schedulers/request-schedulers.routes.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | 2 | ||
3 | import { UserRightGuard } from '../../core' | ||
4 | import { UserRight } from '../../../../../shared' | ||
3 | import { RequestSchedulersComponent } from './request-schedulers.component' | 5 | import { RequestSchedulersComponent } from './request-schedulers.component' |
4 | import { RequestSchedulersStatsComponent } from './request-schedulers-stats' | 6 | import { RequestSchedulersStatsComponent } from './request-schedulers-stats' |
5 | 7 | ||
@@ -7,6 +9,10 @@ export const RequestSchedulersRoutes: Routes = [ | |||
7 | { | 9 | { |
8 | path: 'requests', | 10 | path: 'requests', |
9 | component: RequestSchedulersComponent, | 11 | component: RequestSchedulersComponent, |
12 | canActivate: [ UserRightGuard ], | ||
13 | data: { | ||
14 | userRight: UserRight.MANAGE_REQUEST_SCHEDULERS | ||
15 | }, | ||
10 | children: [ | 16 | children: [ |
11 | { | 17 | { |
12 | path: '', | 18 | path: '', |
diff --git a/client/src/app/+admin/users/user-edit/user-add.component.ts b/client/src/app/+admin/users/user-edit/user-add.component.ts index 6d8151b42..8e3e3d53d 100644 --- a/client/src/app/+admin/users/user-edit/user-add.component.ts +++ b/client/src/app/+admin/users/user-edit/user-add.component.ts | |||
@@ -9,10 +9,11 @@ import { | |||
9 | USER_USERNAME, | 9 | USER_USERNAME, |
10 | USER_EMAIL, | 10 | USER_EMAIL, |
11 | USER_PASSWORD, | 11 | USER_PASSWORD, |
12 | USER_VIDEO_QUOTA | 12 | USER_VIDEO_QUOTA, |
13 | USER_ROLE | ||
13 | } from '../../../shared' | 14 | } from '../../../shared' |
14 | import { ServerService } from '../../../core' | 15 | import { ServerService } from '../../../core' |
15 | import { UserCreate } from '../../../../../../shared' | 16 | import { UserCreate, UserRole } from '../../../../../../shared' |
16 | import { UserEdit } from './user-edit' | 17 | import { UserEdit } from './user-edit' |
17 | 18 | ||
18 | @Component({ | 19 | @Component({ |
@@ -28,12 +29,14 @@ export class UserAddComponent extends UserEdit implements OnInit { | |||
28 | 'username': '', | 29 | 'username': '', |
29 | 'email': '', | 30 | 'email': '', |
30 | 'password': '', | 31 | 'password': '', |
32 | 'role': '', | ||
31 | 'videoQuota': '' | 33 | 'videoQuota': '' |
32 | } | 34 | } |
33 | validationMessages = { | 35 | validationMessages = { |
34 | 'username': USER_USERNAME.MESSAGES, | 36 | 'username': USER_USERNAME.MESSAGES, |
35 | 'email': USER_EMAIL.MESSAGES, | 37 | 'email': USER_EMAIL.MESSAGES, |
36 | 'password': USER_PASSWORD.MESSAGES, | 38 | 'password': USER_PASSWORD.MESSAGES, |
39 | 'role': USER_ROLE.MESSAGES, | ||
37 | 'videoQuota': USER_VIDEO_QUOTA.MESSAGES | 40 | 'videoQuota': USER_VIDEO_QUOTA.MESSAGES |
38 | } | 41 | } |
39 | 42 | ||
@@ -52,6 +55,7 @@ export class UserAddComponent extends UserEdit implements OnInit { | |||
52 | username: [ '', USER_USERNAME.VALIDATORS ], | 55 | username: [ '', USER_USERNAME.VALIDATORS ], |
53 | email: [ '', USER_EMAIL.VALIDATORS ], | 56 | email: [ '', USER_EMAIL.VALIDATORS ], |
54 | password: [ '', USER_PASSWORD.VALIDATORS ], | 57 | password: [ '', USER_PASSWORD.VALIDATORS ], |
58 | role: [ UserRole.USER, USER_ROLE.VALIDATORS ], | ||
55 | videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ] | 59 | videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ] |
56 | }) | 60 | }) |
57 | 61 | ||
diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 6988071ce..349be13c1 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html | |||
@@ -41,6 +41,19 @@ | |||
41 | </div> | 41 | </div> |
42 | 42 | ||
43 | <div class="form-group"> | 43 | <div class="form-group"> |
44 | <label for="role">Role</label> | ||
45 | <select class="form-control" id="role" formControlName="role"> | ||
46 | <option *ngFor="let role of roles" [value]="role.value"> | ||
47 | {{ role.label }} | ||
48 | </option> | ||
49 | </select> | ||
50 | |||
51 | <div *ngIf="formErrors.role" class="alert alert-danger"> | ||
52 | {{ formErrors.role }} | ||
53 | </div> | ||
54 | </div> | ||
55 | |||
56 | <div class="form-group"> | ||
44 | <label for="videoQuota">Video quota</label> | 57 | <label for="videoQuota">Video quota</label> |
45 | <select class="form-control" id="videoQuota" formControlName="videoQuota"> | 58 | <select class="form-control" id="videoQuota" formControlName="videoQuota"> |
46 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> | 59 | <option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value"> |
diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 76497c9b6..51d90da39 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { ServerService } from '../../../core' | 1 | import { ServerService } from '../../../core' |
2 | import { FormReactive } from '../../../shared' | 2 | import { FormReactive } from '../../../shared' |
3 | import { VideoResolution } from '../../../../../../shared/models/videos/video-resolution.enum' | 3 | import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared' |
4 | 4 | ||
5 | export abstract class UserEdit extends FormReactive { | 5 | export abstract class UserEdit extends FormReactive { |
6 | videoQuotaOptions = [ | 6 | videoQuotaOptions = [ |
@@ -14,6 +14,8 @@ export abstract class UserEdit extends FormReactive { | |||
14 | { value: 50 * 1024 * 1024 * 1024, label: '50GB' } | 14 | { value: 50 * 1024 * 1024 * 1024, label: '50GB' } |
15 | ] | 15 | ] |
16 | 16 | ||
17 | roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key, label: USER_ROLE_LABELS[key] })) | ||
18 | |||
17 | protected abstract serverService: ServerService | 19 | protected abstract serverService: ServerService |
18 | abstract isCreation (): boolean | 20 | abstract isCreation (): boolean |
19 | abstract getFormButtonTitle (): string | 21 | abstract getFormButtonTitle (): string |
diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index bd901e655..bcba78a35 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts | |||
@@ -6,11 +6,15 @@ import { Subscription } from 'rxjs/Subscription' | |||
6 | import { NotificationsService } from 'angular2-notifications' | 6 | import { NotificationsService } from 'angular2-notifications' |
7 | 7 | ||
8 | import { UserService } from '../shared' | 8 | import { UserService } from '../shared' |
9 | import { USER_EMAIL, USER_VIDEO_QUOTA } from '../../../shared' | 9 | import { |
10 | USER_EMAIL, | ||
11 | USER_VIDEO_QUOTA, | ||
12 | USER_ROLE, | ||
13 | User | ||
14 | } from '../../../shared' | ||
10 | import { ServerService } from '../../../core' | 15 | import { ServerService } from '../../../core' |
11 | import { UserUpdate } from '../../../../../../shared/models/users/user-update.model' | ||
12 | import { User } from '../../../shared/users/user.model' | ||
13 | import { UserEdit } from './user-edit' | 16 | import { UserEdit } from './user-edit' |
17 | import { UserUpdate, UserRole } from '../../../../../../shared' | ||
14 | 18 | ||
15 | @Component({ | 19 | @Component({ |
16 | selector: 'my-user-update', | 20 | selector: 'my-user-update', |
@@ -25,10 +29,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
25 | form: FormGroup | 29 | form: FormGroup |
26 | formErrors = { | 30 | formErrors = { |
27 | 'email': '', | 31 | 'email': '', |
32 | 'role': '', | ||
28 | 'videoQuota': '' | 33 | 'videoQuota': '' |
29 | } | 34 | } |
30 | validationMessages = { | 35 | validationMessages = { |
31 | 'email': USER_EMAIL.MESSAGES, | 36 | 'email': USER_EMAIL.MESSAGES, |
37 | 'role': USER_ROLE.MESSAGES, | ||
32 | 'videoQuota': USER_VIDEO_QUOTA.MESSAGES | 38 | 'videoQuota': USER_VIDEO_QUOTA.MESSAGES |
33 | } | 39 | } |
34 | 40 | ||
@@ -48,6 +54,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
48 | buildForm () { | 54 | buildForm () { |
49 | this.form = this.formBuilder.group({ | 55 | this.form = this.formBuilder.group({ |
50 | email: [ '', USER_EMAIL.VALIDATORS ], | 56 | email: [ '', USER_EMAIL.VALIDATORS ], |
57 | role: [ '', USER_ROLE.VALIDATORS ], | ||
51 | videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ] | 58 | videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ] |
52 | }) | 59 | }) |
53 | 60 | ||
@@ -103,6 +110,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy { | |||
103 | 110 | ||
104 | this.form.patchValue({ | 111 | this.form.patchValue({ |
105 | email: userJson.email, | 112 | email: userJson.email, |
113 | role: userJson.role, | ||
106 | videoQuota: userJson.videoQuota | 114 | videoQuota: userJson.videoQuota |
107 | }) | 115 | }) |
108 | } | 116 | } |
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 2944e3cbf..16a8a8033 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 | |||
@@ -11,7 +11,7 @@ | |||
11 | <p-column field="username" header="Username" [sortable]="true"></p-column> | 11 | <p-column field="username" header="Username" [sortable]="true"></p-column> |
12 | <p-column field="email" header="Email"></p-column> | 12 | <p-column field="email" header="Email"></p-column> |
13 | <p-column field="videoQuota" header="Video quota"></p-column> | 13 | <p-column field="videoQuota" header="Video quota"></p-column> |
14 | <p-column field="role" header="Role"></p-column> | 14 | <p-column field="roleLabel" header="Role"></p-column> |
15 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> | 15 | <p-column field="createdAt" header="Created date" [sortable]="true"></p-column> |
16 | <p-column header="Edit" styleClass="action-cell"> | 16 | <p-column header="Edit" styleClass="action-cell"> |
17 | <ng-template pTemplate="body" let-user="rowData"> | 17 | <ng-template pTemplate="body" let-user="rowData"> |
diff --git a/client/src/app/+admin/users/users.routes.ts b/client/src/app/+admin/users/users.routes.ts index a6a9c4c19..3718dfd5c 100644 --- a/client/src/app/+admin/users/users.routes.ts +++ b/client/src/app/+admin/users/users.routes.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | 2 | ||
3 | import { UserRightGuard } from '../../core' | ||
4 | import { UserRight } from '../../../../../shared' | ||
3 | import { UsersComponent } from './users.component' | 5 | import { UsersComponent } from './users.component' |
4 | import { UserAddComponent, UserUpdateComponent } from './user-edit' | 6 | import { UserAddComponent, UserUpdateComponent } from './user-edit' |
5 | import { UserListComponent } from './user-list' | 7 | import { UserListComponent } from './user-list' |
@@ -8,6 +10,10 @@ export const UsersRoutes: Routes = [ | |||
8 | { | 10 | { |
9 | path: 'users', | 11 | path: 'users', |
10 | component: UsersComponent, | 12 | component: UsersComponent, |
13 | canActivate: [ UserRightGuard ], | ||
14 | data: { | ||
15 | userRight: UserRight.MANAGE_USERS | ||
16 | }, | ||
11 | children: [ | 17 | children: [ |
12 | { | 18 | { |
13 | path: '', | 19 | path: '', |
diff --git a/client/src/app/+admin/video-abuses/video-abuses.routes.ts b/client/src/app/+admin/video-abuses/video-abuses.routes.ts index a8c1561cd..68b756059 100644 --- a/client/src/app/+admin/video-abuses/video-abuses.routes.ts +++ b/client/src/app/+admin/video-abuses/video-abuses.routes.ts | |||
@@ -1,13 +1,18 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | 2 | ||
3 | import { UserRightGuard } from '../../core' | ||
4 | import { UserRight } from '../../../../../shared' | ||
3 | import { VideoAbusesComponent } from './video-abuses.component' | 5 | import { VideoAbusesComponent } from './video-abuses.component' |
4 | import { VideoAbuseListComponent } from './video-abuse-list' | 6 | import { VideoAbuseListComponent } from './video-abuse-list' |
5 | 7 | ||
6 | export const VideoAbusesRoutes: Routes = [ | 8 | export const VideoAbusesRoutes: Routes = [ |
7 | { | 9 | { |
8 | path: 'video-abuses', | 10 | path: 'video-abuses', |
9 | component: VideoAbusesComponent | 11 | component: VideoAbusesComponent, |
10 | , | 12 | canActivate: [ UserRightGuard ], |
13 | data: { | ||
14 | userRight: UserRight.MANAGE_VIDEO_ABUSES | ||
15 | }, | ||
11 | children: [ | 16 | children: [ |
12 | { | 17 | { |
13 | path: '', | 18 | path: '', |
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist.routes.ts b/client/src/app/+admin/video-blacklist/video-blacklist.routes.ts index 682b6f8bd..b1e0e5049 100644 --- a/client/src/app/+admin/video-blacklist/video-blacklist.routes.ts +++ b/client/src/app/+admin/video-blacklist/video-blacklist.routes.ts | |||
@@ -1,5 +1,7 @@ | |||
1 | import { Routes } from '@angular/router' | 1 | import { Routes } from '@angular/router' |
2 | 2 | ||
3 | import { UserRightGuard } from '../../core' | ||
4 | import { UserRight } from '../../../../../shared' | ||
3 | import { VideoBlacklistComponent } from './video-blacklist.component' | 5 | import { VideoBlacklistComponent } from './video-blacklist.component' |
4 | import { VideoBlacklistListComponent } from './video-blacklist-list' | 6 | import { VideoBlacklistListComponent } from './video-blacklist-list' |
5 | 7 | ||
@@ -7,6 +9,10 @@ export const VideoBlacklistRoutes: Routes = [ | |||
7 | { | 9 | { |
8 | path: 'video-blacklist', | 10 | path: 'video-blacklist', |
9 | component: VideoBlacklistComponent, | 11 | component: VideoBlacklistComponent, |
12 | canActivate: [ UserRightGuard ], | ||
13 | data: { | ||
14 | userRight: UserRight.MANAGE_VIDEO_BLACKLIST | ||
15 | }, | ||
10 | children: [ | 16 | children: [ |
11 | { | 17 | { |
12 | path: '', | 18 | path: '', |
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index 81bff99a0..085b763ec 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | // Do not use the barrel (dependency loop) | 1 | // Do not use the barrel (dependency loop) |
2 | import { UserRole } from '../../../../../shared/models/users/user-role.type' | 2 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' |
3 | import { User, UserConstructorHash } from '../../shared/users/user.model' | 3 | import { User, UserConstructorHash } from '../../shared/users/user.model' |
4 | import { UserRight } from '../../../../../shared/models/users/user-right.enum' | ||
4 | 5 | ||
5 | export type TokenOptions = { | 6 | export type TokenOptions = { |
6 | accessToken: string | 7 | accessToken: string |
@@ -81,7 +82,7 @@ export class AuthUser extends User { | |||
81 | id: parseInt(localStorage.getItem(this.KEYS.ID), 10), | 82 | id: parseInt(localStorage.getItem(this.KEYS.ID), 10), |
82 | username: localStorage.getItem(this.KEYS.USERNAME), | 83 | username: localStorage.getItem(this.KEYS.USERNAME), |
83 | email: localStorage.getItem(this.KEYS.EMAIL), | 84 | email: localStorage.getItem(this.KEYS.EMAIL), |
84 | role: localStorage.getItem(this.KEYS.ROLE) as UserRole, | 85 | role: parseInt(localStorage.getItem(this.KEYS.ROLE), 10) as UserRole, |
85 | displayNSFW: localStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true' | 86 | displayNSFW: localStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true' |
86 | }, | 87 | }, |
87 | Tokens.load() | 88 | Tokens.load() |
@@ -122,11 +123,15 @@ export class AuthUser extends User { | |||
122 | this.tokens.refreshToken = refreshToken | 123 | this.tokens.refreshToken = refreshToken |
123 | } | 124 | } |
124 | 125 | ||
126 | hasRight(right: UserRight) { | ||
127 | return hasUserRight(this.role, right) | ||
128 | } | ||
129 | |||
125 | save () { | 130 | save () { |
126 | localStorage.setItem(AuthUser.KEYS.ID, this.id.toString()) | 131 | localStorage.setItem(AuthUser.KEYS.ID, this.id.toString()) |
127 | localStorage.setItem(AuthUser.KEYS.USERNAME, this.username) | 132 | localStorage.setItem(AuthUser.KEYS.USERNAME, this.username) |
128 | localStorage.setItem(AuthUser.KEYS.EMAIL, this.email) | 133 | localStorage.setItem(AuthUser.KEYS.EMAIL, this.email) |
129 | localStorage.setItem(AuthUser.KEYS.ROLE, this.role) | 134 | localStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) |
130 | localStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW)) | 135 | localStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW)) |
131 | this.tokens.save() | 136 | this.tokens.save() |
132 | } | 137 | } |
diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 9ac9ba7bb..df6e5135b 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts | |||
@@ -21,7 +21,7 @@ import { | |||
21 | // Do not use the barrel (dependency loop) | 21 | // Do not use the barrel (dependency loop) |
22 | import { RestExtractor } from '../../shared/rest' | 22 | import { RestExtractor } from '../../shared/rest' |
23 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' | 23 | import { UserLogin } from '../../../../../shared/models/users/user-login.model' |
24 | import { User, UserConstructorHash } from '../../shared/users/user.model' | 24 | import { UserConstructorHash } from '../../shared/users/user.model' |
25 | 25 | ||
26 | interface UserLoginWithUsername extends UserLogin { | 26 | interface UserLoginWithUsername extends UserLogin { |
27 | access_token: string | 27 | access_token: string |
@@ -126,12 +126,6 @@ export class AuthService { | |||
126 | return this.user | 126 | return this.user |
127 | } | 127 | } |
128 | 128 | ||
129 | isAdmin () { | ||
130 | if (this.user === null) return false | ||
131 | |||
132 | return this.user.isAdmin() | ||
133 | } | ||
134 | |||
135 | isLoggedIn () { | 129 | isLoggedIn () { |
136 | return !!this.getAccessToken() | 130 | return !!this.getAccessToken() |
137 | } | 131 | } |
diff --git a/client/src/app/core/auth/index.ts b/client/src/app/core/auth/index.ts index a81f2c002..bc7bfec0e 100644 --- a/client/src/app/core/auth/index.ts +++ b/client/src/app/core/auth/index.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export * from './auth-status.model' | 1 | export * from './auth-status.model' |
2 | export * from './auth-user.model' | 2 | export * from './auth-user.model' |
3 | export * from './auth.service' | 3 | export * from './auth.service' |
4 | export * from './login-guard.service' | 4 | export * from '../routing/login-guard.service' |
diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 163a6bbde..90e2cb190 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts | |||
@@ -7,7 +7,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations' | |||
7 | import { SimpleNotificationsModule } from 'angular2-notifications' | 7 | import { SimpleNotificationsModule } from 'angular2-notifications' |
8 | import { ModalModule } from 'ngx-bootstrap/modal' | 8 | import { ModalModule } from 'ngx-bootstrap/modal' |
9 | 9 | ||
10 | import { AuthService, LoginGuard } from './auth' | 10 | import { AuthService } from './auth' |
11 | import { LoginGuard, UserRightGuard } from './routing' | ||
11 | import { ServerService } from './server' | 12 | import { ServerService } from './server' |
12 | import { ConfirmComponent, ConfirmService } from './confirm' | 13 | import { ConfirmComponent, ConfirmService } from './confirm' |
13 | import { MenuComponent, MenuAdminComponent } from './menu' | 14 | import { MenuComponent, MenuAdminComponent } from './menu' |
@@ -42,7 +43,8 @@ import { throwIfAlreadyLoaded } from './module-import-guard' | |||
42 | AuthService, | 43 | AuthService, |
43 | ConfirmService, | 44 | ConfirmService, |
44 | ServerService, | 45 | ServerService, |
45 | LoginGuard | 46 | LoginGuard, |
47 | UserRightGuard | ||
46 | ] | 48 | ] |
47 | }) | 49 | }) |
48 | export class CoreModule { | 50 | export class CoreModule { |
diff --git a/client/src/app/core/menu/menu-admin.component.html b/client/src/app/core/menu/menu-admin.component.html index edacdee6d..c2b2958b4 100644 --- a/client/src/app/core/menu/menu-admin.component.html +++ b/client/src/app/core/menu/menu-admin.component.html | |||
@@ -1,26 +1,26 @@ | |||
1 | <menu> | 1 | <menu> |
2 | <div class="panel-block"> | 2 | <div class="panel-block"> |
3 | <a routerLink="/admin/users/list" routerLinkActive="active"> | 3 | <a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active"> |
4 | <span class="hidden-xs glyphicon glyphicon-user"></span> | 4 | <span class="hidden-xs glyphicon glyphicon-user"></span> |
5 | List users | 5 | List users |
6 | </a> | 6 | </a> |
7 | 7 | ||
8 | <a routerLink="/admin/friends/list" routerLinkActive="active"> | 8 | <a *ngIf="hasFriendsRight()" routerLink="/admin/friends" routerLinkActive="active"> |
9 | <span class="hidden-xs glyphicon glyphicon-cloud"></span> | 9 | <span class="hidden-xs glyphicon glyphicon-cloud"></span> |
10 | List friends | 10 | List friends |
11 | </a> | 11 | </a> |
12 | 12 | ||
13 | <a routerLink="/admin/requests/stats" routerLinkActive="active"> | 13 | <a *ngIf="hasRequestsStatRight()" routerLink="/admin/requests/stats" routerLinkActive="active"> |
14 | <span class="hidden-xs glyphicon glyphicon-stats"></span> | 14 | <span class="hidden-xs glyphicon glyphicon-stats"></span> |
15 | Request stats | 15 | Request stats |
16 | </a> | 16 | </a> |
17 | 17 | ||
18 | <a routerLink="/admin/video-abuses/list" routerLinkActive="active"> | 18 | <a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active"> |
19 | <span class="hidden-xs glyphicon glyphicon-alert"></span> | 19 | <span class="hidden-xs glyphicon glyphicon-alert"></span> |
20 | Video abuses | 20 | Video abuses |
21 | </a> | 21 | </a> |
22 | 22 | ||
23 | <a routerLink="/admin/video-blacklist/list" routerLinkActive="active"> | 23 | <a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active"> |
24 | <span class="hidden-xs glyphicon glyphicon-eye-close"></span> | 24 | <span class="hidden-xs glyphicon glyphicon-eye-close"></span> |
25 | Video blacklist | 25 | Video blacklist |
26 | </a> | 26 | </a> |
diff --git a/client/src/app/core/menu/menu-admin.component.ts b/client/src/app/core/menu/menu-admin.component.ts index f6cc6554c..074f1dbaf 100644 --- a/client/src/app/core/menu/menu-admin.component.ts +++ b/client/src/app/core/menu/menu-admin.component.ts | |||
@@ -1,8 +1,33 @@ | |||
1 | import { Component } from '@angular/core' | 1 | import { Component } from '@angular/core' |
2 | 2 | ||
3 | import { AuthService } from '../auth/auth.service' | ||
4 | import { UserRight } from '../../../../../shared' | ||
5 | |||
3 | @Component({ | 6 | @Component({ |
4 | selector: 'my-menu-admin', | 7 | selector: 'my-menu-admin', |
5 | templateUrl: './menu-admin.component.html', | 8 | templateUrl: './menu-admin.component.html', |
6 | styleUrls: [ './menu.component.scss' ] | 9 | styleUrls: [ './menu.component.scss' ] |
7 | }) | 10 | }) |
8 | export class MenuAdminComponent { } | 11 | export class MenuAdminComponent { |
12 | constructor (private auth: AuthService) {} | ||
13 | |||
14 | hasUsersRight () { | ||
15 | return this.auth.getUser().hasRight(UserRight.MANAGE_USERS) | ||
16 | } | ||
17 | |||
18 | hasFriendsRight () { | ||
19 | return this.auth.getUser().hasRight(UserRight.MANAGE_PODS) | ||
20 | } | ||
21 | |||
22 | hasRequestsStatRight () { | ||
23 | return this.auth.getUser().hasRight(UserRight.MANAGE_REQUEST_SCHEDULERS) | ||
24 | } | ||
25 | |||
26 | hasVideoAbusesRight () { | ||
27 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES) | ||
28 | } | ||
29 | |||
30 | hasVideoBlacklistRight () { | ||
31 | return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) | ||
32 | } | ||
33 | } | ||
diff --git a/client/src/app/core/menu/menu.component.html b/client/src/app/core/menu/menu.component.html index ca341a0fd..2d8aace54 100644 --- a/client/src/app/core/menu/menu.component.html +++ b/client/src/app/core/menu/menu.component.html | |||
@@ -39,10 +39,10 @@ | |||
39 | </a> | 39 | </a> |
40 | </div> | 40 | </div> |
41 | 41 | ||
42 | <div *ngIf="isUserAdmin()" class="panel-block"> | 42 | <div *ngIf="userHasAdminAccess" class="panel-block"> |
43 | <div class="block-title">Other</div> | 43 | <div class="block-title">Other</div> |
44 | 44 | ||
45 | <a routerLink="/admin" routerLinkActive="active"> | 45 | <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active"> |
46 | <span class="hidden-xs glyphicon glyphicon-cog"></span> | 46 | <span class="hidden-xs glyphicon glyphicon-cog"></span> |
47 | Administration | 47 | Administration |
48 | </a> | 48 | </a> |
diff --git a/client/src/app/core/menu/menu.component.ts b/client/src/app/core/menu/menu.component.ts index 8f15d8838..c66a5eccc 100644 --- a/client/src/app/core/menu/menu.component.ts +++ b/client/src/app/core/menu/menu.component.ts | |||
@@ -3,6 +3,7 @@ import { Router } from '@angular/router' | |||
3 | 3 | ||
4 | import { AuthService, AuthStatus } from '../auth' | 4 | import { AuthService, AuthStatus } from '../auth' |
5 | import { ServerService } from '../server' | 5 | import { ServerService } from '../server' |
6 | import { UserRight } from '../../../../../shared/models/users/user-right.enum' | ||
6 | 7 | ||
7 | @Component({ | 8 | @Component({ |
8 | selector: 'my-menu', | 9 | selector: 'my-menu', |
@@ -11,6 +12,15 @@ import { ServerService } from '../server' | |||
11 | }) | 12 | }) |
12 | export class MenuComponent implements OnInit { | 13 | export class MenuComponent implements OnInit { |
13 | isLoggedIn: boolean | 14 | isLoggedIn: boolean |
15 | userHasAdminAccess = false | ||
16 | |||
17 | private routesPerRight = { | ||
18 | [UserRight.MANAGE_USERS]: '/admin/users', | ||
19 | [UserRight.MANAGE_PODS]: '/admin/friends', | ||
20 | [UserRight.MANAGE_REQUEST_SCHEDULERS]: '/admin/requests/stats', | ||
21 | [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/video-abuses', | ||
22 | [UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/video-blacklist' | ||
23 | } | ||
14 | 24 | ||
15 | constructor ( | 25 | constructor ( |
16 | private authService: AuthService, | 26 | private authService: AuthService, |
@@ -20,14 +30,17 @@ export class MenuComponent implements OnInit { | |||
20 | 30 | ||
21 | ngOnInit () { | 31 | ngOnInit () { |
22 | this.isLoggedIn = this.authService.isLoggedIn() | 32 | this.isLoggedIn = this.authService.isLoggedIn() |
33 | this.computeIsUserHasAdminAccess() | ||
23 | 34 | ||
24 | this.authService.loginChangedSource.subscribe( | 35 | this.authService.loginChangedSource.subscribe( |
25 | status => { | 36 | status => { |
26 | if (status === AuthStatus.LoggedIn) { | 37 | if (status === AuthStatus.LoggedIn) { |
27 | this.isLoggedIn = true | 38 | this.isLoggedIn = true |
39 | this.computeIsUserHasAdminAccess() | ||
28 | console.log('Logged in.') | 40 | console.log('Logged in.') |
29 | } else if (status === AuthStatus.LoggedOut) { | 41 | } else if (status === AuthStatus.LoggedOut) { |
30 | this.isLoggedIn = false | 42 | this.isLoggedIn = false |
43 | this.computeIsUserHasAdminAccess() | ||
31 | console.log('Logged out.') | 44 | console.log('Logged out.') |
32 | } else { | 45 | } else { |
33 | console.error('Unknown auth status: ' + status) | 46 | console.error('Unknown auth status: ' + status) |
@@ -40,8 +53,31 @@ export class MenuComponent implements OnInit { | |||
40 | return this.serverService.getConfig().signup.allowed | 53 | return this.serverService.getConfig().signup.allowed |
41 | } | 54 | } |
42 | 55 | ||
43 | isUserAdmin () { | 56 | getFirstAdminRightAvailable () { |
44 | return this.authService.isAdmin() | 57 | const user = this.authService.getUser() |
58 | if (!user) return undefined | ||
59 | |||
60 | const adminRights = [ | ||
61 | UserRight.MANAGE_USERS, | ||
62 | UserRight.MANAGE_PODS, | ||
63 | UserRight.MANAGE_REQUEST_SCHEDULERS, | ||
64 | UserRight.MANAGE_VIDEO_ABUSES, | ||
65 | UserRight.MANAGE_VIDEO_BLACKLIST | ||
66 | ] | ||
67 | |||
68 | for (const adminRight of adminRights) { | ||
69 | if (user.hasRight(adminRight)) { | ||
70 | return adminRight | ||
71 | } | ||
72 | } | ||
73 | |||
74 | return undefined | ||
75 | } | ||
76 | |||
77 | getFirstAdminRouteAvailable () { | ||
78 | const right = this.getFirstAdminRightAvailable() | ||
79 | |||
80 | return this.routesPerRight[right] | ||
45 | } | 81 | } |
46 | 82 | ||
47 | logout () { | 83 | logout () { |
@@ -49,4 +85,10 @@ export class MenuComponent implements OnInit { | |||
49 | // Redirect to home page | 85 | // Redirect to home page |
50 | this.router.navigate(['/videos/list']) | 86 | this.router.navigate(['/videos/list']) |
51 | } | 87 | } |
88 | |||
89 | private computeIsUserHasAdminAccess () { | ||
90 | const right = this.getFirstAdminRightAvailable() | ||
91 | |||
92 | this.userHasAdminAccess = right !== undefined | ||
93 | } | ||
52 | } | 94 | } |
diff --git a/client/src/app/core/routing/index.ts b/client/src/app/core/routing/index.ts index 17f3ee833..d1b982834 100644 --- a/client/src/app/core/routing/index.ts +++ b/client/src/app/core/routing/index.ts | |||
@@ -1 +1,3 @@ | |||
1 | export * from './login-guard.service' | ||
2 | export * from './user-right-guard.service' | ||
1 | export * from './preload-selected-modules-list' | 3 | export * from './preload-selected-modules-list' |
diff --git a/client/src/app/core/auth/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts index c09e8fe97..18bc41ca6 100644 --- a/client/src/app/core/auth/login-guard.service.ts +++ b/client/src/app/core/routing/login-guard.service.ts | |||
@@ -7,7 +7,7 @@ import { | |||
7 | Router | 7 | Router |
8 | } from '@angular/router' | 8 | } from '@angular/router' |
9 | 9 | ||
10 | import { AuthService } from './auth.service' | 10 | import { AuthService } from '../auth/auth.service' |
11 | 11 | ||
12 | @Injectable() | 12 | @Injectable() |
13 | export class LoginGuard implements CanActivate, CanActivateChild { | 13 | export class LoginGuard implements CanActivate, CanActivateChild { |
diff --git a/client/src/app/+admin/admin-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts index 429dc032d..65d029977 100644 --- a/client/src/app/+admin/admin-guard.service.ts +++ b/client/src/app/core/routing/user-right-guard.service.ts | |||
@@ -7,10 +7,10 @@ import { | |||
7 | Router | 7 | Router |
8 | } from '@angular/router' | 8 | } from '@angular/router' |
9 | 9 | ||
10 | import { AuthService } from '../core' | 10 | import { AuthService } from '../auth' |
11 | 11 | ||
12 | @Injectable() | 12 | @Injectable() |
13 | export class AdminGuard implements CanActivate, CanActivateChild { | 13 | export class UserRightGuard implements CanActivate, CanActivateChild { |
14 | 14 | ||
15 | constructor ( | 15 | constructor ( |
16 | private router: Router, | 16 | private router: Router, |
@@ -18,7 +18,12 @@ export class AdminGuard implements CanActivate, CanActivateChild { | |||
18 | ) {} | 18 | ) {} |
19 | 19 | ||
20 | canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { | 20 | canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { |
21 | if (this.auth.isAdmin() === true) return true | 21 | const user = this.auth.getUser() |
22 | if (user) { | ||
23 | const neededUserRight = route.data.userRight | ||
24 | |||
25 | if (user.hasRight(neededUserRight)) return true | ||
26 | } | ||
22 | 27 | ||
23 | this.router.navigate([ '/login' ]) | 28 | this.router.navigate([ '/login' ]) |
24 | return false | 29 | return false |
diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts index d4c4c1d33..e7473b75b 100644 --- a/client/src/app/shared/forms/form-validators/user.ts +++ b/client/src/app/shared/forms/form-validators/user.ts | |||
@@ -29,3 +29,9 @@ export const USER_VIDEO_QUOTA = { | |||
29 | 'min': 'Quota must be greater than -1.' | 29 | 'min': 'Quota must be greater than -1.' |
30 | } | 30 | } |
31 | } | 31 | } |
32 | export const USER_ROLE = { | ||
33 | VALIDATORS: [ Validators.required ], | ||
34 | MESSAGES: { | ||
35 | 'required': 'User role is required.', | ||
36 | } | ||
37 | } | ||
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 7beea5910..d738899ab 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -1,7 +1,9 @@ | |||
1 | import { | 1 | import { |
2 | User as UserServerModel, | 2 | User as UserServerModel, |
3 | UserRole, | 3 | UserRole, |
4 | VideoChannel | 4 | VideoChannel, |
5 | UserRight, | ||
6 | hasUserRight | ||
5 | } from '../../../../../shared' | 7 | } from '../../../../../shared' |
6 | 8 | ||
7 | export type UserConstructorHash = { | 9 | export type UserConstructorHash = { |
@@ -56,7 +58,7 @@ export class User implements UserServerModel { | |||
56 | } | 58 | } |
57 | } | 59 | } |
58 | 60 | ||
59 | isAdmin () { | 61 | hasRight (right: UserRight) { |
60 | return this.role === 'admin' | 62 | return hasUserRight(this.role, right) |
61 | } | 63 | } |
62 | } | 64 | } |
diff --git a/client/src/app/videos/shared/video-details.model.ts b/client/src/app/videos/shared/video-details.model.ts index e99a5ce2e..3a6ecc480 100644 --- a/client/src/app/videos/shared/video-details.model.ts +++ b/client/src/app/videos/shared/video-details.model.ts | |||
@@ -1,9 +1,11 @@ | |||
1 | import { Video } from './video.model' | 1 | import { Video } from './video.model' |
2 | import { AuthUser } from '../../core' | ||
2 | import { | 3 | import { |
3 | VideoDetails as VideoDetailsServerModel, | 4 | VideoDetails as VideoDetailsServerModel, |
4 | VideoFile, | 5 | VideoFile, |
5 | VideoChannel, | 6 | VideoChannel, |
6 | VideoResolution | 7 | VideoResolution, |
8 | UserRight | ||
7 | } from '../../../../../shared' | 9 | } from '../../../../../shared' |
8 | 10 | ||
9 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 11 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
@@ -61,15 +63,15 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
61 | return betterResolutionFile.magnetUri | 63 | return betterResolutionFile.magnetUri |
62 | } | 64 | } |
63 | 65 | ||
64 | isRemovableBy (user) { | 66 | isRemovableBy (user: AuthUser) { |
65 | return user && this.isLocal === true && (this.author === user.username || user.isAdmin() === true) | 67 | return user && this.isLocal === true && (this.author === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO)) |
66 | } | 68 | } |
67 | 69 | ||
68 | isBlackistableBy (user) { | 70 | isBlackistableBy (user: AuthUser) { |
69 | return user && user.isAdmin() === true && this.isLocal === false | 71 | return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false |
70 | } | 72 | } |
71 | 73 | ||
72 | isUpdatableBy (user) { | 74 | isUpdatableBy (user: AuthUser) { |
73 | return user && this.isLocal === true && user.username === this.author | 75 | return user && this.isLocal === true && user.username === this.author |
74 | } | 76 | } |
75 | } | 77 | } |
diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts index 35a7b6521..bf6f60215 100644 --- a/client/src/app/videos/video-list/video-list.component.ts +++ b/client/src/app/videos/video-list/video-list.component.ts | |||
@@ -12,7 +12,7 @@ import { | |||
12 | VideoService, | 12 | VideoService, |
13 | VideoPagination | 13 | VideoPagination |
14 | } from '../shared' | 14 | } from '../shared' |
15 | import { Search, SearchField, SearchService, User} from '../../shared' | 15 | import { Search, SearchField, SearchService, User } from '../../shared' |
16 | 16 | ||
17 | @Component({ | 17 | @Component({ |
18 | selector: 'my-videos-list', | 18 | selector: 'my-videos-list', |
diff --git a/server/controllers/api/pods.ts b/server/controllers/api/pods.ts index bf1b744e5..b44cd6b83 100644 --- a/server/controllers/api/pods.ts +++ b/server/controllers/api/pods.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | } from '../../lib' | 9 | } from '../../lib' |
10 | import { | 10 | import { |
11 | authenticate, | 11 | authenticate, |
12 | ensureIsAdmin, | 12 | ensureUserHasRight, |
13 | makeFriendsValidator, | 13 | makeFriendsValidator, |
14 | setBodyHostsPort, | 14 | setBodyHostsPort, |
15 | podRemoveValidator, | 15 | podRemoveValidator, |
@@ -20,6 +20,7 @@ import { | |||
20 | asyncMiddleware | 20 | asyncMiddleware |
21 | } from '../../middlewares' | 21 | } from '../../middlewares' |
22 | import { PodInstance } from '../../models' | 22 | import { PodInstance } from '../../models' |
23 | import { UserRight } from '../../../shared' | ||
23 | 24 | ||
24 | const podsRouter = express.Router() | 25 | const podsRouter = express.Router() |
25 | 26 | ||
@@ -32,19 +33,19 @@ podsRouter.get('/', | |||
32 | ) | 33 | ) |
33 | podsRouter.post('/make-friends', | 34 | podsRouter.post('/make-friends', |
34 | authenticate, | 35 | authenticate, |
35 | ensureIsAdmin, | 36 | ensureUserHasRight(UserRight.MANAGE_PODS), |
36 | makeFriendsValidator, | 37 | makeFriendsValidator, |
37 | setBodyHostsPort, | 38 | setBodyHostsPort, |
38 | asyncMiddleware(makeFriendsController) | 39 | asyncMiddleware(makeFriendsController) |
39 | ) | 40 | ) |
40 | podsRouter.get('/quit-friends', | 41 | podsRouter.get('/quit-friends', |
41 | authenticate, | 42 | authenticate, |
42 | ensureIsAdmin, | 43 | ensureUserHasRight(UserRight.MANAGE_PODS), |
43 | asyncMiddleware(quitFriendsController) | 44 | asyncMiddleware(quitFriendsController) |
44 | ) | 45 | ) |
45 | podsRouter.delete('/:id', | 46 | podsRouter.delete('/:id', |
46 | authenticate, | 47 | authenticate, |
47 | ensureIsAdmin, | 48 | ensureUserHasRight(UserRight.MANAGE_PODS), |
48 | podRemoveValidator, | 49 | podRemoveValidator, |
49 | asyncMiddleware(removeFriendController) | 50 | asyncMiddleware(removeFriendController) |
50 | ) | 51 | ) |
diff --git a/server/controllers/api/request-schedulers.ts b/server/controllers/api/request-schedulers.ts index 28f46f3ee..4c8fbe18b 100644 --- a/server/controllers/api/request-schedulers.ts +++ b/server/controllers/api/request-schedulers.ts | |||
@@ -7,14 +7,14 @@ import { | |||
7 | getRequestVideoQaduScheduler, | 7 | getRequestVideoQaduScheduler, |
8 | getRequestVideoEventScheduler | 8 | getRequestVideoEventScheduler |
9 | } from '../../lib' | 9 | } from '../../lib' |
10 | import { authenticate, ensureIsAdmin, asyncMiddleware } from '../../middlewares' | 10 | import { authenticate, ensureUserHasRight, asyncMiddleware } from '../../middlewares' |
11 | import { RequestSchedulerStatsAttributes } from '../../../shared' | 11 | import { RequestSchedulerStatsAttributes, UserRight } from '../../../shared' |
12 | 12 | ||
13 | const requestSchedulerRouter = express.Router() | 13 | const requestSchedulerRouter = express.Router() |
14 | 14 | ||
15 | requestSchedulerRouter.get('/stats', | 15 | requestSchedulerRouter.get('/stats', |
16 | authenticate, | 16 | authenticate, |
17 | ensureIsAdmin, | 17 | ensureUserHasRight(UserRight.MANAGE_REQUEST_SCHEDULERS), |
18 | asyncMiddleware(getRequestSchedulersStats) | 18 | asyncMiddleware(getRequestSchedulersStats) |
19 | ) | 19 | ) |
20 | 20 | ||
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 18a094f03..fdc9b0c87 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts | |||
@@ -1,11 +1,10 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | 2 | ||
3 | import { database as db } from '../../initializers/database' | 3 | import { database as db, CONFIG } from '../../initializers' |
4 | import { USER_ROLES, CONFIG } from '../../initializers' | ||
5 | import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers' | 4 | import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers' |
6 | import { | 5 | import { |
7 | authenticate, | 6 | authenticate, |
8 | ensureIsAdmin, | 7 | ensureUserHasRight, |
9 | ensureUserRegistrationAllowed, | 8 | ensureUserRegistrationAllowed, |
10 | usersAddValidator, | 9 | usersAddValidator, |
11 | usersRegisterValidator, | 10 | usersRegisterValidator, |
@@ -25,7 +24,9 @@ import { | |||
25 | UserVideoRate as FormattedUserVideoRate, | 24 | UserVideoRate as FormattedUserVideoRate, |
26 | UserCreate, | 25 | UserCreate, |
27 | UserUpdate, | 26 | UserUpdate, |
28 | UserUpdateMe | 27 | UserUpdateMe, |
28 | UserRole, | ||
29 | UserRight | ||
29 | } from '../../../shared' | 30 | } from '../../../shared' |
30 | import { createUserAuthorAndChannel } from '../../lib' | 31 | import { createUserAuthorAndChannel } from '../../lib' |
31 | import { UserInstance } from '../../models' | 32 | import { UserInstance } from '../../models' |
@@ -58,7 +59,7 @@ usersRouter.get('/:id', | |||
58 | 59 | ||
59 | usersRouter.post('/', | 60 | usersRouter.post('/', |
60 | authenticate, | 61 | authenticate, |
61 | ensureIsAdmin, | 62 | ensureUserHasRight(UserRight.MANAGE_USERS), |
62 | usersAddValidator, | 63 | usersAddValidator, |
63 | createUserRetryWrapper | 64 | createUserRetryWrapper |
64 | ) | 65 | ) |
@@ -77,14 +78,14 @@ usersRouter.put('/me', | |||
77 | 78 | ||
78 | usersRouter.put('/:id', | 79 | usersRouter.put('/:id', |
79 | authenticate, | 80 | authenticate, |
80 | ensureIsAdmin, | 81 | ensureUserHasRight(UserRight.MANAGE_USERS), |
81 | usersUpdateValidator, | 82 | usersUpdateValidator, |
82 | asyncMiddleware(updateUser) | 83 | asyncMiddleware(updateUser) |
83 | ) | 84 | ) |
84 | 85 | ||
85 | usersRouter.delete('/:id', | 86 | usersRouter.delete('/:id', |
86 | authenticate, | 87 | authenticate, |
87 | ensureIsAdmin, | 88 | ensureUserHasRight(UserRight.MANAGE_USERS), |
88 | usersRemoveValidator, | 89 | usersRemoveValidator, |
89 | asyncMiddleware(removeUser) | 90 | asyncMiddleware(removeUser) |
90 | ) | 91 | ) |
@@ -119,7 +120,7 @@ async function createUser (req: express.Request, res: express.Response, next: ex | |||
119 | password: body.password, | 120 | password: body.password, |
120 | email: body.email, | 121 | email: body.email, |
121 | displayNSFW: false, | 122 | displayNSFW: false, |
122 | role: USER_ROLES.USER, | 123 | role: body.role, |
123 | videoQuota: body.videoQuota | 124 | videoQuota: body.videoQuota |
124 | }) | 125 | }) |
125 | 126 | ||
@@ -136,7 +137,7 @@ async function registerUser (req: express.Request, res: express.Response, next: | |||
136 | password: body.password, | 137 | password: body.password, |
137 | email: body.email, | 138 | email: body.email, |
138 | displayNSFW: false, | 139 | displayNSFW: false, |
139 | role: USER_ROLES.USER, | 140 | role: UserRole.USER, |
140 | videoQuota: CONFIG.USER.VIDEO_QUOTA | 141 | videoQuota: CONFIG.USER.VIDEO_QUOTA |
141 | }) | 142 | }) |
142 | 143 | ||
@@ -203,6 +204,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex | |||
203 | 204 | ||
204 | if (body.email !== undefined) user.email = body.email | 205 | if (body.email !== undefined) user.email = body.email |
205 | if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota | 206 | if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota |
207 | if (body.role !== undefined) user.role = body.role | ||
206 | 208 | ||
207 | await user.save() | 209 | await user.save() |
208 | 210 | ||
diff --git a/server/controllers/api/videos/abuse.ts b/server/controllers/api/videos/abuse.ts index 4c7abf395..04349042b 100644 --- a/server/controllers/api/videos/abuse.ts +++ b/server/controllers/api/videos/abuse.ts | |||
@@ -9,7 +9,7 @@ import { | |||
9 | } from '../../../helpers' | 9 | } from '../../../helpers' |
10 | import { | 10 | import { |
11 | authenticate, | 11 | authenticate, |
12 | ensureIsAdmin, | 12 | ensureUserHasRight, |
13 | paginationValidator, | 13 | paginationValidator, |
14 | videoAbuseReportValidator, | 14 | videoAbuseReportValidator, |
15 | videoAbusesSortValidator, | 15 | videoAbusesSortValidator, |
@@ -18,13 +18,13 @@ import { | |||
18 | asyncMiddleware | 18 | asyncMiddleware |
19 | } from '../../../middlewares' | 19 | } from '../../../middlewares' |
20 | import { VideoInstance } from '../../../models' | 20 | import { VideoInstance } from '../../../models' |
21 | import { VideoAbuseCreate } from '../../../../shared' | 21 | import { VideoAbuseCreate, UserRight } from '../../../../shared' |
22 | 22 | ||
23 | const abuseVideoRouter = express.Router() | 23 | const abuseVideoRouter = express.Router() |
24 | 24 | ||
25 | abuseVideoRouter.get('/abuse', | 25 | abuseVideoRouter.get('/abuse', |
26 | authenticate, | 26 | authenticate, |
27 | ensureIsAdmin, | 27 | ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES), |
28 | paginationValidator, | 28 | paginationValidator, |
29 | videoAbusesSortValidator, | 29 | videoAbusesSortValidator, |
30 | setVideoAbusesSort, | 30 | setVideoAbusesSort, |
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts index 5a2c3fd80..be7cf6ea4 100644 --- a/server/controllers/api/videos/blacklist.ts +++ b/server/controllers/api/videos/blacklist.ts | |||
@@ -4,7 +4,7 @@ import { database as db } from '../../../initializers' | |||
4 | import { logger, getFormattedObjects } from '../../../helpers' | 4 | import { logger, getFormattedObjects } from '../../../helpers' |
5 | import { | 5 | import { |
6 | authenticate, | 6 | authenticate, |
7 | ensureIsAdmin, | 7 | ensureUserHasRight, |
8 | videosBlacklistAddValidator, | 8 | videosBlacklistAddValidator, |
9 | videosBlacklistRemoveValidator, | 9 | videosBlacklistRemoveValidator, |
10 | paginationValidator, | 10 | paginationValidator, |
@@ -14,20 +14,20 @@ import { | |||
14 | asyncMiddleware | 14 | asyncMiddleware |
15 | } from '../../../middlewares' | 15 | } from '../../../middlewares' |
16 | import { BlacklistedVideoInstance } from '../../../models' | 16 | import { BlacklistedVideoInstance } from '../../../models' |
17 | import { BlacklistedVideo } from '../../../../shared' | 17 | import { BlacklistedVideo, UserRight } from '../../../../shared' |
18 | 18 | ||
19 | const blacklistRouter = express.Router() | 19 | const blacklistRouter = express.Router() |
20 | 20 | ||
21 | blacklistRouter.post('/:videoId/blacklist', | 21 | blacklistRouter.post('/:videoId/blacklist', |
22 | authenticate, | 22 | authenticate, |
23 | ensureIsAdmin, | 23 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), |
24 | videosBlacklistAddValidator, | 24 | videosBlacklistAddValidator, |
25 | asyncMiddleware(addVideoToBlacklist) | 25 | asyncMiddleware(addVideoToBlacklist) |
26 | ) | 26 | ) |
27 | 27 | ||
28 | blacklistRouter.get('/blacklist', | 28 | blacklistRouter.get('/blacklist', |
29 | authenticate, | 29 | authenticate, |
30 | ensureIsAdmin, | 30 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), |
31 | paginationValidator, | 31 | paginationValidator, |
32 | blacklistSortValidator, | 32 | blacklistSortValidator, |
33 | setBlacklistSort, | 33 | setBlacklistSort, |
@@ -37,7 +37,7 @@ blacklistRouter.get('/blacklist', | |||
37 | 37 | ||
38 | blacklistRouter.delete('/:videoId/blacklist', | 38 | blacklistRouter.delete('/:videoId/blacklist', |
39 | authenticate, | 39 | authenticate, |
40 | ensureIsAdmin, | 40 | ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST), |
41 | videosBlacklistRemoveValidator, | 41 | videosBlacklistRemoveValidator, |
42 | asyncMiddleware(removeVideoFromBlacklistController) | 42 | asyncMiddleware(removeVideoFromBlacklistController) |
43 | ) | 43 | ) |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index c180eccda..f423d6317 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -1,9 +1,8 @@ | |||
1 | import { values } from 'lodash' | ||
2 | import * as validator from 'validator' | 1 | import * as validator from 'validator' |
3 | import 'express-validator' | 2 | import 'express-validator' |
4 | 3 | ||
5 | import { exists } from './misc' | 4 | import { exists } from './misc' |
6 | import { CONSTRAINTS_FIELDS, USER_ROLES } from '../../initializers' | 5 | import { CONSTRAINTS_FIELDS } from '../../initializers' |
7 | import { UserRole } from '../../../shared' | 6 | import { UserRole } from '../../../shared' |
8 | 7 | ||
9 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS | 8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS |
@@ -12,10 +11,6 @@ function isUserPasswordValid (value: string) { | |||
12 | return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD) | 11 | return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD) |
13 | } | 12 | } |
14 | 13 | ||
15 | function isUserRoleValid (value: string) { | ||
16 | return values(USER_ROLES).indexOf(value as UserRole) !== -1 | ||
17 | } | ||
18 | |||
19 | function isUserVideoQuotaValid (value: string) { | 14 | function isUserVideoQuotaValid (value: string) { |
20 | return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA) | 15 | return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA) |
21 | } | 16 | } |
@@ -30,6 +25,10 @@ function isUserDisplayNSFWValid (value: any) { | |||
30 | return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) | 25 | return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) |
31 | } | 26 | } |
32 | 27 | ||
28 | function isUserRoleValid (value: any) { | ||
29 | return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined | ||
30 | } | ||
31 | |||
33 | // --------------------------------------------------------------------------- | 32 | // --------------------------------------------------------------------------- |
34 | 33 | ||
35 | export { | 34 | export { |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 1581a3195..6dc9737d2 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -5,7 +5,6 @@ import { join } from 'path' | |||
5 | import { root, isTestInstance } from '../helpers/core-utils' | 5 | import { root, isTestInstance } from '../helpers/core-utils' |
6 | 6 | ||
7 | import { | 7 | import { |
8 | UserRole, | ||
9 | VideoRateType, | 8 | VideoRateType, |
10 | RequestEndpoint, | 9 | RequestEndpoint, |
11 | RequestVideoEventType, | 10 | RequestVideoEventType, |
@@ -16,7 +15,7 @@ import { | |||
16 | 15 | ||
17 | // --------------------------------------------------------------------------- | 16 | // --------------------------------------------------------------------------- |
18 | 17 | ||
19 | const LAST_MIGRATION_VERSION = 80 | 18 | const LAST_MIGRATION_VERSION = 85 |
20 | 19 | ||
21 | // --------------------------------------------------------------------------- | 20 | // --------------------------------------------------------------------------- |
22 | 21 | ||
@@ -283,7 +282,6 @@ const JOB_STATES: { [ id: string ]: JobState } = { | |||
283 | } | 282 | } |
284 | // How many maximum jobs we fetch from the database per cycle | 283 | // How many maximum jobs we fetch from the database per cycle |
285 | const JOBS_FETCH_LIMIT_PER_CYCLE = 10 | 284 | const JOBS_FETCH_LIMIT_PER_CYCLE = 10 |
286 | const JOBS_CONCURRENCY = 1 | ||
287 | // 1 minutes | 285 | // 1 minutes |
288 | let JOBS_FETCHING_INTERVAL = 60000 | 286 | let JOBS_FETCHING_INTERVAL = 60000 |
289 | 287 | ||
@@ -334,13 +332,6 @@ const CACHE = { | |||
334 | 332 | ||
335 | // --------------------------------------------------------------------------- | 333 | // --------------------------------------------------------------------------- |
336 | 334 | ||
337 | const USER_ROLES: { [ id: string ]: UserRole } = { | ||
338 | ADMIN: 'admin', | ||
339 | USER: 'user' | ||
340 | } | ||
341 | |||
342 | // --------------------------------------------------------------------------- | ||
343 | |||
344 | const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->' | 335 | const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->' |
345 | 336 | ||
346 | // --------------------------------------------------------------------------- | 337 | // --------------------------------------------------------------------------- |
@@ -367,7 +358,6 @@ export { | |||
367 | EMBED_SIZE, | 358 | EMBED_SIZE, |
368 | FRIEND_SCORE, | 359 | FRIEND_SCORE, |
369 | JOB_STATES, | 360 | JOB_STATES, |
370 | JOBS_CONCURRENCY, | ||
371 | JOBS_FETCH_LIMIT_PER_CYCLE, | 361 | JOBS_FETCH_LIMIT_PER_CYCLE, |
372 | JOBS_FETCHING_INTERVAL, | 362 | JOBS_FETCHING_INTERVAL, |
373 | LAST_MIGRATION_VERSION, | 363 | LAST_MIGRATION_VERSION, |
@@ -401,7 +391,6 @@ export { | |||
401 | STATIC_MAX_AGE, | 391 | STATIC_MAX_AGE, |
402 | STATIC_PATHS, | 392 | STATIC_PATHS, |
403 | THUMBNAILS_SIZE, | 393 | THUMBNAILS_SIZE, |
404 | USER_ROLES, | ||
405 | VIDEO_CATEGORIES, | 394 | VIDEO_CATEGORIES, |
406 | VIDEO_LANGUAGES, | 395 | VIDEO_LANGUAGES, |
407 | VIDEO_LICENCES, | 396 | VIDEO_LICENCES, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 4c04290fc..077472341 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -2,10 +2,11 @@ import * as passwordGenerator from 'password-generator' | |||
2 | import * as Bluebird from 'bluebird' | 2 | import * as Bluebird from 'bluebird' |
3 | 3 | ||
4 | import { database as db } from './database' | 4 | import { database as db } from './database' |
5 | import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' | 5 | import { CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' |
6 | import { clientsExist, usersExist } from './checker' | 6 | import { clientsExist, usersExist } from './checker' |
7 | import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' | 7 | import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' |
8 | import { createUserAuthorAndChannel } from '../lib' | 8 | import { createUserAuthorAndChannel } from '../lib' |
9 | import { UserRole } from '../../shared' | ||
9 | 10 | ||
10 | async function installApplication () { | 11 | async function installApplication () { |
11 | await db.sequelize.sync() | 12 | await db.sequelize.sync() |
@@ -88,7 +89,7 @@ async function createOAuthAdminIfNotExist () { | |||
88 | logger.info('Creating the administrator.') | 89 | logger.info('Creating the administrator.') |
89 | 90 | ||
90 | const username = 'root' | 91 | const username = 'root' |
91 | const role = USER_ROLES.ADMIN | 92 | const role = UserRole.ADMINISTRATOR |
92 | const email = CONFIG.ADMIN.EMAIL | 93 | const email = CONFIG.ADMIN.EMAIL |
93 | let validatePassword = true | 94 | let validatePassword = true |
94 | let password = '' | 95 | let password = '' |
diff --git a/server/initializers/migrations/0085-user-role.ts b/server/initializers/migrations/0085-user-role.ts new file mode 100644 index 000000000..e67c5ca24 --- /dev/null +++ b/server/initializers/migrations/0085-user-role.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | import * as uuidv4 from 'uuid/v4' | ||
3 | |||
4 | async function up (utils: { | ||
5 | transaction: Sequelize.Transaction, | ||
6 | queryInterface: Sequelize.QueryInterface, | ||
7 | sequelize: Sequelize.Sequelize, | ||
8 | db: any | ||
9 | }): Promise<void> { | ||
10 | const q = utils.queryInterface | ||
11 | |||
12 | await q.renameColumn('Users', 'role', 'oldRole') | ||
13 | |||
14 | const data = { | ||
15 | type: Sequelize.INTEGER, | ||
16 | allowNull: true | ||
17 | } | ||
18 | await q.addColumn('Users', 'role', data) | ||
19 | |||
20 | let query = 'UPDATE "Users" SET "role" = 0 WHERE "oldRole" = \'admin\'' | ||
21 | await utils.sequelize.query(query) | ||
22 | |||
23 | query = 'UPDATE "Users" SET "role" = 2 WHERE "oldRole" = \'user\'' | ||
24 | await utils.sequelize.query(query) | ||
25 | |||
26 | data.allowNull = false | ||
27 | await q.changeColumn('Users', 'role', data) | ||
28 | |||
29 | await q.removeColumn('Users', 'oldRole') | ||
30 | } | ||
31 | |||
32 | function down (options) { | ||
33 | throw new Error('Not implemented.') | ||
34 | } | ||
35 | |||
36 | export { | ||
37 | up, | ||
38 | down | ||
39 | } | ||
diff --git a/server/middlewares/admin.ts b/server/middlewares/admin.ts deleted file mode 100644 index 812397352..000000000 --- a/server/middlewares/admin.ts +++ /dev/null | |||
@@ -1,20 +0,0 @@ | |||
1 | import 'express-validator' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { logger } from '../helpers' | ||
5 | |||
6 | function ensureIsAdmin (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
7 | const user = res.locals.oauth.token.user | ||
8 | if (user.isAdmin() === false) { | ||
9 | logger.info('A non admin user is trying to access to an admin content.') | ||
10 | return res.sendStatus(403) | ||
11 | } | ||
12 | |||
13 | return next() | ||
14 | } | ||
15 | |||
16 | // --------------------------------------------------------------------------- | ||
17 | |||
18 | export { | ||
19 | ensureIsAdmin | ||
20 | } | ||
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 0e2c850e1..cec3e0b2a 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts | |||
@@ -1,5 +1,4 @@ | |||
1 | export * from './validators' | 1 | export * from './validators' |
2 | export * from './admin' | ||
3 | export * from './async' | 2 | export * from './async' |
4 | export * from './oauth' | 3 | export * from './oauth' |
5 | export * from './pagination' | 4 | export * from './pagination' |
@@ -7,3 +6,4 @@ export * from './pods' | |||
7 | export * from './search' | 6 | export * from './search' |
8 | export * from './secure' | 7 | export * from './secure' |
9 | export * from './sort' | 8 | export * from './sort' |
9 | export * from './user-right' | ||
diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts new file mode 100644 index 000000000..bcebe9d7f --- /dev/null +++ b/server/middlewares/user-right.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import 'express-validator' | ||
2 | import * as express from 'express' | ||
3 | |||
4 | import { UserInstance } from '../models' | ||
5 | import { UserRight } from '../../shared' | ||
6 | import { logger } from '../helpers' | ||
7 | |||
8 | function ensureUserHasRight (userRight: UserRight) { | ||
9 | return function (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
10 | const user: UserInstance = res.locals.oauth.token.user | ||
11 | if (user.hasRight(userRight) === false) { | ||
12 | logger.info('User %s does not have right %s to access to %s.', user.username, UserRight[userRight], req.path) | ||
13 | return res.sendStatus(403) | ||
14 | } | ||
15 | |||
16 | return next() | ||
17 | } | ||
18 | } | ||
19 | |||
20 | // --------------------------------------------------------------------------- | ||
21 | |||
22 | export { | ||
23 | ensureUserHasRight | ||
24 | } | ||
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 1a33cfd8c..0b463acc0 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -13,7 +13,8 @@ import { | |||
13 | isUserPasswordValid, | 13 | isUserPasswordValid, |
14 | isUserVideoQuotaValid, | 14 | isUserVideoQuotaValid, |
15 | isUserDisplayNSFWValid, | 15 | isUserDisplayNSFWValid, |
16 | isIdOrUUIDValid | 16 | isIdOrUUIDValid, |
17 | isUserRoleValid | ||
17 | } from '../../helpers' | 18 | } from '../../helpers' |
18 | import { UserInstance, VideoInstance } from '../../models' | 19 | import { UserInstance, VideoInstance } from '../../models' |
19 | 20 | ||
@@ -22,6 +23,7 @@ const usersAddValidator = [ | |||
22 | body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), | 23 | body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'), |
23 | body('email').isEmail().withMessage('Should have a valid email'), | 24 | body('email').isEmail().withMessage('Should have a valid email'), |
24 | body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 25 | body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
26 | body('role').custom(isUserRoleValid).withMessage('Should have a valid role'), | ||
25 | 27 | ||
26 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 28 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
27 | logger.debug('Checking usersAdd parameters', { parameters: req.body }) | 29 | logger.debug('Checking usersAdd parameters', { parameters: req.body }) |
@@ -75,6 +77,7 @@ const usersUpdateValidator = [ | |||
75 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), | 77 | param('id').isInt().not().isEmpty().withMessage('Should have a valid id'), |
76 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 78 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
77 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), | 79 | body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'), |
80 | body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'), | ||
78 | 81 | ||
79 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 82 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
80 | logger.debug('Checking usersUpdate parameters', { parameters: req.body }) | 83 | logger.debug('Checking usersUpdate parameters', { parameters: req.body }) |
diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index 979fbd34a..7d611728b 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts | |||
@@ -11,6 +11,8 @@ import { | |||
11 | checkVideoChannelExists, | 11 | checkVideoChannelExists, |
12 | checkVideoAuthorExists | 12 | checkVideoAuthorExists |
13 | } from '../../helpers' | 13 | } from '../../helpers' |
14 | import { UserInstance } from '../../models' | ||
15 | import { UserRight } from '../../../shared' | ||
14 | 16 | ||
15 | const listVideoAuthorChannelsValidator = [ | 17 | const listVideoAuthorChannelsValidator = [ |
16 | param('authorId').custom(isIdOrUUIDValid).withMessage('Should have a valid author id'), | 18 | param('authorId').custom(isIdOrUUIDValid).withMessage('Should have a valid author id'), |
@@ -106,7 +108,7 @@ export { | |||
106 | // --------------------------------------------------------------------------- | 108 | // --------------------------------------------------------------------------- |
107 | 109 | ||
108 | function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) { | 110 | function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) { |
109 | const user = res.locals.oauth.token.User | 111 | const user: UserInstance = res.locals.oauth.token.User |
110 | 112 | ||
111 | // Retrieve the user who did the request | 113 | // Retrieve the user who did the request |
112 | if (res.locals.videoChannel.isOwned() === false) { | 114 | if (res.locals.videoChannel.isOwned() === false) { |
@@ -118,7 +120,7 @@ function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => | |||
118 | // Check if the user can delete the video channel | 120 | // Check if the user can delete the video channel |
119 | // The user can delete it if s/he is an admin | 121 | // The user can delete it if s/he is an admin |
120 | // Or if s/he is the video channel's author | 122 | // Or if s/he is the video channel's author |
121 | if (user.isAdmin() === false && res.locals.videoChannel.Author.userId !== user.id) { | 123 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && res.locals.videoChannel.Author.userId !== user.id) { |
122 | return res.status(403) | 124 | return res.status(403) |
123 | .json({ error: 'Cannot remove video channel of another user' }) | 125 | .json({ error: 'Cannot remove video channel of another user' }) |
124 | .end() | 126 | .end() |
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index a032d14ce..0c07404c5 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts | |||
@@ -22,6 +22,7 @@ import { | |||
22 | checkVideoExists, | 22 | checkVideoExists, |
23 | isIdValid | 23 | isIdValid |
24 | } from '../../helpers' | 24 | } from '../../helpers' |
25 | import { UserRight } from '../../../shared' | ||
25 | 26 | ||
26 | const videosAddValidator = [ | 27 | const videosAddValidator = [ |
27 | body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( | 28 | body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage( |
@@ -231,7 +232,7 @@ function checkUserCanDeleteVideo (userId: number, res: express.Response, callbac | |||
231 | // Check if the user can delete the video | 232 | // Check if the user can delete the video |
232 | // The user can delete it if s/he is an admin | 233 | // The user can delete it if s/he is an admin |
233 | // Or if s/he is the video's author | 234 | // Or if s/he is the video's author |
234 | if (user.isAdmin() === false && res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { | 235 | if (user.hasRight(UserRight.REMOVE_ANY_VIDEO) === false && res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { |
235 | return res.status(403) | 236 | return res.status(403) |
236 | .json({ error: 'Cannot remove video of another user' }) | 237 | .json({ error: 'Cannot remove video of another user' }) |
237 | .end() | 238 | .end() |
diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts index 1b5233eaf..49c75aa3b 100644 --- a/server/models/user/user-interface.ts +++ b/server/models/user/user-interface.ts | |||
@@ -3,15 +3,16 @@ import * as Promise from 'bluebird' | |||
3 | 3 | ||
4 | // Don't use barrel, import just what we need | 4 | // Don't use barrel, import just what we need |
5 | import { User as FormattedUser } from '../../../shared/models/users/user.model' | 5 | import { User as FormattedUser } from '../../../shared/models/users/user.model' |
6 | import { UserRole } from '../../../shared/models/users/user-role.type' | ||
7 | import { ResultList } from '../../../shared/models/result-list.model' | 6 | import { ResultList } from '../../../shared/models/result-list.model' |
8 | import { AuthorInstance } from '../video/author-interface' | 7 | import { AuthorInstance } from '../video/author-interface' |
8 | import { UserRight } from '../../../shared/models/users/user-right.enum' | ||
9 | import { UserRole } from '../../../shared/models/users/user-role' | ||
9 | 10 | ||
10 | export namespace UserMethods { | 11 | export namespace UserMethods { |
12 | export type HasRight = (this: UserInstance, right: UserRight) => boolean | ||
11 | export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean> | 13 | export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean> |
12 | 14 | ||
13 | export type ToFormattedJSON = (this: UserInstance) => FormattedUser | 15 | export type ToFormattedJSON = (this: UserInstance) => FormattedUser |
14 | export type IsAdmin = (this: UserInstance) => boolean | ||
15 | export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean> | 16 | export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean> |
16 | 17 | ||
17 | export type CountTotal = () => Promise<number> | 18 | export type CountTotal = () => Promise<number> |
@@ -31,7 +32,7 @@ export namespace UserMethods { | |||
31 | export interface UserClass { | 32 | export interface UserClass { |
32 | isPasswordMatch: UserMethods.IsPasswordMatch, | 33 | isPasswordMatch: UserMethods.IsPasswordMatch, |
33 | toFormattedJSON: UserMethods.ToFormattedJSON, | 34 | toFormattedJSON: UserMethods.ToFormattedJSON, |
34 | isAdmin: UserMethods.IsAdmin, | 35 | hasRight: UserMethods.HasRight, |
35 | isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo, | 36 | isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo, |
36 | 37 | ||
37 | countTotal: UserMethods.CountTotal, | 38 | countTotal: UserMethods.CountTotal, |
@@ -62,7 +63,7 @@ export interface UserInstance extends UserClass, UserAttributes, Sequelize.Insta | |||
62 | 63 | ||
63 | isPasswordMatch: UserMethods.IsPasswordMatch | 64 | isPasswordMatch: UserMethods.IsPasswordMatch |
64 | toFormattedJSON: UserMethods.ToFormattedJSON | 65 | toFormattedJSON: UserMethods.ToFormattedJSON |
65 | isAdmin: UserMethods.IsAdmin | 66 | hasRight: UserMethods.HasRight |
66 | } | 67 | } |
67 | 68 | ||
68 | export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {} | 69 | export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {} |
diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 074c9c121..3c625e450 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts | |||
@@ -1,17 +1,17 @@ | |||
1 | import { values } from 'lodash' | ||
2 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
3 | import * as Promise from 'bluebird' | 2 | import * as Promise from 'bluebird' |
4 | 3 | ||
5 | import { getSort } from '../utils' | 4 | import { getSort } from '../utils' |
6 | import { USER_ROLES } from '../../initializers' | ||
7 | import { | 5 | import { |
8 | cryptPassword, | 6 | cryptPassword, |
9 | comparePassword, | 7 | comparePassword, |
10 | isUserPasswordValid, | 8 | isUserPasswordValid, |
11 | isUserUsernameValid, | 9 | isUserUsernameValid, |
12 | isUserDisplayNSFWValid, | 10 | isUserDisplayNSFWValid, |
13 | isUserVideoQuotaValid | 11 | isUserVideoQuotaValid, |
12 | isUserRoleValid | ||
14 | } from '../../helpers' | 13 | } from '../../helpers' |
14 | import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared' | ||
15 | 15 | ||
16 | import { addMethodsToModel } from '../utils' | 16 | import { addMethodsToModel } from '../utils' |
17 | import { | 17 | import { |
@@ -23,8 +23,8 @@ import { | |||
23 | 23 | ||
24 | let User: Sequelize.Model<UserInstance, UserAttributes> | 24 | let User: Sequelize.Model<UserInstance, UserAttributes> |
25 | let isPasswordMatch: UserMethods.IsPasswordMatch | 25 | let isPasswordMatch: UserMethods.IsPasswordMatch |
26 | let hasRight: UserMethods.HasRight | ||
26 | let toFormattedJSON: UserMethods.ToFormattedJSON | 27 | let toFormattedJSON: UserMethods.ToFormattedJSON |
27 | let isAdmin: UserMethods.IsAdmin | ||
28 | let countTotal: UserMethods.CountTotal | 28 | let countTotal: UserMethods.CountTotal |
29 | let getByUsername: UserMethods.GetByUsername | 29 | let getByUsername: UserMethods.GetByUsername |
30 | let listForApi: UserMethods.ListForApi | 30 | let listForApi: UserMethods.ListForApi |
@@ -76,8 +76,14 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
76 | } | 76 | } |
77 | }, | 77 | }, |
78 | role: { | 78 | role: { |
79 | type: DataTypes.ENUM(values(USER_ROLES)), | 79 | type: DataTypes.INTEGER, |
80 | allowNull: false | 80 | allowNull: false, |
81 | validate: { | ||
82 | roleValid: value => { | ||
83 | const res = isUserRoleValid(value) | ||
84 | if (res === false) throw new Error('Role is not valid.') | ||
85 | } | ||
86 | } | ||
81 | }, | 87 | }, |
82 | videoQuota: { | 88 | videoQuota: { |
83 | type: DataTypes.BIGINT, | 89 | type: DataTypes.BIGINT, |
@@ -120,9 +126,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da | |||
120 | loadByUsernameOrEmail | 126 | loadByUsernameOrEmail |
121 | ] | 127 | ] |
122 | const instanceMethods = [ | 128 | const instanceMethods = [ |
129 | hasRight, | ||
123 | isPasswordMatch, | 130 | isPasswordMatch, |
124 | toFormattedJSON, | 131 | toFormattedJSON, |
125 | isAdmin, | ||
126 | isAbleToUploadVideo | 132 | isAbleToUploadVideo |
127 | ] | 133 | ] |
128 | addMethodsToModel(User, classMethods, instanceMethods) | 134 | addMethodsToModel(User, classMethods, instanceMethods) |
@@ -139,6 +145,10 @@ function beforeCreateOrUpdate (user: UserInstance) { | |||
139 | 145 | ||
140 | // ------------------------------ METHODS ------------------------------ | 146 | // ------------------------------ METHODS ------------------------------ |
141 | 147 | ||
148 | hasRight = function (this: UserInstance, right: UserRight) { | ||
149 | return hasUserRight(this.role, right) | ||
150 | } | ||
151 | |||
142 | isPasswordMatch = function (this: UserInstance, password: string) { | 152 | isPasswordMatch = function (this: UserInstance, password: string) { |
143 | return comparePassword(password, this.password) | 153 | return comparePassword(password, this.password) |
144 | } | 154 | } |
@@ -150,6 +160,7 @@ toFormattedJSON = function (this: UserInstance) { | |||
150 | email: this.email, | 160 | email: this.email, |
151 | displayNSFW: this.displayNSFW, | 161 | displayNSFW: this.displayNSFW, |
152 | role: this.role, | 162 | role: this.role, |
163 | roleLabel: USER_ROLE_LABELS[this.role], | ||
153 | videoQuota: this.videoQuota, | 164 | videoQuota: this.videoQuota, |
154 | createdAt: this.createdAt, | 165 | createdAt: this.createdAt, |
155 | author: { | 166 | author: { |
@@ -174,10 +185,6 @@ toFormattedJSON = function (this: UserInstance) { | |||
174 | return json | 185 | return json |
175 | } | 186 | } |
176 | 187 | ||
177 | isAdmin = function (this: UserInstance) { | ||
178 | return this.role === USER_ROLES.ADMIN | ||
179 | } | ||
180 | |||
181 | isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { | 188 | isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { |
182 | if (this.videoQuota === -1) return Promise.resolve(true) | 189 | if (this.videoQuota === -1) return Promise.resolve(true) |
183 | 190 | ||
diff --git a/shared/models/users/index.ts b/shared/models/users/index.ts index efb58c320..a260bd380 100644 --- a/shared/models/users/index.ts +++ b/shared/models/users/index.ts | |||
@@ -4,4 +4,5 @@ export * from './user-login.model' | |||
4 | export * from './user-refresh-token.model' | 4 | export * from './user-refresh-token.model' |
5 | export * from './user-update.model' | 5 | export * from './user-update.model' |
6 | export * from './user-update-me.model' | 6 | export * from './user-update-me.model' |
7 | export * from './user-role.type' | 7 | export * from './user-right.enum' |
8 | export * from './user-role' | ||
diff --git a/shared/models/users/user-create.model.ts b/shared/models/users/user-create.model.ts index 49fa2549d..65830f55e 100644 --- a/shared/models/users/user-create.model.ts +++ b/shared/models/users/user-create.model.ts | |||
@@ -1,6 +1,9 @@ | |||
1 | import { UserRole } from './user-role' | ||
2 | |||
1 | export interface UserCreate { | 3 | export interface UserCreate { |
2 | username: string | 4 | username: string |
3 | password: string | 5 | password: string |
4 | email: string | 6 | email: string |
5 | videoQuota: number | 7 | videoQuota: number |
8 | role: UserRole | ||
6 | } | 9 | } |
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts new file mode 100644 index 000000000..c8c710450 --- /dev/null +++ b/shared/models/users/user-right.enum.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | export enum UserRight { | ||
2 | ALL, | ||
3 | MANAGE_USERS, | ||
4 | MANAGE_PODS, | ||
5 | MANAGE_VIDEO_ABUSES, | ||
6 | MANAGE_REQUEST_SCHEDULERS, | ||
7 | MANAGE_VIDEO_BLACKLIST, | ||
8 | REMOVE_ANY_VIDEO, | ||
9 | REMOVE_ANY_VIDEO_CHANNEL, | ||
10 | } | ||
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts new file mode 100644 index 000000000..cc32c768d --- /dev/null +++ b/shared/models/users/user-role.ts | |||
@@ -0,0 +1,36 @@ | |||
1 | import { UserRight } from './user-right.enum' | ||
2 | |||
3 | // Keep the order | ||
4 | export enum UserRole { | ||
5 | ADMINISTRATOR = 0, | ||
6 | MODERATOR = 1, | ||
7 | USER = 2 | ||
8 | } | ||
9 | |||
10 | export const USER_ROLE_LABELS = { | ||
11 | [UserRole.USER]: 'User', | ||
12 | [UserRole.MODERATOR]: 'Moderator', | ||
13 | [UserRole.ADMINISTRATOR]: 'Administrator' | ||
14 | } | ||
15 | |||
16 | // TODO: use UserRole for key once https://github.com/Microsoft/TypeScript/issues/13042 is fixed | ||
17 | const userRoleRights: { [ id: number ]: UserRight[] } = { | ||
18 | [UserRole.ADMINISTRATOR]: [ | ||
19 | UserRight.ALL | ||
20 | ], | ||
21 | |||
22 | [UserRole.MODERATOR]: [ | ||
23 | UserRight.MANAGE_VIDEO_BLACKLIST, | ||
24 | UserRight.MANAGE_VIDEO_ABUSES, | ||
25 | UserRight.REMOVE_ANY_VIDEO, | ||
26 | UserRight.REMOVE_ANY_VIDEO_CHANNEL | ||
27 | ], | ||
28 | |||
29 | [UserRole.USER]: [] | ||
30 | } | ||
31 | |||
32 | export function hasUserRight (userRole: UserRole, userRight: UserRight) { | ||
33 | const userRights = userRoleRights[userRole] | ||
34 | |||
35 | return userRights.indexOf(UserRight.ALL) !== -1 || userRights.indexOf(userRight) !== -1 | ||
36 | } | ||
diff --git a/shared/models/users/user-role.type.ts b/shared/models/users/user-role.type.ts deleted file mode 100644 index b38c4c8c3..000000000 --- a/shared/models/users/user-role.type.ts +++ /dev/null | |||
@@ -1 +0,0 @@ | |||
1 | export type UserRole = 'admin' | 'user' | ||
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts index e22166fdc..96b454b7c 100644 --- a/shared/models/users/user-update.model.ts +++ b/shared/models/users/user-update.model.ts | |||
@@ -1,4 +1,7 @@ | |||
1 | import { UserRole } from './user-role' | ||
2 | |||
1 | export interface UserUpdate { | 3 | export interface UserUpdate { |
2 | email?: string | 4 | email?: string |
3 | videoQuota?: number | 5 | videoQuota?: number |
6 | role?: UserRole | ||
4 | } | 7 | } |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 175e72f28..ee2147590 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { UserRole } from './user-role.type' | ||
2 | import { VideoChannel } from '../videos/video-channel.model' | 1 | import { VideoChannel } from '../videos/video-channel.model' |
2 | import { UserRole } from './user-role' | ||
3 | 3 | ||
4 | export interface User { | 4 | export interface User { |
5 | id: number | 5 | id: number |