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:
parent
ae22c59f14
commit
40346ead2b
122 changed files with 2631 additions and 251 deletions
|
@ -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,
|
||||
|
|
|
@ -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' }),
|
||||
|
|
|
@ -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 }
|
||||
|
|
105
server/controllers/api/videos/passwords.ts
Normal file
105
server/controllers/api/videos/passwords.ts
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue