Other
-
+
Administration
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'
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
+ }
}
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 @@
+export * from './login-guard.service'
+export * from './user-right-guard.service'
export * from './preload-selected-modules-list'
diff --git a/client/src/app/core/auth/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts
similarity index 92%
rename from client/src/app/core/auth/login-guard.service.ts
rename to 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 {
Router
} from '@angular/router'
-import { AuthService } from './auth.service'
+import { AuthService } from '../auth/auth.service'
@Injectable()
export class LoginGuard implements CanActivate, CanActivateChild {
diff --git a/client/src/app/+admin/admin-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts
similarity index 66%
rename from client/src/app/+admin/admin-guard.service.ts
rename to 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 {
Router
} from '@angular/router'
-import { AuthService } from '../core'
+import { AuthService } from '../auth'
@Injectable()
-export class AdminGuard implements CanActivate, CanActivateChild {
+export class UserRightGuard implements CanActivate, CanActivateChild {
constructor (
private router: Router,
@@ -18,7 +18,12 @@ export class AdminGuard implements CanActivate, CanActivateChild {
) {}
canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
- if (this.auth.isAdmin() === true) return true
+ const user = this.auth.getUser()
+ if (user) {
+ const neededUserRight = route.data.userRight
+
+ if (user.hasRight(neededUserRight)) return true
+ }
this.router.navigate([ '/login' ])
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 = {
'min': 'Quota must be greater than -1.'
}
}
+export const USER_ROLE = {
+ VALIDATORS: [ Validators.required ],
+ MESSAGES: {
+ 'required': 'User role is required.',
+ }
+}
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 @@
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)
}
}
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 @@
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
}
}
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 {
VideoService,
VideoPagination
} from '../shared'
-import { Search, SearchField, SearchService, User} from '../../shared'
+import { Search, SearchField, SearchService, User } from '../../shared'
@Component({
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 {
} 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)
)
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 {
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)
)
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 @@
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()
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 {
} 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,
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'
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)
)
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 @@
-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 {
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'
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 = ''
// ---------------------------------------------------------------------------
@@ -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,
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'
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
index 000000000..e67c5ca24
--- /dev/null
+++ b/server/initializers/migrations/0085-user-role.ts
@@ -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
{
+ 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
index 812397352..000000000
--- a/server/middlewares/admin.ts
+++ /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
-}
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 @@
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
index 000000000..bcebe9d7f
--- /dev/null
+++ b/server/middlewares/user-right.ts
@@ -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
+}
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 {
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 })
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 {
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()
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 {
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()
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'
// 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
export type ToFormattedJSON = (this: UserInstance) => FormattedUser
- export type IsAdmin = (this: UserInstance) => boolean
export type IsAbleToUploadVideo = (this: UserInstance, videoFile: Express.Multer.File) => Promise
export type CountTotal = () => Promise
@@ -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 {}
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 @@
-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
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)
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'
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'
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 @@
+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
index 000000000..c8c710450
--- /dev/null
+++ b/shared/models/users/user-right.enum.ts
@@ -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
index 000000000..cc32c768d
--- /dev/null
+++ b/shared/models/users/user-role.ts
@@ -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
index b38c4c8c3..000000000
--- a/shared/models/users/user-role.type.ts
+++ /dev/null
@@ -1 +0,0 @@
-export type UserRole = 'admin' | 'user'
diff --git a/shared/models/users/user-update.model.ts b/shared/models/users/user-update.model.ts
index e22166fdc..96b454b7c 100644
--- a/shared/models/users/user-update.model.ts
+++ b/shared/models/users/user-update.model.ts
@@ -1,4 +1,7 @@
+import { UserRole } from './user-role'
+
export interface UserUpdate {
email?: string
videoQuota?: number
+ role?: UserRole
}
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 @@
-import { UserRole } from './user-role.type'
import { VideoChannel } from '../videos/video-channel.model'
+import { UserRole } from './user-role'
export interface User {
id: number