refactor API errors to standard error format

This commit is contained in:
Rigel Kent 2021-06-01 01:36:53 +02:00 committed by Chocobozzz
parent 5ed25fb76e
commit 76148b27f7
75 changed files with 785 additions and 547 deletions

View File

@ -431,7 +431,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
.pipe( .pipe(
// If 400, 403 or 404, the video is private or blocked so redirect to 404 // If 400, 403 or 404, the video is private or blocked so redirect to 404
catchError(err => { catchError(err => {
if (err.body.errorCode === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) { if (err.body.type === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) {
const search = window.location.search const search = window.location.search
let originUrl = err.body.originUrl let originUrl = err.body.originUrl
if (search) originUrl += search if (search) originUrl += search

View File

@ -41,7 +41,7 @@ export class RestExtractor {
if (err.error instanceof Error) { if (err.error instanceof Error) {
// A client-side or network error occurred. Handle it accordingly. // A client-side or network error occurred. Handle it accordingly.
errorMessage = err.error.message errorMessage = err.error.detail || err.error.title
console.error('An error occurred:', errorMessage) console.error('An error occurred:', errorMessage)
} else if (typeof err.error === 'string') { } else if (typeof err.error === 'string') {
errorMessage = err.error errorMessage = err.error

View File

@ -100,6 +100,7 @@
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"helmet": "^4.1.0", "helmet": "^4.1.0",
"http-problem-details": "^0.1.5",
"http-signature": "1.3.5", "http-signature": "1.3.5",
"ip-anonymize": "^0.1.0", "ip-anonymize": "^0.1.0",
"ipaddr.js": "2.0.0", "ipaddr.js": "2.0.0",

View File

@ -128,6 +128,7 @@ import { LiveManager } from './server/lib/live-manager'
import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes' import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache' import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
import { ServerConfigManager } from '@server/lib/server-config-manager' import { ServerConfigManager } from '@server/lib/server-config-manager'
import { apiResponseHelpers } from '@server/helpers/express-utils'
// ----------- Command line ----------- // ----------- Command line -----------
@ -186,6 +187,9 @@ app.use(cookieParser())
// W3C DNT Tracking Status // W3C DNT Tracking Status
app.use(advertiseDoNotTrack) app.use(advertiseDoNotTrack)
// Response helpers used in developement
app.use(apiResponseHelpers)
// ----------- Views, routes and static files ----------- // ----------- Views, routes and static files -----------
// API // API
@ -235,7 +239,11 @@ app.use(function (err, req, res, next) {
const sql = err.parent ? err.parent.sql : undefined const sql = err.parent ? err.parent.sql : undefined
logger.error('Error in controller.', { err: error, sql }) logger.error('Error in controller.', { err: error, sql })
return res.status(err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500).end() return res.fail({
status: err.status || HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: err.message,
type: err.name
})
}) })
const server = createWebsocketTrackerServer(app) const server = createWebsocketTrackerServer(app)

View File

@ -142,7 +142,7 @@ async function updateAbuse (req: express.Request, res: express.Response) {
// Do not send the delete to other instances, we updated OUR copy of this abuse // Do not send the delete to other instances, we updated OUR copy of this abuse
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function deleteAbuse (req: express.Request, res: express.Response) { async function deleteAbuse (req: express.Request, res: express.Response) {
@ -154,7 +154,7 @@ async function deleteAbuse (req: express.Request, res: express.Response) {
// Do not send the delete to other instances, we delete OUR copy of this abuse // Do not send the delete to other instances, we delete OUR copy of this abuse
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function reportAbuse (req: express.Request, res: express.Response) { async function reportAbuse (req: express.Request, res: express.Response) {
@ -244,5 +244,5 @@ async function deleteAbuseMessage (req: express.Request, res: express.Response)
return abuseMessage.destroy({ transaction: t }) return abuseMessage.destroy({ transaction: t })
}) })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }

View File

@ -34,7 +34,7 @@ async function bulkRemoveCommentsOf (req: express.Request, res: express.Response
const comments = await VideoCommentModel.listForBulkDelete(account, filter) const comments = await VideoCommentModel.listForBulkDelete(account, filter)
// Don't wait result // Don't wait result
res.sendStatus(HttpStatusCode.NO_CONTENT_204) res.status(HttpStatusCode.NO_CONTENT_204).end()
for (const comment of comments) { for (const comment of comments) {
await removeComment(comment) await removeComment(comment)

View File

@ -27,7 +27,12 @@ export {
async function getInstanceHomepage (req: express.Request, res: express.Response) { async function getInstanceHomepage (req: express.Request, res: express.Response) {
const page = await ActorCustomPageModel.loadInstanceHomepage() const page = await ActorCustomPageModel.loadInstanceHomepage()
if (!page) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!page) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Instance homepage could not be found'
})
}
return res.json(page.toFormattedJSON()) return res.json(page.toFormattedJSON())
} }
@ -38,5 +43,5 @@ async function updateInstanceHomepage (req: express.Request, res: express.Respon
await ActorCustomPageModel.updateInstanceHomepage(content) await ActorCustomPageModel.updateInstanceHomepage(content)
ServerConfigManager.Instance.updateHomepageState(content) ServerConfigManager.Instance.updateHomepageState(content)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }

View File

@ -24,7 +24,10 @@ async function getLocalClient (req: express.Request, res: express.Response, next
// Don't make this check if this is a test instance // Don't make this check if this is a test instance
if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) { if (process.env.NODE_ENV !== 'test' && req.get('host') !== headerHostShouldBe) {
logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe) logger.info('Getting client tokens for host %s is forbidden (expected %s).', req.get('host'), headerHostShouldBe)
return res.type('json').status(HttpStatusCode.FORBIDDEN_403).end() return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: `Getting client tokens for host ${req.get('host')} is forbidden`
})
} }
const client = await OAuthClientModel.loadFirstClient() const client = await OAuthClientModel.loadFirstClient()

View File

@ -144,7 +144,7 @@ async function installPlugin (req: express.Request, res: express.Response) {
return res.json(plugin.toFormattedJSON()) return res.json(plugin.toFormattedJSON())
} catch (err) { } catch (err) {
logger.warn('Cannot install plugin %s.', toInstall, { err }) logger.warn('Cannot install plugin %s.', toInstall, { err })
return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot install plugin ' + toInstall })
} }
} }
@ -159,7 +159,7 @@ async function updatePlugin (req: express.Request, res: express.Response) {
return res.json(plugin.toFormattedJSON()) return res.json(plugin.toFormattedJSON())
} catch (err) { } catch (err) {
logger.warn('Cannot update plugin %s.', toUpdate, { err }) logger.warn('Cannot update plugin %s.', toUpdate, { err })
return res.sendStatus(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot update plugin ' + toUpdate })
} }
} }
@ -168,7 +168,7 @@ async function uninstallPlugin (req: express.Request, res: express.Response) {
await PluginManager.Instance.uninstall(body.npmName) await PluginManager.Instance.uninstall(body.npmName)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
function getPublicPluginSettings (req: express.Request, res: express.Response) { function getPublicPluginSettings (req: express.Request, res: express.Response) {
@ -197,7 +197,7 @@ async function updatePluginSettings (req: express.Request, res: express.Response
await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings) await PluginManager.Instance.onSettingsChanged(plugin.name, plugin.settings)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function listAvailablePlugins (req: express.Request, res: express.Response) { async function listAvailablePlugins (req: express.Request, res: express.Response) {
@ -206,8 +206,10 @@ async function listAvailablePlugins (req: express.Request, res: express.Response
const resultList = await listAvailablePluginsFromIndex(query) const resultList = await listAvailablePluginsFromIndex(query)
if (!resultList) { if (!resultList) {
return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503) return res.fail({
.json({ error: 'Plugin index unavailable. Please retry later' }) status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
message: 'Plugin index unavailable. Please retry later'
})
} }
return res.json(resultList) return res.json(resultList)

View File

@ -102,7 +102,10 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
} catch (err) { } catch (err) {
logger.warn('Cannot use search index to make video channels search.', { err }) logger.warn('Cannot use search index to make video channels search.', { err })
return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Cannot use search index to make video channels search'
})
} }
} }
@ -202,7 +205,10 @@ async function searchVideosIndex (query: VideosSearchQuery, res: express.Respons
} catch (err) { } catch (err) {
logger.warn('Cannot use search index to make video search.', { err }) logger.warn('Cannot use search index to make video search.', { err })
return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Cannot use search index to make video search'
})
} }
} }

View File

@ -1,5 +1,6 @@
import { InboxManager } from '@server/lib/activitypub/inbox-manager' import { InboxManager } from '@server/lib/activitypub/inbox-manager'
import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler' import { RemoveDanglingResumableUploadsScheduler } from '@server/lib/schedulers/remove-dangling-resumable-uploads-scheduler'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { SendDebugCommand } from '@shared/models' import { SendDebugCommand } from '@shared/models'
import * as express from 'express' import * as express from 'express'
import { UserRight } from '../../../../shared/models/users' import { UserRight } from '../../../../shared/models/users'
@ -41,5 +42,5 @@ async function runCommand (req: express.Request, res: express.Response) {
await RemoveDanglingResumableUploadsScheduler.Instance.execute() await RemoveDanglingResumableUploadsScheduler.Instance.execute()
} }
return res.sendStatus(204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }

View File

@ -90,13 +90,13 @@ async function addVideoRedundancy (req: express.Request, res: express.Response)
payload payload
}) })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function removeVideoRedundancyController (req: express.Request, res: express.Response) { async function removeVideoRedundancyController (req: express.Request, res: express.Response) {
await removeVideoRedundancy(res.locals.videoRedundancy) await removeVideoRedundancy(res.locals.videoRedundancy)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function updateRedundancy (req: express.Request, res: express.Response) { async function updateRedundancy (req: express.Request, res: express.Response) {
@ -110,5 +110,5 @@ async function updateRedundancy (req: express.Request, res: express.Response) {
removeRedundanciesOfServer(server.id) removeRedundanciesOfServer(server.id)
.catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err })) .catch(err => logger.error('Cannot remove redundancy of %s.', server.host, { err }))
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }

View File

@ -314,7 +314,7 @@ async function removeUser (req: express.Request, res: express.Response) {
Hooks.runAction('action:api.user.deleted', { user }) Hooks.runAction('action:api.user.deleted', { user })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function updateUser (req: express.Request, res: express.Response) { async function updateUser (req: express.Request, res: express.Response) {
@ -349,7 +349,7 @@ async function updateUser (req: express.Request, res: express.Response) {
// Don't need to send this update to followers, these attributes are not federated // Don't need to send this update to followers, these attributes are not federated
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function askResetUserPassword (req: express.Request, res: express.Response) { async function askResetUserPassword (req: express.Request, res: express.Response) {

View File

@ -183,7 +183,7 @@ async function deleteMe (req: express.Request, res: express.Response) {
await user.destroy() await user.destroy()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function updateMe (req: express.Request, res: express.Response) { async function updateMe (req: express.Request, res: express.Response) {
@ -237,7 +237,7 @@ async function updateMe (req: express.Request, res: express.Response) {
await sendVerifyUserEmail(user, true) await sendVerifyUserEmail(user, true)
} }
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function updateMyAvatar (req: express.Request, res: express.Response) { async function updateMyAvatar (req: express.Request, res: express.Response) {
@ -257,5 +257,5 @@ async function deleteMyAvatar (req: express.Request, res: express.Response) {
const userAccount = await AccountModel.load(user.Account.id) const userAccount = await AccountModel.load(user.Account.id)
await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR) await deleteLocalActorImageFile(userAccount, ActorImageType.AVATAR)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }

View File

@ -78,9 +78,10 @@ async function handleToken (req: express.Request, res: express.Response, next: e
} catch (err) { } catch (err) {
logger.warn('Login error', { err }) logger.warn('Login error', { err })
return res.status(err.code || 400).json({ return res.fail({
code: err.name, status: err.code,
error: err.message message: err.message,
type: err.name
}) })
} }
} }

View File

@ -180,7 +180,7 @@ async function deleteVideoChannelAvatar (req: express.Request, res: express.Resp
await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR) await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function deleteVideoChannelBanner (req: express.Request, res: express.Response) { async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
@ -188,7 +188,7 @@ async function deleteVideoChannelBanner (req: express.Request, res: express.Resp
await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER) await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function addVideoChannel (req: express.Request, res: express.Response) { async function addVideoChannel (req: express.Request, res: express.Response) {

View File

@ -70,7 +70,7 @@ async function addVideoToBlacklistController (req: express.Request, res: express
logger.info('Video %s blacklisted.', videoInstance.uuid) logger.info('Video %s blacklisted.', videoInstance.uuid)
return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function updateVideoBlacklistController (req: express.Request, res: express.Response) { async function updateVideoBlacklistController (req: express.Request, res: express.Response) {
@ -82,7 +82,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
return videoBlacklist.save({ transaction: t }) return videoBlacklist.save({ transaction: t })
}) })
return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function listBlacklist (req: express.Request, res: express.Response) { async function listBlacklist (req: express.Request, res: express.Response) {
@ -105,5 +105,5 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
logger.info('Video %s removed from blacklist.', video.uuid) logger.info('Video %s removed from blacklist.', video.uuid)
return res.type('json').sendStatus(HttpStatusCode.NO_CONTENT_204) return res.type('json').status(HttpStatusCode.NO_CONTENT_204).end()
} }

View File

@ -166,7 +166,10 @@ async function listVideoThreadComments (req: express.Request, res: express.Respo
} }
if (resultList.data.length === 0) { if (resultList.data.length === 0) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No comments were found'
})
} }
return res.json(buildFormattedCommentTree(resultList)) return res.json(buildFormattedCommentTree(resultList))

View File

@ -18,7 +18,6 @@ import {
} from '@server/types/models' } from '@server/types/models'
import { MVideoImportFormattable } from '@server/types/models/video/video-import' import { MVideoImportFormattable } from '@server/types/models/video/video-import'
import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared' import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger' import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils' import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
@ -143,10 +142,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
} catch (err) { } catch (err) {
logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err }) logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({
.json({ message: 'Cannot fetch remote information of this URL.',
error: 'Cannot fetch remote information of this URL.' data: {
}) targetUrl
}
})
} }
const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo) const video = buildVideo(res.locals.videoChannel.id, body, youtubeDLInfo)
@ -333,12 +334,10 @@ async function processTorrentOrAbortRequest (req: express.Request, res: express.
if (parsedTorrent.files.length !== 1) { if (parsedTorrent.files.length !== 1) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({
.json({ type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT.toString(),
code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT, message: 'Torrents with only 1 file are supported.'
error: 'Torrents with only 1 file are supported.' })
})
return undefined return undefined
} }

View File

@ -146,7 +146,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid) const exists = await Redis.Instance.doesVideoIPViewExist(ip, immutableVideoAttrs.uuid)
if (exists) { if (exists) {
logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid) logger.debug('View for ip %s and video %s already exists.', ip, immutableVideoAttrs.uuid)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
const video = await VideoModel.load(immutableVideoAttrs.id) const video = await VideoModel.load(immutableVideoAttrs.id)
@ -179,7 +179,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
Hooks.runAction('action:api.video.viewed', { video, ip }) Hooks.runAction('action:api.video.viewed', { video, ip })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function getVideoDescription (req: express.Request, res: express.Response) { async function getVideoDescription (req: express.Request, res: express.Response) {

View File

@ -76,7 +76,7 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
await federateVideoIfNeeded(video, false) await federateVideoIfNeeded(video, false)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
async function addLiveVideo (req: express.Request, res: express.Response) { async function addLiveVideo (req: express.Request, res: express.Response) {

View File

@ -122,7 +122,7 @@ function acceptOwnership (req: express.Request, res: express.Response) {
videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED
await videoChangeOwnership.save({ transaction: t }) await videoChangeOwnership.save({ transaction: t })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
}) })
} }
@ -133,6 +133,6 @@ function refuseOwnership (req: express.Request, res: express.Response) {
videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED videoChangeOwnership.status = VideoChangeOwnershipStatus.REFUSED
await videoChangeOwnership.save({ transaction: t }) await videoChangeOwnership.save({ transaction: t })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
}) })
} }

View File

@ -97,8 +97,11 @@ export async function addVideoLegacy (req: express.Request, res: express.Respons
// Uploading the video could be long // Uploading the video could be long
// Set timeout to 10 minutes, as Express's default is 2 minutes // Set timeout to 10 minutes, as Express's default is 2 minutes
req.setTimeout(1000 * 60 * 10, () => { req.setTimeout(1000 * 60 * 10, () => {
logger.error('Upload video has timed out.') logger.error('Video upload has timed out.')
return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408) return res.fail({
status: HttpStatusCode.REQUEST_TIMEOUT_408,
message: 'Video upload has timed out.'
})
}) })
const videoPhysicalFile = req.files['videofile'][0] const videoPhysicalFile = req.files['videofile'][0]

View File

@ -78,7 +78,7 @@ clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.C
// 404 for static files not found // 404 for static files not found
clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => { clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => {
res.sendStatus(HttpStatusCode.NOT_FOUND_404) res.status(HttpStatusCode.NOT_FOUND_404).end()
}) })
// Always serve index client page (the client is a single page application, let it handle routing) // Always serve index client page (the client is a single page application, let it handle routing)
@ -105,7 +105,7 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
} }
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.status(HttpStatusCode.NOT_FOUND_404).end()
} }
async function generateEmbedHtmlPage (req: express.Request, res: express.Response) { async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {

View File

@ -41,7 +41,12 @@ export {
async function downloadTorrent (req: express.Request, res: express.Response) { async function downloadTorrent (req: express.Request, res: express.Response) {
const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!result) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Torrent file not found'
})
}
const allowParameters = { torrentPath: result.path, downloadName: result.downloadName } const allowParameters = { torrentPath: result.path, downloadName: result.downloadName }
@ -60,7 +65,12 @@ async function downloadVideoFile (req: express.Request, res: express.Response) {
const video = res.locals.videoAll const video = res.locals.videoAll
const videoFile = getVideoFile(req, video.VideoFiles) const videoFile = getVideoFile(req, video.VideoFiles)
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() if (!videoFile) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video file not found'
})
}
const allowParameters = { video, videoFile } const allowParameters = { video, videoFile }
@ -81,7 +91,12 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response
if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles) const videoFile = getVideoFile(req, streamingPlaylist.VideoFiles)
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).end() if (!videoFile) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video file not found'
})
}
const allowParameters = { video, streamingPlaylist, videoFile } const allowParameters = { video, streamingPlaylist, videoFile }
@ -131,9 +146,11 @@ function isVideoDownloadAllowed (_object: {
function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) { function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
if (!result || result.allowed !== true) { if (!result || result.allowed !== true) {
logger.info('Download is not allowed.', { result, allowParameters }) logger.info('Download is not allowed.', { result, allowParameters })
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: result?.errorMessage || 'Refused download' })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: result?.errorMessage || 'Refused download'
})
return false return false
} }

View File

@ -56,10 +56,10 @@ async function getActorImage (req: express.Request, res: express.Response) {
} }
const image = await ActorImageModel.loadByName(filename) const image = await ActorImageModel.loadByName(filename)
if (!image) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!image) return res.status(HttpStatusCode.NOT_FOUND_404).end()
if (image.onDisk === false) { if (image.onDisk === false) {
if (!image.fileUrl) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!image.fileUrl) return res.status(HttpStatusCode.NOT_FOUND_404).end()
logger.info('Lazy serve remote actor image %s.', image.fileUrl) logger.info('Lazy serve remote actor image %s.', image.fileUrl)
@ -67,7 +67,7 @@ async function getActorImage (req: express.Request, res: express.Response) {
await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type }) await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
} catch (err) { } catch (err) {
logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err }) logger.warn('Cannot process remote actor image %s.', image.fileUrl, { err })
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.status(HttpStatusCode.NOT_FOUND_404).end()
} }
image.onDisk = true image.onDisk = true
@ -83,21 +83,21 @@ async function getActorImage (req: express.Request, res: express.Response) {
async function getPreview (req: express.Request, res: express.Response) { async function getPreview (req: express.Request, res: express.Response) {
const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename) const result = await VideosPreviewCache.Instance.getFilePath(req.params.filename)
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
} }
async function getVideoCaption (req: express.Request, res: express.Response) { async function getVideoCaption (req: express.Request, res: express.Response) {
const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename) const result = await VideosCaptionCache.Instance.getFilePath(req.params.filename)
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER }) return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.LAZY_SERVER })
} }
async function getTorrent (req: express.Request, res: express.Response) { async function getTorrent (req: express.Request, res: express.Response) {
const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename) const result = await VideosTorrentCache.Instance.getFilePath(req.params.filename)
if (!result) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!result) return res.status(HttpStatusCode.NOT_FOUND_404).end()
// Torrents still use the old naming convention (video uuid + .torrent) // Torrents still use the old naming convention (video uuid + .torrent)
return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER }) return res.sendFile(result.path, { maxAge: STATIC_MAX_AGE.SERVER })

View File

@ -25,7 +25,7 @@ function getSegmentsSha256 (req: express.Request, res: express.Response) {
const result = LiveManager.Instance.getSegmentsSha256(videoUUID) const result = LiveManager.Instance.getSegmentsSha256(videoUUID)
if (!result) { if (!result) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.status(HttpStatusCode.NOT_FOUND_404).end()
} }
return res.json(mapToJSON(result)) return res.json(mapToJSON(result))

View File

@ -100,7 +100,7 @@ function getPluginTranslations (req: express.Request, res: express.Response) {
return res.json(json) return res.json(json)
} }
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.status(HttpStatusCode.NOT_FOUND_404).end()
} }
function servePluginStaticDirectory (req: express.Request, res: express.Response) { function servePluginStaticDirectory (req: express.Request, res: express.Response) {
@ -110,7 +110,7 @@ function servePluginStaticDirectory (req: express.Request, res: express.Response
const [ directory, ...file ] = staticEndpoint.split('/') const [ directory, ...file ] = staticEndpoint.split('/')
const staticPath = plugin.staticDirs[directory] const staticPath = plugin.staticDirs[directory]
if (!staticPath) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!staticPath) return res.status(HttpStatusCode.NOT_FOUND_404).end()
const filepath = file.join('/') const filepath = file.join('/')
return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions) return res.sendFile(join(plugin.path, staticPath, filepath), sendFileOptions)
@ -120,7 +120,7 @@ function servePluginCustomRoutes (req: express.Request, res: express.Response, n
const plugin: RegisteredPlugin = res.locals.registeredPlugin const plugin: RegisteredPlugin = res.locals.registeredPlugin
const router = PluginManager.Instance.getRouter(plugin.npmName) const router = PluginManager.Instance.getRouter(plugin.npmName)
if (!router) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!router) return res.status(HttpStatusCode.NOT_FOUND_404).end()
return router(req, res, next) return router(req, res, next)
} }
@ -130,7 +130,7 @@ function servePluginClientScripts (req: express.Request, res: express.Response)
const staticEndpoint = req.params.staticEndpoint const staticEndpoint = req.params.staticEndpoint
const file = plugin.clientScripts[staticEndpoint] const file = plugin.clientScripts[staticEndpoint]
if (!file) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!file) return res.status(HttpStatusCode.NOT_FOUND_404).end()
return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)
} }
@ -140,7 +140,7 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
const staticEndpoint = req.params.staticEndpoint const staticEndpoint = req.params.staticEndpoint
if (plugin.css.includes(staticEndpoint) === false) { if (plugin.css.includes(staticEndpoint) === false) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.status(HttpStatusCode.NOT_FOUND_404).end()
} }
return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions) return res.sendFile(join(plugin.path, staticEndpoint), sendFileOptions)

View File

@ -160,10 +160,9 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
const { totalVideos } = await VideoModel.getStats() const { totalVideos } = await VideoModel.getStats()
const { totalLocalVideoComments } = await VideoCommentModel.getStats() const { totalLocalVideoComments } = await VideoCommentModel.getStats()
const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats() const { totalUsers, totalMonthlyActiveUsers, totalHalfYearActiveUsers } = await UserModel.getStats()
let json = {}
if (req.params.version && (req.params.version === '2.0')) { if (req.params.version && (req.params.version === '2.0')) {
json = { const json = {
version: '2.0', version: '2.0',
software: { software: {
name: 'peertube', name: 'peertube',
@ -291,12 +290,14 @@ async function generateNodeinfo (req: express.Request, res: express.Response) {
} }
} as HttpNodeinfoDiasporaSoftwareNsSchema20 } as HttpNodeinfoDiasporaSoftwareNsSchema20
res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"') res.contentType('application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"')
} else { .send(json)
json = { error: 'Nodeinfo schema version not handled' } .end()
res.status(HttpStatusCode.NOT_FOUND_404)
} }
return res.send(json).end() return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Nodeinfo schema version not handled'
})
} }
function getCup (req: express.Request, res: express.Response, next: express.NextFunction) { function getCup (req: express.Request, res: express.Response, next: express.NextFunction) {

View File

@ -16,26 +16,20 @@ async function doesVideoCommentThreadExist (idArg: number | string, video: MVide
const videoComment = await VideoCommentModel.loadById(id) const videoComment = await VideoCommentModel.loadById(id)
if (!videoComment) { if (!videoComment) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video comment thread not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video comment thread not found'
})
return false return false
} }
if (videoComment.videoId !== video.id) { if (videoComment.videoId !== video.id) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Video comment is not associated to this video.' })
.json({ error: 'Video comment is not associated to this video.' })
.end()
return false return false
} }
if (videoComment.inReplyToCommentId !== null) { if (videoComment.inReplyToCommentId !== null) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Video comment is not a thread.' })
.json({ error: 'Video comment is not a thread.' })
.end()
return false return false
} }
@ -48,18 +42,15 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r
const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
if (!videoComment) { if (!videoComment) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video comment thread not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video comment thread not found'
})
return false return false
} }
if (videoComment.videoId !== video.id) { if (videoComment.videoId !== video.id) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Video comment is not associated to this video.' })
.json({ error: 'Video comment is not associated to this video.' })
.end()
return false return false
} }
@ -72,14 +63,14 @@ async function doesCommentIdExist (idArg: number | string, res: express.Response
const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id)
if (!videoComment) { if (!videoComment) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video comment thread not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Video comment thread not found'
})
return false return false
} }
res.locals.videoCommentFull = videoComment res.locals.videoCommentFull = videoComment
return true return true
} }

View File

@ -36,10 +36,10 @@ async function doesVideoImportExist (id: number, res: express.Response) {
const videoImport = await VideoImportModel.loadAndPopulateVideo(id) const videoImport = await VideoImportModel.loadAndPopulateVideo(id)
if (!videoImport) { if (!videoImport) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video import not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video import not found'
})
return false return false
} }

View File

@ -9,10 +9,10 @@ export async function doesChangeVideoOwnershipExist (idArg: number | string, res
const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) const videoChangeOwnership = await VideoChangeOwnershipModel.load(id)
if (!videoChangeOwnership) { if (!videoChangeOwnership) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video change ownership not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video change ownership not found'
})
return false return false
} }
@ -25,8 +25,9 @@ export function checkUserCanTerminateOwnershipChange (user: MUserId, videoChange
return true return true
} }
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot terminate an ownership change of another user' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot terminate an ownership change of another user'
})
return false return false
} }

View File

@ -8,6 +8,7 @@ import { isArray } from './custom-validators/misc'
import { logger } from './logger' import { logger } from './logger'
import { deleteFileAndCatch, generateRandomString } from './utils' import { deleteFileAndCatch, generateRandomString } from './utils'
import { getExtFromMimetype } from './video' import { getExtFromMimetype } from './video'
import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) { function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
if (paramNSFW === 'true') return true if (paramNSFW === 'true') return true
@ -125,6 +126,34 @@ function getCountVideos (req: express.Request) {
return req.query.skipCount !== true return req.query.skipCount !== true
} }
// helpers added in server.ts and used in subsequent controllers used
const apiResponseHelpers = (req, res: express.Response, next = null) => {
res.fail = (options) => {
const { data, status, message, title, type, docs, instance } = {
data: null,
status: HttpStatusCode.BAD_REQUEST_400,
...options
}
const extension = new ProblemDocumentExtension({
...data,
docs: docs || res.docs
})
res.status(status)
res.setHeader('Content-Type', 'application/problem+json')
res.json(new ProblemDocument({
status,
title,
instance,
type: type && '' + type,
detail: message
}, extension))
}
if (next !== null) next()
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export { export {
@ -134,5 +163,6 @@ export {
badRequest, badRequest,
createReqFiles, createReqFiles,
cleanUpReqFiles, cleanUpReqFiles,
getCountVideos getCountVideos,
apiResponseHelpers
} }

View File

@ -6,8 +6,10 @@ async function doesAbuseExist (abuseId: number | string, res: Response) {
const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10)) const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10))
if (!abuse) { if (!abuse) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Abuse not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Abuse not found'
})
return false return false
} }

View File

@ -27,15 +27,15 @@ async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sen
if (!account) { if (!account) {
if (sendNotFound === true) { if (sendNotFound === true) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Account not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Account not found'
})
} }
return false return false
} }
res.locals.account = account res.locals.account = account
return true return true
} }
@ -43,14 +43,14 @@ async function doesUserFeedTokenCorrespond (id: number, token: string, res: Resp
const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10))
if (token !== user.feedToken) { if (token !== user.feedToken) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'User and token mismatch' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'User and token mismatch'
})
return false return false
} }
res.locals.user = user res.locals.user = user
return true return true
} }

View File

@ -6,10 +6,10 @@ async function doesVideoBlacklistExist (videoId: number, res: Response) {
const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId) const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
if (videoBlacklist === null) { if (videoBlacklist === null) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Blacklisted video not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Blacklisted video not found'
})
return false return false
} }

View File

@ -7,9 +7,10 @@ async function doesVideoCaptionExist (video: MVideoId, language: string, res: Re
const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language) const videoCaption = await VideoCaptionModel.loadByVideoIdAndLanguage(video.id, language)
if (!videoCaption) { if (!videoCaption) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video caption not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Video caption not found'
})
return false return false
} }

View File

@ -31,9 +31,10 @@ export {
function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) { function processVideoChannelExist (videoChannel: MChannelBannerAccountDefault, res: express.Response) {
if (!videoChannel) { if (!videoChannel) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video channel not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Video channel not found'
})
return false return false
} }

View File

@ -28,10 +28,10 @@ export {
function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) { function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
if (!videoPlaylist) { if (!videoPlaylist) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video playlist not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video playlist not found'
})
return false return false
} }

View File

@ -21,10 +21,10 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
const video = await fetchVideo(id, fetchType, userId) const video = await fetchVideo(id, fetchType, userId)
if (video === null) { if (video === null) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video not found'
})
return false return false
} }
@ -55,10 +55,10 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) { async function doesVideoFileOfVideoExist (id: number, videoIdOrUUID: number | string, res: Response) {
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) { if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'VideoFile matching Video not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'VideoFile matching Video not found'
})
return false return false
} }
@ -69,9 +69,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId) const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
if (videoChannel === null) { if (videoChannel === null) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Unknown video "video channel" for this instance.' })
.json({ error: 'Unknown video "video channel" for this instance.' })
return false return false
} }
@ -82,9 +80,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
} }
if (videoChannel.Account.id !== user.Account.id) { if (videoChannel.Account.id !== user.Account.id) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({
.json({ error: 'Unknown video "video channel" for this account.' }) message: 'Unknown video "video channel" for this account.'
})
return false return false
} }
@ -95,9 +93,10 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) {
// Retrieve the user who did the request // Retrieve the user who did the request
if (onlyOwned && video.isOwned() === false) { if (onlyOwned && video.isOwned() === false) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot manage a video of another server.' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot manage a video of another server.'
})
return false return false
} }
@ -106,9 +105,10 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
// Or if s/he is the video's account // Or if s/he is the video's account
const account = video.VideoChannel.Account const account = video.VideoChannel.Account
if (user.hasRight(right) === false && account.userId !== user.id) { if (user.hasRight(right) === false && account.userId !== user.id) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot manage a video of another user.' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot manage a video of another user.'
})
return false return false
} }

View File

@ -549,11 +549,11 @@ async function serveIndexHTML (req: express.Request, res: express.Response) {
return return
} catch (err) { } catch (err) {
logger.error('Cannot generate HTML page.', err) logger.error('Cannot generate HTML page.', err)
return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) return res.status(HttpStatusCode.INTERNAL_SERVER_ERROR_500).end()
} }
} }
return res.sendStatus(HttpStatusCode.NOT_ACCEPTABLE_406) return res.status(HttpStatusCode.NOT_ACCEPTABLE_406).end()
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -29,11 +29,14 @@ async function checkSignature (req: Request, res: Response, next: NextFunction)
const activity: ActivityDelete = req.body const activity: ActivityDelete = req.body
if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) { if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
logger.debug('Handling signature error on actor delete activity', { err }) logger.debug('Handling signature error on actor delete activity', { err })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204) return res.status(HttpStatusCode.NO_CONTENT_204).end()
} }
logger.warn('Error in ActivityPub signature checker.', { err }) logger.warn('Error in ActivityPub signature checker.', { err })
return res.sendStatus(HttpStatusCode.FORBIDDEN_403) return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'ActivityPub signature could not be checked'
})
} }
} }
@ -71,13 +74,22 @@ async function checkHttpSignature (req: Request, res: Response) {
} catch (err) { } catch (err) {
logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err }) logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err })
res.status(HttpStatusCode.FORBIDDEN_403).json({ error: err.message }) res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: err.message
})
return false return false
} }
const keyId = parsed.keyId const keyId = parsed.keyId
if (!keyId) { if (!keyId) {
res.sendStatus(HttpStatusCode.FORBIDDEN_403) res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Invalid key ID',
data: {
keyId
}
})
return false return false
} }
@ -94,12 +106,17 @@ async function checkHttpSignature (req: Request, res: Response) {
if (verified !== true) { if (verified !== true) {
logger.warn('Signature from %s is invalid', actorUrl, { parsed }) logger.warn('Signature from %s is invalid', actorUrl, { parsed })
res.sendStatus(HttpStatusCode.FORBIDDEN_403) res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Invalid signature',
data: {
actorUrl
}
})
return false return false
} }
res.locals.signature = { actor } res.locals.signature = { actor }
return true return true
} }
@ -107,7 +124,10 @@ async function checkJsonLDSignature (req: Request, res: Response) {
const signatureObject: ActivityPubSignature = req.body.signature const signatureObject: ActivityPubSignature = req.body.signature
if (!signatureObject || !signatureObject.creator) { if (!signatureObject || !signatureObject.creator) {
res.sendStatus(HttpStatusCode.FORBIDDEN_403) res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Object and creator signature do not match'
})
return false return false
} }
@ -121,11 +141,13 @@ async function checkJsonLDSignature (req: Request, res: Response) {
if (verified !== true) { if (verified !== true) {
logger.warn('Signature not verified.', req.body) logger.warn('Signature not verified.', req.body)
res.sendStatus(HttpStatusCode.FORBIDDEN_403) res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Signature could not be verified'
})
return false return false
} }
res.locals.signature = { actor } res.locals.signature = { actor }
return true return true
} }

View File

@ -16,11 +16,11 @@ function authenticate (req: express.Request, res: express.Response, next: expres
.catch(err => { .catch(err => {
logger.warn('Cannot authenticate.', { err }) logger.warn('Cannot authenticate.', { err })
return res.status(err.status) return res.fail({
.json({ status: err.status,
error: 'Token is invalid.', message: 'Token is invalid',
code: err.name type: err.name
}) })
}) })
} }
@ -52,7 +52,12 @@ function authenticatePromiseIfNeeded (req: express.Request, res: express.Respons
// Already authenticated? (or tried to) // Already authenticated? (or tried to)
if (res.locals.oauth?.token.User) return resolve() if (res.locals.oauth?.token.User) return resolve()
if (res.locals.authenticated === false) return res.sendStatus(HttpStatusCode.UNAUTHORIZED_401) if (res.locals.authenticated === false) {
return res.fail({
status: HttpStatusCode.UNAUTHORIZED_401,
message: 'Not authenticated'
})
}
authenticate(req, res, () => resolve(), authenticateInQuery) authenticate(req, res, () => resolve(), authenticateInQuery)
}) })

View File

@ -10,7 +10,10 @@ function setBodyHostsPort (req: express.Request, res: express.Response, next: ex
// Problem with the url parsing? // Problem with the url parsing?
if (hostWithPort === null) { if (hostWithPort === null) {
return res.sendStatus(HttpStatusCode.INTERNAL_SERVER_ERROR_500) return res.fail({
status: HttpStatusCode.INTERNAL_SERVER_ERROR_500,
message: 'Could not parse hosts'
})
} }
req.body.hosts[i] = hostWithPort req.body.hosts[i] = hostWithPort

View File

@ -10,8 +10,10 @@ function ensureUserHasRight (userRight: UserRight) {
const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.` const message = `User ${user.username} does not have right ${userRight} to access to ${req.path}.`
logger.info(message) logger.info(message)
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: message }) status: HttpStatusCode.FORBIDDEN_403,
message
})
} }
return next() return next()

View File

@ -71,9 +71,7 @@ const abuseReportValidator = [
if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return if (body.comment?.id && !await doesCommentIdExist(body.comment.id, res)) return
if (!body.video?.id && !body.account?.id && !body.comment?.id) { if (!body.video?.id && !body.account?.id && !body.comment?.id) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'video id or account id or comment id is required.' })
.json({ error: 'video id or account id or comment id is required.' })
return return
} }
@ -195,8 +193,10 @@ const getAbuseValidator = [
const message = `User ${user.username} does not have right to get abuse ${abuse.id}` const message = `User ${user.username} does not have right to get abuse ${abuse.id}`
logger.warn(message) logger.warn(message)
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: message }) status: HttpStatusCode.FORBIDDEN_403,
message
})
} }
return next() return next()
@ -209,10 +209,7 @@ const checkAbuseValidForMessagesValidator = [
const abuse = res.locals.abuse const abuse = res.locals.abuse
if (abuse.ReporterAccount.isOwned() === false) { if (abuse.ReporterAccount.isOwned() === false) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'This abuse was created by a user of your instance.' })
.json({
error: 'This abuse was created by a user of your instance.'
})
} }
return next() return next()
@ -246,13 +243,17 @@ const deleteAbuseMessageValidator = [
const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id)
if (!abuseMessage) { if (!abuseMessage) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.json({ error: 'Abuse message not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Abuse message not found'
})
} }
if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) { if (user.hasRight(UserRight.MANAGE_ABUSES) !== true && abuseMessage.accountId !== user.Account.id) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Cannot delete this abuse message' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot delete this abuse message'
})
} }
res.locals.abuseMessage = abuseMessage res.locals.abuseMessage = abuseMessage

View File

@ -10,7 +10,7 @@ async function activityPubValidator (req: express.Request, res: express.Response
if (!isRootActivityValid(req.body)) { if (!isRootActivityValid(req.body)) {
logger.warn('Incorrect activity parameters.', { activity: req.body }) logger.warn('Incorrect activity parameters.', { activity: req.body })
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Incorrect activity.' }) .end()
} }
const serverActor = await getServerActor() const serverActor = await getServerActor()

View File

@ -24,9 +24,10 @@ const blockAccountValidator = [
const accountToBlock = res.locals.account const accountToBlock = res.locals.account
if (user.Account.id === accountToBlock.id) { if (user.Account.id === accountToBlock.id) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.json({ error: 'You cannot block yourself.' }) status: HttpStatusCode.CONFLICT_409,
message: 'You cannot block yourself.'
})
return return
} }
@ -79,8 +80,10 @@ const blockServerValidator = [
const host: string = req.body.host const host: string = req.body.host
if (host === WEBSERVER.HOST) { if (host === WEBSERVER.HOST) {
return res.status(HttpStatusCode.CONFLICT_409) return res.fail({
.json({ error: 'You cannot block your own server.' }) status: HttpStatusCode.CONFLICT_409,
message: 'You cannot block your own server.'
})
} }
const server = await ServerModel.loadOrCreateByHost(host) const server = await ServerModel.loadOrCreateByHost(host)
@ -137,27 +140,27 @@ export {
async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) { async function doesUnblockAccountExist (accountId: number, targetAccountId: number, res: express.Response) {
const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId) const accountBlock = await AccountBlocklistModel.loadByAccountAndTarget(accountId, targetAccountId)
if (!accountBlock) { if (!accountBlock) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Account block entry not found.' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Account block entry not found.'
})
return false return false
} }
res.locals.accountBlock = accountBlock res.locals.accountBlock = accountBlock
return true return true
} }
async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) { async function doesUnblockServerExist (accountId: number, host: string, res: express.Response) {
const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host) const serverBlock = await ServerBlocklistModel.loadByAccountAndHost(accountId, host)
if (!serverBlock) { if (!serverBlock) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Server block entry not found.' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Server block entry not found.'
})
return false return false
} }
res.locals.serverBlock = serverBlock res.locals.serverBlock = serverBlock
return true return true
} }

View File

@ -23,9 +23,9 @@ const bulkRemoveCommentsOfValidator = [
const body = req.body as BulkRemoveCommentsOfBody const body = req.body as BulkRemoveCommentsOfBody
if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) { if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ status: HttpStatusCode.FORBIDDEN_403,
error: 'User cannot remove any comments of this instance.' message: 'User cannot remove any comments of this instance.'
}) })
} }

View File

@ -2,7 +2,6 @@ import * as express from 'express'
import { body } from 'express-validator' import { body } from 'express-validator'
import { isIntOrNull } from '@server/helpers/custom-validators/misc' import { isIntOrNull } from '@server/helpers/custom-validators/misc'
import { isEmailEnabled } from '@server/initializers/config' import { isEmailEnabled } from '@server/initializers/config'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { CustomConfig } from '../../../shared/models/server/custom-config.model' import { CustomConfig } from '../../../shared/models/server/custom-config.model'
import { isThemeNameValid } from '../../helpers/custom-validators/plugins' import { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
@ -115,9 +114,7 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp
if (isEmailEnabled()) return true if (isEmailEnabled()) return true
if (customConfig.signup.requiresEmailVerification === true) { if (customConfig.signup.requiresEmailVerification === true) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Emailer is disabled but you require signup email verification.' })
.send({ error: 'Emailer is disabled but you require signup email verification.' })
.end()
return false return false
} }
@ -128,9 +125,7 @@ function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express
if (customConfig.transcoding.enabled === false) return true if (customConfig.transcoding.enabled === false) return true
if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) { if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' })
.send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' })
.end()
return false return false
} }
@ -141,9 +136,7 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon
if (customConfig.live.enabled === false) return true if (customConfig.live.enabled === false) return true
if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) { if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
.send({ error: 'You cannot allow live replay if transcoding is not enabled' })
.end()
return false return false
} }

View File

@ -36,10 +36,10 @@ function setFeedFormatContentType (req: express.Request, res: express.Response,
if (req.accepts(acceptableContentTypes)) { if (req.accepts(acceptableContentTypes)) {
res.set('Content-Type', req.accepts(acceptableContentTypes) as string) res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
} else { } else {
return res.status(HttpStatusCode.NOT_ACCEPTABLE_406) return res.fail({
.json({ status: HttpStatusCode.NOT_ACCEPTABLE_406,
message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}` message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
}) })
} }
return next() return next()
@ -106,10 +106,7 @@ const videoCommentsFeedsValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) { if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
.json({
message: 'videoId cannot be mixed with a channel filter'
})
} }
if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return if (req.query.videoId && !await doesVideoExist(req.query.videoId, res)) return

View File

@ -63,11 +63,10 @@ const removeFollowingValidator = [
const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host) const follow = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(serverActor.id, SERVER_ACTOR_NAME, req.params.host)
if (!follow) { if (!follow) {
return res return res.fail({
.status(HttpStatusCode.NOT_FOUND_404) status: HttpStatusCode.NOT_FOUND_404,
.json({ message: `Following ${req.params.host} not found.`
error: `Following ${req.params.host} not found.` })
})
} }
res.locals.follow = follow res.locals.follow = follow
@ -95,12 +94,10 @@ const getFollowerValidator = [
} }
if (!follow) { if (!follow) {
return res return res.fail({
.status(HttpStatusCode.NOT_FOUND_404) status: HttpStatusCode.NOT_FOUND_404,
.json({ message: `Follower ${req.params.nameWithHost} not found.`
error: `Follower ${req.params.nameWithHost} not found.` })
})
.end()
} }
res.locals.follow = follow res.locals.follow = follow
@ -114,12 +111,7 @@ const acceptOrRejectFollowerValidator = [
const follow = res.locals.follow const follow = res.locals.follow
if (follow.state !== 'pending') { if (follow.state !== 'pending') {
return res return res.fail({ message: 'Follow is not in pending state.' })
.status(HttpStatusCode.BAD_REQUEST_400)
.json({
error: 'Follow is not in pending state.'
})
.end()
} }
return next() return next()

View File

@ -51,8 +51,13 @@ const oembedValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (req.query.format !== undefined && req.query.format !== 'json') { if (req.query.format !== undefined && req.query.format !== 'json') {
return res.status(HttpStatusCode.NOT_IMPLEMENTED_501) return res.fail({
.json({ error: 'Requested format is not implemented on server.' }) status: HttpStatusCode.NOT_IMPLEMENTED_501,
message: 'Requested format is not implemented on server.',
data: {
format: req.query.format
}
})
} }
const url = req.query.url as string const url = req.query.url as string
@ -65,27 +70,35 @@ const oembedValidator = [
const matches = watchRegex.exec(url) const matches = watchRegex.exec(url)
if (startIsOk === false || matches === null) { if (startIsOk === false || matches === null) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({
.json({ error: 'Invalid url.' }) status: HttpStatusCode.BAD_REQUEST_400,
message: 'Invalid url.',
data: {
url
}
})
} }
const elementId = matches[1] const elementId = matches[1]
if (isIdOrUUIDValid(elementId) === false) { if (isIdOrUUIDValid(elementId) === false) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Invalid video or playlist id.' })
.json({ error: 'Invalid video or playlist id.' })
} }
if (isVideo) { if (isVideo) {
const video = await fetchVideo(elementId, 'all') const video = await fetchVideo(elementId, 'all')
if (!video) { if (!video) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.json({ error: 'Video not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Video not found'
})
} }
if (video.privacy !== VideoPrivacy.PUBLIC) { if (video.privacy !== VideoPrivacy.PUBLIC) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Video is not public' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Video is not public'
})
} }
res.locals.videoAll = video res.locals.videoAll = video
@ -96,13 +109,17 @@ const oembedValidator = [
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined) const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
if (!videoPlaylist) { if (!videoPlaylist) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.json({ error: 'Video playlist not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Video playlist not found'
})
} }
if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) { if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Playlist is not public' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Playlist is not public'
})
} }
res.locals.videoPlaylistSummary = videoPlaylist res.locals.videoPlaylistSummary = videoPlaylist

View File

@ -31,8 +31,18 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType) const npmName = PluginModel.buildNpmName(req.params.pluginName, pluginType)
const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName) const plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
if (!plugin) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!plugin) {
if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No plugin found named ' + npmName
})
}
if (withVersion && plugin.version !== req.params.pluginVersion) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No plugin found named ' + npmName + ' with version ' + req.params.pluginVersion
})
}
res.locals.registeredPlugin = plugin res.locals.registeredPlugin = plugin
@ -50,10 +60,20 @@ const getExternalAuthValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
const plugin = res.locals.registeredPlugin const plugin = res.locals.registeredPlugin
if (!plugin.registerHelpers) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!plugin.registerHelpers) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No registered helpers were found for this plugin'
})
}
const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName) const externalAuth = plugin.registerHelpers.getExternalAuths().find(a => a.authName === req.params.authName)
if (!externalAuth) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!externalAuth) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No external auths were found for this plugin'
})
}
res.locals.externalAuth = externalAuth res.locals.externalAuth = externalAuth
@ -107,8 +127,7 @@ const installOrUpdatePluginValidator = [
const body: InstallOrUpdatePlugin = req.body const body: InstallOrUpdatePlugin = req.body
if (!body.path && !body.npmName) { if (!body.path && !body.npmName) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Should have either a npmName or a path' })
.json({ error: 'Should have either a npmName or a path' })
} }
return next() return next()
@ -137,12 +156,13 @@ const existingPluginValidator = [
const plugin = await PluginModel.loadByNpmName(req.params.npmName) const plugin = await PluginModel.loadByNpmName(req.params.npmName)
if (!plugin) { if (!plugin) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.json({ error: 'Plugin not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Plugin not found'
})
} }
res.locals.plugin = plugin res.locals.plugin = plugin
return next() return next()
} }
] ]
@ -177,9 +197,7 @@ const listAvailablePluginsValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (CONFIG.PLUGINS.INDEX.ENABLED === false) { if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Plugin index is not enabled' })
.json({ error: 'Plugin index is not enabled' })
.end()
} }
return next() return next()

View File

@ -35,11 +35,21 @@ const videoFileRedundancyGetValidator = [
return f.resolution === paramResolution && (!req.params.fps || paramFPS) return f.resolution === paramResolution && (!req.params.fps || paramFPS)
}) })
if (!videoFile) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video file not found.' }) if (!videoFile) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video file not found.'
})
}
res.locals.videoFile = videoFile res.locals.videoFile = videoFile
const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id) const videoRedundancy = await VideoRedundancyModel.loadLocalByFileId(videoFile.id)
if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) if (!videoRedundancy) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video redundancy not found.'
})
}
res.locals.videoRedundancy = videoRedundancy res.locals.videoRedundancy = videoRedundancy
return next() return next()
@ -65,11 +75,21 @@ const videoPlaylistRedundancyGetValidator = [
const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above const paramPlaylistType = req.params.streamingPlaylistType as unknown as number // We casted to int above
const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType) const videoStreamingPlaylist = video.VideoStreamingPlaylists.find(p => p.type === paramPlaylistType)
if (!videoStreamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video playlist not found.' }) if (!videoStreamingPlaylist) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video playlist not found.'
})
}
res.locals.videoStreamingPlaylist = videoStreamingPlaylist res.locals.videoStreamingPlaylist = videoStreamingPlaylist
const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id) const videoRedundancy = await VideoRedundancyModel.loadLocalByStreamingPlaylistId(videoStreamingPlaylist.id)
if (!videoRedundancy) return res.status(HttpStatusCode.NOT_FOUND_404).json({ error: 'Video redundancy not found.' }) if (!videoRedundancy) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video redundancy not found.'
})
}
res.locals.videoRedundancy = videoRedundancy res.locals.videoRedundancy = videoRedundancy
return next() return next()
@ -90,12 +110,10 @@ const updateServerRedundancyValidator = [
const server = await ServerModel.loadByHost(req.params.host) const server = await ServerModel.loadByHost(req.params.host)
if (!server) { if (!server) {
return res return res.fail({
.status(HttpStatusCode.NOT_FOUND_404) status: HttpStatusCode.NOT_FOUND_404,
.json({ message: `Server ${req.params.host} not found.`
error: `Server ${req.params.host} not found.` })
})
.end()
} }
res.locals.server = server res.locals.server = server
@ -129,19 +147,19 @@ const addVideoRedundancyValidator = [
if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return if (!await doesVideoExist(req.body.videoId, res, 'only-video')) return
if (res.locals.onlyVideo.remote === false) { if (res.locals.onlyVideo.remote === false) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot create a redundancy on a local video' })
.json({ error: 'Cannot create a redundancy on a local video' })
} }
if (res.locals.onlyVideo.isLive) { if (res.locals.onlyVideo.isLive) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot create a redundancy of a live video' })
.json({ error: 'Cannot create a redundancy of a live video' })
} }
const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid) const alreadyExists = await VideoRedundancyModel.isLocalByVideoUUIDExists(res.locals.onlyVideo.uuid)
if (alreadyExists) { if (alreadyExists) {
return res.status(HttpStatusCode.CONFLICT_409) return res.fail({
.json({ error: 'This video is already duplicated by your instance.' }) status: HttpStatusCode.CONFLICT_409,
message: 'This video is already duplicated by your instance.'
})
} }
return next() return next()
@ -160,9 +178,10 @@ const removeVideoRedundancyValidator = [
const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10))
if (!redundancy) { if (!redundancy) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.json({ error: 'Video redundancy not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video redundancy not found'
})
} }
res.locals.videoRedundancy = redundancy res.locals.videoRedundancy = redundancy

View File

@ -19,9 +19,10 @@ const serverGetValidator = [
const server = await ServerModel.loadByHost(req.body.host) const server = await ServerModel.loadByHost(req.body.host)
if (!server) { if (!server) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.send({ error: 'Server host not found.' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Server host not found.'
})
} }
res.locals.server = server res.locals.server = server
@ -44,26 +45,26 @@ const contactAdministratorValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (CONFIG.CONTACT_FORM.ENABLED === false) { if (CONFIG.CONTACT_FORM.ENABLED === false) {
return res return res.fail({
.status(HttpStatusCode.CONFLICT_409) status: HttpStatusCode.CONFLICT_409,
.send({ error: 'Contact form is not enabled on this instance.' }) message: 'Contact form is not enabled on this instance.'
.end() })
} }
if (isEmailEnabled() === false) { if (isEmailEnabled() === false) {
return res return res.fail({
.status(HttpStatusCode.CONFLICT_409) status: HttpStatusCode.CONFLICT_409,
.send({ error: 'Emailer is not enabled on this instance.' }) message: 'Emailer is not enabled on this instance.'
.end() })
} }
if (await Redis.Instance.doesContactFormIpExist(req.ip)) { if (await Redis.Instance.doesContactFormIpExist(req.ip)) {
logger.info('Refusing a contact form by %s: already sent one recently.', req.ip) logger.info('Refusing a contact form by %s: already sent one recently.', req.ip)
return res return res.fail({
.status(HttpStatusCode.FORBIDDEN_403) status: HttpStatusCode.FORBIDDEN_403,
.send({ error: 'You already sent a contact form recently.' }) message: 'You already sent a contact form recently.'
.end() })
} }
return next() return next()

View File

@ -20,11 +20,17 @@ const serveThemeCSSValidator = [
const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName) const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
if (!theme || theme.version !== req.params.themeVersion) { if (!theme || theme.version !== req.params.themeVersion) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No theme named ' + req.params.themeName + ' was found with version ' + req.params.themeVersion
})
} }
if (theme.css.includes(req.params.staticEndpoint) === false) { if (theme.css.includes(req.params.staticEndpoint) === false) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404) return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'No static endpoint was found for this theme'
})
} }
res.locals.registeredPlugin = theme res.locals.registeredPlugin = theme

View File

@ -61,11 +61,10 @@ const userSubscriptionGetValidator = [
const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host) const subscription = await ActorFollowModel.loadByActorAndTargetNameAndHostForAPI(user.Account.Actor.id, name, host)
if (!subscription || !subscription.ActorFollowing.VideoChannel) { if (!subscription || !subscription.ActorFollowing.VideoChannel) {
return res return res.fail({
.status(HttpStatusCode.NOT_FOUND_404) status: HttpStatusCode.NOT_FOUND_404,
.json({ message: `Subscription ${req.params.uri} not found.`
error: `Subscription ${req.params.uri} not found.` })
})
} }
res.locals.subscription = subscription res.locals.subscription = subscription

View File

@ -73,23 +73,23 @@ const usersAddValidator = [
const authUser = res.locals.oauth.token.User const authUser = res.locals.oauth.token.User
if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) { if (authUser.role !== UserRole.ADMINISTRATOR && req.body.role !== UserRole.USER) {
return res return res.fail({
.status(HttpStatusCode.FORBIDDEN_403) status: HttpStatusCode.FORBIDDEN_403,
.json({ error: 'You can only create users (and not administrators or moderators)' }) message: 'You can only create users (and not administrators or moderators)'
})
} }
if (req.body.channelName) { if (req.body.channelName) {
if (req.body.channelName === req.body.username) { if (req.body.channelName === req.body.username) {
return res return res.fail({ message: 'Channel name cannot be the same as user username.' })
.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Channel name cannot be the same as user username.' })
} }
const existing = await ActorModel.loadLocalByName(req.body.channelName) const existing = await ActorModel.loadLocalByName(req.body.channelName)
if (existing) { if (existing) {
return res return res.fail({
.status(HttpStatusCode.CONFLICT_409) status: HttpStatusCode.CONFLICT_409,
.json({ error: `Channel with name ${req.body.channelName} already exists.` }) message: `Channel with name ${req.body.channelName} already exists.`
})
} }
} }
@ -121,20 +121,19 @@ const usersRegisterValidator = [
const body: UserRegister = req.body const body: UserRegister = req.body
if (body.channel) { if (body.channel) {
if (!body.channel.name || !body.channel.displayName) { if (!body.channel.name || !body.channel.displayName) {
return res return res.fail({ message: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Channel is optional but if you specify it, channel.name and channel.displayName are required.' })
} }
if (body.channel.name === body.username) { if (body.channel.name === body.username) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Channel name cannot be the same as user username.' })
.json({ error: 'Channel name cannot be the same as user username.' })
} }
const existing = await ActorModel.loadLocalByName(body.channel.name) const existing = await ActorModel.loadLocalByName(body.channel.name)
if (existing) { if (existing) {
return res.status(HttpStatusCode.CONFLICT_409) return res.fail({
.json({ error: `Channel with name ${body.channel.name} already exists.` }) status: HttpStatusCode.CONFLICT_409,
message: `Channel with name ${body.channel.name} already exists.`
})
} }
} }
@ -153,8 +152,7 @@ const usersRemoveValidator = [
const user = res.locals.user const user = res.locals.user
if (user.username === 'root') { if (user.username === 'root') {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot remove the root user' })
.json({ error: 'Cannot remove the root user' })
} }
return next() return next()
@ -173,8 +171,7 @@ const usersBlockingValidator = [
const user = res.locals.user const user = res.locals.user
if (user.username === 'root') { if (user.username === 'root') {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot block the root user' })
.json({ error: 'Cannot block the root user' })
} }
return next() return next()
@ -185,9 +182,7 @@ const deleteMeValidator = [
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
if (user.username === 'root') { if (user.username === 'root') {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'You cannot delete your root account.' })
.json({ error: 'You cannot delete your root account.' })
.end()
} }
return next() return next()
@ -217,8 +212,7 @@ const usersUpdateValidator = [
const user = res.locals.user const user = res.locals.user
if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) { if (user.username === 'root' && req.body.role !== undefined && user.role !== req.body.role) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot change root role.' })
.json({ error: 'Cannot change root role.' })
} }
return next() return next()
@ -273,18 +267,18 @@ const usersUpdateMeValidator = [
if (req.body.password || req.body.email) { if (req.body.password || req.body.email) {
if (user.pluginAuth !== null) { if (user.pluginAuth !== null) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'You cannot update your email or password that is associated with an external auth system.' })
.json({ error: 'You cannot update your email or password that is associated with an external auth system.' })
} }
if (!req.body.currentPassword) { if (!req.body.currentPassword) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'currentPassword parameter is missing.' })
.json({ error: 'currentPassword parameter is missing.' })
} }
if (await user.isPasswordMatch(req.body.currentPassword) !== true) { if (await user.isPasswordMatch(req.body.currentPassword) !== true) {
return res.status(HttpStatusCode.UNAUTHORIZED_401) return res.fail({
.json({ error: 'currentPassword is invalid.' }) status: HttpStatusCode.UNAUTHORIZED_401,
message: 'currentPassword is invalid.'
})
} }
} }
@ -335,8 +329,10 @@ const ensureUserRegistrationAllowed = [
) )
if (allowedResult.allowed === false) { if (allowedResult.allowed === false) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.' }) status: HttpStatusCode.FORBIDDEN_403,
message: allowedResult.errorMessage || 'User registration is not enabled or user limit is reached.'
})
} }
return next() return next()
@ -348,8 +344,10 @@ const ensureUserRegistrationAllowedForIP = [
const allowed = isSignupAllowedForCurrentIP(req.ip) const allowed = isSignupAllowedForCurrentIP(req.ip)
if (allowed === false) { if (allowed === false) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'You are not on a network authorized for registration.' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'You are not on a network authorized for registration.'
})
} }
return next() return next()
@ -390,9 +388,10 @@ const usersResetPasswordValidator = [
const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id)
if (redisVerificationString !== req.body.verificationString) { if (redisVerificationString !== req.body.verificationString) {
return res return res.fail({
.status(HttpStatusCode.FORBIDDEN_403) status: HttpStatusCode.FORBIDDEN_403,
.json({ error: 'Invalid verification string.' }) message: 'Invalid verification string.'
})
} }
return next() return next()
@ -437,9 +436,10 @@ const usersVerifyEmailValidator = [
const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id) const redisVerificationString = await Redis.Instance.getVerifyEmailLink(user.id)
if (redisVerificationString !== req.body.verificationString) { if (redisVerificationString !== req.body.verificationString) {
return res return res.fail({
.status(HttpStatusCode.FORBIDDEN_403) status: HttpStatusCode.FORBIDDEN_403,
.json({ error: 'Invalid verification string.' }) message: 'Invalid verification string.'
})
} }
return next() return next()
@ -455,8 +455,10 @@ const ensureAuthUserOwnsAccountValidator = [
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
if (res.locals.account.id !== user.Account.id) { if (res.locals.account.id !== user.Account.id) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Only owner can access ratings list.' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Only owner can access ratings list.'
})
} }
return next() return next()
@ -471,8 +473,10 @@ const ensureCanManageUser = [
if (authUser.role === UserRole.ADMINISTRATOR) return next() if (authUser.role === UserRole.ADMINISTRATOR) return next()
if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next() if (authUser.role === UserRole.MODERATOR && onUser.role === UserRole.USER) return next()
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'A moderator can only manager users.' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'A moderator can only manager users.'
})
} }
] ]
@ -515,15 +519,19 @@ async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email:
const user = await UserModel.loadByUsernameOrEmail(username, email) const user = await UserModel.loadByUsernameOrEmail(username, email)
if (user) { if (user) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.json({ error: 'User with this username or email already exists.' }) status: HttpStatusCode.CONFLICT_409,
message: 'User with this username or email already exists.'
})
return false return false
} }
const actor = await ActorModel.loadLocalByName(username) const actor = await ActorModel.loadLocalByName(username)
if (actor) { if (actor) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.json({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) status: HttpStatusCode.CONFLICT_409,
message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
})
return false return false
} }
@ -535,14 +543,15 @@ async function checkUserExist (finder: () => Promise<MUserDefault>, res: express
if (!user) { if (!user) {
if (abortResponse === true) { if (abortResponse === true) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'User not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'User not found'
})
} }
return false return false
} }
res.locals.user = user res.locals.user = user
return true return true
} }

View File

@ -1,15 +1,19 @@
import * as express from 'express' import * as express from 'express'
import { query, validationResult } from 'express-validator' import { query, validationResult } from 'express-validator'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
function areValidationErrors (req: express.Request, res: express.Response) { function areValidationErrors (req: express.Request, res: express.Response) {
const errors = validationResult(req) const errors = validationResult(req)
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() }) logger.warn('Incorrect request parameters', { path: req.originalUrl, err: errors.mapped() })
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({
.json({ errors: errors.mapped() }) message: 'Incorrect request parameters: ' + Object.keys(errors.mapped()).join(', '),
instance: req.originalUrl,
data: {
'invalid-params': errors.mapped()
}
})
return true return true
} }

View File

@ -39,10 +39,10 @@ const videosBlacklistAddValidator = [
const video = res.locals.videoAll const video = res.locals.videoAll
if (req.body.unfederate === true && video.remote === true) { if (req.body.unfederate === true && video.remote === true) {
return res return res.fail({
.status(HttpStatusCode.CONFLICT_409) status: HttpStatusCode.CONFLICT_409,
.send({ error: 'You cannot unfederate a remote video.' }) message: 'You cannot unfederate a remote video.'
.end() })
} }
return next() return next()

View File

@ -30,17 +30,16 @@ const videoChannelsAddValidator = [
const actor = await ActorModel.loadLocalByName(req.body.name) const actor = await ActorModel.loadLocalByName(req.body.name)
if (actor) { if (actor) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.send({ error: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' }) status: HttpStatusCode.CONFLICT_409,
.end() message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
})
return false return false
} }
const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
if (count >= VIDEO_CHANNELS.MAX_PER_USER) { if (count >= VIDEO_CHANNELS.MAX_PER_USER) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` })
.send({ error: `You cannot create more than ${VIDEO_CHANNELS.MAX_PER_USER} channels` })
.end()
return false return false
} }
@ -71,13 +70,17 @@ const videoChannelsUpdateValidator = [
// We need to make additional checks // We need to make additional checks
if (res.locals.videoChannel.Actor.isOwned() === false) { if (res.locals.videoChannel.Actor.isOwned() === false) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Cannot update video channel of another server' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot update video channel of another server'
})
} }
if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) { if (res.locals.videoChannel.Account.userId !== res.locals.oauth.token.User.id) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Cannot update video channel of another user' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot update video channel of another user'
})
} }
return next() return next()
@ -154,10 +157,10 @@ export {
function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) { function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAccountDefault, res: express.Response) {
if (videoChannel.Actor.isOwned() === false) { if (videoChannel.Actor.isOwned() === false) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot remove video channel of another server.' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot remove video channel of another server.'
})
return false return false
} }
@ -165,10 +168,10 @@ function checkUserCanDeleteVideoChannel (user: MUser, videoChannel: MChannelAcco
// The user can delete it if s/he is an admin // The user can delete it if s/he is an admin
// Or if s/he is the video channel's account // Or if s/he is the video channel's account
if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) { if (user.hasRight(UserRight.REMOVE_ANY_VIDEO_CHANNEL) === false && videoChannel.Account.userId !== user.id) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot remove video channel of another user' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot remove video channel of another user'
})
return false return false
} }
@ -179,10 +182,10 @@ async function checkVideoChannelIsNotTheLastOne (res: express.Response) {
const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id) const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
if (count <= 1) { if (count <= 1) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.json({ error: 'Cannot remove the last channel of this user' }) status: HttpStatusCode.CONFLICT_409,
.end() message: 'Cannot remove the last channel of this user'
})
return false return false
} }

View File

@ -155,9 +155,10 @@ export {
function isVideoCommentsEnabled (video: MVideo, res: express.Response) { function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
if (video.commentsEnabled !== true) { if (video.commentsEnabled !== true) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.json({ error: 'Video comments are disabled for this video.' }) status: HttpStatusCode.CONFLICT_409,
message: 'Video comments are disabled for this video.'
})
return false return false
} }
@ -166,9 +167,10 @@ function isVideoCommentsEnabled (video: MVideo, res: express.Response) {
function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) { function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MCommentOwnerVideoReply, res: express.Response) {
if (videoComment.isDeleted()) { if (videoComment.isDeleted()) {
res.status(HttpStatusCode.CONFLICT_409) res.fail({
.json({ error: 'This comment is already deleted' }) status: HttpStatusCode.CONFLICT_409,
message: 'This comment is already deleted'
})
return false return false
} }
@ -179,9 +181,10 @@ function checkUserCanDeleteVideoComment (user: MUserAccountUrl, videoComment: MC
videoComment.accountId !== userAccount.id && // Not the comment owner videoComment.accountId !== userAccount.id && // Not the comment owner
videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner videoComment.Video.VideoChannel.accountId !== userAccount.id // Not the video owner
) { ) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot remove video comment of another user' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot remove video comment of another user'
})
return false return false
} }
@ -215,9 +218,11 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon
if (!acceptedResult || acceptedResult.accepted !== true) { if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused local comment.', { acceptedResult, acceptParameters }) logger.info('Refused local comment.', { acceptedResult, acceptParameters })
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: acceptedResult?.errorMessage || 'Refused local comment' })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult?.errorMessage || 'Refused local comment'
})
return false return false
} }

View File

@ -47,14 +47,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) { if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.CONFLICT_409)
.json({ error: 'HTTP import is not enabled on this instance.' }) return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'HTTP import is not enabled on this instance.'
})
} }
if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) { if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.CONFLICT_409)
.json({ error: 'Torrent/magnet URI import is not enabled on this instance.' }) return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Torrent/magnet URI import is not enabled on this instance.'
})
} }
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req) if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
@ -63,8 +69,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) { if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' })
.json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
} }
if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req) if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
@ -100,9 +105,11 @@ async function isImportAccepted (req: express.Request, res: express.Response) {
if (!acceptedResult || acceptedResult.accepted !== true) { if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused to import video.', { acceptedResult, acceptParameters }) logger.info('Refused to import video.', { acceptedResult, acceptParameters })
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: acceptedResult.errorMessage || 'Refused to import video' })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult.errorMessage || 'Refused to import video'
})
return false return false
} }

View File

@ -30,7 +30,12 @@ const videoLiveGetValidator = [
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return
const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id) const videoLive = await VideoLiveModel.loadByVideoId(res.locals.videoAll.id)
if (!videoLive) return res.sendStatus(HttpStatusCode.NOT_FOUND_404) if (!videoLive) {
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Live video not found'
})
}
res.locals.videoLive = videoLive res.locals.videoLive = videoLive
@ -66,22 +71,25 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
if (CONFIG.LIVE.ENABLED !== true) { if (CONFIG.LIVE.ENABLED !== true) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Live is not enabled on this instance' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Live is not enabled on this instance'
})
} }
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Saving live replay is not allowed instance' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Saving live replay is not allowed instance'
})
} }
if (req.body.permanentLive && req.body.saveReplay) { if (req.body.permanentLive && req.body.saveReplay) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
.json({ error: 'Cannot set this live as permanent while saving its replay' })
} }
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
@ -93,11 +101,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) { if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ status: HttpStatusCode.FORBIDDEN_403,
code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED, message: 'Cannot create this live because the max instance lives limit is reached.',
error: 'Cannot create this live because the max instance lives limit is reached.' type: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED.toString()
}) })
} }
} }
@ -107,11 +115,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) { if (totalUserLives >= CONFIG.LIVE.MAX_USER_LIVES) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ status: HttpStatusCode.FORBIDDEN_403,
code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED, type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED.toString(),
error: 'Cannot create this live because the max user lives limit is reached.' message: 'Cannot create this live because the max user lives limit is reached.'
}) })
} }
} }
@ -133,18 +141,18 @@ const videoLiveUpdateValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (req.body.permanentLive && req.body.saveReplay) { if (req.body.permanentLive && req.body.saveReplay) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
.json({ error: 'Cannot set this live as permanent while saving its replay' })
} }
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) { if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Saving live replay is not allowed instance' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Saving live replay is not allowed instance'
})
} }
if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) { if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot update a live that has already started' })
.json({ error: 'Cannot update a live that has already started' })
} }
// Check the user can manage the live // Check the user can manage the live
@ -180,9 +188,10 @@ async function isLiveVideoAccepted (req: express.Request, res: express.Response)
if (!acceptedResult || acceptedResult.accepted !== true) { if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused local live video.', { acceptedResult, acceptParameters }) logger.info('Refused local live video.', { acceptedResult, acceptParameters })
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: acceptedResult.errorMessage || 'Refused local live video' }) status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult.errorMessage || 'Refused local live video'
})
return false return false
} }

View File

@ -46,8 +46,8 @@ const videoPlaylistsAddValidator = getCommonPlaylistEditAttributes().concat([
if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) { if (body.privacy === VideoPlaylistPrivacy.PUBLIC && !body.videoChannelId) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
} }
return next() return next()
@ -85,14 +85,14 @@ const videoPlaylistsUpdateValidator = getCommonPlaylistEditAttributes().concat([
) )
) { ) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Cannot set "public" a playlist that is not assigned to a channel.' }) return res.fail({ message: 'Cannot set "public" a playlist that is not assigned to a channel.' })
} }
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
cleanUpReqFiles(req) cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Cannot update a watch later playlist.' }) return res.fail({ message: 'Cannot update a watch later playlist.' })
} }
if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req) if (body.videoChannelId && !await doesVideoChannelIdExist(body.videoChannelId, res)) return cleanUpReqFiles(req)
@ -114,8 +114,7 @@ const videoPlaylistsDeleteValidator = [
const videoPlaylist = getPlaylist(res) const videoPlaylist = getPlaylist(res)
if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) { if (videoPlaylist.type === VideoPlaylistType.WATCH_LATER) {
return res.status(HttpStatusCode.BAD_REQUEST_400) return res.fail({ message: 'Cannot delete a watch later playlist.' })
.json({ error: 'Cannot delete a watch later playlist.' })
} }
if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) { if (!checkUserCanManageVideoPlaylist(res.locals.oauth.token.User, videoPlaylist, UserRight.REMOVE_ANY_VIDEO_PLAYLIST, res)) {
@ -144,7 +143,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) { if (videoPlaylist.privacy === VideoPlaylistPrivacy.UNLISTED) {
if (isUUIDValid(req.params.playlistId)) return next() if (isUUIDValid(req.params.playlistId)) return next()
return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Playlist not found'
})
} }
if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { if (videoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
@ -156,8 +158,10 @@ const videoPlaylistsGetValidator = (fetchType: VideoPlaylistFetchType) => {
!user || !user ||
(videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST)) (videoPlaylist.OwnerAccount.id !== user.Account.id && !user.hasRight(UserRight.UPDATE_ANY_VIDEO_PLAYLIST))
) { ) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Cannot get this private video playlist.' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot get this private video playlist.'
})
} }
return next() return next()
@ -233,10 +237,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
if (!videoPlaylistElement) { if (!videoPlaylistElement) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video playlist element not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video playlist element not found'
})
return return
} }
res.locals.videoPlaylistElement = videoPlaylistElement res.locals.videoPlaylistElement = videoPlaylistElement
@ -263,15 +267,18 @@ const videoPlaylistElementAPGetValidator = [
const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId)
if (!videoPlaylistElement) { if (!videoPlaylistElement) {
res.status(HttpStatusCode.NOT_FOUND_404) res.fail({
.json({ error: 'Video playlist element not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Video playlist element not found'
})
return return
} }
if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) { if (videoPlaylistElement.VideoPlaylist.privacy === VideoPlaylistPrivacy.PRIVATE) {
return res.status(HttpStatusCode.FORBIDDEN_403).end() return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot get this private video playlist.'
})
} }
res.locals.videoPlaylistElementAP = videoPlaylistElement res.locals.videoPlaylistElementAP = videoPlaylistElement
@ -307,18 +314,12 @@ const videoPlaylistsReorderVideosValidator = [
const reorderLength: number = req.body.reorderLength const reorderLength: number = req.body.reorderLength
if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) { if (startPosition >= nextPosition || insertAfterPosition >= nextPosition) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
.json({ error: `Start position or insert after position exceed the playlist limits (max: ${nextPosition - 1})` })
.end()
return return
} }
if (reorderLength && reorderLength + startPosition > nextPosition) { if (reorderLength && reorderLength + startPosition > nextPosition) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
.json({ error: `Reorder length with this start position exceeds the playlist limits (max: ${nextPosition - startPosition})` })
.end()
return return
} }
@ -401,10 +402,10 @@ function getCommonPlaylistEditAttributes () {
function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) { function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: MVideoPlaylist, right: UserRight, res: express.Response) {
if (videoPlaylist.isOwned() === false) { if (videoPlaylist.isOwned() === false) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot manage video playlist of another server.' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot manage video playlist of another server.'
})
return false return false
} }
@ -412,10 +413,10 @@ function checkUserCanManageVideoPlaylist (user: MUserAccountId, videoPlaylist: M
// The user can delete it if s/he is an admin // The user can delete it if s/he is an admin
// Or if s/he is the video playlist's owner // Or if s/he is the video playlist's owner
if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) { if (user.hasRight(right) === false && videoPlaylist.ownerAccountId !== user.Account.id) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Cannot manage video playlist of another user' }) status: HttpStatusCode.FORBIDDEN_403,
.end() message: 'Cannot manage video playlist of another user'
})
return false return false
} }

View File

@ -37,8 +37,10 @@ const getAccountVideoRateValidatorFactory = function (rateType: VideoRateType) {
const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId) const rate = await AccountVideoRateModel.loadLocalAndPopulateVideo(rateType, req.params.name, +req.params.videoId)
if (!rate) { if (!rate) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.json({ error: 'Video rate not found' }) status: HttpStatusCode.NOT_FOUND_404,
message: 'Video rate not found'
})
} }
res.locals.accountVideoRate = rate res.locals.accountVideoRate = rate

View File

@ -21,7 +21,10 @@ const videoWatchingValidator = [
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
if (user.videosHistoryEnabled === false) { if (user.videosHistoryEnabled === false) {
logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id) logger.warn('Cannot set videos to watch by user %d: videos history is disabled.', user.id)
return res.status(HttpStatusCode.CONFLICT_409).end() return res.fail({
status: HttpStatusCode.CONFLICT_409,
message: 'Video history is disabled'
})
} }
return next() return next()

View File

@ -73,6 +73,7 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
.custom(isIdValid).withMessage('Should have correct video channel id'), .custom(isIdValid).withMessage('Should have correct video channel id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy"
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files }) logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
if (areValidationErrors(req, res)) return cleanUpReqFiles(req) if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@ -88,9 +89,11 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
if (!videoFile.duration) await addDurationToVideo(videoFile) if (!videoFile.duration) await addDurationToVideo(videoFile)
} catch (err) { } catch (err) {
logger.error('Invalid input file in videosAddLegacyValidator.', { err }) logger.error('Invalid input file in videosAddLegacyValidator.', { err })
res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
.json({ error: 'Video file unreadable.' })
res.fail({
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
message: 'Video file unreadable.'
})
return cleanUpReqFiles(req) return cleanUpReqFiles(req)
} }
@ -105,6 +108,7 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
*/ */
const videosAddResumableValidator = [ const videosAddResumableValidator = [
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable"
const user = res.locals.oauth.token.User const user = res.locals.oauth.token.User
const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
@ -118,9 +122,11 @@ const videosAddResumableValidator = [
if (!file.duration) await addDurationToVideo(file) if (!file.duration) await addDurationToVideo(file)
} catch (err) { } catch (err) {
logger.error('Invalid input file in videosAddResumableValidator.', { err }) logger.error('Invalid input file in videosAddResumableValidator.', { err })
res.status(HttpStatusCode.UNPROCESSABLE_ENTITY_422)
.json({ error: 'Video file unreadable.' })
res.fail({
status: HttpStatusCode.UNPROCESSABLE_ENTITY_422,
message: 'Video file unreadable.'
})
return cleanup() return cleanup()
} }
@ -164,6 +170,7 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
.withMessage('Should specify the file mimetype'), .withMessage('Should specify the file mimetype'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit"
const videoFileMetadata = { const videoFileMetadata = {
mimetype: req.headers['x-upload-content-type'] as string, mimetype: req.headers['x-upload-content-type'] as string,
size: +req.headers['x-upload-content-length'], size: +req.headers['x-upload-content-length'],
@ -207,6 +214,7 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
.custom(isIdValid).withMessage('Should have correct video channel id'), .custom(isIdValid).withMessage('Should have correct video channel id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'
logger.debug('Checking videosUpdate parameters', { parameters: req.body }) logger.debug('Checking videosUpdate parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return cleanUpReqFiles(req) if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
@ -242,12 +250,14 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
const serverActor = await getServerActor() const serverActor = await getServerActor()
if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next() if (await VideoModel.checkVideoHasInstanceFollow(video.id, serverActor.id) === true) return next()
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ status: HttpStatusCode.FORBIDDEN_403,
errorCode: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS, message: 'Cannot get this video regarding follow constraints.',
error: 'Cannot get this video regarding follow constraints.', type: ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS.toString(),
originUrl: video.url data: {
}) originUrl: video.url
}
})
} }
const videosCustomGetValidator = ( const videosCustomGetValidator = (
@ -258,6 +268,7 @@ const videosCustomGetValidator = (
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'
logger.debug('Checking videosGet parameters', { parameters: req.params }) logger.debug('Checking videosGet parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
@ -276,8 +287,10 @@ const videosCustomGetValidator = (
// Only the owner or a user that have blacklist rights can see the video // Only the owner or a user that have blacklist rights can see the video
if (!user || !user.canGetVideo(video)) { if (!user || !user.canGetVideo(video)) {
return res.status(HttpStatusCode.FORBIDDEN_403) return res.fail({
.json({ error: 'Cannot get this private/internal or blacklisted video.' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot get this private/internal or blacklisted video.'
})
} }
return next() return next()
@ -291,7 +304,10 @@ const videosCustomGetValidator = (
if (isUUIDValid(req.params.id)) return next() if (isUUIDValid(req.params.id)) return next()
// Don't leak this unlisted video // Don't leak this unlisted video
return res.status(HttpStatusCode.NOT_FOUND_404).end() return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video not found'
})
} }
} }
] ]
@ -318,6 +334,7 @@ const videosRemoveValidator = [
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
async (req: express.Request, res: express.Response, next: express.NextFunction) => { async (req: express.Request, res: express.Response, next: express.NextFunction) => {
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo"
logger.debug('Checking videosRemove parameters', { parameters: req.params }) logger.debug('Checking videosRemove parameters', { parameters: req.params })
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
@ -344,13 +361,11 @@ const videosChangeOwnershipValidator = [
const nextOwner = await AccountModel.loadLocalByName(req.body.username) const nextOwner = await AccountModel.loadLocalByName(req.body.username)
if (!nextOwner) { if (!nextOwner) {
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Changing video ownership to a remote account is not supported yet' })
.json({ error: 'Changing video ownership to a remote account is not supported yet' })
return return
} }
res.locals.nextOwner = nextOwner
res.locals.nextOwner = nextOwner
return next() return next()
} }
] ]
@ -370,8 +385,10 @@ const videosTerminateChangeOwnershipValidator = [
const videoChangeOwnership = res.locals.videoChangeOwnership const videoChangeOwnership = res.locals.videoChangeOwnership
if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) { if (videoChangeOwnership.status !== VideoChangeOwnershipStatus.WAITING) {
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: 'Ownership already accepted or refused' }) status: HttpStatusCode.FORBIDDEN_403,
message: 'Ownership already accepted or refused'
})
return return
} }
@ -388,9 +405,10 @@ const videosAcceptChangeOwnershipValidator = [
const videoChangeOwnership = res.locals.videoChangeOwnership const videoChangeOwnership = res.locals.videoChangeOwnership
const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size) const isAble = await isAbleToUploadVideo(user.id, videoChangeOwnership.Video.getMaxQualityFile().size)
if (isAble === false) { if (isAble === false) {
res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) res.fail({
.json({ error: 'The user video quota is exceeded with this video.' }) status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
message: 'The user video quota is exceeded with this video.'
})
return return
} }
@ -538,9 +556,10 @@ const commonVideosFiltersValidator = [
(req.query.filter === 'all-local' || req.query.filter === 'all') && (req.query.filter === 'all-local' || req.query.filter === 'all') &&
(!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false) (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)
) { ) {
res.status(HttpStatusCode.UNAUTHORIZED_401) res.fail({
.json({ error: 'You are not allowed to see all local videos.' }) status: HttpStatusCode.UNAUTHORIZED_401,
message: 'You are not allowed to see all local videos.'
})
return return
} }
@ -581,9 +600,7 @@ function areErrorsInScheduleUpdate (req: express.Request, res: express.Response)
if (!req.body.scheduleUpdate.updateAt) { if (!req.body.scheduleUpdate.updateAt) {
logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.') logger.warn('Invalid parameters: scheduleUpdate.updateAt is mandatory.')
res.status(HttpStatusCode.BAD_REQUEST_400) res.fail({ message: 'Schedule update at is mandatory.' })
.json({ error: 'Schedule update at is mandatory.' })
return true return true
} }
} }
@ -605,26 +622,27 @@ async function commonVideoChecksPass (parameters: {
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return false
if (!isVideoFileMimeTypeValid(files)) { if (!isVideoFileMimeTypeValid(files)) {
res.status(HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415) res.fail({
.json({ status: HttpStatusCode.UNSUPPORTED_MEDIA_TYPE_415,
error: 'This file is not supported. Please, make sure it is of the following type: ' + message: 'This file is not supported. Please, make sure it is of the following type: ' +
CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ') CONSTRAINTS_FIELDS.VIDEOS.EXTNAME.join(', ')
}) })
return false return false
} }
if (!isVideoFileSizeValid(videoFileSize.toString())) { if (!isVideoFileSizeValid(videoFileSize.toString())) {
res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) res.fail({
.json({ error: 'This file is too large. It exceeds the maximum file size authorized.' }) status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
message: 'This file is too large. It exceeds the maximum file size authorized.'
})
return false return false
} }
if (await isAbleToUploadVideo(user.id, videoFileSize) === false) { if (await isAbleToUploadVideo(user.id, videoFileSize) === false) {
res.status(HttpStatusCode.PAYLOAD_TOO_LARGE_413) res.fail({
.json({ error: 'The user video quota is exceeded with this video.' }) status: HttpStatusCode.PAYLOAD_TOO_LARGE_413,
message: 'The user video quota is exceeded with this video.'
})
return false return false
} }
@ -650,9 +668,10 @@ export async function isVideoAccepted (
if (!acceptedResult || acceptedResult.accepted !== true) { if (!acceptedResult || acceptedResult.accepted !== true) {
logger.info('Refused local video.', { acceptedResult, acceptParameters }) logger.info('Refused local video.', { acceptedResult, acceptParameters })
res.status(HttpStatusCode.FORBIDDEN_403) res.fail({
.json({ error: acceptedResult.errorMessage || 'Refused local video' }) status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult.errorMessage || 'Refused local video'
})
return false return false
} }

View File

@ -21,9 +21,10 @@ const webfingerValidator = [
const actor = await ActorModel.loadLocalUrlByName(name) const actor = await ActorModel.loadLocalUrlByName(name)
if (!actor) { if (!actor) {
return res.status(HttpStatusCode.NOT_FOUND_404) return res.fail({
.send({ error: 'Actor not found' }) status: HttpStatusCode.NOT_FOUND_404,
.end() message: 'Actor not found'
})
} }
res.locals.actorUrl = actor res.locals.actorUrl = actor

View File

@ -19,6 +19,7 @@ import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/l
import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email' import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
import { waitJobs } from '../../../../shared/extra-utils/server/jobs' import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { User } from '../../../../shared/models/users' import { User } from '../../../../shared/models/users'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
const expect = chai.expect const expect = chai.expect
@ -89,8 +90,8 @@ describe('Test users account verification', function () {
}) })
it('Should not allow login for user with unverified email', async function () { it('Should not allow login for user with unverified email', async function () {
const resLogin = await login(server.url, server.client, user1, 400) const resLogin = await login(server.url, server.client, user1, HttpStatusCode.BAD_REQUEST_400)
expect(resLogin.body.error).to.contain('User email is not verified.') expect(resLogin.body.detail).to.contain('User email is not verified.')
}) })
it('Should verify the user via email and allow login', async function () { it('Should verify the user via email and allow login', async function () {

View File

@ -93,16 +93,16 @@ describe('Test users', function () {
const client = { id: 'client', secret: server.client.secret } const client = { id: 'client', secret: server.client.secret }
const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_client') expect(res.body.type).to.equal('invalid_client')
expect(res.body.error).to.contain('client is invalid') expect(res.body.detail).to.contain('client is invalid')
}) })
it('Should not login with an invalid client secret', async function () { it('Should not login with an invalid client secret', async function () {
const client = { id: server.client.id, secret: 'coucou' } const client = { id: server.client.id, secret: 'coucou' }
const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400) const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_client') expect(res.body.type).to.equal('invalid_client')
expect(res.body.error).to.contain('client is invalid') expect(res.body.detail).to.contain('client is invalid')
}) })
}) })
@ -112,16 +112,16 @@ describe('Test users', function () {
const user = { username: 'captain crochet', password: server.user.password } const user = { username: 'captain crochet', password: server.user.password }
const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_grant') expect(res.body.type).to.equal('invalid_grant')
expect(res.body.error).to.contain('credentials are invalid') expect(res.body.detail).to.contain('credentials are invalid')
}) })
it('Should not login with an invalid password', async function () { it('Should not login with an invalid password', async function () {
const user = { username: server.user.username, password: 'mew_three' } const user = { username: server.user.username, password: 'mew_three' }
const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400) const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_grant') expect(res.body.type).to.equal('invalid_grant')
expect(res.body.error).to.contain('credentials are invalid') expect(res.body.detail).to.contain('credentials are invalid')
}) })
it('Should not be able to upload a video', async function () { it('Should not be able to upload a video', async function () {

View File

@ -22,6 +22,7 @@ import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-
import { HttpMethod } from '@shared/core-utils/miscs/http-methods' import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
import { VideoCreate } from '@shared/models' import { VideoCreate } from '@shared/models'
import { File as UploadXFile, Metadata } from '@uploadx/core' import { File as UploadXFile, Metadata } from '@uploadx/core'
import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument'
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager' import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
import { import {
MAccountDefault, MAccountDefault,
@ -83,8 +84,15 @@ declare module 'express' {
filename: string filename: string
} }
// Extends locals property from Response // Extends Response with added functions and potential variables passed by middlewares
interface Response { interface Response {
docs?: string
fail: (options: {
data?: Record<string, Object>
docs?: string
message: string
} & ProblemDocumentOptions) => void
locals: { locals: {
videoAll?: MVideoFullLight videoAll?: MVideoFullLight
onlyImmutableVideo?: MVideoImmutable onlyImmutableVideo?: MVideoImmutable

View File

@ -38,46 +38,53 @@ info:
# Errors # Errors
The API uses standard HTTP status codes to indicate the success or failure The API uses standard HTTP status codes to indicate the success or failure
of the API call. of the API call, completed by a [RFC7807-compliant](https://tools.ietf.org/html/rfc7807) response body.
``` ```
HTTP 1.1 404 Not Found HTTP 1.1 404 Not Found
Content-Type: application/json Content-Type: application/problem+json; charset=utf-8
{ {
"errorCode": 1 "detail": "Video not found",
"error": "Account not found" "status": 404,
"title": "Not Found",
"type": "about:blank"
} }
``` ```
We provide error codes for [a growing number of cases](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/server/server-error-code.enum.ts), We provide error types for [a growing number of cases](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/server/server-error-code.enum.ts),
but it is still optional. but it is still optional.
### Validation errors ### Validation errors
Each parameter is evaluated on its own against a set of rules before the route validator Each parameter is evaluated on its own against a set of rules before the route validator
proceeds with potential testing involving parameter combinations. Errors coming from Validation proceeds with potential testing involving parameter combinations. Errors coming from validation
errors appear earlier and benefit from a more detailed error type: errors appear earlier and benefit from a more detailed error type:
``` ```
HTTP 1.1 400 Bad Request HTTP 1.1 400 Bad Request
Content-Type: application/json Content-Type: application/problem+json; charset=utf-8
{ {
"errors": { "detail": "Incorrect request parameters: id",
"instance": "/api/v1/videos/9c9de5e8-0a1e-484a-b099-e80766180",
"invalid-params": {
"id": { "id": {
"value": "a117eb-c6a9-4756-bb09-2a956239f", "location": "params",
"msg": "Should have a valid id", "msg": "Invalid value",
"param": "id", "param": "id",
"location": "params" "value": "9c9de5e8-0a1e-484a-b099-e80766180"
} }
} },
"status": 400,
"title": "Bad Request",
"type": "about:blank"
} }
``` ```
Where `id` is the name of the field concerned by the error, within the route definition. Where `id` is the name of the field concerned by the error, within the route definition.
`errors.<field>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and `invalid-params.<field>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and
`errors.<field>.value` reports the value that didn't pass validation whose `errors.<field>.msg` `invalid-params.<field>.value` reports the value that didn't pass validation whose `invalid-params.<field>.msg`
is about. is about.
# Rate limits # Rate limits

View File

@ -4136,6 +4136,11 @@ http-parser-js@^0.5.2:
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9"
integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg== integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==
http-problem-details@^0.1.5:
version "0.1.5"
resolved "https://registry.yarnpkg.com/http-problem-details/-/http-problem-details-0.1.5.tgz#f8f94f4ab9d4050749e9f8566fb85bb8caa2be56"
integrity sha512-GHxfQZ0POP4FWbAM0guOyZyJNWwbLUXp+4XOJdmitS2tp3gHVSatrSX59Yyq/dCkhk4KiGtTWIlXZC83yCkBkA==
http-signature@1.3.5: http-signature@1.3.5:
version "1.3.5" version "1.3.5"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"