はじまりの大地
このコミットが含まれているのは:
@@ -0,0 +1,254 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { AbuseCreate, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import {
|
||||
areAbusePredefinedReasonsValid,
|
||||
isAbuseFilterValid,
|
||||
isAbuseMessageValid,
|
||||
isAbuseModerationCommentValid,
|
||||
isAbusePredefinedReasonValid,
|
||||
isAbuseReasonValid,
|
||||
isAbuseStateValid,
|
||||
isAbuseTimestampCoherent,
|
||||
isAbuseTimestampValid,
|
||||
isAbuseVideoIsValid
|
||||
} from '@server/helpers/custom-validators/abuses.js'
|
||||
import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID, toIntOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { AbuseMessageModel } from '@server/models/abuse/abuse-message.js'
|
||||
import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared/index.js'
|
||||
|
||||
const abuseReportValidator = [
|
||||
body('account.id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
body('video.id')
|
||||
.optional()
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
body('video.startAt')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isAbuseTimestampValid),
|
||||
body('video.endAt')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isAbuseTimestampValid)
|
||||
.bail()
|
||||
.custom(isAbuseTimestampCoherent)
|
||||
.withMessage('Should have a startAt timestamp beginning before endAt'),
|
||||
|
||||
body('comment.id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
body('reason')
|
||||
.custom(isAbuseReasonValid),
|
||||
|
||||
body('predefinedReasons')
|
||||
.optional()
|
||||
.custom(areAbusePredefinedReasonsValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: AbuseCreate = req.body
|
||||
|
||||
if (body.video?.id && !await doesVideoExist(body.video.id, res)) return
|
||||
if (body.account?.id && !await doesAccountIdExist(body.account.id, res)) return
|
||||
if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
|
||||
|
||||
if (!body.video?.id && !body.account?.id && !body.comment?.id) {
|
||||
res.fail({ message: 'video id or account id or comment id is required.' })
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseGetValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAbuseExist(req.params.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseUpdateValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
body('state')
|
||||
.optional()
|
||||
.custom(isAbuseStateValid),
|
||||
body('moderationComment')
|
||||
.optional()
|
||||
.custom(isAbuseModerationCommentValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAbuseExist(req.params.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseListForAdminsValidator = [
|
||||
query('id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
query('filter')
|
||||
.optional()
|
||||
.custom(isAbuseFilterValid),
|
||||
query('predefinedReason')
|
||||
.optional()
|
||||
.custom(isAbusePredefinedReasonValid),
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('state')
|
||||
.optional()
|
||||
.custom(isAbuseStateValid),
|
||||
query('videoIs')
|
||||
.optional()
|
||||
.custom(isAbuseVideoIsValid),
|
||||
query('searchReporter')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('searchReportee')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('searchVideo')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('searchVideoChannel')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const abuseListForUserValidator = [
|
||||
query('id')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('state')
|
||||
.optional()
|
||||
.custom(isAbuseStateValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getAbuseValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAbuseExist(req.params.id, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.user
|
||||
const abuse = res.locals.abuse
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuse.reporterAccountId !== user.Account.id) {
|
||||
const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
|
||||
logger.warn(message)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const checkAbuseValidForMessagesValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const abuse = res.locals.abuse
|
||||
if (abuse.ReporterAccount.isOwned() === false) {
|
||||
return res.fail({ message: 'This abuse was created by a user of your instance.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const addAbuseMessageValidator = [
|
||||
body('message')
|
||||
.custom(isAbuseMessageValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const deleteAbuseMessageValidator = [
|
||||
param('messageId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.user
|
||||
const abuse = res.locals.abuse
|
||||
|
||||
const messageId = forceNumber(req.params.messageId)
|
||||
const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
|
||||
|
||||
if (!abuseMessage) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Abuse message not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot delete this abuse message'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.abuseMessage = abuseMessage
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
abuseListForAdminsValidator,
|
||||
abuseReportValidator,
|
||||
abuseGetValidator,
|
||||
addAbuseMessageValidator,
|
||||
checkAbuseValidForMessagesValidator,
|
||||
abuseUpdateValidator,
|
||||
deleteAbuseMessageValidator,
|
||||
abuseListForUserValidator,
|
||||
getAbuseValidator
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isAccountNameValid } from '../../helpers/custom-validators/accounts.js'
|
||||
import { areValidationErrors, doesAccountNameWithHostExist, doesLocalAccountNameExist } from './shared/index.js'
|
||||
|
||||
const localAccountValidator = [
|
||||
param('name')
|
||||
.custom(isAccountNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesLocalAccountNameExist(req.params.name, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const accountNameWithHostGetValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
localAccountValidator,
|
||||
accountNameWithHostGetValidator
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import express from 'express'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { isRootActivityValid } from '../../../helpers/custom-validators/activitypub/activity.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
|
||||
async function activityPubValidator (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
logger.debug('Checking activity pub parameters')
|
||||
|
||||
if (!isRootActivityValid(req.body)) {
|
||||
logger.warn('Incorrect activity parameters.', { activity: req.body })
|
||||
return res.fail({ message: 'Incorrect activity' })
|
||||
}
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
const remoteActor = res.locals.signature.actor
|
||||
if (serverActor.id === remoteActor.id || remoteActor.serverId === null) {
|
||||
logger.error('Receiving request in INBOX by ourselves!', req.body)
|
||||
return res.status(HttpStatusCode.CONFLICT_409).end()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
activityPubValidator
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './activity.js'
|
||||
export * from './signature.js'
|
||||
export * from './pagination.js'
|
||||
@@ -0,0 +1,25 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { PAGINATION } from '@server/initializers/constants.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const apPaginationValidator = [
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1 }),
|
||||
query('size')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: PAGINATION.OUTBOX.COUNT.MAX }).withMessage(`Should have a valid page size (max: ${PAGINATION.OUTBOX.COUNT.MAX})`),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
apPaginationValidator
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import {
|
||||
isSignatureCreatorValid,
|
||||
isSignatureTypeValid,
|
||||
isSignatureValueValid
|
||||
} from '../../../helpers/custom-validators/activitypub/signature.js'
|
||||
import { isDateValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const signatureValidator = [
|
||||
body('signature.type')
|
||||
.optional()
|
||||
.custom(isSignatureTypeValid),
|
||||
body('signature.created')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a signature created date that conforms to ISO 8601'),
|
||||
body('signature.creator')
|
||||
.optional()
|
||||
.custom(isSignatureCreatorValid),
|
||||
body('signature.signatureValue')
|
||||
.optional()
|
||||
.custom(isSignatureValueValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking Linked Data Signature parameter', { parameters: { signature: req.body.signature } })
|
||||
|
||||
if (areValidationErrors(req, res, { omitLog: true })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
signatureValidator
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { isActorImageFile } from '@server/helpers/custom-validators/actor-images.js'
|
||||
import { cleanUpReqFiles } from '../../helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const updateActorImageValidatorFactory = (fieldname: string) => ([
|
||||
body(fieldname).custom((value, { req }) => isActorImageFile(req.files, fieldname)).withMessage(
|
||||
'This file is not supported or too large. Please, make sure it is of the following type : ' +
|
||||
CONSTRAINTS_FIELDS.ACTORS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
export const updateAvatarValidator = updateActorImageValidatorFactory('avatarfile')
|
||||
export const updateBannerValidator = updateActorImageValidatorFactory('bannerfile')
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CommentAutomaticTagPoliciesUpdate } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { AutomaticTagger } from '@server/lib/automatic-tags/automatic-tagger.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { doesAccountNameWithHostExist } from './shared/accounts.js'
|
||||
import { checkUserCanManageAccount } from './shared/users.js'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
export const manageAccountAutomaticTagsValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
|
||||
if (!checkUserCanManageAccount({ user: res.locals.oauth.token.User, account: res.locals.account, specialRight: null, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateAutomaticTagPoliciesValidator = [
|
||||
...manageAccountAutomaticTagsValidator,
|
||||
|
||||
body('review')
|
||||
.custom(isStringArray).withMessage('Should have a valid review array'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body = req.body as CommentAutomaticTagPoliciesUpdate
|
||||
|
||||
const tagsObj = await AutomaticTagger.getAutomaticTagAvailable(res.locals.account)
|
||||
const available = new Set(tagsObj.available.map(({ name }) => name))
|
||||
|
||||
for (const name of body.review) {
|
||||
if (!available.has(name)) {
|
||||
return res.fail({ message: `${name} is not an available automatic tag` })
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,179 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidActorHandles } from '@server/helpers/custom-validators/activitypub/actor.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { WEBSERVER } from '../../initializers/constants.js'
|
||||
import { AccountBlocklistModel } from '../../models/account/account-blocklist.js'
|
||||
import { ServerBlocklistModel } from '../../models/server/server-blocklist.js'
|
||||
import { ServerModel } from '../../models/server/server.js'
|
||||
import { areValidationErrors, doesAccountNameWithHostExist } from './shared/index.js'
|
||||
|
||||
const blockAccountValidator = [
|
||||
body('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const accountToBlock = res.locals.account
|
||||
|
||||
if (user.Account.id === accountToBlock.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'You cannot block yourself.'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockAccountByAccountValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const targetAccount = res.locals.account
|
||||
if (!await doesUnblockAccountExist(user.Account.id, targetAccount.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockAccountByServerValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
const targetAccount = res.locals.account
|
||||
if (!await doesUnblockAccountExist(serverActor.Account.id, targetAccount.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const blockServerValidator = [
|
||||
body('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const host: string = req.body.host
|
||||
|
||||
if (host === WEBSERVER.HOST) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'You cannot block your own server.'
|
||||
})
|
||||
}
|
||||
|
||||
const server = await ServerModel.loadOrCreateByHost(host)
|
||||
|
||||
res.locals.server = server
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockServerByAccountValidator = [
|
||||
param('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await doesUnblockServerExist(user.Account.id, req.params.host, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const unblockServerByServerValidator = [
|
||||
param('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
if (!await doesUnblockServerExist(serverActor.Account.id, req.params.host, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const blocklistStatusValidator = [
|
||||
query('hosts')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isEachUniqueHostValid).withMessage('Should have a valid hosts array'),
|
||||
|
||||
query('accounts')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(areValidActorHandles).withMessage('Should have a valid accounts array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
blockServerValidator,
|
||||
blockAccountValidator,
|
||||
unblockAccountByAccountValidator,
|
||||
unblockServerByAccountValidator,
|
||||
unblockAccountByServerValidator,
|
||||
unblockServerByServerValidator,
|
||||
blocklistStatusValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
|
||||
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
|
||||
if (!accountBlock) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Account block entry not found.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.accountBlock = accountBlock
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
|
||||
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
|
||||
if (!serverBlock) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Server block entry not found.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.serverBlock = serverBlock
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { BulkRemoveCommentsOfBody, HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { isBulkRemoveCommentsOfScopeValid } from '@server/helpers/custom-validators/bulk.js'
|
||||
import { areValidationErrors, doesAccountNameWithHostExist } from './shared/index.js'
|
||||
|
||||
const bulkRemoveCommentsOfValidator = [
|
||||
body('accountName')
|
||||
.exists(),
|
||||
body('scope')
|
||||
.custom(isBulkRemoveCommentsOfScopeValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.body.accountName, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const body = req.body as BulkRemoveCommentsOfBody
|
||||
|
||||
if (body.scope === 'instance' && user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'User cannot remove any comments of this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
bulkRemoveCommentsOfValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -0,0 +1,198 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { CustomConfig, HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isIntOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG, isEmailEnabled } from '@server/initializers/config.js'
|
||||
import { isThemeNameValid } from '../../helpers/custom-validators/plugins.js'
|
||||
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users.js'
|
||||
import { isThemeRegistered } from '../../lib/plugins/theme-utils.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const customConfigUpdateValidator = [
|
||||
body('instance.name').exists(),
|
||||
body('instance.shortDescription').exists(),
|
||||
body('instance.description').exists(),
|
||||
body('instance.terms').exists(),
|
||||
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid),
|
||||
body('instance.defaultClientRoute').exists(),
|
||||
body('instance.customizations.css').exists(),
|
||||
body('instance.customizations.javascript').exists(),
|
||||
|
||||
body('services.twitter.username').exists(),
|
||||
|
||||
body('cache.previews.size').isInt(),
|
||||
body('cache.captions.size').isInt(),
|
||||
body('cache.torrents.size').isInt(),
|
||||
body('cache.storyboards.size').isInt(),
|
||||
|
||||
body('signup.enabled').isBoolean(),
|
||||
body('signup.limit').isInt(),
|
||||
body('signup.requiresEmailVerification').isBoolean(),
|
||||
body('signup.requiresApproval').isBoolean(),
|
||||
body('signup.minimumAge').isInt(),
|
||||
|
||||
body('admin.email').isEmail(),
|
||||
body('contactForm.enabled').isBoolean(),
|
||||
|
||||
body('user.history.videos.enabled').isBoolean(),
|
||||
body('user.videoQuota').custom(isUserVideoQuotaValid),
|
||||
body('user.videoQuotaDaily').custom(isUserVideoQuotaDailyValid),
|
||||
|
||||
body('videoChannels.maxPerUser').isInt(),
|
||||
|
||||
body('transcoding.enabled').isBoolean(),
|
||||
body('transcoding.originalFile.keep').isBoolean(),
|
||||
body('transcoding.allowAdditionalExtensions').isBoolean(),
|
||||
body('transcoding.threads').isInt(),
|
||||
body('transcoding.concurrency').isInt({ min: 1 }),
|
||||
body('transcoding.resolutions.0p').isBoolean(),
|
||||
body('transcoding.resolutions.144p').isBoolean(),
|
||||
body('transcoding.resolutions.240p').isBoolean(),
|
||||
body('transcoding.resolutions.360p').isBoolean(),
|
||||
body('transcoding.resolutions.480p').isBoolean(),
|
||||
body('transcoding.resolutions.720p').isBoolean(),
|
||||
body('transcoding.resolutions.1080p').isBoolean(),
|
||||
body('transcoding.resolutions.1440p').isBoolean(),
|
||||
body('transcoding.resolutions.2160p').isBoolean(),
|
||||
body('transcoding.remoteRunners.enabled').isBoolean(),
|
||||
|
||||
body('transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
||||
|
||||
body('transcoding.webVideos.enabled').isBoolean(),
|
||||
body('transcoding.hls.enabled').isBoolean(),
|
||||
|
||||
body('videoStudio.enabled').isBoolean(),
|
||||
body('videoStudio.remoteRunners.enabled').isBoolean(),
|
||||
|
||||
body('videoFile.update.enabled').isBoolean(),
|
||||
|
||||
body('import.videos.concurrency').isInt({ min: 0 }),
|
||||
body('import.videos.http.enabled').isBoolean(),
|
||||
body('import.videos.torrent.enabled').isBoolean(),
|
||||
|
||||
body('import.videoChannelSynchronization.enabled').isBoolean(),
|
||||
body('import.users.enabled').isBoolean(),
|
||||
|
||||
body('export.users.enabled').isBoolean(),
|
||||
body('export.users.maxUserVideoQuota').exists(),
|
||||
body('export.users.exportExpiration').exists(),
|
||||
|
||||
body('trending.videos.algorithms.default').exists(),
|
||||
body('trending.videos.algorithms.enabled').exists(),
|
||||
|
||||
body('followers.instance.enabled').isBoolean(),
|
||||
body('followers.instance.manualApproval').isBoolean(),
|
||||
|
||||
body('theme.default').custom(v => isThemeNameValid(v) && isThemeRegistered(v)),
|
||||
|
||||
body('broadcastMessage.enabled').isBoolean(),
|
||||
body('broadcastMessage.message').exists(),
|
||||
body('broadcastMessage.level').exists(),
|
||||
body('broadcastMessage.dismissable').isBoolean(),
|
||||
|
||||
body('live.enabled').isBoolean(),
|
||||
body('live.allowReplay').isBoolean(),
|
||||
body('live.maxDuration').isInt(),
|
||||
body('live.maxInstanceLives').custom(isIntOrNull),
|
||||
body('live.maxUserLives').custom(isIntOrNull),
|
||||
body('live.transcoding.enabled').isBoolean(),
|
||||
body('live.transcoding.threads').isInt(),
|
||||
body('live.transcoding.resolutions.144p').isBoolean(),
|
||||
body('live.transcoding.resolutions.240p').isBoolean(),
|
||||
body('live.transcoding.resolutions.360p').isBoolean(),
|
||||
body('live.transcoding.resolutions.480p').isBoolean(),
|
||||
body('live.transcoding.resolutions.720p').isBoolean(),
|
||||
body('live.transcoding.resolutions.1080p').isBoolean(),
|
||||
body('live.transcoding.resolutions.1440p').isBoolean(),
|
||||
body('live.transcoding.resolutions.2160p').isBoolean(),
|
||||
body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean(),
|
||||
body('live.transcoding.remoteRunners.enabled').isBoolean(),
|
||||
|
||||
body('search.remoteUri.users').isBoolean(),
|
||||
body('search.remoteUri.anonymous').isBoolean(),
|
||||
body('search.searchIndex.enabled').isBoolean(),
|
||||
body('search.searchIndex.url').exists(),
|
||||
body('search.searchIndex.disableLocalSearch').isBoolean(),
|
||||
body('search.searchIndex.isDefaultSearch').isBoolean(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!checkInvalidConfigIfEmailDisabled(req.body, res)) return
|
||||
if (!checkInvalidTranscodingConfig(req.body, res)) return
|
||||
if (!checkInvalidSynchronizationConfig(req.body, res)) return
|
||||
if (!checkInvalidLiveConfig(req.body, res)) return
|
||||
if (!checkInvalidVideoStudioConfig(req.body, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
function ensureConfigIsEditable (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
if (!CONFIG.WEBADMIN.CONFIGURATION.EDITION.ALLOWED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.METHOD_NOT_ALLOWED_405,
|
||||
message: 'Server configuration is static and cannot be edited'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
customConfigUpdateValidator,
|
||||
ensureConfigIsEditable
|
||||
}
|
||||
|
||||
function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: express.Response) {
|
||||
if (isEmailEnabled()) return true
|
||||
|
||||
if (customConfig.signup.requiresEmailVerification === true) {
|
||||
res.fail({ message: 'SMTP is not configured but you require signup email verification.' })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express.Response) {
|
||||
if (customConfig.transcoding.enabled === false) return true
|
||||
|
||||
if (customConfig.transcoding.webVideos.enabled === false && customConfig.transcoding.hls.enabled === false) {
|
||||
res.fail({ message: 'You need to enable at least web_videos transcoding or hls transcoding' })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidSynchronizationConfig (customConfig: CustomConfig, res: express.Response) {
|
||||
if (customConfig.import.videoChannelSynchronization.enabled && !customConfig.import.videos.http.enabled) {
|
||||
res.fail({ message: 'You need to enable HTTP video import in order to enable channel synchronization' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Response) {
|
||||
if (customConfig.live.enabled === false) return true
|
||||
|
||||
if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
|
||||
res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkInvalidVideoStudioConfig (customConfig: CustomConfig, res: express.Response) {
|
||||
if (customConfig.videoStudio.enabled === false) return true
|
||||
|
||||
if (customConfig.videoStudio.enabled === true && customConfig.transcoding.enabled === false) {
|
||||
res.fail({ message: 'You cannot enable video studio if transcoding is not enabled' })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as express from 'express'
|
||||
|
||||
const methodsValidator = (methods: string[]) => {
|
||||
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (methods.includes(req.method) !== true) {
|
||||
return res.sendStatus(405)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
methodsValidator
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isValidRSSFeed } from '../../helpers/custom-validators/feeds.js'
|
||||
import { exists, isIdOrUUIDValid, isIdValid, toCompleteUUID } from '../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanSeeVideo,
|
||||
doesAccountIdExist,
|
||||
doesAccountNameWithHostExist,
|
||||
doesUserFeedTokenCorrespond,
|
||||
doesVideoChannelIdExist,
|
||||
doesVideoChannelNameWithHostExist,
|
||||
doesVideoExist
|
||||
} from './shared/index.js'
|
||||
|
||||
const feedsFormatValidator = [
|
||||
param('format')
|
||||
.optional()
|
||||
.custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
|
||||
query('format')
|
||||
.optional()
|
||||
.custom(isValidRSSFeed).withMessage('Should have a valid format (rss, atom, json)'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
function setFeedFormatContentType (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const format = req.query.format || req.params.format || 'rss'
|
||||
|
||||
let acceptableContentTypes: string[]
|
||||
if (format === 'atom' || format === 'atom1') {
|
||||
acceptableContentTypes = [ 'application/atom+xml', 'application/xml', 'text/xml' ]
|
||||
} else if (format === 'json' || format === 'json1') {
|
||||
acceptableContentTypes = [ 'application/json' ]
|
||||
} else if (format === 'rss' || format === 'rss2') {
|
||||
acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
|
||||
} else {
|
||||
acceptableContentTypes = [ 'application/xml', 'text/xml' ]
|
||||
}
|
||||
|
||||
return feedContentTypeResponse(req, res, next, acceptableContentTypes)
|
||||
}
|
||||
|
||||
function setFeedPodcastContentType (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const acceptableContentTypes = [ 'application/rss+xml', 'application/xml', 'text/xml' ]
|
||||
|
||||
return feedContentTypeResponse(req, res, next, acceptableContentTypes)
|
||||
}
|
||||
|
||||
function feedContentTypeResponse (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction,
|
||||
acceptableContentTypes: string[]
|
||||
) {
|
||||
if (req.accepts(acceptableContentTypes)) {
|
||||
res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
|
||||
} else {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_ACCEPTABLE_406,
|
||||
message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const feedsAccountOrChannelFiltersValidator = [
|
||||
query('accountId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('accountName')
|
||||
.optional(),
|
||||
|
||||
query('videoChannelId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
query('videoChannelName')
|
||||
.optional(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.accountId && !await doesAccountIdExist(req.query.accountId, res)) return
|
||||
if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
|
||||
if (req.query.accountName && !await doesAccountNameWithHostExist(req.query.accountName, res)) return
|
||||
if (req.query.videoChannelName && !await doesVideoChannelNameWithHostExist(req.query.videoChannelName, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const videoFeedsPodcastValidator = [
|
||||
query('videoChannelId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const videoSubscriptionFeedsValidator = [
|
||||
query('accountId')
|
||||
.custom(isIdValid),
|
||||
|
||||
query('token')
|
||||
.custom(exists),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesAccountIdExist(req.query.accountId, res)) return
|
||||
if (!await doesUserFeedTokenCorrespond(res.locals.account.userId, req.query.token, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoCommentsFeedsValidator = [
|
||||
query('videoId')
|
||||
.optional()
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
|
||||
return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
|
||||
}
|
||||
|
||||
if (req.query.videoId) {
|
||||
if (!await doesVideoExist(req.query.videoId, res)) return
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.query.videoId, video: res.locals.videoAll })) return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
feedsFormatValidator,
|
||||
setFeedFormatContentType,
|
||||
setFeedPodcastContentType,
|
||||
feedsAccountOrChannelFiltersValidator,
|
||||
videoFeedsPodcastValidator,
|
||||
videoSubscriptionFeedsValidator,
|
||||
videoCommentsFeedsValidator
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, ServerFollowCreate } from '@peertube/peertube-models'
|
||||
import { isProdInstance } from '@peertube/peertube-node-utils'
|
||||
import { isEachUniqueHandleValid, isFollowStateValid, isRemoteHandleValid } from '@server/helpers/custom-validators/follows.js'
|
||||
import { loadActorUrlOrGetFromWebfinger } from '@server/lib/activitypub/actors/index.js'
|
||||
import { getRemoteNameAndHost } from '@server/lib/activitypub/follow.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { MActorFollowActorsDefault } from '@server/types/models/index.js'
|
||||
import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor.js'
|
||||
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { WEBSERVER } from '../../initializers/constants.js'
|
||||
import { ActorFollowModel } from '../../models/actor/actor-follow.js'
|
||||
import { ActorModel } from '../../models/actor/actor.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const listFollowsValidator = [
|
||||
query('state')
|
||||
.optional()
|
||||
.custom(isFollowStateValid),
|
||||
query('actorType')
|
||||
.optional()
|
||||
.custom(isActorTypeValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const followValidator = [
|
||||
body('hosts')
|
||||
.toArray()
|
||||
.custom(isEachUniqueHostValid).withMessage('Should have an array of unique hosts'),
|
||||
|
||||
body('handles')
|
||||
.toArray()
|
||||
.custom(isEachUniqueHandleValid).withMessage('Should have an array of handles'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// Force https if the administrator wants to follow remote actors
|
||||
if (isProdInstance() && WEBSERVER.SCHEME === 'http') {
|
||||
return res
|
||||
.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500)
|
||||
.json({
|
||||
error: 'Cannot follow on a non HTTPS web server.'
|
||||
})
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: ServerFollowCreate = req.body
|
||||
if (body.hosts.length === 0 && body.handles.length === 0) {
|
||||
|
||||
return res
|
||||
.status(HttpStatusCode.BAD_REQUEST_400)
|
||||
.json({
|
||||
error: 'You must provide at least one handle or one host.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const removeFollowingValidator = [
|
||||
param('hostOrHandle')
|
||||
.custom(value => isHostValid(value) || isRemoteHandleValid(value)),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
|
||||
const { name, host } = getRemoteNameAndHost(req.params.hostOrHandle)
|
||||
const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI({
|
||||
actorId: serverActor.id,
|
||||
targetName: name,
|
||||
targetHost: host
|
||||
})
|
||||
|
||||
if (!follow) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Follow ${req.params.hostOrHandle} not found.`
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.follow = follow
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getFollowerValidator = [
|
||||
param('nameWithHost')
|
||||
.custom(isValidActorHandle),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
let follow: MActorFollowActorsDefault
|
||||
try {
|
||||
const actorUrl = await loadActorUrlOrGetFromWebfinger(req.params.nameWithHost)
|
||||
const actor = await ActorModel.loadByUrl(actorUrl)
|
||||
|
||||
const serverActor = await getServerActor()
|
||||
follow = await ActorFollowModel.loadByActorAndTarget(actor.id, serverActor.id)
|
||||
} catch (err) {
|
||||
logger.warn('Cannot get actor from handle.', { handle: req.params.nameWithHost, err })
|
||||
}
|
||||
|
||||
if (!follow) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Follower ${req.params.nameWithHost} not found.`
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.follow = follow
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const acceptFollowerValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const follow = res.locals.follow
|
||||
if (follow.state !== 'pending' && follow.state !== 'rejected') {
|
||||
return res.fail({ message: 'Follow is not in pending/rejected state.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const rejectFollowerValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const follow = res.locals.follow
|
||||
if (follow.state !== 'pending' && follow.state !== 'accepted') {
|
||||
return res.fail({ message: 'Follow is not in pending/accepted state.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
followValidator,
|
||||
removeFollowingValidator,
|
||||
getFollowerValidator,
|
||||
acceptFollowerValidator,
|
||||
rejectFollowerValidator,
|
||||
listFollowsValidator
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export * from './abuse.js'
|
||||
export * from './account.js'
|
||||
export * from './activitypub/index.js'
|
||||
export * from './actor-image.js'
|
||||
export * from './blocklist.js'
|
||||
export * from './bulk.js'
|
||||
export * from './config.js'
|
||||
export * from './express.js'
|
||||
export * from './feeds.js'
|
||||
export * from './follows.js'
|
||||
export * from './jobs.js'
|
||||
export * from './logs.js'
|
||||
export * from './metrics.js'
|
||||
export * from './object-storage-proxy.js'
|
||||
export * from './oembed.js'
|
||||
export * from './pagination.js'
|
||||
export * from './plugins.js'
|
||||
export * from './redundancy.js'
|
||||
export * from './resumable-upload.js'
|
||||
export * from './search.js'
|
||||
export * from './server.js'
|
||||
export * from './sort.js'
|
||||
export * from './static.js'
|
||||
export * from './themes.js'
|
||||
export * from './webfinger.js'
|
||||
export * from './users/index.js'
|
||||
export * from './videos/index.js'
|
||||
export * from './runners/index.js'
|
||||
@@ -0,0 +1,29 @@
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { isValidJobState, isValidJobType } from '../../helpers/custom-validators/jobs.js'
|
||||
import { loggerTagsFactory } from '../../helpers/logger.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const lTags = loggerTagsFactory('validators', 'jobs')
|
||||
|
||||
const listJobsValidator = [
|
||||
param('state')
|
||||
.optional()
|
||||
.custom(isValidJobState),
|
||||
|
||||
query('jobType')
|
||||
.optional()
|
||||
.custom(isValidJobType),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, lTags())) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
listJobsValidator
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
isValidClientLogLevel,
|
||||
isValidClientLogMessage,
|
||||
isValidClientLogMeta,
|
||||
isValidClientLogStackTrace,
|
||||
isValidClientLogUserAgent,
|
||||
isValidLogLevel
|
||||
} from '../../helpers/custom-validators/logs.js'
|
||||
import { isDateValid } from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const createClientLogValidator = [
|
||||
body('message')
|
||||
.custom(isValidClientLogMessage),
|
||||
|
||||
body('url')
|
||||
.custom(isUrlValid),
|
||||
|
||||
body('level')
|
||||
.custom(isValidClientLogLevel),
|
||||
|
||||
body('stackTrace')
|
||||
.optional()
|
||||
.custom(isValidClientLogStackTrace),
|
||||
|
||||
body('meta')
|
||||
.optional()
|
||||
.custom(isValidClientLogMeta),
|
||||
|
||||
body('userAgent')
|
||||
.optional()
|
||||
.custom(isValidClientLogUserAgent),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (CONFIG.LOG.ACCEPT_CLIENT_LOG !== true) {
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getLogsValidator = [
|
||||
query('startDate')
|
||||
.custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
|
||||
query('level')
|
||||
.optional()
|
||||
.custom(isValidLogLevel),
|
||||
query('tagsOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid one of tags array'),
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have an end date that conforms to ISO 8601'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getAuditLogsValidator = [
|
||||
query('startDate')
|
||||
.custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
getLogsValidator,
|
||||
getAuditLogsValidator,
|
||||
createClientLogValidator
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { isValidPlayerMode } from '@server/helpers/custom-validators/metrics.js'
|
||||
import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { HttpStatusCode, PlaybackMetricCreate } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, doesVideoExist } from './shared/index.js'
|
||||
|
||||
const addPlaybackMetricValidator = [
|
||||
body('resolution')
|
||||
.isInt({ min: 0 }),
|
||||
body('fps')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('p2pPeers')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('p2pEnabled')
|
||||
.isBoolean(),
|
||||
|
||||
body('playerMode')
|
||||
.custom(isValidPlayerMode),
|
||||
|
||||
body('resolutionChanges')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('errors')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('downloadedBytesP2P')
|
||||
.isInt({ min: 0 }),
|
||||
body('downloadedBytesHTTP')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('uploadedBytesP2P')
|
||||
.isInt({ min: 0 }),
|
||||
|
||||
body('videoId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!CONFIG.OPEN_TELEMETRY.METRICS.ENABLED) return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
|
||||
const body: PlaybackMetricCreate = req.body
|
||||
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(body.videoId, res, 'unsafe-only-immutable-attributes')) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
addPlaybackMetricValidator
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import express from 'express'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
const ensurePrivateObjectStorageProxyIsEnabled = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES !== true) {
|
||||
return res.fail({
|
||||
message: 'Private object storage proxy is not enabled',
|
||||
status: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
ensurePrivateObjectStorageProxyIsEnabled
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { join } from 'path'
|
||||
import { HttpStatusCode, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { isTestOrDevInstance } from '@peertube/peertube-node-utils'
|
||||
import { loadVideo } from '@server/lib/model-loaders/index.js'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { isIdOrUUIDValid, isUUIDValid, toCompleteUUID } from '../../helpers/custom-validators/misc.js'
|
||||
import { WEBSERVER } from '../../initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const playlistPaths = [
|
||||
join('videos', 'watch', 'playlist'),
|
||||
join('w', 'p')
|
||||
]
|
||||
|
||||
const videoPaths = [
|
||||
join('videos', 'watch'),
|
||||
'w'
|
||||
]
|
||||
|
||||
function buildUrls (paths: string[]) {
|
||||
return paths.map(p => WEBSERVER.SCHEME + '://' + join(WEBSERVER.HOST, p) + '/')
|
||||
}
|
||||
|
||||
const startPlaylistURLs = buildUrls(playlistPaths)
|
||||
const startVideoURLs = buildUrls(videoPaths)
|
||||
|
||||
const isURLOptions = {
|
||||
require_host: true,
|
||||
require_tld: true
|
||||
}
|
||||
|
||||
// We validate 'localhost', so we don't have the top level domain
|
||||
if (isTestOrDevInstance()) {
|
||||
isURLOptions.require_tld = false
|
||||
}
|
||||
|
||||
const oembedValidator = [
|
||||
query('url')
|
||||
.isURL(isURLOptions),
|
||||
query('maxwidth')
|
||||
.optional()
|
||||
.isInt(),
|
||||
query('maxheight')
|
||||
.optional()
|
||||
.isInt(),
|
||||
query('format')
|
||||
.optional()
|
||||
.isIn([ 'xml', 'json' ]),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.format !== undefined && req.query.format !== 'json') {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_IMPLEMENTED_501,
|
||||
message: 'Requested format is not implemented on server.',
|
||||
data: {
|
||||
format: req.query.format
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const url = req.query.url as string
|
||||
|
||||
let urlPath: string
|
||||
|
||||
try {
|
||||
urlPath = new URL(url).pathname
|
||||
} catch (err) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: err.message,
|
||||
data: {
|
||||
url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isPlaylist = startPlaylistURLs.some(u => url.startsWith(u))
|
||||
const isVideo = isPlaylist ? false : startVideoURLs.some(u => url.startsWith(u))
|
||||
|
||||
const startIsOk = isVideo || isPlaylist
|
||||
|
||||
const parts = urlPath.split('/')
|
||||
|
||||
if (startIsOk === false || parts.length === 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Invalid url.',
|
||||
data: {
|
||||
url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const elementId = toCompleteUUID(parts.pop())
|
||||
if (isIdOrUUIDValid(elementId) === false) {
|
||||
return res.fail({ message: 'Invalid video or playlist id.' })
|
||||
}
|
||||
|
||||
if (isVideo) {
|
||||
const video = await loadVideo(elementId, 'all')
|
||||
|
||||
if (!video) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
video.privacy === VideoPrivacy.PUBLIC ||
|
||||
(video.privacy === VideoPrivacy.UNLISTED && isUUIDValid(elementId) === true)
|
||||
) {
|
||||
res.locals.videoAll = video
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Video is not publicly available'
|
||||
})
|
||||
}
|
||||
|
||||
// Is playlist
|
||||
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
|
||||
if (!videoPlaylist) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
videoPlaylist.privacy === VideoPlaylistPrivacy.PUBLIC ||
|
||||
(videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED && isUUIDValid(elementId))
|
||||
) {
|
||||
res.locals.videoPlaylistSummary = videoPlaylist
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Playlist is not public'
|
||||
})
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
oembedValidator
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { PAGINATION } from '@server/initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const paginationValidator = paginationValidatorBuilder()
|
||||
|
||||
function paginationValidatorBuilder (tags: string[] = []) {
|
||||
return [
|
||||
query('start')
|
||||
.optional()
|
||||
.isInt({ min: 0 }),
|
||||
query('count')
|
||||
.optional()
|
||||
.isInt({ min: 0, max: PAGINATION.GLOBAL.COUNT.MAX }).withMessage(`Should have a number count (max: ${PAGINATION.GLOBAL.COUNT.MAX})`),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
paginationValidator,
|
||||
paginationValidatorBuilder
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import { HttpStatusCode, InstallOrUpdatePlugin, PluginType_Type } from '@peertube/peertube-models'
|
||||
import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isNpmPluginNameValid,
|
||||
isPluginNameValid,
|
||||
isPluginStableOrUnstableVersionValid,
|
||||
isPluginTypeValid
|
||||
} from '../../helpers/custom-validators/plugins.js'
|
||||
import { CONFIG } from '../../initializers/config.js'
|
||||
import { PluginManager } from '../../lib/plugins/plugin-manager.js'
|
||||
import { PluginModel } from '../../models/server/plugin.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const getPluginValidator = (pluginType: PluginType_Type, withVersion = true) => {
|
||||
const validators: (ValidationChain | express.Handler)[] = [
|
||||
param('pluginName')
|
||||
.custom(isPluginNameValid)
|
||||
]
|
||||
|
||||
if (withVersion) {
|
||||
validators.push(
|
||||
param('pluginVersion')
|
||||
.custom(isPluginStableOrUnstableVersionValid)
|
||||
)
|
||||
}
|
||||
|
||||
return validators.concat([
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
|
||||
const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
|
||||
|
||||
if (!plugin) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No plugin found named ' + npmName
|
||||
})
|
||||
}
|
||||
if (withVersion && plugin.version !== req.params.pluginVersion) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.registeredPlugin = plugin
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const getExternalAuthValidator = [
|
||||
param('authName')
|
||||
.custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const plugin = res.locals.registeredPlugin
|
||||
if (!plugin.registerHelpers) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No registered helpers were found for this plugin'
|
||||
})
|
||||
}
|
||||
|
||||
const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
|
||||
if (!externalAuth) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No external auths were found for this plugin'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.externalAuth = externalAuth
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const pluginStaticDirectoryValidator = [
|
||||
param('staticEndpoint')
|
||||
.custom(isSafePath),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const listPluginsValidator = [
|
||||
query('pluginType')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isPluginTypeValid),
|
||||
query('uninstalled')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const installOrUpdatePluginValidator = [
|
||||
body('npmName')
|
||||
.optional()
|
||||
.custom(isNpmPluginNameValid),
|
||||
body('pluginVersion')
|
||||
.optional()
|
||||
.custom(isPluginStableOrUnstableVersionValid),
|
||||
body('path')
|
||||
.optional()
|
||||
.custom(isSafePath),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: InstallOrUpdatePlugin = req.body
|
||||
if (!body.path && !body.npmName) {
|
||||
return res.fail({ message: 'Should have either a npmName or a path' })
|
||||
}
|
||||
if (body.pluginVersion && !body.npmName) {
|
||||
return res.fail({ message: 'Should have a npmName when specifying a pluginVersion' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const uninstallPluginValidator = [
|
||||
body('npmName')
|
||||
.custom(isNpmPluginNameValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const existingPluginValidator = [
|
||||
param('npmName')
|
||||
.custom(isNpmPluginNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const plugin = await PluginModel.loadByNpmName(req.params.npmName)
|
||||
if (!plugin) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Plugin not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.plugin = plugin
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const updatePluginSettingsValidator = [
|
||||
body('settings')
|
||||
.exists(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const listAvailablePluginsValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.exists(),
|
||||
query('pluginType')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isPluginTypeValid),
|
||||
query('currentPeerTubeEngine')
|
||||
.optional()
|
||||
.custom(isPluginStableOrUnstableVersionValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
|
||||
return res.fail({ message: 'Plugin index is not enabled' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
pluginStaticDirectoryValidator,
|
||||
getPluginValidator,
|
||||
updatePluginSettingsValidator,
|
||||
uninstallPluginValidator,
|
||||
listAvailablePluginsValidator,
|
||||
existingPluginValidator,
|
||||
installOrUpdatePluginValidator,
|
||||
listPluginsValidator,
|
||||
getExternalAuthValidator
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies.js'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
toBooleanOrNull,
|
||||
toCompleteUUID,
|
||||
toIntOrNull
|
||||
} from '../../helpers/custom-validators/misc.js'
|
||||
import { isHostValid } from '../../helpers/custom-validators/servers.js'
|
||||
import { VideoRedundancyModel } from '../../models/redundancy/video-redundancy.js'
|
||||
import { ServerModel } from '../../models/server/server.js'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from './shared/index.js'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
|
||||
const videoFileRedundancyGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('resolution')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(exists),
|
||||
param('fps')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(exists),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!canVideoBeFederated(res.locals.onlyVideo)) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
const paramResolution = req.params.resolution as unknown as number // We casted to int above
|
||||
const paramFPS = req.params.fps as unknown as number // We casted to int above
|
||||
|
||||
const videoFile = video.VideoFiles.find(f => {
|
||||
return f.resolution === paramResolution && (!req.params.fps || paramFPS)
|
||||
})
|
||||
|
||||
if (!videoFile) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video file not found.'
|
||||
})
|
||||
}
|
||||
res.locals.videoFile = videoFile
|
||||
|
||||
const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
|
||||
if (!videoRedundancy) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video redundancy not found.'
|
||||
})
|
||||
}
|
||||
res.locals.videoRedundancy = videoRedundancy
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistRedundancyGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('streamingPlaylistType')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(exists),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (!canVideoBeFederated(video)) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above
|
||||
const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType)
|
||||
|
||||
if (!videoStreamingPlaylist) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist not found.'
|
||||
})
|
||||
}
|
||||
res.locals.videoStreamingPlaylist = videoStreamingPlaylist
|
||||
|
||||
const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
|
||||
if (!videoRedundancy) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video redundancy not found.'
|
||||
})
|
||||
}
|
||||
res.locals.videoRedundancy = videoRedundancy
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const updateServerRedundancyValidator = [
|
||||
param('host')
|
||||
.custom(isHostValid),
|
||||
|
||||
body('redundancyAllowed')
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid redundancyAllowed boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const server = await ServerModel.loadByHost(req.params.host)
|
||||
|
||||
if (!server) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: `Server ${req.params.host} not found.`
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.server = server
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const listVideoRedundanciesValidator = [
|
||||
query('target')
|
||||
.custom(isVideoRedundancyTarget),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const addVideoRedundancyValidator = [
|
||||
body('videoId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
if (res.locals.onlyVideo.remote === false) {
|
||||
return res.fail({ message: 'Cannot create a redundancy on a local video' })
|
||||
}
|
||||
|
||||
if (res.locals.onlyVideo.isLive) {
|
||||
return res.fail({ message: 'Cannot create a redundancy of a live video' })
|
||||
}
|
||||
|
||||
const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
|
||||
if (alreadyExists) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'This video is already duplicated by your instance.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const removeVideoRedundancyValidator = [
|
||||
param('redundancyId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const redundancy = await VideoRedundancyModel.loadByIdWithVideo(forceNumber(req.params.redundancyId))
|
||||
if (!redundancy) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video redundancy not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoRedundancy = redundancy
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoFileRedundancyGetValidator,
|
||||
videoPlaylistRedundancyGetValidator,
|
||||
updateServerRedundancyValidator,
|
||||
listVideoRedundanciesValidator,
|
||||
addVideoRedundancyValidator,
|
||||
removeVideoRedundancyValidator
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import express from 'express'
|
||||
import { body, header } from 'express-validator'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
|
||||
export const resumableInitValidator = [
|
||||
body('filename')
|
||||
.exists(),
|
||||
|
||||
header('x-upload-content-length')
|
||||
.isNumeric()
|
||||
.exists()
|
||||
.withMessage('Should specify the file length'),
|
||||
header('x-upload-content-type')
|
||||
.isString()
|
||||
.exists()
|
||||
.withMessage('Should specify the file mimetype'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.debug('Checking resumableInitValidator parameters and headers', {
|
||||
parameters: req.body,
|
||||
headers: req.headers
|
||||
})
|
||||
|
||||
if (areValidationErrors(req, res, { omitLog: true })) return cleanUpReqFiles(req)
|
||||
|
||||
res.locals.uploadVideoFileResumableMetadata = {
|
||||
mimetype: req.headers['x-upload-content-type'] as string,
|
||||
size: +req.headers['x-upload-content-length'],
|
||||
originalname: req.body.filename
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './jobs.js'
|
||||
export * from './registration-token.js'
|
||||
export * from './runners.js'
|
||||
@@ -0,0 +1,60 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { basename } from 'path'
|
||||
import { isSafeFilename } from '@server/helpers/custom-validators/misc.js'
|
||||
import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobStudioTranscodingPayload } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
export const runnerJobGetVideoTranscodingFileValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return
|
||||
|
||||
const runnerJob = res.locals.runnerJob
|
||||
|
||||
if (runnerJob.privatePayload.videoUUID !== res.locals.videoAll.uuid) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Job is not associated to this video',
|
||||
tags: [ ...tags, res.locals.videoAll.uuid ]
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const runnerJobGetVideoStudioTaskFileValidator = [
|
||||
param('filename').custom(v => isSafeFilename(v)),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const filename = req.params.filename
|
||||
|
||||
const payload = res.locals.runnerJob.payload as RunnerJobStudioTranscodingPayload
|
||||
|
||||
const found = Array.isArray(payload?.tasks) && payload.tasks.some(t => {
|
||||
if (hasVideoStudioTaskFile(t)) {
|
||||
return basename(t.options.file) === filename
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!found) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'File is not associated to this edition task',
|
||||
tags: [ ...tags, res.locals.videoAll.uuid ]
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,217 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
RunnerJobLiveRTMPHLSTranscodingPrivatePayload,
|
||||
RunnerJobState,
|
||||
RunnerJobStateType,
|
||||
RunnerJobSuccessBody,
|
||||
RunnerJobUpdateBody,
|
||||
ServerErrorCode
|
||||
} from '@peertube/peertube-models'
|
||||
import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isRunnerJobAbortReasonValid,
|
||||
isRunnerJobArrayOfStateValid,
|
||||
isRunnerJobErrorMessageValid,
|
||||
isRunnerJobProgressValid,
|
||||
isRunnerJobSuccessPayloadValid,
|
||||
isRunnerJobTokenValid,
|
||||
isRunnerJobUpdatePayloadValid
|
||||
} from '@server/helpers/custom-validators/runners/jobs.js'
|
||||
import { isRunnerTokenValid } from '@server/helpers/custom-validators/runners/runners.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { LiveManager } from '@server/lib/live/index.js'
|
||||
import { runnerJobCanBeCancelled } from '@server/lib/runners/index.js'
|
||||
import { RunnerJobModel } from '@server/models/runner/runner-job.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { areValidationErrors } from '../shared/index.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
export const acceptRunnerJobValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (res.locals.runnerJob.state !== RunnerJobState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This runner job is not in pending state',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const abortRunnerJobValidator = [
|
||||
body('reason').custom(isRunnerJobAbortReasonValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const updateRunnerJobValidator = [
|
||||
body('progress').optional().custom(isRunnerJobProgressValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return cleanUpReqFiles(req)
|
||||
|
||||
const body = req.body as RunnerJobUpdateBody
|
||||
const job = res.locals.runnerJob
|
||||
|
||||
if (isRunnerJobUpdatePayloadValid(body.payload, job.type, req.files) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Payload is invalid',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
if (res.locals.runnerJob.type === 'live-rtmp-hls-transcoding') {
|
||||
const privatePayload = job.privatePayload as RunnerJobLiveRTMPHLSTranscodingPrivatePayload
|
||||
|
||||
if (!LiveManager.Instance.hasSession(privatePayload.sessionId)) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
type: ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE,
|
||||
message: 'Session of this live ended',
|
||||
tags
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const errorRunnerJobValidator = [
|
||||
body('message').custom(isRunnerJobErrorMessageValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const successRunnerJobValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const body = req.body as RunnerJobSuccessBody
|
||||
|
||||
if (isRunnerJobSuccessPayloadValid(body.payload, res.locals.runnerJob.type, req.files) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Payload is invalid',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const cancelRunnerJobValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const runnerJob = res.locals.runnerJob
|
||||
|
||||
if (runnerJobCanBeCancelled(runnerJob) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot cancel this job that is not in "pending", "processing" or "waiting for parent job" state',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listRunnerJobsValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('stateOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isRunnerJobArrayOfStateValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const runnerJobGetValidator = [
|
||||
param('jobUUID').custom(isUUIDValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const runnerJob = await RunnerJobModel.loadWithRunner(req.params.jobUUID)
|
||||
|
||||
if (!runnerJob) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown runner job',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerJob = runnerJob
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function jobOfRunnerGetValidatorFactory (allowedStates: RunnerJobStateType[]) {
|
||||
return [
|
||||
param('jobUUID').custom(isUUIDValid),
|
||||
|
||||
body('runnerToken').custom(isRunnerTokenValid),
|
||||
body('jobToken').custom(isRunnerJobTokenValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return cleanUpReqFiles(req)
|
||||
|
||||
const runnerJob = await RunnerJobModel.loadByRunnerAndJobTokensWithRunner({
|
||||
uuid: req.params.jobUUID,
|
||||
runnerToken: req.body.runnerToken,
|
||||
jobToken: req.body.jobToken
|
||||
})
|
||||
|
||||
if (!runnerJob) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown runner job',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
if (!allowedStates.includes(runnerJob.state)) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
type: ServerErrorCode.RUNNER_JOB_NOT_IN_PROCESSING_STATE,
|
||||
message: 'Job is not in "processing" state',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerJob = runnerJob
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { areValidationErrors } from '../shared/utils.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
const deleteRegistrationTokenValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const registrationToken = await RunnerRegistrationTokenModel.load(forceNumber(req.params.id))
|
||||
|
||||
if (!registrationToken) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration token not found',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerRegistrationToken = registrationToken
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
deleteRegistrationTokenValidator
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isRunnerDescriptionValid,
|
||||
isRunnerNameValid,
|
||||
isRunnerRegistrationTokenValid,
|
||||
isRunnerTokenValid
|
||||
} from '@server/helpers/custom-validators/runners/runners.js'
|
||||
import { RunnerModel } from '@server/models/runner/runner.js'
|
||||
import { RunnerRegistrationTokenModel } from '@server/models/runner/runner-registration-token.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, RegisterRunnerBody, ServerErrorCode } from '@peertube/peertube-models'
|
||||
import { areValidationErrors } from '../shared/utils.js'
|
||||
|
||||
const tags = [ 'runner' ]
|
||||
|
||||
const registerRunnerValidator = [
|
||||
body('registrationToken').custom(isRunnerRegistrationTokenValid),
|
||||
body('name').custom(isRunnerNameValid),
|
||||
body('description').optional().custom(isRunnerDescriptionValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const body: RegisterRunnerBody = req.body
|
||||
|
||||
const runnerRegistrationToken = await RunnerRegistrationTokenModel.loadByRegistrationToken(body.registrationToken)
|
||||
|
||||
if (!runnerRegistrationToken) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Registration token is invalid',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
const existing = await RunnerModel.loadByName(body.name)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This runner name already exists on this instance',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runnerRegistrationToken = runnerRegistrationToken
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const deleteRunnerValidator = [
|
||||
param('runnerId').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const runner = await RunnerModel.load(forceNumber(req.params.runnerId))
|
||||
|
||||
if (!runner) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Runner not found',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runner = runner
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getRunnerFromTokenValidator = [
|
||||
body('runnerToken').custom(isRunnerTokenValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const runner = await RunnerModel.loadByToken(req.body.runnerToken)
|
||||
|
||||
if (!runner) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown runner token',
|
||||
type: ServerErrorCode.UNKNOWN_RUNNER_TOKEN,
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.runner = runner
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
registerRunnerValidator,
|
||||
deleteRunnerValidator,
|
||||
getRunnerFromTokenValidator
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { isSearchTargetValid } from '@server/helpers/custom-validators/search.js'
|
||||
import { isHostValid } from '@server/helpers/custom-validators/servers.js'
|
||||
import { areUUIDsValid, isDateValid, isNotEmptyStringArray, toCompleteUUIDs } from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const videosSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
query('host')
|
||||
.optional()
|
||||
.custom(isHostValid),
|
||||
|
||||
query('startDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a start date that conforms to ISO 8601'),
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a end date that conforms to ISO 8601'),
|
||||
|
||||
query('originallyPublishedStartDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a published start date that conforms to ISO 8601'),
|
||||
query('originallyPublishedEndDate')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a published end date that conforms to ISO 8601'),
|
||||
|
||||
query('durationMin')
|
||||
.optional()
|
||||
.isInt(),
|
||||
query('durationMax')
|
||||
.optional()
|
||||
.isInt(),
|
||||
|
||||
query('uuids')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toCompleteUUIDs)
|
||||
.custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
|
||||
|
||||
query('searchTarget')
|
||||
.optional()
|
||||
.custom(isSearchTargetValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoChannelsListSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
query('host')
|
||||
.optional()
|
||||
.custom(isHostValid),
|
||||
|
||||
query('searchTarget')
|
||||
.optional()
|
||||
.custom(isSearchTargetValid),
|
||||
|
||||
query('handles')
|
||||
.optional()
|
||||
.toArray()
|
||||
.custom(isNotEmptyStringArray).withMessage('Should have valid array of handles'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistsListSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
query('host')
|
||||
.optional()
|
||||
.custom(isHostValid),
|
||||
|
||||
query('searchTarget')
|
||||
.optional()
|
||||
.custom(isSearchTargetValid),
|
||||
|
||||
query('uuids')
|
||||
.optional()
|
||||
.toArray()
|
||||
.customSanitizer(toCompleteUUIDs)
|
||||
.custom(areUUIDsValid).withMessage('Should have valid array of uuid'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosSearchValidator,
|
||||
videoChannelsListSearchValidator,
|
||||
videoPlaylistsListSearchValidator
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isHostValid, isValidContactBody } from '../../helpers/custom-validators/servers.js'
|
||||
import { isUserDisplayNameValid } from '../../helpers/custom-validators/users.js'
|
||||
import { logger } from '../../helpers/logger.js'
|
||||
import { CONFIG, isEmailEnabled } from '../../initializers/config.js'
|
||||
import { Redis } from '../../lib/redis.js'
|
||||
import { ServerModel } from '../../models/server/server.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const serverGetValidator = [
|
||||
body('host').custom(isHostValid).withMessage('Should have a valid host'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const server = await ServerModel.loadByHost(req.body.host)
|
||||
if (!server) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Server host not found.'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.server = server
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const contactAdministratorValidator = [
|
||||
body('fromName')
|
||||
.custom(isUserDisplayNameValid),
|
||||
body('fromEmail')
|
||||
.isEmail(),
|
||||
body('body')
|
||||
.custom(isValidContactBody),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.CONTACT_FORM.ENABLED === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Contact form is not enabled on this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
if (isEmailEnabled() === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'SMTP is not configured on this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
|
||||
logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'You already sent a contact form recently.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
serverGetValidator,
|
||||
contactAdministratorValidator
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Response } from 'express'
|
||||
import { AbuseModel } from '@server/models/abuse/abuse.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
|
||||
async function doesAbuseExist (abuseId: number | string, res: Response) {
|
||||
const abuse = await AbuseModel.loadByIdWithReporter(forceNumber(abuseId))
|
||||
|
||||
if (!abuse) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Abuse not found'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.abuse = abuse
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesAbuseExist
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Response } from 'express'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MAccountDefault } from '@server/types/models/index.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) {
|
||||
const promise = AccountModel.load(forceNumber(id))
|
||||
|
||||
return doesAccountExist(promise, res, sendNotFound)
|
||||
}
|
||||
|
||||
function doesLocalAccountNameExist (name: string, res: Response, sendNotFound = true) {
|
||||
const promise = AccountModel.loadLocalByName(name)
|
||||
|
||||
return doesAccountExist(promise, res, sendNotFound)
|
||||
}
|
||||
|
||||
function doesAccountNameWithHostExist (nameWithDomain: string, res: Response, sendNotFound = true) {
|
||||
const promise = AccountModel.loadByNameWithHost(nameWithDomain)
|
||||
|
||||
return doesAccountExist(promise, res, sendNotFound)
|
||||
}
|
||||
|
||||
async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sendNotFound: boolean) {
|
||||
const account = await p
|
||||
|
||||
if (!account) {
|
||||
if (sendNotFound === true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Account not found'
|
||||
})
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.account = account
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) {
|
||||
const user = await UserModel.loadByIdWithChannels(forceNumber(id))
|
||||
|
||||
if (token !== user.feedToken) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'User and token mismatch'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesAccountIdExist,
|
||||
doesLocalAccountNameExist,
|
||||
doesAccountNameWithHostExist,
|
||||
doesAccountExist,
|
||||
doesUserFeedTokenCorrespond
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export * from './abuses.js'
|
||||
export * from './accounts.js'
|
||||
export * from './users.js'
|
||||
export * from './utils.js'
|
||||
export * from './video-blacklists.js'
|
||||
export * from './video-captions.js'
|
||||
export * from './video-channels.js'
|
||||
export * from './video-channel-syncs.js'
|
||||
export * from './video-comments.js'
|
||||
export * from './video-imports.js'
|
||||
export * from './video-ownerships.js'
|
||||
export * from './video-playlists.js'
|
||||
export * from './video-passwords.js'
|
||||
export * from './videos.js'
|
||||
@@ -0,0 +1,84 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
||||
import { ActorModel } from '@server/models/actor/actor.js'
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { MAccountId, MUserAccountId, MUserDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
|
||||
export function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res)
|
||||
}
|
||||
|
||||
export function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse)
|
||||
}
|
||||
|
||||
export async function checkUserNameOrEmailDoNotAlreadyExist (username: string, email: string, res: express.Response) {
|
||||
const user = await UserModel.loadByUsernameOrEmail(username, email)
|
||||
|
||||
if (user) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'User with this username or email already exists.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const actor = await ActorModel.loadLocalByName(username)
|
||||
if (actor) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) {
|
||||
const user = await finder()
|
||||
|
||||
if (!user) {
|
||||
if (abortResponse === true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'User not found'
|
||||
})
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkUserCanManageAccount (options: {
|
||||
user: MUserAccountId
|
||||
account: MAccountId
|
||||
specialRight: UserRightType
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, account, specialRight, res } = options
|
||||
|
||||
if (account.id === user.Account.id) return true
|
||||
if (specialRight && user.hasRight(specialRight) === true) return true
|
||||
|
||||
if (!specialRight) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only the owner of this account can manage this account resource.'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only a user with sufficient right can access this account resource.'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import express from 'express'
|
||||
import { param, validationResult } from 'express-validator'
|
||||
import { isIdOrUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
|
||||
function areValidationErrors (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
options: {
|
||||
omitLog?: boolean
|
||||
omitBodyLog?: boolean
|
||||
tags?: (number | string)[]
|
||||
} = {}) {
|
||||
const { omitLog = false, omitBodyLog = false, tags = [] } = options
|
||||
|
||||
if (!omitLog) {
|
||||
logger.debug(
|
||||
'Checking %s - %s parameters',
|
||||
req.method, req.originalUrl,
|
||||
{
|
||||
body: omitBodyLog
|
||||
? 'omitted'
|
||||
: req.body,
|
||||
params: req.params,
|
||||
query: req.query,
|
||||
files: req.files,
|
||||
tags
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const errors = validationResult(req)
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
|
||||
|
||||
res.fail({
|
||||
message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '),
|
||||
instance: req.originalUrl,
|
||||
data: {
|
||||
'invalid-params': errors.mapped()
|
||||
}
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isValidVideoIdParam (paramName: string) {
|
||||
return param(paramName)
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id (id, short UUID or UUID)')
|
||||
}
|
||||
|
||||
function isValidPlaylistIdParam (paramName: string) {
|
||||
return param(paramName)
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id (id, short UUID or UUID)')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
areValidationErrors,
|
||||
isValidVideoIdParam,
|
||||
isValidPlaylistIdParam
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Response } from 'express'
|
||||
import { VideoBlacklistModel } from '@server/models/video/video-blacklist.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoBlacklistExist (videoId: number, res: Response) {
|
||||
const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
|
||||
|
||||
if (videoBlacklist === null) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Blacklisted video not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoBlacklist = videoBlacklist
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoBlacklistExist
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Response } from 'express'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { MVideoId } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoCaptionExist (video: MVideoId, language: string, res: Response) {
|
||||
const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
|
||||
|
||||
if (!videoCaption) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video caption not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCaption = videoCaption
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoCaptionExist
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import express from 'express'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoChannelSyncIdExist (id: number, res: express.Response) {
|
||||
const sync = await VideoChannelSyncModel.loadWithChannel(+id)
|
||||
|
||||
if (!sync) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video channel sync not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannelSync = sync
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoChannelSyncIdExist
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import express from 'express'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoChannelIdExist (id: number, res: express.Response) {
|
||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(+id)
|
||||
|
||||
return processVideoChannelExist(videoChannel, res)
|
||||
}
|
||||
|
||||
async function doesVideoChannelNameWithHostExist (nameWithDomain: string, res: express.Response) {
|
||||
const videoChannel = await VideoChannelModel.loadByNameWithHostAndPopulateAccount(nameWithDomain)
|
||||
|
||||
return processVideoChannelExist(videoChannel, res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoChannelIdExist,
|
||||
doesVideoChannelNameWithHostExist
|
||||
}
|
||||
|
||||
function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
|
||||
if (!videoChannel) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video channel not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannel = videoChannel
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import express from 'express'
|
||||
import { VideoCommentModel } from '@server/models/video/video-comment.js'
|
||||
import { MVideoId } from '@server/types/models/index.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoComment = await VideoCommentModel.loadById(id)
|
||||
|
||||
if (!videoComment) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video comment thread not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.videoId !== video.id) {
|
||||
res.fail({
|
||||
type: ServerErrorCode.COMMENT_NOT_ASSOCIATED_TO_VIDEO,
|
||||
message: 'Video comment is not associated to this video.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.inReplyToCommentId !== null) {
|
||||
res.fail({ message: 'Video comment is not a thread.' })
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCommentThread = videoComment
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
|
||||
|
||||
if (!videoComment) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video comment thread not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.videoId !== video.id) {
|
||||
res.fail({
|
||||
type: ServerErrorCode.COMMENT_NOT_ASSOCIATED_TO_VIDEO,
|
||||
message: 'Video comment is not associated to this video.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCommentFull = videoComment
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesCommentIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
|
||||
|
||||
if (!videoComment) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video comment thread not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoCommentFull = videoComment
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
doesVideoCommentThreadExist,
|
||||
doesVideoCommentExist,
|
||||
doesCommentIdExist
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import express from 'express'
|
||||
import { VideoImportModel } from '@server/models/video/video-import.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesVideoImportExist (id: number, res: express.Response) {
|
||||
const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
|
||||
|
||||
if (!videoImport) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video import not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoImport = videoImport
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
doesVideoImportExist
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import express from 'express'
|
||||
import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership.js'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
|
||||
|
||||
if (!videoChangeOwnership) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video change ownership not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChangeOwnership = videoChangeOwnership
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
doesChangeVideoOwnershipExist
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import express from 'express'
|
||||
import { HttpStatusCode, UserRight, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { header } from 'express-validator'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
|
||||
function isValidVideoPasswordHeader () {
|
||||
return header('x-peertube-video-password')
|
||||
.optional()
|
||||
.isString()
|
||||
}
|
||||
|
||||
function checkVideoIsPasswordProtected (res: express.Response) {
|
||||
const video = getVideoWithAttributes(res)
|
||||
if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video is not password protected'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function doesVideoPasswordExist (idArg: number | string, res: express.Response) {
|
||||
const video = getVideoWithAttributes(res)
|
||||
const id = forceNumber(idArg)
|
||||
const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id })
|
||||
|
||||
if (!videoPassword) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video password not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoPassword = videoPassword
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function isVideoPasswordDeletable (res: express.Response) {
|
||||
const user = res.locals.oauth.token.User
|
||||
const userAccount = user.Account
|
||||
const video = res.locals.videoAll
|
||||
|
||||
// Check if the user who did the request is able to delete the video passwords
|
||||
if (
|
||||
user.hasRight(UserRight.UPDATE_ANY_VIDEO) === false && // Not a moderator
|
||||
video.VideoChannel.accountId !== userAccount.id // Not the video owner
|
||||
) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot remove passwords of another user\'s video'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const passwordCount = await VideoPasswordModel.countByVideoId(video.id)
|
||||
|
||||
if (passwordCount <= 1) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete the last password of the protected video'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export {
|
||||
isValidVideoPasswordHeader,
|
||||
checkVideoIsPasswordProtected as isVideoPasswordProtected,
|
||||
doesVideoPasswordExist,
|
||||
isVideoPasswordDeletable
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import express from 'express'
|
||||
import { VideoPlaylistModel } from '@server/models/video/video-playlist.js'
|
||||
import { MVideoPlaylist } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
export type VideoPlaylistFetchType = 'summary' | 'all'
|
||||
async function doesVideoPlaylistExist (id: number | string, res: express.Response, fetchType: VideoPlaylistFetchType = 'summary') {
|
||||
if (fetchType === 'summary') {
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
|
||||
res.locals.videoPlaylistSummary = videoPlaylist
|
||||
|
||||
return handleVideoPlaylist(videoPlaylist, res)
|
||||
}
|
||||
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
|
||||
res.locals.videoPlaylistFull = videoPlaylist
|
||||
|
||||
return handleVideoPlaylist(videoPlaylist, res)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
doesVideoPlaylistExist
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
|
||||
if (!videoPlaylist) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, UserRightType, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
import { VideoLoadType, loadVideo } from '@server/lib/model-loaders/index.js'
|
||||
import { isUserQuotaValid } from '@server/lib/user.js'
|
||||
import { VideoTokensManager } from '@server/lib/video-tokens-manager.js'
|
||||
import { authenticatePromise } from '@server/middlewares/auth.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import {
|
||||
MUser,
|
||||
MUserAccountId,
|
||||
MUserId,
|
||||
MVideo,
|
||||
MVideoAccountLight,
|
||||
MVideoFormattableDetails,
|
||||
MVideoFullLight,
|
||||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnailBlacklist,
|
||||
MVideoUUID,
|
||||
MVideoWithRights
|
||||
} from '@server/types/models/index.js'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
export async function doesVideoExist (id: number | string, res: Response, fetchType: VideoLoadType = 'all') {
|
||||
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||
|
||||
const video = await loadVideo(id, fetchType, userId)
|
||||
|
||||
if (!video) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video not found'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
switch (fetchType) {
|
||||
case 'for-api':
|
||||
res.locals.videoAPI = video as MVideoFormattableDetails
|
||||
break
|
||||
|
||||
case 'all':
|
||||
res.locals.videoAll = video as MVideoFullLight
|
||||
break
|
||||
|
||||
case 'unsafe-only-immutable-attributes':
|
||||
res.locals.onlyImmutableVideo = video as MVideoImmutable
|
||||
break
|
||||
|
||||
case 'id':
|
||||
res.locals.videoId = video as MVideoId
|
||||
break
|
||||
|
||||
case 'only-video-and-blacklist':
|
||||
res.locals.onlyVideo = video as MVideoThumbnailBlacklist
|
||||
break
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
|
||||
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'VideoFile matching Video not found'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAccountId, res: Response) {
|
||||
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
|
||||
|
||||
if (videoChannel === null) {
|
||||
res.fail({ message: 'Unknown video "video channel" for this instance.' })
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't check account id if the user can update any video
|
||||
if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) === true) {
|
||||
res.locals.videoChannel = videoChannel
|
||||
return true
|
||||
}
|
||||
|
||||
if (videoChannel.Account.id !== user.Account.id) {
|
||||
res.fail({
|
||||
message: 'Unknown video "video channel" for this account.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannel = videoChannel
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanSeeVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
video: MVideo
|
||||
}) {
|
||||
const { req, res, video, paramId } = options
|
||||
|
||||
if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) {
|
||||
return checkCanSeeUserAuthVideo({ req, res, video })
|
||||
}
|
||||
|
||||
if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
|
||||
return checkCanSeePasswordProtectedVideo({ req, res, video })
|
||||
}
|
||||
|
||||
if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) {
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error('Unknown video privacy when checking video right ' + video.url)
|
||||
}
|
||||
|
||||
export async function checkCanSeeUserAuthVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideoId | MVideoWithRights
|
||||
}) {
|
||||
const { req, res, video } = options
|
||||
|
||||
const fail = () => {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot fetch information of private/internal/blocked video'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
await authenticatePromise({ req, res })
|
||||
|
||||
const user = res.locals.oauth?.token.User
|
||||
if (!user) return fail()
|
||||
|
||||
const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
|
||||
|
||||
const privacy = videoWithRights.privacy
|
||||
|
||||
if (privacy === VideoPrivacy.INTERNAL) {
|
||||
// We know we have a user
|
||||
return true
|
||||
}
|
||||
|
||||
if (videoWithRights.isBlacklisted()) {
|
||||
if (canUserAccessVideo(user, videoWithRights, UserRight.MANAGE_VIDEO_BLACKLIST)) return true
|
||||
|
||||
return fail()
|
||||
}
|
||||
|
||||
if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
|
||||
if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
|
||||
|
||||
return fail()
|
||||
}
|
||||
|
||||
// Should not happen
|
||||
return fail()
|
||||
}
|
||||
|
||||
export async function checkCanSeePasswordProtectedVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideo
|
||||
}) {
|
||||
const { req, res, video } = options
|
||||
|
||||
const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
|
||||
|
||||
const videoPassword = req.header('x-peertube-video-password')
|
||||
|
||||
if (!exists(videoPassword)) {
|
||||
const errorMessage = 'Please provide a password to access this password protected video'
|
||||
const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD
|
||||
|
||||
if (req.header('authorization')) {
|
||||
await authenticatePromise({ req, res, errorMessage, errorStatus: HttpStatusCode.FORBIDDEN_403, errorType })
|
||||
const user = res.locals.oauth?.token.User
|
||||
|
||||
if (canUserAccessVideo(user, videoWithRights, UserRight.SEE_ALL_VIDEOS)) return true
|
||||
}
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
type: errorType,
|
||||
message: errorMessage
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (await VideoPasswordModel.isACorrectPassword({ videoId: video.id, password: videoPassword })) return true
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
type: ServerErrorCode.INCORRECT_VIDEO_PASSWORD,
|
||||
message: 'Incorrect video password. Access to the video is denied.'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function canUserAccessVideo (user: MUser, video: MVideoWithRights | MVideoAccountLight, right: UserRightType) {
|
||||
const isOwnedByUser = video.VideoChannel.Account.userId === user.id
|
||||
|
||||
return isOwnedByUser || user.hasRight(right)
|
||||
}
|
||||
|
||||
export async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
||||
return video.VideoChannel?.Account?.userId
|
||||
? video
|
||||
: VideoModel.loadFull(video.id)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanAccessVideoStaticFiles (options: {
|
||||
video: MVideo
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
}) {
|
||||
const { video, req, res } = options
|
||||
|
||||
if (res.locals.oauth?.token.User || exists(req.header('x-peertube-video-password'))) {
|
||||
return checkCanSeeVideo(options)
|
||||
}
|
||||
|
||||
assignVideoTokenIfNeeded(req, res, video)
|
||||
|
||||
if (res.locals.videoFileToken) return true
|
||||
if (!video.hasPrivateStaticPath()) return true
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
export async function checkCanAccessVideoSourceFile (options: {
|
||||
videoId: number
|
||||
req: Request
|
||||
res: Response
|
||||
}) {
|
||||
const { req, res, videoId } = options
|
||||
|
||||
const video = await VideoModel.loadFull(videoId)
|
||||
|
||||
if (res.locals.oauth?.token.User) {
|
||||
if (canUserAccessVideo(res.locals.oauth.token.User, video, UserRight.SEE_ALL_VIDEOS) === true) return true
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
assignVideoTokenIfNeeded(req, res, video)
|
||||
if (res.locals.videoFileToken) return true
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
function assignVideoTokenIfNeeded (req: Request, res: Response, video: MVideoUUID) {
|
||||
const videoFileToken = req.query.videoFileToken
|
||||
|
||||
if (videoFileToken && VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
||||
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
||||
|
||||
res.locals.videoFileToken = { user }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRightType, res: Response, onlyOwned = true) {
|
||||
if (onlyOwned && video.isOwned() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage a video of another server.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const account = video.VideoChannel.Account
|
||||
if (user.hasRight(right) === false && account.userId !== user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage a video of another user.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkUserQuota (user: MUserId, videoFileSize: number, res: Response) {
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
message: 'The user video quota is exceeded with this video.',
|
||||
type: ServerErrorCode.QUOTA_REACHED
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { SORTABLE_COLUMNS } from '../../initializers/constants.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
export const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
|
||||
export const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
|
||||
export const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
|
||||
export const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
|
||||
export const videosSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS)
|
||||
export const videoImportsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_IMPORTS)
|
||||
export const videosSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEOS_SEARCH)
|
||||
export const videoChannelsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS_SEARCH)
|
||||
export const videoPlaylistsSearchSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS_SEARCH)
|
||||
export const videoCommentsValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENTS)
|
||||
export const videoCommentThreadsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_COMMENT_THREADS)
|
||||
export const videoRatesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_RATES)
|
||||
export const blacklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.BLACKLISTS)
|
||||
export const videoChannelsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNELS)
|
||||
export const instanceFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWERS)
|
||||
export const instanceFollowingSortValidator = checkSortFactory(SORTABLE_COLUMNS.INSTANCE_FOLLOWING)
|
||||
export const userSubscriptionsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_SUBSCRIPTIONS)
|
||||
export const accountsBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS_BLOCKLIST)
|
||||
export const serversBlocklistSortValidator = checkSortFactory(SORTABLE_COLUMNS.SERVERS_BLOCKLIST)
|
||||
export const userNotificationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_NOTIFICATIONS)
|
||||
export const videoPlaylistsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PLAYLISTS)
|
||||
export const pluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.PLUGINS)
|
||||
export const availablePluginsSortValidator = checkSortFactory(SORTABLE_COLUMNS.AVAILABLE_PLUGINS)
|
||||
export const videoRedundanciesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_REDUNDANCIES)
|
||||
export const videoChannelSyncsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_SYNCS)
|
||||
export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_PASSWORDS)
|
||||
|
||||
export const watchedWordsListsSortValidator = checkSortFactory(SORTABLE_COLUMNS.WATCHED_WORDS_LISTS)
|
||||
|
||||
export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
|
||||
export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
|
||||
|
||||
export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS)
|
||||
|
||||
export const runnersSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNERS)
|
||||
export const runnerRegistrationTokensSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_REGISTRATION_TOKENS)
|
||||
export const runnerJobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.RUNNER_JOBS)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkSortFactory (columns: string[], tags: string[] = []) {
|
||||
return checkSort(createSortableColumns(columns), tags)
|
||||
}
|
||||
|
||||
function checkSort (sortableColumns: string[], tags: string[] = []) {
|
||||
return [
|
||||
query('sort')
|
||||
.optional()
|
||||
.isIn(sortableColumns),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function createSortableColumns (sortableColumns: string[]) {
|
||||
const sortableColumnDesc = sortableColumns.map(sortableColumn => '-' + sortableColumn)
|
||||
|
||||
return sortableColumns.concat(sortableColumnDesc)
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { exists, isSafePeerTubeFilenameWithoutExtension, isUUIDValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { LRU_CACHE } from '@server/initializers/constants.js'
|
||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MStreamingPlaylist, MVideoFile, MVideoThumbnailBlacklist } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
import { basename, dirname } from 'path'
|
||||
import { areValidationErrors, checkCanAccessVideoStaticFiles, isValidVideoPasswordHeader } from './shared/index.js'
|
||||
|
||||
type LRUValue = {
|
||||
allowed: boolean
|
||||
video?: MVideoThumbnailBlacklist
|
||||
file?: MVideoFile
|
||||
playlist?: MStreamingPlaylist }
|
||||
|
||||
const staticFileTokenBypass = new LRUCache<string, LRUValue>({
|
||||
max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE,
|
||||
ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL
|
||||
})
|
||||
|
||||
const ensureCanAccessVideoPrivateWebVideoFiles = [
|
||||
query('videoFileToken').optional().custom(exists),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const token = extractTokenOrDie(req, res)
|
||||
if (!token) return
|
||||
|
||||
const cacheKey = token + '-' + req.originalUrl
|
||||
|
||||
if (staticFileTokenBypass.has(cacheKey)) {
|
||||
const { allowed, file, video } = staticFileTokenBypass.get(cacheKey)
|
||||
|
||||
if (allowed === true) {
|
||||
res.locals.onlyVideo = video
|
||||
res.locals.videoFile = file
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
const result = await isWebVideoAllowed(req, res)
|
||||
|
||||
staticFileTokenBypass.set(cacheKey, result)
|
||||
|
||||
if (result.allowed !== true) return
|
||||
|
||||
res.locals.onlyVideo = result.video
|
||||
res.locals.videoFile = result.file
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const ensureCanAccessPrivateVideoHLSFiles = [
|
||||
query('videoFileToken')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('reinjectVideoFileToken')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should be a valid reinjectVideoFileToken boolean'),
|
||||
|
||||
query('playlistName')
|
||||
.optional()
|
||||
.customSanitizer(isSafePeerTubeFilenameWithoutExtension),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const videoUUID = basename(dirname(req.originalUrl))
|
||||
|
||||
if (!isUUIDValid(videoUUID)) {
|
||||
logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl)
|
||||
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
const token = extractTokenOrDie(req, res)
|
||||
if (!token) return
|
||||
|
||||
const cacheKey = token + '-' + videoUUID
|
||||
|
||||
if (staticFileTokenBypass.has(cacheKey)) {
|
||||
const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey)
|
||||
|
||||
if (allowed === true) {
|
||||
res.locals.onlyVideo = video
|
||||
res.locals.videoFile = file
|
||||
res.locals.videoStreamingPlaylist = playlist
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
}
|
||||
|
||||
const result = await isHLSAllowed(req, res, videoUUID)
|
||||
|
||||
staticFileTokenBypass.set(cacheKey, result)
|
||||
|
||||
if (result.allowed !== true) return
|
||||
|
||||
res.locals.onlyVideo = result.video
|
||||
res.locals.videoFile = result.file
|
||||
res.locals.videoStreamingPlaylist = result.playlist
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
ensureCanAccessPrivateVideoHLSFiles, ensureCanAccessVideoPrivateWebVideoFiles
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function isWebVideoAllowed (req: express.Request, res: express.Response) {
|
||||
const filename = basename(req.path)
|
||||
|
||||
const file = await VideoFileModel.loadWithVideoByFilename(filename)
|
||||
if (!file) {
|
||||
logger.debug('Unknown static file %s to serve', req.originalUrl, { filename })
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return { allowed: false }
|
||||
}
|
||||
|
||||
const video = await VideoModel.loadWithBlacklist(file.getVideo().id)
|
||||
|
||||
return {
|
||||
file,
|
||||
video,
|
||||
allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
|
||||
}
|
||||
}
|
||||
|
||||
async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) {
|
||||
const filename = basename(req.path)
|
||||
|
||||
const video = await VideoModel.loadAndPopulateAccountAndFiles(videoUUID)
|
||||
|
||||
if (!video) {
|
||||
logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID })
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return { allowed: false }
|
||||
}
|
||||
|
||||
const file = await VideoFileModel.loadByFilename(filename)
|
||||
|
||||
return {
|
||||
file,
|
||||
video,
|
||||
playlist: video.getHLSPlaylist(),
|
||||
allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid })
|
||||
}
|
||||
}
|
||||
|
||||
function extractTokenOrDie (req: express.Request, res: express.Response) {
|
||||
const token = req.header('x-peertube-video-password') || req.query.videoFileToken || res.locals.oauth?.token.accessToken
|
||||
|
||||
if (!token) {
|
||||
return res.fail({
|
||||
message: 'Video password header, video file token query parameter and bearer token are all missing', //
|
||||
status: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isSafePath } from '../../helpers/custom-validators/misc.js'
|
||||
import { isPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins.js'
|
||||
import { PluginManager } from '../../lib/plugins/plugin-manager.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const serveThemeCSSValidator = [
|
||||
param('themeName')
|
||||
.custom(isPluginNameValid),
|
||||
param('themeVersion')
|
||||
.custom(isPluginStableOrUnstableVersionValid),
|
||||
param('staticEndpoint')
|
||||
.custom(isSafePath),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
|
||||
|
||||
if (!theme || theme.version !== req.params.themeVersion) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion
|
||||
})
|
||||
}
|
||||
|
||||
if (theme.css.includes(req.params.staticEndpoint) === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No static endpoint was found for this theme'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.registeredPlugin = theme
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
serveThemeCSSValidator
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { exists, isIdValid } from '../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors, checkUserIdExist } from './shared/index.js'
|
||||
|
||||
const requestOrConfirmTwoFactorValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return
|
||||
|
||||
if (res.locals.user.otpSecret) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Two factor is already enabled.`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const confirmTwoFactorValidator = [
|
||||
body('requestToken').custom(exists),
|
||||
body('otpToken').custom(exists),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const disableTwoFactorValidator = [
|
||||
param('id').custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return
|
||||
|
||||
if (!res.locals.user.otpSecret) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Two factor is already disabled.`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
requestOrConfirmTwoFactorValidator,
|
||||
confirmTwoFactorValidator,
|
||||
disableTwoFactorValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanEnableOrDisableTwoFactor (userId: number | string, res: express.Response) {
|
||||
const authUser = res.locals.oauth.token.user
|
||||
|
||||
if (!await checkUserIdExist(userId, res)) return
|
||||
|
||||
if (res.locals.user.id !== authUser.id && authUser.hasRight(UserRight.MANAGE_USERS) !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: `User ${authUser.username} does not have right to change two factor setting of this user.`
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -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'
|
||||
@@ -0,0 +1 @@
|
||||
export * from './user-registrations.js'
|
||||
@@ -0,0 +1,60 @@
|
||||
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'
|
||||
|
||||
function checkRegistrationIdExist (idArg: number | string, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
return checkRegistrationExist(() => UserRegistrationModel.load(id), res)
|
||||
}
|
||||
|
||||
function checkRegistrationEmailExist (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkRegistrationExist(() => UserRegistrationModel.loadByEmail(email), res, abortResponse)
|
||||
}
|
||||
|
||||
async function checkRegistrationHandlesDoNotAlreadyExist (options: {
|
||||
username: string
|
||||
channelHandle: string
|
||||
email: string
|
||||
res: express.Response
|
||||
}) {
|
||||
const { res } = options
|
||||
|
||||
const registration = await UserRegistrationModel.loadByEmailOrHandle(pick(options, [ 'username', 'email', 'channelHandle' ]))
|
||||
|
||||
if (registration) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Registration with this username, channel name or email already exists.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export {
|
||||
checkRegistrationIdExist,
|
||||
checkRegistrationEmailExist,
|
||||
checkRegistrationHandlesDoNotAlreadyExist,
|
||||
checkRegistrationExist
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { Redis } from '../../../lib/redis.js'
|
||||
import { areValidationErrors, checkUserEmailExist, checkUserIdExist } from '../shared/index.js'
|
||||
import { checkRegistrationEmailExist, checkRegistrationIdExist } from './shared/user-registrations.js'
|
||||
|
||||
const usersAskSendVerifyEmailValidator = [
|
||||
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 [ userExists, registrationExists ] = await Promise.all([
|
||||
checkUserEmailExist(req.body.email, res, false),
|
||||
checkRegistrationEmailExist(req.body.email, res, false)
|
||||
])
|
||||
|
||||
if (!userExists && !registrationExists) {
|
||||
logger.debug('User or registration with email %s does not exist (asking verify email).', req.body.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 ask verification email of a user that uses a plugin authentication.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
usersAskSendVerifyEmailValidator,
|
||||
usersVerifyEmailValidator,
|
||||
|
||||
registrationVerifyEmailValidator
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,71 @@
|
||||
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'
|
||||
|
||||
const listUserNotificationsValidator = [
|
||||
query('unread')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should have a valid unread boolean'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
listUserNotificationsValidator,
|
||||
updateNotificationSettingsValidator,
|
||||
markAsReadUserNotificationsValidator
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
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 { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { HttpStatusCode, UserRegister, UserRegistrationRequest, UserRegistrationState } from '@peertube/peertube-models'
|
||||
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 { ActorModel } from '../../../models/actor/actor.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: '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 || '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: '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: '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 {
|
||||
usersDirectRegistrationValidator,
|
||||
usersRequestRegistrationValidator,
|
||||
|
||||
ensureUserRegistrationAllowedFactory,
|
||||
ensureUserRegistrationAllowedForIP,
|
||||
|
||||
getRegistrationValidator,
|
||||
listRegistrationsValidator,
|
||||
|
||||
acceptOrRejectRegistrationValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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(body.username, body.email, res)) return
|
||||
|
||||
if (body.channel) {
|
||||
if (!body.channel.name || !body.channel.displayName) {
|
||||
return res.fail({ message: '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: 'Channel name cannot be the same as user username.' })
|
||||
}
|
||||
|
||||
const existing = await ActorModel.loadLocalByName(body.channel.name)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: `Channel with name ${body.channel.name} already exists.`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, UserRole } from '@peertube/peertube-models'
|
||||
import { exists, isBooleanValid, 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,
|
||||
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 { ActorModel } from '../../../models/actor/actor.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkUserEmailExist,
|
||||
checkUserIdExist,
|
||||
checkUserNameOrEmailDoNotAlreadyExist,
|
||||
checkUserCanManageAccount,
|
||||
doesVideoChannelIdExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
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(req.body.username, req.body.email, 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 ActorModel.loadLocalByName(req.body.channelName)
|
||||
if (existing) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: `Channel with name ${req.body.channelName} already exists.`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
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' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const usersBlockingValidator = [
|
||||
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' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
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.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const usersUpdateMeValidator = [
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isUserDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isUserDescriptionValid),
|
||||
body('currentPassword')
|
||||
.optional()
|
||||
.custom(isUserPasswordValid),
|
||||
body('password')
|
||||
.optional()
|
||||
.custom(isUserPasswordValid),
|
||||
body('emailPublic')
|
||||
.optional()
|
||||
.custom(isUserEmailPublicValid),
|
||||
body('email')
|
||||
.optional()
|
||||
.isEmail(),
|
||||
body('nsfwPolicy')
|
||||
.optional()
|
||||
.custom(isUserNSFWPolicyValid),
|
||||
body('autoPlayVideo')
|
||||
.optional()
|
||||
.custom(isUserAutoPlayVideoValid),
|
||||
body('p2pEnabled')
|
||||
.optional()
|
||||
.custom(isUserP2PEnabledValid).withMessage('Should have a valid p2p enabled boolean'),
|
||||
body('videoLanguages')
|
||||
.optional()
|
||||
.custom(isUserVideoLanguages),
|
||||
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
|
||||
|
||||
if (req.body.password || req.body.email) {
|
||||
if (user.pluginAuth !== null) {
|
||||
return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
|
||||
}
|
||||
|
||||
if (!req.body.currentPassword) {
|
||||
return res.fail({ message: 'currentPassword parameter is missing.' })
|
||||
}
|
||||
|
||||
if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: 'currentPassword is invalid.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res, { omitBodyLog: true })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
const usersVideosValidator = [
|
||||
query('isLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
|
||||
|
||||
query('channelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.channelId && !await doesVideoChannelIdExist(req.query.channelId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const usersAskResetPasswordValidator = [
|
||||
body('email')
|
||||
.isEmail(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const exists = await checkUserEmailExist(req.body.email, res, false)
|
||||
if (!exists) {
|
||||
logger.debug('User with email %s does not exist (asking reset password).', req.body.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()
|
||||
}
|
||||
]
|
||||
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
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.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const userAutocompleteValidator = [
|
||||
param('search')
|
||||
.isString()
|
||||
.not().isEmpty()
|
||||
]
|
||||
|
||||
const ensureAuthUserOwnsAccountValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!checkUserCanManageAccount({ user, account: res.locals.account, specialRight: null, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const ensureCanManageChannelOrAccount = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.user
|
||||
const account = res.locals.videoChannel?.Account ?? res.locals.account
|
||||
|
||||
if (!checkUserCanManageAccount({ account, user, res, specialRight: UserRight.MANAGE_ANY_VIDEO_CHANNEL })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const ensureCanModerateUser = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const authUser = res.locals.oauth.token.User
|
||||
const onUser = res.locals.user
|
||||
|
||||
if (authUser.role === UserRole.ADMINISTRATOR) return next()
|
||||
if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Users can only be managed by moderators or admins.'
|
||||
})
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
usersListValidator,
|
||||
usersAddValidator,
|
||||
deleteMeValidator,
|
||||
usersBlockingValidator,
|
||||
usersRemoveValidator,
|
||||
usersUpdateValidator,
|
||||
usersUpdateMeValidator,
|
||||
usersVideoRatingValidator,
|
||||
usersCheckCurrentPasswordFactory,
|
||||
usersGetValidator,
|
||||
usersVideosValidator,
|
||||
usersAskResetPasswordValidator,
|
||||
usersResetPasswordValidator,
|
||||
userAutocompleteValidator,
|
||||
ensureAuthUserOwnsAccountValidator,
|
||||
ensureCanModerateUser,
|
||||
ensureCanManageChannelOrAccount
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export * from './video-blacklist.js'
|
||||
export * from './video-captions.js'
|
||||
export * from './video-channel-sync.js'
|
||||
export * from './video-channels.js'
|
||||
export * from './video-chapters.js'
|
||||
export * from './video-comments.js'
|
||||
export * from './video-files.js'
|
||||
export * from './video-imports.js'
|
||||
export * from './video-live.js'
|
||||
export * from './video-ownership-changes.js'
|
||||
export * from './video-passwords.js'
|
||||
export * from './video-rates.js'
|
||||
export * from './video-shares.js'
|
||||
export * from './video-source.js'
|
||||
export * from './video-stats.js'
|
||||
export * from './video-studio.js'
|
||||
export * from './video-token.js'
|
||||
export * from './video-transcoding.js'
|
||||
export * from './video-view.js'
|
||||
export * from './videos.js'
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './upload.js'
|
||||
export * from './video-validators.js'
|
||||
@@ -0,0 +1,42 @@
|
||||
import express from 'express'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { ffprobePromise, getVideoStreamDuration } from '@peertube/peertube-ffmpeg'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
|
||||
export async function addDurationToVideoFileIfNeeded (options: {
|
||||
res: express.Response
|
||||
videoFile: { path: string, duration?: number }
|
||||
middlewareName: string
|
||||
}) {
|
||||
const { res, middlewareName, videoFile } = options
|
||||
|
||||
try {
|
||||
if (!videoFile.duration) await addDurationToVideo(res, videoFile)
|
||||
} catch (err) {
|
||||
logger.error('Invalid input file in ' + middlewareName, { err })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
|
||||
message: 'Video file unreadable.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function addDurationToVideo (res: express.Response, videoFile: { path: string, duration?: number }) {
|
||||
const probe = await ffprobePromise(videoFile.path)
|
||||
res.locals.ffprobe = probe
|
||||
|
||||
const duration = await getVideoStreamDuration(videoFile.path, probe)
|
||||
|
||||
// FFmpeg may not be able to guess video duration
|
||||
// For example with m2v files: https://trac.ffmpeg.org/ticket/9726#comment:2
|
||||
if (isNaN(duration)) videoFile.duration = 0
|
||||
else videoFile.duration = duration
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { HttpStatusCode, ServerErrorCode, ServerFilterHookName, VideoState, VideoStateType } from '@peertube/peertube-models'
|
||||
import { isVideoFileMimeTypeValid, isVideoFileSizeValid } from '@server/helpers/custom-validators/videos.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { CONSTRAINTS_FIELDS, VIDEO_STATES } from '@server/initializers/constants.js'
|
||||
import { isLocalVideoFileAccepted } from '@server/lib/moderation.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { MUserAccountId, MVideo } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { checkUserQuota } from '../../shared/index.js'
|
||||
|
||||
export async function commonVideoFileChecks (options: {
|
||||
res: express.Response
|
||||
user: MUserAccountId
|
||||
videoFileSize: number
|
||||
files: express.UploadFilesForCheck
|
||||
}): Promise<boolean> {
|
||||
const { res, user, videoFileSize, files } = options
|
||||
|
||||
if (!isVideoFileMimeTypeValid(files)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
|
||||
message: `This file is not supported. Please, make sure it is of the following type: ${CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')}`
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isVideoFileSizeValid(videoFileSize.toString())) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
message: 'This file is too large. It exceeds the maximum file size authorized.',
|
||||
type: ServerErrorCode.MAX_FILE_SIZE_REACHED
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (await checkUserQuota(user, videoFileSize, res) === false) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function isVideoFileAccepted (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
videoFile: express.VideoLegacyUploadFile
|
||||
hook: Extract<ServerFilterHookName, 'filter:api.video.upload.accept.result' | 'filter:api.video.update-file.accept.result'>
|
||||
}) {
|
||||
const { req, res, videoFile, hook } = options
|
||||
|
||||
// Check we accept this video
|
||||
const acceptParameters = {
|
||||
videoBody: req.body,
|
||||
videoFile,
|
||||
user: res.locals.oauth.token.User
|
||||
}
|
||||
const acceptedResult = await Hooks.wrapFun(isLocalVideoFileAccepted, acceptParameters, hook)
|
||||
|
||||
if (!acceptedResult || acceptedResult.accepted !== true) {
|
||||
logger.info('Refused local video file.', { acceptedResult, acceptParameters })
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult.errorMessage || 'Refused local video file'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkVideoFileCanBeEdited (video: MVideo, res: express.Response) {
|
||||
if (video.isLive) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot edit a live video'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (video.state === VideoState.TO_TRANSCODE || video.state === VideoState.TO_EDIT) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot edit video that is already waiting for transcoding/edition'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const validStates = new Set<VideoStateType>([
|
||||
VideoState.PUBLISHED,
|
||||
VideoState.TO_MOVE_TO_EXTERNAL_STORAGE_FAILED,
|
||||
VideoState.TO_MOVE_TO_FILE_SYSTEM_FAILED,
|
||||
VideoState.TRANSCODING_FAILED
|
||||
])
|
||||
|
||||
if (!validStates.has(video.state)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video state is not compatible with edition'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkVideoCanBeTranscribedOrTranscripted (video: MVideo, res: express.Response) {
|
||||
if (video.remote) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot run this task on a remote video'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (video.isLive) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot run this task on a live'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const incompatibleStates = new Set<VideoStateType>([
|
||||
VideoState.TO_IMPORT,
|
||||
VideoState.TO_EDIT,
|
||||
VideoState.TO_MOVE_TO_EXTERNAL_STORAGE,
|
||||
VideoState.TO_MOVE_TO_FILE_SYSTEM
|
||||
])
|
||||
if (incompatibleStates.has(video.state)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Cannot run this task on a video with "${VIDEO_STATES[video.state]}" state`
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import express from 'express'
|
||||
import { body, query } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../../helpers/custom-validators/video-blacklist.js'
|
||||
import { areValidationErrors, doesVideoBlacklistExist, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const videosBlacklistRemoveValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosBlacklistAddValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('unfederate')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid unfederate boolean'),
|
||||
body('reason')
|
||||
.optional()
|
||||
.custom(isVideoBlacklistReasonValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (req.body.unfederate === true && video.remote === true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'You cannot unfederate a remote video.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosBlacklistUpdateValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('reason')
|
||||
.optional()
|
||||
.custom(isVideoBlacklistReasonValid).withMessage('Should have a valid reason'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoBlacklistExist(res.locals.videoAll.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosBlacklistFiltersValidator = [
|
||||
query('type')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoBlacklistTypeValid).withMessage('Should have a valid video blacklist type attribute'),
|
||||
query('search')
|
||||
.optional()
|
||||
.not()
|
||||
.isEmpty().withMessage('Should have a valid search'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videosBlacklistAddValidator,
|
||||
videosBlacklistRemoveValidator,
|
||||
videosBlacklistUpdateValidator,
|
||||
videosBlacklistFiltersValidator
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoCaptionGenerate } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoCaptionModel } from '@server/models/video/video-caption.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanSeeVideo,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoCaptionExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam,
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
import { checkVideoCanBeTranscribedOrTranscripted } from './shared/video-validators.js'
|
||||
|
||||
export const addVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('captionLanguage')
|
||||
.custom(isVideoCaptionLanguageValid).not().isEmpty(),
|
||||
|
||||
body('captionfile')
|
||||
.custom((_, { req }) => isVideoCaptionFile(req.files, 'captionfile'))
|
||||
.withMessage(
|
||||
'This caption file is not supported or too large. ' +
|
||||
`Please, make sure it is under ${CONSTRAINTS_FIELDS.VIDEO_CAPTIONS.CAPTION_FILE.FILE_SIZE.max} bytes ` +
|
||||
'and one of the following mimetypes: ' +
|
||||
Object.keys(MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT).map(key => `${key} (${MIMETYPES.VIDEO_CAPTIONS.MIMETYPE_EXT[key]})`).join(', ')
|
||||
),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const generateVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('forceTranscription')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (CONFIG.VIDEO_TRANSCRIPTION.ENABLED !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video transcription is disabled on this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkVideoCanBeTranscribedOrTranscripted(video, res)) return
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||
|
||||
// Check the video has not already a caption
|
||||
const captions = await VideoCaptionModel.listVideoCaptions(video.id)
|
||||
if (captions.length !== 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_HAS_CAPTIONS,
|
||||
message: 'This video already has captions'
|
||||
})
|
||||
}
|
||||
|
||||
// Bypass "video is already transcribed" check
|
||||
const body = req.body as VideoCaptionGenerate
|
||||
if (body.forceTranscription === true) {
|
||||
if (user.hasRight(UserRight.UPDATE_ANY_VIDEO) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Only admins can force transcription'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
const info = await VideoJobInfoModel.load(video.id)
|
||||
if (info && info.pendingTranscription > 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCRIBED,
|
||||
message: 'This video is already being transcribed'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const deleteVideoCaptionValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('captionLanguage')
|
||||
.custom(isVideoCaptionLanguageValid).not().isEmpty().withMessage('Should have a valid caption language'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoCaptionExist(res.locals.videoAll, req.params.captionLanguage, res)) return
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listVideoCaptionsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
const video = res.locals.onlyVideo
|
||||
if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.videoId })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import { HttpStatusCode, VideoChannelSyncCreate } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, doesVideoChannelIdExist } from '../shared/index.js'
|
||||
import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs.js'
|
||||
|
||||
export const ensureSyncIsEnabled = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.ENABLED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Synchronization is impossible as video channel synchronization is not enabled on the server'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
export const videoChannelSyncValidator = [
|
||||
body('externalChannelUrl')
|
||||
.custom(isUrlValid),
|
||||
|
||||
body('videoChannelId')
|
||||
.isInt(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: VideoChannelSyncCreate = req.body
|
||||
if (!await doesVideoChannelIdExist(body.videoChannelId, res)) return
|
||||
|
||||
const count = await VideoChannelSyncModel.countByAccount(res.locals.videoChannel.accountId)
|
||||
if (count >= CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER) {
|
||||
return res.fail({
|
||||
message: `You cannot create more than ${CONFIG.IMPORT.VIDEO_CHANNEL_SYNCHRONIZATION.MAX_PER_USER} channel synchronizations`
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const ensureSyncExists = [
|
||||
param('id').exists().isInt().withMessage('Should have an sync id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoChannelSyncIdExist(+req.params.id, res)) return
|
||||
if (!await doesVideoChannelIdExist(res.locals.videoChannelSync.videoChannelId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,193 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, VideosImportInChannelCreate } from '@peertube/peertube-models'
|
||||
import { isUrlValid } from '@server/helpers/custom-validators/activitypub/misc.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { MChannelAccountDefault } from '@server/types/models/index.js'
|
||||
import { isBooleanValid, isIdValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isVideoChannelDescriptionValid,
|
||||
isVideoChannelDisplayNameValid,
|
||||
isVideoChannelSupportValid,
|
||||
isVideoChannelUsernameValid
|
||||
} from '../../../helpers/custom-validators/video-channels.js'
|
||||
import { ActorModel } from '../../../models/actor/actor.js'
|
||||
import { VideoChannelModel } from '../../../models/video/video-channel.js'
|
||||
import { areValidationErrors, checkUserQuota, doesVideoChannelNameWithHostExist } from '../shared/index.js'
|
||||
import { doesVideoChannelSyncIdExist } from '../shared/video-channel-syncs.js'
|
||||
|
||||
export const videoChannelsAddValidator = [
|
||||
body('name')
|
||||
.custom(isVideoChannelUsernameValid),
|
||||
body('displayName')
|
||||
.custom(isVideoChannelDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isVideoChannelDescriptionValid),
|
||||
body('support')
|
||||
.optional()
|
||||
.custom(isVideoChannelSupportValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const actor = await ActorModel.loadLocalByName(req.body.name)
|
||||
if (actor) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
|
||||
if (count >= CONFIG.VIDEO_CHANNELS.MAX_PER_USER) {
|
||||
res.fail({ message: `You cannot create more than ${CONFIG.VIDEO_CHANNELS.MAX_PER_USER} channels` })
|
||||
return false
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsUpdateValidator = [
|
||||
param('nameWithHost')
|
||||
.exists(),
|
||||
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isVideoChannelDisplayNameValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.custom(isVideoChannelDescriptionValid),
|
||||
body('support')
|
||||
.optional()
|
||||
.custom(isVideoChannelSupportValid),
|
||||
body('bulkVideosSupportUpdate')
|
||||
.optional()
|
||||
.custom(isBooleanValid).withMessage('Should have a valid bulkVideosSupportUpdate boolean field'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsRemoveValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsNameWithHostValidator = [
|
||||
param('nameWithHost')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoChannelNameWithHostExist(req.params.nameWithHost, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const ensureIsLocalChannel = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (res.locals.videoChannel.Actor.isOwned() === false) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'This channel is not owned.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const ensureChannelOwnerCanUpload = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const channel = res.locals.videoChannel
|
||||
const user = { id: channel.Account.userId }
|
||||
|
||||
if (!await checkUserQuota(user, 1, res)) return
|
||||
|
||||
next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelStatsValidator = [
|
||||
query('withStats')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid stats flag boolean'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelsListValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoChannelImportVideosValidator = [
|
||||
body('externalChannelUrl')
|
||||
.custom(isUrlValid),
|
||||
|
||||
body('videoChannelSyncId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: VideosImportInChannelCreate = req.body
|
||||
|
||||
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Channel import is impossible as video upload via HTTP is not enabled on the server'
|
||||
})
|
||||
}
|
||||
|
||||
if (body.videoChannelSyncId && !await doesVideoChannelSyncIdExist(body.videoChannelSyncId, res)) return
|
||||
|
||||
if (res.locals.videoChannelSync && res.locals.videoChannelSync.videoChannelId !== res.locals.videoChannel.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'This channel sync is not owned by this channel'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
|
||||
const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
|
||||
|
||||
if (count <= 1) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot remove the last channel of this user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import {
|
||||
areValidationErrors, checkUserCanManageVideo, doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
import { areVideoChaptersValid } from '@server/helpers/custom-validators/video-chapters.js'
|
||||
|
||||
export const updateVideoChaptersValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('chapters')
|
||||
.custom(areVideoChaptersValid)
|
||||
.withMessage('Chapters must have a valid title and timecode, and each timecode must be unique'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
if (res.locals.videoAll.isLive) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'You cannot add chapters to a live video'
|
||||
})
|
||||
}
|
||||
|
||||
// Check if the user who did the request is able to update video chapters (same right as updating the video)
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,358 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, VideoCommentPolicy } from '@peertube/peertube-models'
|
||||
import { isStringArray } from '@server/helpers/custom-validators/search.js'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
import { MUserAccountUrl } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
toBooleanOrNull,
|
||||
toCompleteUUID,
|
||||
toIntOrNull
|
||||
} from '../../../helpers/custom-validators/misc.js'
|
||||
import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { AcceptResult, isLocalVideoCommentReplyAccepted, isLocalVideoThreadAccepted } from '../../../lib/moderation.js'
|
||||
import { Hooks } from '../../../lib/plugins/hooks.js'
|
||||
import { MCommentOwnerVideoReply, MVideo, MVideoFullLight } from '../../../types/models/video/index.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanSeeVideo,
|
||||
checkUserCanManageAccount,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoChannelIdExist,
|
||||
doesVideoCommentExist,
|
||||
doesVideoCommentThreadExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam,
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
|
||||
export const listAllVideoCommentsForAdminValidator = [
|
||||
...getCommonVideoCommentsValidators(),
|
||||
|
||||
query('isLocal')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid)
|
||||
.withMessage('Should have a valid isLocal boolean'),
|
||||
|
||||
query('onLocalVideo')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid)
|
||||
.withMessage('Should have a valid onLocalVideo boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.videoId && !await doesVideoExist(req.query.videoId, res, 'unsafe-only-immutable-attributes')) return
|
||||
if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listCommentsOnUserVideosValidator = [
|
||||
...getCommonVideoCommentsValidators(),
|
||||
|
||||
query('isHeldForReview')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid)
|
||||
.withMessage('Should have a valid isHeldForReview boolean'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (req.query.videoId && !await doesVideoExist(req.query.videoId, res, 'all')) return
|
||||
if (req.query.videoChannelId && !await doesVideoChannelIdExist(req.query.videoChannelId, res)) return
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (video && !checkUserCanManageVideo(user, video, UserRight.SEE_ALL_COMMENTS, res)) return
|
||||
|
||||
const channel = res.locals.videoChannel
|
||||
if (channel && !checkUserCanManageAccount({ account: channel.Account, user, res, specialRight: UserRight.SEE_ALL_COMMENTS })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const listVideoCommentThreadsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listVideoThreadCommentsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('threadId')
|
||||
.custom(isIdValid),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
if (!await doesVideoCommentThreadExist(req.params.threadId, res.locals.onlyVideo, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.onlyVideo })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const addVideoCommentThreadValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('text')
|
||||
.custom(isValidVideoCommentText),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
|
||||
|
||||
if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
|
||||
if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, false)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const addVideoCommentReplyValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId').custom(isIdValid),
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
body('text').custom(isValidVideoCommentText),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.videoId, video: res.locals.videoAll })) return
|
||||
|
||||
if (!isVideoCommentsEnabled(res.locals.videoAll, res)) return
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
|
||||
if (!await isVideoCommentAccepted(req, res, res.locals.videoAll, true)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoCommentGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
if (!canVideoBeFederated(res.locals.onlyVideo)) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.onlyVideo, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const removeVideoCommentValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
|
||||
|
||||
if (!checkUserCanDeleteVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const approveVideoCommentValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('commentId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!await doesVideoCommentExist(req.params.commentId, res.locals.videoAll, res)) return
|
||||
|
||||
if (!checkUserCanApproveVideoComment(res.locals.oauth.token.User, res.locals.videoCommentFull, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
|
||||
if (video.commentsPolicy === VideoCommentPolicy.DISABLED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Video comments are disabled for this video.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
|
||||
if (videoComment.isDeleted()) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'This comment is already deleted'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const userAccount = user.Account
|
||||
|
||||
if (
|
||||
user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT) === false && // Not a moderator
|
||||
videoComment.accountId !== userAccount.id && // Not the comment owner
|
||||
videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
|
||||
) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot remove video comment of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkUserCanApproveVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
|
||||
if (videoComment.isDeleted()) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'This comment is deleted'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (videoComment.heldForReview !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This comment is not held for review'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const userAccount = user.Account
|
||||
|
||||
if (
|
||||
user.hasRight(UserRight.MANAGE_ANY_VIDEO_COMMENT) === false && // Not a moderator
|
||||
videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
|
||||
) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot approve video comment of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function isVideoCommentAccepted (req: express.Request, res: express.Response, video: MVideoFullLight, isReply: boolean) {
|
||||
const acceptParameters = {
|
||||
video,
|
||||
commentBody: req.body,
|
||||
user: res.locals.oauth.token.User,
|
||||
req
|
||||
}
|
||||
|
||||
let acceptedResult: AcceptResult
|
||||
|
||||
if (isReply) {
|
||||
const acceptReplyParameters = Object.assign(acceptParameters, { parentComment: res.locals.videoCommentFull })
|
||||
|
||||
acceptedResult = await Hooks.wrapFun(
|
||||
isLocalVideoCommentReplyAccepted,
|
||||
acceptReplyParameters,
|
||||
'filter:api.video-comment-reply.create.accept.result'
|
||||
)
|
||||
} else {
|
||||
acceptedResult = await Hooks.wrapFun(
|
||||
isLocalVideoThreadAccepted,
|
||||
acceptParameters,
|
||||
'filter:api.video-thread.create.accept.result'
|
||||
)
|
||||
}
|
||||
|
||||
if (!acceptedResult || acceptedResult.accepted !== true) {
|
||||
logger.info('Refused local comment.', { acceptedResult, acceptParameters })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult?.errorMessage || 'Comment has been rejected.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getCommonVideoCommentsValidators () {
|
||||
return [
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('searchAccount')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('searchVideo')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
|
||||
query('videoId')
|
||||
.optional()
|
||||
.custom(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid),
|
||||
|
||||
query('videoChannelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
query('autoTagOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid autoTagOneOf array')
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { MVideo } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const videoFilesDeleteWebVideoValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
if (!video.hasWebVideoFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have Web Video files'
|
||||
})
|
||||
}
|
||||
|
||||
if (!video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete Web Video files since this video does not have HLS playlist'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoFilesDeleteWebVideoFileValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
const files = video.VideoFiles
|
||||
if (!files.find(f => f.id === +req.params.videoFileId)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'This video does not have this Web Video file id'
|
||||
})
|
||||
}
|
||||
|
||||
if (files.length === 1 && !video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete Web Video files since this video does not have HLS playlist'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const videoFilesDeleteHLSValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
if (!video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have HLS files'
|
||||
})
|
||||
}
|
||||
|
||||
if (!video.hasWebVideoFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete HLS playlist since this video does not have Web Video files'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoFilesDeleteHLSFileValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkLocalVideo(video, res)) return
|
||||
|
||||
if (!video.getHLSPlaylist()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'This video does not have HLS files'
|
||||
})
|
||||
}
|
||||
|
||||
const hlsFiles = video.getHLSPlaylist().VideoFiles
|
||||
if (!hlsFiles.find(f => f.id === +req.params.videoFileId)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'This HLS playlist does not have this file id'
|
||||
})
|
||||
}
|
||||
|
||||
// Last file to delete
|
||||
if (hlsFiles.length === 1 && !video.hasWebVideoFiles()) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete last HLS playlist file since this video does not have Web Video files'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
videoFilesDeleteWebVideoValidator,
|
||||
videoFilesDeleteWebVideoFileValidator,
|
||||
|
||||
videoFilesDeleteHLSValidator,
|
||||
videoFilesDeleteHLSFileValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function checkLocalVideo (video: MVideo, res: express.Response) {
|
||||
if (video.remote) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot delete files of remote video'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, VideoImportCreate, VideoImportState } from '@peertube/peertube-models'
|
||||
import { isResolvingToUnicastOnly } from '@server/helpers/dns.js'
|
||||
import { isPreImportVideoAccepted } from '@server/lib/moderation.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { MUserAccountId, MVideoImport } from '@server/types/models/index.js'
|
||||
import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports.js'
|
||||
import { isValidPasswordProtectedPrivacy, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { areValidationErrors, doesVideoChannelOfAccountExist, doesVideoImportExist } from '../shared/index.js'
|
||||
import { getCommonVideoEditAttributes } from './videos.js'
|
||||
|
||||
const videoImportAddValidator = getCommonVideoEditAttributes().concat([
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
body('targetUrl')
|
||||
.optional()
|
||||
.custom(isVideoImportTargetUrlValid),
|
||||
body('magnetUri')
|
||||
.optional()
|
||||
.custom(isVideoMagnetUriValid),
|
||||
body('torrentfile')
|
||||
.custom((value, { req }) => isVideoImportTorrentFile(req.files))
|
||||
.withMessage(
|
||||
'This torrent file is not supported or too large. Please, make sure it is of the following type: ' +
|
||||
CONSTRAINTS_FIELDS.VIDEO_IMPORTS.TORRENT_FILE.EXTNAME.join(', ')
|
||||
),
|
||||
body('name')
|
||||
.optional()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
const torrentFile = req.files?.['torrentfile'] ? req.files['torrentfile'][0] : undefined
|
||||
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'HTTP import is not enabled on this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Torrent/magnet URI import is not enabled on this instance.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||
|
||||
// Check we have at least 1 required param
|
||||
if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' })
|
||||
}
|
||||
|
||||
if (req.body.targetUrl) {
|
||||
const hostname = new URL(req.body.targetUrl).hostname
|
||||
|
||||
if (await isResolvingToUnicastOnly(hostname) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot use non unicast IP as targetUrl.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
const getMyVideoImportsValidator = [
|
||||
query('videoChannelSyncId')
|
||||
.optional()
|
||||
.custom(isIdValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoImportDeleteValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoImportExist(parseInt(req.params.id), res)) return
|
||||
if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
|
||||
|
||||
if (res.locals.videoImport.state === VideoImportState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot delete a pending video import. Cancel it or wait for the end of the import first.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoImportCancelValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoImportExist(forceNumber(req.params.id), res)) return
|
||||
if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return
|
||||
|
||||
if (res.locals.videoImport.state !== VideoImportState.PENDING) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot cancel a non pending video import.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoImportAddValidator,
|
||||
videoImportCancelValidator,
|
||||
videoImportDeleteValidator,
|
||||
getMyVideoImportsValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function isImportAccepted (req: express.Request, res: express.Response) {
|
||||
const body: VideoImportCreate = req.body
|
||||
const hookName = body.targetUrl
|
||||
? 'filter:api.video.pre-import-url.accept.result'
|
||||
: 'filter:api.video.pre-import-torrent.accept.result'
|
||||
|
||||
// Check we accept this video
|
||||
const acceptParameters = {
|
||||
videoImportBody: body,
|
||||
user: res.locals.oauth.token.User
|
||||
}
|
||||
const acceptedResult = await Hooks.wrapFun(
|
||||
isPreImportVideoAccepted,
|
||||
acceptParameters,
|
||||
hookName
|
||||
)
|
||||
|
||||
if (!acceptedResult || acceptedResult.accepted !== true) {
|
||||
logger.info('Refused to import video.', { acceptedResult, acceptParameters })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult.errorMessage || 'Refused to import video'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkUserCanManageImport (user: MUserAccountId, videoImport: MVideoImport, res: express.Response) {
|
||||
if (user.hasRight(UserRight.MANAGE_VIDEO_IMPORTS) === false && videoImport.userId !== user.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage video import of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { isLiveLatencyModeValid } from '@server/helpers/custom-validators/video-lives.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import { isLocalLiveVideoAccepted } from '@server/lib/moderation.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
||||
import { VideoLiveSessionModel } from '@server/models/video/video-live-session.js'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideoCreate,
|
||||
LiveVideoLatencyMode,
|
||||
LiveVideoUpdate,
|
||||
ServerErrorCode,
|
||||
UserRight,
|
||||
VideoState
|
||||
} from '@peertube/peertube-models'
|
||||
import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isValidPasswordProtectedPrivacy, isVideoNameValid, isVideoReplayPrivacyValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoChannelOfAccountExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
import { getCommonVideoEditAttributes } from './videos.js'
|
||||
|
||||
const videoLiveGetValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return
|
||||
|
||||
const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
|
||||
if (!videoLive) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Live video not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoLive = videoLive
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
|
||||
body('name')
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
|
||||
body('saveReplay')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
|
||||
|
||||
body('replaySettings.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoReplayPrivacyValid),
|
||||
|
||||
body('permanentLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid permanentLive boolean'),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isLiveLatencyModeValid),
|
||||
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (CONFIG.LIVE.ENABLED !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Live is not enabled on this instance',
|
||||
type: ServerErrorCode.LIVE_NOT_ENABLED
|
||||
})
|
||||
}
|
||||
|
||||
const body: LiveVideoCreate = req.body
|
||||
|
||||
if (hasValidSaveReplay(body) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Saving live replay is not enabled on this instance',
|
||||
type: ServerErrorCode.LIVE_NOT_ALLOWING_REPLAY
|
||||
})
|
||||
}
|
||||
|
||||
if (hasValidLatencyMode(body) !== true) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Custom latency mode is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!await doesVideoChannelOfAccountExist(body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (CONFIG.LIVE.MAX_INSTANCE_LIVES !== -1) {
|
||||
const totalInstanceLives = await VideoModel.countLives({ remote: false, mode: 'not-ended' })
|
||||
|
||||
if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot create this live because the max instance lives limit is reached.',
|
||||
type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (CONFIG.LIVE.MAX_USER_LIVES !== -1) {
|
||||
const totalUserLives = await VideoModel.countLivesOfAccount(user.Account.id)
|
||||
|
||||
if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot create this live because the max user lives limit is reached.',
|
||||
type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!await isLiveVideoAccepted(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
const videoLiveUpdateValidator = [
|
||||
body('saveReplay')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid saveReplay boolean'),
|
||||
|
||||
body('replaySettings.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoReplayPrivacyValid),
|
||||
|
||||
body('latencyMode')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isLiveLatencyModeValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const body: LiveVideoUpdate = req.body
|
||||
|
||||
if (hasValidSaveReplay(body) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Saving live replay is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (hasValidLatencyMode(body) !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Custom latency mode is not allowed by this instance'
|
||||
})
|
||||
}
|
||||
|
||||
if (!checkLiveSettingsReplayConsistency({ res, body })) return
|
||||
|
||||
if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
return res.fail({ message: 'Cannot update a live that has already started' })
|
||||
}
|
||||
|
||||
// Check the user can manage the live
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoLiveListSessionsValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
// Check the user can manage the live
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoLiveFindReplaySessionValidator = [
|
||||
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
|
||||
|
||||
const session = await VideoLiveSessionModel.findSessionOfReplay(res.locals.videoId.id)
|
||||
if (!session) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'No live replay found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoLiveSession = session
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoLiveAddValidator,
|
||||
videoLiveUpdateValidator,
|
||||
videoLiveListSessionsValidator,
|
||||
videoLiveFindReplaySessionValidator,
|
||||
videoLiveGetValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function isLiveVideoAccepted (req: express.Request, res: express.Response) {
|
||||
// Check we accept this video
|
||||
const acceptParameters = {
|
||||
liveVideoBody: req.body,
|
||||
user: res.locals.oauth.token.User
|
||||
}
|
||||
const acceptedResult = await Hooks.wrapFun(
|
||||
isLocalLiveVideoAccepted,
|
||||
acceptParameters,
|
||||
'filter:api.live-video.create.accept.result'
|
||||
)
|
||||
|
||||
if (!acceptedResult || acceptedResult.accepted !== true) {
|
||||
logger.info('Refused local live video.', { acceptedResult, acceptParameters })
|
||||
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: acceptedResult.errorMessage || 'Refused local live video'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function hasValidSaveReplay (body: LiveVideoUpdate | LiveVideoCreate) {
|
||||
if (CONFIG.LIVE.ALLOW_REPLAY !== true && body.saveReplay === true) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function hasValidLatencyMode (body: LiveVideoUpdate | LiveVideoCreate) {
|
||||
if (
|
||||
CONFIG.LIVE.LATENCY_SETTING.ENABLED !== true &&
|
||||
exists(body.latencyMode) &&
|
||||
body.latencyMode !== LiveVideoLatencyMode.DEFAULT
|
||||
) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function checkLiveSettingsReplayConsistency (options: {
|
||||
res: express.Response
|
||||
body: LiveVideoUpdate
|
||||
}) {
|
||||
const { res, body } = options
|
||||
|
||||
// We now save replays of this live, so replay settings are mandatory
|
||||
if (res.locals.videoLive.saveReplay !== true && body.saveReplay === true) {
|
||||
|
||||
if (!exists(body.replaySettings)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Replay settings are missing now the live replay is saved'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!exists(body.replaySettings.privacy)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Privacy replay setting is missing now the live replay is saved'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Save replay was and is not enabled, so send an error the user if it specified replay settings
|
||||
if ((!exists(body.saveReplay) && res.locals.videoLive.saveReplay === false) || body.saveReplay === false) {
|
||||
if (exists(body.replaySettings)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot save replay settings since live replay is not enabled'
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { checkUserCanTerminateOwnershipChange } from '@server/helpers/custom-validators/video-ownership.js'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { MVideoWithAllFiles } from '@server/types/models/index.js'
|
||||
import { HttpStatusCode, UserRight, VideoChangeOwnershipAccept, VideoChangeOwnershipStatus, VideoState } from '@peertube/peertube-models'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkUserCanManageVideo,
|
||||
checkUserQuota,
|
||||
doesChangeVideoOwnershipExist,
|
||||
doesVideoChannelOfAccountExist,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
|
||||
const videosChangeOwnershipValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
|
||||
// Check if the user who did the request is able to change the ownership of the video
|
||||
if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.CHANGE_VIDEO_OWNERSHIP, res)) return
|
||||
|
||||
const nextOwner = await AccountModel.loadLocalByName(req.body.username)
|
||||
if (!nextOwner) {
|
||||
res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
|
||||
return
|
||||
}
|
||||
|
||||
res.locals.nextOwner = nextOwner
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosTerminateChangeOwnershipValidator = [
|
||||
param('id')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesChangeVideoOwnershipExist(req.params.id, res)) return
|
||||
|
||||
// Check if the user who did the request is able to change the ownership of the video
|
||||
if (!checkUserCanTerminateOwnershipChange(res.locals.oauth.token.User, res.locals.videoChangeOwnership, res)) return
|
||||
|
||||
const videoChangeOwnership = res.locals.videoChangeOwnership
|
||||
|
||||
if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Ownership already accepted or refused'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosAcceptChangeOwnershipValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const body = req.body as VideoChangeOwnershipAccept
|
||||
if (!await doesVideoChannelOfAccountExist(body.channelId, res.locals.oauth.token.User, res)) return
|
||||
|
||||
const videoChangeOwnership = res.locals.videoChangeOwnership
|
||||
|
||||
const video = videoChangeOwnership.Video
|
||||
|
||||
if (!await checkCanAccept(video, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
videosChangeOwnershipValidator,
|
||||
videosTerminateChangeOwnershipValidator,
|
||||
videosAcceptChangeOwnershipValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanAccept (video: MVideoWithAllFiles, res: express.Response): Promise<boolean> {
|
||||
if (video.isLive) {
|
||||
|
||||
if (video.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'You can accept an ownership change of a published live.'
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!await checkUserQuota(user, video.getMaxQualityFile().size, res)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import express from 'express'
|
||||
import {
|
||||
areValidationErrors,
|
||||
doesVideoExist,
|
||||
isVideoPasswordProtected,
|
||||
isValidVideoIdParam,
|
||||
doesVideoPasswordExist,
|
||||
isVideoPasswordDeletable,
|
||||
checkUserCanManageVideo
|
||||
} from '../shared/index.js'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isValidPasswordProtectedPrivacy } from '@server/helpers/custom-validators/videos.js'
|
||||
import { UserRight } from '@peertube/peertube-models'
|
||||
|
||||
const listVideoPasswordValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!isVideoPasswordProtected(res)) return
|
||||
|
||||
// Check if the user who did the request is able to access video password list
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const updateVideoPasswordListValidator = [
|
||||
body('passwords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return
|
||||
|
||||
// Check if the user who did the request is able to update video passwords
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const removeVideoPasswordValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('passwordId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return
|
||||
if (!isVideoPasswordProtected(res)) return
|
||||
if (!await doesVideoPasswordExist(req.params.passwordId, res)) return
|
||||
if (!await isVideoPasswordDeletable(res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
listVideoPasswordValidator,
|
||||
updateVideoPasswordListValidator,
|
||||
removeVideoPasswordValidator
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
import express from 'express'
|
||||
import { body, param, query, ValidationChain } from 'express-validator'
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
UserRight,
|
||||
UserRightType,
|
||||
VideoPlaylistCreate,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPlaylistType,
|
||||
VideoPlaylistUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
||||
import { MUserAccountId } from '@server/types/models/index.js'
|
||||
import {
|
||||
isArrayOf,
|
||||
isIdOrUUIDValid,
|
||||
isIdValid,
|
||||
isUUIDValid,
|
||||
toCompleteUUID,
|
||||
toIntArray,
|
||||
toIntOrNull,
|
||||
toValueOrNull
|
||||
} from '../../../helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isVideoPlaylistDescriptionValid,
|
||||
isVideoPlaylistNameValid,
|
||||
isVideoPlaylistPrivacyValid,
|
||||
isVideoPlaylistTimestampValid,
|
||||
isVideoPlaylistTypeValid
|
||||
} from '../../../helpers/custom-validators/video-playlists.js'
|
||||
import { isVideoImageValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS } from '../../../initializers/constants.js'
|
||||
import { VideoPlaylistElementModel } from '../../../models/video/video-playlist-element.js'
|
||||
import { MVideoPlaylist } from '../../../types/models/video/video-playlist.js'
|
||||
import { authenticatePromise } from '../../auth.js'
|
||||
import {
|
||||
areValidationErrors,
|
||||
doesVideoChannelIdExist,
|
||||
doesVideoExist,
|
||||
doesVideoPlaylistExist,
|
||||
isValidPlaylistIdParam,
|
||||
VideoPlaylistFetchType
|
||||
} from '../shared/index.js'
|
||||
|
||||
const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
|
||||
body('displayName')
|
||||
.custom(isVideoPlaylistNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const body: VideoPlaylistCreate = req.body
|
||||
if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (
|
||||
!body.videoChannelId &&
|
||||
(body.privacy === VideoPlaylistPrivacy.PUBLIC || body.privacy === VideoPlaylistPrivacy.UNLISTED)
|
||||
) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: 'Cannot set "public" or "unlisted" a playlist that is not assigned to a channel.' })
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
body('displayName')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistNameValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return cleanUpReqFiles(req)
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
|
||||
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
const body: VideoPlaylistUpdate = req.body
|
||||
|
||||
const newPrivacy = body.privacy || videoPlaylist.privacy
|
||||
if (newPrivacy === VideoPlaylistPrivacy.PUBLIC &&
|
||||
(
|
||||
(!videoPlaylist.videoChannelId && !body.videoChannelId) ||
|
||||
body.videoChannelId === null
|
||||
)
|
||||
) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
|
||||
}
|
||||
|
||||
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
|
||||
cleanUpReqFiles(req)
|
||||
|
||||
return res.fail({ message: 'Cannot update a watch later playlist.' })
|
||||
}
|
||||
|
||||
if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
const videoPlaylistsDeleteValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res)) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
|
||||
return res.fail({ message: 'Cannot delete a watch later playlist.' })
|
||||
}
|
||||
|
||||
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
|
||||
return [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res, fetchType)) return
|
||||
|
||||
const videoPlaylist = res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
|
||||
|
||||
// Playlist is unlisted, check we used the uuid to fetch it
|
||||
if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
|
||||
if (isUUIDValid(req.params.playlistId)) return next()
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Playlist not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
|
||||
await authenticatePromise({ req, res })
|
||||
|
||||
const user = res.locals.oauth ? res.locals.oauth.token.User : null
|
||||
|
||||
if (
|
||||
!user ||
|
||||
(videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
|
||||
) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot get this private video playlist.'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const videoPlaylistsSearchValidator = [
|
||||
query('search')
|
||||
.optional()
|
||||
.not().isEmpty(),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistsAddVideoValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
body('videoId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid/short uuid'),
|
||||
body('startTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
body('stopTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
|
||||
if (!await doesVideoExist(req.body.videoId, res, 'only-video-and-blacklist')) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
|
||||
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) {
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistsUpdateOrRemoveVideoValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
param('playlistElementId')
|
||||
.customSanitizer(toCompleteUUID)
|
||||
.custom(isIdValid).withMessage('Should have an element id/uuid/short uuid'),
|
||||
body('startTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
body('stopTimestamp')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTimestampValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
|
||||
const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
|
||||
if (!videoPlaylistElement) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist element not found'
|
||||
})
|
||||
return
|
||||
}
|
||||
res.locals.videoPlaylistElement = videoPlaylistElement
|
||||
|
||||
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistElementAPGetValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
param('playlistElementId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const playlistElementId = forceNumber(req.params.playlistElementId)
|
||||
const playlistId = req.params.playlistId
|
||||
|
||||
const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
|
||||
if (!videoPlaylistElement) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video playlist element not found'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot get this private video playlist.'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.videoPlaylistElementAP = videoPlaylistElement
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoPlaylistsReorderVideosValidator = [
|
||||
isValidPlaylistIdParam('playlistId'),
|
||||
|
||||
body('startPosition')
|
||||
.isInt({ min: 1 }),
|
||||
body('insertAfterPosition')
|
||||
.isInt({ min: 0 }),
|
||||
body('reorderLength')
|
||||
.optional()
|
||||
.isInt({ min: 1 }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
|
||||
|
||||
const videoPlaylist = getPlaylist(res)
|
||||
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.UPDATE_ANY_VIDEO_PLAYLIST, res)) return
|
||||
|
||||
const nextPosition = await VideoPlaylistElementModel.getNextPositionOf(videoPlaylist.id)
|
||||
const startPosition: number = req.body.startPosition
|
||||
const insertAfterPosition: number = req.body.insertAfterPosition
|
||||
const reorderLength: number = req.body.reorderLength
|
||||
|
||||
if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
|
||||
res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
|
||||
return
|
||||
}
|
||||
|
||||
if (reorderLength && reorderLength + startPosition > nextPosition) {
|
||||
res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
|
||||
return
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const commonVideoPlaylistFiltersValidator = [
|
||||
query('playlistType')
|
||||
.optional()
|
||||
.custom(isVideoPlaylistTypeValid),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const doVideosInPlaylistExistValidator = [
|
||||
query('videoIds')
|
||||
.customSanitizer(toIntArray)
|
||||
.custom(v => isArrayOf(v, isIdValid)).withMessage('Should have a valid video ids array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoPlaylistsAddValidator,
|
||||
videoPlaylistsUpdateValidator,
|
||||
videoPlaylistsDeleteValidator,
|
||||
videoPlaylistsGetValidator,
|
||||
videoPlaylistsSearchValidator,
|
||||
|
||||
videoPlaylistsAddVideoValidator,
|
||||
videoPlaylistsUpdateOrRemoveVideoValidator,
|
||||
videoPlaylistsReorderVideosValidator,
|
||||
|
||||
videoPlaylistElementAPGetValidator,
|
||||
|
||||
commonVideoPlaylistFiltersValidator,
|
||||
|
||||
doVideosInPlaylistExistValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getCommonPlaylistEditAttributes () {
|
||||
return [
|
||||
body('thumbnailfile')
|
||||
.custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile'))
|
||||
.withMessage(
|
||||
'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
|
||||
CONSTRAINTS_FIELDS.VIDEO_PLAYLISTS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
body('description')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoPlaylistDescriptionValid),
|
||||
body('privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoPlaylistPrivacyValid),
|
||||
body('videoChannelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
] as (ValidationChain | ExpressPromiseHandler)[]
|
||||
}
|
||||
|
||||
function checkUserCanManageVideoPlaylist (
|
||||
user: MUserAccountId,
|
||||
videoPlaylist: MVideoPlaylist,
|
||||
right: UserRightType,
|
||||
res: express.Response
|
||||
) {
|
||||
if (videoPlaylist.isOwned() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage video playlist of another server.'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the user can manage the video playlist
|
||||
// The user can delete it if s/he is an admin
|
||||
// Or if s/he is the video playlist's owner
|
||||
if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot manage video playlist of another user'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getPlaylist (res: express.Response) {
|
||||
return res.locals.videoPlaylistFull || res.locals.videoPlaylistSummary
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import express from 'express'
|
||||
import { body, param, query } from 'express-validator'
|
||||
import { HttpStatusCode, VideoRateType } from '@peertube/peertube-models'
|
||||
import { isAccountNameValid } from '../../../helpers/custom-validators/accounts.js'
|
||||
import { isIdValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { isRatingValid } from '../../../helpers/custom-validators/video-rates.js'
|
||||
import { isVideoRatingTypeValid } from '../../../helpers/custom-validators/videos.js'
|
||||
import { AccountVideoRateModel } from '../../../models/account/account-video-rate.js'
|
||||
import { areValidationErrors, checkCanSeeVideo, doesVideoExist, isValidVideoIdParam, isValidVideoPasswordHeader } from '../shared/index.js'
|
||||
|
||||
const videoUpdateRateValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
body('rating')
|
||||
.custom(isVideoRatingTypeValid),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, paramId: req.params.id, video: res.locals.videoAll })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) {
|
||||
return [
|
||||
param('name')
|
||||
.custom(isAccountNameValid),
|
||||
param('videoId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId)
|
||||
if (!rate) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video rate not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.accountVideoRate = rate
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const videoRatingValidator = [
|
||||
query('rating')
|
||||
.optional()
|
||||
.custom(isRatingValid).withMessage('Value must be one of "like" or "dislike"'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoUpdateRateValidator,
|
||||
getAccountVideoRateValidatorFactory,
|
||||
videoRatingValidator
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { canVideoBeFederated } from '@server/lib/activitypub/videos/federate.js'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import { isIdValid } from '../../../helpers/custom-validators/misc.js'
|
||||
import { VideoShareModel } from '../../../models/video/video-share.js'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
export const videosShareValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('actorId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (!canVideoBeFederated(video)) res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
const share = await VideoShareModel.load(req.params.actorId, video.id)
|
||||
if (!share) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
|
||||
res.locals.videoShare = share
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,131 @@
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { VideoSourceModel } from '@server/models/video/video-source.js'
|
||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { Metadata as UploadXMetadata } from '@uploadx/core'
|
||||
import express from 'express'
|
||||
import { param } from 'express-validator'
|
||||
import {
|
||||
areValidationErrors,
|
||||
checkCanAccessVideoSourceFile,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoExist,
|
||||
isValidVideoIdParam
|
||||
} from '../shared/index.js'
|
||||
import { addDurationToVideoFileIfNeeded, checkVideoFileCanBeEdited, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
||||
|
||||
export const videoSourceGetLatestValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res, 'all')) return
|
||||
|
||||
const video = getVideoWithAttributes(res) as MVideoFullLight
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return
|
||||
|
||||
res.locals.videoSource = await VideoSourceModel.loadLatest(video.id)
|
||||
|
||||
if (!res.locals.videoSource) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Video source not found'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const replaceVideoSourceResumableValidator = [
|
||||
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 checkCanUpdateVideoFile({ req, res })) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'updateVideoFileResumableValidator' })) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.update-file.accept.result' })) {
|
||||
return cleanup()
|
||||
}
|
||||
|
||||
res.locals.updateVideoFileResumable = { ...file, originalname: file.filename }
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const replaceVideoSourceResumableInitValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!await checkCanUpdateVideoFile({ req, res })) return
|
||||
|
||||
const fileMetadata = res.locals.uploadVideoFileResumableMetadata
|
||||
const files = { videofile: [ fileMetadata ] }
|
||||
if (await commonVideoFileChecks({ res, user, videoFileSize: fileMetadata.size, files }) === false) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const originalVideoFileDownloadValidator = [
|
||||
param('filename').exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const videoSource = await VideoSourceModel.loadByKeptOriginalFilename(req.params.filename)
|
||||
if (!videoSource) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Original video file not found'
|
||||
})
|
||||
}
|
||||
|
||||
if (!await checkCanAccessVideoSourceFile({ req, res, videoId: videoSource.videoId })) return
|
||||
|
||||
res.locals.videoSource = videoSource
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkCanUpdateVideoFile (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { req, res } = options
|
||||
|
||||
if (!CONFIG.VIDEO_FILE.UPDATE.ENABLED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Updating the file of an existing video is not allowed on this instance'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!await doesVideoExist(req.params.id, res)) return false
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return false
|
||||
|
||||
if (!checkVideoFileCanBeEdited(video, res)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import express from 'express'
|
||||
import { param, query } from 'express-validator'
|
||||
import { isDateValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isValidStatTimeserieMetric } from '@server/helpers/custom-validators/video-stats.js'
|
||||
import { STATS_TIMESERIE } from '@server/initializers/constants.js'
|
||||
import { HttpStatusCode, UserRight, VideoStatsTimeserieQuery } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkUserCanManageVideo, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const videoOverallStatsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
query('startDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await commonStatsCheck(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoRetentionStatsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await commonStatsCheck(req, res)) return
|
||||
|
||||
if (res.locals.videoAll.isLive) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot get retention stats of live video'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videoTimeserieStatsValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
param('metric')
|
||||
.custom(isValidStatTimeserieMetric),
|
||||
|
||||
query('startDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
query('endDate')
|
||||
.optional()
|
||||
.custom(isDateValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await commonStatsCheck(req, res)) return
|
||||
|
||||
const query: VideoStatsTimeserieQuery = req.query
|
||||
if (
|
||||
(query.startDate && !query.endDate) ||
|
||||
(!query.startDate && query.endDate)
|
||||
) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Both start date and end date should be defined if one of them is specified'
|
||||
})
|
||||
}
|
||||
|
||||
if (query.startDate && getIntervalByDays(query.startDate, query.endDate) > STATS_TIMESERIE.MAX_DAYS) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Star date and end date interval is too big'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoOverallStatsValidator,
|
||||
videoTimeserieStatsValidator,
|
||||
videoRetentionStatsValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function commonStatsCheck (req: express.Request, res: express.Response) {
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return false
|
||||
if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.SEE_ALL_VIDEOS, res)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function getIntervalByDays (startDateString: string, endDateString: string) {
|
||||
const startDate = new Date(startDateString)
|
||||
const endDate = new Date(endDateString)
|
||||
|
||||
return (endDate.getTime() - startDate.getTime()) / 1000 / 86400
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isIdOrUUIDValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import {
|
||||
isStudioCutTaskValid,
|
||||
isStudioTaskAddIntroOutroValid,
|
||||
isStudioTaskAddWatermarkValid,
|
||||
isValidStudioTasksArray
|
||||
} from '@server/helpers/custom-validators/video-studio.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { approximateIntroOutroAdditionalSize, getTaskFileFromReq } from '@server/lib/video-studio.js'
|
||||
import { isAudioFile } from '@peertube/peertube-ffmpeg'
|
||||
import { HttpStatusCode, UserRight, VideoStudioCreateEdition, VideoStudioTask } from '@peertube/peertube-models'
|
||||
import { areValidationErrors, checkUserCanManageVideo, checkUserQuota, doesVideoExist } from '../shared/index.js'
|
||||
import { checkVideoFileCanBeEdited } from './shared/index.js'
|
||||
|
||||
const videoStudioAddEditionValidator = [
|
||||
param('videoId')
|
||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid video id/uuid/short uuid'),
|
||||
|
||||
body('tasks')
|
||||
.custom(isValidStudioTasksArray).withMessage('Should have a valid array of tasks'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (CONFIG.VIDEO_STUDIO.ENABLED !== true) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Video studio is disabled on this instance'
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const body: VideoStudioCreateEdition = req.body
|
||||
const files = req.files as Express.Multer.File[]
|
||||
|
||||
for (let i = 0; i < body.tasks.length; i++) {
|
||||
const task = body.tasks[i]
|
||||
|
||||
if (!checkTask(req, task, i)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Task ${task.name} is invalid`
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
if (task.name === 'add-intro' || task.name === 'add-outro') {
|
||||
const filePath = getTaskFileFromReq(files, i).path
|
||||
|
||||
// Our concat filter needs a video stream
|
||||
if (await isAudioFile(filePath)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Task ${task.name} is invalid: file does not contain a video stream`
|
||||
})
|
||||
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!await doesVideoExist(req.params.videoId, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const video = res.locals.videoAll
|
||||
if (!checkVideoFileCanBeEdited(video, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, video, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
|
||||
|
||||
// Try to make an approximation of bytes added by the intro/outro
|
||||
const additionalBytes = await approximateIntroOutroAdditionalSize(video, body.tasks, i => getTaskFileFromReq(files, i).path)
|
||||
if (await checkUserQuota(user, additionalBytes, res) === false) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoStudioAddEditionValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const taskCheckers: {
|
||||
[id in VideoStudioTask['name']]: (task: VideoStudioTask, indice?: number, files?: Express.Multer.File[]) => boolean
|
||||
} = {
|
||||
'cut': isStudioCutTaskValid,
|
||||
'add-intro': isStudioTaskAddIntroOutroValid,
|
||||
'add-outro': isStudioTaskAddIntroOutroValid,
|
||||
'add-watermark': isStudioTaskAddWatermarkValid
|
||||
}
|
||||
|
||||
function checkTask (req: express.Request, task: VideoStudioTask, indice?: number) {
|
||||
const checker = taskCheckers[task.name]
|
||||
if (!checker) return false
|
||||
|
||||
return checker(task, indice, req.files as Express.Multer.File[])
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import express from 'express'
|
||||
import { HttpStatusCode, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { exists } from '@server/helpers/custom-validators/misc.js'
|
||||
|
||||
const videoFileTokenValidator = [
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const video = res.locals.onlyVideo
|
||||
if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED && !exists(res.locals.oauth.token.User)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: 'Not authenticated'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoFileTokenValidator
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HttpStatusCode, ServerErrorCode, VideoTranscodingCreate } from '@peertube/peertube-models'
|
||||
import { isBooleanValid, toBooleanOrNull } from '@server/helpers/custom-validators/misc.js'
|
||||
import { isValidCreateTranscodingType } from '@server/helpers/custom-validators/video-transcoding.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { VideoJobInfoModel } from '@server/models/video/video-job-info.js'
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
import { checkVideoCanBeTranscribedOrTranscripted } from './shared/video-validators.js'
|
||||
|
||||
const createTranscodingValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('transcodingType')
|
||||
.custom(isValidCreateTranscodingType),
|
||||
|
||||
body('forceTranscoding')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'all')) return
|
||||
|
||||
const video = res.locals.videoAll
|
||||
|
||||
if (!checkVideoCanBeTranscribedOrTranscripted(video, res)) return
|
||||
|
||||
if (CONFIG.TRANSCODING.ENABLED !== true) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot run transcoding job because transcoding is disabled on this instance'
|
||||
})
|
||||
}
|
||||
|
||||
const body = req.body as VideoTranscodingCreate
|
||||
if (body.forceTranscoding === true) return next()
|
||||
|
||||
const info = await VideoJobInfoModel.load(video.id)
|
||||
if (info && info.pendingTranscode > 0) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
type: ServerErrorCode.VIDEO_ALREADY_BEING_TRANSCODED,
|
||||
message: 'This video is already being transcoded'
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
createTranscodingValidator
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isVideoTimeValid } from '@server/helpers/custom-validators/video-view.js'
|
||||
import { getCachedVideoDuration } from '@server/lib/video.js'
|
||||
import { LocalVideoViewerModel } from '@server/models/view/local-video-viewer.js'
|
||||
import express from 'express'
|
||||
import { body, param } from 'express-validator'
|
||||
import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc.js'
|
||||
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared/index.js'
|
||||
|
||||
const tags = [ 'views' ]
|
||||
|
||||
export const getVideoLocalViewerValidator = [
|
||||
param('localViewerId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
|
||||
const localViewer = await LocalVideoViewerModel.loadFullById(+req.params.localViewerId)
|
||||
if (!localViewer) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Local viewer not found',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.localViewerFull = localViewer
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export const videoViewValidator = [
|
||||
isValidVideoIdParam('videoId'),
|
||||
|
||||
body('currentTime')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.isInt(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res, { tags })) return
|
||||
if (!await doesVideoExist(req.params.videoId, res, 'unsafe-only-immutable-attributes')) return
|
||||
|
||||
const video = res.locals.onlyImmutableVideo
|
||||
const { duration } = await getCachedVideoDuration(video.id)
|
||||
|
||||
const currentTime = req.body.currentTime
|
||||
if (!isVideoTimeValid(currentTime, duration)) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Current time ${currentTime} is invalid (video ${video.uuid} duration: ${duration})`,
|
||||
logLevel: 'warn',
|
||||
tags
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,559 @@
|
||||
import { arrayify } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, ServerErrorCode, UserRight, VideoState } from '@peertube/peertube-models'
|
||||
import { Redis } from '@server/lib/redis.js'
|
||||
import { buildUploadXFile, safeUploadXCleanup } from '@server/lib/uploadx.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { ExpressPromiseHandler } from '@server/types/express-handler.js'
|
||||
import { MUserAccountId, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { ValidationChain, body, param, query } from 'express-validator'
|
||||
import {
|
||||
exists,
|
||||
isBooleanValid,
|
||||
isDateValid,
|
||||
isFileValid,
|
||||
isIdValid,
|
||||
toBooleanOrNull,
|
||||
toIntOrNull,
|
||||
toValueOrNull
|
||||
} from '../../../helpers/custom-validators/misc.js'
|
||||
import { isBooleanBothQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search.js'
|
||||
import {
|
||||
areVideoTagsValid,
|
||||
isScheduleVideoUpdatePrivacyValid,
|
||||
isValidPasswordProtectedPrivacy,
|
||||
isVideoCategoryValid,
|
||||
isVideoCommentsPolicyValid,
|
||||
isVideoDescriptionValid,
|
||||
isVideoImageValid,
|
||||
isVideoIncludeValid,
|
||||
isVideoLanguageValid,
|
||||
isVideoLicenceValid,
|
||||
isVideoNameValid,
|
||||
isVideoOriginallyPublishedAtValid,
|
||||
isVideoPrivacyValid,
|
||||
isVideoSourceFilenameValid,
|
||||
isVideoSupportValid
|
||||
} from '../../../helpers/custom-validators/videos.js'
|
||||
import { cleanUpReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { getVideoWithAttributes } from '../../../helpers/video.js'
|
||||
import { CONFIG } from '../../../initializers/config.js'
|
||||
import { CONSTRAINTS_FIELDS, OVERVIEWS } from '../../../initializers/constants.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import {
|
||||
areValidationErrors, checkCanAccessVideoStaticFiles,
|
||||
checkCanSeeVideo,
|
||||
checkUserCanManageVideo,
|
||||
doesVideoChannelOfAccountExist,
|
||||
doesVideoExist,
|
||||
doesVideoFileOfVideoExist,
|
||||
isValidVideoIdParam,
|
||||
isValidVideoPasswordHeader
|
||||
} from '../shared/index.js'
|
||||
import { addDurationToVideoFileIfNeeded, commonVideoFileChecks, isVideoFileAccepted } from './shared/index.js'
|
||||
|
||||
const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
|
||||
body('videofile')
|
||||
.custom((_, { req }) => isFileValid({ files: req.files, field: 'videofile', mimeTypeRegex: null, maxSize: null }))
|
||||
.withMessage('Should have a file'),
|
||||
body('name')
|
||||
.trim()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const videoFile: express.VideoLegacyUploadFile = req.files['videofile'][0]
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (
|
||||
!await commonVideoChecksPass({ req, res, user, videoFileSize: videoFile.size, files: req.files }) ||
|
||||
!isValidPasswordProtectedPrivacy(req, res) ||
|
||||
!await addDurationToVideoFileIfNeeded({ videoFile, res, middlewareName: 'videosAddvideosAddLegacyValidatorResumableValidator' }) ||
|
||||
!await isVideoFileAccepted({ req, res, videoFile, hook: 'filter:api.video.upload.accept.result' })
|
||||
) {
|
||||
return cleanUpReqFiles(req)
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
/**
|
||||
* Gets called after the last PUT request
|
||||
*/
|
||||
const videosAddResumableValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
const file = buildUploadXFile(req.body as express.CustomUploadXFile<express.UploadNewVideoXFileMetadata>)
|
||||
const cleanup = () => safeUploadXCleanup(file)
|
||||
|
||||
const uploadId = req.query.upload_id
|
||||
const sessionExists = await Redis.Instance.doesUploadSessionExist(uploadId)
|
||||
|
||||
if (sessionExists) {
|
||||
res.setHeader('Retry-After', 300) // ask to retry after 5 min, knowing the upload_id is kept for up to 15 min after completion
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
|
||||
message: 'The upload is already being processed'
|
||||
})
|
||||
}
|
||||
|
||||
await Redis.Instance.setUploadSession(uploadId)
|
||||
|
||||
if (!await doesVideoChannelOfAccountExist(file.metadata.channelId, user, res)) return cleanup()
|
||||
if (!await addDurationToVideoFileIfNeeded({ videoFile: file, res, middlewareName: 'videosAddResumableValidator' })) return cleanup()
|
||||
if (!await isVideoFileAccepted({ req, res, videoFile: file, hook: 'filter:api.video.upload.accept.result' })) return cleanup()
|
||||
|
||||
res.locals.uploadVideoFileResumable = { ...file, originalname: file.filename }
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
/**
|
||||
* File is created in POST initialisation, and its body is saved as a 'metadata' field is saved by uploadx for later use.
|
||||
* see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/uploadx.ts
|
||||
*
|
||||
* Uploadx doesn't use next() until the upload completes, so this middleware has to be placed before uploadx
|
||||
* see https://github.com/kukhariev/node-uploadx/blob/dc9fb4a8ac5a6f481902588e93062f591ec6ef03/packages/core/src/handlers/base-handler.ts
|
||||
*
|
||||
*/
|
||||
const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
|
||||
body('filename')
|
||||
.custom(isVideoSourceFilenameValid),
|
||||
body('name')
|
||||
.trim()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
body('channelId')
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const user = res.locals.oauth.token.User
|
||||
const cleanup = () => cleanUpReqFiles(req)
|
||||
|
||||
logger.debug('Checking videosAddResumableInitValidator parameters and headers', {
|
||||
parameters: req.body,
|
||||
headers: req.headers,
|
||||
files: req.files
|
||||
})
|
||||
|
||||
if (areValidationErrors(req, res, { omitLog: true })) return cleanup()
|
||||
|
||||
const fileMetadata = res.locals.uploadVideoFileResumableMetadata
|
||||
const files = { videofile: [ fileMetadata ] }
|
||||
if (!await commonVideoChecksPass({ req, res, user, videoFileSize: fileMetadata.size, files })) return cleanup()
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanup()
|
||||
|
||||
// Multer required unsetting the Content-Type, now we can set it for node-uploadx
|
||||
req.headers['content-type'] = 'application/json; charset=utf-8'
|
||||
|
||||
// Place thumbnail/previewfile in metadata so that uploadx saves it in .META
|
||||
if (req.files?.['previewfile']) req.body.previewfile = req.files['previewfile']
|
||||
if (req.files?.['thumbnailfile']) req.body.thumbnailfile = req.files['thumbnailfile']
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
const videosUpdateValidator = getCommonVideoEditAttributes().concat([
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
body('name')
|
||||
.optional()
|
||||
.trim()
|
||||
.custom(isVideoNameValid).withMessage(
|
||||
`Should have a video name between ${CONSTRAINTS_FIELDS.VIDEOS.NAME.min} and ${CONSTRAINTS_FIELDS.VIDEOS.NAME.max} characters long`
|
||||
),
|
||||
body('channelId')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isIdValid),
|
||||
body('videoPasswords')
|
||||
.optional()
|
||||
.isArray()
|
||||
.withMessage('Video passwords should be an array.'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||
if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req)
|
||||
if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (!isValidPasswordProtectedPrivacy(req, res)) return cleanUpReqFiles(req)
|
||||
|
||||
const video = getVideoWithAttributes(res)
|
||||
if (exists(req.body.privacy) && video.isLive && video.privacy !== req.body.privacy && video.state !== VideoState.WAITING_FOR_LIVE) {
|
||||
return res.fail({ message: 'Cannot update privacy of a live that has already started' })
|
||||
}
|
||||
|
||||
// Check if the user who did the request is able to update the video
|
||||
const user = res.locals.oauth.token.User
|
||||
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req)
|
||||
|
||||
if (req.body.channelId && !await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
async function checkVideoFollowConstraints (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||
const video = getVideoWithAttributes(res)
|
||||
|
||||
// Anybody can watch local videos
|
||||
if (video.isOwned() === true) return next()
|
||||
|
||||
// Logged user
|
||||
if (res.locals.oauth) {
|
||||
// Users can search or watch remote videos
|
||||
if (CONFIG.SEARCH.REMOTE_URI.USERS === true) return next()
|
||||
}
|
||||
|
||||
// Anybody can search or watch remote videos
|
||||
if (CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true) return next()
|
||||
|
||||
// Check our instance follows an actor that shared this video
|
||||
const serverActor = await getServerActor()
|
||||
if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
|
||||
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Cannot get this video regarding follow constraints',
|
||||
type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS,
|
||||
data: {
|
||||
originUrl: video.url
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video-and-blacklist' | 'unsafe-only-immutable-attributes') => {
|
||||
return [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
isValidVideoPasswordHeader(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res, fetchType)) return
|
||||
|
||||
// Controllers does not need to check video rights
|
||||
if (fetchType === 'unsafe-only-immutable-attributes') return next()
|
||||
|
||||
const video = getVideoWithAttributes(res) as MVideoFullLight
|
||||
|
||||
if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const videosGetValidator = videosCustomGetValidator('all')
|
||||
|
||||
const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
param('videoFileId')
|
||||
.custom(isIdValid).not().isEmpty().withMessage('Should have a valid videoFileId'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoFileOfVideoExist(+req.params.videoFileId, req.params.id, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
])
|
||||
|
||||
const videosDownloadValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res, 'all')) return
|
||||
|
||||
const video = getVideoWithAttributes(res)
|
||||
|
||||
if (!await checkCanAccessVideoStaticFiles({ req, res, video, paramId: req.params.id })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosRemoveValidator = [
|
||||
isValidVideoIdParam('id'),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesVideoExist(req.params.id, res)) return
|
||||
|
||||
// Check if the user who did the request is able to delete the video
|
||||
if (!checkUserCanManageVideo(res.locals.oauth.token.User, res.locals.videoAll, UserRight.REMOVE_ANY_VIDEO, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
const videosOverviewValidator = [
|
||||
query('page')
|
||||
.optional()
|
||||
.isInt({ min: 1, max: OVERVIEWS.VIDEOS.SAMPLES_COUNT }),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
function getCommonVideoEditAttributes () {
|
||||
return [
|
||||
body('thumbnailfile')
|
||||
.custom((value, { req }) => isVideoImageValid(req.files, 'thumbnailfile')).withMessage(
|
||||
'This thumbnail file is not supported or too large. Please, make sure it is of the following type: ' +
|
||||
CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
body('previewfile')
|
||||
.custom((value, { req }) => isVideoImageValid(req.files, 'previewfile')).withMessage(
|
||||
'This preview file is not supported or too large. Please, make sure it is of the following type: ' +
|
||||
CONSTRAINTS_FIELDS.VIDEOS.IMAGE.EXTNAME.join(', ')
|
||||
),
|
||||
|
||||
body('category')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoCategoryValid),
|
||||
body('licence')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoLicenceValid),
|
||||
body('language')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoLanguageValid),
|
||||
body('nsfw')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid nsfw boolean'),
|
||||
body('waitTranscoding')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'),
|
||||
body('privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isVideoPrivacyValid),
|
||||
body('description')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoDescriptionValid),
|
||||
body('support')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoSupportValid),
|
||||
body('tags')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(areVideoTagsValid)
|
||||
.withMessage(
|
||||
`Should have an array of up to ${CONSTRAINTS_FIELDS.VIDEOS.TAGS.max} tags between ` +
|
||||
`${CONSTRAINTS_FIELDS.VIDEOS.TAG.min} and ${CONSTRAINTS_FIELDS.VIDEOS.TAG.max} characters each`
|
||||
),
|
||||
// TODO: remove, deprecated in PeerTube 6.2
|
||||
body('commentsEnabled')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have valid commentsEnabled boolean'),
|
||||
body('commentsPolicy')
|
||||
.optional()
|
||||
.custom(isVideoCommentsPolicyValid),
|
||||
|
||||
body('downloadEnabled')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have downloadEnabled boolean'),
|
||||
body('originallyPublishedAt')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull)
|
||||
.custom(isVideoOriginallyPublishedAtValid),
|
||||
body('scheduleUpdate')
|
||||
.optional()
|
||||
.customSanitizer(toValueOrNull),
|
||||
body('scheduleUpdate.updateAt')
|
||||
.optional()
|
||||
.custom(isDateValid).withMessage('Should have a schedule update date that conforms to ISO 8601'),
|
||||
body('scheduleUpdate.privacy')
|
||||
.optional()
|
||||
.customSanitizer(toIntOrNull)
|
||||
.custom(isScheduleVideoUpdatePrivacyValid)
|
||||
] as (ValidationChain | ExpressPromiseHandler)[]
|
||||
}
|
||||
|
||||
const commonVideosFiltersValidator = [
|
||||
query('categoryOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid categoryOneOf array'),
|
||||
query('licenceOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid licenceOneOf array'),
|
||||
query('languageOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid languageOneOf array'),
|
||||
query('privacyOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isNumberArray).withMessage('Should have a valid privacyOneOf array'),
|
||||
query('tagsOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid tagsOneOf array'),
|
||||
query('tagsAllOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid tagsAllOf array'),
|
||||
query('nsfw')
|
||||
.optional()
|
||||
.custom(isBooleanBothQueryValid),
|
||||
query('isLive')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid isLive boolean'),
|
||||
query('include')
|
||||
.optional()
|
||||
.custom(isVideoIncludeValid),
|
||||
query('isLocal')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid isLocal boolean'),
|
||||
query('hasHLSFiles')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid hasHLSFiles boolean'),
|
||||
query('hasWebtorrentFiles') // TODO: remove in v7
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid hasWebtorrentFiles boolean'),
|
||||
query('hasWebVideoFiles')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid hasWebVideoFiles boolean'),
|
||||
query('skipCount')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.custom(isBooleanValid).withMessage('Should have a valid skipCount boolean'),
|
||||
query('search')
|
||||
.optional()
|
||||
.custom(exists),
|
||||
query('excludeAlreadyWatched')
|
||||
.optional()
|
||||
.customSanitizer(toBooleanOrNull)
|
||||
.isBoolean().withMessage('Should be a valid excludeAlreadyWatched boolean'),
|
||||
query('autoTagOneOf')
|
||||
.optional()
|
||||
.customSanitizer(arrayify)
|
||||
.custom(isStringArray).withMessage('Should have a valid autoTagOneOf array'),
|
||||
|
||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const user = res.locals.oauth?.token.User
|
||||
|
||||
if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
|
||||
if (req.query.include || req.query.privacyOneOf || req.query.autoTagOneOf) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: 'You are not allowed to see all videos, specify a custom include or auto tags filter.'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!user && exists(req.query.excludeAlreadyWatched)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: 'Cannot use excludeAlreadyWatched parameter when auth token is not provided'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (req.query.filter) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: '"filter" query parameter is not supported anymore by PeerTube. Please use "isLocal" and "include" instead'
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
checkVideoFollowConstraints,
|
||||
commonVideosFiltersValidator,
|
||||
getCommonVideoEditAttributes,
|
||||
videoFileMetadataGetValidator,
|
||||
videosAddLegacyValidator,
|
||||
videosAddResumableInitValidator,
|
||||
videosAddResumableValidator,
|
||||
videosCustomGetValidator,
|
||||
videosDownloadValidator,
|
||||
videosGetValidator,
|
||||
videosOverviewValidator,
|
||||
videosRemoveValidator,
|
||||
videosUpdateValidator
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function areErrorsInScheduleUpdate (req: express.Request, res: express.Response) {
|
||||
if (req.body.scheduleUpdate) {
|
||||
if (!req.body.scheduleUpdate.updateAt) {
|
||||
logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
|
||||
|
||||
res.fail({ message: 'Schedule update at is mandatory.' })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function commonVideoChecksPass (options: {
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
user: MUserAccountId
|
||||
videoFileSize: number
|
||||
files: express.UploadFilesForCheck
|
||||
}): Promise<boolean> {
|
||||
const { req, res, user } = options
|
||||
|
||||
if (areErrorsInScheduleUpdate(req, res)) return false
|
||||
|
||||
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
|
||||
|
||||
if (!await commonVideoFileChecks(options)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { Awaitable } from '@peertube/peertube-typescript-utils'
|
||||
import { isIdValid } from '@server/helpers/custom-validators/misc.js'
|
||||
import { areWatchedWordsValid, isWatchedWordListNameValid } from '@server/helpers/custom-validators/watched-words.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import { WatchedWordsListModel } from '@server/models/watched-words/watched-words-list.js'
|
||||
import { MAccountId, MWatchedWordsList } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { ValidationChain, body, param } from 'express-validator'
|
||||
import { doesAccountNameWithHostExist } from './shared/accounts.js'
|
||||
import { checkUserCanManageAccount } from './shared/users.js'
|
||||
import { areValidationErrors } from './shared/utils.js'
|
||||
|
||||
export const manageAccountWatchedWordsListValidator = [
|
||||
param('accountName')
|
||||
.exists(),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
if (!await doesAccountNameWithHostExist(req.params.accountName, res)) return
|
||||
if (!checkUserCanManageAccount({ user: res.locals.oauth.token.User, account: res.locals.account, specialRight: null, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
export function getWatchedWordsListValidatorFactory (accountGetter: (res: express.Response) => Awaitable<MAccountId>) {
|
||||
return [
|
||||
param('listId')
|
||||
.custom(isIdValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const watchedWordsList = await WatchedWordsListModel.load({ id: +req.params.listId, accountId: (await accountGetter(res)).id })
|
||||
if (!watchedWordsList) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Unknown watched words list id for this account'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.watchedWordsList = watchedWordsList
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function buildUpdateOrAddValidators ({ optional }: { optional: boolean }) {
|
||||
const makeOptionalIfNeeded = (chain: ValidationChain) => {
|
||||
if (optional) return chain.optional()
|
||||
|
||||
return chain
|
||||
}
|
||||
|
||||
return [
|
||||
makeOptionalIfNeeded(body('listName'))
|
||||
.trim()
|
||||
.custom(isWatchedWordListNameValid).withMessage(
|
||||
`Should have a list name between ` +
|
||||
`${CONSTRAINTS_FIELDS.WATCHED_WORDS.LIST_NAME.min} and ${CONSTRAINTS_FIELDS.WATCHED_WORDS.LIST_NAME.max} characters long`
|
||||
),
|
||||
|
||||
makeOptionalIfNeeded(body('words'))
|
||||
.custom(areWatchedWordsValid)
|
||||
.withMessage(
|
||||
`Should have an array of up to ${CONSTRAINTS_FIELDS.WATCHED_WORDS.WORDS.max} words between ` +
|
||||
`${CONSTRAINTS_FIELDS.WATCHED_WORDS.WORD.min} and ${CONSTRAINTS_FIELDS.WATCHED_WORDS.WORD.max} characters each`
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
export function addWatchedWordsListValidatorFactory (accountGetter: (res: express.Response) => Awaitable<MAccountId>) {
|
||||
return [
|
||||
...buildUpdateOrAddValidators({ optional: false }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const listName = req.body.listName
|
||||
if (!await checkListNameIsUnique({ accountId: (await accountGetter(res)).id, listName, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export function updateWatchedWordsListValidatorFactory (accountGetter: (res: express.Response) => Awaitable<MAccountId>) {
|
||||
return [
|
||||
...buildUpdateOrAddValidators({ optional: true }),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
const currentList = res.locals.watchedWordsList
|
||||
const listName = req.body.listName
|
||||
if (listName && !await checkListNameIsUnique({ accountId: (await accountGetter(res)).id, listName, currentList, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkListNameIsUnique (options: {
|
||||
accountId: number
|
||||
listName: string
|
||||
res: express.Response
|
||||
currentList?: MWatchedWordsList
|
||||
}) {
|
||||
const { accountId, listName, currentList, res } = options
|
||||
|
||||
const existing = await WatchedWordsListModel.loadByListName({ accountId, listName })
|
||||
if (existing && (!currentList || currentList.id !== existing.id)) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: `Watched words list with name ${listName} already exists`
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import express from 'express'
|
||||
import { query } from 'express-validator'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger.js'
|
||||
import { getHostWithPort } from '../../helpers/express-utils.js'
|
||||
import { ActorModel } from '../../models/actor/actor.js'
|
||||
import { areValidationErrors } from './shared/index.js'
|
||||
|
||||
const webfingerValidator = [
|
||||
query('resource')
|
||||
.custom(isWebfingerLocalResourceValid),
|
||||
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (areValidationErrors(req, res)) return
|
||||
|
||||
// Remove 'acct:' from the beginning of the string
|
||||
const nameWithHost = getHostWithPort(req.query.resource.substr(5))
|
||||
const [ name ] = nameWithHost.split('@')
|
||||
|
||||
const actor = await ActorModel.loadLocalUrlByName(name)
|
||||
if (!actor) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: 'Actor not found'
|
||||
})
|
||||
}
|
||||
|
||||
res.locals.actorUrl = actor
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
webfingerValidator
|
||||
}
|
||||
新しい課題から参照
ユーザをブロックする