aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/index.ts4
-rw-r--r--server/controllers/api/users/index.ts12
-rw-r--r--server/controllers/api/users/token.ts5
-rw-r--r--server/controllers/feeds.ts10
-rw-r--r--server/helpers/audit-logger.ts12
-rw-r--r--server/helpers/core-utils.ts37
-rw-r--r--server/helpers/image-utils.ts2
-rw-r--r--server/helpers/upload.ts9
-rw-r--r--server/initializers/checker-before-init.ts5
-rw-r--r--server/initializers/config.ts9
-rw-r--r--server/initializers/constants.ts13
-rw-r--r--server/lib/activitypub/process/process-create.ts2
-rw-r--r--server/lib/activitypub/videos/shared/creator.ts2
-rw-r--r--server/lib/activitypub/videos/shared/object-to-model-attributes.ts2
-rw-r--r--server/lib/client-html.ts6
-rw-r--r--server/lib/emailer.ts2
-rw-r--r--server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts2
-rw-r--r--server/lib/redis.ts2
-rw-r--r--server/lib/schedulers/geo-ip-update-scheduler.ts2
-rw-r--r--server/lib/signup.ts2
-rw-r--r--server/middlewares/index.ts1
-rw-r--r--server/middlewares/rate-limiter.ts31
-rw-r--r--server/middlewares/validators/sort.ts4
-rw-r--r--server/middlewares/validators/users.ts2
-rw-r--r--server/models/abuse/abuse-query-builder.ts2
-rw-r--r--server/models/shared/abstract-run-query.ts2
-rw-r--r--server/models/user/user.ts6
-rw-r--r--server/models/utils.ts24
-rw-r--r--server/models/video/video-channel.ts13
-rw-r--r--server/tests/api/check-params/abuses.ts4
-rw-r--r--server/tests/api/live/live.ts2
-rw-r--r--server/tests/api/moderation/abuses.ts4
-rw-r--r--server/tests/api/notifications/user-notifications.ts2
-rw-r--r--server/tests/api/search/search-index.ts8
-rw-r--r--server/tests/api/server/contact-form.ts2
-rw-r--r--server/tests/api/server/reverse-proxy.ts11
-rw-r--r--server/tests/api/videos/video-channels.ts19
-rw-r--r--server/tests/helpers/core-utils.ts65
38 files changed, 232 insertions, 110 deletions
diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts
index 5f49336b1..d1d4ef765 100644
--- a/server/controllers/api/index.ts
+++ b/server/controllers/api/index.ts
@@ -1,6 +1,6 @@
1import cors from 'cors' 1import cors from 'cors'
2import express from 'express' 2import express from 'express'
3import RateLimit from 'express-rate-limit' 3import { buildRateLimiter } from '@server/middlewares'
4import { HttpStatusCode } from '../../../shared/models' 4import { HttpStatusCode } from '../../../shared/models'
5import { badRequest } from '../../helpers/express-utils' 5import { badRequest } from '../../helpers/express-utils'
6import { CONFIG } from '../../initializers/config' 6import { CONFIG } from '../../initializers/config'
@@ -29,7 +29,7 @@ apiRouter.use(cors({
29 credentials: true 29 credentials: true
30})) 30}))
31 31
32const apiRateLimiter = RateLimit({ 32const apiRateLimiter = buildRateLimiter({
33 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS, 33 windowMs: CONFIG.RATES_LIMIT.API.WINDOW_MS,
34 max: CONFIG.RATES_LIMIT.API.MAX 34 max: CONFIG.RATES_LIMIT.API.MAX
35}) 35})
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 8a06bfe93..46e80d56d 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -1,5 +1,4 @@
1import express from 'express' 1import express from 'express'
2import RateLimit from 'express-rate-limit'
3import { tokensRouter } from '@server/controllers/api/users/token' 2import { tokensRouter } from '@server/controllers/api/users/token'
4import { Hooks } from '@server/lib/plugins/hooks' 3import { Hooks } from '@server/lib/plugins/hooks'
5import { OAuthTokenModel } from '@server/models/oauth/oauth-token' 4import { OAuthTokenModel } from '@server/models/oauth/oauth-token'
@@ -17,9 +16,11 @@ import { Notifier } from '../../../lib/notifier'
17import { Redis } from '../../../lib/redis' 16import { Redis } from '../../../lib/redis'
18import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user' 17import { buildUser, createUserAccountAndChannelAndPlaylist, sendVerifyUserEmail } from '../../../lib/user'
19import { 18import {
19 adminUsersSortValidator,
20 asyncMiddleware, 20 asyncMiddleware,
21 asyncRetryTransactionMiddleware, 21 asyncRetryTransactionMiddleware,
22 authenticate, 22 authenticate,
23 buildRateLimiter,
23 ensureUserHasRight, 24 ensureUserHasRight,
24 ensureUserRegistrationAllowed, 25 ensureUserRegistrationAllowed,
25 ensureUserRegistrationAllowedForIP, 26 ensureUserRegistrationAllowedForIP,
@@ -32,7 +33,6 @@ import {
32 usersListValidator, 33 usersListValidator,
33 usersRegisterValidator, 34 usersRegisterValidator,
34 usersRemoveValidator, 35 usersRemoveValidator,
35 usersSortValidator,
36 usersUpdateValidator 36 usersUpdateValidator
37} from '../../../middlewares' 37} from '../../../middlewares'
38import { 38import {
@@ -54,13 +54,13 @@ import { myVideoPlaylistsRouter } from './my-video-playlists'
54 54
55const auditLogger = auditLoggerFactory('users') 55const auditLogger = auditLoggerFactory('users')
56 56
57const signupRateLimiter = RateLimit({ 57const signupRateLimiter = buildRateLimiter({
58 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS, 58 windowMs: CONFIG.RATES_LIMIT.SIGNUP.WINDOW_MS,
59 max: CONFIG.RATES_LIMIT.SIGNUP.MAX, 59 max: CONFIG.RATES_LIMIT.SIGNUP.MAX,
60 skipFailedRequests: true 60 skipFailedRequests: true
61}) 61})
62 62
63const askSendEmailLimiter = RateLimit({ 63const askSendEmailLimiter = buildRateLimiter({
64 windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS, 64 windowMs: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.WINDOW_MS,
65 max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX 65 max: CONFIG.RATES_LIMIT.ASK_SEND_EMAIL.MAX
66}) 66})
@@ -84,7 +84,7 @@ usersRouter.get('/',
84 authenticate, 84 authenticate,
85 ensureUserHasRight(UserRight.MANAGE_USERS), 85 ensureUserHasRight(UserRight.MANAGE_USERS),
86 paginationValidator, 86 paginationValidator,
87 usersSortValidator, 87 adminUsersSortValidator,
88 setDefaultSort, 88 setDefaultSort,
89 setDefaultPagination, 89 setDefaultPagination,
90 usersListValidator, 90 usersListValidator,
@@ -277,7 +277,7 @@ async function autocompleteUsers (req: express.Request, res: express.Response) {
277} 277}
278 278
279async function listUsers (req: express.Request, res: express.Response) { 279async function listUsers (req: express.Request, res: express.Response) {
280 const resultList = await UserModel.listForApi({ 280 const resultList = await UserModel.listForAdminApi({
281 start: req.query.start, 281 start: req.query.start,
282 count: req.query.count, 282 count: req.query.count,
283 sort: req.query.sort, 283 sort: req.query.sort,
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 258b50fe9..012a49791 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,18 +1,17 @@
1import express from 'express' 1import express from 'express'
2import RateLimit from 'express-rate-limit'
3import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
4import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
5import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' 4import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
6import { handleOAuthToken } from '@server/lib/auth/oauth' 5import { handleOAuthToken } from '@server/lib/auth/oauth'
7import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' 6import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
8import { Hooks } from '@server/lib/plugins/hooks' 7import { Hooks } from '@server/lib/plugins/hooks'
9import { asyncMiddleware, authenticate, openapiOperationDoc } from '@server/middlewares' 8import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares'
10import { buildUUID } from '@shared/extra-utils' 9import { buildUUID } from '@shared/extra-utils'
11import { ScopedToken } from '@shared/models/users/user-scoped-token' 10import { ScopedToken } from '@shared/models/users/user-scoped-token'
12 11
13const tokensRouter = express.Router() 12const tokensRouter = express.Router()
14 13
15const loginRateLimiter = RateLimit({ 14const loginRateLimiter = buildRateLimiter({
16 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS, 15 windowMs: CONFIG.RATES_LIMIT.LOGIN.WINDOW_MS,
17 max: CONFIG.RATES_LIMIT.LOGIN.MAX 16 max: CONFIG.RATES_LIMIT.LOGIN.MAX
18}) 17})
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index c929a6726..9eb31ed93 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -1,13 +1,13 @@
1import express from 'express' 1import express from 'express'
2import { Feed } from '@peertube/feed'
3import { extname } from 'path' 2import { extname } from 'path'
3import { Feed } from '@peertube/feed'
4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown' 4import { mdToOneLinePlainText, toSafeHtml } from '@server/helpers/markdown'
5import { getServerActor } from '@server/models/application/application' 5import { getServerActor } from '@server/models/application/application'
6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils' 6import { getCategoryLabel } from '@server/models/video/formatter/video-format-utils'
7import { VideoInclude } from '@shared/models' 7import { VideoInclude } from '@shared/models'
8import { buildNSFWFilter } from '../helpers/express-utils' 8import { buildNSFWFilter } from '../helpers/express-utils'
9import { CONFIG } from '../initializers/config' 9import { CONFIG } from '../initializers/config'
10import { FEEDS, MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 10import { MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
11import { 11import {
12 asyncMiddleware, 12 asyncMiddleware,
13 commonVideosFiltersValidator, 13 commonVideosFiltersValidator,
@@ -76,7 +76,7 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
76 76
77 const comments = await VideoCommentModel.listForFeed({ 77 const comments = await VideoCommentModel.listForFeed({
78 start, 78 start,
79 count: FEEDS.COUNT, 79 count: CONFIG.FEEDS.COMMENTS.COUNT,
80 videoId: video ? video.id : undefined, 80 videoId: video ? video.id : undefined,
81 accountId: account ? account.id : undefined, 81 accountId: account ? account.id : undefined,
82 videoChannelId: videoChannel ? videoChannel.id : undefined 82 videoChannelId: videoChannel ? videoChannel.id : undefined
@@ -166,7 +166,7 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
166 const server = await getServerActor() 166 const server = await getServerActor()
167 const { data } = await VideoModel.listForApi({ 167 const { data } = await VideoModel.listForApi({
168 start, 168 start,
169 count: FEEDS.COUNT, 169 count: CONFIG.FEEDS.VIDEOS.COUNT,
170 sort: req.query.sort, 170 sort: req.query.sort,
171 displayOnlyForFollower: { 171 displayOnlyForFollower: {
172 actorId: server.id, 172 actorId: server.id,
@@ -202,7 +202,7 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
202 202
203 const { data } = await VideoModel.listForApi({ 203 const { data } = await VideoModel.listForApi({
204 start, 204 start,
205 count: FEEDS.COUNT, 205 count: CONFIG.FEEDS.VIDEOS.COUNT,
206 sort: req.query.sort, 206 sort: req.query.sort,
207 nsfw, 207 nsfw,
208 208
diff --git a/server/helpers/audit-logger.ts b/server/helpers/audit-logger.ts
index 79ef44be1..076b7f11d 100644
--- a/server/helpers/audit-logger.ts
+++ b/server/helpers/audit-logger.ts
@@ -120,7 +120,7 @@ const videoKeysToKeep = [
120 'downloadEnabled' 120 'downloadEnabled'
121] 121]
122class VideoAuditView extends EntityAuditView { 122class VideoAuditView extends EntityAuditView {
123 constructor (private readonly video: VideoDetails) { 123 constructor (video: VideoDetails) {
124 super(videoKeysToKeep, 'video', video) 124 super(videoKeysToKeep, 'video', video)
125 } 125 }
126} 126}
@@ -131,7 +131,7 @@ const videoImportKeysToKeep = [
131 'video-name' 131 'video-name'
132] 132]
133class VideoImportAuditView extends EntityAuditView { 133class VideoImportAuditView extends EntityAuditView {
134 constructor (private readonly videoImport: VideoImport) { 134 constructor (videoImport: VideoImport) {
135 super(videoImportKeysToKeep, 'video-import', videoImport) 135 super(videoImportKeysToKeep, 'video-import', videoImport)
136 } 136 }
137} 137}
@@ -150,7 +150,7 @@ const commentKeysToKeep = [
150 'account-name' 150 'account-name'
151] 151]
152class CommentAuditView extends EntityAuditView { 152class CommentAuditView extends EntityAuditView {
153 constructor (private readonly comment: VideoComment) { 153 constructor (comment: VideoComment) {
154 super(commentKeysToKeep, 'comment', comment) 154 super(commentKeysToKeep, 'comment', comment)
155 } 155 }
156} 156}
@@ -179,7 +179,7 @@ const userKeysToKeep = [
179 'videoChannels' 179 'videoChannels'
180] 180]
181class UserAuditView extends EntityAuditView { 181class UserAuditView extends EntityAuditView {
182 constructor (private readonly user: User) { 182 constructor (user: User) {
183 super(userKeysToKeep, 'user', user) 183 super(userKeysToKeep, 'user', user)
184 } 184 }
185} 185}
@@ -205,7 +205,7 @@ const channelKeysToKeep = [
205 'ownerAccount-displayedName' 205 'ownerAccount-displayedName'
206] 206]
207class VideoChannelAuditView extends EntityAuditView { 207class VideoChannelAuditView extends EntityAuditView {
208 constructor (private readonly channel: VideoChannel) { 208 constructor (channel: VideoChannel) {
209 super(channelKeysToKeep, 'channel', channel) 209 super(channelKeysToKeep, 'channel', channel)
210 } 210 }
211} 211}
@@ -217,7 +217,7 @@ const abuseKeysToKeep = [
217 'createdAt' 217 'createdAt'
218] 218]
219class AbuseAuditView extends EntityAuditView { 219class AbuseAuditView extends EntityAuditView {
220 constructor (private readonly abuse: AdminAbuse) { 220 constructor (abuse: AdminAbuse) {
221 super(abuseKeysToKeep, 'abuse', abuse) 221 super(abuseKeysToKeep, 'abuse', abuse)
222 } 222 }
223} 223}
diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts
index 0ec45eb2e..6ebe8e2ac 100644
--- a/server/helpers/core-utils.ts
+++ b/server/helpers/core-utils.ts
@@ -56,6 +56,7 @@ const timeTable = {
56export function parseDurationToMs (duration: number | string): number { 56export function parseDurationToMs (duration: number | string): number {
57 if (duration === null) return null 57 if (duration === null) return null
58 if (typeof duration === 'number') return duration 58 if (typeof duration === 'number') return duration
59 if (!isNaN(+duration)) return +duration
59 60
60 if (typeof duration === 'string') { 61 if (typeof duration === 'string') {
61 const split = duration.match(/^([\d.,]+)\s?(\w+)$/) 62 const split = duration.match(/^([\d.,]+)\s?(\w+)$/)
@@ -76,6 +77,7 @@ export function parseDurationToMs (duration: number | string): number {
76 77
77export function parseBytes (value: string | number): number { 78export function parseBytes (value: string | number): number {
78 if (typeof value === 'number') return value 79 if (typeof value === 'number') return value
80 if (!isNaN(+value)) return +value
79 81
80 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/ 82 const tgm = /^(\d+)\s*TB\s*(\d+)\s*GB\s*(\d+)\s*MB$/
81 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/ 83 const tg = /^(\d+)\s*TB\s*(\d+)\s*GB$/
@@ -85,40 +87,55 @@ export function parseBytes (value: string | number): number {
85 const g = /^(\d+)\s*GB$/ 87 const g = /^(\d+)\s*GB$/
86 const m = /^(\d+)\s*MB$/ 88 const m = /^(\d+)\s*MB$/
87 const b = /^(\d+)\s*B$/ 89 const b = /^(\d+)\s*B$/
88 let match 90
91 let match: RegExpMatchArray
89 92
90 if (value.match(tgm)) { 93 if (value.match(tgm)) {
91 match = value.match(tgm) 94 match = value.match(tgm)
92 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + 95 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
93 parseInt(match[2], 10) * 1024 * 1024 * 1024 + 96 parseInt(match[2], 10) * 1024 * 1024 * 1024 +
94 parseInt(match[3], 10) * 1024 * 1024 97 parseInt(match[3], 10) * 1024 * 1024
95 } else if (value.match(tg)) { 98 }
99
100 if (value.match(tg)) {
96 match = value.match(tg) 101 match = value.match(tg)
97 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + 102 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
98 parseInt(match[2], 10) * 1024 * 1024 * 1024 103 parseInt(match[2], 10) * 1024 * 1024 * 1024
99 } else if (value.match(tm)) { 104 }
105
106 if (value.match(tm)) {
100 match = value.match(tm) 107 match = value.match(tm)
101 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 + 108 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 +
102 parseInt(match[2], 10) * 1024 * 1024 109 parseInt(match[2], 10) * 1024 * 1024
103 } else if (value.match(gm)) { 110 }
111
112 if (value.match(gm)) {
104 match = value.match(gm) 113 match = value.match(gm)
105 return parseInt(match[1], 10) * 1024 * 1024 * 1024 + 114 return parseInt(match[1], 10) * 1024 * 1024 * 1024 +
106 parseInt(match[2], 10) * 1024 * 1024 115 parseInt(match[2], 10) * 1024 * 1024
107 } else if (value.match(t)) { 116 }
117
118 if (value.match(t)) {
108 match = value.match(t) 119 match = value.match(t)
109 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024 120 return parseInt(match[1], 10) * 1024 * 1024 * 1024 * 1024
110 } else if (value.match(g)) { 121 }
122
123 if (value.match(g)) {
111 match = value.match(g) 124 match = value.match(g)
112 return parseInt(match[1], 10) * 1024 * 1024 * 1024 125 return parseInt(match[1], 10) * 1024 * 1024 * 1024
113 } else if (value.match(m)) { 126 }
127
128 if (value.match(m)) {
114 match = value.match(m) 129 match = value.match(m)
115 return parseInt(match[1], 10) * 1024 * 1024 130 return parseInt(match[1], 10) * 1024 * 1024
116 } else if (value.match(b)) { 131 }
132
133 if (value.match(b)) {
117 match = value.match(b) 134 match = value.match(b)
118 return parseInt(match[1], 10) * 1024 135 return parseInt(match[1], 10) * 1024
119 } else {
120 return parseInt(value, 10)
121 } 136 }
137
138 return parseInt(value, 10)
122} 139}
123 140
124// --------------------------------------------------------------------------- 141// ---------------------------------------------------------------------------
diff --git a/server/helpers/image-utils.ts b/server/helpers/image-utils.ts
index 7d6451db9..ebb102a0d 100644
--- a/server/helpers/image-utils.ts
+++ b/server/helpers/image-utils.ts
@@ -114,7 +114,7 @@ async function autoResize (options: {
114}) { 114}) {
115 const { sourceImage, newSize, destination } = options 115 const { sourceImage, newSize, destination } = options
116 116
117 // Portrait mode targetting a landscape, apply some effect on the image 117 // Portrait mode targeting a landscape, apply some effect on the image
118 const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight() 118 const sourceIsPortrait = sourceImage.getWidth() < sourceImage.getHeight()
119 const destIsPortraitOrSquare = newSize.width <= newSize.height 119 const destIsPortraitOrSquare = newSize.width <= newSize.height
120 120
diff --git a/server/helpers/upload.ts b/server/helpers/upload.ts
index c94c7ab82..3cb17edd0 100644
--- a/server/helpers/upload.ts
+++ b/server/helpers/upload.ts
@@ -1,5 +1,4 @@
1import { join } from 'path' 1import { join } from 'path'
2import { JobQueue } from '@server/lib/job-queue'
3import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants' 2import { RESUMABLE_UPLOAD_DIRECTORY } from '../initializers/constants'
4 3
5function getResumableUploadPath (filename?: string) { 4function getResumableUploadPath (filename?: string) {
@@ -8,14 +7,8 @@ function getResumableUploadPath (filename?: string) {
8 return RESUMABLE_UPLOAD_DIRECTORY 7 return RESUMABLE_UPLOAD_DIRECTORY
9} 8}
10 9
11function scheduleDeleteResumableUploadMetaFile (filepath: string) {
12 const payload = { filepath }
13 JobQueue.Instance.createJob({ type: 'delete-resumable-upload-meta-file', payload }, { delay: 900 * 1000 }) // executed in 15 min
14}
15
16// --------------------------------------------------------------------------- 10// ---------------------------------------------------------------------------
17 11
18export { 12export {
19 getResumableUploadPath, 13 getResumableUploadPath
20 scheduleDeleteResumableUploadMetaFile
21} 14}
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 794303743..359f0c31d 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -31,8 +31,8 @@ function checkMissedConfig () {
31 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 31 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
32 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', 32 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
33 'transcoding.resolutions.2160p', 'video_studio.enabled', 33 'transcoding.resolutions.2160p', 'video_studio.enabled',
34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'auto_blacklist.videos.of_users.enabled', 34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
35 'trending.videos.interval_days', 35 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days',
36 'client.videos.miniature.display_author_avatar', 36 'client.videos.miniature.display_author_avatar',
37 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth', 37 'client.videos.miniature.prefer_author_display_name', 'client.menu.login.redirect_on_single_external_auth',
38 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence', 38 'defaults.publish.download_enabled', 'defaults.publish.comments_enabled', 'defaults.publish.privacy', 'defaults.publish.licence',
@@ -44,6 +44,7 @@ function checkMissedConfig () {
44 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration', 44 'history.videos.max_age', 'views.videos.remote.max_age', 'views.videos.local_buffer_update_interval', 'views.videos.ip_view_expiration',
45 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max', 45 'rates_limit.login.window', 'rates_limit.login.max', 'rates_limit.ask_send_email.window', 'rates_limit.ask_send_email.max',
46 'theme.default', 46 'theme.default',
47 'feeds.videos.count', 'feeds.comments.count',
47 'geo_ip.enabled', 'geo_ip.country.database_url', 48 'geo_ip.enabled', 'geo_ip.country.database_url',
48 'remote_redundancy.videos.accept_from', 49 'remote_redundancy.videos.accept_from',
49 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions', 50 'federation.videos.federate_unlisted', 'federation.videos.cleanup_remote_interactions',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 59a65d6a5..c76a839bc 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -247,6 +247,14 @@ const CONFIG = {
247 } 247 }
248 } 248 }
249 }, 249 },
250 FEEDS: {
251 VIDEOS: {
252 COUNT: config.get<number>('feeds.videos.count')
253 },
254 COMMENTS: {
255 COUNT: config.get<number>('feeds.comments.count')
256 }
257 },
250 ADMIN: { 258 ADMIN: {
251 get EMAIL () { return config.get<string>('admin.email') } 259 get EMAIL () { return config.get<string>('admin.email') }
252 }, 260 },
@@ -349,6 +357,7 @@ const CONFIG = {
349 IMPORT: { 357 IMPORT: {
350 VIDEOS: { 358 VIDEOS: {
351 get CONCURRENCY () { return config.get<number>('import.videos.concurrency') }, 359 get CONCURRENCY () { return config.get<number>('import.videos.concurrency') },
360 get TIMEOUT () { return parseDurationToMs(config.get<string>('import.videos.timeout')) },
352 361
353 HTTP: { 362 HTTP: {
354 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') }, 363 get ENABLED () { return config.get<boolean>('import.videos.http.enabled') },
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 909fffdb6..2d324d1eb 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -58,7 +58,7 @@ const WEBSERVER = {
58 58
59// Sortable columns per schema 59// Sortable columns per schema
60const SORTABLE_COLUMNS = { 60const SORTABLE_COLUMNS = {
61 USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ], 61 ADMIN_USERS: [ 'id', 'username', 'videoQuotaUsed', 'createdAt', 'lastLoginDate', 'role' ],
62 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ], 62 USER_SUBSCRIPTIONS: [ 'id', 'createdAt' ],
63 ACCOUNTS: [ 'createdAt' ], 63 ACCOUNTS: [ 'createdAt' ],
64 JOBS: [ 'createdAt' ], 64 JOBS: [ 'createdAt' ],
@@ -183,7 +183,7 @@ const JOB_TTL: { [id in JobType]: number } = {
183 'video-file-import': 1000 * 3600, // 1 hour 183 'video-file-import': 1000 * 3600, // 1 hour
184 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long 184 'video-transcoding': 1000 * 3600 * 48, // 2 days, transcoding could be long
185 'video-studio-edition': 1000 * 3600 * 10, // 10 hours 185 'video-studio-edition': 1000 * 3600 * 10, // 10 hours
186 'video-import': 1000 * 3600 * 2, // 2 hours 186 'video-import': CONFIG.IMPORT.VIDEOS.TIMEOUT,
187 'email': 60000 * 10, // 10 minutes 187 'email': 60000 * 10, // 10 minutes
188 'actor-keys': 60000 * 20, // 20 minutes 188 'actor-keys': 60000 * 20, // 20 minutes
189 'videos-views-stats': undefined, // Unlimited 189 'videos-views-stats': undefined, // Unlimited
@@ -210,7 +210,7 @@ const CRAWL_REQUEST_CONCURRENCY = 1 // How many requests in parallel to fetch re
210 210
211const AP_CLEANER = { 211const AP_CLEANER = {
212 CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job 212 CONCURRENCY: 10, // How many requests in parallel we do in activitypub-cleaner job
213 UNAVAILABLE_TRESHOLD: 3, // How many attemps we do before removing an unavailable remote resource 213 UNAVAILABLE_TRESHOLD: 3, // How many attempts we do before removing an unavailable remote resource
214 PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS 214 PERIOD: parseDurationToMs('1 week') // /!\ Has to be sync with REPEAT_JOBS
215} 215}
216 216
@@ -766,12 +766,6 @@ const CUSTOM_HTML_TAG_COMMENTS = {
766 SERVER_CONFIG: '<!-- server config -->' 766 SERVER_CONFIG: '<!-- server config -->'
767} 767}
768 768
769// ---------------------------------------------------------------------------
770
771const FEEDS = {
772 COUNT: 20
773}
774
775const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000 769const MAX_LOGS_OUTPUT_CHARACTERS = 10 * 1000 * 1000
776const LOG_FILENAME = 'peertube.log' 770const LOG_FILENAME = 'peertube.log'
777const AUDIT_LOG_FILENAME = 'peertube-audit.log' 771const AUDIT_LOG_FILENAME = 'peertube-audit.log'
@@ -939,7 +933,6 @@ export {
939 ROUTE_CACHE_LIFETIME, 933 ROUTE_CACHE_LIFETIME,
940 SORTABLE_COLUMNS, 934 SORTABLE_COLUMNS,
941 HLS_STREAMING_PLAYLIST_DIRECTORY, 935 HLS_STREAMING_PLAYLIST_DIRECTORY,
942 FEEDS,
943 JOB_TTL, 936 JOB_TTL,
944 DEFAULT_THEME_NAME, 937 DEFAULT_THEME_NAME,
945 NSFW_POLICY_TYPES, 938 NSFW_POLICY_TYPES,
diff --git a/server/lib/activitypub/process/process-create.ts b/server/lib/activitypub/process/process-create.ts
index 3e7931bb2..76ed37aae 100644
--- a/server/lib/activitypub/process/process-create.ts
+++ b/server/lib/activitypub/process/process-create.ts
@@ -124,7 +124,7 @@ async function processCreateVideoComment (activity: ActivityCreate, byActor: MAc
124 return 124 return
125 } 125 }
126 126
127 // Try to not forward unwanted commments on our videos 127 // Try to not forward unwanted comments on our videos
128 if (video.isOwned()) { 128 if (video.isOwned()) {
129 if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) { 129 if (await isBlockedByServerOrAccount(comment.Account, video.VideoChannel.Account)) {
130 logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url) 130 logger.info('Skip comment forward from blocked account or server %s.', comment.Account.Actor.url)
diff --git a/server/lib/activitypub/videos/shared/creator.ts b/server/lib/activitypub/videos/shared/creator.ts
index 688bcbb53..07252fea2 100644
--- a/server/lib/activitypub/videos/shared/creator.ts
+++ b/server/lib/activitypub/videos/shared/creator.ts
@@ -24,7 +24,7 @@ export class APVideoCreator extends APVideoAbstractBuilder {
24 const channel = channelActor.VideoChannel 24 const channel = channelActor.VideoChannel
25 25
26 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to) 26 const videoData = getVideoAttributesFromObject(channel, this.videoObject, this.videoObject.to)
27 const video = VideoModel.build(videoData) as MVideoThumbnail 27 const video = VideoModel.build({ ...videoData, likes: 0, dislikes: 0 }) as MVideoThumbnail
28 28
29 const promiseThumbnail = this.tryToGenerateThumbnail(video) 29 const promiseThumbnail = this.tryToGenerateThumbnail(video)
30 30
diff --git a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
index f02b9cba6..86699c5b8 100644
--- a/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
+++ b/server/lib/activitypub/videos/shared/object-to-model-attributes.ts
@@ -210,8 +210,6 @@ function getVideoAttributesFromObject (videoChannel: MChannelId, videoObject: Vi
210 210
211 updatedAt: new Date(videoObject.updated), 211 updatedAt: new Date(videoObject.updated),
212 views: videoObject.views, 212 views: videoObject.views,
213 likes: 0,
214 dislikes: 0,
215 remote: true, 213 remote: true,
216 privacy 214 privacy
217 } 215 }
diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts
index 337364ac9..1e8d03023 100644
--- a/server/lib/client-html.ts
+++ b/server/lib/client-html.ts
@@ -30,6 +30,7 @@ import { MAccountActor, MChannelActor } from '../types/models'
30import { getActivityStreamDuration } from './activitypub/activity' 30import { getActivityStreamDuration } from './activitypub/activity'
31import { getBiggestActorImage } from './actor-image' 31import { getBiggestActorImage } from './actor-image'
32import { ServerConfigManager } from './server-config-manager' 32import { ServerConfigManager } from './server-config-manager'
33import { isTestInstance } from '@server/helpers/core-utils'
33 34
34type Tags = { 35type Tags = {
35 ogType: string 36 ogType: string
@@ -232,7 +233,10 @@ class ClientHtml {
232 static async getEmbedHTML () { 233 static async getEmbedHTML () {
233 const path = ClientHtml.getEmbedPath() 234 const path = ClientHtml.getEmbedPath()
234 235
235 if (ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path] 236 // Disable HTML cache in dev mode because webpack can regenerate JS files
237 if (!isTestInstance() && ClientHtml.htmlCache[path]) {
238 return ClientHtml.htmlCache[path]
239 }
236 240
237 const buffer = await readFile(path) 241 const buffer = await readFile(path)
238 const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig() 242 const serverConfig = await ServerConfigManager.Instance.getHTMLServerConfig()
diff --git a/server/lib/emailer.ts b/server/lib/emailer.ts
index aebca04fe..edc99057c 100644
--- a/server/lib/emailer.ts
+++ b/server/lib/emailer.ts
@@ -179,7 +179,7 @@ class Emailer {
179 } 179 }
180 } 180 }
181 181
182 // overriden/new variables given for a specific template in the payload 182 // overridden/new variables given for a specific template in the payload
183 const sendOptions = merge(baseOptions, options) 183 const sendOptions = merge(baseOptions, options)
184 184
185 await email.send(sendOptions) 185 await email.send(sendOptions)
diff --git a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
index daefa25bd..a7292de69 100644
--- a/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
+++ b/server/lib/notifier/shared/abuse/abstract-new-abuse-message.ts
@@ -5,7 +5,7 @@ import { MAbuseFull, MAbuseMessage, MAccountDefault, MUserWithNotificationSettin
5import { UserNotificationType } from '@shared/models' 5import { UserNotificationType } from '@shared/models'
6import { AbstractNotification } from '../common/abstract-notification' 6import { AbstractNotification } from '../common/abstract-notification'
7 7
8export type NewAbuseMessagePayload = { 8type NewAbuseMessagePayload = {
9 abuse: MAbuseFull 9 abuse: MAbuseFull
10 message: MAbuseMessage 10 message: MAbuseMessage
11} 11}
diff --git a/server/lib/redis.ts b/server/lib/redis.ts
index d052de786..158f3c080 100644
--- a/server/lib/redis.ts
+++ b/server/lib/redis.ts
@@ -308,7 +308,7 @@ class Redis {
308 return this.deleteKey('resumable-upload-' + uploadId) 308 return this.deleteKey('resumable-upload-' + uploadId)
309 } 309 }
310 310
311 /* ************ AP ressource unavailability ************ */ 311 /* ************ AP resource unavailability ************ */
312 312
313 async addAPUnavailability (url: string) { 313 async addAPUnavailability (url: string) {
314 const key = this.generateAPUnavailabilityKey(url) 314 const key = this.generateAPUnavailabilityKey(url)
diff --git a/server/lib/schedulers/geo-ip-update-scheduler.ts b/server/lib/schedulers/geo-ip-update-scheduler.ts
index 9dda6d76c..b06f5a9b5 100644
--- a/server/lib/schedulers/geo-ip-update-scheduler.ts
+++ b/server/lib/schedulers/geo-ip-update-scheduler.ts
@@ -6,7 +6,7 @@ export class GeoIPUpdateScheduler extends AbstractScheduler {
6 6
7 private static instance: AbstractScheduler 7 private static instance: AbstractScheduler
8 8
9 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.YOUTUBE_DL_UPDATE 9 protected schedulerIntervalMs = SCHEDULER_INTERVALS_MS.GEO_IP_UPDATE
10 10
11 private constructor () { 11 private constructor () {
12 super() 12 super()
diff --git a/server/lib/signup.ts b/server/lib/signup.ts
index 3c1397a12..f094531eb 100644
--- a/server/lib/signup.ts
+++ b/server/lib/signup.ts
@@ -26,7 +26,7 @@ function isSignupAllowedForCurrentIP (ip: string) {
26 const excludeList = [ 'blacklist' ] 26 const excludeList = [ 'blacklist' ]
27 let matched = '' 27 let matched = ''
28 28
29 // if there is a valid, non-empty whitelist, we exclude all unknown adresses too 29 // if there is a valid, non-empty whitelist, we exclude all unknown addresses too
30 if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) { 30 if (CONFIG.SIGNUP.FILTERS.CIDR.WHITELIST.filter(cidr => isCidr(cidr)).length > 0) {
31 excludeList.push('unknown') 31 excludeList.push('unknown')
32 } 32 }
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index d2ed079b6..b40f864ce 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -4,6 +4,7 @@ export * from './activitypub'
4export * from './async' 4export * from './async'
5export * from './auth' 5export * from './auth'
6export * from './pagination' 6export * from './pagination'
7export * from './rate-limiter'
7export * from './robots' 8export * from './robots'
8export * from './servers' 9export * from './servers'
9export * from './sort' 10export * from './sort'
diff --git a/server/middlewares/rate-limiter.ts b/server/middlewares/rate-limiter.ts
new file mode 100644
index 000000000..bc9513969
--- /dev/null
+++ b/server/middlewares/rate-limiter.ts
@@ -0,0 +1,31 @@
1import { UserRole } from '@shared/models'
2import RateLimit from 'express-rate-limit'
3import { optionalAuthenticate } from './auth'
4
5const whitelistRoles = new Set([ UserRole.ADMINISTRATOR, UserRole.MODERATOR ])
6
7function buildRateLimiter (options: {
8 windowMs: number
9 max: number
10 skipFailedRequests?: boolean
11}) {
12 return RateLimit({
13 windowMs: options.windowMs,
14 max: options.max,
15 skipFailedRequests: options.skipFailedRequests,
16
17 handler: (req, res, next, options) => {
18 return optionalAuthenticate(req, res, () => {
19 if (res.locals.authenticated === true && whitelistRoles.has(res.locals.oauth.token.User.role)) {
20 return next()
21 }
22
23 return res.status(options.statusCode).send(options.message)
24 })
25 }
26 })
27}
28
29export {
30 buildRateLimiter
31}
diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts
index 3ba668460..c9978e3b4 100644
--- a/server/middlewares/validators/sort.ts
+++ b/server/middlewares/validators/sort.ts
@@ -28,7 +28,7 @@ function createSortableColumns (sortableColumns: string[]) {
28 return sortableColumns.concat(sortableColumnDesc) 28 return sortableColumns.concat(sortableColumnDesc)
29} 29}
30 30
31const usersSortValidator = checkSortFactory(SORTABLE_COLUMNS.USERS) 31const adminUsersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ADMIN_USERS)
32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS) 32const accountsSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNTS)
33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ]) 33const jobsSortValidator = checkSortFactory(SORTABLE_COLUMNS.JOBS, [ 'jobs' ])
34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES) 34const abusesSortValidator = checkSortFactory(SORTABLE_COLUMNS.ABUSES)
@@ -59,7 +59,7 @@ const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CH
59// --------------------------------------------------------------------------- 59// ---------------------------------------------------------------------------
60 60
61export { 61export {
62 usersSortValidator, 62 adminUsersSortValidator,
63 abusesSortValidator, 63 abusesSortValidator,
64 videoChannelsSortValidator, 64 videoChannelsSortValidator,
65 videoImportsSortValidator, 65 videoImportsSortValidator,
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index bc6007c6d..6d306121e 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -486,7 +486,7 @@ const ensureAuthUserOwnsAccountValidator = [
486 if (res.locals.account.id !== user.Account.id) { 486 if (res.locals.account.id !== user.Account.id) {
487 return res.fail({ 487 return res.fail({
488 status: HttpStatusCode.FORBIDDEN_403, 488 status: HttpStatusCode.FORBIDDEN_403,
489 message: 'Only owner of this account can access this ressource.' 489 message: 'Only owner of this account can access this resource.'
490 }) 490 })
491 } 491 }
492 492
diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts
index 025e6ba55..cfc924ba4 100644
--- a/server/models/abuse/abuse-query-builder.ts
+++ b/server/models/abuse/abuse-query-builder.ts
@@ -13,7 +13,7 @@ export type BuildAbusesQueryOptions = {
13 searchReporter?: string 13 searchReporter?: string
14 searchReportee?: string 14 searchReportee?: string
15 15
16 // video releated 16 // video related
17 searchVideo?: string 17 searchVideo?: string
18 searchVideoChannel?: string 18 searchVideoChannel?: string
19 videoIs?: AbuseVideoIs 19 videoIs?: AbuseVideoIs
diff --git a/server/models/shared/abstract-run-query.ts b/server/models/shared/abstract-run-query.ts
index f1182c7be..7f27a0c4b 100644
--- a/server/models/shared/abstract-run-query.ts
+++ b/server/models/shared/abstract-run-query.ts
@@ -2,7 +2,7 @@ import { QueryTypes, Sequelize, Transaction } from 'sequelize'
2 2
3/** 3/**
4 * 4 *
5 * Abstact builder to run video SQL queries 5 * Abstract builder to run video SQL queries
6 * 6 *
7 */ 7 */
8 8
diff --git a/server/models/user/user.ts b/server/models/user/user.ts
index 326b2e789..20c2222a7 100644
--- a/server/models/user/user.ts
+++ b/server/models/user/user.ts
@@ -66,7 +66,7 @@ import { ActorModel } from '../actor/actor'
66import { ActorFollowModel } from '../actor/actor-follow' 66import { ActorFollowModel } from '../actor/actor-follow'
67import { ActorImageModel } from '../actor/actor-image' 67import { ActorImageModel } from '../actor/actor-image'
68import { OAuthTokenModel } from '../oauth/oauth-token' 68import { OAuthTokenModel } from '../oauth/oauth-token'
69import { getSort, throwIfNotValid } from '../utils' 69import { getAdminUsersSort, throwIfNotValid } from '../utils'
70import { VideoModel } from '../video/video' 70import { VideoModel } from '../video/video'
71import { VideoChannelModel } from '../video/video-channel' 71import { VideoChannelModel } from '../video/video-channel'
72import { VideoImportModel } from '../video/video-import' 72import { VideoImportModel } from '../video/video-import'
@@ -461,7 +461,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
461 return this.count() 461 return this.count()
462 } 462 }
463 463
464 static listForApi (parameters: { 464 static listForAdminApi (parameters: {
465 start: number 465 start: number
466 count: number 466 count: number
467 sort: string 467 sort: string
@@ -497,7 +497,7 @@ export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
497 const query: FindOptions = { 497 const query: FindOptions = {
498 offset: start, 498 offset: start,
499 limit: count, 499 limit: count,
500 order: getSort(sort), 500 order: getAdminUsersSort(sort),
501 where 501 where
502 } 502 }
503 503
diff --git a/server/models/utils.ts b/server/models/utils.ts
index b57290aff..c468f748d 100644
--- a/server/models/utils.ts
+++ b/server/models/utils.ts
@@ -11,8 +11,6 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
11 11
12 if (field.toLowerCase() === 'match') { // Search 12 if (field.toLowerCase() === 'match') { // Search
13 finalField = Sequelize.col('similarity') 13 finalField = Sequelize.col('similarity')
14 } else if (field === 'videoQuotaUsed') { // Users list
15 finalField = Sequelize.col('videoQuotaUsed')
16 } else { 14 } else {
17 finalField = field 15 finalField = field
18 } 16 }
@@ -20,6 +18,25 @@ function getSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderIt
20 return [ [ finalField, direction ], lastSort ] 18 return [ [ finalField, direction ], lastSort ]
21} 19}
22 20
21function getAdminUsersSort (value: string): OrderItem[] {
22 const { direction, field } = buildDirectionAndField(value)
23
24 let finalField: string | ReturnType<typeof Sequelize.col>
25
26 if (field === 'videoQuotaUsed') { // Users list
27 finalField = Sequelize.col('videoQuotaUsed')
28 } else {
29 finalField = field
30 }
31
32 const nullPolicy = direction === 'ASC'
33 ? 'NULLS FIRST'
34 : 'NULLS LAST'
35
36 // FIXME: typings
37 return [ [ finalField as any, direction, nullPolicy ], [ 'id', 'ASC' ] ]
38}
39
23function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] { 40function getPlaylistSort (value: string, lastSort: OrderItem = [ 'id', 'ASC' ]): OrderItem[] {
24 const { direction, field } = buildDirectionAndField(value) 41 const { direction, field } = buildDirectionAndField(value)
25 42
@@ -102,7 +119,7 @@ function getInstanceFollowsSort (value: string, lastSort: OrderItem = [ 'id', 'A
102 119
103function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) { 120function isOutdated (model: { createdAt: Date, updatedAt: Date }, refreshInterval: number) {
104 if (!model.createdAt || !model.updatedAt) { 121 if (!model.createdAt || !model.updatedAt) {
105 throw new Error('Miss createdAt & updatedAt attribuets to model') 122 throw new Error('Miss createdAt & updatedAt attributes to model')
106 } 123 }
107 124
108 const now = Date.now() 125 const now = Date.now()
@@ -260,6 +277,7 @@ export {
260 buildLocalAccountIdsIn, 277 buildLocalAccountIdsIn,
261 getSort, 278 getSort,
262 getCommentSort, 279 getCommentSort,
280 getAdminUsersSort,
263 getVideoSort, 281 getVideoSort,
264 getBlacklistSort, 282 getBlacklistSort,
265 createSimilarityAttribute, 283 createSimilarityAttribute,
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index d6dd1b8bb..91dafbcf1 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -311,6 +311,16 @@ export type SummaryOptions = {
311 ')' 311 ')'
312 ), 312 ),
313 'viewsPerDay' 313 'viewsPerDay'
314 ],
315 [
316 literal(
317 '(' +
318 'SELECT COALESCE(SUM("video".views), 0) AS totalViews ' +
319 'FROM "video" ' +
320 'WHERE "video"."channelId" = "VideoChannelModel"."id"' +
321 ')'
322 ),
323 'totalViews'
314 ] 324 ]
315 ] 325 ]
316 } 326 }
@@ -766,6 +776,8 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
766 }) 776 })
767 } 777 }
768 778
779 const totalViews = this.get('totalViews') as number
780
769 const actor = this.Actor.toFormattedJSON() 781 const actor = this.Actor.toFormattedJSON()
770 const videoChannel = { 782 const videoChannel = {
771 id: this.id, 783 id: this.id,
@@ -779,6 +791,7 @@ ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
779 791
780 videosCount, 792 videosCount,
781 viewsPerDay, 793 viewsPerDay,
794 totalViews,
782 795
783 avatars: actor.avatars, 796 avatars: actor.avatars,
784 797
diff --git a/server/tests/api/check-params/abuses.ts b/server/tests/api/check-params/abuses.ts
index c4b051723..bc2cc640f 100644
--- a/server/tests/api/check-params/abuses.ts
+++ b/server/tests/api/check-params/abuses.ts
@@ -269,7 +269,7 @@ describe('Test abuses API validators', function () {
269 await makePostBodyRequest({ url: server.url, path, token: userToken, fields }) 269 await makePostBodyRequest({ url: server.url, path, token: userToken, fields })
270 }) 270 })
271 271
272 it('Should succeed with the corret parameters (advanced)', async function () { 272 it('Should succeed with the correct parameters (advanced)', async function () {
273 const fields: AbuseCreate = { 273 const fields: AbuseCreate = {
274 video: { 274 video: {
275 id: server.store.videoCreated.id, 275 id: server.store.videoCreated.id,
@@ -333,7 +333,7 @@ describe('Test abuses API validators', function () {
333 await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 333 await command.addMessage({ token: userToken, abuseId, message: 'a'.repeat(5000), expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
334 }) 334 })
335 335
336 it('Should suceed with the correct params', async function () { 336 it('Should succeed with the correct params', async function () {
337 const res = await command.addMessage({ token: userToken, abuseId, message }) 337 const res = await command.addMessage({ token: userToken, abuseId, message })
338 messageId = res.body.abuseMessage.id 338 messageId = res.body.abuseMessage.id
339 }) 339 })
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts
index 9b8fbe3e2..c497f7840 100644
--- a/server/tests/api/live/live.ts
+++ b/server/tests/api/live/live.ts
@@ -654,7 +654,7 @@ describe('Test live', function () {
654 }) 654 })
655 655
656 it('Should save a non permanent live replay', async function () { 656 it('Should save a non permanent live replay', async function () {
657 this.timeout(120000) 657 this.timeout(240000)
658 658
659 await commands[0].waitUntilPublished({ videoId: liveVideoReplayId }) 659 await commands[0].waitUntilPublished({ videoId: liveVideoReplayId })
660 660
diff --git a/server/tests/api/moderation/abuses.ts b/server/tests/api/moderation/abuses.ts
index 7bf49c7ec..568fbefcf 100644
--- a/server/tests/api/moderation/abuses.ts
+++ b/server/tests/api/moderation/abuses.ts
@@ -168,7 +168,7 @@ describe('Test abuses', function () {
168 expect(abuse2.reporterAccount.name).to.equal('root') 168 expect(abuse2.reporterAccount.name).to.equal('root')
169 expect(abuse2.reporterAccount.host).to.equal(servers[0].host) 169 expect(abuse2.reporterAccount.host).to.equal(servers[0].host)
170 170
171 expect(abuse2.video.id).to.equal(servers[1].store.videoCreated.id) 171 expect(abuse2.video.uuid).to.equal(servers[1].store.videoCreated.uuid)
172 172
173 expect(abuse2.comment).to.be.null 173 expect(abuse2.comment).to.be.null
174 174
@@ -530,7 +530,7 @@ describe('Test abuses', function () {
530 it('Should keep the comment abuse when deleting the comment', async function () { 530 it('Should keep the comment abuse when deleting the comment', async function () {
531 this.timeout(10000) 531 this.timeout(10000)
532 532
533 const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.id) 533 const commentServer2 = await getComment(servers[0], servers[1].store.videoCreated.uuid)
534 534
535 await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id }) 535 await servers[0].comments.delete({ videoId: servers[1].store.videoCreated.uuid, commentId: commentServer2.id })
536 536
diff --git a/server/tests/api/notifications/user-notifications.ts b/server/tests/api/notifications/user-notifications.ts
index a7cc529f8..a11289236 100644
--- a/server/tests/api/notifications/user-notifications.ts
+++ b/server/tests/api/notifications/user-notifications.ts
@@ -545,7 +545,7 @@ describe('Test user notifications', function () {
545 await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port }) 545 await servers[1].subscriptions.remove({ uri: 'user_1_channel@localhost:' + servers[0].port })
546 }) 546 })
547 547
548 // PeerTube does not support accout -> account follows 548 // PeerTube does not support account -> account follows
549 // it('Should notify when a local account is following one of our channel', async function () { 549 // it('Should notify when a local account is following one of our channel', async function () {
550 // this.timeout(50000) 550 // this.timeout(50000)
551 // 551 //
diff --git a/server/tests/api/search/search-index.ts b/server/tests/api/search/search-index.ts
index 287abe455..53b91e1cb 100644
--- a/server/tests/api/search/search-index.ts
+++ b/server/tests/api/search/search-index.ts
@@ -136,14 +136,14 @@ describe('Test index search', function () {
136 expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft') 136 expect(video.account.url).to.equal('https://framatube.org/accounts/framasoft')
137 // TODO: remove, deprecated in 4.2 137 // TODO: remove, deprecated in 4.2
138 expect(video.account.avatar).to.exist 138 expect(video.account.avatar).to.exist
139 expect(video.account.avatars.length).to.equal(1, 'Account should have one avatar image') 139 expect(video.account.avatars.length).to.equal(2, 'Account should have one avatar image')
140 140
141 expect(video.channel.host).to.equal('framatube.org') 141 expect(video.channel.host).to.equal('framatube.org')
142 expect(video.channel.name).to.equal('joinpeertube') 142 expect(video.channel.name).to.equal('joinpeertube')
143 expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube') 143 expect(video.channel.url).to.equal('https://framatube.org/video-channels/joinpeertube')
144 // TODO: remove, deprecated in 4.2 144 // TODO: remove, deprecated in 4.2
145 expect(video.channel.avatar).to.exist 145 expect(video.channel.avatar).to.exist
146 expect(video.channel.avatars.length).to.equal(1, 'Channel should have one avatar image') 146 expect(video.channel.avatars.length).to.equal(2, 'Channel should have one avatar image')
147 } 147 }
148 148
149 const baseSearch: VideosSearchQuery = { 149 const baseSearch: VideosSearchQuery = {
@@ -322,7 +322,7 @@ describe('Test index search', function () {
322 expect(videoChannel.host).to.equal('framatube.org') 322 expect(videoChannel.host).to.equal('framatube.org')
323 // TODO: remove, deprecated in 4.2 323 // TODO: remove, deprecated in 4.2
324 expect(videoChannel.avatar).to.exist 324 expect(videoChannel.avatar).to.exist
325 expect(videoChannel.avatars.length).to.equal(1, 'Channel should have two avatar images') 325 expect(videoChannel.avatars.length).to.equal(2, 'Channel should have two avatar images')
326 expect(videoChannel.displayName).to.exist 326 expect(videoChannel.displayName).to.exist
327 327
328 expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft') 328 expect(videoChannel.ownerAccount.url).to.equal('https://framatube.org/accounts/framasoft')
@@ -330,7 +330,7 @@ describe('Test index search', function () {
330 expect(videoChannel.ownerAccount.host).to.equal('framatube.org') 330 expect(videoChannel.ownerAccount.host).to.equal('framatube.org')
331 // TODO: remove, deprecated in 4.2 331 // TODO: remove, deprecated in 4.2
332 expect(videoChannel.ownerAccount.avatar).to.exist 332 expect(videoChannel.ownerAccount.avatar).to.exist
333 expect(videoChannel.ownerAccount.avatars.length).to.equal(1, 'Account should have two avatar images') 333 expect(videoChannel.ownerAccount.avatars.length).to.equal(2, 'Account should have two avatar images')
334 } 334 }
335 335
336 it('Should make a simple search and not have results', async function () { 336 it('Should make a simple search and not have results', async function () {
diff --git a/server/tests/api/server/contact-form.ts b/server/tests/api/server/contact-form.ts
index 4f01f6fd5..d6165b293 100644
--- a/server/tests/api/server/contact-form.ts
+++ b/server/tests/api/server/contact-form.ts
@@ -61,7 +61,7 @@ describe('Test contact form', function () {
61 expect(email['text']).contains('my super message') 61 expect(email['text']).contains('my super message')
62 }) 62 })
63 63
64 it('Should not have duplicated email adress in text message', async function () { 64 it('Should not have duplicated email address in text message', async function () {
65 const text = emails[0]['text'] as string 65 const text = emails[0]['text'] as string
66 66
67 const matches = text.match(/toto@example.com/g) 67 const matches = text.match(/toto@example.com/g)
diff --git a/server/tests/api/server/reverse-proxy.ts b/server/tests/api/server/reverse-proxy.ts
index fa2063536..0a1565faf 100644
--- a/server/tests/api/server/reverse-proxy.ts
+++ b/server/tests/api/server/reverse-proxy.ts
@@ -7,6 +7,7 @@ import { cleanupTests, createSingleServer, PeerTubeServer, setAccessTokensToServ
7 7
8describe('Test application behind a reverse proxy', function () { 8describe('Test application behind a reverse proxy', function () {
9 let server: PeerTubeServer 9 let server: PeerTubeServer
10 let userAccessToken: string
10 let videoId: string 11 let videoId: string
11 12
12 before(async function () { 13 before(async function () {
@@ -34,6 +35,8 @@ describe('Test application behind a reverse proxy', function () {
34 server = await createSingleServer(1, config) 35 server = await createSingleServer(1, config)
35 await setAccessTokensToServers([ server ]) 36 await setAccessTokensToServers([ server ])
36 37
38 userAccessToken = await server.users.generateUserAndToken('user')
39
37 const { uuid } = await server.videos.upload() 40 const { uuid } = await server.videos.upload()
38 videoId = uuid 41 videoId = uuid
39 }) 42 })
@@ -93,7 +96,7 @@ describe('Test application behind a reverse proxy', function () {
93 it('Should rate limit logins', async function () { 96 it('Should rate limit logins', async function () {
94 const user = { username: 'root', password: 'fail' } 97 const user = { username: 'root', password: 'fail' }
95 98
96 for (let i = 0; i < 19; i++) { 99 for (let i = 0; i < 18; i++) {
97 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 100 await server.login.login({ user, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
98 } 101 }
99 102
@@ -141,6 +144,12 @@ describe('Test application behind a reverse proxy', function () {
141 await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 }) 144 await server.videos.get({ id: videoId, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
142 }) 145 })
143 146
147 it('Should rate limit API calls with a user but not with an admin', async function () {
148 await server.videos.get({ id: videoId, token: userAccessToken, expectedStatus: HttpStatusCode.TOO_MANY_REQUESTS_429 })
149
150 await server.videos.get({ id: videoId, token: server.accessToken, expectedStatus: HttpStatusCode.OK_200 })
151 })
152
144 after(async function () { 153 after(async function () {
145 await cleanupTests([ server ]) 154 await cleanupTests([ server ])
146 }) 155 })
diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts
index 6f495c42d..42e0cf431 100644
--- a/server/tests/api/videos/video-channels.ts
+++ b/server/tests/api/videos/video-channels.ts
@@ -478,6 +478,25 @@ describe('Test video channels', function () {
478 } 478 }
479 }) 479 })
480 480
481 it('Should report correct total views count', async function () {
482 // check if there's the property
483 {
484 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
485
486 for (const channel of data) {
487 expect(channel).to.haveOwnProperty('totalViews')
488 expect(channel.totalViews).to.be.a('number')
489 }
490 }
491
492 // Check if the totalViews count can be updated
493 {
494 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
495 const channelWithView = data.find(channel => channel.id === servers[0].store.channel.id)
496 expect(channelWithView.totalViews).to.equal(2)
497 }
498 })
499
481 it('Should report correct videos count', async function () { 500 it('Should report correct videos count', async function () {
482 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true }) 501 const { data } = await servers[0].channels.listByAccount({ accountName, withStats: true })
483 502
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts
index fa0a71341..e3e5eb45c 100644
--- a/server/tests/helpers/core-utils.ts
+++ b/server/tests/helpers/core-utils.ts
@@ -6,47 +6,64 @@ import { snakeCase } from 'lodash'
6import validator from 'validator' 6import validator from 'validator'
7import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils' 7import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils'
8import { VideoResolution } from '@shared/models' 8import { VideoResolution } from '@shared/models'
9import { objectConverter, parseBytes } from '../../helpers/core-utils' 9import { objectConverter, parseBytes, parseDurationToMs } from '../../helpers/core-utils'
10 10
11const expect = chai.expect 11const expect = chai.expect
12 12
13describe('Parse Bytes', function () { 13describe('Parse Bytes', function () {
14 14
15 it('Should pass when given valid value', async function () { 15 it('Should pass on valid value', async function () {
16 // just return it 16 // just return it
17 expect(parseBytes(1024)).to.be.eq(1024) 17 expect(parseBytes(-1024)).to.equal(-1024)
18 expect(parseBytes(1048576)).to.be.eq(1048576) 18 expect(parseBytes(1024)).to.equal(1024)
19 expect(parseBytes('1024')).to.be.eq(1024) 19 expect(parseBytes(1048576)).to.equal(1048576)
20 expect(parseBytes('1048576')).to.be.eq(1048576) 20 expect(parseBytes('1024')).to.equal(1024)
21 expect(parseBytes('1048576')).to.equal(1048576)
21 22
22 // sizes 23 // sizes
23 expect(parseBytes('1B')).to.be.eq(1024) 24 expect(parseBytes('1B')).to.equal(1024)
24 expect(parseBytes('1MB')).to.be.eq(1048576) 25 expect(parseBytes('1MB')).to.equal(1048576)
25 expect(parseBytes('1GB')).to.be.eq(1073741824) 26 expect(parseBytes('1GB')).to.equal(1073741824)
26 expect(parseBytes('1TB')).to.be.eq(1099511627776) 27 expect(parseBytes('1TB')).to.equal(1099511627776)
27 28
28 expect(parseBytes('5GB')).to.be.eq(5368709120) 29 expect(parseBytes('5GB')).to.equal(5368709120)
29 expect(parseBytes('5TB')).to.be.eq(5497558138880) 30 expect(parseBytes('5TB')).to.equal(5497558138880)
30 31
31 expect(parseBytes('1024B')).to.be.eq(1048576) 32 expect(parseBytes('1024B')).to.equal(1048576)
32 expect(parseBytes('1024MB')).to.be.eq(1073741824) 33 expect(parseBytes('1024MB')).to.equal(1073741824)
33 expect(parseBytes('1024GB')).to.be.eq(1099511627776) 34 expect(parseBytes('1024GB')).to.equal(1099511627776)
34 expect(parseBytes('1024TB')).to.be.eq(1125899906842624) 35 expect(parseBytes('1024TB')).to.equal(1125899906842624)
35 36
36 // with whitespace 37 // with whitespace
37 expect(parseBytes('1 GB')).to.be.eq(1073741824) 38 expect(parseBytes('1 GB')).to.equal(1073741824)
38 expect(parseBytes('1\tGB')).to.be.eq(1073741824) 39 expect(parseBytes('1\tGB')).to.equal(1073741824)
39 40
40 // sum value 41 // sum value
41 expect(parseBytes('1TB 1024MB')).to.be.eq(1100585369600) 42 expect(parseBytes('1TB 1024MB')).to.equal(1100585369600)
42 expect(parseBytes('4GB 1024MB')).to.be.eq(5368709120) 43 expect(parseBytes('4GB 1024MB')).to.equal(5368709120)
43 expect(parseBytes('4TB 1024GB')).to.be.eq(5497558138880) 44 expect(parseBytes('4TB 1024GB')).to.equal(5497558138880)
44 expect(parseBytes('4TB 1024GB 0MB')).to.be.eq(5497558138880) 45 expect(parseBytes('4TB 1024GB 0MB')).to.equal(5497558138880)
45 expect(parseBytes('1024TB 1024GB 1024MB')).to.be.eq(1127000492212224) 46 expect(parseBytes('1024TB 1024GB 1024MB')).to.equal(1127000492212224)
47 })
48
49 it('Should be invalid when given invalid value', async function () {
50 expect(parseBytes('6GB 1GB')).to.equal(6)
51 })
52})
53
54describe('Parse duration', function () {
55
56 it('Should pass when given valid value', async function () {
57 expect(parseDurationToMs(35)).to.equal(35)
58 expect(parseDurationToMs(-35)).to.equal(-35)
59 expect(parseDurationToMs('35 seconds')).to.equal(35 * 1000)
60 expect(parseDurationToMs('1 minute')).to.equal(60 * 1000)
61 expect(parseDurationToMs('1 hour')).to.equal(3600 * 1000)
62 expect(parseDurationToMs('35 hours')).to.equal(3600 * 35 * 1000)
46 }) 63 })
47 64
48 it('Should be invalid when given invalid value', async function () { 65 it('Should be invalid when given invalid value', async function () {
49 expect(parseBytes('6GB 1GB')).to.be.eq(6) 66 expect(parseBytes('35m 5s')).to.equal(35)
50 }) 67 })
51}) 68})
52 69