.subscribe(
() => this.redirectService.redirectToHomepage(),
- err => this.error = err.message
+ err => {
+ if (err.message.indexOf('credentials are invalid') !== -1) this.error = this.i18n('Incorrect username or password.')
+ else if (err.message.indexOf('blocked') !== -1) this.error = this.i18n('You account is blocked.')
+ else this.error = err.message
+ }
)
}
"@types/bluebird": "3.5.21"
},
"dependencies": {
+ "@types/oauth2-server": "^3.0.8",
"async": "^2.0.0",
"async-lock": "^1.1.2",
"async-lru": "^1.1.1",
"morgan": "^1.5.3",
"multer": "^1.1.0",
"nodemailer": "^4.4.2",
+ "oauth2-server": "^3.0.0",
"parse-torrent": "^6.0.0",
"password-generator": "^2.0.2",
"pem": "^1.12.3",
import {
deleteMeValidator,
usersAskResetPasswordValidator,
+ usersBlockingValidator,
usersResetPasswordValidator,
videoImportsSortValidator,
videosSortValidator
asyncMiddleware(listUsers)
)
+usersRouter.post('/:id/block',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_USERS),
+ asyncMiddleware(usersBlockingValidator),
+ asyncMiddleware(blockUser)
+)
+usersRouter.post('/:id/unblock',
+ authenticate,
+ ensureUserHasRight(UserRight.MANAGE_USERS),
+ asyncMiddleware(usersBlockingValidator),
+ asyncMiddleware(unblockUser)
+)
+
usersRouter.get('/:id',
authenticate,
ensureUserHasRight(UserRight.MANAGE_USERS),
return res.json(data)
}
+async function unblockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const user: UserModel = res.locals.user
+
+ await changeUserBlock(res, user, false)
+
+ return res.status(204).end()
+}
+
+async function blockUser (req: express.Request, res: express.Response, next: express.NextFunction) {
+ const user: UserModel = res.locals.user
+
+ await changeUserBlock(res, user, true)
+
+ return res.status(204).end()
+}
+
function getUser (req: express.Request, res: express.Response, next: express.NextFunction) {
return res.json((res.locals.user as UserModel).toFormattedJSON())
}
function success (req: express.Request, res: express.Response, next: express.NextFunction) {
res.end()
}
+
+async function changeUserBlock (res: express.Response, user: UserModel, block: boolean) {
+ const oldUserAuditView = new UserAuditView(user.toFormattedJSON())
+
+ user.blocked = block
+
+ await sequelizeTypescript.transaction(async t => {
+ await OAuthTokenModel.deleteUserToken(user.id, t)
+
+ await user.save({ transaction: t })
+ })
+
+ auditLogger.update(
+ res.locals.oauth.token.User.Account.Actor.getIdentifier(),
+ new UserAuditView(user.toFormattedJSON()),
+ oldUserAuditView
+ )
+}
import * as validator from 'validator'
import { UserRole } from '../../../shared'
import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers'
-import { exists, isFileValid } from './misc'
+import { exists, isFileValid, isBooleanValid } from './misc'
import { values } from 'lodash'
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
return value === null || (exists(value) && validator.isLength(value, CONSTRAINTS_FIELDS.USERS.DESCRIPTION))
}
-function isBoolean (value: any) {
- return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
-}
-
const nsfwPolicies = values(NSFW_POLICY_TYPES)
function isUserNSFWPolicyValid (value: any) {
return exists(value) && nsfwPolicies.indexOf(value) !== -1
}
function isUserAutoPlayVideoValid (value: any) {
- return isBoolean(value)
+ return isBooleanValid(value)
+}
+
+function isUserBlockedValid (value: any) {
+ return isBooleanValid(value)
}
function isUserRoleValid (value: any) {
// ---------------------------------------------------------------------------
export {
+ isUserBlockedValid,
isUserPasswordValid,
isUserRoleValid,
isUserVideoQuotaValid,
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 240
+const LAST_MIGRATION_VERSION = 245
// ---------------------------------------------------------------------------
--- /dev/null
+import * as Sequelize from 'sequelize'
+import { createClient } from 'redis'
+import { CONFIG } from '../constants'
+import { JobQueue } from '../../lib/job-queue'
+import { initDatabaseModels } from '../database'
+
+async function up (utils: {
+ transaction: Sequelize.Transaction
+ queryInterface: Sequelize.QueryInterface
+ sequelize: Sequelize.Sequelize
+}): Promise<any> {
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: true,
+ defaultValue: null
+ }
+ await utils.queryInterface.addColumn('user', 'blocked', data)
+ }
+
+ {
+ const query = 'UPDATE "user" SET "blocked" = false'
+ await utils.sequelize.query(query)
+ }
+
+ {
+ const data = {
+ type: Sequelize.BOOLEAN,
+ allowNull: false,
+ defaultValue: null
+ }
+ await utils.queryInterface.changeColumn('user', 'blocked', data)
+ }
+}
+
+function down (options) {
+ throw new Error('Not implemented.')
+}
+
+export { up, down }
+import { AccessDeniedError} from 'oauth2-server'
import { logger } from '../helpers/logger'
import { UserModel } from '../models/account/user'
import { OAuthClientModel } from '../models/oauth/oauth-client'
const passwordMatch = await user.isPasswordMatch(password)
if (passwordMatch === false) return null
+ if (user.blocked) throw new AccessDeniedError('User is blocked.')
+
return user
}
}
const tokenCreated = await OAuthTokenModel.create(tokenToCreate)
- const tokenToReturn = Object.assign(tokenCreated, { client, user })
-
- return tokenToReturn
+ return Object.assign(tokenCreated, { client, user })
}
// ---------------------------------------------------------------------------
if (err) {
return res.status(err.status)
.json({
- error: 'Authentication failed.',
+ error: err.message,
code: err.name
})
.end()
}
]
+const usersBlockingValidator = [
+ param('id').isInt().not().isEmpty().withMessage('Should have a valid id'),
+
+ async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ logger.debug('Checking usersRemove parameters', { parameters: req.params })
+
+ if (areValidationErrors(req, res)) return
+ if (!await checkUserIdExist(req.params.id, res)) return
+
+ const user = res.locals.user
+ if (user.username === 'root') {
+ return res.status(400)
+ .send({ error: 'Cannot block the root user' })
+ .end()
+ }
+
+ return next()
+ }
+]
+
const deleteMeValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user: UserModel = res.locals.oauth.token.User
usersAddValidator,
deleteMeValidator,
usersRegisterValidator,
+ usersBlockingValidator,
usersRemoveValidator,
usersUpdateValidator,
usersUpdateMeValidator,
import { User, UserRole } from '../../../shared/models/users'
import {
isUserAutoPlayVideoValid,
+ isUserBlockedValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
@Column
autoPlayVideo: boolean
+ @AllowNull(false)
+ @Default(false)
+ @Is('UserBlocked', value => throwIfNotValid(value, isUserBlockedValid, 'blocked boolean'))
+ @Column
+ blocked: boolean
+
@AllowNull(false)
@Is('UserRole', value => throwIfNotValid(value, isUserRoleValid, 'role'))
@Column
import { AccountModel } from '../account/account'
import { UserModel } from '../account/user'
import { OAuthClientModel } from './oauth-client'
+import { Transaction } from 'sequelize'
export type OAuthTokenInfo = {
refreshToken: string
} as OAuthTokenInfo
})
.catch(err => {
- logger.info('getRefreshToken error.', { err })
+ logger.error('getRefreshToken error.', { err })
throw err
})
}
})
}
- static deleteUserToken (userId: number) {
+ static deleteUserToken (userId: number, t?: Transaction) {
const query = {
where: {
userId
- }
+ },
+ transaction: t
}
return OAuthTokenModel.destroy(query)
import {
createUser, flushTests, getMyUserInformation, getMyUserVideoRating, getUsersList, immutableAssign, killallServers, makeGetRequest,
makePostBodyRequest, makeUploadRequest, makePutBodyRequest, registerUser, removeUser, runServer, ServerInfo, setAccessTokensToServers,
- updateUser, uploadVideo, userLogin, deleteMe
+ updateUser, uploadVideo, userLogin, deleteMe, unblockUser, blockUser
} from '../../utils'
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
import { getMagnetURI, getMyVideoImports, getYoutubeVideoUrl, importVideo } from '../../utils/videos/video-imports'
})
})
- describe('When removing an user', function () {
+ describe('When blocking/unblocking/removing user', function () {
it('Should fail with an incorrect id', async function () {
await removeUser(server.url, 'blabla', server.accessToken, 400)
+ await blockUser(server.url, 'blabla', server.accessToken, 400)
+ await unblockUser(server.url, 'blabla', server.accessToken, 400)
})
it('Should fail with the root user', async function () {
await removeUser(server.url, rootId, server.accessToken, 400)
+ await blockUser(server.url, rootId, server.accessToken, 400)
+ await unblockUser(server.url, rootId, server.accessToken, 400)
})
it('Should return 404 with a non existing id', async function () {
await removeUser(server.url, 4545454, server.accessToken, 404)
+ await blockUser(server.url, 4545454, server.accessToken, 404)
+ await unblockUser(server.url, 4545454, server.accessToken, 404)
+ })
+
+ it('Should fail with a non admin user', async function () {
+ await removeUser(server.url, userId, userAccessToken, 403)
+ await blockUser(server.url, userId, userAccessToken, 403)
+ await unblockUser(server.url, userId, userAccessToken, 403)
})
})
createUser, flushTests, getBlacklistedVideosList, getMyUserInformation, getMyUserVideoQuotaUsed, getMyUserVideoRating,
getUserInformation, getUsersList, getUsersListPaginationAndSort, getVideosList, killallServers, login, makePutBodyRequest, rateVideo,
registerUser, removeUser, removeVideo, runServer, ServerInfo, testImage, updateMyAvatar, updateMyUser, updateUser, uploadVideo, userLogin,
- deleteMe
+ deleteMe, blockUser, unblockUser
} from '../../utils/index'
import { follow } from '../../utils/server/follows'
import { setAccessTokensToServers } from '../../utils/users/login'
const client = { id: 'client', secret: server.client.secret }
const res = await login(server.url, client, server.user, 400)
- expect(res.body.error).to.equal('Authentication failed.')
+ expect(res.body.error).to.contain('client is invalid')
})
it('Should not login with an invalid client secret', async function () {
const client = { id: server.client.id, secret: 'coucou' }
const res = await login(server.url, client, server.user, 400)
- expect(res.body.error).to.equal('Authentication failed.')
+ expect(res.body.error).to.contain('client is invalid')
})
it('Should not login with an invalid username', async function () {
const user = { username: 'captain crochet', password: server.user.password }
const res = await login(server.url, server.client, user, 400)
- expect(res.body.error).to.equal('Authentication failed.')
+ expect(res.body.error).to.contain('credentials are invalid')
})
it('Should not login with an invalid password', async function () {
const user = { username: server.user.username, password: 'mew_three' }
const res = await login(server.url, server.client, user, 400)
- expect(res.body.error).to.equal('Authentication failed.')
+ expect(res.body.error).to.contain('credentials are invalid')
})
it('Should not be able to upload a video', async function () {
}
})
+ it('Should block and unblock a user', async function () {
+ const user16 = {
+ username: 'user_16',
+ password: 'my super password'
+ }
+ const resUser = await createUser(server.url, server.accessToken, user16.username, user16.password)
+ const user16Id = resUser.body.user.id
+
+ accessToken = await userLogin(server, user16)
+
+ await getMyUserInformation(server.url, accessToken, 200)
+ await blockUser(server.url, user16Id, server.accessToken)
+
+ await getMyUserInformation(server.url, accessToken, 401)
+ await userLogin(server, user16, 400)
+
+ await unblockUser(server.url, user16Id, server.accessToken)
+ accessToken = await userLogin(server, user16)
+ await getMyUserInformation(server.url, accessToken, 200)
+ })
+
after(async function () {
killallServers([ server ])
.expect(expectedStatus)
}
+function blockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
+ const path = '/api/v1/users'
+
+ return request(url)
+ .post(path + '/' + userId + '/block')
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(expectedStatus)
+}
+
+function unblockUser (url: string, userId: number | string, accessToken: string, expectedStatus = 204) {
+ const path = '/api/v1/users'
+
+ return request(url)
+ .post(path + '/' + userId + '/unblock')
+ .set('Accept', 'application/json')
+ .set('Authorization', 'Bearer ' + accessToken)
+ .expect(expectedStatus)
+}
+
function updateMyUser (options: {
url: string
accessToken: string,
updateUser,
updateMyUser,
getUserInformation,
+ blockUser,
+ unblockUser,
askResetPassword,
resetPassword,
updateMyAvatar
"@types/events" "*"
"@types/node" "*"
+"@types/oauth2-server@^3.0.8":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/oauth2-server/-/oauth2-server-3.0.8.tgz#0b7f5083790732ea00bf8c5e0b04b9fa1f22f22c"
+ dependencies:
+ "@types/express" "*"
+
"@types/parse-torrent-file@*":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/parse-torrent-file/-/parse-torrent-file-4.0.1.tgz#056a6c18f3fac0cd7c6c74540f00496a3225976b"
nan "^2.9.2"
node-pre-gyp "^0.10.0"
-fstream@^1.0.0, fstream@^1.0.2:
+fstream-ignore@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/fstream-ignore/-/fstream-ignore-1.0.5.tgz#9c31dae34767018fe1d249b24dada67d092da105"
+ dependencies:
+ fstream "^1.0.0"
+ inherits "2"
+ minimatch "^3.0.0"
+
+fstream@^1.0.0, fstream@^1.0.10, fstream@^1.0.2:
version "1.0.11"
resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171"
dependencies:
dependencies:
traverse ">=0.2.4"
-hawk@~3.1.3:
+hawk@3.1.3, hawk@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
dependencies:
prelude-ls "~1.1.2"
type-check "~0.3.2"
-libxmljs@0.19.1:
- version "0.19.1"
- resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.1.tgz#bc7a62822c4392363feaab49b116b4786b2d5ada"
+libxmljs@0.19.0:
+ version "0.19.0"
+ resolved "https://registry.yarnpkg.com/libxmljs/-/libxmljs-0.19.0.tgz#dd0e635ce752af7701492ceb8c565ab74d494473"
dependencies:
bindings "~1.3.0"
nan "~2.10.0"
- node-pre-gyp "~0.10.2"
+ node-pre-gyp "~0.6.37"
lint-staged@^7.1.0:
version "7.2.0"
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
-"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
+"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
semver "^5.3.0"
tar "^4"
-node-pre-gyp@^0.10.0, node-pre-gyp@~0.10.2:
+node-pre-gyp@^0.10.0:
version "0.10.3"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
dependencies:
semver "^5.3.0"
tar "^4"
+node-pre-gyp@~0.6.37:
+ version "0.6.39"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649"
+ dependencies:
+ detect-libc "^1.0.2"
+ hawk "3.1.3"
+ mkdirp "^0.5.1"
+ nopt "^4.0.1"
+ npmlog "^4.0.2"
+ rc "^1.1.7"
+ request "2.81.0"
+ rimraf "^2.6.1"
+ semver "^5.3.0"
+ tar "^2.2.1"
+ tar-pack "^3.4.0"
+
node-sass@^4.9.0:
version "4.9.2"
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.9.2.tgz#5e63fe6bd0f2ae3ac9d6c14ede8620e2b8bdb437"
version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
-oauth2-server@3.0.0:
+oauth2-server@3.0.0, oauth2-server@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/oauth2-server/-/oauth2-server-3.0.0.tgz#c46276b74c3d28634d59ee981f76b58a6459cc28"
dependencies:
bytes "1"
string_decoder "0.10"
-rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
+rc@^1.0.1, rc@^1.1.6, rc@^1.1.7, rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
dependencies:
dependencies:
is-finite "^1.0.0"
-request@2.87.0, request@^2.81.0, request@^2.83.0:
- version "2.87.0"
- resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
- dependencies:
- aws-sign2 "~0.7.0"
- aws4 "^1.6.0"
- caseless "~0.12.0"
- combined-stream "~1.0.5"
- extend "~3.0.1"
- forever-agent "~0.6.1"
- form-data "~2.3.1"
- har-validator "~5.0.3"
- http-signature "~1.2.0"
- is-typedarray "~1.0.0"
- isstream "~0.1.2"
- json-stringify-safe "~5.0.1"
- mime-types "~2.1.17"
- oauth-sign "~0.8.2"
- performance-now "^2.1.0"
- qs "~6.5.1"
- safe-buffer "^5.1.1"
- tough-cookie "~2.3.3"
- tunnel-agent "^0.6.0"
- uuid "^3.1.0"
-
-"request@>=2.9.0 <2.82.0":
+request@2.81.0, "request@>=2.9.0 <2.82.0":
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
tunnel-agent "^0.6.0"
uuid "^3.0.0"
+request@2.87.0, request@^2.81.0, request@^2.83.0:
+ version "2.87.0"
+ resolved "https://registry.yarnpkg.com/request/-/request-2.87.0.tgz#32f00235cd08d482b4d0d68db93a829c0ed5756e"
+ dependencies:
+ aws-sign2 "~0.7.0"
+ aws4 "^1.6.0"
+ caseless "~0.12.0"
+ combined-stream "~1.0.5"
+ extend "~3.0.1"
+ forever-agent "~0.6.1"
+ form-data "~2.3.1"
+ har-validator "~5.0.3"
+ http-signature "~1.2.0"
+ is-typedarray "~1.0.0"
+ isstream "~0.1.2"
+ json-stringify-safe "~5.0.1"
+ mime-types "~2.1.17"
+ oauth-sign "~0.8.2"
+ performance-now "^2.1.0"
+ qs "~6.5.1"
+ safe-buffer "^5.1.1"
+ tough-cookie "~2.3.3"
+ tunnel-agent "^0.6.0"
+ uuid "^3.1.0"
+
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
pump "^1.0.0"
tar-stream "^1.1.2"
+tar-pack@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.1.tgz#e1dbc03a9b9d3ba07e896ad027317eb679a10a1f"
+ dependencies:
+ debug "^2.2.0"
+ fstream "^1.0.10"
+ fstream-ignore "^1.0.5"
+ once "^1.3.3"
+ readable-stream "^2.1.4"
+ rimraf "^2.5.1"
+ tar "^2.2.1"
+ uid-number "^0.0.6"
+
tar-stream@^1.1.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.1.tgz#f84ef1696269d6223ca48f6e1eeede3f7e81f395"
to-buffer "^1.1.0"
xtend "^4.0.0"
-tar@^2.0.0:
+tar@^2.0.0, tar@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
dependencies:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+uid-number@^0.0.6:
+ version "0.0.6"
+ resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
+
uint64be@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/uint64be/-/uint64be-1.0.1.tgz#1f7154202f2a1b8af353871dda651bf34ce93e95"