From 40346ead2b0b7afa475aef057d3673b6c7574b7a Mon Sep 17 00:00:00 2001 From: Wicklow <123956049+wickloww@users.noreply.github.com> Date: Thu, 29 Jun 2023 07:48:55 +0000 Subject: 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 --- server/models/video/video-password.ts | 137 ++++++++++++++++++++++++++ server/models/video/video-playlist-element.ts | 5 +- server/models/video/video.ts | 18 +++- 3 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 server/models/video/video-password.ts (limited to 'server/models') diff --git a/server/models/video/video-password.ts b/server/models/video/video-password.ts new file mode 100644 index 000000000..648366c3b --- /dev/null +++ b/server/models/video/video-password.ts @@ -0,0 +1,137 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, DefaultScope, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { VideoModel } from './video' +import { AttributesOnly } from '@shared/typescript-utils' +import { ResultList, VideoPassword } from '@shared/models' +import { getSort, throwIfNotValid } from '../shared' +import { FindOptions, Transaction } from 'sequelize' +import { MVideoPassword } from '@server/types/models' +import { isPasswordValid } from '@server/helpers/custom-validators/videos' +import { pick } from '@shared/core-utils' + +@DefaultScope(() => ({ + include: [ + { + model: VideoModel.unscoped(), + required: true + } + ] +})) +@Table({ + tableName: 'videoPassword', + indexes: [ + { + fields: [ 'videoId', 'password' ], + unique: true + } + ] +}) +export class VideoPasswordModel extends Model>> { + + @AllowNull(false) + @Is('VideoPassword', value => throwIfNotValid(value, isPasswordValid, 'videoPassword')) + @Column + password: string + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => VideoModel) + @Column + videoId: number + + @BelongsTo(() => VideoModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'cascade' + }) + Video: VideoModel + + static async countByVideoId (videoId: number, t?: Transaction) { + const query: FindOptions = { + where: { + videoId + }, + transaction: t + } + + return VideoPasswordModel.count(query) + } + + static async loadByIdAndVideo (options: { id: number, videoId: number, t?: Transaction }): Promise { + const { id, videoId, t } = options + const query: FindOptions = { + where: { + id, + videoId + }, + transaction: t + } + + return VideoPasswordModel.findOne(query) + } + + static async listPasswords (options: { + start: number + count: number + sort: string + videoId: number + }): Promise> { + const { start, count, sort, videoId } = options + + const { count: total, rows: data } = await VideoPasswordModel.findAndCountAll({ + where: { videoId }, + order: getSort(sort), + offset: start, + limit: count + }) + + return { total, data } + } + + static async addPasswords (passwords: string[], videoId: number, transaction?: Transaction): Promise { + for (const password of passwords) { + await VideoPasswordModel.create({ + password, + videoId + }, { transaction }) + } + } + + static async deleteAllPasswords (videoId: number, transaction?: Transaction) { + await VideoPasswordModel.destroy({ + where: { videoId }, + transaction + }) + } + + static async deletePassword (passwordId: number, transaction?: Transaction) { + await VideoPasswordModel.destroy({ + where: { id: passwordId }, + transaction + }) + } + + static async isACorrectPassword (options: { + videoId: number + password: string + }) { + const query = { + where: pick(options, [ 'videoId', 'password' ]) + } + return VideoPasswordModel.findOne(query) + } + + toFormattedJSON (): VideoPassword { + return { + id: this.id, + password: this.password, + videoId: this.videoId, + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } +} diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index b832f9768..61ae6b9fe 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts @@ -336,7 +336,10 @@ export class VideoPlaylistElementModel extends Model>> { }) VideoCaptions: VideoCaptionModel[] + @HasMany(() => VideoPasswordModel, { + foreignKey: { + name: 'videoId', + allowNull: false + }, + onDelete: 'cascade' + }) + VideoPasswords: VideoPasswordModel[] + @HasOne(() => VideoJobInfoModel, { foreignKey: { name: 'videoId', @@ -1918,7 +1928,7 @@ export class VideoModel extends Model>> { // --------------------------------------------------------------------------- - requiresAuth (options: { + requiresUserAuth (options: { urlParamId: string checkBlacklist: boolean }) { @@ -1936,11 +1946,11 @@ export class VideoModel extends Model>> { if (checkBlacklist && this.VideoBlacklist) return true - if (this.privacy !== VideoPrivacy.PUBLIC) { - throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) + if (this.privacy === VideoPrivacy.PUBLIC || this.privacy === VideoPrivacy.PASSWORD_PROTECTED) { + return false } - return false + throw new Error(`Unknown video privacy ${this.privacy} to know if the video requires auth`) } hasPrivateStaticPath () { -- cgit v1.2.3