Add ability for an administrator to remove any video (#61)
* Add ability for an admin to remove every video on the pod. * Server: add BlacklistedVideos relation. * Server: Insert in BlacklistedVideos relation upon deletion of a video. * Server: Modify BlacklistedVideos schema to add Pod id information. * Server: Moving insertion of a blacklisted video from the `afterDestroy` hook into the process of deletion of a video. To avoid inserting a video when it is removed on its origin pod. When a video is removed on its origin pod, the `afterDestroy` hook is fire, but no request is made on the delete('/:videoId') interface. Hence, we insert into `BlacklistedVideos` only on request on delete('/:videoId') (if requirements for insertion are met). * Server: Add removeVideoFromBlacklist hook on deletion of a video. We are going to proceed in another way :). We will add a new route : /:videoId/blacklist to blacklist a video. We do not blacklist a video upon its deletion now (to distinguish a video blacklist from a regular video delete) When we blacklist a video, the video remains in the DB, so we don't have any concern about its update. It just doesn't appear in the video list. When we remove a video, we then have to remove it from the blacklist too. We could also remove a video from the blacklist to 'unremove' it and make it appear again in the video list (will be another feature). * Server: Add handler for new route post(/:videoId/blacklist) * Client: Add isBlacklistable method * Client: Update isRemovableBy method. * Client: Move 'Delete video' feature from the video-list to the video-watch module. * Server: Exclude blacklisted videos from the video list * Server: Use findAll() in BlacklistedVideos.list() method * Server: Fix addVideoToBlacklist function. * Client: Add blacklist feature. * Server: Use JavaScript Standard Style. * Server: In checkUserCanDeleteVideo, move the callback call inside the db callback function * Server: Modify BlacklistVideo relation * Server: Modifiy Videos methods. * Server: Add checkVideoIsBlacklistable method * Server: Rewrite addVideoToBlacklist method * Server: Fix checkVideoIsBlacklistable method * Server: Add return to addVideoToBlacklist method
This commit is contained in:
parent
00871a2617
commit
198b205c10
13 changed files with 283 additions and 66 deletions
|
@ -85,8 +85,12 @@ export class Video {
|
|||
this.by = Video.createByString(hash.author, hash.podHost);
|
||||
}
|
||||
|
||||
isRemovableBy(user: User) {
|
||||
return this.isLocal === true && user && this.author === user.username;
|
||||
isRemovableBy(user) {
|
||||
return user && this.isLocal === true && (this.author === user.username || user.isAdmin() === true);
|
||||
}
|
||||
|
||||
isBlackistableBy(user) {
|
||||
return user && user.isAdmin() === true && this.isLocal === false;
|
||||
}
|
||||
|
||||
isVideoNSFWForUser(user: User) {
|
||||
|
|
|
@ -150,6 +150,12 @@ export class VideoService {
|
|||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
blacklistVideo(id: string) {
|
||||
return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {})
|
||||
.map(this.restExtractor.extractDataBool)
|
||||
.catch((res) => this.restExtractor.handleError(res));
|
||||
}
|
||||
|
||||
private setVideoRate(id: string, rateType: RateType) {
|
||||
const url = VideoService.BASE_VIDEO_URL + id + '/rate';
|
||||
const body = {
|
||||
|
|
|
@ -108,11 +108,6 @@ export class VideoListComponent implements OnInit, OnDestroy {
|
|||
this.navigateToNewParams();
|
||||
}
|
||||
|
||||
onRemoved(video: Video) {
|
||||
this.notificationsService.success('Success', `Video ${video.name} deleted.`);
|
||||
this.getVideos();
|
||||
}
|
||||
|
||||
onSort(sort: SortField) {
|
||||
this.sort = sort;
|
||||
|
||||
|
|
|
@ -10,10 +10,6 @@
|
|||
|
||||
<span class="video-miniature-duration">{{ video.duration }}</span>
|
||||
</a>
|
||||
<span
|
||||
*ngIf="displayRemoveIcon()" (click)="removeVideo(video.id)"
|
||||
class="video-miniature-remove glyphicon glyphicon-remove"
|
||||
></span>
|
||||
|
||||
<div class="video-miniature-informations">
|
||||
<span class="video-miniature-name-tags">
|
||||
|
|
|
@ -42,20 +42,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.video-miniature-remove {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.video-miniature-informations {
|
||||
width: 200px;
|
||||
|
||||
|
|
|
@ -13,8 +13,6 @@ import { User } from '../../shared';
|
|||
})
|
||||
|
||||
export class VideoMiniatureComponent {
|
||||
@Output() removed = new EventEmitter<any>();
|
||||
|
||||
@Input() currentSort: SortField;
|
||||
@Input() user: User;
|
||||
@Input() video: Video;
|
||||
|
@ -28,10 +26,6 @@ export class VideoMiniatureComponent {
|
|||
private videoService: VideoService
|
||||
) {}
|
||||
|
||||
displayRemoveIcon() {
|
||||
return this.hovering && this.video.isRemovableBy(this.user);
|
||||
}
|
||||
|
||||
getVideoName() {
|
||||
if (this.isVideoNSFWForThisUser())
|
||||
return 'NSFW';
|
||||
|
@ -47,20 +41,6 @@ export class VideoMiniatureComponent {
|
|||
this.hovering = true;
|
||||
}
|
||||
|
||||
removeVideo(id: string) {
|
||||
this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
|
||||
res => {
|
||||
if (res === false) return;
|
||||
|
||||
this.videoService.removeVideo(id).subscribe(
|
||||
status => this.removed.emit(true),
|
||||
|
||||
error => this.notificationsService.error('Error', error.text)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
isVideoNSFWForThisUser() {
|
||||
return this.video.isVideoNSFWForUser(this.user);
|
||||
}
|
||||
|
|
|
@ -96,6 +96,18 @@
|
|||
<span class="glyphicon glyphicon-alert"></span> Report
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li *ngIf="isVideoRemovable()" role="menuitem">
|
||||
<a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
|
||||
<span class="glyphicon glyphicon-remove"></span> Delete
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li *ngIf="isVideoBlacklistable()" role="menuitem">
|
||||
<a class="dropdown-item" title="Blacklist this video" href="#" (click)="blacklistVideo($event)">
|
||||
<span class="glyphicon glyphicon-eye-close"></span> Blacklist
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -169,6 +169,45 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
removeVideo(event: Event) {
|
||||
event.preventDefault();
|
||||
this.confirmService.confirm('Do you really want to delete this video?', 'Delete').subscribe(
|
||||
res => {
|
||||
if (res === false) return;
|
||||
|
||||
this.videoService.removeVideo(this.video.id)
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success('Success', `Video ${this.video.name} deleted.`)
|
||||
// Go back to the video-list.
|
||||
this.router.navigate(['/videos/list'])
|
||||
},
|
||||
|
||||
error => this.notificationsService.error('Error', error.text)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
blacklistVideo(event: Event) {
|
||||
event.preventDefault()
|
||||
this.confirmService.confirm('Do you really want to blacklist this video ?', 'Blacklist').subscribe(
|
||||
res => {
|
||||
if (res === false) return;
|
||||
|
||||
this.videoService.blacklistVideo(this.video.id)
|
||||
.subscribe(
|
||||
status => {
|
||||
this.notificationsService.success('Success', `Video ${this.video.name} had been blacklisted.`)
|
||||
this.router.navigate(['/videos/list'])
|
||||
},
|
||||
|
||||
error => this.notificationsService.error('Error', error.text)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
showReportModal(event: Event) {
|
||||
event.preventDefault();
|
||||
this.videoReportModal.show();
|
||||
|
@ -192,6 +231,14 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
|||
this.authService.getUser().username === this.video.author;
|
||||
}
|
||||
|
||||
isVideoRemovable() {
|
||||
return this.video.isRemovableBy(this.authService.getUser());
|
||||
}
|
||||
|
||||
isVideoBlacklistable() {
|
||||
return this.video.isBlackistableBy(this.authService.getUser());
|
||||
}
|
||||
|
||||
private checkUserRating() {
|
||||
// Unlogged users do not have ratings
|
||||
if (this.isUserLoggedIn() === false) return;
|
||||
|
|
|
@ -93,11 +93,13 @@ router.get('/:id',
|
|||
validatorsVideos.videosGet,
|
||||
getVideo
|
||||
)
|
||||
|
||||
router.delete('/:id',
|
||||
oAuth.authenticate,
|
||||
validatorsVideos.videosRemove,
|
||||
removeVideo
|
||||
)
|
||||
|
||||
router.get('/search/:value',
|
||||
validatorsVideos.videosSearch,
|
||||
validatorsPagination.pagination,
|
||||
|
@ -108,6 +110,13 @@ router.get('/search/:value',
|
|||
searchVideos
|
||||
)
|
||||
|
||||
router.post('/:id/blacklist',
|
||||
oAuth.authenticate,
|
||||
admin.ensureIsAdmin,
|
||||
validatorsVideos.videosBlacklist,
|
||||
addVideoToBlacklist
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = router
|
||||
|
@ -622,3 +631,19 @@ function reportVideoAbuse (req, res, finalCallback) {
|
|||
return finalCallback(null)
|
||||
})
|
||||
}
|
||||
|
||||
function addVideoToBlacklist (req, res, next) {
|
||||
const videoInstance = res.locals.video
|
||||
|
||||
db.BlacklistedVideo.create({
|
||||
videoId: videoInstance.id
|
||||
})
|
||||
.asCallback(function (err) {
|
||||
if (err) {
|
||||
logger.error('Errors when blacklisting video ', { error: err })
|
||||
return next(err)
|
||||
}
|
||||
|
||||
return res.type('json').status(204).end()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,7 +15,9 @@ const validatorsVideos = {
|
|||
|
||||
videoAbuseReport,
|
||||
|
||||
videoRate
|
||||
videoRate,
|
||||
|
||||
videosBlacklist
|
||||
}
|
||||
|
||||
function videosAdd (req, res, next) {
|
||||
|
@ -95,15 +97,10 @@ function videosRemove (req, res, next) {
|
|||
checkVideoExists(req.params.id, res, function () {
|
||||
// We need to make additional checks
|
||||
|
||||
if (res.locals.video.isOwned() === false) {
|
||||
return res.status(403).send('Cannot remove video of another pod')
|
||||
}
|
||||
|
||||
if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
|
||||
return res.status(403).send('Cannot remove video of another user')
|
||||
}
|
||||
|
||||
next()
|
||||
// Check if the user who did the request is able to delete the video
|
||||
checkUserCanDeleteVideo(res.locals.oauth.token.User.id, res, function () {
|
||||
next()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -159,3 +156,49 @@ function checkVideoExists (id, res, callback) {
|
|||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
function checkUserCanDeleteVideo (userId, res, callback) {
|
||||
// Retrieve the user who did the request
|
||||
db.User.loadById(userId, function (err, user) {
|
||||
if (err) {
|
||||
logger.error('Error in video request validator.', { error: err })
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
// Check if the user can delete the video
|
||||
// The user can delete it if s/he an admin
|
||||
// Or if s/he is the video's author
|
||||
if (user.isAdmin() === false) {
|
||||
if (res.locals.video.isOwned() === false) {
|
||||
return res.status(403).send('Cannot remove video of another pod')
|
||||
}
|
||||
|
||||
if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) {
|
||||
return res.status(403).send('Cannot remove video of another user')
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach this comment, we can delete the video
|
||||
callback()
|
||||
})
|
||||
}
|
||||
|
||||
function checkVideoIsBlacklistable (req, res, callback) {
|
||||
if (res.locals.video.isOwned() === true) {
|
||||
return res.status(403).send('Cannot blacklist a local video')
|
||||
}
|
||||
|
||||
callback()
|
||||
}
|
||||
|
||||
function videosBlacklist (req, res, next) {
|
||||
req.checkParams('id', 'Should have a valid id').notEmpty().isUUID(4)
|
||||
|
||||
logger.debug('Checking videosBlacklist parameters', { parameters: req.params })
|
||||
|
||||
checkErrors(req, res, function () {
|
||||
checkVideoExists(req.params.id, res, function() {
|
||||
checkVideoIsBlacklistable(req, res, next)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -79,7 +79,8 @@ module.exports = function (sequelize, DataTypes) {
|
|||
},
|
||||
instanceMethods: {
|
||||
isPasswordMatch,
|
||||
toFormatedJSON
|
||||
toFormatedJSON,
|
||||
isAdmin
|
||||
},
|
||||
hooks: {
|
||||
beforeCreate: beforeCreateOrUpdate,
|
||||
|
@ -117,6 +118,11 @@ function toFormatedJSON () {
|
|||
createdAt: this.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
function isAdmin () {
|
||||
return this.role === constants.USER_ROLES.ADMIN
|
||||
}
|
||||
|
||||
// ------------------------------ STATICS ------------------------------
|
||||
|
||||
function associate (models) {
|
||||
|
|
89
server/models/video-blacklist.js
Normal file
89
server/models/video-blacklist.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
'use strict'
|
||||
|
||||
const modelUtils = require('./utils')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
const BlacklistedVideo = sequelize.define('BlacklistedVideo',
|
||||
{},
|
||||
{
|
||||
indexes: [
|
||||
{
|
||||
fields: [ 'videoId' ],
|
||||
unique: true
|
||||
}
|
||||
],
|
||||
classMethods: {
|
||||
associate,
|
||||
|
||||
countTotal,
|
||||
list,
|
||||
listForApi,
|
||||
loadById,
|
||||
loadByVideoId
|
||||
},
|
||||
instanceMethods: {
|
||||
toFormatedJSON
|
||||
},
|
||||
hooks: {}
|
||||
}
|
||||
)
|
||||
|
||||
return BlacklistedVideo
|
||||
}
|
||||
|
||||
// ------------------------------ METHODS ------------------------------
|
||||
|
||||
function toFormatedJSON () {
|
||||
return {
|
||||
id: this.id,
|
||||
videoId: this.videoId,
|
||||
createdAt: this.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------ STATICS ------------------------------
|
||||
|
||||
function associate (models) {
|
||||
this.belongsTo(models.Video, {
|
||||
foreignKey: 'videoId',
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
}
|
||||
|
||||
function countTotal (callback) {
|
||||
return this.count().asCallback(callback)
|
||||
}
|
||||
|
||||
function list (callback) {
|
||||
return this.findAll().asCallback(callback)
|
||||
}
|
||||
|
||||
function listForApi (start, count, sort, callback) {
|
||||
const query = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
order: [ modelUtils.getSort(sort) ]
|
||||
}
|
||||
|
||||
return this.findAndCountAll(query).asCallback(function (err, result) {
|
||||
if (err) return callback(err)
|
||||
|
||||
return callback(null, result.rows, result.count)
|
||||
})
|
||||
}
|
||||
|
||||
function loadById (id, callback) {
|
||||
return this.findById(id).asCallback(callback)
|
||||
}
|
||||
|
||||
function loadByVideoId (id, callback) {
|
||||
const query = {
|
||||
where: {
|
||||
videoId: id
|
||||
}
|
||||
}
|
||||
|
||||
return this.find(query).asCallback(callback)
|
||||
}
|
|
@ -16,6 +16,7 @@ const logger = require('../helpers/logger')
|
|||
const friends = require('../lib/friends')
|
||||
const modelUtils = require('./utils')
|
||||
const customVideosValidators = require('../helpers/custom-validators').videos
|
||||
const db = require('../initializers/database')
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
|
@ -201,7 +202,8 @@ module.exports = function (sequelize, DataTypes) {
|
|||
isOwned,
|
||||
toFormatedJSON,
|
||||
toAddRemoteJSON,
|
||||
toUpdateRemoteJSON
|
||||
toUpdateRemoteJSON,
|
||||
removeFromBlacklist
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate,
|
||||
|
@ -528,6 +530,7 @@ function list (callback) {
|
|||
}
|
||||
|
||||
function listForApi (start, count, sort, callback) {
|
||||
// Exclude Blakclisted videos from the list
|
||||
const query = {
|
||||
offset: start,
|
||||
limit: count,
|
||||
|
@ -540,7 +543,12 @@ function listForApi (start, count, sort, callback) {
|
|||
},
|
||||
|
||||
this.sequelize.models.Tag
|
||||
]
|
||||
],
|
||||
where: {
|
||||
id: { $notIn: this.sequelize.literal(
|
||||
'(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
|
||||
)}
|
||||
}
|
||||
}
|
||||
|
||||
return this.findAndCountAll(query).asCallback(function (err, result) {
|
||||
|
@ -648,7 +656,11 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort,
|
|||
}
|
||||
|
||||
const query = {
|
||||
where: {},
|
||||
where: {
|
||||
id: { $notIn: this.sequelize.literal(
|
||||
'(SELECT "BlacklistedVideos"."videoId" FROM "BlacklistedVideos")'
|
||||
)}
|
||||
},
|
||||
offset: start,
|
||||
limit: count,
|
||||
distinct: true, // For the count, a video can have many tags
|
||||
|
@ -661,13 +673,9 @@ function searchAndPopulateAuthorAndPodAndTags (value, field, start, count, sort,
|
|||
query.where.infoHash = infoHash
|
||||
} else if (field === 'tags') {
|
||||
const escapedValue = this.sequelize.escape('%' + value + '%')
|
||||
query.where = {
|
||||
id: {
|
||||
$in: this.sequelize.literal(
|
||||
'(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
|
||||
)
|
||||
}
|
||||
}
|
||||
query.where.id.$in = this.sequelize.literal(
|
||||
'(SELECT "VideoTags"."videoId" FROM "Tags" INNER JOIN "VideoTags" ON "Tags"."id" = "VideoTags"."tagId" WHERE name LIKE ' + escapedValue + ')'
|
||||
)
|
||||
} else if (field === 'host') {
|
||||
// FIXME: Include our pod? (not stored in the database)
|
||||
podInclude.where = {
|
||||
|
@ -755,3 +763,23 @@ function generateImage (video, videoPath, folder, imageName, size, callback) {
|
|||
})
|
||||
.thumbnail(options)
|
||||
}
|
||||
|
||||
function removeFromBlacklist (video, callback) {
|
||||
// Find the blacklisted video
|
||||
db.BlacklistedVideo.loadByVideoId(video.id, function (err, video) {
|
||||
// If an error occured, stop here
|
||||
if (err) {
|
||||
logger.error('Error when fetching video from blacklist.', { error: err })
|
||||
|
||||
return callback(err)
|
||||
}
|
||||
|
||||
// If we found the video, remove it from the blacklist
|
||||
if (video) {
|
||||
video.destroy().asCallback(callback)
|
||||
} else {
|
||||
// If haven't found it, simply ignore it and do nothing
|
||||
return callback()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue