Init commit
This commit is contained in:
26
server/core/middlewares/validators/shared/abuses.ts
Normal file
26
server/core/middlewares/validators/shared/abuses.ts
Normal file
@@ -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
|
||||
}
|
||||
76
server/core/middlewares/validators/shared/accounts.ts
Normal file
76
server/core/middlewares/validators/shared/accounts.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight } from '@peertube/peertube-models'
|
||||
import { AccountModel } from '@server/models/account/account.js'
|
||||
import { MAccountDefault } from '@server/types/models/index.js'
|
||||
import { Request, Response } from 'express'
|
||||
import { checkCanManageAccount } from './users.js'
|
||||
|
||||
export async function doesAccountIdExist (options: {
|
||||
id: string | number
|
||||
req: Request
|
||||
res: Response
|
||||
checkCanManage: boolean // Also check the user can manage the account
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}) {
|
||||
const { id, req, res, checkIsLocal, checkCanManage } = options
|
||||
|
||||
const account = await AccountModel.load(forceNumber(id))
|
||||
|
||||
return doesAccountExist({ account, req, res, checkIsLocal, checkCanManage })
|
||||
}
|
||||
|
||||
export async function doesAccountHandleExist (options: {
|
||||
handle: string
|
||||
req: Request
|
||||
res: Response
|
||||
checkCanManage: boolean // Also check the user can manage the account
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}) {
|
||||
const { handle, req, res, checkIsLocal, checkCanManage } = options
|
||||
|
||||
const account = await AccountModel.loadByHandle(handle)
|
||||
|
||||
return doesAccountExist({ account, req, res, checkIsLocal, checkCanManage })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function doesAccountExist (options: {
|
||||
account: MAccountDefault
|
||||
req: Request
|
||||
res: Response
|
||||
checkCanManage: boolean
|
||||
checkIsLocal: boolean
|
||||
}) {
|
||||
const { account, req, res, checkIsLocal, checkCanManage } = options
|
||||
|
||||
if (!account) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Account not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkCanManage) {
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (!checkCanManageAccount({ account, user, req, res, specialRight: UserRight.MANAGE_USERS })) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (checkIsLocal && account.Actor.isLocal() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This account is not owned by the platform')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.account = account
|
||||
return true
|
||||
}
|
||||
19
server/core/middlewares/validators/shared/images.ts
Normal file
19
server/core/middlewares/validators/shared/images.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { isActorImageFile } from '@server/helpers/custom-validators/actor-images.js'
|
||||
import { cleanUpReqFiles } from '@server/helpers/express-utils.js'
|
||||
import { CONSTRAINTS_FIELDS } from '@server/initializers/constants.js'
|
||||
import express from 'express'
|
||||
import { body } from 'express-validator'
|
||||
import { areValidationErrors } from './utils.js'
|
||||
|
||||
export 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()
|
||||
}
|
||||
]
|
||||
15
server/core/middlewares/validators/shared/index.ts
Normal file
15
server/core/middlewares/validators/shared/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export * from './abuses.js'
|
||||
export * from './accounts.js'
|
||||
export * from './images.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'
|
||||
150
server/core/middlewares/validators/shared/users.ts
Normal file
150
server/core/middlewares/validators/shared/users.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRightType } from '@peertube/peertube-models'
|
||||
import { loadReservedActorName } from '@server/lib/local-actor.js'
|
||||
import { getByEmailPermissive } from '@server/lib/user.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 checkUserEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(
|
||||
async () => {
|
||||
const users = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(users, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export function checkUserPendingEmailExistPermissive (email: string, res: express.Response, abortResponse = true) {
|
||||
return checkUserExist(
|
||||
async () => {
|
||||
const users = await UserModel.loadByPendingEmailCaseInsensitive(email)
|
||||
|
||||
return getByEmailPermissive(users, email)
|
||||
},
|
||||
res,
|
||||
abortResponse
|
||||
)
|
||||
}
|
||||
|
||||
export async function checkUsernameOrEmailDoNotAlreadyExist (options: {
|
||||
username: string
|
||||
email: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { username, email, req, res } = options
|
||||
|
||||
const existingUser = await UserModel.loadByUsernameOrEmailCaseInsensitive(username)
|
||||
const existingEmail = await UserModel.loadByUsernameOrEmailCaseInsensitive(email)
|
||||
|
||||
if (existingUser.length > 0 || existingEmail.length > 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('User with this username or email already exists.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const actor = await loadReservedActorName(username)
|
||||
if (actor) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('Another actor (account/channel) with this name on this instance already exists or has already existed.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkEmailDoesNotAlreadyExist (options: {
|
||||
email: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { email, req, res } = options
|
||||
const user = await UserModel.loadByEmailCaseInsensitive(email)
|
||||
|
||||
if (user.length !== 0) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: req.t('User with this email already exists.')
|
||||
})
|
||||
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.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
}
|
||||
|
||||
export function checkCanManageAccount (options: {
|
||||
user: MUserAccountId
|
||||
account: MAccountId
|
||||
specialRight: UserRightType
|
||||
req: express.Request
|
||||
res: express.Response | null
|
||||
}) {
|
||||
const { user, account, specialRight, res, req } = options
|
||||
|
||||
if (!user) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('Authentication is required')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (account.id === user.Account.id) return true
|
||||
if (specialRight && user.hasRight(specialRight) === true) return true
|
||||
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Only a user with sufficient right can manage this account resource.')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export async function doesUserFeedTokenCorrespond (options: {
|
||||
id: number
|
||||
token: string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { id, token, req, res } = options
|
||||
const user = await UserModel.loadByIdWithChannels(forceNumber(id))
|
||||
|
||||
if (token !== user.feedToken) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('User and token mismatch')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.user = user
|
||||
return true
|
||||
}
|
||||
69
server/core/middlewares/validators/shared/utils.ts
Normal file
69
server/core/middlewares/validators/shared/utils.ts
Normal file
@@ -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
|
||||
}
|
||||
25
server/core/middlewares/validators/shared/video-captions.ts
Normal file
25
server/core/middlewares/validators/shared/video-captions.ts
Normal file
@@ -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
|
||||
}
|
||||
124
server/core/middlewares/validators/shared/video-channels.ts
Normal file
124
server/core/middlewares/validators/shared/video-channels.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { HttpStatusCode, UserRight, UserRightType } from '@peertube/peertube-models'
|
||||
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||
import { VideoChannelModel } from '@server/models/video/video-channel.js'
|
||||
import { MChannelBannerAccountDefault, MChannelUserId, MUserAccountId } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { checkCanManageAccount } from './users.js'
|
||||
|
||||
type CommonOptions = {
|
||||
checkCanManage: boolean // Also check the user can manage the account
|
||||
checkIsOwner: boolean // Also check this is the owner of the channel
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
specialRight?: UserRightType
|
||||
}
|
||||
|
||||
export async function doesChannelIdExist (
|
||||
options: CommonOptions & {
|
||||
id: number
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}
|
||||
) {
|
||||
const { id, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight } = options
|
||||
|
||||
const channel = await VideoChannelModel.loadAndPopulateAccount(+id)
|
||||
|
||||
return processVideoChannelExist({ channel, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight })
|
||||
}
|
||||
|
||||
export async function doesChannelHandleExist (
|
||||
options: CommonOptions & {
|
||||
handle: string
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}
|
||||
) {
|
||||
const { handle, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight } = options
|
||||
|
||||
const channel = await VideoChannelModel.loadByHandleAndPopulateAccount(handle)
|
||||
|
||||
return processVideoChannelExist({ channel, checkCanManage, checkIsLocal, checkIsOwner, req, res, specialRight })
|
||||
}
|
||||
|
||||
export async function checkCanManageChannel (
|
||||
options: CommonOptions & {
|
||||
user: MUserAccountId
|
||||
channel: MChannelUserId
|
||||
}
|
||||
) {
|
||||
const { channel, user, req, res, checkCanManage, checkIsOwner, specialRight = UserRight.MANAGE_ANY_VIDEO_CHANNEL } = options
|
||||
|
||||
if (!channel) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video channel not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkIsOwner || checkCanManage) {
|
||||
if (!user) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('Authentication is required')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const isOwner = checkCanManageAccount({
|
||||
account: channel.Account,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
specialRight
|
||||
})
|
||||
|
||||
if (!isOwner) {
|
||||
if (checkIsOwner) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This user has not owner rights on this channel')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkCanManage && !await VideoChannelCollaboratorModel.isCollaborator({ user, channel })) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('This user cannot manage this channel')
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function processVideoChannelExist (
|
||||
options: CommonOptions & {
|
||||
channel: MChannelBannerAccountDefault
|
||||
checkIsLocal: boolean // Also check this is a local channel
|
||||
}
|
||||
) {
|
||||
const { channel, req, res, checkCanManage, checkIsLocal, checkIsOwner, specialRight = UserRight.MANAGE_ANY_VIDEO_CHANNEL } = options
|
||||
|
||||
const user = res.locals.oauth?.token.User
|
||||
if (!await checkCanManageChannel({ channel, user, req, res, checkCanManage, checkIsOwner, specialRight })) return false
|
||||
|
||||
if (checkIsLocal && channel.Actor.isLocal() === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('The channel must be local.')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChannel = channel
|
||||
return true
|
||||
}
|
||||
80
server/core/middlewares/validators/shared/video-comments.ts
Normal file
80
server/core/middlewares/validators/shared/video-comments.ts
Normal file
@@ -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
|
||||
}
|
||||
22
server/core/middlewares/validators/shared/video-imports.ts
Normal file
22
server/core/middlewares/validators/shared/video-imports.ts
Normal file
@@ -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,21 @@
|
||||
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'
|
||||
|
||||
export async function doesChangeVideoOwnershipExist (idArg: number | string, req: express.Request, res: express.Response) {
|
||||
const id = forceNumber(idArg)
|
||||
const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
|
||||
|
||||
if (!videoChangeOwnership) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video ownership change not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoChangeOwnership = videoChangeOwnership
|
||||
|
||||
return true
|
||||
}
|
||||
72
server/core/middlewares/validators/shared/video-passwords.ts
Normal file
72
server/core/middlewares/validators/shared/video-passwords.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, UserRight, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { MUserAccountId, MVideoAccountLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { header } from 'express-validator'
|
||||
import { checkCanManageVideo } from './videos.js'
|
||||
|
||||
export function isValidVideoPasswordHeader () {
|
||||
return header('x-peertube-video-password')
|
||||
.optional()
|
||||
.isString()
|
||||
}
|
||||
|
||||
export function checkVideoIsPasswordProtected (req: express.Request, res: express.Response) {
|
||||
const video = getVideoWithAttributes(res)
|
||||
if (video.privacy !== VideoPrivacy.PASSWORD_PROTECTED) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Video is not password protected')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function doesVideoPasswordExist (options: {
|
||||
id: number | string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { req, res } = options
|
||||
|
||||
const video = getVideoWithAttributes(res)
|
||||
const id = forceNumber(options.id)
|
||||
const videoPassword = await VideoPasswordModel.loadByIdAndVideo({ id, videoId: video.id })
|
||||
|
||||
if (!videoPassword) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video password not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
res.locals.videoPassword = videoPassword
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function checkCanDeleteVideoPassword (options: {
|
||||
user: MUserAccountId
|
||||
video: MVideoAccountLight
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
}) {
|
||||
const { user, video, req, res } = options
|
||||
|
||||
const passwordCount = await VideoPasswordModel.countByVideoId(video.id)
|
||||
|
||||
if (passwordCount <= 1) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.BAD_REQUEST_400,
|
||||
message: req.t('Cannot delete the last password of the protected video')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return checkCanManageVideo({ user, video, right: UserRight.UPDATE_ANY_VIDEO, req, res, checkIsLocal: true, checkIsOwner: false })
|
||||
}
|
||||
42
server/core/middlewares/validators/shared/video-playlists.ts
Normal file
42
server/core/middlewares/validators/shared/video-playlists.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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'
|
||||
|
||||
export async function doesVideoPlaylistExist (options: {
|
||||
id: number | string
|
||||
req: express.Request
|
||||
res: express.Response
|
||||
fetchType?: VideoPlaylistFetchType
|
||||
}) {
|
||||
const { id, req, res, fetchType = 'summary' } = options
|
||||
|
||||
if (fetchType === 'summary') {
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(id, undefined)
|
||||
res.locals.videoPlaylistSummary = videoPlaylist
|
||||
|
||||
return handleVideoPlaylist(videoPlaylist, req, res)
|
||||
}
|
||||
|
||||
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannel(id, undefined)
|
||||
res.locals.videoPlaylistFull = videoPlaylist
|
||||
|
||||
return handleVideoPlaylist(videoPlaylist, req, res)
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
// Private
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function handleVideoPlaylist (playlist: MVideoPlaylist, req: express.Request, res: express.Response) {
|
||||
if (!playlist) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.NOT_FOUND_404,
|
||||
message: req.t('Video playlist not found')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
364
server/core/middlewares/validators/shared/videos.ts
Normal file
364
server/core/middlewares/validators/shared/videos.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
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 { authenticateOrFail } from '@server/middlewares/auth.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 {
|
||||
MUserAccountId,
|
||||
MUserAccountUrl,
|
||||
MUserId,
|
||||
MVideo,
|
||||
MVideoAccountLight,
|
||||
MVideoFormattableDetails,
|
||||
MVideoFullLight,
|
||||
MVideoId,
|
||||
MVideoImmutable,
|
||||
MVideoThumbnailBlacklist,
|
||||
MVideoUUID,
|
||||
MVideoWithRights
|
||||
} from '@server/types/models/index.js'
|
||||
import { Request, Response } from 'express'
|
||||
import { checkCanManageChannel } from './video-channels.js'
|
||||
|
||||
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.sendStatus(HttpStatusCode.NOT_FOUND_404)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanSeeVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
video: MVideo
|
||||
videoFileToken?: {
|
||||
user: MUserAccountUrl
|
||||
}
|
||||
}) {
|
||||
const { req, res, video, videoFileToken, paramId } = options
|
||||
|
||||
if (video.requiresUserAuth({ urlParamId: paramId, checkBlacklist: true })) {
|
||||
return checkCanSeeUserAuthVideo({ req, res, video, videoFileTokenUser: videoFileToken?.user })
|
||||
}
|
||||
|
||||
if (video.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
|
||||
return checkCanSeePasswordProtectedVideo({ req, res, video, hasVideoFileToken: !!videoFileToken })
|
||||
}
|
||||
|
||||
if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) {
|
||||
return true
|
||||
}
|
||||
|
||||
throw new Error('Unknown video privacy when checking video right ' + video.url)
|
||||
}
|
||||
|
||||
async function checkCanSeeUserAuthVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideoId | MVideoWithRights
|
||||
videoFileTokenUser?: MUserAccountUrl
|
||||
}) {
|
||||
const { req, res, video } = options
|
||||
|
||||
const fail = () => {
|
||||
res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot fetch information of private/internal/blocked video')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let user = options.videoFileTokenUser
|
||||
if (!user) {
|
||||
if (!await authenticateOrFail({ req, res })) return false
|
||||
|
||||
user = res.locals.oauth.token.User
|
||||
}
|
||||
|
||||
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 (await canUserManageProtectedVideo({ user, req, video: videoWithRights, right: UserRight.MANAGE_VIDEO_BLACKLIST })) return true
|
||||
|
||||
return fail()
|
||||
}
|
||||
|
||||
if (privacy === VideoPrivacy.PRIVATE || privacy === VideoPrivacy.UNLISTED) {
|
||||
if (await canUserManageProtectedVideo({ user, req, video: videoWithRights, right: UserRight.SEE_ALL_VIDEOS })) return true
|
||||
|
||||
return fail()
|
||||
}
|
||||
|
||||
// Should not happen
|
||||
return fail()
|
||||
}
|
||||
|
||||
async function checkCanSeePasswordProtectedVideo (options: {
|
||||
req: Request
|
||||
res: Response
|
||||
video: MVideo
|
||||
hasVideoFileToken: boolean
|
||||
}) {
|
||||
const { req, res, video, hasVideoFileToken } = options
|
||||
|
||||
const videoWithRights = await getVideoWithRights(video as MVideoWithRights)
|
||||
|
||||
const videoPassword = req.header('x-peertube-video-password')
|
||||
|
||||
if (!exists(videoPassword)) {
|
||||
if (hasVideoFileToken === true) return true
|
||||
|
||||
const errorMessage = req.t('Please provide a password to access this password protected video')
|
||||
const errorType = ServerErrorCode.VIDEO_REQUIRES_PASSWORD
|
||||
|
||||
if (!await authenticateOrFail({ req, res, errorMessage, errorStatus: HttpStatusCode.UNAUTHORIZED_401, errorType })) return false
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
if (await canUserManageProtectedVideo({ user, req, video: videoWithRights, right: UserRight.SEE_ALL_VIDEOS })) return true
|
||||
|
||||
res.fail({
|
||||
status: user
|
||||
? HttpStatusCode.FORBIDDEN_403
|
||||
: HttpStatusCode.UNAUTHORIZED_401,
|
||||
|
||||
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: req.t('Incorrect video password. Access to the video is denied')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function canUserManageProtectedVideo (options: {
|
||||
req: Request
|
||||
user: MUserAccountId
|
||||
video: MVideoWithRights | MVideoAccountLight
|
||||
right: UserRightType
|
||||
}) {
|
||||
const { user, video, right, req } = options
|
||||
|
||||
return checkCanManageChannel({
|
||||
channel: video.VideoChannel,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
checkCanManage: true,
|
||||
checkIsOwner: false,
|
||||
specialRight: right
|
||||
})
|
||||
}
|
||||
|
||||
async function getVideoWithRights (video: MVideoWithRights): Promise<MVideoWithRights> {
|
||||
const channel = video.VideoChannel
|
||||
|
||||
if (channel?.id && channel?.Account?.userId && channel?.Account?.id) return video
|
||||
|
||||
return VideoModel.loadFull(video.id)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanAccessVideoStaticFiles (options: {
|
||||
video: MVideo
|
||||
req: Request
|
||||
res: Response
|
||||
paramId: string
|
||||
}): Promise<boolean> {
|
||||
const { video, req, res } = options
|
||||
|
||||
if (!checkVideoTokenIfNeeded(req, res, video)) return false
|
||||
|
||||
return checkCanSeeVideo({ ...options, videoFileToken: res.locals.videoFileToken })
|
||||
}
|
||||
|
||||
export async function checkCanAccessVideoSourceFile (options: {
|
||||
videoId: number
|
||||
req: Request
|
||||
res: Response
|
||||
}): Promise<boolean> {
|
||||
const { req, res, videoId } = options
|
||||
|
||||
const video = await VideoModel.loadFull(videoId)
|
||||
|
||||
let user = res.locals.oauth?.token.User
|
||||
if (!user) {
|
||||
if (!checkVideoTokenIfNeeded(req, res, video)) return false
|
||||
|
||||
user = res.locals.videoFileToken?.user
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
res.fail({ status: HttpStatusCode.UNAUTHORIZED_401, message: req.t('Authentication is required to access the video source file') })
|
||||
return false
|
||||
}
|
||||
|
||||
if (await canUserManageProtectedVideo({ user, req, video, right: UserRight.SEE_ALL_VIDEOS }) === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
|
||||
return false
|
||||
}
|
||||
|
||||
function checkVideoTokenIfNeeded (req: Request, res: Response, video: MVideoUUID) {
|
||||
const videoFileToken = req.query.videoFileToken
|
||||
|
||||
if (videoFileToken) {
|
||||
if (VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) {
|
||||
const user = VideoTokensManager.Instance.getUserFromToken({ token: videoFileToken })
|
||||
|
||||
res.locals.videoFileToken = { user }
|
||||
} else {
|
||||
res.fail({
|
||||
message: req.t('Invalid video file token'),
|
||||
status: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkCanManageVideo (options: {
|
||||
user: MUserAccountId
|
||||
video: MVideoAccountLight
|
||||
right: UserRightType
|
||||
req: Request
|
||||
res: Response | null
|
||||
checkIsLocal: boolean
|
||||
checkIsOwner: boolean
|
||||
}) {
|
||||
const { user, video, right, req, res, checkIsLocal, checkIsOwner } = options
|
||||
|
||||
if (!user) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.UNAUTHORIZED_401,
|
||||
message: req.t('Authentication is required.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (checkIsLocal && video.isLocal() === false) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage a video of another server.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
!await checkCanManageChannel({
|
||||
channel: video.VideoChannel,
|
||||
user,
|
||||
req,
|
||||
res: null,
|
||||
checkCanManage: true,
|
||||
checkIsOwner,
|
||||
specialRight: right
|
||||
})
|
||||
) {
|
||||
res?.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: req.t('Cannot manage a video of another user.')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type NewType = MUserId
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function checkUserQuota (options: {
|
||||
user: NewType
|
||||
videoFileSize: number
|
||||
req: Request
|
||||
res: Response
|
||||
}) {
|
||||
const { user, videoFileSize, req, res } = options
|
||||
|
||||
if (await isUserQuotaValid({ userId: user.id, uploadSize: videoFileSize }) === false) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
|
||||
message: req.t('The user video quota is exceeded with this video'),
|
||||
type: ServerErrorCode.QUOTA_REACHED
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user