Init commit
This commit is contained in:
7
server/core/middlewares/validators/users/index.ts
Normal file
7
server/core/middlewares/validators/users/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './user-email-verification.js'
|
||||
export * from './user-exports.js'
|
||||
export * from './user-history.js'
|
||||
export * from './user-notifications.js'
|
||||
export * from './user-registrations.js'
|
||||
export * from './user-subscriptions.js'
|
||||
export * from './users.js'
|
||||
1
server/core/middlewares/validators/users/shared/index.ts
Normal file
1
server/core/middlewares/validators/users/shared/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './user-registrations.js'
|
||||
@@ -0,0 +1,64 @@
|
||||
import express from 'express'
|
||||
import { UserRegistrationModel } from '@server/models/user/user-registration.js'
|
||||
import { MRegistration } from '@server/types/models/index.js'
|
||||
import { forceNumber, pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
|
||||
export function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
||||
}
|
||||
|
||||
export function checkRegistrationEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(
|
||||
async () => {
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(registrations, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||
username: string
|
||||
channelHandle: string
|
||||
email: string
|
||||
res: express.Response
|
||||
}) {
|
||||
const { res } = options
|
||||
|
||||
const registrations = await UserRegistrationModel.listByEmailCaseInsensitiveOrHandle(
|
||||
pick(options, [ 'username', 'email', 'channelHandle' ])
|
||||
)
|
||||
|
||||
if (registrations.length !== 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Registration with this username, channel name or email already exists.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkRegistrationExist (finder: () => Promise<MRegistration>, res: express.Response, abortResponse = true) {
|
||||
const registration = await finder()
|
||||
|
||||
if (!registration) {
|
||||
if (abortResponse === true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'User not found'
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.userRegistration = registration
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { checkRegistrationEmailExistPermissive, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
export const usersAskSendUserVerifyEmailValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const [ userEmail, userPendingEmail ] = await Promise.all([
|
||||
UserModel.loadByEmailCaseInsensitive(email).then(users => getByEmailPermissive(users, email)),
|
||||
UserModel.loadByPendingEmailCaseInsensitive(email).then(users => getByEmailPermissive(users, email))
|
||||
])
|
||||
|
||||
if (userEmail && userPendingEmail) {
|
||||
logger.error(`Found 2 users with email ${email} to send verification link.`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
if (!userEmail && !userPendingEmail) {
|
||||
logger.debug(`User with email ${email} does not exist (asking verify email).`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
res.locals.userEmail = userEmail
|
||||
res.locals.userPendingEmail = userPendingEmail
|
||||
|
||||
const user = userEmail || userPendingEmail
|
||||
|
||||
if (user.pluginAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot ask verification email of a user that uses a plugin authentication.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersAskSendRegistrationVerifyEmailValidator = [
|
||||
body('email').isEmail().not().isEmpty().withMessage('Should have a valid email'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.email-verification.ask-send-verify-email.body')
|
||||
|
||||
const registrationExists = await checkRegistrationEmailExistPermissive(email, res, false)
|
||||
|
||||
if (!registrationExists) {
|
||||
logger.debug(`Registration with email ${email} does not exist (asking verify email).`)
|
||||
|
||||
// Do not leak our emails
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersVerifyEmailValidator = [
|
||||
param('id')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
body('verificationString')
|
||||
.not().isEmpty().withMessage('Should have a valid verification string'),
|
||||
body('isPendingEmail')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
const redisVerificationString = await Redis.Instance.getUserVerifyEmailLink(user.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const registrationVerifyEmailValidator = [
|
||||
param('registrationId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid registrationId'),
|
||||
|
||||
body('verificationString')
|
||||
.not().isEmpty().withMessage('Should have a valid verification string'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
|
||||
|
||||
const registration = res.locals.userRegistration
|
||||
const redisVerificationString = await Redis.Instance.getRegistrationVerifyEmailLink(registration.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res.fail({ status: HttpStatusCode.FORBIDDEN_403, message: 'Invalid verification string.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
155
server/core/middlewares/validators/users/user-exports.ts
Normal file
155
server/core/middlewares/validators/users/user-exports.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, ServerErrorCode, UserExportRequest, UserExportState, UserRight } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { UserExportModel } from '@server/models/user/user-export.js'
|
||||
import { MUserExport } from '@server/types/models/index.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { getOriginalVideoFileTotalFromUser } from '@server/lib/user.js'
|
||||
|
||||
export const userExportsListValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userExportRequestValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
body('withVideoFiles')
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have withVideoFiles boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
// Check not already created
|
||||
const exportsList = await UserExportModel.listByUser(res.locals.user)
|
||||
if (exportsList.filter(e => e.state !== UserExportState.ERRORED).length !== 0) {
|
||||
return res.fail({
|
||||
message: 'User has already processing or completed exports'
|
||||
})
|
||||
}
|
||||
|
||||
const body: UserExportRequest = req.body
|
||||
|
||||
if (body.withVideoFiles) {
|
||||
const quota = await getOriginalVideoFileTotalFromUser(res.locals.user)
|
||||
|
||||
if (quota > CONFIG.EXPORT.USERS.MAX_USER_VIDEO_QUOTA) {
|
||||
return res.fail({
|
||||
message: 'User video quota exceeds the maximum limit set by the admin to create a user archive containing videos',
|
||||
type: ServerErrorCode.MAX_USER_VIDEO_QUOTA_EXCEEDED_FOR_USER_EXPORT,
|
||||
status: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userExportDeleteValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
param('id')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
const userExport = await UserExportModel.load(req.params.id + '')
|
||||
if (!userExport) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (!checkUserExportRight(userExport, res)) return
|
||||
|
||||
if (!userExport.canBeSafelyRemoved()) {
|
||||
return res.fail({
|
||||
message: 'Cannot delete this user export because its state is not compatible with a deletion'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.userExport = userExport
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userExportDownloadValidator = [
|
||||
param('filename').exists(),
|
||||
|
||||
query('jwt').isJWT(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!ensureExportIsEnabled(res)) return
|
||||
|
||||
const userExport = await UserExportModel.loadByFilename(req.params.filename)
|
||||
if (!userExport) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (userExport.isJWTValid(req.query.jwt) !== true) return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
|
||||
res.locals.userExport = userExport
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserIdRight (userId: number | string, res: express.Response) {
|
||||
if (!await checkUserIdExist(userId, res)) return false
|
||||
|
||||
const oauthUser = res.locals.oauth.token.User
|
||||
|
||||
if (!oauthUser.hasRight(UserRight.MANAGE_USER_EXPORTS) && oauthUser.id !== res.locals.user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage exports of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkUserExportRight (userExport: MUserExport, res: express.Response) {
|
||||
if (userExport.userId !== res.locals.user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Export is not associated to this user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function ensureExportIsEnabled (res: express.Response) {
|
||||
if (CONFIG.EXPORT.USERS.ENABLED !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'User export is disabled on this instance'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
47
server/core/middlewares/validators/users/user-history.ts
Normal file
47
server/core/middlewares/validators/users/user-history.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { exists, isDateValid, isIdValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const userHistoryListValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userHistoryRemoveAllValidator = [
|
||||
body('beforeDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a before date that conforms to ISO 8601'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userHistoryRemoveElementValidator = [
|
||||
param('videoId')
|
||||
.custom(isIdValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
userHistoryListValidator,
|
||||
userHistoryRemoveElementValidator,
|
||||
userHistoryRemoveAllValidator
|
||||
}
|
||||
110
server/core/middlewares/validators/users/user-import.ts
Normal file
110
server/core/middlewares/validators/users/user-import.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
||||
import { areValidationErrors, checkUserIdExist } from '../shared/index.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { HttpStatusCode, ServerErrorCode, UserImportState, UserRight } from '@peertube/peertube-models'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { UserImportModel } from '@server/models/user/user-import.js'
|
||||
|
||||
export const userImportRequestResumableValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const file = buildUploadXFile(req.body as express.CustomUploadXFile<UploadXMetadata>)
|
||||
const cleanup = () => safeUploadXCleanup(file)
|
||||
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return cleanup()
|
||||
|
||||
if (CONFIG.IMPORT.USERS.ENABLED !== true) {
|
||||
res.fail({
|
||||
message: 'User import is not enabled by the administrator',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
res.locals.importUserFileResumable = { ...file, originalname: file.filename }
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const userImportRequestResumableInitValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.IMPORT.USERS.ENABLED !== true) {
|
||||
return res.fail({
|
||||
message: 'User import is not enabled by the administrator',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
if (req.body.filename.endsWith('.zip') !== true) {
|
||||
return res.fail({
|
||||
message: 'User import file must be a zip',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
const fileMetadata = res.locals.uploadVideoFileResumableMetadata
|
||||
const user = res.locals.user
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: fileMetadata.size }) === false) {
|
||||
return res.fail({
|
||||
message: 'User video quota is exceeded with this import',
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
type: ServerErrorCode.QUOTA_REACHED
|
||||
})
|
||||
}
|
||||
|
||||
const userImport = await UserImportModel.loadLatestByUserId(user.id)
|
||||
if (userImport && userImport.state !== UserImportState.ERRORED && userImport.state !== UserImportState.COMPLETED) {
|
||||
return res.fail({
|
||||
message: 'An import is already being processed',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const getLatestImportStatusValidator = [
|
||||
param('userId')
|
||||
.isInt().not().isEmpty().withMessage('Should have a valid userId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!await checkUserIdRight(req.params.userId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkUserIdRight (userId: number | string, res: express.Response) {
|
||||
if (!await checkUserIdExist(userId, res)) return false
|
||||
|
||||
const oauthUser = res.locals.oauth.token.User
|
||||
|
||||
if (!oauthUser.hasRight(UserRight.MANAGE_USER_IMPORTS) && oauthUser.id !== res.locals.user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage imports of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { isNumberArray } from '@server/helpers/custom-validators/search.js'
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { isNotEmptyIntArray, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isUserNotificationSettingValid } from '../../../helpers/custom-validators/user-notifications.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
export const listUserNotificationsValidator = [
|
||||
query('unread')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should have a valid unread boolean'),
|
||||
|
||||
query('typeOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid typeOneOf array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateNotificationSettingsValidator = [
|
||||
body('newVideoFromSubscription')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newCommentOnMyVideo')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('abuseAsModerator')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('videoAutoBlacklistAsModerator')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('blacklistOnMyVideo')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('myVideoImportFinished')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('myVideoPublished')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('commentMention')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newFollow')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newUserRegistration')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('newInstanceFollower')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
body('autoInstanceFollowing')
|
||||
.custom(isUserNotificationSettingValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const markAsReadUserNotificationsValidator = [
|
||||
body('ids')
|
||||
.optional()
|
||||
.custom(isNotEmptyIntArray).withMessage('Should have a valid array of notification ids'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
206
server/core/middlewares/validators/users/user-registrations.ts
Normal file
206
server/core/middlewares/validators/users/user-registrations.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@peertube/peertube-models'
|
||||
import { exists, isBooleanValid, isIdValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isRegistrationModerationResponseValid, isRegistrationReasonValid } from '@server/helpers/custom-validators/user-registration.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import { isUserDisplayNameValid, isUserPasswordValid, isUserUsernameValid } from '../../../helpers/custom-validators/users.js'
|
||||
import { isVideoChannelDisplayNameValid, isVideoChannelUsernameValid } from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { isSignupAllowed, isSignupAllowedForCurrentIP, SignupMode } from '../../../lib/signup.js'
|
||||
import { areValidationErrors, checkUsernameOrEmailDoNotAlreadyExist } from '../shared/index.js'
|
||||
import { checkRegistrationHandlesDoNotAlreadyExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
const usersDirectRegistrationValidator = usersCommonRegistrationValidatorFactory()
|
||||
|
||||
const usersRequestRegistrationValidator = [
|
||||
...usersCommonRegistrationValidatorFactory([
|
||||
body('registrationReason')
|
||||
.custom(isRegistrationReasonValid)
|
||||
]),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const body: UserRegistrationRequest = req.body
|
||||
|
||||
if (CONFIG.SIGNUP.REQUIRES_APPROVAL !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Signup approval is not enabled on this instance')
|
||||
})
|
||||
}
|
||||
|
||||
const options = { username: body.username, email: body.email, channelHandle: body.channel?.name, res }
|
||||
if (!await checkRegistrationHandlesDoNotAlreadyExist(options)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ensureUserRegistrationAllowedFactory (signupMode: SignupMode) {
|
||||
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const allowedParams = {
|
||||
body: req.body,
|
||||
ip: req.ip,
|
||||
signupMode
|
||||
}
|
||||
|
||||
const allowedResult = await Hooks.wrapPromiseFun(
|
||||
isSignupAllowed,
|
||||
allowedParams,
|
||||
signupMode === 'direct-registration'
|
||||
? 'filter:api.user.signup.allowed.result'
|
||||
: 'filter:api.user.request-signup.allowed.result'
|
||||
)
|
||||
|
||||
if (allowedResult.allowed === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: allowedResult.errorMessage || req.t('User registration is not allowed')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
const ensureUserRegistrationAllowedForIP = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const allowed = isSignupAllowedForCurrentIP(req.ip)
|
||||
|
||||
if (allowed === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('You are not on a network authorized for registration.')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const acceptOrRejectRegistrationValidator = [
|
||||
param('registrationId')
|
||||
.custom(isIdValid),
|
||||
|
||||
body('moderationResponse')
|
||||
.custom(isRegistrationModerationResponseValid),
|
||||
|
||||
body('preventEmailDelivery')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have preventEmailDelivery boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
|
||||
|
||||
if (res.locals.userRegistration.state !== UserRegistrationState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('This registration is already accepted or rejected.')
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const getRegistrationValidator = [
|
||||
param('registrationId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkRegistrationIdExist(req.params.registrationId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const listRegistrationsValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
acceptOrRejectRegistrationValidator,
|
||||
ensureUserRegistrationAllowedFactory,
|
||||
ensureUserRegistrationAllowedForIP,
|
||||
getRegistrationValidator,
|
||||
listRegistrationsValidator,
|
||||
usersDirectRegistrationValidator,
|
||||
usersRequestRegistrationValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function usersCommonRegistrationValidatorFactory (additionalValidationChain: ValidationChain[] = []) {
|
||||
return [
|
||||
body('username')
|
||||
.custom(isUserUsernameValid),
|
||||
body('password')
|
||||
.custom(isUserPasswordValid),
|
||||
body('email')
|
||||
.isEmail(),
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isUserDisplayNameValid),
|
||||
|
||||
body('channel.name')
|
||||
.optional()
|
||||
.custom(isVideoChannelUsernameValid),
|
||||
body('channel.displayName')
|
||||
.optional()
|
||||
.custom(isVideoChannelDisplayNameValid),
|
||||
|
||||
...additionalValidationChain,
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
|
||||
const body: UserRegister | UserRegistrationRequest = req.body
|
||||
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist({ username: body.username, email: body.email, req, res })) return
|
||||
|
||||
if (body.channel) {
|
||||
if (!body.channel.name || !body.channel.displayName) {
|
||||
return res.fail({
|
||||
message: req.t('Channel is optional but if you specify it, channel.name and channel.displayName are required.')
|
||||
})
|
||||
}
|
||||
|
||||
if (body.channel.name === body.username) {
|
||||
return res.fail({ message: req.t('Channel name cannot be the same as user username.') })
|
||||
}
|
||||
|
||||
const existing = await loadReservedActorName(body.channel.name)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t(`Channel with name {name} already exists.`, { name: body.channel.name })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
110
server/core/middlewares/validators/users/user-subscriptions.ts
Normal file
110
server/core/middlewares/validators/users/user-subscriptions.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { FollowState, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { areValidActorHandles, isValidActorHandle } from '../../../helpers/custom-validators/activitypub/actor.js'
|
||||
import { WEBSERVER } from '../../../initializers/constants.js'
|
||||
import { ActorFollowModel } from '../../../models/actor/actor-follow.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const userSubscriptionListValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userSubscriptionAddValidator = [
|
||||
body('uri')
|
||||
.custom(isValidActorHandle).withMessage('Should have a valid URI to follow (username@domain)'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const areSubscriptionsExistValidator = [
|
||||
query('uris')
|
||||
.customSanitizer(arrayify)
|
||||
.custom(areValidActorHandles).withMessage('Should have a valid array of URIs'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userSubscriptionGetValidator = [
|
||||
param('uri')
|
||||
.custom(isValidActorHandle),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesSubscriptionExist({ uri: req.params.uri, res, state: 'accepted' })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const userSubscriptionDeleteValidator = [
|
||||
param('uri')
|
||||
.custom(isValidActorHandle),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesSubscriptionExist({ uri: req.params.uri, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
areSubscriptionsExistValidator,
|
||||
userSubscriptionListValidator,
|
||||
userSubscriptionAddValidator,
|
||||
userSubscriptionGetValidator,
|
||||
userSubscriptionDeleteValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesSubscriptionExist (options: {
|
||||
uri: string
|
||||
res: express.Response
|
||||
state?: FollowState
|
||||
}) {
|
||||
const { uri, res, state } = options
|
||||
|
||||
let [ name, host ] = uri.split('@')
|
||||
if (host === WEBSERVER.HOST) host = null
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
|
||||
actorId: user.Account.Actor.id,
|
||||
targetName: name,
|
||||
targetHost: host,
|
||||
state
|
||||
})
|
||||
|
||||
if (!subscription?.ActorFollowing.VideoChannel) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Subscription ${uri} not found.`
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.subscription = subscription
|
||||
|
||||
return true
|
||||
}
|
||||
522
server/core/middlewares/validators/users/users.ts
Normal file
522
server/core/middlewares/validators/users/users.ts
Normal file
@@ -0,0 +1,522 @@
|
||||
import { arrayify, forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRole, UserUpdateMe } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { isNSFWFlagsValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { MUser } from '@server/types/models/user/user.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { exists, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isThemeNameValid } from '../../../helpers/custom-validators/plugins.js'
|
||||
import {
|
||||
isUserAdminFlagsValid,
|
||||
isUserAutoPlayNextVideoValid,
|
||||
isUserAutoPlayVideoValid,
|
||||
isUserBlockedReasonValid,
|
||||
isUserDescriptionValid,
|
||||
isUserDisplayNameValid,
|
||||
isUserEmailPublicValid,
|
||||
isUserFeatureInfo,
|
||||
isUserLanguage,
|
||||
isUserNoModal,
|
||||
isUserNSFWPolicyValid,
|
||||
isUserP2PEnabledValid,
|
||||
isUserPasswordValid,
|
||||
isUserPasswordValidOrEmpty,
|
||||
isUserRoleValid,
|
||||
isUserUsernameValid,
|
||||
isUserVideoLanguages,
|
||||
isUserVideoQuotaDailyValid,
|
||||
isUserVideoQuotaValid,
|
||||
isUserVideosHistoryEnabledValid
|
||||
} from '../../../helpers/custom-validators/users.js'
|
||||
import { isVideoChannelUsernameValid } from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { isThemeRegistered } from '../../../lib/plugins/theme-utils.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkEmailDoesNotAlreadyExist,
|
||||
checkUserEmailExistPermissive,
|
||||
checkUserIdExist,
|
||||
checkUsernameOrEmailDoNotAlreadyExist,
|
||||
doesChannelIdExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const usersListValidator = [
|
||||
query('blocked')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should be a valid blocked boolean'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersAddValidator = [
|
||||
body('username')
|
||||
.custom(isUserUsernameValid)
|
||||
.withMessage('Should have a valid username (lowercase alphanumeric characters)'),
|
||||
body('password')
|
||||
.custom(isUserPasswordValidOrEmpty),
|
||||
body('email')
|
||||
.isEmail(),
|
||||
|
||||
body('channelName')
|
||||
.optional()
|
||||
.custom(isVideoChannelUsernameValid),
|
||||
|
||||
body('videoQuota')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaValid),
|
||||
|
||||
body('videoQuotaDaily')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaDailyValid),
|
||||
|
||||
body('role')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isUserRoleValid),
|
||||
|
||||
body('adminFlags')
|
||||
.optional()
|
||||
.custom(isUserAdminFlagsValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
if (!await checkUsernameOrEmailDoNotAlreadyExist({ username: req.body.username, email: req.body.email, req, res })) return
|
||||
|
||||
const authUser = res.locals.oauth.token.User
|
||||
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'You can only create users (and not administrators or moderators)'
|
||||
})
|
||||
}
|
||||
|
||||
if (req.body.channelName) {
|
||||
if (req.body.channelName === req.body.username) {
|
||||
return res.fail({ message: 'Channel name cannot be the same as user username.' })
|
||||
}
|
||||
|
||||
const existing = await loadReservedActorName(req.body.channelName)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: `Channel with name ${req.body.channelName} already exists.`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersRemoveValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
if (user.username === 'root') {
|
||||
return res.fail({ message: 'Cannot remove the root user' })
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersBlockToggleValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
body('reason')
|
||||
.optional()
|
||||
.custom(isUserBlockedReasonValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
if (user.username === 'root') {
|
||||
return res.fail({ message: 'Cannot block the root user' })
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteMeValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
if (user.username === 'root') {
|
||||
return res.fail({ message: 'You cannot delete your root account.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersUpdateValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
body('password')
|
||||
.optional()
|
||||
.custom(isUserPasswordValid),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail(),
|
||||
body('emailVerified')
|
||||
.optional()
|
||||
.isBoolean(),
|
||||
body('videoQuota')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaValid),
|
||||
body('videoQuotaDaily')
|
||||
.optional()
|
||||
.custom(isUserVideoQuotaDailyValid),
|
||||
body('pluginAuth')
|
||||
.optional()
|
||||
.exists(),
|
||||
body('role')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isUserRoleValid),
|
||||
body('adminFlags')
|
||||
.optional()
|
||||
.custom(isUserAdminFlagsValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
|
||||
return res.fail({ message: 'Cannot change root role.' })
|
||||
}
|
||||
|
||||
if (!checkCanModerate(user, res)) return
|
||||
|
||||
if (
|
||||
req.body.email &&
|
||||
req.body.email !== user.email &&
|
||||
!await checkEmailDoesNotAlreadyExist({ email: req.body.email, req, res })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersUpdateMeValidator = [
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isUserDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isUserDescriptionValid),
|
||||
body('currentPassword')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
body('password')
|
||||
.optional()
|
||||
.custom(isUserPasswordValid),
|
||||
body('emailPublic')
|
||||
.optional()
|
||||
.custom(isUserEmailPublicValid),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail(),
|
||||
|
||||
body('nsfwPolicy')
|
||||
.optional()
|
||||
.custom(isUserNSFWPolicyValid),
|
||||
body('nsfwFlagsDisplayed')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwFlagsHidden')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwFlagsWarned')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
body('nsfwFlagsBlurred')
|
||||
.optional()
|
||||
.custom(isNSFWFlagsValid),
|
||||
|
||||
body('autoPlayVideo')
|
||||
.optional()
|
||||
.custom(isUserAutoPlayVideoValid),
|
||||
body('p2pEnabled')
|
||||
.optional()
|
||||
.custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
|
||||
body('videoLanguages')
|
||||
.optional()
|
||||
.custom(isUserVideoLanguages),
|
||||
body('language')
|
||||
.optional()
|
||||
.custom(isUserLanguage),
|
||||
body('videosHistoryEnabled')
|
||||
.optional()
|
||||
.custom(isUserVideosHistoryEnabledValid).withMessage('Should have a valid videos history enabled boolean'),
|
||||
body('theme')
|
||||
.optional()
|
||||
.custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
|
||||
|
||||
body('noInstanceConfigWarningModal')
|
||||
.optional()
|
||||
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noInstanceConfigWarningModal boolean'),
|
||||
body('noWelcomeModal')
|
||||
.optional()
|
||||
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noWelcomeModal boolean'),
|
||||
body('noAccountSetupWarningModal')
|
||||
.optional()
|
||||
.custom(v => isUserNoModal(v)).withMessage('Should have a valid noAccountSetupWarningModal boolean'),
|
||||
|
||||
body('autoPlayNextVideo')
|
||||
.optional()
|
||||
.custom(v => isUserAutoPlayNextVideoValid(v)).withMessage('Should have a valid autoPlayNextVideo boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const body = req.body as UserUpdateMe
|
||||
|
||||
if (
|
||||
((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsWarned || 0)) !== 0 ||
|
||||
((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsDisplayed || 0)) !== 0 ||
|
||||
((body.nsfwFlagsBlurred || 0) & (body.nsfwFlagsHidden || 0)) !== 0 ||
|
||||
((body.nsfwFlagsDisplayed || 0) & (body.nsfwFlagsHidden || 0)) !== 0 ||
|
||||
((body.nsfwFlagsDisplayed || 0) & (body.nsfwFlagsWarned || 0)) !== 0 ||
|
||||
((body.nsfwFlagsHidden || 0) & (body.nsfwFlagsWarned || 0)) !== 0
|
||||
) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t(
|
||||
'Cannot use same flags in nsfwFlagsDisplayed, nsfwFlagsHidden, nsfwFlagsBlurred and nsfwFlagsWarned at the same time'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (body.password || body.email) {
|
||||
if (user.pluginAuth !== null) {
|
||||
return res.fail({ message: req.t('You cannot update your email or password that is associated with an external auth system.') })
|
||||
}
|
||||
|
||||
if (!body.currentPassword) {
|
||||
return res.fail({ message: req.t('currentPassword parameter is missing') })
|
||||
}
|
||||
|
||||
if (await user.isPasswordMatch(body.currentPassword) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('currentPassword is invalid.'),
|
||||
type: ServerErrorCode.CURRENT_PASSWORD_IS_INVALID
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
|
||||
if (
|
||||
body.email &&
|
||||
body.email !== user.email &&
|
||||
!await checkEmailDoesNotAlreadyExist({ email: body.email, req, res })
|
||||
) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersGetValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
query('withStats')
|
||||
.optional()
|
||||
.isBoolean().withMessage('Should have a valid withStats boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res, req.query.withStats)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersVideoRatingValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'id')) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listMyVideosValidator = [
|
||||
query('channelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
query('channelNameOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid channelNameOneOf array'),
|
||||
|
||||
query('includeCollaborations')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (
|
||||
req.query.channelId &&
|
||||
!await doesChannelIdExist({ id: req.query.channelId, checkCanManage: true, checkIsLocal: true, checkIsOwner: false, req, res })
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersAskResetPasswordValidator = [
|
||||
body('email')
|
||||
.isEmail(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const { email } = await Hooks.wrapObject({
|
||||
email: req.body.email
|
||||
}, 'filter:api.users.ask-reset-password.body')
|
||||
|
||||
const exists = await checkUserEmailExistPermissive(email, res, false)
|
||||
if (!exists) {
|
||||
logger.debug('User with email %s does not exist (asking reset password).', email)
|
||||
// Do not leak our emails
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
if (res.locals.user.pluginAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot recover password of a user that uses a plugin authentication.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersResetPasswordValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
body('verificationString')
|
||||
.not().isEmpty(),
|
||||
body('password')
|
||||
.custom(isUserPasswordValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await checkUserIdExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.user
|
||||
const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id)
|
||||
|
||||
if (redisVerificationString !== req.body.verificationString) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Invalid verification string.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => {
|
||||
return [
|
||||
body('currentPassword').optional().custom(exists),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR
|
||||
const targetUserId = forceNumber(targetUserIdGetter(req))
|
||||
|
||||
// Admin/moderator action on another user, skip the password check
|
||||
if (isAdminOrModerator && targetUserId !== user.id) {
|
||||
return next()
|
||||
}
|
||||
|
||||
if (!req.body.currentPassword) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'currentPassword is missing'
|
||||
})
|
||||
}
|
||||
|
||||
if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'currentPassword is invalid.',
|
||||
type: ServerErrorCode.CURRENT_PASSWORD_IS_INVALID
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const userAutocompleteValidator = [
|
||||
param('search')
|
||||
.isString()
|
||||
.not().isEmpty()
|
||||
]
|
||||
|
||||
export const usersNewFeatureInfoReadValidator = [
|
||||
body('feature')
|
||||
.custom(isUserFeatureInfo),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkCanModerate (onUser: MUser, res: express.Response) {
|
||||
const authUser = res.locals.oauth.token.User
|
||||
|
||||
if (authUser.role === UserRole.ADMINISTRATOR) return true
|
||||
if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return true
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Users can only be managed by moderators or admins.'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user