Refactor server errors handler
This commit is contained in:
parent
463206948d
commit
e030bfb59d
20 changed files with 236 additions and 60 deletions
|
@ -8,7 +8,7 @@ import { FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
import { VideoCaptionService, VideoEdit, VideoService } from '@app/shared/shared-main'
|
||||||
import { LiveVideoService } from '@app/shared/shared-video-live'
|
import { LiveVideoService } from '@app/shared/shared-video-live'
|
||||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||||
import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, ServerErrorCode, VideoPrivacy } from '@shared/models'
|
import { LiveVideo, LiveVideoCreate, LiveVideoUpdate, PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy } from '@shared/models'
|
||||||
import { VideoSend } from './video-send'
|
import { VideoSend } from './video-send'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -92,9 +92,11 @@ export class VideoGoLiveComponent extends VideoSend implements OnInit, AfterView
|
||||||
|
|
||||||
let message = err.message
|
let message = err.message
|
||||||
|
|
||||||
if (err.body?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
|
const error = err.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
if (error?.code === ServerErrorCode.MAX_INSTANCE_LIVES_LIMIT_REACHED) {
|
||||||
message = $localize`Cannot create live because this instance have too many created lives`
|
message = $localize`Cannot create live because this instance have too many created lives`
|
||||||
} else if (err.body?.code) {
|
} else if (error?.code === ServerErrorCode.MAX_USER_LIVES_LIMIT_REACHED) {
|
||||||
message = $localize`Cannot create live because you created too many lives`
|
message = $localize`Cannot create live because you created too many lives`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { scrollToTop } from '@app/helpers'
|
||||||
import { FormValidatorService } from '@app/shared/shared-forms'
|
import { FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
|
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
|
||||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||||
import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
|
import { PeerTubeProblemDocument, ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
|
||||||
import { hydrateFormFromVideo } from '../shared/video-edit-utils'
|
import { hydrateFormFromVideo } from '../shared/video-edit-utils'
|
||||||
import { VideoSend } from './video-send'
|
import { VideoSend } from './video-send'
|
||||||
|
|
||||||
|
@ -115,7 +115,9 @@ export class VideoImportTorrentComponent extends VideoSend implements OnInit, Af
|
||||||
this.firstStepError.emit()
|
this.firstStepError.emit()
|
||||||
|
|
||||||
let message = err.message
|
let message = err.message
|
||||||
if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
|
|
||||||
|
const error = err.body as PeerTubeProblemDocument
|
||||||
|
if (error?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
|
||||||
message = $localize`Torrents with only 1 file are supported.`
|
message = $localize`Torrents with only 1 file are supported.`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { VideoActionsDisplayType, VideoDownloadComponent } from '@app/shared/sha
|
||||||
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
import { VideoPlaylist, VideoPlaylistService } from '@app/shared/shared-video-playlist'
|
||||||
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
|
||||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||||
import { ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
|
import { PeerTubeProblemDocument, ServerConfig, ServerErrorCode, UserVideoRateType, VideoCaption, VideoPrivacy, VideoState } from '@shared/models'
|
||||||
import {
|
import {
|
||||||
cleanupVideoWatch,
|
cleanupVideoWatch,
|
||||||
getStoredP2PEnabled,
|
getStoredP2PEnabled,
|
||||||
|
@ -431,9 +431,11 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
.pipe(
|
.pipe(
|
||||||
// If 400, 403 or 404, the video is private or blocked so redirect to 404
|
// If 400, 403 or 404, the video is private or blocked so redirect to 404
|
||||||
catchError(err => {
|
catchError(err => {
|
||||||
if (err.body.type === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && err.body.originUrl) {
|
const errorBody = err.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
if (errorBody.code === ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS && errorBody.originUrl) {
|
||||||
const search = window.location.search
|
const search = window.location.search
|
||||||
let originUrl = err.body.originUrl
|
let originUrl = errorBody.originUrl
|
||||||
if (search) originUrl += search
|
if (search) originUrl += search
|
||||||
|
|
||||||
this.confirmService.confirm(
|
this.confirmService.confirm(
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { Injectable, Injector } from '@angular/core'
|
||||||
import { AuthService } from '@app/core/auth/auth.service'
|
import { AuthService } from '@app/core/auth/auth.service'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
|
||||||
|
import { OAuth2ErrorCode, PeerTubeProblemDocument, ServerErrorCode } from '@shared/models/server'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
|
@ -25,7 +26,9 @@ export class AuthInterceptor implements HttpInterceptor {
|
||||||
return next.handle(authReq)
|
return next.handle(authReq)
|
||||||
.pipe(
|
.pipe(
|
||||||
catchError((err: HttpErrorResponse) => {
|
catchError((err: HttpErrorResponse) => {
|
||||||
if (err.status === HttpStatusCode.UNAUTHORIZED_401 && err.error && err.error.code === 'invalid_token') {
|
const error = err.error as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
if (err.status === HttpStatusCode.UNAUTHORIZED_401 && error && error.code === OAuth2ErrorCode.INVALID_TOKEN) {
|
||||||
return this.handleTokenExpired(req, next)
|
return this.handleTokenExpired(req, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-c
|
||||||
import {
|
import {
|
||||||
ClientHookName,
|
ClientHookName,
|
||||||
HTMLServerConfig,
|
HTMLServerConfig,
|
||||||
|
OAuth2ErrorCode,
|
||||||
PluginType,
|
PluginType,
|
||||||
ResultList,
|
ResultList,
|
||||||
UserRefreshToken,
|
UserRefreshToken,
|
||||||
|
@ -118,8 +119,8 @@ export class PeerTubeEmbed {
|
||||||
if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
|
if (res.status === HttpStatusCode.UNAUTHORIZED_401) return undefined
|
||||||
|
|
||||||
return res.json()
|
return res.json()
|
||||||
}).then((obj: UserRefreshToken & { code: 'invalid_grant'}) => {
|
}).then((obj: UserRefreshToken & { code?: OAuth2ErrorCode }) => {
|
||||||
if (!obj || obj.code === 'invalid_grant') {
|
if (!obj || obj.code === OAuth2ErrorCode.INVALID_GRANT) {
|
||||||
Tokens.flush()
|
Tokens.flush()
|
||||||
this.removeTokensFromHeaders()
|
this.removeTokensFromHeaders()
|
||||||
|
|
||||||
|
|
|
@ -106,6 +106,7 @@ import {
|
||||||
downloadRouter
|
downloadRouter
|
||||||
} from './server/controllers'
|
} from './server/controllers'
|
||||||
import { advertiseDoNotTrack } from './server/middlewares/dnt'
|
import { advertiseDoNotTrack } from './server/middlewares/dnt'
|
||||||
|
import { apiFailMiddleware } from './server/middlewares/error'
|
||||||
import { Redis } from './server/lib/redis'
|
import { Redis } from './server/lib/redis'
|
||||||
import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
|
import { ActorFollowScheduler } from './server/lib/schedulers/actor-follow-scheduler'
|
||||||
import { RemoveOldViewsScheduler } from './server/lib/schedulers/remove-old-views-scheduler'
|
import { RemoveOldViewsScheduler } from './server/lib/schedulers/remove-old-views-scheduler'
|
||||||
|
@ -127,7 +128,6 @@ import { LiveManager } from './server/lib/live-manager'
|
||||||
import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from './shared/core-utils/miscs/http-error-codes'
|
||||||
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
|
import { VideosTorrentCache } from '@server/lib/files-cache/videos-torrent-cache'
|
||||||
import { ServerConfigManager } from '@server/lib/server-config-manager'
|
import { ServerConfigManager } from '@server/lib/server-config-manager'
|
||||||
import { apiResponseHelpers } from '@server/helpers/express-utils'
|
|
||||||
|
|
||||||
// ----------- Command line -----------
|
// ----------- Command line -----------
|
||||||
|
|
||||||
|
@ -169,8 +169,8 @@ app.use(morgan('combined', {
|
||||||
skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping'
|
skip: req => CONFIG.LOG.LOG_PING_REQUESTS === false && req.originalUrl === '/api/v1/ping'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Response helpers used for errors
|
// Add .fail() helper to response
|
||||||
app.use(apiResponseHelpers)
|
app.use(apiFailMiddleware)
|
||||||
|
|
||||||
// For body requests
|
// For body requests
|
||||||
app.use(express.urlencoded({ extended: false }))
|
app.use(express.urlencoded({ extended: false }))
|
||||||
|
@ -179,6 +179,7 @@ app.use(express.json({
|
||||||
limit: '500kb',
|
limit: '500kb',
|
||||||
verify: (req: express.Request, res: express.Response, buf: Buffer) => {
|
verify: (req: express.Request, res: express.Response, buf: Buffer) => {
|
||||||
const valid = isHTTPSignatureDigestValid(buf, req)
|
const valid = isHTTPSignatureDigestValid(buf, req)
|
||||||
|
|
||||||
if (valid !== true) {
|
if (valid !== true) {
|
||||||
res.fail({
|
res.fail({
|
||||||
status: HttpStatusCode.FORBIDDEN_403,
|
status: HttpStatusCode.FORBIDDEN_403,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import * as express from 'express'
|
||||||
import toInt from 'validator/lib/toInt'
|
import toInt from 'validator/lib/toInt'
|
||||||
import { doJSONRequest } from '@server/helpers/requests'
|
import { doJSONRequest } from '@server/helpers/requests'
|
||||||
import { LiveManager } from '@server/lib/live-manager'
|
import { LiveManager } from '@server/lib/live-manager'
|
||||||
|
import { docMiddleware } from '@server/middlewares/doc'
|
||||||
import { getServerActor } from '@server/models/application/application'
|
import { getServerActor } from '@server/models/application/application'
|
||||||
import { MVideoAccountLight } from '@server/types/models'
|
import { MVideoAccountLight } from '@server/types/models'
|
||||||
import { VideosCommonQuery } from '../../../../shared'
|
import { VideosCommonQuery } from '../../../../shared'
|
||||||
|
@ -83,6 +84,7 @@ videosRouter.get('/:id/metadata/:videoFileId',
|
||||||
asyncMiddleware(getVideoFileMetadata)
|
asyncMiddleware(getVideoFileMetadata)
|
||||||
)
|
)
|
||||||
videosRouter.get('/:id',
|
videosRouter.get('/:id',
|
||||||
|
docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'),
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
|
asyncMiddleware(videosCustomGetValidator('only-video-with-rights')),
|
||||||
asyncMiddleware(checkVideoFollowConstraints),
|
asyncMiddleware(checkVideoFollowConstraints),
|
||||||
|
@ -94,6 +96,7 @@ videosRouter.post('/:id/views',
|
||||||
)
|
)
|
||||||
|
|
||||||
videosRouter.delete('/:id',
|
videosRouter.delete('/:id',
|
||||||
|
docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo'),
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videosRemoveValidator),
|
asyncMiddleware(videosRemoveValidator),
|
||||||
asyncRetryTransactionMiddleware(removeVideo)
|
asyncRetryTransactionMiddleware(removeVideo)
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
|
||||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
|
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
|
||||||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
|
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
|
import { docMiddleware } from '@server/middlewares/doc'
|
||||||
|
|
||||||
const lTags = loggerTagsFactory('api', 'video')
|
const lTags = loggerTagsFactory('api', 'video')
|
||||||
const auditLogger = auditLoggerFactory('videos')
|
const auditLogger = auditLoggerFactory('videos')
|
||||||
|
@ -35,6 +36,7 @@ const reqVideoFileUpdate = createReqFiles(
|
||||||
)
|
)
|
||||||
|
|
||||||
updateRouter.put('/:id',
|
updateRouter.put('/:id',
|
||||||
|
docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'),
|
||||||
authenticate,
|
authenticate,
|
||||||
reqVideoFileUpdate,
|
reqVideoFileUpdate,
|
||||||
asyncMiddleware(videosUpdateValidator),
|
asyncMiddleware(videosUpdateValidator),
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
|
||||||
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
|
||||||
import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
|
import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
|
||||||
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||||
|
import { docMiddleware } from '@server/middlewares/doc'
|
||||||
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
|
||||||
import { uploadx } from '@uploadx/core'
|
import { uploadx } from '@uploadx/core'
|
||||||
import { VideoCreate, VideoState } from '../../../../shared'
|
import { VideoCreate, VideoState } from '../../../../shared'
|
||||||
|
@ -60,6 +61,7 @@ const reqVideoFileAddResumable = createReqFiles(
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadRouter.post('/upload',
|
uploadRouter.post('/upload',
|
||||||
|
docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy'),
|
||||||
authenticate,
|
authenticate,
|
||||||
reqVideoFileAdd,
|
reqVideoFileAdd,
|
||||||
asyncMiddleware(videosAddLegacyValidator),
|
asyncMiddleware(videosAddLegacyValidator),
|
||||||
|
@ -67,6 +69,7 @@ uploadRouter.post('/upload',
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadRouter.post('/upload-resumable',
|
uploadRouter.post('/upload-resumable',
|
||||||
|
docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit'),
|
||||||
authenticate,
|
authenticate,
|
||||||
reqVideoFileAddResumable,
|
reqVideoFileAddResumable,
|
||||||
asyncMiddleware(videosAddResumableInitValidator),
|
asyncMiddleware(videosAddResumableInitValidator),
|
||||||
|
@ -79,6 +82,7 @@ uploadRouter.delete('/upload-resumable',
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadRouter.put('/upload-resumable',
|
uploadRouter.put('/upload-resumable',
|
||||||
|
docMiddleware('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable'),
|
||||||
authenticate,
|
authenticate,
|
||||||
uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
|
uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
|
||||||
asyncMiddleware(videosAddResumableValidator),
|
asyncMiddleware(videosAddResumableValidator),
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { isArray } from './custom-validators/misc'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
import { deleteFileAndCatch, generateRandomString } from './utils'
|
import { deleteFileAndCatch, generateRandomString } from './utils'
|
||||||
import { getExtFromMimetype } from './video'
|
import { getExtFromMimetype } from './video'
|
||||||
import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
|
|
||||||
|
|
||||||
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
|
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
|
||||||
if (paramNSFW === 'true') return true
|
if (paramNSFW === 'true') return true
|
||||||
|
@ -126,34 +125,6 @@ function getCountVideos (req: express.Request) {
|
||||||
return req.query.skipCount !== true
|
return req.query.skipCount !== true
|
||||||
}
|
}
|
||||||
|
|
||||||
// helpers added in server.ts and used in subsequent controllers used
|
|
||||||
const apiResponseHelpers = (req, res: express.Response, next = null) => {
|
|
||||||
res.fail = (options) => {
|
|
||||||
const { data, status = HttpStatusCode.BAD_REQUEST_400, message, title, type, docs = res.docs, instance } = options
|
|
||||||
|
|
||||||
const extension = new ProblemDocumentExtension({
|
|
||||||
...data,
|
|
||||||
docs,
|
|
||||||
// fields for <= 3.2 compatibility, deprecated
|
|
||||||
error: message,
|
|
||||||
code: type
|
|
||||||
})
|
|
||||||
|
|
||||||
res.status(status)
|
|
||||||
res.setHeader('Content-Type', 'application/problem+json')
|
|
||||||
res.json(new ProblemDocument({
|
|
||||||
status,
|
|
||||||
title,
|
|
||||||
instance,
|
|
||||||
// fields intended to replace 'error' and 'code' respectively
|
|
||||||
detail: message,
|
|
||||||
type: type && 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/' + type
|
|
||||||
}, extension))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (next) next()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -163,6 +134,5 @@ export {
|
||||||
badRequest,
|
badRequest,
|
||||||
createReqFiles,
|
createReqFiles,
|
||||||
cleanUpReqFiles,
|
cleanUpReqFiles,
|
||||||
getCountVideos,
|
getCountVideos
|
||||||
apiResponseHelpers
|
|
||||||
}
|
}
|
||||||
|
|
13
server/middlewares/doc.ts
Normal file
13
server/middlewares/doc.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
|
||||||
|
function docMiddleware (docUrl: string) {
|
||||||
|
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
res.locals.docUrl = docUrl
|
||||||
|
|
||||||
|
if (next) return next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
docMiddleware
|
||||||
|
}
|
39
server/middlewares/error.ts
Normal file
39
server/middlewares/error.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { ProblemDocument, ProblemDocumentExtension } from 'http-problem-details'
|
||||||
|
import { HttpStatusCode } from '@shared/core-utils'
|
||||||
|
|
||||||
|
function apiFailMiddleware (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
|
res.fail = options => {
|
||||||
|
const { status = HttpStatusCode.BAD_REQUEST_400, message, title, type, data, instance } = options
|
||||||
|
|
||||||
|
const extension = new ProblemDocumentExtension({
|
||||||
|
...data,
|
||||||
|
|
||||||
|
docs: res.locals.docUrl,
|
||||||
|
code: type,
|
||||||
|
|
||||||
|
// For <= 3.2 compatibility
|
||||||
|
error: message
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(status)
|
||||||
|
res.setHeader('Content-Type', 'application/problem+json')
|
||||||
|
res.json(new ProblemDocument({
|
||||||
|
status,
|
||||||
|
title,
|
||||||
|
instance,
|
||||||
|
|
||||||
|
detail: message,
|
||||||
|
|
||||||
|
type: type
|
||||||
|
? `https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/${type}`
|
||||||
|
: undefined
|
||||||
|
}, extension))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next) next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
apiFailMiddleware
|
||||||
|
}
|
|
@ -7,4 +7,6 @@ export * from './servers'
|
||||||
export * from './sort'
|
export * from './sort'
|
||||||
export * from './user-right'
|
export * from './user-right'
|
||||||
export * from './dnt'
|
export * from './dnt'
|
||||||
|
export * from './error'
|
||||||
|
export * from './doc'
|
||||||
export * from './csp'
|
export * from './csp'
|
||||||
|
|
|
@ -73,7 +73,6 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
|
||||||
.custom(isIdValid).withMessage('Should have correct video channel id'),
|
.custom(isIdValid).withMessage('Should have correct video channel id'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy"
|
|
||||||
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
|
logger.debug('Checking videosAdd parameters', { parameters: req.body, files: req.files })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||||
|
@ -108,7 +107,6 @@ const videosAddLegacyValidator = getCommonVideoEditAttributes().concat([
|
||||||
*/
|
*/
|
||||||
const videosAddResumableValidator = [
|
const videosAddResumableValidator = [
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumable"
|
|
||||||
const user = res.locals.oauth.token.User
|
const user = res.locals.oauth.token.User
|
||||||
|
|
||||||
const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
|
const body: express.CustomUploadXFile<express.UploadXFileMetadata> = req.body
|
||||||
|
@ -170,7 +168,6 @@ const videosAddResumableInitValidator = getCommonVideoEditAttributes().concat([
|
||||||
.withMessage('Should specify the file mimetype'),
|
.withMessage('Should specify the file mimetype'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit"
|
|
||||||
const videoFileMetadata = {
|
const videoFileMetadata = {
|
||||||
mimetype: req.headers['x-upload-content-type'] as string,
|
mimetype: req.headers['x-upload-content-type'] as string,
|
||||||
size: +req.headers['x-upload-content-length'],
|
size: +req.headers['x-upload-content-length'],
|
||||||
|
@ -214,7 +211,6 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([
|
||||||
.custom(isIdValid).withMessage('Should have correct video channel id'),
|
.custom(isIdValid).withMessage('Should have correct video channel id'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo'
|
|
||||||
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
|
logger.debug('Checking videosUpdate parameters', { parameters: req.body })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
if (areValidationErrors(req, res)) return cleanUpReqFiles(req)
|
||||||
|
@ -268,7 +264,6 @@ const videosCustomGetValidator = (
|
||||||
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.docs = 'https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo'
|
|
||||||
logger.debug('Checking videosGet parameters', { parameters: req.params })
|
logger.debug('Checking videosGet parameters', { parameters: req.params })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
@ -334,7 +329,6 @@ const videosRemoveValidator = [
|
||||||
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
|
||||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
res.docs = "https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo"
|
|
||||||
logger.debug('Checking videosRemove parameters', { parameters: req.params })
|
logger.debug('Checking videosRemove parameters', { parameters: req.params })
|
||||||
|
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
|
@ -4,6 +4,8 @@ import 'mocha'
|
||||||
import * as chai from 'chai'
|
import * as chai from 'chai'
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { randomInt } from '@shared/core-utils'
|
||||||
|
import { PeerTubeProblemDocument } from '@shared/models'
|
||||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||||
import {
|
import {
|
||||||
checkUploadVideoParam,
|
checkUploadVideoParam,
|
||||||
|
@ -30,7 +32,6 @@ import {
|
||||||
checkBadStartPagination
|
checkBadStartPagination
|
||||||
} from '../../../../shared/extra-utils/requests/check-api-params'
|
} from '../../../../shared/extra-utils/requests/check-api-params'
|
||||||
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
|
import { VideoPrivacy } from '../../../../shared/models/videos/video-privacy.enum'
|
||||||
import { randomInt } from '@shared/core-utils'
|
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
@ -411,6 +412,31 @@ describe('Test videos API validator', function () {
|
||||||
await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
|
await checkUploadVideoParam(server.url, server.accessToken, { ...fields, ...attaches }, HttpStatusCode.BAD_REQUEST_400, mode)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should report the appropriate error', async function () {
|
||||||
|
const fields = immutableAssign(baseCorrectParams, { language: 'a'.repeat(15) })
|
||||||
|
const attaches = baseCorrectAttaches
|
||||||
|
|
||||||
|
const attributes = { ...fields, ...attaches }
|
||||||
|
const res = await checkUploadVideoParam(server.url, server.accessToken, attributes, HttpStatusCode.BAD_REQUEST_400, mode)
|
||||||
|
|
||||||
|
const error = res.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
if (mode === 'legacy') {
|
||||||
|
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadLegacy')
|
||||||
|
} else {
|
||||||
|
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/uploadResumableInit')
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(error.type).to.equal('about:blank')
|
||||||
|
expect(error.title).to.equal('Bad Request')
|
||||||
|
|
||||||
|
expect(error.detail).to.equal('Incorrect request parameters: language')
|
||||||
|
expect(error.error).to.equal('Incorrect request parameters: language')
|
||||||
|
|
||||||
|
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||||
|
expect(error['invalid-params'].language).to.exist
|
||||||
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
this.timeout(10000)
|
this.timeout(10000)
|
||||||
|
|
||||||
|
@ -645,6 +671,24 @@ describe('Test videos API validator', function () {
|
||||||
|
|
||||||
it('Should fail with a video of another server')
|
it('Should fail with a video of another server')
|
||||||
|
|
||||||
|
it('Shoud report the appropriate error', async function () {
|
||||||
|
const fields = immutableAssign(baseCorrectParams, { licence: 125 })
|
||||||
|
|
||||||
|
const res = await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields })
|
||||||
|
const error = res.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/putVideo')
|
||||||
|
|
||||||
|
expect(error.type).to.equal('about:blank')
|
||||||
|
expect(error.title).to.equal('Bad Request')
|
||||||
|
|
||||||
|
expect(error.detail).to.equal('Incorrect request parameters: licence')
|
||||||
|
expect(error.error).to.equal('Incorrect request parameters: licence')
|
||||||
|
|
||||||
|
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||||
|
expect(error['invalid-params'].licence).to.exist
|
||||||
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
const fields = baseCorrectParams
|
const fields = baseCorrectParams
|
||||||
|
|
||||||
|
@ -678,6 +722,22 @@ describe('Test videos API validator', function () {
|
||||||
await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
|
await getVideo(server.url, '4da6fde3-88f7-4d16-b119-108df5630b06', HttpStatusCode.NOT_FOUND_404)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Shoud report the appropriate error', async function () {
|
||||||
|
const res = await getVideo(server.url, 'hi', HttpStatusCode.BAD_REQUEST_400)
|
||||||
|
const error = res.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/getVideo')
|
||||||
|
|
||||||
|
expect(error.type).to.equal('about:blank')
|
||||||
|
expect(error.title).to.equal('Bad Request')
|
||||||
|
|
||||||
|
expect(error.detail).to.equal('Incorrect request parameters: id')
|
||||||
|
expect(error.error).to.equal('Incorrect request parameters: id')
|
||||||
|
|
||||||
|
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||||
|
expect(error['invalid-params'].id).to.exist
|
||||||
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
await getVideo(server.url, videoId)
|
await getVideo(server.url, videoId)
|
||||||
})
|
})
|
||||||
|
@ -755,6 +815,22 @@ describe('Test videos API validator', function () {
|
||||||
|
|
||||||
it('Should fail with a video of another server')
|
it('Should fail with a video of another server')
|
||||||
|
|
||||||
|
it('Shoud report the appropriate error', async function () {
|
||||||
|
const res = await removeVideo(server.url, server.accessToken, 'hello', HttpStatusCode.BAD_REQUEST_400)
|
||||||
|
const error = res.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
expect(error.docs).to.equal('https://docs.joinpeertube.org/api-rest-reference.html#operation/delVideo')
|
||||||
|
|
||||||
|
expect(error.type).to.equal('about:blank')
|
||||||
|
expect(error.title).to.equal('Bad Request')
|
||||||
|
|
||||||
|
expect(error.detail).to.equal('Incorrect request parameters: id')
|
||||||
|
expect(error.error).to.equal('Incorrect request parameters: id')
|
||||||
|
|
||||||
|
expect(error.status).to.equal(HttpStatusCode.BAD_REQUEST_400)
|
||||||
|
expect(error['invalid-params'].id).to.exist
|
||||||
|
})
|
||||||
|
|
||||||
it('Should succeed with the correct parameters', async function () {
|
it('Should succeed with the correct parameters', async function () {
|
||||||
await removeVideo(server.url, server.accessToken, videoId)
|
await removeVideo(server.url, server.accessToken, videoId)
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,6 +18,7 @@ import { unfollow } from '../../../../shared/extra-utils/server/follows'
|
||||||
import { userLogin } from '../../../../shared/extra-utils/users/login'
|
import { userLogin } from '../../../../shared/extra-utils/users/login'
|
||||||
import { createUser } from '../../../../shared/extra-utils/users/users'
|
import { createUser } from '../../../../shared/extra-utils/users/users'
|
||||||
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
|
||||||
|
import { PeerTubeProblemDocument, ServerErrorCode } from '@shared/models'
|
||||||
|
|
||||||
const expect = chai.expect
|
const expect = chai.expect
|
||||||
|
|
||||||
|
@ -153,7 +154,20 @@ describe('Test follow constraints', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should not get the remote video', async function () {
|
it('Should not get the remote video', async function () {
|
||||||
await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403)
|
const res = await getVideo(servers[0].url, video2UUID, HttpStatusCode.FORBIDDEN_403)
|
||||||
|
|
||||||
|
const error = res.body as PeerTubeProblemDocument
|
||||||
|
|
||||||
|
const doc = 'https://docs.joinpeertube.org/api-rest-reference.html#section/Errors/does_not_respect_follow_constraints'
|
||||||
|
expect(error.type).to.equal(doc)
|
||||||
|
expect(error.code).to.equal(ServerErrorCode.DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS)
|
||||||
|
|
||||||
|
expect(error.detail).to.equal('Cannot get this video regarding follow constraints')
|
||||||
|
expect(error.error).to.equal(error.detail)
|
||||||
|
|
||||||
|
expect(error.status).to.equal(HttpStatusCode.FORBIDDEN_403)
|
||||||
|
|
||||||
|
expect(error.originUrl).to.contains(servers[1].url)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should list local account videos', async function () {
|
it('Should list local account videos', async function () {
|
||||||
|
|
19
server/typings/express/index.d.ts
vendored
19
server/typings/express/index.d.ts
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
|
||||||
import { RegisterServerAuthExternalOptions } from '@server/types'
|
import { RegisterServerAuthExternalOptions } from '@server/types'
|
||||||
import {
|
import {
|
||||||
MAbuseMessage,
|
MAbuseMessage,
|
||||||
|
@ -20,9 +21,8 @@ import { MVideoImportDefault } from '@server/types/models/video/video-import'
|
||||||
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
|
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/types/models/video/video-playlist-element'
|
||||||
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
|
import { MAccountVideoRateAccountVideo } from '@server/types/models/video/video-rate'
|
||||||
import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
|
import { HttpMethod } from '@shared/core-utils/miscs/http-methods'
|
||||||
import { VideoCreate } from '@shared/models'
|
import { PeerTubeProblemDocumentData, ServerErrorCode, VideoCreate } from '@shared/models'
|
||||||
import { File as UploadXFile, Metadata } from '@uploadx/core'
|
import { File as UploadXFile, Metadata } from '@uploadx/core'
|
||||||
import { ProblemDocumentOptions } from 'http-problem-details/dist/ProblemDocument'
|
|
||||||
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
|
import { RegisteredPlugin } from '../../lib/plugins/plugin-manager'
|
||||||
import {
|
import {
|
||||||
MAccountDefault,
|
MAccountDefault,
|
||||||
|
@ -41,6 +41,7 @@ import {
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
MVideoWithRights
|
MVideoWithRights
|
||||||
} from '../../types/models'
|
} from '../../types/models'
|
||||||
|
|
||||||
declare module 'express' {
|
declare module 'express' {
|
||||||
export interface Request {
|
export interface Request {
|
||||||
query: any
|
query: any
|
||||||
|
@ -86,14 +87,20 @@ declare module 'express' {
|
||||||
|
|
||||||
// Extends Response with added functions and potential variables passed by middlewares
|
// Extends Response with added functions and potential variables passed by middlewares
|
||||||
interface Response {
|
interface Response {
|
||||||
docs?: string
|
|
||||||
fail: (options: {
|
fail: (options: {
|
||||||
data?: Record<string, Object>
|
|
||||||
docs?: string
|
|
||||||
message: string
|
message: string
|
||||||
} & ProblemDocumentOptions) => void
|
|
||||||
|
title?: string
|
||||||
|
status?: number
|
||||||
|
type?: ServerErrorCode
|
||||||
|
instance?: string
|
||||||
|
|
||||||
|
data?: PeerTubeProblemDocumentData
|
||||||
|
}) => void
|
||||||
|
|
||||||
locals: {
|
locals: {
|
||||||
|
docUrl?: string
|
||||||
|
|
||||||
videoAll?: MVideoFullLight
|
videoAll?: MVideoFullLight
|
||||||
onlyImmutableVideo?: MVideoImmutable
|
onlyImmutableVideo?: MVideoImmutable
|
||||||
onlyVideo?: MVideoThumbnail
|
onlyVideo?: MVideoThumbnail
|
||||||
|
|
|
@ -6,6 +6,7 @@ export * from './debug.model'
|
||||||
export * from './emailer.model'
|
export * from './emailer.model'
|
||||||
export * from './job.model'
|
export * from './job.model'
|
||||||
export * from './log-level.type'
|
export * from './log-level.type'
|
||||||
|
export * from './peertube-problem-document.model'
|
||||||
export * from './server-config.model'
|
export * from './server-config.model'
|
||||||
export * from './server-debug.model'
|
export * from './server-debug.model'
|
||||||
export * from './server-error-code.enum'
|
export * from './server-error-code.enum'
|
||||||
|
|
32
shared/models/server/peertube-problem-document.model.ts
Normal file
32
shared/models/server/peertube-problem-document.model.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { HttpStatusCode } from '@shared/core-utils'
|
||||||
|
import { OAuth2ErrorCode, ServerErrorCode } from './server-error-code.enum'
|
||||||
|
|
||||||
|
export interface PeerTubeProblemDocumentData {
|
||||||
|
'invalid-params'?: Record<string, Object>
|
||||||
|
|
||||||
|
originUrl?: string
|
||||||
|
|
||||||
|
keyId?: string
|
||||||
|
|
||||||
|
targetUrl?: string
|
||||||
|
|
||||||
|
actorUrl?: string
|
||||||
|
|
||||||
|
// Feeds
|
||||||
|
format?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PeerTubeProblemDocument extends PeerTubeProblemDocumentData {
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
|
||||||
|
detail: string
|
||||||
|
// Compat PeerTube <= 3.2
|
||||||
|
error: string
|
||||||
|
|
||||||
|
status: HttpStatusCode
|
||||||
|
|
||||||
|
docs?: string
|
||||||
|
code?: ServerErrorCode | OAuth2ErrorCode
|
||||||
|
}
|
|
@ -48,5 +48,13 @@ export const enum OAuth2ErrorCode {
|
||||||
*
|
*
|
||||||
* @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-client-error.js
|
* @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-client-error.js
|
||||||
*/
|
*/
|
||||||
INVALID_CLIENT = 'invalid_client'
|
INVALID_CLIENT = 'invalid_client',
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The access token provided is expired, revoked, malformed, or invalid for other reasons
|
||||||
|
*
|
||||||
|
* @see https://github.com/oauthjs/node-oauth2-server/blob/master/lib/errors/invalid-token-error.js
|
||||||
|
*/
|
||||||
|
INVALID_TOKEN = 'invalid_token',
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue