aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-10-27 16:55:03 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-10-27 16:55:03 +0200
commit954605a804da399317ca62afa2fb9244afa11ebf (patch)
treede6ee69280bfb928bc01c29430e13d5b820e921a
parente02573ad67626210ed279bad321ee139094921a1 (diff)
downloadPeerTube-954605a804da399317ca62afa2fb9244afa11ebf.tar.gz
PeerTube-954605a804da399317ca62afa2fb9244afa11ebf.tar.zst
PeerTube-954605a804da399317ca62afa2fb9244afa11ebf.zip
Support roles with rights and add moderator role
-rw-r--r--client/src/app/+admin/admin-routing.module.ts5
-rw-r--r--client/src/app/+admin/admin.module.ts4
-rw-r--r--client/src/app/+admin/friends/friends.routes.ts6
-rw-r--r--client/src/app/+admin/request-schedulers/request-schedulers.routes.ts6
-rw-r--r--client/src/app/+admin/users/user-edit/user-add.component.ts8
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.component.html13
-rw-r--r--client/src/app/+admin/users/user-edit/user-edit.ts4
-rw-r--r--client/src/app/+admin/users/user-edit/user-update.component.ts14
-rw-r--r--client/src/app/+admin/users/user-list/user-list.component.html2
-rw-r--r--client/src/app/+admin/users/users.routes.ts6
-rw-r--r--client/src/app/+admin/video-abuses/video-abuses.routes.ts9
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist.routes.ts6
-rw-r--r--client/src/app/core/auth/auth-user.model.ts11
-rw-r--r--client/src/app/core/auth/auth.service.ts8
-rw-r--r--client/src/app/core/auth/index.ts2
-rw-r--r--client/src/app/core/core.module.ts6
-rw-r--r--client/src/app/core/menu/menu-admin.component.html10
-rw-r--r--client/src/app/core/menu/menu-admin.component.ts27
-rw-r--r--client/src/app/core/menu/menu.component.html4
-rw-r--r--client/src/app/core/menu/menu.component.ts46
-rw-r--r--client/src/app/core/routing/index.ts2
-rw-r--r--client/src/app/core/routing/login-guard.service.ts (renamed from client/src/app/core/auth/login-guard.service.ts)2
-rw-r--r--client/src/app/core/routing/user-right-guard.service.ts (renamed from client/src/app/+admin/admin-guard.service.ts)11
-rw-r--r--client/src/app/shared/forms/form-validators/user.ts6
-rw-r--r--client/src/app/shared/users/user.model.ts8
-rw-r--r--client/src/app/videos/shared/video-details.model.ts14
-rw-r--r--client/src/app/videos/video-list/video-list.component.ts2
-rw-r--r--server/controllers/api/pods.ts9
-rw-r--r--server/controllers/api/request-schedulers.ts6
-rw-r--r--server/controllers/api/users.ts20
-rw-r--r--server/controllers/api/videos/abuse.ts6
-rw-r--r--server/controllers/api/videos/blacklist.ts10
-rw-r--r--server/helpers/custom-validators/users.ts11
-rw-r--r--server/initializers/constants.ts13
-rw-r--r--server/initializers/installer.ts5
-rw-r--r--server/initializers/migrations/0085-user-role.ts39
-rw-r--r--server/middlewares/admin.ts20
-rw-r--r--server/middlewares/index.ts2
-rw-r--r--server/middlewares/user-right.ts24
-rw-r--r--server/middlewares/validators/users.ts5
-rw-r--r--server/middlewares/validators/video-channels.ts6
-rw-r--r--server/middlewares/validators/videos.ts3
-rw-r--r--server/models/user/user-interface.ts9
-rw-r--r--server/models/user/user.ts29
-rw-r--r--shared/models/users/index.ts3
-rw-r--r--shared/models/users/user-create.model.ts3
-rw-r--r--shared/models/users/user-right.enum.ts10
-rw-r--r--shared/models/users/user-role.ts36
-rw-r--r--shared/models/users/user-role.type.ts1
-rw-r--r--shared/models/users/user-update.model.ts3
-rw-r--r--shared/models/users/user.model.ts2
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'
8import { RequestSchedulersRoutes } from './request-schedulers' 8import { RequestSchedulersRoutes } from './request-schedulers'
9import { UsersRoutes } from './users' 9import { UsersRoutes } from './users'
10import { VideoAbusesRoutes } from './video-abuses' 10import { VideoAbusesRoutes } from './video-abuses'
11import { AdminGuard } from './admin-guard.service'
12import { VideoBlacklistRoutes } from './video-blacklist' 11import { VideoBlacklistRoutes } from './video-blacklist'
13 12
14const adminRoutes: Routes = [ 13const 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
8import { VideoAbusesComponent, VideoAbuseListComponent } from './video-abuses' 8import { VideoAbusesComponent, VideoAbuseListComponent } from './video-abuses'
9import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist' 9import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
10import { SharedModule } from '../shared' 10import { SharedModule } from '../shared'
11import { 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})
52export class AdminModule { } 50export 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 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2 2
3import { UserRightGuard } from '../../core'
3import { FriendsComponent } from './friends.component' 4import { FriendsComponent } from './friends.component'
4import { FriendAddComponent } from './friend-add' 5import { FriendAddComponent } from './friend-add'
5import { FriendListComponent } from './friend-list' 6import { FriendListComponent } from './friend-list'
7import { UserRight } from '../../../../../shared'
6 8
7export const FriendsRoutes: Routes = [ 9export 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 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2 2
3import { UserRightGuard } from '../../core'
4import { UserRight } from '../../../../../shared'
3import { RequestSchedulersComponent } from './request-schedulers.component' 5import { RequestSchedulersComponent } from './request-schedulers.component'
4import { RequestSchedulersStatsComponent } from './request-schedulers-stats' 6import { 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'
14import { ServerService } from '../../../core' 15import { ServerService } from '../../../core'
15import { UserCreate } from '../../../../../../shared' 16import { UserCreate, UserRole } from '../../../../../../shared'
16import { UserEdit } from './user-edit' 17import { 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 @@
1import { ServerService } from '../../../core' 1import { ServerService } from '../../../core'
2import { FormReactive } from '../../../shared' 2import { FormReactive } from '../../../shared'
3import { VideoResolution } from '../../../../../../shared/models/videos/video-resolution.enum' 3import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
4 4
5export abstract class UserEdit extends FormReactive { 5export 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'
6import { NotificationsService } from 'angular2-notifications' 6import { NotificationsService } from 'angular2-notifications'
7 7
8import { UserService } from '../shared' 8import { UserService } from '../shared'
9import { USER_EMAIL, USER_VIDEO_QUOTA } from '../../../shared' 9import {
10 USER_EMAIL,
11 USER_VIDEO_QUOTA,
12 USER_ROLE,
13 User
14} from '../../../shared'
10import { ServerService } from '../../../core' 15import { ServerService } from '../../../core'
11import { UserUpdate } from '../../../../../../shared/models/users/user-update.model'
12import { User } from '../../../shared/users/user.model'
13import { UserEdit } from './user-edit' 16import { UserEdit } from './user-edit'
17import { 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 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2 2
3import { UserRightGuard } from '../../core'
4import { UserRight } from '../../../../../shared'
3import { UsersComponent } from './users.component' 5import { UsersComponent } from './users.component'
4import { UserAddComponent, UserUpdateComponent } from './user-edit' 6import { UserAddComponent, UserUpdateComponent } from './user-edit'
5import { UserListComponent } from './user-list' 7import { 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 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2 2
3import { UserRightGuard } from '../../core'
4import { UserRight } from '../../../../../shared'
3import { VideoAbusesComponent } from './video-abuses.component' 5import { VideoAbusesComponent } from './video-abuses.component'
4import { VideoAbuseListComponent } from './video-abuse-list' 6import { VideoAbuseListComponent } from './video-abuse-list'
5 7
6export const VideoAbusesRoutes: Routes = [ 8export 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 @@
1import { Routes } from '@angular/router' 1import { Routes } from '@angular/router'
2 2
3import { UserRightGuard } from '../../core'
4import { UserRight } from '../../../../../shared'
3import { VideoBlacklistComponent } from './video-blacklist.component' 5import { VideoBlacklistComponent } from './video-blacklist.component'
4import { VideoBlacklistListComponent } from './video-blacklist-list' 6import { 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)
2import { UserRole } from '../../../../../shared/models/users/user-role.type' 2import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role'
3import { User, UserConstructorHash } from '../../shared/users/user.model' 3import { User, UserConstructorHash } from '../../shared/users/user.model'
4import { UserRight } from '../../../../../shared/models/users/user-right.enum'
4 5
5export type TokenOptions = { 6export 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)
22import { RestExtractor } from '../../shared/rest' 22import { RestExtractor } from '../../shared/rest'
23import { UserLogin } from '../../../../../shared/models/users/user-login.model' 23import { UserLogin } from '../../../../../shared/models/users/user-login.model'
24import { User, UserConstructorHash } from '../../shared/users/user.model' 24import { UserConstructorHash } from '../../shared/users/user.model'
25 25
26interface UserLoginWithUsername extends UserLogin { 26interface 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 @@
1export * from './auth-status.model' 1export * from './auth-status.model'
2export * from './auth-user.model' 2export * from './auth-user.model'
3export * from './auth.service' 3export * from './auth.service'
4export * from './login-guard.service' 4export * 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'
7import { SimpleNotificationsModule } from 'angular2-notifications' 7import { SimpleNotificationsModule } from 'angular2-notifications'
8import { ModalModule } from 'ngx-bootstrap/modal' 8import { ModalModule } from 'ngx-bootstrap/modal'
9 9
10import { AuthService, LoginGuard } from './auth' 10import { AuthService } from './auth'
11import { LoginGuard, UserRightGuard } from './routing'
11import { ServerService } from './server' 12import { ServerService } from './server'
12import { ConfirmComponent, ConfirmService } from './confirm' 13import { ConfirmComponent, ConfirmService } from './confirm'
13import { MenuComponent, MenuAdminComponent } from './menu' 14import { 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})
48export class CoreModule { 50export 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 @@
1import { Component } from '@angular/core' 1import { Component } from '@angular/core'
2 2
3import { AuthService } from '../auth/auth.service'
4import { 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})
8export class MenuAdminComponent { } 11export 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
4import { AuthService, AuthStatus } from '../auth' 4import { AuthService, AuthStatus } from '../auth'
5import { ServerService } from '../server' 5import { ServerService } from '../server'
6import { 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})
12export class MenuComponent implements OnInit { 13export 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 @@
1export * from './login-guard.service'
2export * from './user-right-guard.service'
1export * from './preload-selected-modules-list' 3export * 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
10import { AuthService } from './auth.service' 10import { AuthService } from '../auth/auth.service'
11 11
12@Injectable() 12@Injectable()
13export class LoginGuard implements CanActivate, CanActivateChild { 13export 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
10import { AuthService } from '../core' 10import { AuthService } from '../auth'
11 11
12@Injectable() 12@Injectable()
13export class AdminGuard implements CanActivate, CanActivateChild { 13export 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}
32export 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 @@
1import { 1import {
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
7export type UserConstructorHash = { 9export 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 @@
1import { Video } from './video.model' 1import { Video } from './video.model'
2import { AuthUser } from '../../core'
2import { 3import {
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
9export class VideoDetails extends Video implements VideoDetailsServerModel { 11export 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'
15import { Search, SearchField, SearchService, User} from '../../shared' 15import { 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'
10import { 10import {
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'
22import { PodInstance } from '../../models' 22import { PodInstance } from '../../models'
23import { UserRight } from '../../../shared'
23 24
24const podsRouter = express.Router() 25const podsRouter = express.Router()
25 26
@@ -32,19 +33,19 @@ podsRouter.get('/',
32) 33)
33podsRouter.post('/make-friends', 34podsRouter.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)
40podsRouter.get('/quit-friends', 41podsRouter.get('/quit-friends',
41 authenticate, 42 authenticate,
42 ensureIsAdmin, 43 ensureUserHasRight(UserRight.MANAGE_PODS),
43 asyncMiddleware(quitFriendsController) 44 asyncMiddleware(quitFriendsController)
44) 45)
45podsRouter.delete('/:id', 46podsRouter.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'
10import { authenticate, ensureIsAdmin, asyncMiddleware } from '../../middlewares' 10import { authenticate, ensureUserHasRight, asyncMiddleware } from '../../middlewares'
11import { RequestSchedulerStatsAttributes } from '../../../shared' 11import { RequestSchedulerStatsAttributes, UserRight } from '../../../shared'
12 12
13const requestSchedulerRouter = express.Router() 13const requestSchedulerRouter = express.Router()
14 14
15requestSchedulerRouter.get('/stats', 15requestSchedulerRouter.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 @@
1import * as express from 'express' 1import * as express from 'express'
2 2
3import { database as db } from '../../initializers/database' 3import { database as db, CONFIG } from '../../initializers'
4import { USER_ROLES, CONFIG } from '../../initializers'
5import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers' 4import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers'
6import { 5import {
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'
30import { createUserAuthorAndChannel } from '../../lib' 31import { createUserAuthorAndChannel } from '../../lib'
31import { UserInstance } from '../../models' 32import { UserInstance } from '../../models'
@@ -58,7 +59,7 @@ usersRouter.get('/:id',
58 59
59usersRouter.post('/', 60usersRouter.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
78usersRouter.put('/:id', 79usersRouter.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
85usersRouter.delete('/:id', 86usersRouter.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'
10import { 10import {
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'
20import { VideoInstance } from '../../../models' 20import { VideoInstance } from '../../../models'
21import { VideoAbuseCreate } from '../../../../shared' 21import { VideoAbuseCreate, UserRight } from '../../../../shared'
22 22
23const abuseVideoRouter = express.Router() 23const abuseVideoRouter = express.Router()
24 24
25abuseVideoRouter.get('/abuse', 25abuseVideoRouter.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'
4import { logger, getFormattedObjects } from '../../../helpers' 4import { logger, getFormattedObjects } from '../../../helpers'
5import { 5import {
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'
16import { BlacklistedVideoInstance } from '../../../models' 16import { BlacklistedVideoInstance } from '../../../models'
17import { BlacklistedVideo } from '../../../../shared' 17import { BlacklistedVideo, UserRight } from '../../../../shared'
18 18
19const blacklistRouter = express.Router() 19const blacklistRouter = express.Router()
20 20
21blacklistRouter.post('/:videoId/blacklist', 21blacklistRouter.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
28blacklistRouter.get('/blacklist', 28blacklistRouter.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
38blacklistRouter.delete('/:videoId/blacklist', 38blacklistRouter.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 @@
1import { values } from 'lodash'
2import * as validator from 'validator' 1import * as validator from 'validator'
3import 'express-validator' 2import 'express-validator'
4 3
5import { exists } from './misc' 4import { exists } from './misc'
6import { CONSTRAINTS_FIELDS, USER_ROLES } from '../../initializers' 5import { CONSTRAINTS_FIELDS } from '../../initializers'
7import { UserRole } from '../../../shared' 6import { UserRole } from '../../../shared'
8 7
9const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 8const 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
15function isUserRoleValid (value: string) {
16 return values(USER_ROLES).indexOf(value as UserRole) !== -1
17}
18
19function isUserVideoQuotaValid (value: string) { 14function 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
28function isUserRoleValid (value: any) {
29 return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
30}
31
33// --------------------------------------------------------------------------- 32// ---------------------------------------------------------------------------
34 33
35export { 34export {
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'
5import { root, isTestInstance } from '../helpers/core-utils' 5import { root, isTestInstance } from '../helpers/core-utils'
6 6
7import { 7import {
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
19const LAST_MIGRATION_VERSION = 80 18const 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
285const JOBS_FETCH_LIMIT_PER_CYCLE = 10 284const JOBS_FETCH_LIMIT_PER_CYCLE = 10
286const JOBS_CONCURRENCY = 1
287// 1 minutes 285// 1 minutes
288let JOBS_FETCHING_INTERVAL = 60000 286let JOBS_FETCHING_INTERVAL = 60000
289 287
@@ -334,13 +332,6 @@ const CACHE = {
334 332
335// --------------------------------------------------------------------------- 333// ---------------------------------------------------------------------------
336 334
337const USER_ROLES: { [ id: string ]: UserRole } = {
338 ADMIN: 'admin',
339 USER: 'user'
340}
341
342// ---------------------------------------------------------------------------
343
344const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->' 335const 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'
2import * as Bluebird from 'bluebird' 2import * as Bluebird from 'bluebird'
3 3
4import { database as db } from './database' 4import { database as db } from './database'
5import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' 5import { CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants'
6import { clientsExist, usersExist } from './checker' 6import { clientsExist, usersExist } from './checker'
7import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' 7import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers'
8import { createUserAuthorAndChannel } from '../lib' 8import { createUserAuthorAndChannel } from '../lib'
9import { UserRole } from '../../shared'
9 10
10async function installApplication () { 11async 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 @@
1import * as Sequelize from 'sequelize'
2import * as uuidv4 from 'uuid/v4'
3
4async 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
32function down (options) {
33 throw new Error('Not implemented.')
34}
35
36export {
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 @@
1import 'express-validator'
2import * as express from 'express'
3
4import { logger } from '../helpers'
5
6function 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
18export {
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 @@
1export * from './validators' 1export * from './validators'
2export * from './admin'
3export * from './async' 2export * from './async'
4export * from './oauth' 3export * from './oauth'
5export * from './pagination' 4export * from './pagination'
@@ -7,3 +6,4 @@ export * from './pods'
7export * from './search' 6export * from './search'
8export * from './secure' 7export * from './secure'
9export * from './sort' 8export * from './sort'
9export * 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 @@
1import 'express-validator'
2import * as express from 'express'
3
4import { UserInstance } from '../models'
5import { UserRight } from '../../shared'
6import { logger } from '../helpers'
7
8function 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
22export {
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'
18import { UserInstance, VideoInstance } from '../../models' 19import { 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'
14import { UserInstance } from '../../models'
15import { UserRight } from '../../../shared'
14 16
15const listVideoAuthorChannelsValidator = [ 17const 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
108function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) { 110function 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'
25import { UserRight } from '../../../shared'
25 26
26const videosAddValidator = [ 27const 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
5import { User as FormattedUser } from '../../../shared/models/users/user.model' 5import { User as FormattedUser } from '../../../shared/models/users/user.model'
6import { UserRole } from '../../../shared/models/users/user-role.type'
7import { ResultList } from '../../../shared/models/result-list.model' 6import { ResultList } from '../../../shared/models/result-list.model'
8import { AuthorInstance } from '../video/author-interface' 7import { AuthorInstance } from '../video/author-interface'
8import { UserRight } from '../../../shared/models/users/user-right.enum'
9import { UserRole } from '../../../shared/models/users/user-role'
9 10
10export namespace UserMethods { 11export 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 {
31export interface UserClass { 32export 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
68export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {} 69export 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 @@
1import { values } from 'lodash'
2import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
3import * as Promise from 'bluebird' 2import * as Promise from 'bluebird'
4 3
5import { getSort } from '../utils' 4import { getSort } from '../utils'
6import { USER_ROLES } from '../../initializers'
7import { 5import {
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'
14import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared'
15 15
16import { addMethodsToModel } from '../utils' 16import { addMethodsToModel } from '../utils'
17import { 17import {
@@ -23,8 +23,8 @@ import {
23 23
24let User: Sequelize.Model<UserInstance, UserAttributes> 24let User: Sequelize.Model<UserInstance, UserAttributes>
25let isPasswordMatch: UserMethods.IsPasswordMatch 25let isPasswordMatch: UserMethods.IsPasswordMatch
26let hasRight: UserMethods.HasRight
26let toFormattedJSON: UserMethods.ToFormattedJSON 27let toFormattedJSON: UserMethods.ToFormattedJSON
27let isAdmin: UserMethods.IsAdmin
28let countTotal: UserMethods.CountTotal 28let countTotal: UserMethods.CountTotal
29let getByUsername: UserMethods.GetByUsername 29let getByUsername: UserMethods.GetByUsername
30let listForApi: UserMethods.ListForApi 30let 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
148hasRight = function (this: UserInstance, right: UserRight) {
149 return hasUserRight(this.role, right)
150}
151
142isPasswordMatch = function (this: UserInstance, password: string) { 152isPasswordMatch = 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
177isAdmin = function (this: UserInstance) {
178 return this.role === USER_ROLES.ADMIN
179}
180
181isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) { 188isAbleToUploadVideo = 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'
4export * from './user-refresh-token.model' 4export * from './user-refresh-token.model'
5export * from './user-update.model' 5export * from './user-update.model'
6export * from './user-update-me.model' 6export * from './user-update-me.model'
7export * from './user-role.type' 7export * from './user-right.enum'
8export * 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 @@
1import { UserRole } from './user-role'
2
1export interface UserCreate { 3export 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 @@
1export 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 @@
1import { UserRight } from './user-right.enum'
2
3// Keep the order
4export enum UserRole {
5 ADMINISTRATOR = 0,
6 MODERATOR = 1,
7 USER = 2
8}
9
10export 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
17const 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
32export 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 @@
1export 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 @@
1import { UserRole } from './user-role'
2
1export interface UserUpdate { 3export 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 @@
1import { UserRole } from './user-role.type'
2import { VideoChannel } from '../videos/video-channel.model' 1import { VideoChannel } from '../videos/video-channel.model'
2import { UserRole } from './user-role'
3 3
4export interface User { 4export interface User {
5 id: number 5 id: number