1
0
Fork 0
mirror of https://github.com/Chocobozzz/PeerTube.git synced 2025-10-05 19:42:24 +02:00

Feature/password protected videos (#5836)

* Add server endpoints

* Refactoring test suites

* Update server and add openapi documentation

* fix compliation and tests

* upload/import password protected video on client

* add server error code

* Add video password to update resolver

* add custom message when sharing pw protected video

* improve confirm component

* Add new alert in component

* Add ability to watch protected video on client

* Cannot have password protected replay privacy

* Add migration

* Add tests

* update after review

* Update check params tests

* Add live videos test

* Add more filter test

* Update static file privacy test

* Update object storage tests

* Add test on feeds

* Add missing word

* Fix tests

* Fix tests on live videos

* add embed support on password protected videos

* fix style

* Correcting data leaks

* Unable to add password protected privacy on replay

* Updated code based on review comments

* fix validator and command

* Updated code based on review comments
This commit is contained in:
Wicklow 2023-06-29 07:48:55 +00:00 committed by GitHub
parent ae22c59f14
commit 40346ead2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
122 changed files with 2631 additions and 251 deletions

View file

@ -120,6 +120,7 @@ async function handleTorrentImport (req: express.Request, res: express.Response,
videoChannel: res.locals.videoChannel,
tags: body.tags || undefined,
user,
videoPasswords: body.videoPasswords,
videoImportAttributes: {
magnetUri,
torrentName,

View file

@ -47,6 +47,7 @@ import { transcodingRouter } from './transcoding'
import { updateRouter } from './update'
import { uploadRouter } from './upload'
import { viewRouter } from './view'
import { videoPasswordRouter } from './passwords'
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
@ -68,6 +69,7 @@ videosRouter.use('/', updateRouter)
videosRouter.use('/', filesRouter)
videosRouter.use('/', transcodingRouter)
videosRouter.use('/', tokenRouter)
videosRouter.use('/', videoPasswordRouter)
videosRouter.get('/categories',
openapiOperationDoc({ operationId: 'getCategories' }),

View file

@ -18,13 +18,14 @@ import { VideoLiveModel } from '@server/models/video/video-live'
import { VideoLiveSessionModel } from '@server/models/video/video-live-session'
import { MVideoDetails, MVideoFullLight, MVideoLive } from '@server/types/models'
import { buildUUID, uuidToShort } from '@shared/extra-utils'
import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoState } from '@shared/models'
import { HttpStatusCode, LiveVideoCreate, LiveVideoLatencyMode, LiveVideoUpdate, UserRight, VideoPrivacy, VideoState } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers/database'
import { updateVideoMiniatureFromExisting } from '../../../lib/thumbnail'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares'
import { VideoModel } from '../../../models/video/video'
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting'
import { VideoPasswordModel } from '@server/models/video/video-password'
const liveRouter = express.Router()
@ -202,6 +203,10 @@ async function addLiveVideo (req: express.Request, res: express.Response) {
await federateVideoIfNeeded(videoCreated, true, t)
if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t)
}
logger.info('Video live %s with uuid %s created.', videoInfo.name, videoCreated.uuid)
return { videoCreated }

View file

@ -0,0 +1,105 @@
import express from 'express'
import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
import { getFormattedObjects } from '../../../helpers/utils'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
authenticate,
setDefaultPagination,
setDefaultSort
} from '../../../middlewares'
import {
listVideoPasswordValidator,
paginationValidator,
removeVideoPasswordValidator,
updateVideoPasswordListValidator,
videoPasswordsSortValidator
} from '../../../middlewares/validators'
import { VideoPasswordModel } from '@server/models/video/video-password'
import { logger, loggerTagsFactory } from '@server/helpers/logger'
import { Transaction } from 'sequelize'
import { getVideoWithAttributes } from '@server/helpers/video'
const lTags = loggerTagsFactory('api', 'video')
const videoPasswordRouter = express.Router()
videoPasswordRouter.get('/:videoId/passwords',
authenticate,
paginationValidator,
videoPasswordsSortValidator,
setDefaultSort,
setDefaultPagination,
asyncMiddleware(listVideoPasswordValidator),
asyncMiddleware(listVideoPasswords)
)
videoPasswordRouter.put('/:videoId/passwords',
authenticate,
asyncMiddleware(updateVideoPasswordListValidator),
asyncMiddleware(updateVideoPasswordList)
)
videoPasswordRouter.delete('/:videoId/passwords/:passwordId',
authenticate,
asyncMiddleware(removeVideoPasswordValidator),
asyncRetryTransactionMiddleware(removeVideoPassword)
)
// ---------------------------------------------------------------------------
export {
videoPasswordRouter
}
// ---------------------------------------------------------------------------
async function listVideoPasswords (req: express.Request, res: express.Response) {
const options = {
videoId: res.locals.videoAll.id,
start: req.query.start,
count: req.query.count,
sort: req.query.sort
}
const resultList = await VideoPasswordModel.listPasswords(options)
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
async function updateVideoPasswordList (req: express.Request, res: express.Response) {
const videoInstance = getVideoWithAttributes(res)
const videoId = videoInstance.id
const passwordArray = req.body.passwords as string[]
await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => {
await VideoPasswordModel.deleteAllPasswords(videoId, t)
await VideoPasswordModel.addPasswords(passwordArray, videoId, t)
})
logger.info(
`Video passwords for video with name %s and uuid %s have been updated`,
videoInstance.name,
videoInstance.uuid,
lTags(videoInstance.uuid)
)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}
async function removeVideoPassword (req: express.Request, res: express.Response) {
const videoInstance = getVideoWithAttributes(res)
const password = res.locals.videoPassword
await VideoPasswordModel.deletePassword(password.id)
logger.info(
'Password with id %d of video named %s and uuid %s has been deleted.',
password.id,
videoInstance.name,
videoInstance.uuid,
lTags(videoInstance.uuid)
)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
}

View file

@ -1,13 +1,14 @@
import express from 'express'
import { VideoTokensManager } from '@server/lib/video-tokens-manager'
import { VideoToken } from '@shared/models'
import { asyncMiddleware, authenticate, videosCustomGetValidator } from '../../../middlewares'
import { VideoPrivacy, VideoToken } from '@shared/models'
import { asyncMiddleware, optionalAuthenticate, videoFileTokenValidator, videosCustomGetValidator } from '../../../middlewares'
const tokenRouter = express.Router()
tokenRouter.post('/:id/token',
authenticate,
optionalAuthenticate,
asyncMiddleware(videosCustomGetValidator('only-video')),
videoFileTokenValidator,
generateToken
)
@ -22,12 +23,11 @@ export {
function generateToken (req: express.Request, res: express.Response) {
const video = res.locals.onlyVideo
const { token, expires } = VideoTokensManager.Instance.create({ videoUUID: video.uuid, user: res.locals.oauth.token.User })
const files = video.privacy === VideoPrivacy.PASSWORD_PROTECTED
? VideoTokensManager.Instance.createForPasswordProtectedVideo({ videoUUID: video.uuid })
: VideoTokensManager.Instance.createForAuthUser({ videoUUID: video.uuid, user: res.locals.oauth.token.User })
return res.json({
files: {
token,
expires
}
files
} as VideoToken)
}

View file

@ -2,13 +2,12 @@ import express from 'express'
import { Transaction } from 'sequelize/types'
import { changeVideoChannelShare } from '@server/lib/activitypub/share'
import { addVideoJobsAfterUpdate, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
import { VideoPathManager } from '@server/lib/video-path-manager'
import { setVideoPrivacy } from '@server/lib/video-privacy'
import { openapiOperationDoc } from '@server/middlewares/doc'
import { FilteredModelAttributes } from '@server/types'
import { MVideoFullLight } from '@server/types/models'
import { forceNumber } from '@shared/core-utils'
import { HttpStatusCode, VideoUpdate } from '@shared/models'
import { HttpStatusCode, VideoPrivacy, VideoUpdate } from '@shared/models'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { resetSequelizeInstance } from '../../../helpers/database-utils'
import { createReqFiles } from '../../../helpers/express-utils'
@ -20,6 +19,9 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/video'
import { VideoPathManager } from '@server/lib/video-path-manager'
import { VideoPasswordModel } from '@server/models/video/video-password'
import { exists } from '@server/helpers/custom-validators/misc'
const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
@ -176,6 +178,16 @@ async function updateVideoPrivacy (options: {
const newPrivacy = forceNumber(videoInfoToUpdate.privacy)
setVideoPrivacy(videoInstance, newPrivacy)
// Delete passwords if video is not anymore password protected
if (videoInstance.privacy === VideoPrivacy.PASSWORD_PROTECTED && newPrivacy !== VideoPrivacy.PASSWORD_PROTECTED) {
await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction)
}
if (newPrivacy === VideoPrivacy.PASSWORD_PROTECTED && exists(videoInfoToUpdate.videoPasswords)) {
await VideoPasswordModel.deleteAllPasswords(videoInstance.id, transaction)
await VideoPasswordModel.addPasswords(videoInfoToUpdate.videoPasswords, videoInstance.id, transaction)
}
// Unfederate the video if the new privacy is not compatible with federation
if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
await VideoModel.sendDelete(videoInstance, { transaction })

View file

@ -14,7 +14,7 @@ import { openapiOperationDoc } from '@server/middlewares/doc'
import { VideoSourceModel } from '@server/models/video/video-source'
import { MUserId, MVideoFile, MVideoFullLight } from '@server/types/models'
import { uuidToShort } from '@shared/extra-utils'
import { HttpStatusCode, VideoCreate, VideoState } from '@shared/models'
import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@shared/models'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
import { createReqFiles } from '../../../helpers/express-utils'
import { logger, loggerTagsFactory } from '../../../helpers/logger'
@ -33,6 +33,7 @@ import {
} from '../../../middlewares'
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/video'
import { VideoPasswordModel } from '@server/models/video/video-password'
const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
@ -195,6 +196,10 @@ async function addVideo (options: {
transaction: t
})
if (videoInfo.privacy === VideoPrivacy.PASSWORD_PROTECTED) {
await VideoPasswordModel.addPasswords(videoInfo.videoPasswords, video.id, t)
}
auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))