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(
// If 400, 403 or 404, the video is private or blocked so redirect to 404
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
let originUrl = err.body.originUrl
if (search) originUrl += search

View file

@ -41,7 +41,7 @@ export class RestExtractor {
if (err.error instanceof Error) {
// 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)
} else if (typeof err.error === 'string') {
errorMessage = err.error

View file

@ -100,6 +100,7 @@
"fs-extra": "^10.0.0",
"got": "^11.8.2",
"helmet": "^4.1.0",
"http-problem-details": "^0.1.5",
"http-signature": "1.3.5",
"ip-anonymize": "^0.1.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 { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
import { ServerConfigManager } from '@server/lib/server-config-manager'
import { apiResponseHelpers } from '@server/helpers/express-utils'
// ----------- Command line -----------
@ -186,6 +187,9 @@ app.use(cookieParser())
// W3C DNT Tracking Status
app.use(advertiseDoNotTrack)
// Response helpers used in developement
app.use(apiResponseHelpers)
// ----------- Views, routes and static files -----------
// API
@ -235,7 +239,11 @@ app.use(function (err, req, res, next) {
const sql = err.parent ? err.parent.sql : undefined
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)

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
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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 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)
// Don't wait result
res.sendStatus(HttpStatusCode.NO_CONTENT_204)
res.status(HttpStatusCode.NO_CONTENT_204).end()
for (const comment of comments) {
await removeComment(comment)

View file

@ -27,7 +27,12 @@ export {
async function getInstanceHomepage (req: express.Request, res: express.Response) {
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())
}
@ -38,5 +43,5 @@ async function updateInstanceHomepage (req: express.Request, res: express.Respon
await ActorCustomPageModel.updateInstanceHomepage(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
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)
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()

View file

@ -144,7 +144,7 @@ async function installPlugin (req: express.Request, res: express.Response) {
return res.json(plugin.toFormattedJSON())
} catch (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())
} catch (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)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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)
if (!resultList) {
return res.status(HttpStatusCode.SERVICE_UNAVAILABLE_503)
.json({ error: 'Plugin index unavailable. Please retry later' })
return res.fail({
status: HttpStatusCode.SERVICE_UNAVAILABLE_503,
message: 'Plugin index unavailable. Please retry later'
})
}
return res.json(resultList)

View file

@ -102,7 +102,10 @@ async function searchVideoChannelsIndex (query: VideoChannelsSearchQuery, res: e
} catch (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) {
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 { 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 * as express from 'express'
import { UserRight } from '../../../../shared/models/users'
@ -41,5 +42,5 @@ async function runCommand (req: express.Request, res: express.Response) {
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
})
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
async function removeVideoRedundancyController (req: express.Request, res: express.Response) {
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) {
@ -110,5 +110,5 @@ async function updateRedundancy (req: express.Request, res: express.Response) {
removeRedundanciesOfServer(server.id)
.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 })
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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)
}
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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)
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) {
logger.warn('Login error', { err })
return res.status(err.code || 400).json({
code: err.name,
error: err.message
return res.fail({
status: err.code,
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)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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)
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
return res.status(HttpStatusCode.NO_CONTENT_204).end()
}
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)
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) {
@ -82,7 +82,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
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) {
@ -105,5 +105,5 @@ async function removeVideoFromBlacklistController (req: express.Request, res: ex
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) {
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))

View file

@ -18,7 +18,6 @@ import {
} from '@server/types/models'
import { MVideoImportFormattable } from '@server/types/models/video/video-import'
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 { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
@ -143,10 +142,12 @@ async function addYoutubeDLImport (req: express.Request, res: express.Response)
} catch (err) {
logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({
error: 'Cannot fetch remote information of this URL.'
})
return res.fail({
message: 'Cannot fetch remote information of this URL.',
data: {
targetUrl
}
})
}
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) {
cleanUpReqFiles(req)
res.status(HttpStatusCode.BAD_REQUEST_400)
.json({
code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
error: 'Torrents with only 1 file are supported.'
})
res.fail({
type: ServerErrorCode.INCORRECT_FILES_IN_TORRENT.toString(),
message: 'Torrents with only 1 file are supported.'
})
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)
if (exists) {
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)
@ -179,7 +179,7 @@ async function viewVideo (req: express.Request, res: express.Response) {
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) {

View file

@ -76,7 +76,7 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
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) {

View file

@ -122,7 +122,7 @@ function acceptOwnership (req: express.Request, res: express.Response) {
videoChangeOwnership.status = VideoChangeOwnershipStatus.ACCEPTED
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
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
// Set timeout to 10 minutes, as Express's default is 2 minutes
req.setTimeout(1000 * 60 * 10, () => {
logger.error('Upload video has timed out.')
return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408)
logger.error('Video upload has timed out.')
return res.fail({
status: HttpStatusCode.REQUEST_TIMEOUT_408,
message: 'Video upload has timed out.'
})
})
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
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)
@ -105,7 +105,7 @@ function serveServerTranslations (req: express.Request, res: express.Response) {
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) {

View file

@ -41,7 +41,12 @@ export {
async function downloadTorrent (req: express.Request, res: express.Response) {
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 }
@ -60,7 +65,12 @@ async function downloadVideoFile (req: express.Request, res: express.Response) {
const video = res.locals.videoAll
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 }
@ -81,7 +91,12 @@ async function downloadHLSVideoFile (req: express.Request, res: express.Response
if (!streamingPlaylist) return res.status(HttpStatusCode.NOT_FOUND_404).end
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 }
@ -131,9 +146,11 @@ function isVideoDownloadAllowed (_object: {
function checkAllowResult (res: express.Response, allowParameters: any, result?: AllowedResult) {
if (!result || result.allowed !== true) {
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
}

View file

@ -56,10 +56,10 @@ async function getActorImage (req: express.Request, res: express.Response) {
}
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.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)
@ -67,7 +67,7 @@ async function getActorImage (req: express.Request, res: express.Response) {
await pushActorImageProcessInQueue({ filename: image.filename, fileUrl: image.fileUrl, type: image.type })
} catch (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
@ -83,21 +83,21 @@ async function getActorImage (req: express.Request, res: express.Response) {
async function getPreview (req: express.Request, res: express.Response) {
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 })
}
async function getVideoCaption (req: express.Request, res: express.Response) {
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 })
}
async function getTorrent (req: express.Request, res: express.Response) {
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)
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)
if (!result) {
return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
return res.status(HttpStatusCode.NOT_FOUND_404).end()
}
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.sendStatus(HttpStatusCode.NOT_FOUND_404)
return res.status(HttpStatusCode.NOT_FOUND_404).end()
}
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 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('/')
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 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)
}
@ -130,7 +130,7 @@ function servePluginClientScripts (req: express.Request, res: express.Response)
const staticEndpoint = req.params.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)
}
@ -140,7 +140,7 @@ function serveThemeCSSDirectory (req: express.Request, res: express.Response) {
const staticEndpoint = req.params.staticEndpoint
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)

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,7 @@ import { isArray } from './custom-validators/misc'
import { logger } from './logger'
import { deleteFileAndCatch, generateRandomString } from './utils'
import { getExtFromMimetype } from './video'
import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
if (paramNSFW === 'true') return true
@ -125,6 +126,34 @@ function getCountVideos (req: express.Request) {
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 {
@ -134,5 +163,6 @@ export {
badRequest,
createReqFiles,
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))
if (!abuse) {
res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Abuse not found' })
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Abuse not found'
})
return false
}

View file

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

View file

@ -6,10 +6,10 @@ async function doesVideoBlacklistExist (videoId: number, res: Response) {
const videoBlacklist = await VideoBlacklistModel.loadByVideoId(videoId)
if (videoBlacklist === null) {
res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Blacklisted video not found' })
.end()
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Blacklisted video not found'
})
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)
if (!videoCaption) {
res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Video caption not found' })
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video caption not found'
})
return false
}

View file

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

View file

@ -28,10 +28,10 @@ export {
function handleVideoPlaylist (videoPlaylist: MVideoPlaylist, res: express.Response) {
if (!videoPlaylist) {
res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Video playlist not found' })
.end()
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video playlist not found'
})
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)
if (video === null) {
res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Video not found' })
.end()
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video not found'
})
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) {
if (!await VideoFileModel.doesVideoExistForVideoFile(id, videoIdOrUUID)) {
res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'VideoFile matching Video not found' })
.end()
res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'VideoFile matching Video not found'
})
return false
}
@ -69,9 +69,7 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
const videoChannel = await VideoChannelModel.loadAndPopulateAccount(channelId)
if (videoChannel === null) {
res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Unknown video "video channel" for this instance.' })
res.fail({ message: 'Unknown video "video channel" for this instance.' })
return false
}
@ -82,9 +80,9 @@ async function doesVideoChannelOfAccountExist (channelId: number, user: MUserAcc
}
if (videoChannel.Account.id !== user.Account.id) {
res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Unknown video "video channel" for this account.' })
res.fail({
message: 'Unknown video "video channel" for this account.'
})
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) {
// Retrieve the user who did the request
if (onlyOwned && video.isOwned() === false) {
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Cannot manage a video of another server.' })
.end()
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage a video of another server.'
})
return false
}
@ -106,9 +105,10 @@ function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right:
// Or if s/he is the video's account
const account = video.VideoChannel.Account
if (user.hasRight(right) === false && account.userId !== user.id) {
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Cannot manage a video of another user.' })
.end()
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Cannot manage a video of another user.'
})
return false
}

View file

@ -549,11 +549,11 @@ async function serveIndexHTML (req: express.Request, res: express.Response) {
return
} catch (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
if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
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 })
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) {
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
}
const keyId = parsed.keyId
if (!keyId) {
res.sendStatus(HttpStatusCode.FORBIDDEN_403)
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Invalid key ID',
data: {
keyId
}
})
return false
}
@ -94,12 +106,17 @@ async function checkHttpSignature (req: Request, res: Response) {
if (verified !== true) {
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
}
res.locals.signature = { actor }
return true
}
@ -107,7 +124,10 @@ async function checkJsonLDSignature (req: Request, res: Response) {
const signatureObject: ActivityPubSignature = req.body.signature
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
}
@ -121,11 +141,13 @@ async function checkJsonLDSignature (req: Request, res: Response) {
if (verified !== true) {
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
}
res.locals.signature = { actor }
return true
}

View file

@ -16,11 +16,11 @@ function authenticate (req: express.Request, res: express.Response, next: expres
.catch(err => {
logger.warn('Cannot authenticate.', { err })
return res.status(err.status)
.json({
error: 'Token is invalid.',
code: err.name
})
return res.fail({
status: err.status,
message: 'Token is invalid',
type: err.name
})
})
}
@ -52,7 +52,12 @@ function authenticatePromiseIfNeeded (req: express.Request, res: express.Respons
// Already authenticated? (or tried to)
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)
})

View file

@ -10,7 +10,10 @@ function setBodyHostsPort (req: express.Request, res: express.Response, next: ex
// Problem with the url parsing?
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

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}.`
logger.info(message)
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: message })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message
})
}
return next()

View file

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

View file

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

View file

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

View file

@ -23,9 +23,9 @@ const bulkRemoveCommentsOfValidator = [
const body = req.body as BulkRemoveCommentsOfBody
if (body.scope === 'instance' && user.hasRight(UserRight.REMOVE_ANY_VIDEO_COMMENT) !== true) {
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({
error: 'User cannot remove any comments of this instance.'
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
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 { isIntOrNull } from '@server/helpers/custom-validators/misc'
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 { isThemeNameValid } from '../../helpers/custom-validators/plugins'
import { isUserNSFWPolicyValid, isUserVideoQuotaDailyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
@ -115,9 +114,7 @@ function checkInvalidConfigIfEmailDisabled (customConfig: CustomConfig, res: exp
if (isEmailEnabled()) return true
if (customConfig.signup.requiresEmailVerification === true) {
res.status(HttpStatusCode.BAD_REQUEST_400)
.send({ error: 'Emailer is disabled but you require signup email verification.' })
.end()
res.fail({ message: 'Emailer is disabled but you require signup email verification.' })
return false
}
@ -128,9 +125,7 @@ function checkInvalidTranscodingConfig (customConfig: CustomConfig, res: express
if (customConfig.transcoding.enabled === false) return true
if (customConfig.transcoding.webtorrent.enabled === false && customConfig.transcoding.hls.enabled === false) {
res.status(HttpStatusCode.BAD_REQUEST_400)
.send({ error: 'You need to enable at least webtorrent transcoding or hls transcoding' })
.end()
res.fail({ message: 'You need to enable at least webtorrent transcoding or hls transcoding' })
return false
}
@ -141,9 +136,7 @@ function checkInvalidLiveConfig (customConfig: CustomConfig, res: express.Respon
if (customConfig.live.enabled === false) return true
if (customConfig.live.allowReplay === true && customConfig.transcoding.enabled === false) {
res.status(HttpStatusCode.BAD_REQUEST_400)
.send({ error: 'You cannot allow live replay if transcoding is not enabled' })
.end()
res.fail({ message: 'You cannot allow live replay if transcoding is not enabled' })
return false
}

View file

@ -36,10 +36,10 @@ function setFeedFormatContentType (req: express.Request, res: express.Response,
if (req.accepts(acceptableContentTypes)) {
res.set('Content-Type', req.accepts(acceptableContentTypes) as string)
} else {
return res.status(HttpStatusCode.NOT_ACCEPTABLE_406)
.json({
message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
})
return res.fail({
status: HttpStatusCode.NOT_ACCEPTABLE_406,
message: `You should accept at least one of the following content-types: ${acceptableContentTypes.join(', ')}`
})
}
return next()
@ -106,10 +106,7 @@ const videoCommentsFeedsValidator = [
if (areValidationErrors(req, res)) return
if (req.query.videoId && (req.query.videoChannelId || req.query.videoChannelName)) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({
message: 'videoId cannot be mixed with a channel filter'
})
return res.fail({ message: 'videoId cannot be mixed with a channel filter' })
}
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)
if (!follow) {
return res
.status(HttpStatusCode.NOT_FOUND_404)
.json({
error: `Following ${req.params.host} not found.`
})
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: `Following ${req.params.host} not found.`
})
}
res.locals.follow = follow
@ -95,12 +94,10 @@ const getFollowerValidator = [
}
if (!follow) {
return res
.status(HttpStatusCode.NOT_FOUND_404)
.json({
error: `Follower ${req.params.nameWithHost} not found.`
})
.end()
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: `Follower ${req.params.nameWithHost} not found.`
})
}
res.locals.follow = follow
@ -114,12 +111,7 @@ const acceptOrRejectFollowerValidator = [
const follow = res.locals.follow
if (follow.state !== 'pending') {
return res
.status(HttpStatusCode.BAD_REQUEST_400)
.json({
error: 'Follow is not in pending state.'
})
.end()
return res.fail({ message: 'Follow is not in pending state.' })
}
return next()

View file

@ -51,8 +51,13 @@ const oembedValidator = [
if (areValidationErrors(req, res)) return
if (req.query.format !== undefined && req.query.format !== 'json') {
return res.status(HttpStatusCode.NOT_IMPLEMENTED_501)
.json({ error: 'Requested format is not implemented on server.' })
return res.fail({
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
@ -65,27 +70,35 @@ const oembedValidator = [
const matches = watchRegex.exec(url)
if (startIsOk === false || matches === null) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Invalid url.' })
return res.fail({
status: HttpStatusCode.BAD_REQUEST_400,
message: 'Invalid url.',
data: {
url
}
})
}
const elementId = matches[1]
if (isIdOrUUIDValid(elementId) === false) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Invalid video or playlist id.' })
return res.fail({ message: 'Invalid video or playlist id.' })
}
if (isVideo) {
const video = await fetchVideo(elementId, 'all')
if (!video) {
return res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Video not found' })
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video not found'
})
}
if (video.privacy !== VideoPrivacy.PUBLIC) {
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Video is not public' })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Video is not public'
})
}
res.locals.videoAll = video
@ -96,13 +109,17 @@ const oembedValidator = [
const videoPlaylist = await VideoPlaylistModel.loadWithAccountAndChannelSummary(elementId, undefined)
if (!videoPlaylist) {
return res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Video playlist not found' })
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Video playlist not found'
})
}
if (videoPlaylist.privacy !== VideoPlaylistPrivacy.PUBLIC) {
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Playlist is not public' })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Playlist is not public'
})
}
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 plugin = PluginManager.Instance.getRegisteredPluginOrTheme(npmName)
if (!plugin) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (withVersion && plugin.version !== req.params.pluginVersion) return res.sendStatus(HttpStatusCode.NOT_FOUND_404)
if (!plugin) {
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
@ -50,10 +60,20 @@ const getExternalAuthValidator = [
if (areValidationErrors(req, res)) return
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)
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
@ -107,8 +127,7 @@ const installOrUpdatePluginValidator = [
const body: InstallOrUpdatePlugin = req.body
if (!body.path && !body.npmName) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Should have either a npmName or a path' })
return res.fail({ message: 'Should have either a npmName or a path' })
}
return next()
@ -137,12 +156,13 @@ const existingPluginValidator = [
const plugin = await PluginModel.loadByNpmName(req.params.npmName)
if (!plugin) {
return res.status(HttpStatusCode.NOT_FOUND_404)
.json({ error: 'Plugin not found' })
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Plugin not found'
})
}
res.locals.plugin = plugin
return next()
}
]
@ -177,9 +197,7 @@ const listAvailablePluginsValidator = [
if (areValidationErrors(req, res)) return
if (CONFIG.PLUGINS.INDEX.ENABLED === false) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Plugin index is not enabled' })
.end()
return res.fail({ message: 'Plugin index is not enabled' })
}
return next()

View file

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

View file

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

View file

@ -20,11 +20,17 @@ const serveThemeCSSValidator = [
const theme = PluginManager.Instance.getRegisteredThemeByShortName(req.params.themeName)
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) {
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,14 +47,20 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
if (CONFIG.IMPORT.VIDEOS.HTTP.ENABLED !== true && req.body.targetUrl) {
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)) {
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)
@ -63,8 +69,7 @@ const videoImportAddValidator = getCommonVideoEditAttributes().concat([
if (!req.body.targetUrl && !req.body.magnetUri && !torrentFile) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
return res.fail({ message: 'Should have a magnetUri or a targetUrl or a torrent file.' })
}
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) {
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
}

View file

@ -30,7 +30,12 @@ const videoLiveGetValidator = [
if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.GET_ANY_LIVE, res, false)) return
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
@ -66,22 +71,25 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
if (CONFIG.LIVE.ENABLED !== true) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Live is not enabled on this instance' })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Live is not enabled on this instance'
})
}
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Saving live replay is not allowed instance' })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Saving live replay is not allowed instance'
})
}
if (req.body.permanentLive && req.body.saveReplay) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Cannot set this live as permanent while saving its replay' })
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
}
const user = res.locals.oauth.token.User
@ -93,11 +101,11 @@ const videoLiveAddValidator = getCommonVideoEditAttributes().concat([
if (totalInstanceLives >= CONFIG.LIVE.MAX_INSTANCE_LIVES) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({
code: ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED,
error: 'Cannot create this live because the max instance lives limit is reached.'
})
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: '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) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({
code: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED,
error: 'Cannot create this live because the max user lives limit is reached.'
})
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
type: ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED.toString(),
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 (req.body.permanentLive && req.body.saveReplay) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Cannot set this live as permanent while saving its replay' })
return res.fail({ message: 'Cannot set this live as permanent while saving its replay' })
}
if (CONFIG.LIVE.ALLOW_REPLAY !== true && req.body.saveReplay === true) {
return res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: 'Saving live replay is not allowed instance' })
return res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: 'Saving live replay is not allowed instance'
})
}
if (res.locals.videoAll.state !== VideoState.WAITING_FOR_LIVE) {
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Cannot update a live that has already started' })
return res.fail({ message: 'Cannot update a live that has already started' })
}
// 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) {
logger.info('Refused local live video.', { acceptedResult, acceptParameters })
res.status(HttpStatusCode.FORBIDDEN_403)
.json({ error: acceptedResult.errorMessage || 'Refused local live video' })
res.fail({
status: HttpStatusCode.FORBIDDEN_403,
message: acceptedResult.errorMessage || 'Refused local live video'
})
return false
}

View file

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

View file

@ -21,7 +21,10 @@ const videoWatchingValidator = [
const user = res.locals.oauth.token.User
if (user.videosHistoryEnabled === false) {
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()

View file

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

View file

@ -21,9 +21,10 @@ const webfingerValidator = [
const actor = await ActorModel.loadLocalUrlByName(name)
if (!actor) {
return res.status(HttpStatusCode.NOT_FOUND_404)
.send({ error: 'Actor not found' })
.end()
return res.fail({
status: HttpStatusCode.NOT_FOUND_404,
message: 'Actor not found'
})
}
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 { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import { User } from '../../../../shared/models/users'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
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 () {
const resLogin = await login(server.url, server.client, user1, 400)
expect(resLogin.body.error).to.contain('User email is not verified.')
const resLogin = await login(server.url, server.client, user1, HttpStatusCode.BAD_REQUEST_400)
expect(resLogin.body.detail).to.contain('User email is not verified.')
})
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 res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_client')
expect(res.body.error).to.contain('client is invalid')
expect(res.body.type).to.equal('invalid_client')
expect(res.body.detail).to.contain('client is invalid')
})
it('Should not login with an invalid client secret', async function () {
const client = { id: server.client.id, secret: 'coucou' }
const res = await login(server.url, client, server.user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_client')
expect(res.body.error).to.contain('client is invalid')
expect(res.body.type).to.equal('invalid_client')
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 res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_grant')
expect(res.body.error).to.contain('credentials are invalid')
expect(res.body.type).to.equal('invalid_grant')
expect(res.body.detail).to.contain('credentials are invalid')
})
it('Should not login with an invalid password', async function () {
const user = { username: server.user.username, password: 'mew_three' }
const res = await login(server.url, server.client, user, HttpStatusCode.BAD_REQUEST_400)
expect(res.body.code).to.equal('invalid_grant')
expect(res.body.error).to.contain('credentials are invalid')
expect(res.body.type).to.equal('invalid_grant')
expect(res.body.detail).to.contain('credentials are invalid')
})
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 { VideoCreate } from '@shared/models'
import { File as UploadXFile, Metadata } from '@uploadx/core'
import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument'
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
import {
MAccountDefault,
@ -83,8 +84,15 @@ declare module 'express' {
filename: string
}
// Extends locals property from Response
// Extends Response with added functions and potential variables passed by middlewares
interface Response {
docs?: string
fail: (options: {
data?: Record<string, Object>
docs?: string
message: string
} & ProblemDocumentOptions) => void
locals: {
videoAll?: MVideoFullLight
onlyImmutableVideo?: MVideoImmutable

View file

@ -38,46 +38,53 @@ info:
# Errors
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
Content-Type: application/json
Content-Type: application/problem+json; charset=utf-8
{
"errorCode": 1
"error": "Account not found"
"detail": "Video 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.
### Validation errors
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:
```
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": {
"value": "a117eb-c6a9-4756-bb09-2a956239f",
"msg": "Should have a valid id",
"location": "params",
"msg": "Invalid value",
"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.
`errors.<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>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and
`invalid-params.<field>.value` reports the value that didn't pass validation whose `invalid-params.<field>.msg`
is about.
# 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"
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:
version "1.3.5"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.5.tgz#9f19496ffbf3227298d7b5f156e0e1a948678683"