diff options
Diffstat (limited to 'server/middlewares')
25 files changed, 512 insertions, 113 deletions
diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index 0064a4760..261b9f690 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts | |||
@@ -125,7 +125,7 @@ async function checkJsonLDSignature (req: Request, res: Response) { | |||
125 | return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => { | 125 | return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => { |
126 | const signatureObject: ActivityPubSignature = req.body.signature | 126 | const signatureObject: ActivityPubSignature = req.body.signature |
127 | 127 | ||
128 | if (!signatureObject || !signatureObject.creator) { | 128 | if (!signatureObject?.creator) { |
129 | res.fail({ | 129 | res.fail({ |
130 | status: HttpStatusCode.FORBIDDEN_403, | 130 | status: HttpStatusCode.FORBIDDEN_403, |
131 | message: 'Object and creator signature do not match' | 131 | message: 'Object and creator signature do not match' |
diff --git a/server/middlewares/auth.ts b/server/middlewares/auth.ts index 904d47efd..e6025c8ce 100644 --- a/server/middlewares/auth.ts +++ b/server/middlewares/auth.ts | |||
@@ -5,8 +5,8 @@ import { HttpStatusCode } from '../../shared/models/http/http-error-codes' | |||
5 | import { logger } from '../helpers/logger' | 5 | import { logger } from '../helpers/logger' |
6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' | 6 | import { handleOAuthAuthenticate } from '../lib/auth/oauth' |
7 | 7 | ||
8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction, authenticateInQuery = false) { | 8 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
9 | handleOAuthAuthenticate(req, res, authenticateInQuery) | 9 | handleOAuthAuthenticate(req, res) |
10 | .then((token: any) => { | 10 | .then((token: any) => { |
11 | res.locals.oauth = { token } | 11 | res.locals.oauth = { token } |
12 | res.locals.authenticated = true | 12 | res.locals.authenticated = true |
@@ -47,7 +47,7 @@ function authenticateSocket (socket: Socket, next: (err?: any) => void) { | |||
47 | .catch(err => logger.error('Cannot get access token.', { err })) | 47 | .catch(err => logger.error('Cannot get access token.', { err })) |
48 | } | 48 | } |
49 | 49 | ||
50 | function authenticatePromise (req: express.Request, res: express.Response, authenticateInQuery = false) { | 50 | function authenticatePromise (req: express.Request, res: express.Response) { |
51 | return new Promise<void>(resolve => { | 51 | return new Promise<void>(resolve => { |
52 | // Already authenticated? (or tried to) | 52 | // Already authenticated? (or tried to) |
53 | if (res.locals.oauth?.token.User) return resolve() | 53 | if (res.locals.oauth?.token.User) return resolve() |
@@ -59,7 +59,7 @@ function authenticatePromise (req: express.Request, res: express.Response, authe | |||
59 | }) | 59 | }) |
60 | } | 60 | } |
61 | 61 | ||
62 | authenticate(req, res, () => resolve(), authenticateInQuery) | 62 | authenticate(req, res, () => resolve()) |
63 | }) | 63 | }) |
64 | } | 64 | } |
65 | 65 | ||
diff --git a/server/middlewares/cache/shared/api-cache.ts b/server/middlewares/cache/shared/api-cache.ts index abc919339..9e15bf2d6 100644 --- a/server/middlewares/cache/shared/api-cache.ts +++ b/server/middlewares/cache/shared/api-cache.ts | |||
@@ -49,7 +49,7 @@ export class ApiCache { | |||
49 | if (!Redis.Instance.isConnected()) return this.makeResponseCacheable(res, next, key, duration) | 49 | if (!Redis.Instance.isConnected()) return this.makeResponseCacheable(res, next, key, duration) |
50 | 50 | ||
51 | try { | 51 | try { |
52 | const obj = await redis.hGetAll(key) | 52 | const obj = await redis.hgetall(key) |
53 | if (obj?.response) { | 53 | if (obj?.response) { |
54 | return this.sendCachedResponse(req, res, JSON.parse(obj.response), duration) | 54 | return this.sendCachedResponse(req, res, JSON.parse(obj.response), duration) |
55 | } | 55 | } |
@@ -100,8 +100,8 @@ export class ApiCache { | |||
100 | 100 | ||
101 | if (Redis.Instance.isConnected()) { | 101 | if (Redis.Instance.isConnected()) { |
102 | await Promise.all([ | 102 | await Promise.all([ |
103 | redis.hSet(key, 'response', JSON.stringify(value)), | 103 | redis.hset(key, 'response', JSON.stringify(value)), |
104 | redis.hSet(key, 'duration', duration + ''), | 104 | redis.hset(key, 'duration', duration + ''), |
105 | redis.expire(key, duration / 1000) | 105 | redis.expire(key, duration / 1000) |
106 | ]) | 106 | ]) |
107 | } | 107 | } |
diff --git a/server/middlewares/pagination.ts b/server/middlewares/pagination.ts index 9812af9e4..17e43f743 100644 --- a/server/middlewares/pagination.ts +++ b/server/middlewares/pagination.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { forceNumber } from '@shared/core-utils' | ||
2 | import { PAGINATION } from '../initializers/constants' | 3 | import { PAGINATION } from '../initializers/constants' |
3 | 4 | ||
4 | function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) { | 5 | function setDefaultPagination (req: express.Request, res: express.Response, next: express.NextFunction) { |
5 | if (!req.query.start) req.query.start = 0 | 6 | if (!req.query.start) req.query.start = 0 |
6 | else req.query.start = parseInt(req.query.start, 10) | 7 | else req.query.start = forceNumber(req.query.start) |
7 | 8 | ||
8 | if (!req.query.count) req.query.count = PAGINATION.GLOBAL.COUNT.DEFAULT | 9 | if (!req.query.count) req.query.count = PAGINATION.GLOBAL.COUNT.DEFAULT |
9 | else req.query.count = parseInt(req.query.count, 10) | 10 | else req.query.count = forceNumber(req.query.count) |
10 | 11 | ||
11 | return next() | 12 | return next() |
12 | } | 13 | } |
diff --git a/server/middlewares/validators/abuse.ts b/server/middlewares/validators/abuse.ts index 9b94008ce..70bae1775 100644 --- a/server/middlewares/validators/abuse.ts +++ b/server/middlewares/validators/abuse.ts | |||
@@ -18,6 +18,7 @@ import { AbuseMessageModel } from '@server/models/abuse/abuse-message' | |||
18 | import { AbuseCreate, UserRight } from '@shared/models' | 18 | import { AbuseCreate, UserRight } from '@shared/models' |
19 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 19 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' |
20 | import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared' | 20 | import { areValidationErrors, doesAbuseExist, doesAccountIdExist, doesCommentIdExist, doesVideoExist } from './shared' |
21 | import { forceNumber } from '@shared/core-utils' | ||
21 | 22 | ||
22 | const abuseReportValidator = [ | 23 | const abuseReportValidator = [ |
23 | body('account.id') | 24 | body('account.id') |
@@ -216,7 +217,7 @@ const deleteAbuseMessageValidator = [ | |||
216 | const user = res.locals.oauth.token.user | 217 | const user = res.locals.oauth.token.user |
217 | const abuse = res.locals.abuse | 218 | const abuse = res.locals.abuse |
218 | 219 | ||
219 | const messageId = parseInt(req.params.messageId + '', 10) | 220 | const messageId = forceNumber(req.params.messageId) |
220 | const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) | 221 | const abuseMessage = await AbuseMessageModel.loadByIdAndAbuseId(messageId, abuse.id) |
221 | 222 | ||
222 | if (!abuseMessage) { | 223 | if (!abuseMessage) { |
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index ffadb3b49..9bc8887ff 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts | |||
@@ -1,7 +1,6 @@ | |||
1 | export * from './activitypub' | ||
2 | export * from './videos' | ||
3 | export * from './abuse' | 1 | export * from './abuse' |
4 | export * from './account' | 2 | export * from './account' |
3 | export * from './activitypub' | ||
5 | export * from './actor-image' | 4 | export * from './actor-image' |
6 | export * from './blocklist' | 5 | export * from './blocklist' |
7 | export * from './bulk' | 6 | export * from './bulk' |
@@ -10,8 +9,9 @@ export * from './express' | |||
10 | export * from './feeds' | 9 | export * from './feeds' |
11 | export * from './follows' | 10 | export * from './follows' |
12 | export * from './jobs' | 11 | export * from './jobs' |
13 | export * from './metrics' | ||
14 | export * from './logs' | 12 | export * from './logs' |
13 | export * from './metrics' | ||
14 | export * from './object-storage-proxy' | ||
15 | export * from './oembed' | 15 | export * from './oembed' |
16 | export * from './pagination' | 16 | export * from './pagination' |
17 | export * from './plugins' | 17 | export * from './plugins' |
@@ -19,9 +19,11 @@ export * from './redundancy' | |||
19 | export * from './search' | 19 | export * from './search' |
20 | export * from './server' | 20 | export * from './server' |
21 | export * from './sort' | 21 | export * from './sort' |
22 | export * from './static' | ||
22 | export * from './themes' | 23 | export * from './themes' |
23 | export * from './user-history' | 24 | export * from './user-history' |
24 | export * from './user-notifications' | 25 | export * from './user-notifications' |
25 | export * from './user-subscriptions' | 26 | export * from './user-subscriptions' |
26 | export * from './users' | 27 | export * from './users' |
28 | export * from './videos' | ||
27 | export * from './webfinger' | 29 | export * from './webfinger' |
diff --git a/server/middlewares/validators/object-storage-proxy.ts b/server/middlewares/validators/object-storage-proxy.ts new file mode 100644 index 000000000..bbd77f262 --- /dev/null +++ b/server/middlewares/validators/object-storage-proxy.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | import express from 'express' | ||
2 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { HttpStatusCode } from '@shared/models' | ||
4 | |||
5 | const ensurePrivateObjectStorageProxyIsEnabled = [ | ||
6 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
7 | if (CONFIG.OBJECT_STORAGE.PROXY.PROXIFY_PRIVATE_FILES !== true) { | ||
8 | return res.fail({ | ||
9 | message: 'Private object storage proxy is not enabled', | ||
10 | status: HttpStatusCode.BAD_REQUEST_400 | ||
11 | }) | ||
12 | } | ||
13 | |||
14 | return next() | ||
15 | } | ||
16 | ] | ||
17 | |||
18 | export { | ||
19 | ensurePrivateObjectStorageProxyIsEnabled | ||
20 | } | ||
diff --git a/server/middlewares/validators/plugins.ts b/server/middlewares/validators/plugins.ts index 78c030333..64bef2648 100644 --- a/server/middlewares/validators/plugins.ts +++ b/server/middlewares/validators/plugins.ts | |||
@@ -4,7 +4,12 @@ import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | |||
4 | import { PluginType } from '../../../shared/models/plugins/plugin.type' | 4 | import { PluginType } from '../../../shared/models/plugins/plugin.type' |
5 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model' | 5 | import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model' |
6 | import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 6 | import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
7 | import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' | 7 | import { |
8 | isNpmPluginNameValid, | ||
9 | isPluginNameValid, | ||
10 | isPluginStableOrUnstableVersionValid, | ||
11 | isPluginTypeValid | ||
12 | } from '../../helpers/custom-validators/plugins' | ||
8 | import { CONFIG } from '../../initializers/config' | 13 | import { CONFIG } from '../../initializers/config' |
9 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 14 | import { PluginManager } from '../../lib/plugins/plugin-manager' |
10 | import { PluginModel } from '../../models/server/plugin' | 15 | import { PluginModel } from '../../models/server/plugin' |
@@ -19,7 +24,7 @@ const getPluginValidator = (pluginType: PluginType, withVersion = true) => { | |||
19 | if (withVersion) { | 24 | if (withVersion) { |
20 | validators.push( | 25 | validators.push( |
21 | param('pluginVersion') | 26 | param('pluginVersion') |
22 | .custom(isPluginVersionValid) | 27 | .custom(isPluginStableOrUnstableVersionValid) |
23 | ) | 28 | ) |
24 | } | 29 | } |
25 | 30 | ||
@@ -113,7 +118,7 @@ const installOrUpdatePluginValidator = [ | |||
113 | .custom(isNpmPluginNameValid), | 118 | .custom(isNpmPluginNameValid), |
114 | body('pluginVersion') | 119 | body('pluginVersion') |
115 | .optional() | 120 | .optional() |
116 | .custom(isPluginVersionValid), | 121 | .custom(isPluginStableOrUnstableVersionValid), |
117 | body('path') | 122 | body('path') |
118 | .optional() | 123 | .optional() |
119 | .custom(isSafePath), | 124 | .custom(isSafePath), |
@@ -185,7 +190,7 @@ const listAvailablePluginsValidator = [ | |||
185 | .custom(isPluginTypeValid), | 190 | .custom(isPluginTypeValid), |
186 | query('currentPeerTubeEngine') | 191 | query('currentPeerTubeEngine') |
187 | .optional() | 192 | .optional() |
188 | .custom(isPluginVersionValid), | 193 | .custom(isPluginStableOrUnstableVersionValid), |
189 | 194 | ||
190 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 195 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
191 | if (areValidationErrors(req, res)) return | 196 | if (areValidationErrors(req, res)) return |
diff --git a/server/middlewares/validators/redundancy.ts b/server/middlewares/validators/redundancy.ts index 79460f63c..c80f9b728 100644 --- a/server/middlewares/validators/redundancy.ts +++ b/server/middlewares/validators/redundancy.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' | 3 | import { isVideoRedundancyTarget } from '@server/helpers/custom-validators/video-redundancies' |
4 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 5 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' |
5 | import { | 6 | import { |
6 | exists, | 7 | exists, |
@@ -171,7 +172,7 @@ const removeVideoRedundancyValidator = [ | |||
171 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 172 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
172 | if (areValidationErrors(req, res)) return | 173 | if (areValidationErrors(req, res)) return |
173 | 174 | ||
174 | const redundancy = await VideoRedundancyModel.loadByIdWithVideo(parseInt(req.params.redundancyId, 10)) | 175 | const redundancy = await VideoRedundancyModel.loadByIdWithVideo(forceNumber(req.params.redundancyId)) |
175 | if (!redundancy) { | 176 | if (!redundancy) { |
176 | return res.fail({ | 177 | return res.fail({ |
177 | status: HttpStatusCode.NOT_FOUND_404, | 178 | status: HttpStatusCode.NOT_FOUND_404, |
diff --git a/server/middlewares/validators/shared/abuses.ts b/server/middlewares/validators/shared/abuses.ts index 2b8d86ba5..2c988f9ec 100644 --- a/server/middlewares/validators/shared/abuses.ts +++ b/server/middlewares/validators/shared/abuses.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { Response } from 'express' | 1 | import { Response } from 'express' |
2 | import { AbuseModel } from '@server/models/abuse/abuse' | 2 | import { AbuseModel } from '@server/models/abuse/abuse' |
3 | import { HttpStatusCode } from '@shared/models' | 3 | import { HttpStatusCode } from '@shared/models' |
4 | import { forceNumber } from '@shared/core-utils' | ||
4 | 5 | ||
5 | async function doesAbuseExist (abuseId: number | string, res: Response) { | 6 | async function doesAbuseExist (abuseId: number | string, res: Response) { |
6 | const abuse = await AbuseModel.loadByIdWithReporter(parseInt(abuseId + '', 10)) | 7 | const abuse = await AbuseModel.loadByIdWithReporter(forceNumber(abuseId)) |
7 | 8 | ||
8 | if (!abuse) { | 9 | if (!abuse) { |
9 | res.fail({ | 10 | res.fail({ |
diff --git a/server/middlewares/validators/shared/accounts.ts b/server/middlewares/validators/shared/accounts.ts index fe4f83aa0..72b0e235e 100644 --- a/server/middlewares/validators/shared/accounts.ts +++ b/server/middlewares/validators/shared/accounts.ts | |||
@@ -2,10 +2,11 @@ import { Response } from 'express' | |||
2 | import { AccountModel } from '@server/models/account/account' | 2 | import { AccountModel } from '@server/models/account/account' |
3 | import { UserModel } from '@server/models/user/user' | 3 | import { UserModel } from '@server/models/user/user' |
4 | import { MAccountDefault } from '@server/types/models' | 4 | import { MAccountDefault } from '@server/types/models' |
5 | import { forceNumber } from '@shared/core-utils' | ||
5 | import { HttpStatusCode } from '@shared/models' | 6 | import { HttpStatusCode } from '@shared/models' |
6 | 7 | ||
7 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { | 8 | function doesAccountIdExist (id: number | string, res: Response, sendNotFound = true) { |
8 | const promise = AccountModel.load(parseInt(id + '', 10)) | 9 | const promise = AccountModel.load(forceNumber(id)) |
9 | 10 | ||
10 | return doesAccountExist(promise, res, sendNotFound) | 11 | return doesAccountExist(promise, res, sendNotFound) |
11 | } | 12 | } |
@@ -40,7 +41,7 @@ async function doesAccountExist (p: Promise<MAccountDefault>, res: Response, sen | |||
40 | } | 41 | } |
41 | 42 | ||
42 | async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) { | 43 | async function doesUserFeedTokenCorrespond (id: number, token: string, res: Response) { |
43 | const user = await UserModel.loadByIdWithChannels(parseInt(id + '', 10)) | 44 | const user = await UserModel.loadByIdWithChannels(forceNumber(id)) |
44 | 45 | ||
45 | if (token !== user.feedToken) { | 46 | if (token !== user.feedToken) { |
46 | res.fail({ | 47 | res.fail({ |
diff --git a/server/middlewares/validators/shared/index.ts b/server/middlewares/validators/shared/index.ts index bbd03b248..de98cd442 100644 --- a/server/middlewares/validators/shared/index.ts +++ b/server/middlewares/validators/shared/index.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export * from './abuses' | 1 | export * from './abuses' |
2 | export * from './accounts' | 2 | export * from './accounts' |
3 | export * from './users' | ||
3 | export * from './utils' | 4 | export * from './utils' |
4 | export * from './video-blacklists' | 5 | export * from './video-blacklists' |
5 | export * from './video-captions' | 6 | export * from './video-captions' |
diff --git a/server/middlewares/validators/shared/users.ts b/server/middlewares/validators/shared/users.ts new file mode 100644 index 000000000..b8f1436d3 --- /dev/null +++ b/server/middlewares/validators/shared/users.ts | |||
@@ -0,0 +1,63 @@ | |||
1 | import express from 'express' | ||
2 | import { ActorModel } from '@server/models/actor/actor' | ||
3 | import { UserModel } from '@server/models/user/user' | ||
4 | import { MUserDefault } from '@server/types/models' | ||
5 | import { forceNumber } from '@shared/core-utils' | ||
6 | import { HttpStatusCode } from '@shared/models' | ||
7 | |||
8 | function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { | ||
9 | const id = forceNumber(idArg) | ||
10 | return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) | ||
11 | } | ||
12 | |||
13 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
14 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) | ||
15 | } | ||
16 | |||
17 | async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { | ||
18 | const user = await UserModel.loadByUsernameOrEmail(username, email) | ||
19 | |||
20 | if (user) { | ||
21 | res.fail({ | ||
22 | status: HttpStatusCode.CONFLICT_409, | ||
23 | message: 'User with this username or email already exists.' | ||
24 | }) | ||
25 | return false | ||
26 | } | ||
27 | |||
28 | const actor = await ActorModel.loadLocalByName(username) | ||
29 | if (actor) { | ||
30 | res.fail({ | ||
31 | status: HttpStatusCode.CONFLICT_409, | ||
32 | message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' | ||
33 | }) | ||
34 | return false | ||
35 | } | ||
36 | |||
37 | return true | ||
38 | } | ||
39 | |||
40 | async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) { | ||
41 | const user = await finder() | ||
42 | |||
43 | if (!user) { | ||
44 | if (abortResponse === true) { | ||
45 | res.fail({ | ||
46 | status: HttpStatusCode.NOT_FOUND_404, | ||
47 | message: 'User not found' | ||
48 | }) | ||
49 | } | ||
50 | |||
51 | return false | ||
52 | } | ||
53 | |||
54 | res.locals.user = user | ||
55 | return true | ||
56 | } | ||
57 | |||
58 | export { | ||
59 | checkUserIdExist, | ||
60 | checkUserEmailExist, | ||
61 | checkUserNameOrEmailDoesNotAlreadyExist, | ||
62 | checkUserExist | ||
63 | } | ||
diff --git a/server/middlewares/validators/shared/video-comments.ts b/server/middlewares/validators/shared/video-comments.ts index 8d1a16294..0961b3ec9 100644 --- a/server/middlewares/validators/shared/video-comments.ts +++ b/server/middlewares/validators/shared/video-comments.ts | |||
@@ -1,10 +1,11 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { VideoCommentModel } from '@server/models/video/video-comment' | 2 | import { VideoCommentModel } from '@server/models/video/video-comment' |
3 | import { MVideoId } from '@server/types/models' | 3 | import { MVideoId } from '@server/types/models' |
4 | import { forceNumber } from '@shared/core-utils' | ||
4 | import { HttpStatusCode, ServerErrorCode } from '@shared/models' | 5 | import { HttpStatusCode, ServerErrorCode } from '@shared/models' |
5 | 6 | ||
6 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { | 7 | async function doesVideoCommentThreadExist (idArg: number | string, video: MVideoId, res: express.Response) { |
7 | const id = parseInt(idArg + '', 10) | 8 | const id = forceNumber(idArg) |
8 | const videoComment = await VideoCommentModel.loadById(id) | 9 | const videoComment = await VideoCommentModel.loadById(id) |
9 | 10 | ||
10 | if (!videoComment) { | 11 | if (!videoComment) { |
@@ -33,7 +34,7 @@ async function doesVideoCommentThreadExist (idArg: number | string, video: MVide | |||
33 | } | 34 | } |
34 | 35 | ||
35 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { | 36 | async function doesVideoCommentExist (idArg: number | string, video: MVideoId, res: express.Response) { |
36 | const id = parseInt(idArg + '', 10) | 37 | const id = forceNumber(idArg) |
37 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | 38 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) |
38 | 39 | ||
39 | if (!videoComment) { | 40 | if (!videoComment) { |
@@ -57,7 +58,7 @@ async function doesVideoCommentExist (idArg: number | string, video: MVideoId, r | |||
57 | } | 58 | } |
58 | 59 | ||
59 | async function doesCommentIdExist (idArg: number | string, res: express.Response) { | 60 | async function doesCommentIdExist (idArg: number | string, res: express.Response) { |
60 | const id = parseInt(idArg + '', 10) | 61 | const id = forceNumber(idArg) |
61 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) | 62 | const videoComment = await VideoCommentModel.loadByIdAndPopulateVideoAndAccountAndReply(id) |
62 | 63 | ||
63 | if (!videoComment) { | 64 | if (!videoComment) { |
diff --git a/server/middlewares/validators/shared/video-ownerships.ts b/server/middlewares/validators/shared/video-ownerships.ts index 680613cda..33ac9c8b6 100644 --- a/server/middlewares/validators/shared/video-ownerships.ts +++ b/server/middlewares/validators/shared/video-ownerships.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership' | 2 | import { VideoChangeOwnershipModel } from '@server/models/video/video-change-ownership' |
3 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { HttpStatusCode } from '@shared/models' | 4 | import { HttpStatusCode } from '@shared/models' |
4 | 5 | ||
5 | async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) { | 6 | async function doesChangeVideoOwnershipExist (idArg: number | string, res: express.Response) { |
6 | const id = parseInt(idArg + '', 10) | 7 | const id = forceNumber(idArg) |
7 | const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) | 8 | const videoChangeOwnership = await VideoChangeOwnershipModel.load(id) |
8 | 9 | ||
9 | if (!videoChangeOwnership) { | 10 | if (!videoChangeOwnership) { |
diff --git a/server/middlewares/validators/shared/videos.ts b/server/middlewares/validators/shared/videos.ts index e3a98c58f..ebbfc0a0a 100644 --- a/server/middlewares/validators/shared/videos.ts +++ b/server/middlewares/validators/shared/videos.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { Request, Response } from 'express' | 1 | import { Request, Response } from 'express' |
2 | import { isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
3 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' | 2 | import { loadVideo, VideoLoadType } from '@server/lib/model-loaders' |
4 | import { isAbleToUploadVideo } from '@server/lib/user' | 3 | import { isAbleToUploadVideo } from '@server/lib/user' |
4 | import { VideoTokensManager } from '@server/lib/video-tokens-manager' | ||
5 | import { authenticatePromise } from '@server/middlewares/auth' | 5 | import { authenticatePromise } from '@server/middlewares/auth' |
6 | import { VideoModel } from '@server/models/video/video' | 6 | import { VideoModel } from '@server/models/video/video' |
7 | import { VideoChannelModel } from '@server/models/video/video-channel' | 7 | import { VideoChannelModel } from '@server/models/video/video-channel' |
@@ -108,26 +108,21 @@ async function checkCanSeeVideo (options: { | |||
108 | res: Response | 108 | res: Response |
109 | paramId: string | 109 | paramId: string |
110 | video: MVideo | 110 | video: MVideo |
111 | authenticateInQuery?: boolean // default false | ||
112 | }) { | 111 | }) { |
113 | const { req, res, video, paramId, authenticateInQuery = false } = options | 112 | const { req, res, video, paramId } = options |
114 | 113 | ||
115 | if (video.requiresAuth()) { | 114 | if (video.requiresAuth({ urlParamId: paramId, checkBlacklist: true })) { |
116 | return checkCanSeeAuthVideo(req, res, video, authenticateInQuery) | 115 | return checkCanSeeAuthVideo(req, res, video) |
117 | } | 116 | } |
118 | 117 | ||
119 | if (video.privacy === VideoPrivacy.UNLISTED) { | 118 | if (video.privacy === VideoPrivacy.UNLISTED || video.privacy === VideoPrivacy.PUBLIC) { |
120 | if (isUUIDValid(paramId)) return true | 119 | return true |
121 | |||
122 | return checkCanSeeAuthVideo(req, res, video, authenticateInQuery) | ||
123 | } | 120 | } |
124 | 121 | ||
125 | if (video.privacy === VideoPrivacy.PUBLIC) return true | 122 | throw new Error('Unknown video privacy when checking video right ' + video.url) |
126 | |||
127 | throw new Error('Fatal error when checking video right ' + video.url) | ||
128 | } | 123 | } |
129 | 124 | ||
130 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights, authenticateInQuery = false) { | 125 | async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoId | MVideoWithRights) { |
131 | const fail = () => { | 126 | const fail = () => { |
132 | res.fail({ | 127 | res.fail({ |
133 | status: HttpStatusCode.FORBIDDEN_403, | 128 | status: HttpStatusCode.FORBIDDEN_403, |
@@ -137,7 +132,7 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
137 | return false | 132 | return false |
138 | } | 133 | } |
139 | 134 | ||
140 | await authenticatePromise(req, res, authenticateInQuery) | 135 | await authenticatePromise(req, res) |
141 | 136 | ||
142 | const user = res.locals.oauth?.token.User | 137 | const user = res.locals.oauth?.token.User |
143 | if (!user) return fail() | 138 | if (!user) return fail() |
@@ -173,6 +168,36 @@ async function checkCanSeeAuthVideo (req: Request, res: Response, video: MVideoI | |||
173 | 168 | ||
174 | // --------------------------------------------------------------------------- | 169 | // --------------------------------------------------------------------------- |
175 | 170 | ||
171 | async function checkCanAccessVideoStaticFiles (options: { | ||
172 | video: MVideo | ||
173 | req: Request | ||
174 | res: Response | ||
175 | paramId: string | ||
176 | }) { | ||
177 | const { video, req, res } = options | ||
178 | |||
179 | if (res.locals.oauth?.token.User) { | ||
180 | return checkCanSeeVideo(options) | ||
181 | } | ||
182 | |||
183 | if (!video.hasPrivateStaticPath()) return true | ||
184 | |||
185 | const videoFileToken = req.query.videoFileToken | ||
186 | if (!videoFileToken) { | ||
187 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
188 | return false | ||
189 | } | ||
190 | |||
191 | if (VideoTokensManager.Instance.hasToken({ token: videoFileToken, videoUUID: video.uuid })) { | ||
192 | return true | ||
193 | } | ||
194 | |||
195 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
196 | return false | ||
197 | } | ||
198 | |||
199 | // --------------------------------------------------------------------------- | ||
200 | |||
176 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { | 201 | function checkUserCanManageVideo (user: MUser, video: MVideoAccountLight, right: UserRight, res: Response, onlyOwned = true) { |
177 | // Retrieve the user who did the request | 202 | // Retrieve the user who did the request |
178 | if (onlyOwned && video.isOwned() === false) { | 203 | if (onlyOwned && video.isOwned() === false) { |
@@ -220,6 +245,7 @@ export { | |||
220 | doesVideoExist, | 245 | doesVideoExist, |
221 | doesVideoFileOfVideoExist, | 246 | doesVideoFileOfVideoExist, |
222 | 247 | ||
248 | checkCanAccessVideoStaticFiles, | ||
223 | checkUserCanManageVideo, | 249 | checkUserCanManageVideo, |
224 | checkCanSeeVideo, | 250 | checkCanSeeVideo, |
225 | checkUserQuota | 251 | checkUserQuota |
diff --git a/server/middlewares/validators/static.ts b/server/middlewares/validators/static.ts new file mode 100644 index 000000000..13fde6dd1 --- /dev/null +++ b/server/middlewares/validators/static.ts | |||
@@ -0,0 +1,169 @@ | |||
1 | import express from 'express' | ||
2 | import { query } from 'express-validator' | ||
3 | import LRUCache from 'lru-cache' | ||
4 | import { basename, dirname } from 'path' | ||
5 | import { exists, isUUIDValid } from '@server/helpers/custom-validators/misc' | ||
6 | import { logger } from '@server/helpers/logger' | ||
7 | import { LRU_CACHE } from '@server/initializers/constants' | ||
8 | import { VideoModel } from '@server/models/video/video' | ||
9 | import { VideoFileModel } from '@server/models/video/video-file' | ||
10 | import { MStreamingPlaylist, MVideoFile, MVideoThumbnail } from '@server/types/models' | ||
11 | import { HttpStatusCode } from '@shared/models' | ||
12 | import { areValidationErrors, checkCanAccessVideoStaticFiles } from './shared' | ||
13 | |||
14 | type LRUValue = { | ||
15 | allowed: boolean | ||
16 | video?: MVideoThumbnail | ||
17 | file?: MVideoFile | ||
18 | playlist?: MStreamingPlaylist } | ||
19 | |||
20 | const staticFileTokenBypass = new LRUCache<string, LRUValue>({ | ||
21 | max: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.MAX_SIZE, | ||
22 | ttl: LRU_CACHE.STATIC_VIDEO_FILES_RIGHTS_CHECK.TTL | ||
23 | }) | ||
24 | |||
25 | const ensureCanAccessVideoPrivateWebTorrentFiles = [ | ||
26 | query('videoFileToken').optional().custom(exists), | ||
27 | |||
28 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
29 | if (areValidationErrors(req, res)) return | ||
30 | |||
31 | const token = extractTokenOrDie(req, res) | ||
32 | if (!token) return | ||
33 | |||
34 | const cacheKey = token + '-' + req.originalUrl | ||
35 | |||
36 | if (staticFileTokenBypass.has(cacheKey)) { | ||
37 | const { allowed, file, video } = staticFileTokenBypass.get(cacheKey) | ||
38 | |||
39 | if (allowed === true) { | ||
40 | res.locals.onlyVideo = video | ||
41 | res.locals.videoFile = file | ||
42 | |||
43 | return next() | ||
44 | } | ||
45 | |||
46 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
47 | } | ||
48 | |||
49 | const result = await isWebTorrentAllowed(req, res) | ||
50 | |||
51 | staticFileTokenBypass.set(cacheKey, result) | ||
52 | |||
53 | if (result.allowed !== true) return | ||
54 | |||
55 | res.locals.onlyVideo = result.video | ||
56 | res.locals.videoFile = result.file | ||
57 | |||
58 | return next() | ||
59 | } | ||
60 | ] | ||
61 | |||
62 | const ensureCanAccessPrivateVideoHLSFiles = [ | ||
63 | query('videoFileToken').optional().custom(exists), | ||
64 | |||
65 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
66 | if (areValidationErrors(req, res)) return | ||
67 | |||
68 | const videoUUID = basename(dirname(req.originalUrl)) | ||
69 | |||
70 | if (!isUUIDValid(videoUUID)) { | ||
71 | logger.debug('Path does not contain valid video UUID to serve static file %s', req.originalUrl) | ||
72 | |||
73 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
74 | } | ||
75 | |||
76 | const token = extractTokenOrDie(req, res) | ||
77 | if (!token) return | ||
78 | |||
79 | const cacheKey = token + '-' + videoUUID | ||
80 | |||
81 | if (staticFileTokenBypass.has(cacheKey)) { | ||
82 | const { allowed, file, playlist, video } = staticFileTokenBypass.get(cacheKey) | ||
83 | |||
84 | if (allowed === true) { | ||
85 | res.locals.onlyVideo = video | ||
86 | res.locals.videoFile = file | ||
87 | res.locals.videoStreamingPlaylist = playlist | ||
88 | |||
89 | return next() | ||
90 | } | ||
91 | |||
92 | return res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
93 | } | ||
94 | |||
95 | const result = await isHLSAllowed(req, res, videoUUID) | ||
96 | |||
97 | staticFileTokenBypass.set(cacheKey, result) | ||
98 | |||
99 | if (result.allowed !== true) return | ||
100 | |||
101 | res.locals.onlyVideo = result.video | ||
102 | res.locals.videoFile = result.file | ||
103 | res.locals.videoStreamingPlaylist = result.playlist | ||
104 | |||
105 | return next() | ||
106 | } | ||
107 | ] | ||
108 | |||
109 | export { | ||
110 | ensureCanAccessVideoPrivateWebTorrentFiles, | ||
111 | ensureCanAccessPrivateVideoHLSFiles | ||
112 | } | ||
113 | |||
114 | // --------------------------------------------------------------------------- | ||
115 | |||
116 | async function isWebTorrentAllowed (req: express.Request, res: express.Response) { | ||
117 | const filename = basename(req.path) | ||
118 | |||
119 | const file = await VideoFileModel.loadWithVideoByFilename(filename) | ||
120 | if (!file) { | ||
121 | logger.debug('Unknown static file %s to serve', req.originalUrl, { filename }) | ||
122 | |||
123 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
124 | return { allowed: false } | ||
125 | } | ||
126 | |||
127 | const video = await VideoModel.load(file.getVideo().id) | ||
128 | |||
129 | return { | ||
130 | file, | ||
131 | video, | ||
132 | allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | ||
133 | } | ||
134 | } | ||
135 | |||
136 | async function isHLSAllowed (req: express.Request, res: express.Response, videoUUID: string) { | ||
137 | const filename = basename(req.path) | ||
138 | |||
139 | const video = await VideoModel.loadWithFiles(videoUUID) | ||
140 | |||
141 | if (!video) { | ||
142 | logger.debug('Unknown static file %s to serve', req.originalUrl, { videoUUID }) | ||
143 | |||
144 | res.sendStatus(HttpStatusCode.FORBIDDEN_403) | ||
145 | return { allowed: false } | ||
146 | } | ||
147 | |||
148 | const file = await VideoFileModel.loadByFilename(filename) | ||
149 | |||
150 | return { | ||
151 | file, | ||
152 | video, | ||
153 | playlist: video.getHLSPlaylist(), | ||
154 | allowed: await checkCanAccessVideoStaticFiles({ req, res, video, paramId: video.uuid }) | ||
155 | } | ||
156 | } | ||
157 | |||
158 | function extractTokenOrDie (req: express.Request, res: express.Response) { | ||
159 | const token = res.locals.oauth?.token.accessToken || req.query.videoFileToken | ||
160 | |||
161 | if (!token) { | ||
162 | return res.fail({ | ||
163 | message: 'Bearer token is missing in headers or video file token is missing in URL query parameters', | ||
164 | status: HttpStatusCode.FORBIDDEN_403 | ||
165 | }) | ||
166 | } | ||
167 | |||
168 | return token | ||
169 | } | ||
diff --git a/server/middlewares/validators/themes.ts b/server/middlewares/validators/themes.ts index c130801a0..080b3e096 100644 --- a/server/middlewares/validators/themes.ts +++ b/server/middlewares/validators/themes.ts | |||
@@ -2,7 +2,7 @@ import express from 'express' | |||
2 | import { param } from 'express-validator' | 2 | import { param } from 'express-validator' |
3 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 3 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' |
4 | import { isSafePath } from '../../helpers/custom-validators/misc' | 4 | import { isSafePath } from '../../helpers/custom-validators/misc' |
5 | import { isPluginNameValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins' | 5 | import { isPluginNameValid, isPluginStableOrUnstableVersionValid } from '../../helpers/custom-validators/plugins' |
6 | import { PluginManager } from '../../lib/plugins/plugin-manager' | 6 | import { PluginManager } from '../../lib/plugins/plugin-manager' |
7 | import { areValidationErrors } from './shared' | 7 | import { areValidationErrors } from './shared' |
8 | 8 | ||
@@ -10,7 +10,7 @@ const serveThemeCSSValidator = [ | |||
10 | param('themeName') | 10 | param('themeName') |
11 | .custom(isPluginNameValid), | 11 | .custom(isPluginNameValid), |
12 | param('themeVersion') | 12 | param('themeVersion') |
13 | .custom(isPluginVersionValid), | 13 | .custom(isPluginStableOrUnstableVersionValid), |
14 | param('staticEndpoint') | 14 | param('staticEndpoint') |
15 | .custom(isSafePath), | 15 | .custom(isSafePath), |
16 | 16 | ||
diff --git a/server/middlewares/validators/two-factor.ts b/server/middlewares/validators/two-factor.ts new file mode 100644 index 000000000..106b579b5 --- /dev/null +++ b/server/middlewares/validators/two-factor.ts | |||
@@ -0,0 +1,81 @@ | |||
1 | import express from 'express' | ||
2 | import { body, param } from 'express-validator' | ||
3 | import { HttpStatusCode, UserRight } from '@shared/models' | ||
4 | import { exists, isIdValid } from '../../helpers/custom-validators/misc' | ||
5 | import { areValidationErrors, checkUserIdExist } from './shared' | ||
6 | |||
7 | const requestOrConfirmTwoFactorValidator = [ | ||
8 | param('id').custom(isIdValid), | ||
9 | |||
10 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
11 | if (areValidationErrors(req, res)) return | ||
12 | |||
13 | if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return | ||
14 | |||
15 | if (res.locals.user.otpSecret) { | ||
16 | return res.fail({ | ||
17 | status: HttpStatusCode.BAD_REQUEST_400, | ||
18 | message: `Two factor is already enabled.` | ||
19 | }) | ||
20 | } | ||
21 | |||
22 | return next() | ||
23 | } | ||
24 | ] | ||
25 | |||
26 | const confirmTwoFactorValidator = [ | ||
27 | body('requestToken').custom(exists), | ||
28 | body('otpToken').custom(exists), | ||
29 | |||
30 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
31 | if (areValidationErrors(req, res)) return | ||
32 | |||
33 | return next() | ||
34 | } | ||
35 | ] | ||
36 | |||
37 | const disableTwoFactorValidator = [ | ||
38 | param('id').custom(isIdValid), | ||
39 | |||
40 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
41 | if (areValidationErrors(req, res)) return | ||
42 | |||
43 | if (!await checkCanEnableOrDisableTwoFactor(req.params.id, res)) return | ||
44 | |||
45 | if (!res.locals.user.otpSecret) { | ||
46 | return res.fail({ | ||
47 | status: HttpStatusCode.BAD_REQUEST_400, | ||
48 | message: `Two factor is already disabled.` | ||
49 | }) | ||
50 | } | ||
51 | |||
52 | return next() | ||
53 | } | ||
54 | ] | ||
55 | |||
56 | // --------------------------------------------------------------------------- | ||
57 | |||
58 | export { | ||
59 | requestOrConfirmTwoFactorValidator, | ||
60 | confirmTwoFactorValidator, | ||
61 | disableTwoFactorValidator | ||
62 | } | ||
63 | |||
64 | // --------------------------------------------------------------------------- | ||
65 | |||
66 | async function checkCanEnableOrDisableTwoFactor (userId: number | string, res: express.Response) { | ||
67 | const authUser = res.locals.oauth.token.user | ||
68 | |||
69 | if (!await checkUserIdExist(userId, res)) return | ||
70 | |||
71 | if (res.locals.user.id !== authUser.id && authUser.hasRight(UserRight.MANAGE_USERS) !== true) { | ||
72 | res.fail({ | ||
73 | status: HttpStatusCode.FORBIDDEN_403, | ||
74 | message: `User ${authUser.username} does not have right to change two factor setting of this user.` | ||
75 | }) | ||
76 | |||
77 | return false | ||
78 | } | ||
79 | |||
80 | return true | ||
81 | } | ||
diff --git a/server/middlewares/validators/user-subscriptions.ts b/server/middlewares/validators/user-subscriptions.ts index d01043c17..d8d3fc28b 100644 --- a/server/middlewares/validators/user-subscriptions.ts +++ b/server/middlewares/validators/user-subscriptions.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { arrayify } from '@shared/core-utils' | ||
2 | import express from 'express' | 1 | import express from 'express' |
3 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { arrayify } from '@shared/core-utils' | ||
4 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' | 4 | import { HttpStatusCode } from '../../../shared/models/http/http-error-codes' |
5 | import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' | 5 | import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor' |
6 | import { WEBSERVER } from '../../initializers/constants' | 6 | import { WEBSERVER } from '../../initializers/constants' |
@@ -60,7 +60,7 @@ const userSubscriptionGetValidator = [ | |||
60 | state: 'accepted' | 60 | state: 'accepted' |
61 | }) | 61 | }) |
62 | 62 | ||
63 | if (!subscription || !subscription.ActorFollowing.VideoChannel) { | 63 | if (!subscription?.ActorFollowing.VideoChannel) { |
64 | return res.fail({ | 64 | return res.fail({ |
65 | status: HttpStatusCode.NOT_FOUND_404, | 65 | status: HttpStatusCode.NOT_FOUND_404, |
66 | message: `Subscription ${req.params.uri} not found.` | 66 | message: `Subscription ${req.params.uri} not found.` |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 2de5265fb..50327b6ae 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -1,9 +1,9 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { body, param, query } from 'express-validator' | 2 | import { body, param, query } from 'express-validator' |
3 | import { Hooks } from '@server/lib/plugins/hooks' | 3 | import { Hooks } from '@server/lib/plugins/hooks' |
4 | import { MUserDefault } from '@server/types/models' | 4 | import { forceNumber } from '@shared/core-utils' |
5 | import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' | 5 | import { HttpStatusCode, UserRegister, UserRight, UserRole } from '@shared/models' |
6 | import { isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' | 6 | import { exists, isBooleanValid, isIdValid, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc' |
7 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' | 7 | import { isThemeNameValid } from '../../helpers/custom-validators/plugins' |
8 | import { | 8 | import { |
9 | isUserAdminFlagsValid, | 9 | isUserAdminFlagsValid, |
@@ -30,8 +30,15 @@ import { isThemeRegistered } from '../../lib/plugins/theme-utils' | |||
30 | import { Redis } from '../../lib/redis' | 30 | import { Redis } from '../../lib/redis' |
31 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' | 31 | import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../lib/signup' |
32 | import { ActorModel } from '../../models/actor/actor' | 32 | import { ActorModel } from '../../models/actor/actor' |
33 | import { UserModel } from '../../models/user/user' | 33 | import { |
34 | import { areValidationErrors, doesVideoChannelIdExist, doesVideoExist, isValidVideoIdParam } from './shared' | 34 | areValidationErrors, |
35 | checkUserEmailExist, | ||
36 | checkUserIdExist, | ||
37 | checkUserNameOrEmailDoesNotAlreadyExist, | ||
38 | doesVideoChannelIdExist, | ||
39 | doesVideoExist, | ||
40 | isValidVideoIdParam | ||
41 | } from './shared' | ||
35 | 42 | ||
36 | const usersListValidator = [ | 43 | const usersListValidator = [ |
37 | query('blocked') | 44 | query('blocked') |
@@ -411,6 +418,13 @@ const usersAskResetPasswordValidator = [ | |||
411 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 418 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
412 | } | 419 | } |
413 | 420 | ||
421 | if (res.locals.user.pluginAuth) { | ||
422 | return res.fail({ | ||
423 | status: HttpStatusCode.CONFLICT_409, | ||
424 | message: 'Cannot recover password of a user that uses a plugin authentication.' | ||
425 | }) | ||
426 | } | ||
427 | |||
414 | return next() | 428 | return next() |
415 | } | 429 | } |
416 | ] | 430 | ] |
@@ -428,7 +442,7 @@ const usersResetPasswordValidator = [ | |||
428 | if (!await checkUserIdExist(req.params.id, res)) return | 442 | if (!await checkUserIdExist(req.params.id, res)) return |
429 | 443 | ||
430 | const user = res.locals.user | 444 | const user = res.locals.user |
431 | const redisVerificationString = await Redis.Instance.getResetPasswordLink(user.id) | 445 | const redisVerificationString = await Redis.Instance.getResetPasswordVerificationString(user.id) |
432 | 446 | ||
433 | if (redisVerificationString !== req.body.verificationString) { | 447 | if (redisVerificationString !== req.body.verificationString) { |
434 | return res.fail({ | 448 | return res.fail({ |
@@ -454,6 +468,13 @@ const usersAskSendVerifyEmailValidator = [ | |||
454 | return res.status(HttpStatusCode.NO_CONTENT_204).end() | 468 | return res.status(HttpStatusCode.NO_CONTENT_204).end() |
455 | } | 469 | } |
456 | 470 | ||
471 | if (res.locals.user.pluginAuth) { | ||
472 | return res.fail({ | ||
473 | status: HttpStatusCode.CONFLICT_409, | ||
474 | message: 'Cannot ask verification email of a user that uses a plugin authentication.' | ||
475 | }) | ||
476 | } | ||
477 | |||
457 | return next() | 478 | return next() |
458 | } | 479 | } |
459 | ] | 480 | ] |
@@ -486,6 +507,41 @@ const usersVerifyEmailValidator = [ | |||
486 | } | 507 | } |
487 | ] | 508 | ] |
488 | 509 | ||
510 | const usersCheckCurrentPasswordFactory = (targetUserIdGetter: (req: express.Request) => number | string) => { | ||
511 | return [ | ||
512 | body('currentPassword').optional().custom(exists), | ||
513 | |||
514 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
515 | if (areValidationErrors(req, res)) return | ||
516 | |||
517 | const user = res.locals.oauth.token.User | ||
518 | const isAdminOrModerator = user.role === UserRole.ADMINISTRATOR || user.role === UserRole.MODERATOR | ||
519 | const targetUserId = forceNumber(targetUserIdGetter(req)) | ||
520 | |||
521 | // Admin/moderator action on another user, skip the password check | ||
522 | if (isAdminOrModerator && targetUserId !== user.id) { | ||
523 | return next() | ||
524 | } | ||
525 | |||
526 | if (!req.body.currentPassword) { | ||
527 | return res.fail({ | ||
528 | status: HttpStatusCode.BAD_REQUEST_400, | ||
529 | message: 'currentPassword is missing' | ||
530 | }) | ||
531 | } | ||
532 | |||
533 | if (await user.isPasswordMatch(req.body.currentPassword) !== true) { | ||
534 | return res.fail({ | ||
535 | status: HttpStatusCode.FORBIDDEN_403, | ||
536 | message: 'currentPassword is invalid.' | ||
537 | }) | ||
538 | } | ||
539 | |||
540 | return next() | ||
541 | } | ||
542 | ] | ||
543 | } | ||
544 | |||
489 | const userAutocompleteValidator = [ | 545 | const userAutocompleteValidator = [ |
490 | param('search') | 546 | param('search') |
491 | .isString() | 547 | .isString() |
@@ -553,6 +609,7 @@ export { | |||
553 | usersUpdateValidator, | 609 | usersUpdateValidator, |
554 | usersUpdateMeValidator, | 610 | usersUpdateMeValidator, |
555 | usersVideoRatingValidator, | 611 | usersVideoRatingValidator, |
612 | usersCheckCurrentPasswordFactory, | ||
556 | ensureUserRegistrationAllowed, | 613 | ensureUserRegistrationAllowed, |
557 | ensureUserRegistrationAllowedForIP, | 614 | ensureUserRegistrationAllowedForIP, |
558 | usersGetValidator, | 615 | usersGetValidator, |
@@ -566,55 +623,3 @@ export { | |||
566 | ensureCanModerateUser, | 623 | ensureCanModerateUser, |
567 | ensureCanManageChannelOrAccount | 624 | ensureCanManageChannelOrAccount |
568 | } | 625 | } |
569 | |||
570 | // --------------------------------------------------------------------------- | ||
571 | |||
572 | function checkUserIdExist (idArg: number | string, res: express.Response, withStats = false) { | ||
573 | const id = parseInt(idArg + '', 10) | ||
574 | return checkUserExist(() => UserModel.loadByIdWithChannels(id, withStats), res) | ||
575 | } | ||
576 | |||
577 | function checkUserEmailExist (email: string, res: express.Response, abortResponse = true) { | ||
578 | return checkUserExist(() => UserModel.loadByEmail(email), res, abortResponse) | ||
579 | } | ||
580 | |||
581 | async function checkUserNameOrEmailDoesNotAlreadyExist (username: string, email: string, res: express.Response) { | ||
582 | const user = await UserModel.loadByUsernameOrEmail(username, email) | ||
583 | |||
584 | if (user) { | ||
585 | res.fail({ | ||
586 | status: HttpStatusCode.CONFLICT_409, | ||
587 | message: 'User with this username or email already exists.' | ||
588 | }) | ||
589 | return false | ||
590 | } | ||
591 | |||
592 | const actor = await ActorModel.loadLocalByName(username) | ||
593 | if (actor) { | ||
594 | res.fail({ | ||
595 | status: HttpStatusCode.CONFLICT_409, | ||
596 | message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.' | ||
597 | }) | ||
598 | return false | ||
599 | } | ||
600 | |||
601 | return true | ||
602 | } | ||
603 | |||
604 | async function checkUserExist (finder: () => Promise<MUserDefault>, res: express.Response, abortResponse = true) { | ||
605 | const user = await finder() | ||
606 | |||
607 | if (!user) { | ||
608 | if (abortResponse === true) { | ||
609 | res.fail({ | ||
610 | status: HttpStatusCode.NOT_FOUND_404, | ||
611 | message: 'User not found' | ||
612 | }) | ||
613 | } | ||
614 | |||
615 | return false | ||
616 | } | ||
617 | |||
618 | res.locals.user = user | ||
619 | return true | ||
620 | } | ||
diff --git a/server/middlewares/validators/videos/video-comments.ts b/server/middlewares/validators/videos/video-comments.ts index 69062701b..133feb7bd 100644 --- a/server/middlewares/validators/videos/video-comments.ts +++ b/server/middlewares/validators/videos/video-comments.ts | |||
@@ -208,7 +208,8 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon | |||
208 | const acceptParameters = { | 208 | const acceptParameters = { |
209 | video, | 209 | video, |
210 | commentBody: req.body, | 210 | commentBody: req.body, |
211 | user: res.locals.oauth.token.User | 211 | user: res.locals.oauth.token.User, |
212 | req | ||
212 | } | 213 | } |
213 | 214 | ||
214 | let acceptedResult: AcceptResult | 215 | let acceptedResult: AcceptResult |
@@ -234,7 +235,7 @@ async function isVideoCommentAccepted (req: express.Request, res: express.Respon | |||
234 | 235 | ||
235 | res.fail({ | 236 | res.fail({ |
236 | status: HttpStatusCode.FORBIDDEN_403, | 237 | status: HttpStatusCode.FORBIDDEN_403, |
237 | message: acceptedResult?.errorMessage || 'Refused local comment' | 238 | message: acceptedResult?.errorMessage || 'Comment has been rejected.' |
238 | }) | 239 | }) |
239 | return false | 240 | return false |
240 | } | 241 | } |
diff --git a/server/middlewares/validators/videos/video-imports.ts b/server/middlewares/validators/videos/video-imports.ts index f295b1885..72442aeb6 100644 --- a/server/middlewares/validators/videos/video-imports.ts +++ b/server/middlewares/validators/videos/video-imports.ts | |||
@@ -4,6 +4,7 @@ import { isResolvingToUnicastOnly } from '@server/helpers/dns' | |||
4 | import { isPreImportVideoAccepted } from '@server/lib/moderation' | 4 | import { isPreImportVideoAccepted } from '@server/lib/moderation' |
5 | import { Hooks } from '@server/lib/plugins/hooks' | 5 | import { Hooks } from '@server/lib/plugins/hooks' |
6 | import { MUserAccountId, MVideoImport } from '@server/types/models' | 6 | import { MUserAccountId, MVideoImport } from '@server/types/models' |
7 | import { forceNumber } from '@shared/core-utils' | ||
7 | import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' | 8 | import { HttpStatusCode, UserRight, VideoImportState } from '@shared/models' |
8 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' | 9 | import { VideoImportCreate } from '@shared/models/videos/import/video-import-create.model' |
9 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' | 10 | import { isIdValid, toIntOrNull } from '../../../helpers/custom-validators/misc' |
@@ -130,7 +131,7 @@ const videoImportCancelValidator = [ | |||
130 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 131 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
131 | if (areValidationErrors(req, res)) return | 132 | if (areValidationErrors(req, res)) return |
132 | 133 | ||
133 | if (!await doesVideoImportExist(parseInt(req.params.id), res)) return | 134 | if (!await doesVideoImportExist(forceNumber(req.params.id), res)) return |
134 | if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return | 135 | if (!checkUserCanManageImport(res.locals.oauth.token.user, res.locals.videoImport, res)) return |
135 | 136 | ||
136 | if (res.locals.videoImport.state !== VideoImportState.PENDING) { | 137 | if (res.locals.videoImport.state !== VideoImportState.PENDING) { |
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 6d4b8a6f1..e4b7e5c56 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -2,6 +2,7 @@ import express from 'express' | |||
2 | import { body, param, query, ValidationChain } from 'express-validator' | 2 | import { body, param, query, ValidationChain } from 'express-validator' |
3 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 3 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
4 | import { MUserAccountId } from '@server/types/models' | 4 | import { MUserAccountId } from '@server/types/models' |
5 | import { forceNumber } from '@shared/core-utils' | ||
5 | import { | 6 | import { |
6 | HttpStatusCode, | 7 | HttpStatusCode, |
7 | UserRight, | 8 | UserRight, |
@@ -258,7 +259,7 @@ const videoPlaylistElementAPGetValidator = [ | |||
258 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | 259 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { |
259 | if (areValidationErrors(req, res)) return | 260 | if (areValidationErrors(req, res)) return |
260 | 261 | ||
261 | const playlistElementId = parseInt(req.params.playlistElementId + '', 10) | 262 | const playlistElementId = forceNumber(req.params.playlistElementId) |
262 | const playlistId = req.params.playlistId | 263 | const playlistId = req.params.playlistId |
263 | 264 | ||
264 | const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) | 265 | const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndElementIdForAP(playlistId, playlistElementId) |
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts index 7fd2b03d1..e29eb4a32 100644 --- a/server/middlewares/validators/videos/videos.ts +++ b/server/middlewares/validators/videos/videos.ts | |||
@@ -7,7 +7,7 @@ import { getServerActor } from '@server/models/application/application' | |||
7 | import { ExpressPromiseHandler } from '@server/types/express-handler' | 7 | import { ExpressPromiseHandler } from '@server/types/express-handler' |
8 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' | 8 | import { MUserAccountId, MVideoFullLight } from '@server/types/models' |
9 | import { arrayify, getAllPrivacies } from '@shared/core-utils' | 9 | import { arrayify, getAllPrivacies } from '@shared/core-utils' |
10 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude } from '@shared/models' | 10 | import { HttpStatusCode, ServerErrorCode, UserRight, VideoInclude, VideoState } from '@shared/models' |
11 | import { | 11 | import { |
12 | exists, | 12 | exists, |
13 | isBooleanValid, | 13 | isBooleanValid, |
@@ -48,6 +48,7 @@ import { Hooks } from '../../../lib/plugins/hooks' | |||
48 | import { VideoModel } from '../../../models/video/video' | 48 | import { VideoModel } from '../../../models/video/video' |
49 | import { | 49 | import { |
50 | areValidationErrors, | 50 | areValidationErrors, |
51 | checkCanAccessVideoStaticFiles, | ||
51 | checkCanSeeVideo, | 52 | checkCanSeeVideo, |
52 | checkUserCanManageVideo, | 53 | checkUserCanManageVideo, |
53 | checkUserQuota, | 54 | checkUserQuota, |
@@ -232,6 +233,11 @@ const videosUpdateValidator = getCommonVideoEditAttributes().concat([ | |||
232 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) | 233 | if (areErrorsInScheduleUpdate(req, res)) return cleanUpReqFiles(req) |
233 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) | 234 | if (!await doesVideoExist(req.params.id, res)) return cleanUpReqFiles(req) |
234 | 235 | ||
236 | const video = getVideoWithAttributes(res) | ||
237 | if (req.body.privacy && video.isLive && video.state !== VideoState.WAITING_FOR_LIVE) { | ||
238 | return res.fail({ message: 'Cannot update privacy of a live that has already started' }) | ||
239 | } | ||
240 | |||
235 | // Check if the user who did the request is able to update the video | 241 | // Check if the user who did the request is able to update the video |
236 | const user = res.locals.oauth.token.User | 242 | const user = res.locals.oauth.token.User |
237 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) | 243 | if (!checkUserCanManageVideo(user, res.locals.videoAll, UserRight.UPDATE_ANY_VIDEO, res)) return cleanUpReqFiles(req) |
@@ -271,10 +277,7 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R | |||
271 | }) | 277 | }) |
272 | } | 278 | } |
273 | 279 | ||
274 | const videosCustomGetValidator = ( | 280 | const videosCustomGetValidator = (fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes') => { |
275 | fetchType: 'for-api' | 'all' | 'only-video' | 'only-immutable-attributes', | ||
276 | authenticateInQuery = false | ||
277 | ) => { | ||
278 | return [ | 281 | return [ |
279 | isValidVideoIdParam('id'), | 282 | isValidVideoIdParam('id'), |
280 | 283 | ||
@@ -287,7 +290,7 @@ const videosCustomGetValidator = ( | |||
287 | 290 | ||
288 | const video = getVideoWithAttributes(res) as MVideoFullLight | 291 | const video = getVideoWithAttributes(res) as MVideoFullLight |
289 | 292 | ||
290 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id, authenticateInQuery })) return | 293 | if (!await checkCanSeeVideo({ req, res, video, paramId: req.params.id })) return |
291 | 294 | ||
292 | return next() | 295 | return next() |
293 | } | 296 | } |
@@ -295,7 +298,6 @@ const videosCustomGetValidator = ( | |||
295 | } | 298 | } |
296 | 299 | ||
297 | const videosGetValidator = videosCustomGetValidator('all') | 300 | const videosGetValidator = videosCustomGetValidator('all') |
298 | const videosDownloadValidator = videosCustomGetValidator('all', true) | ||
299 | 301 | ||
300 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | 302 | const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ |
301 | isValidVideoIdParam('id'), | 303 | isValidVideoIdParam('id'), |
@@ -311,6 +313,21 @@ const videoFileMetadataGetValidator = getCommonVideoEditAttributes().concat([ | |||
311 | } | 313 | } |
312 | ]) | 314 | ]) |
313 | 315 | ||
316 | const videosDownloadValidator = [ | ||
317 | isValidVideoIdParam('id'), | ||
318 | |||
319 | async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
320 | if (areValidationErrors(req, res)) return | ||
321 | if (!await doesVideoExist(req.params.id, res, 'all')) return | ||
322 | |||
323 | const video = getVideoWithAttributes(res) | ||
324 | |||
325 | if (!await checkCanAccessVideoStaticFiles({ req, res, video, paramId: req.params.id })) return | ||
326 | |||
327 | return next() | ||
328 | } | ||
329 | ] | ||
330 | |||
314 | const videosRemoveValidator = [ | 331 | const videosRemoveValidator = [ |
315 | isValidVideoIdParam('id'), | 332 | isValidVideoIdParam('id'), |
316 | 333 | ||
@@ -372,7 +389,7 @@ function getCommonVideoEditAttributes () { | |||
372 | .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'), | 389 | .custom(isBooleanValid).withMessage('Should have a valid waitTranscoding boolean'), |
373 | body('privacy') | 390 | body('privacy') |
374 | .optional() | 391 | .optional() |
375 | .customSanitizer(toValueOrNull) | 392 | .customSanitizer(toIntOrNull) |
376 | .custom(isVideoPrivacyValid), | 393 | .custom(isVideoPrivacyValid), |
377 | body('description') | 394 | body('description') |
378 | .optional() | 395 | .optional() |