Support roles with rights and add moderator role
authorChocobozzz <florian.bigard@gmail.com>
Fri, 27 Oct 2017 14:55:03 +0000 (16:55 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 27 Oct 2017 14:55:03 +0000 (16:55 +0200)
53 files changed:
client/src/app/+admin/admin-guard.service.ts [deleted file]
client/src/app/+admin/admin-routing.module.ts
client/src/app/+admin/admin.module.ts
client/src/app/+admin/friends/friends.routes.ts
client/src/app/+admin/request-schedulers/request-schedulers.routes.ts
client/src/app/+admin/users/user-edit/user-add.component.ts
client/src/app/+admin/users/user-edit/user-edit.component.html
client/src/app/+admin/users/user-edit/user-edit.ts
client/src/app/+admin/users/user-edit/user-update.component.ts
client/src/app/+admin/users/user-list/user-list.component.html
client/src/app/+admin/users/users.routes.ts
client/src/app/+admin/video-abuses/video-abuses.routes.ts
client/src/app/+admin/video-blacklist/video-blacklist.routes.ts
client/src/app/core/auth/auth-user.model.ts
client/src/app/core/auth/auth.service.ts
client/src/app/core/auth/index.ts
client/src/app/core/auth/login-guard.service.ts [deleted file]
client/src/app/core/core.module.ts
client/src/app/core/menu/menu-admin.component.html
client/src/app/core/menu/menu-admin.component.ts
client/src/app/core/menu/menu.component.html
client/src/app/core/menu/menu.component.ts
client/src/app/core/routing/index.ts
client/src/app/core/routing/login-guard.service.ts [new file with mode: 0644]
client/src/app/core/routing/user-right-guard.service.ts [new file with mode: 0644]
client/src/app/shared/forms/form-validators/user.ts
client/src/app/shared/users/user.model.ts
client/src/app/videos/shared/video-details.model.ts
client/src/app/videos/video-list/video-list.component.ts
server/controllers/api/pods.ts
server/controllers/api/request-schedulers.ts
server/controllers/api/users.ts
server/controllers/api/videos/abuse.ts
server/controllers/api/videos/blacklist.ts
server/helpers/custom-validators/users.ts
server/initializers/constants.ts
server/initializers/installer.ts
server/initializers/migrations/0085-user-role.ts [new file with mode: 0644]
server/middlewares/admin.ts [deleted file]
server/middlewares/index.ts
server/middlewares/user-right.ts [new file with mode: 0644]
server/middlewares/validators/users.ts
server/middlewares/validators/video-channels.ts
server/middlewares/validators/videos.ts
server/models/user/user-interface.ts
server/models/user/user.ts
shared/models/users/index.ts
shared/models/users/user-create.model.ts
shared/models/users/user-right.enum.ts [new file with mode: 0644]
shared/models/users/user-role.ts [new file with mode: 0644]
shared/models/users/user-role.type.ts [deleted file]
shared/models/users/user-update.model.ts
shared/models/users/user.model.ts

diff --git a/client/src/app/+admin/admin-guard.service.ts b/client/src/app/+admin/admin-guard.service.ts
deleted file mode 100644 (file)
index 429dc03..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Injectable } from '@angular/core'
-import {
-  ActivatedRouteSnapshot,
-  CanActivateChild,
-  RouterStateSnapshot,
-  CanActivate,
-  Router
-} from '@angular/router'
-
-import { AuthService } from '../core'
-
-@Injectable()
-export class AdminGuard implements CanActivate, CanActivateChild {
-
-  constructor (
-    private router: Router,
-    private auth: AuthService
-  ) {}
-
-  canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
-    if (this.auth.isAdmin() === true) return true
-
-    this.router.navigate([ '/login' ])
-    return false
-  }
-
-  canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
-    return this.canActivate(route, state)
-  }
-}
index c3e4895ac6c88bc12994ef098138c16cecaa2f11..7262768fedcd1ff9137b52cf99fe6e8e85b32f93 100644 (file)
@@ -8,15 +8,14 @@ import { FriendsRoutes } from './friends'
 import { RequestSchedulersRoutes } from './request-schedulers'
 import { UsersRoutes } from './users'
 import { VideoAbusesRoutes } from './video-abuses'
-import { AdminGuard } from './admin-guard.service'
 import { VideoBlacklistRoutes } from './video-blacklist'
 
 const adminRoutes: Routes = [
   {
     path: '',
     component: AdminComponent,
-    canActivate: [ MetaGuard, AdminGuard ],
-    canActivateChild: [ MetaGuard, AdminGuard ],
+    canActivate: [ MetaGuard ],
+    canActivateChild: [ MetaGuard ],
     children: [
       {
         path: '',
index f29c501b030854e9f0bb43a34ad636e90a618a8f..6c216e5d8fe73a6b7e64ea15bde0f9298aef2cc2 100644 (file)
@@ -8,7 +8,6 @@ import { UsersComponent, UserAddComponent, UserUpdateComponent, UserListComponen
 import { VideoAbusesComponent, VideoAbuseListComponent } from './video-abuses'
 import { VideoBlacklistComponent, VideoBlacklistListComponent } from './video-blacklist'
 import { SharedModule } from '../shared'
-import { AdminGuard } from './admin-guard.service'
 
 @NgModule({
   imports: [
@@ -45,8 +44,7 @@ import { AdminGuard } from './admin-guard.service'
   providers: [
     FriendService,
     RequestSchedulersService,
-    UserService,
-    AdminGuard
+    UserService
   ]
 })
 export class AdminModule { }
index 615b6f4f7ce708cab5e2837aa9b77d04c3488c2a..61cfcae19a65273145666852c730898107ee7801 100644 (file)
@@ -1,13 +1,19 @@
 import { Routes } from '@angular/router'
 
+import { UserRightGuard } from '../../core'
 import { FriendsComponent } from './friends.component'
 import { FriendAddComponent } from './friend-add'
 import { FriendListComponent } from './friend-list'
+import { UserRight } from '../../../../../shared'
 
 export const FriendsRoutes: Routes = [
   {
     path: 'friends',
     component: FriendsComponent,
+    canActivate: [ UserRightGuard ],
+    data: {
+      userRight: UserRight.MANAGE_PODS
+    },
     children: [
       {
         path: '',
index 4961c646bb44a33130b5de156334557755ccd594..c2564de15864d8a10167a4bb3c59cac35912bc90 100644 (file)
@@ -1,5 +1,7 @@
 import { Routes } from '@angular/router'
 
+import { UserRightGuard } from '../../core'
+import { UserRight } from '../../../../../shared'
 import { RequestSchedulersComponent } from './request-schedulers.component'
 import { RequestSchedulersStatsComponent } from './request-schedulers-stats'
 
@@ -7,6 +9,10 @@ export const RequestSchedulersRoutes: Routes = [
   {
     path: 'requests',
     component: RequestSchedulersComponent,
+    canActivate: [ UserRightGuard ],
+    data: {
+      userRight: UserRight.MANAGE_REQUEST_SCHEDULERS
+    },
     children: [
       {
         path: '',
index 6d8151b42b1d9a6adeb5617408d58d4093d6a0c4..8e3e3d53d006a5e6bbfbb9c6e7f2f89e47727e3a 100644 (file)
@@ -9,10 +9,11 @@ import {
   USER_USERNAME,
   USER_EMAIL,
   USER_PASSWORD,
-  USER_VIDEO_QUOTA
+  USER_VIDEO_QUOTA,
+  USER_ROLE
 } from '../../../shared'
 import { ServerService } from '../../../core'
-import { UserCreate } from '../../../../../../shared'
+import { UserCreate, UserRole } from '../../../../../../shared'
 import { UserEdit } from './user-edit'
 
 @Component({
@@ -28,12 +29,14 @@ export class UserAddComponent extends UserEdit implements OnInit {
     'username': '',
     'email': '',
     'password': '',
+    'role': '',
     'videoQuota': ''
   }
   validationMessages = {
     'username': USER_USERNAME.MESSAGES,
     'email': USER_EMAIL.MESSAGES,
     'password': USER_PASSWORD.MESSAGES,
+    'role': USER_ROLE.MESSAGES,
     'videoQuota': USER_VIDEO_QUOTA.MESSAGES
   }
 
@@ -52,6 +55,7 @@ export class UserAddComponent extends UserEdit implements OnInit {
       username: [ '', USER_USERNAME.VALIDATORS ],
       email:    [ '', USER_EMAIL.VALIDATORS ],
       password: [ '', USER_PASSWORD.VALIDATORS ],
+      role: [ UserRole.USER, USER_ROLE.VALIDATORS ],
       videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ]
     })
 
index 6988071ced09354525bf3a24fcdd029f7bf06e48..349be13c1948c53ecc7ecea5215b1cd59b0b367a 100644 (file)
         </div>
       </div>
 
+      <div class="form-group">
+        <label for="role">Role</label>
+        <select class="form-control" id="role" formControlName="role">
+          <option *ngFor="let role of roles" [value]="role.value">
+            {{ role.label }}
+          </option>
+        </select>
+
+        <div *ngIf="formErrors.role" class="alert alert-danger">
+          {{ formErrors.role }}
+        </div>
+      </div>
+
       <div class="form-group">
         <label for="videoQuota">Video quota</label>
         <select class="form-control" id="videoQuota" formControlName="videoQuota">
index 76497c9b652894a36e74cfcadd579f27b9febe8b..51d90da399039b16b0376d0761af880d361d7815 100644 (file)
@@ -1,6 +1,6 @@
 import { ServerService } from '../../../core'
 import { FormReactive } from '../../../shared'
-import { VideoResolution } from '../../../../../../shared/models/videos/video-resolution.enum'
+import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
 
 export abstract class UserEdit extends FormReactive {
   videoQuotaOptions = [
@@ -14,6 +14,8 @@ export abstract class UserEdit extends FormReactive {
     { value: 50 * 1024 * 1024 * 1024, label: '50GB' }
   ]
 
+  roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key, label: USER_ROLE_LABELS[key] }))
+
   protected abstract serverService: ServerService
   abstract isCreation (): boolean
   abstract getFormButtonTitle (): string
index bd901e655ae2a8d17c4d7e6bca8b8d2b082f8de0..bcba78a35272b0bfecbe6610309386f160fbc172 100644 (file)
@@ -6,11 +6,15 @@ import { Subscription } from 'rxjs/Subscription'
 import { NotificationsService } from 'angular2-notifications'
 
 import { UserService } from '../shared'
-import { USER_EMAIL, USER_VIDEO_QUOTA } from '../../../shared'
+import {
+  USER_EMAIL,
+  USER_VIDEO_QUOTA,
+  USER_ROLE,
+  User
+} from '../../../shared'
 import { ServerService } from '../../../core'
-import { UserUpdate } from '../../../../../../shared/models/users/user-update.model'
-import { User } from '../../../shared/users/user.model'
 import { UserEdit } from './user-edit'
+import { UserUpdate, UserRole } from '../../../../../../shared'
 
 @Component({
   selector: 'my-user-update',
@@ -25,10 +29,12 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
   form: FormGroup
   formErrors = {
     'email': '',
+    'role': '',
     'videoQuota': ''
   }
   validationMessages = {
     'email': USER_EMAIL.MESSAGES,
+    'role': USER_ROLE.MESSAGES,
     'videoQuota': USER_VIDEO_QUOTA.MESSAGES
   }
 
@@ -48,6 +54,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
   buildForm () {
     this.form = this.formBuilder.group({
       email:    [ '', USER_EMAIL.VALIDATORS ],
+      role: [ '', USER_ROLE.VALIDATORS ],
       videoQuota: [ '-1', USER_VIDEO_QUOTA.VALIDATORS ]
     })
 
@@ -103,6 +110,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
 
     this.form.patchValue({
       email: userJson.email,
+      role: userJson.role,
       videoQuota: userJson.videoQuota
     })
   }
index 2944e3cbfe6043b02f1bc2a59509b7654f9c88d5..16a8a803361ab3c15729d2685d2bea200265387b 100644 (file)
@@ -11,7 +11,7 @@
       <p-column field="username" header="Username" [sortable]="true"></p-column>
       <p-column field="email" header="Email"></p-column>
       <p-column field="videoQuota" header="Video quota"></p-column>
-      <p-column field="role" header="Role"></p-column>
+      <p-column field="roleLabel" header="Role"></p-column>
       <p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
       <p-column header="Edit" styleClass="action-cell">
         <ng-template pTemplate="body" let-user="rowData">
index a6a9c4c193ede6fd7579da49a8c6d3b1dd8af3cd..3718dfd5cd178817ba259dda35121ead96d7e29a 100644 (file)
@@ -1,5 +1,7 @@
 import { Routes } from '@angular/router'
 
+import { UserRightGuard } from '../../core'
+import { UserRight } from '../../../../../shared'
 import { UsersComponent } from './users.component'
 import { UserAddComponent, UserUpdateComponent } from './user-edit'
 import { UserListComponent } from './user-list'
@@ -8,6 +10,10 @@ export const UsersRoutes: Routes = [
   {
     path: 'users',
     component: UsersComponent,
+    canActivate: [ UserRightGuard ],
+    data: {
+      userRight: UserRight.MANAGE_USERS
+    },
     children: [
       {
         path: '',
index a8c1561cd16fe9884a9ddadb4ababdd611fb5ee1..68b75605946c66b3dd76133d435ecee7b372f867 100644 (file)
@@ -1,13 +1,18 @@
 import { Routes } from '@angular/router'
 
+import { UserRightGuard } from '../../core'
+import { UserRight } from '../../../../../shared'
 import { VideoAbusesComponent } from './video-abuses.component'
 import { VideoAbuseListComponent } from './video-abuse-list'
 
 export const VideoAbusesRoutes: Routes = [
   {
     path: 'video-abuses',
-    component: VideoAbusesComponent
-    ,
+    component: VideoAbusesComponent,
+    canActivate: [ UserRightGuard ],
+    data: {
+      userRight: UserRight.MANAGE_VIDEO_ABUSES
+    },
     children: [
       {
         path: '',
index 682b6f8bd843fda1ac99fe62a783d4c61799656b..b1e0e5049be75225b9e62bd7e4f903b88d00974b 100644 (file)
@@ -1,5 +1,7 @@
 import { Routes } from '@angular/router'
 
+import { UserRightGuard } from '../../core'
+import { UserRight } from '../../../../../shared'
 import { VideoBlacklistComponent } from './video-blacklist.component'
 import { VideoBlacklistListComponent } from './video-blacklist-list'
 
@@ -7,6 +9,10 @@ export const VideoBlacklistRoutes: Routes = [
   {
     path: 'video-blacklist',
     component: VideoBlacklistComponent,
+    canActivate: [ UserRightGuard ],
+    data: {
+      userRight: UserRight.MANAGE_VIDEO_BLACKLIST
+    },
     children: [
       {
         path: '',
index 81bff99a040bdee257799ac007d09797352b6a47..085b763ec733f62177bd7e86c50b3f60e615df5e 100644 (file)
@@ -1,6 +1,7 @@
 // Do not use the barrel (dependency loop)
-import { UserRole } from '../../../../../shared/models/users/user-role.type'
+import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role'
 import { User, UserConstructorHash } from '../../shared/users/user.model'
+import { UserRight } from '../../../../../shared/models/users/user-right.enum'
 
 export type TokenOptions = {
   accessToken: string
@@ -81,7 +82,7 @@ export class AuthUser extends User {
           id: parseInt(localStorage.getItem(this.KEYS.ID), 10),
           username: localStorage.getItem(this.KEYS.USERNAME),
           email: localStorage.getItem(this.KEYS.EMAIL),
-          role: localStorage.getItem(this.KEYS.ROLE) as UserRole,
+          role: parseInt(localStorage.getItem(this.KEYS.ROLE), 10) as UserRole,
           displayNSFW: localStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true'
         },
         Tokens.load()
@@ -122,11 +123,15 @@ export class AuthUser extends User {
     this.tokens.refreshToken = refreshToken
   }
 
+  hasRight(right: UserRight) {
+    return hasUserRight(this.role, right)
+  }
+
   save () {
     localStorage.setItem(AuthUser.KEYS.ID, this.id.toString())
     localStorage.setItem(AuthUser.KEYS.USERNAME, this.username)
     localStorage.setItem(AuthUser.KEYS.EMAIL, this.email)
-    localStorage.setItem(AuthUser.KEYS.ROLE, this.role)
+    localStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString())
     localStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW))
     this.tokens.save()
   }
index 9ac9ba7bb1fcd7a20b2f168af4253d779db998d4..df6e5135b9aff824ecd35f626739b37d25934526 100644 (file)
@@ -21,7 +21,7 @@ import {
 // Do not use the barrel (dependency loop)
 import { RestExtractor } from '../../shared/rest'
 import { UserLogin } from '../../../../../shared/models/users/user-login.model'
-import { User, UserConstructorHash } from '../../shared/users/user.model'
+import { UserConstructorHash } from '../../shared/users/user.model'
 
 interface UserLoginWithUsername extends UserLogin {
   access_token: string
@@ -126,12 +126,6 @@ export class AuthService {
     return this.user
   }
 
-  isAdmin () {
-    if (this.user === null) return false
-
-    return this.user.isAdmin()
-  }
-
   isLoggedIn () {
     return !!this.getAccessToken()
   }
index a81f2c0021b6836c28937cd64249eeeb904dbe3d..bc7bfec0eed3c5b1f07fa982836f2c3e414844fb 100644 (file)
@@ -1,4 +1,4 @@
 export * from './auth-status.model'
 export * from './auth-user.model'
 export * from './auth.service'
-export * from './login-guard.service'
+export * from '../routing/login-guard.service'
diff --git a/client/src/app/core/auth/login-guard.service.ts b/client/src/app/core/auth/login-guard.service.ts
deleted file mode 100644 (file)
index c09e8fe..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Injectable } from '@angular/core'
-import {
-  ActivatedRouteSnapshot,
-  CanActivateChild,
-  RouterStateSnapshot,
-  CanActivate,
-  Router
-} from '@angular/router'
-
-import { AuthService } from './auth.service'
-
-@Injectable()
-export class LoginGuard implements CanActivate, CanActivateChild {
-
-  constructor (
-    private router: Router,
-    private auth: AuthService
-  ) {}
-
-  canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
-    if (this.auth.isLoggedIn() === true) return true
-
-    this.router.navigate([ '/login' ])
-    return false
-  }
-
-  canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
-    return this.canActivate(route, state)
-  }
-}
index 163a6bbde9e5299084733057bc7d5103ab8364af..90e2cb1902945464d4fb0c7e67703634adc22d5a 100644 (file)
@@ -7,7 +7,8 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
 import { SimpleNotificationsModule } from 'angular2-notifications'
 import { ModalModule } from 'ngx-bootstrap/modal'
 
-import { AuthService, LoginGuard } from './auth'
+import { AuthService } from './auth'
+import { LoginGuard, UserRightGuard } from './routing'
 import { ServerService } from './server'
 import { ConfirmComponent, ConfirmService } from './confirm'
 import { MenuComponent, MenuAdminComponent } from './menu'
@@ -42,7 +43,8 @@ import { throwIfAlreadyLoaded } from './module-import-guard'
     AuthService,
     ConfirmService,
     ServerService,
-    LoginGuard
+    LoginGuard,
+    UserRightGuard
   ]
 })
 export class CoreModule {
index edacdee6d1684d5a6b231029021f2147e8849c8b..c2b2958b4048eb0a6153dfd7522d18c8c0f6fb30 100644 (file)
@@ -1,26 +1,26 @@
 <menu>
   <div class="panel-block">
-    <a routerLink="/admin/users/list" routerLinkActive="active">
+    <a *ngIf="hasUsersRight()" routerLink="/admin/users" routerLinkActive="active">
       <span class="hidden-xs glyphicon glyphicon-user"></span>
       List users
     </a>
 
-    <a routerLink="/admin/friends/list" routerLinkActive="active">
+    <a *ngIf="hasFriendsRight()" routerLink="/admin/friends" routerLinkActive="active">
       <span class="hidden-xs glyphicon glyphicon-cloud"></span>
       List friends
     </a>
 
-    <a routerLink="/admin/requests/stats" routerLinkActive="active">
+    <a *ngIf="hasRequestsStatRight()" routerLink="/admin/requests/stats" routerLinkActive="active">
       <span class="hidden-xs glyphicon glyphicon-stats"></span>
       Request stats
     </a>
 
-    <a routerLink="/admin/video-abuses/list" routerLinkActive="active">
+    <a *ngIf="hasVideoAbusesRight()" routerLink="/admin/video-abuses" routerLinkActive="active">
       <span class="hidden-xs glyphicon glyphicon-alert"></span>
       Video abuses
     </a>
 
-    <a routerLink="/admin/video-blacklist/list" routerLinkActive="active">
+    <a *ngIf="hasVideoBlacklistRight()" routerLink="/admin/video-blacklist" routerLinkActive="active">
       <span class="hidden-xs glyphicon glyphicon-eye-close"></span>
       Video blacklist
     </a>
index f6cc6554cb79c697edfb2696d56c35f5b0956715..074f1dbaf26c0a5b777555c1e1ba1f7f6f67e775 100644 (file)
@@ -1,8 +1,33 @@
 import { Component } from '@angular/core'
 
+import { AuthService } from '../auth/auth.service'
+import { UserRight } from '../../../../../shared'
+
 @Component({
   selector: 'my-menu-admin',
   templateUrl: './menu-admin.component.html',
   styleUrls: [ './menu.component.scss' ]
 })
-export class MenuAdminComponent { }
+export class MenuAdminComponent {
+  constructor (private auth: AuthService) {}
+
+  hasUsersRight () {
+    return this.auth.getUser().hasRight(UserRight.MANAGE_USERS)
+  }
+
+  hasFriendsRight () {
+    return this.auth.getUser().hasRight(UserRight.MANAGE_PODS)
+  }
+
+  hasRequestsStatRight () {
+    return this.auth.getUser().hasRight(UserRight.MANAGE_REQUEST_SCHEDULERS)
+  }
+
+  hasVideoAbusesRight () {
+    return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_ABUSES)
+  }
+
+  hasVideoBlacklistRight () {
+    return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
+  }
+}
index ca341a0fd193bcb6075f0b04159719d334236de5..2d8aace54c4f8552c771760c23af05c3d00f4666 100644 (file)
     </a>
   </div>
 
-  <div *ngIf="isUserAdmin()" class="panel-block">
+  <div *ngIf="userHasAdminAccess" class="panel-block">
     <div class="block-title">Other</div>
 
-    <a routerLink="/admin" routerLinkActive="active">
+    <a [routerLink]="getFirstAdminRouteAvailable()" routerLinkActive="active">
       <span class="hidden-xs glyphicon glyphicon-cog"></span>
       Administration
     </a>
index 8f15d88381ac5df578e10dd4503868e60a54ed2f..c66a5eccc9f93f4624ca10bd47897206602cff1f 100644 (file)
@@ -3,6 +3,7 @@ import { Router } from '@angular/router'
 
 import { AuthService, AuthStatus } from '../auth'
 import { ServerService } from '../server'
+import { UserRight } from '../../../../../shared/models/users/user-right.enum'
 
 @Component({
   selector: 'my-menu',
@@ -11,6 +12,15 @@ import { ServerService } from '../server'
 })
 export class MenuComponent implements OnInit {
   isLoggedIn: boolean
+  userHasAdminAccess = false
+
+  private routesPerRight = {
+    [UserRight.MANAGE_USERS]: '/admin/users',
+    [UserRight.MANAGE_PODS]: '/admin/friends',
+    [UserRight.MANAGE_REQUEST_SCHEDULERS]: '/admin/requests/stats',
+    [UserRight.MANAGE_VIDEO_ABUSES]: '/admin/video-abuses',
+    [UserRight.MANAGE_VIDEO_BLACKLIST]: '/admin/video-blacklist'
+  }
 
   constructor (
     private authService: AuthService,
@@ -20,14 +30,17 @@ export class MenuComponent implements OnInit {
 
   ngOnInit () {
     this.isLoggedIn = this.authService.isLoggedIn()
+    this.computeIsUserHasAdminAccess()
 
     this.authService.loginChangedSource.subscribe(
       status => {
         if (status === AuthStatus.LoggedIn) {
           this.isLoggedIn = true
+          this.computeIsUserHasAdminAccess()
           console.log('Logged in.')
         } else if (status === AuthStatus.LoggedOut) {
           this.isLoggedIn = false
+          this.computeIsUserHasAdminAccess()
           console.log('Logged out.')
         } else {
           console.error('Unknown auth status: ' + status)
@@ -40,8 +53,31 @@ export class MenuComponent implements OnInit {
     return this.serverService.getConfig().signup.allowed
   }
 
-  isUserAdmin () {
-    return this.authService.isAdmin()
+  getFirstAdminRightAvailable () {
+    const user = this.authService.getUser()
+    if (!user) return undefined
+
+    const adminRights = [
+      UserRight.MANAGE_USERS,
+      UserRight.MANAGE_PODS,
+      UserRight.MANAGE_REQUEST_SCHEDULERS,
+      UserRight.MANAGE_VIDEO_ABUSES,
+      UserRight.MANAGE_VIDEO_BLACKLIST
+    ]
+
+    for (const adminRight of adminRights) {
+      if (user.hasRight(adminRight)) {
+        return adminRight
+      }
+    }
+
+    return undefined
+  }
+
+  getFirstAdminRouteAvailable () {
+    const right = this.getFirstAdminRightAvailable()
+
+    return this.routesPerRight[right]
   }
 
   logout () {
@@ -49,4 +85,10 @@ export class MenuComponent implements OnInit {
     // Redirect to home page
     this.router.navigate(['/videos/list'])
   }
+
+  private computeIsUserHasAdminAccess () {
+    const right = this.getFirstAdminRightAvailable()
+
+    this.userHasAdminAccess = right !== undefined
+  }
 }
index 17f3ee833fe190e2e9f4390ee68cb574265aa71c..d1b9828344662074d81c2659579da767f36a6dea 100644 (file)
@@ -1 +1,3 @@
+export * from './login-guard.service'
+export * from './user-right-guard.service'
 export * from './preload-selected-modules-list'
diff --git a/client/src/app/core/routing/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts
new file mode 100644 (file)
index 0000000..18bc41c
--- /dev/null
@@ -0,0 +1,30 @@
+import { Injectable } from '@angular/core'
+import {
+  ActivatedRouteSnapshot,
+  CanActivateChild,
+  RouterStateSnapshot,
+  CanActivate,
+  Router
+} from '@angular/router'
+
+import { AuthService } from '../auth/auth.service'
+
+@Injectable()
+export class LoginGuard implements CanActivate, CanActivateChild {
+
+  constructor (
+    private router: Router,
+    private auth: AuthService
+  ) {}
+
+  canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    if (this.auth.isLoggedIn() === true) return true
+
+    this.router.navigate([ '/login' ])
+    return false
+  }
+
+  canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    return this.canActivate(route, state)
+  }
+}
diff --git a/client/src/app/core/routing/user-right-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts
new file mode 100644 (file)
index 0000000..65d0299
--- /dev/null
@@ -0,0 +1,35 @@
+import { Injectable } from '@angular/core'
+import {
+  ActivatedRouteSnapshot,
+  CanActivateChild,
+  RouterStateSnapshot,
+  CanActivate,
+  Router
+} from '@angular/router'
+
+import { AuthService } from '../auth'
+
+@Injectable()
+export class UserRightGuard implements CanActivate, CanActivateChild {
+
+  constructor (
+    private router: Router,
+    private auth: AuthService
+  ) {}
+
+  canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    const user = this.auth.getUser()
+    if (user) {
+      const neededUserRight = route.data.userRight
+
+      if (user.hasRight(neededUserRight)) return true
+    }
+
+    this.router.navigate([ '/login' ])
+    return false
+  }
+
+  canActivateChild (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
+    return this.canActivate(route, state)
+  }
+}
index d4c4c1d33a905f0d8e07d90e49d64836f4aeb5e9..e7473b75b67b6ac8e79aa4c510b0b94347e253a9 100644 (file)
@@ -29,3 +29,9 @@ export const USER_VIDEO_QUOTA = {
     'min': 'Quota must be greater than -1.'
   }
 }
+export const USER_ROLE = {
+  VALIDATORS: [ Validators.required ],
+  MESSAGES: {
+    'required': 'User role is required.',
+  }
+}
index 7beea5910958aa9638715bdb465350420e830413..d738899ab924c7efdd1d12e8e2c5fc24e90f5c44 100644 (file)
@@ -1,7 +1,9 @@
 import {
   User as UserServerModel,
   UserRole,
-  VideoChannel
+  VideoChannel,
+  UserRight,
+  hasUserRight
 } from '../../../../../shared'
 
 export type UserConstructorHash = {
@@ -56,7 +58,7 @@ export class User implements UserServerModel {
     }
   }
 
-  isAdmin () {
-    return this.role === 'admin'
+  hasRight (right: UserRight) {
+    return hasUserRight(this.role, right)
   }
 }
index e99a5ce2e53deb3b82097f612a347718b1ebc8cc..3a6ecc480449ea0c4e585ba8f6b7a587c16c96f5 100644 (file)
@@ -1,9 +1,11 @@
 import { Video } from './video.model'
+import { AuthUser } from '../../core'
 import {
   VideoDetails as VideoDetailsServerModel,
   VideoFile,
   VideoChannel,
-  VideoResolution
+  VideoResolution,
+  UserRight
 } from '../../../../../shared'
 
 export class VideoDetails extends Video implements VideoDetailsServerModel {
@@ -61,15 +63,15 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
     return betterResolutionFile.magnetUri
   }
 
-  isRemovableBy (user) {
-    return user && this.isLocal === true && (this.author === user.username || user.isAdmin() === true)
+  isRemovableBy (user: AuthUser) {
+    return user && this.isLocal === true && (this.author === user.username || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
   }
 
-  isBlackistableBy (user) {
-    return user && user.isAdmin() === true && this.isLocal === false
+  isBlackistableBy (user: AuthUser) {
+    return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true && this.isLocal === false
   }
 
-  isUpdatableBy (user) {
+  isUpdatableBy (user: AuthUser) {
     return user && this.isLocal === true && user.username === this.author
   }
 }
index 35a7b6521374f8c8111b415ca108e55a39c4bbeb..bf6f60215e14b27a6842680c21dc82047f64f135 100644 (file)
@@ -12,7 +12,7 @@ import {
   VideoService,
   VideoPagination
 } from '../shared'
-import { Search, SearchField, SearchService, User} from '../../shared'
+import { Search, SearchField, SearchService, User } from '../../shared'
 
 @Component({
   selector: 'my-videos-list',
index bf1b744e55a39971492d152656bcfe884cdac412..b44cd6b8301d7945fed201704924b656c4433186 100644 (file)
@@ -9,7 +9,7 @@ import {
 } from '../../lib'
 import {
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight,
   makeFriendsValidator,
   setBodyHostsPort,
   podRemoveValidator,
@@ -20,6 +20,7 @@ import {
   asyncMiddleware
 } from '../../middlewares'
 import { PodInstance } from '../../models'
+import { UserRight } from '../../../shared'
 
 const podsRouter = express.Router()
 
@@ -32,19 +33,19 @@ podsRouter.get('/',
 )
 podsRouter.post('/make-friends',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_PODS),
   makeFriendsValidator,
   setBodyHostsPort,
   asyncMiddleware(makeFriendsController)
 )
 podsRouter.get('/quit-friends',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_PODS),
   asyncMiddleware(quitFriendsController)
 )
 podsRouter.delete('/:id',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_PODS),
   podRemoveValidator,
   asyncMiddleware(removeFriendController)
 )
index 28f46f3ee6a007a9981de9e63d7b769e25577d10..4c8fbe18ba228eee8eef7d2c44affb27e9b72ffd 100644 (file)
@@ -7,14 +7,14 @@ import {
   getRequestVideoQaduScheduler,
   getRequestVideoEventScheduler
 } from '../../lib'
-import { authenticate, ensureIsAdmin, asyncMiddleware } from '../../middlewares'
-import { RequestSchedulerStatsAttributes } from '../../../shared'
+import { authenticate, ensureUserHasRight, asyncMiddleware } from '../../middlewares'
+import { RequestSchedulerStatsAttributes, UserRight } from '../../../shared'
 
 const requestSchedulerRouter = express.Router()
 
 requestSchedulerRouter.get('/stats',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_REQUEST_SCHEDULERS),
   asyncMiddleware(getRequestSchedulersStats)
 )
 
index 18a094f03bdd01fd5a1fef55b4f7c399f583ed16..fdc9b0c879ef43653a4b5550e59032d9151a6d2f 100644 (file)
@@ -1,11 +1,10 @@
 import * as express from 'express'
 
-import { database as db } from '../../initializers/database'
-import { USER_ROLES, CONFIG } from '../../initializers'
+import { database as db, CONFIG } from '../../initializers'
 import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers'
 import {
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight,
   ensureUserRegistrationAllowed,
   usersAddValidator,
   usersRegisterValidator,
@@ -25,7 +24,9 @@ import {
   UserVideoRate as FormattedUserVideoRate,
   UserCreate,
   UserUpdate,
-  UserUpdateMe
+  UserUpdateMe,
+  UserRole,
+  UserRight
 } from '../../../shared'
 import { createUserAuthorAndChannel } from '../../lib'
 import { UserInstance } from '../../models'
@@ -58,7 +59,7 @@ usersRouter.get('/:id',
 
 usersRouter.post('/',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_USERS),
   usersAddValidator,
   createUserRetryWrapper
 )
@@ -77,14 +78,14 @@ usersRouter.put('/me',
 
 usersRouter.put('/:id',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_USERS),
   usersUpdateValidator,
   asyncMiddleware(updateUser)
 )
 
 usersRouter.delete('/:id',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_USERS),
   usersRemoveValidator,
   asyncMiddleware(removeUser)
 )
@@ -119,7 +120,7 @@ async function createUser (req: express.Request, res: express.Response, next: ex
     password: body.password,
     email: body.email,
     displayNSFW: false,
-    role: USER_ROLES.USER,
+    role: body.role,
     videoQuota: body.videoQuota
   })
 
@@ -136,7 +137,7 @@ async function registerUser (req: express.Request, res: express.Response, next:
     password: body.password,
     email: body.email,
     displayNSFW: false,
-    role: USER_ROLES.USER,
+    role: UserRole.USER,
     videoQuota: CONFIG.USER.VIDEO_QUOTA
   })
 
@@ -203,6 +204,7 @@ async function updateUser (req: express.Request, res: express.Response, next: ex
 
   if (body.email !== undefined) user.email = body.email
   if (body.videoQuota !== undefined) user.videoQuota = body.videoQuota
+  if (body.role !== undefined) user.role = body.role
 
   await user.save()
 
index 4c7abf3952109ac3d1c8e13d4c7652c2124b6dab..04349042b02b1cb6535b3a354fe37c1ae022186d 100644 (file)
@@ -9,7 +9,7 @@ import {
 } from '../../../helpers'
 import {
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight,
   paginationValidator,
   videoAbuseReportValidator,
   videoAbusesSortValidator,
@@ -18,13 +18,13 @@ import {
   asyncMiddleware
 } from '../../../middlewares'
 import { VideoInstance } from '../../../models'
-import { VideoAbuseCreate } from '../../../../shared'
+import { VideoAbuseCreate, UserRight } from '../../../../shared'
 
 const abuseVideoRouter = express.Router()
 
 abuseVideoRouter.get('/abuse',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_VIDEO_ABUSES),
   paginationValidator,
   videoAbusesSortValidator,
   setVideoAbusesSort,
index 5a2c3fd8005478e8c1b4b71ab6f11816998df898..be7cf6ea4ada4602c57b7c8d50b6c64bdfb4eefe 100644 (file)
@@ -4,7 +4,7 @@ import { database as db } from '../../../initializers'
 import { logger, getFormattedObjects } from '../../../helpers'
 import {
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight,
   videosBlacklistAddValidator,
   videosBlacklistRemoveValidator,
   paginationValidator,
@@ -14,20 +14,20 @@ import {
   asyncMiddleware
 } from '../../../middlewares'
 import { BlacklistedVideoInstance } from '../../../models'
-import { BlacklistedVideo } from '../../../../shared'
+import { BlacklistedVideo, UserRight } from '../../../../shared'
 
 const blacklistRouter = express.Router()
 
 blacklistRouter.post('/:videoId/blacklist',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
   videosBlacklistAddValidator,
   asyncMiddleware(addVideoToBlacklist)
 )
 
 blacklistRouter.get('/blacklist',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
   paginationValidator,
   blacklistSortValidator,
   setBlacklistSort,
@@ -37,7 +37,7 @@ blacklistRouter.get('/blacklist',
 
 blacklistRouter.delete('/:videoId/blacklist',
   authenticate,
-  ensureIsAdmin,
+  ensureUserHasRight(UserRight.MANAGE_VIDEO_BLACKLIST),
   videosBlacklistRemoveValidator,
   asyncMiddleware(removeVideoFromBlacklistController)
 )
index c180eccda34db4468d7676ea43acf701eb84b170..f423d63177a0bfb58efb5196c46b21b873826b80 100644 (file)
@@ -1,9 +1,8 @@
-import { values } from 'lodash'
 import * as validator from 'validator'
 import 'express-validator'
 
 import { exists } from './misc'
-import { CONSTRAINTS_FIELDS, USER_ROLES } from '../../initializers'
+import { CONSTRAINTS_FIELDS } from '../../initializers'
 import { UserRole } from '../../../shared'
 
 const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@@ -12,10 +11,6 @@ function isUserPasswordValid (value: string) {
   return validator.isLength(value, USERS_CONSTRAINTS_FIELDS.PASSWORD)
 }
 
-function isUserRoleValid (value: string) {
-  return values(USER_ROLES).indexOf(value as UserRole) !== -1
-}
-
 function isUserVideoQuotaValid (value: string) {
   return exists(value) && validator.isInt(value + '', USERS_CONSTRAINTS_FIELDS.VIDEO_QUOTA)
 }
@@ -30,6 +25,10 @@ function isUserDisplayNSFWValid (value: any) {
   return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
 }
 
+function isUserRoleValid (value: any) {
+  return exists(value) && validator.isInt('' + value) && UserRole[value] !== undefined
+}
+
 // ---------------------------------------------------------------------------
 
 export {
index 1581a31951d3a7a6028291a2b90f0fb8ee63f0f1..6dc9737d24d359cdd7f91e98d87c9b6dff05590d 100644 (file)
@@ -5,7 +5,6 @@ import { join } from 'path'
 import { root, isTestInstance } from '../helpers/core-utils'
 
 import {
-  UserRole,
   VideoRateType,
   RequestEndpoint,
   RequestVideoEventType,
@@ -16,7 +15,7 @@ import {
 
 // ---------------------------------------------------------------------------
 
-const LAST_MIGRATION_VERSION = 80
+const LAST_MIGRATION_VERSION = 85
 
 // ---------------------------------------------------------------------------
 
@@ -283,7 +282,6 @@ const JOB_STATES: { [ id: string ]: JobState } = {
 }
 // How many maximum jobs we fetch from the database per cycle
 const JOBS_FETCH_LIMIT_PER_CYCLE = 10
-const JOBS_CONCURRENCY = 1
 // 1 minutes
 let JOBS_FETCHING_INTERVAL = 60000
 
@@ -334,13 +332,6 @@ const CACHE = {
 
 // ---------------------------------------------------------------------------
 
-const USER_ROLES: { [ id: string ]: UserRole } = {
-  ADMIN: 'admin',
-  USER: 'user'
-}
-
-// ---------------------------------------------------------------------------
-
 const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->'
 
 // ---------------------------------------------------------------------------
@@ -367,7 +358,6 @@ export {
   EMBED_SIZE,
   FRIEND_SCORE,
   JOB_STATES,
-  JOBS_CONCURRENCY,
   JOBS_FETCH_LIMIT_PER_CYCLE,
   JOBS_FETCHING_INTERVAL,
   LAST_MIGRATION_VERSION,
@@ -401,7 +391,6 @@ export {
   STATIC_MAX_AGE,
   STATIC_PATHS,
   THUMBNAILS_SIZE,
-  USER_ROLES,
   VIDEO_CATEGORIES,
   VIDEO_LANGUAGES,
   VIDEO_LICENCES,
index 4c04290fc455542728504eae71322883b8d529a9..07747234187ed65efe667a2268b790494bc5e4aa 100644 (file)
@@ -2,10 +2,11 @@ import * as passwordGenerator from 'password-generator'
 import * as Bluebird from 'bluebird'
 
 import { database as db } from './database'
-import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants'
+import { CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants'
 import { clientsExist, usersExist } from './checker'
 import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers'
 import { createUserAuthorAndChannel } from '../lib'
+import { UserRole } from '../../shared'
 
 async function installApplication () {
   await db.sequelize.sync()
@@ -88,7 +89,7 @@ async function createOAuthAdminIfNotExist () {
   logger.info('Creating the administrator.')
 
   const username = 'root'
-  const role = USER_ROLES.ADMIN
+  const role = UserRole.ADMINISTRATOR
   const email = CONFIG.ADMIN.EMAIL
   let validatePassword = true
   let password = ''
diff --git a/server/initializers/migrations/0085-user-role.ts b/server/initializers/migrations/0085-user-role.ts
new file mode 100644 (file)
index 0000000..e67c5ca
--- /dev/null
@@ -0,0 +1,39 @@
+import * as Sequelize from 'sequelize'
+import * as uuidv4 from 'uuid/v4'
+
+async function up (utils: {
+  transaction: Sequelize.Transaction,
+  queryInterface: Sequelize.QueryInterface,
+  sequelize: Sequelize.Sequelize,
+  db: any
+}): Promise<void> {
+  const q = utils.queryInterface
+
+  await q.renameColumn('Users', 'role', 'oldRole')
+
+  const data = {
+    type: Sequelize.INTEGER,
+    allowNull: true
+  }
+  await q.addColumn('Users', 'role', data)
+
+  let query = 'UPDATE "Users" SET "role" = 0 WHERE "oldRole" = \'admin\''
+  await utils.sequelize.query(query)
+
+  query = 'UPDATE "Users" SET "role" = 2 WHERE "oldRole" = \'user\''
+  await utils.sequelize.query(query)
+
+  data.allowNull = false
+  await q.changeColumn('Users', 'role', data)
+
+  await q.removeColumn('Users', 'oldRole')
+}
+
+function down (options) {
+  throw new Error('Not implemented.')
+}
+
+export {
+  up,
+  down
+}
diff --git a/server/middlewares/admin.ts b/server/middlewares/admin.ts
deleted file mode 100644 (file)
index 8123973..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-import 'express-validator'
-import * as express from 'express'
-
-import { logger } from '../helpers'
-
-function ensureIsAdmin (req: express.Request, res: express.Response, next: express.NextFunction) {
-  const user = res.locals.oauth.token.user
-  if (user.isAdmin() === false) {
-    logger.info('A non admin user is trying to access to an admin content.')
-    return res.sendStatus(403)
-  }
-
-  return next()
-}
-
-// ---------------------------------------------------------------------------
-
-export {
-  ensureIsAdmin
-}
index 0e2c850e18def51210430193dadaf70b3cc8e9fd..cec3e0b2ac91dcd492a866b01c5d76c2cf1aae7e 100644 (file)
@@ -1,5 +1,4 @@
 export * from './validators'
-export * from './admin'
 export * from './async'
 export * from './oauth'
 export * from './pagination'
@@ -7,3 +6,4 @@ export * from './pods'
 export * from './search'
 export * from './secure'
 export * from './sort'
+export * from './user-right'
diff --git a/server/middlewares/user-right.ts b/server/middlewares/user-right.ts
new file mode 100644 (file)
index 0000000..bcebe9d
--- /dev/null
@@ -0,0 +1,24 @@
+import 'express-validator'
+import * as express from 'express'
+
+import { UserInstance } from '../models'
+import { UserRight } from '../../shared'
+import { logger } from '../helpers'
+
+function ensureUserHasRight (userRight: UserRight) {
+  return function (req: express.Request, res: express.Response, next: express.NextFunction) {
+    const user: UserInstance = res.locals.oauth.token.user
+    if (user.hasRight(userRight) === false) {
+      logger.info('User %s does not have right %s to access to %s.', user.username, UserRight[userRight], req.path)
+      return res.sendStatus(403)
+    }
+
+    return next()
+  }
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  ensureUserHasRight
+}
index 1a33cfd8ceb6571aac43e20d324e5ef9c6b40b09..0b463acc031a7914e03c3c1987a43577e3dfbea1 100644 (file)
@@ -13,7 +13,8 @@ import {
   isUserPasswordValid,
   isUserVideoQuotaValid,
   isUserDisplayNSFWValid,
-  isIdOrUUIDValid
+  isIdOrUUIDValid,
+  isUserRoleValid
 } from '../../helpers'
 import { UserInstance, VideoInstance } from '../../models'
 
@@ -22,6 +23,7 @@ const usersAddValidator = [
   body('password').custom(isUserPasswordValid).withMessage('Should have a valid password'),
   body('email').isEmail().withMessage('Should have a valid email'),
   body('videoQuota').custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
+  body('role').custom(isUserRoleValid).withMessage('Should have a valid role'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersAdd parameters', { parameters: req.body })
@@ -75,6 +77,7 @@ const usersUpdateValidator = [
   param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
   body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
   body('videoQuota').optional().custom(isUserVideoQuotaValid).withMessage('Should have a valid user quota'),
+  body('role').optional().custom(isUserRoleValid).withMessage('Should have a valid role'),
 
   (req: express.Request, res: express.Response, next: express.NextFunction) => {
     logger.debug('Checking usersUpdate parameters', { parameters: req.body })
index 979fbd34a0b3995e1fbb44d8578d977851d6769d..7d611728b8836a2600aa49e5042e9c12351850e1 100644 (file)
@@ -11,6 +11,8 @@ import {
   checkVideoChannelExists,
   checkVideoAuthorExists
 } from '../../helpers'
+import { UserInstance } from '../../models'
+import { UserRight } from '../../../shared'
 
 const listVideoAuthorChannelsValidator = [
   param('authorId').custom(isIdOrUUIDValid).withMessage('Should have a valid author id'),
@@ -106,7 +108,7 @@ export {
 // ---------------------------------------------------------------------------
 
 function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) {
-  const user = res.locals.oauth.token.User
+  const user: UserInstance = res.locals.oauth.token.User
 
   // Retrieve the user who did the request
   if (res.locals.videoChannel.isOwned() === false) {
@@ -118,7 +120,7 @@ function checkUserCanDeleteVideoChannel (res: express.Response, callback: () =>
   // Check if the user can delete the video channel
   // The user can delete it if s/he is an admin
   // Or if s/he is the video channel's author
-  if (user.isAdmin() === false && res.locals.videoChannel.Author.userId !== user.id) {
+  if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && res.locals.videoChannel.Author.userId !== user.id) {
     return res.status(403)
               .json({ error: 'Cannot remove video channel of another user' })
               .end()
index a032d14ce5938abcbef36aca780dd28d9aa746fb..0c07404c5dadf47f345688bb71b29464a7d43e75 100644 (file)
@@ -22,6 +22,7 @@ import {
   checkVideoExists,
   isIdValid
 } from '../../helpers'
+import { UserRight } from '../../../shared'
 
 const videosAddValidator = [
   body('videofile').custom((value, { req }) => isVideoFile(req.files)).withMessage(
@@ -231,7 +232,7 @@ function checkUserCanDeleteVideo (userId: number, res: express.Response, callbac
       // Check if the user can delete the video
       // The user can delete it if s/he is an admin
       // Or if s/he is the video's author
-      if (user.isAdmin() === false && res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
+      if (user.hasRight(UserRight.REMOVE_ANY_VIDEO) === false && res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
         return res.status(403)
                   .json({ error: 'Cannot remove video of another user' })
                   .end()
index 1b5233eaff509b9e3463ae54fe8dae3b6a32ca6b..49c75aa3be84be1227aa1f4ab0cc0cf34216edad 100644 (file)
@@ -3,15 +3,16 @@ import * as Promise from 'bluebird'
 
 // Don't use barrel, import just what we need
 import { User as FormattedUser } from '../../../shared/models/users/user.model'
-import { UserRole } from '../../../shared/models/users/user-role.type'
 import { ResultList } from '../../../shared/models/result-list.model'
 import { AuthorInstance } from '../video/author-interface'
+import { UserRight } from '../../../shared/models/users/user-right.enum'
+import { UserRole } from '../../../shared/models/users/user-role'
 
 export namespace UserMethods {
+  export type HasRight = (this: UserInstance, right: UserRight) => boolean
   export type IsPasswordMatch = (this: UserInstance, password: string) => Promise<boolean>
 
   export type ToFormattedJSON = (this: UserInstance) => FormattedUser
-  export type IsAdmin = (this: UserInstance) => boolean
   export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise<boolean>
 
   export type CountTotal = () => Promise<number>
@@ -31,7 +32,7 @@ export namespace UserMethods {
 export interface UserClass {
   isPasswordMatch: UserMethods.IsPasswordMatch,
   toFormattedJSON: UserMethods.ToFormattedJSON,
-  isAdmin: UserMethods.IsAdmin,
+  hasRight: UserMethods.HasRight,
   isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo,
 
   countTotal: UserMethods.CountTotal,
@@ -62,7 +63,7 @@ export interface UserInstance extends UserClass, UserAttributes, Sequelize.Insta
 
   isPasswordMatch: UserMethods.IsPasswordMatch
   toFormattedJSON: UserMethods.ToFormattedJSON
-  isAdmin: UserMethods.IsAdmin
+  hasRight: UserMethods.HasRight
 }
 
 export interface UserModel extends UserClass, Sequelize.Model<UserInstance, UserAttributes> {}
index 074c9c12143ac388068585a247ec944f2289a78d..3c625e4506b50082f52282bfc659a30f64e4bcd6 100644 (file)
@@ -1,17 +1,17 @@
-import { values } from 'lodash'
 import * as Sequelize from 'sequelize'
 import * as Promise from 'bluebird'
 
 import { getSort } from '../utils'
-import { USER_ROLES } from '../../initializers'
 import {
   cryptPassword,
   comparePassword,
   isUserPasswordValid,
   isUserUsernameValid,
   isUserDisplayNSFWValid,
-  isUserVideoQuotaValid
+  isUserVideoQuotaValid,
+  isUserRoleValid
 } from '../../helpers'
+import { UserRight, USER_ROLE_LABELS, hasUserRight } from '../../../shared'
 
 import { addMethodsToModel } from '../utils'
 import {
@@ -23,8 +23,8 @@ import {
 
 let User: Sequelize.Model<UserInstance, UserAttributes>
 let isPasswordMatch: UserMethods.IsPasswordMatch
+let hasRight: UserMethods.HasRight
 let toFormattedJSON: UserMethods.ToFormattedJSON
-let isAdmin: UserMethods.IsAdmin
 let countTotal: UserMethods.CountTotal
 let getByUsername: UserMethods.GetByUsername
 let listForApi: UserMethods.ListForApi
@@ -76,8 +76,14 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
         }
       },
       role: {
-        type: DataTypes.ENUM(values(USER_ROLES)),
-        allowNull: false
+        type: DataTypes.INTEGER,
+        allowNull: false,
+        validate: {
+          roleValid: value => {
+            const res = isUserRoleValid(value)
+            if (res === false) throw new Error('Role is not valid.')
+          }
+        }
       },
       videoQuota: {
         type: DataTypes.BIGINT,
@@ -120,9 +126,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     loadByUsernameOrEmail
   ]
   const instanceMethods = [
+    hasRight,
     isPasswordMatch,
     toFormattedJSON,
-    isAdmin,
     isAbleToUploadVideo
   ]
   addMethodsToModel(User, classMethods, instanceMethods)
@@ -139,6 +145,10 @@ function beforeCreateOrUpdate (user: UserInstance) {
 
 // ------------------------------ METHODS ------------------------------
 
+hasRight = function (this: UserInstance, right: UserRight) {
+  return hasUserRight(this.role, right)
+}
+
 isPasswordMatch = function (this: UserInstance, password: string) {
   return comparePassword(password, this.password)
 }
@@ -150,6 +160,7 @@ toFormattedJSON = function (this: UserInstance) {
     email: this.email,
     displayNSFW: this.displayNSFW,
     role: this.role,
+    roleLabel: USER_ROLE_LABELS[this.role],
     videoQuota: this.videoQuota,
     createdAt: this.createdAt,
     author: {
@@ -174,10 +185,6 @@ toFormattedJSON = function (this: UserInstance) {
   return json
 }
 
-isAdmin = function (this: UserInstance) {
-  return this.role === USER_ROLES.ADMIN
-}
-
 isAbleToUploadVideo = function (this: UserInstance, videoFile: Express.Multer.File) {
   if (this.videoQuota === -1) return Promise.resolve(true)
 
index efb58c320c598e08785bb5f997905366657437dd..a260bd38058bc288999c969b01ce1d151357b24e 100644 (file)
@@ -4,4 +4,5 @@ export * from './user-login.model'
 export * from './user-refresh-token.model'
 export * from './user-update.model'
 export * from './user-update-me.model'
-export * from './user-role.type'
+export * from './user-right.enum'
+export * from './user-role'
index 49fa2549d5353ae3cf6f88efbc87d3a1f16a842a..65830f55e62ea7d38072b7fa1aa8f6505430bbce 100644 (file)
@@ -1,6 +1,9 @@
+import { UserRole } from './user-role'
+
 export interface UserCreate {
   username: string
   password: string
   email: string
   videoQuota: number
+  role: UserRole
 }
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
new file mode 100644 (file)
index 0000000..c8c7104
--- /dev/null
@@ -0,0 +1,10 @@
+export enum UserRight {
+  ALL,
+  MANAGE_USERS,
+  MANAGE_PODS,
+  MANAGE_VIDEO_ABUSES,
+  MANAGE_REQUEST_SCHEDULERS,
+  MANAGE_VIDEO_BLACKLIST,
+  REMOVE_ANY_VIDEO,
+  REMOVE_ANY_VIDEO_CHANNEL,
+}
diff --git a/shared/models/users/user-role.ts b/shared/models/users/user-role.ts
new file mode 100644 (file)
index 0000000..cc32c76
--- /dev/null
@@ -0,0 +1,36 @@
+import { UserRight } from './user-right.enum'
+
+// Keep the order
+export enum UserRole {
+  ADMINISTRATOR = 0,
+  MODERATOR = 1,
+  USER = 2
+}
+
+export const USER_ROLE_LABELS = {
+  [UserRole.USER]: 'User',
+  [UserRole.MODERATOR]: 'Moderator',
+  [UserRole.ADMINISTRATOR]: 'Administrator'
+}
+
+// TODO: use UserRole for key once https://github.com/Microsoft/TypeScript/issues/13042 is fixed
+const userRoleRights: { [ id: number ]: UserRight[] } = {
+  [UserRole.ADMINISTRATOR]: [
+    UserRight.ALL
+  ],
+
+  [UserRole.MODERATOR]: [
+    UserRight.MANAGE_VIDEO_BLACKLIST,
+    UserRight.MANAGE_VIDEO_ABUSES,
+    UserRight.REMOVE_ANY_VIDEO,
+    UserRight.REMOVE_ANY_VIDEO_CHANNEL
+  ],
+
+  [UserRole.USER]: []
+}
+
+export function hasUserRight (userRole: UserRole, userRight: UserRight) {
+  const userRights = userRoleRights[userRole]
+
+  return userRights.indexOf(UserRight.ALL) !== -1 || userRights.indexOf(userRight) !== -1
+}
diff --git a/shared/models/users/user-role.type.ts b/shared/models/users/user-role.type.ts
deleted file mode 100644 (file)
index b38c4c8..0000000
+++ /dev/null
@@ -1 +0,0 @@
-export type UserRole = 'admin' | 'user'
index e22166fdc9ab15004fa376d8bc67157d897cd497..96b454b7c299fba40a5e6237c9ea97b2ba8b630c 100644 (file)
@@ -1,4 +1,7 @@
+import { UserRole } from './user-role'
+
 export interface UserUpdate {
   email?: string
   videoQuota?: number
+  role?: UserRole
 }
index 175e72f28457ed3cb0862f5d164cf2743f57f761..ee21475900bc054c17bfd46039573277792d03bb 100644 (file)
@@ -1,5 +1,5 @@
-import { UserRole } from './user-role.type'
 import { VideoChannel } from '../videos/video-channel.model'
+import { UserRole } from './user-role'
 
 export interface User {
   id: number