aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-04-19 11:01:34 +0200
committerChocobozzz <me@florianbigard.com>2018-04-19 11:01:34 +0200
commit0883b3245bf0deb9106c4041e9afbd3521b79280 (patch)
treefcb73005e0b31a3b763ee5d22d5fc39c2da89907 /server
parent04ed10b21e8e1339514faae0bb690e4d97c23b0a (diff)
downloadPeerTube-0883b3245bf0deb9106c4041e9afbd3521b79280.tar.gz
PeerTube-0883b3245bf0deb9106c4041e9afbd3521b79280.tar.zst
PeerTube-0883b3245bf0deb9106c4041e9afbd3521b79280.zip
Add ability to choose what policy we have for NSFW videos
There is a global instance setting and a per user setting
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts3
-rw-r--r--server/controllers/api/users.ts15
-rw-r--r--server/controllers/api/videos/index.ts38
-rw-r--r--server/controllers/feeds.ts9
-rw-r--r--server/helpers/custom-validators/users.ts10
-rw-r--r--server/initializers/checker.ts11
-rw-r--r--server/initializers/constants.ts11
-rw-r--r--server/initializers/installer.ts1
-rw-r--r--server/initializers/migrations/0205-user-nsfw-policy.ts46
-rw-r--r--server/middlewares/oauth.ts10
-rw-r--r--server/middlewares/validators/config.ts3
-rw-r--r--server/middlewares/validators/users.ts4
-rw-r--r--server/models/account/user.ts14
-rw-r--r--server/models/video/video.ts24
-rw-r--r--server/tests/api/check-params/config.ts19
-rw-r--r--server/tests/api/check-params/users.ts6
-rw-r--r--server/tests/api/server/config.ts5
-rw-r--r--server/tests/api/users/users.ts26
-rw-r--r--server/tests/api/videos/video-nsfw.ts197
-rw-r--r--server/tests/utils/users/users.ts5
-rw-r--r--server/tests/utils/videos/videos.ts26
21 files changed, 425 insertions, 58 deletions
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts
index 88f047adc..e47b71f44 100644
--- a/server/controllers/api/config.ts
+++ b/server/controllers/api/config.ts
@@ -46,6 +46,7 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
46 name: CONFIG.INSTANCE.NAME, 46 name: CONFIG.INSTANCE.NAME,
47 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, 47 shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
48 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, 48 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
49 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
49 customizations: { 50 customizations: {
50 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, 51 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
51 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS 52 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
@@ -128,6 +129,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
128 toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota 129 toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
129 toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute 130 toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
130 toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription 131 toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
132 toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
131 133
132 await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2)) 134 await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2))
133 135
@@ -153,6 +155,7 @@ function customConfig (): CustomConfig {
153 description: CONFIG.INSTANCE.DESCRIPTION, 155 description: CONFIG.INSTANCE.DESCRIPTION,
154 terms: CONFIG.INSTANCE.TERMS, 156 terms: CONFIG.INSTANCE.TERMS,
155 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, 157 defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
158 defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
156 customizations: { 159 customizations: {
157 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, 160 css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
158 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT 161 javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts
index 56cbf9448..6540adb1c 100644
--- a/server/controllers/api/users.ts
+++ b/server/controllers/api/users.ts
@@ -42,6 +42,7 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate'
42import { UserModel } from '../../models/account/user' 42import { UserModel } from '../../models/account/user'
43import { OAuthTokenModel } from '../../models/oauth/oauth-token' 43import { OAuthTokenModel } from '../../models/oauth/oauth-token'
44import { VideoModel } from '../../models/video/video' 44import { VideoModel } from '../../models/video/video'
45import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type'
45 46
46const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) 47const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
47const loginRateLimiter = new RateLimit({ 48const loginRateLimiter = new RateLimit({
@@ -161,7 +162,13 @@ export {
161 162
162async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 163async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
163 const user = res.locals.oauth.token.User as UserModel 164 const user = res.locals.oauth.token.User as UserModel
164 const resultList = await VideoModel.listAccountVideosForApi(user.Account.id ,req.query.start, req.query.count, req.query.sort) 165 const resultList = await VideoModel.listAccountVideosForApi(
166 user.Account.id,
167 req.query.start as number,
168 req.query.count as number,
169 req.query.sort as VideoSortField,
170 false // Display my NSFW videos
171 )
165 172
166 return res.json(getFormattedObjects(resultList.data, resultList.total)) 173 return res.json(getFormattedObjects(resultList.data, resultList.total))
167} 174}
@@ -188,7 +195,7 @@ async function createUser (req: express.Request) {
188 username: body.username, 195 username: body.username,
189 password: body.password, 196 password: body.password,
190 email: body.email, 197 email: body.email,
191 displayNSFW: false, 198 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
192 autoPlayVideo: true, 199 autoPlayVideo: true,
193 role: body.role, 200 role: body.role,
194 videoQuota: body.videoQuota 201 videoQuota: body.videoQuota
@@ -219,7 +226,7 @@ async function registerUser (req: express.Request) {
219 username: body.username, 226 username: body.username,
220 password: body.password, 227 password: body.password,
221 email: body.email, 228 email: body.email,
222 displayNSFW: false, 229 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
223 autoPlayVideo: true, 230 autoPlayVideo: true,
224 role: UserRole.USER, 231 role: UserRole.USER,
225 videoQuota: CONFIG.USER.VIDEO_QUOTA 232 videoQuota: CONFIG.USER.VIDEO_QUOTA
@@ -286,7 +293,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
286 293
287 if (body.password !== undefined) user.password = body.password 294 if (body.password !== undefined) user.password = body.password
288 if (body.email !== undefined) user.email = body.email 295 if (body.email !== undefined) user.email = body.email
289 if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW 296 if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
290 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo 297 if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
291 298
292 await sequelizeTypescript.transaction(async t => { 299 await sequelizeTypescript.transaction(async t => {
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index b4cd67158..6e8601fa1 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -19,13 +19,18 @@ import {
19 VIDEO_MIMETYPE_EXT, 19 VIDEO_MIMETYPE_EXT,
20 VIDEO_PRIVACIES 20 VIDEO_PRIVACIES
21} from '../../../initializers' 21} from '../../../initializers'
22import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' 22import {
23 fetchRemoteVideoDescription,
24 getVideoActivityPubUrl,
25 shareVideoByServerAndChannel
26} from '../../../lib/activitypub'
23import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' 27import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
24import { JobQueue } from '../../../lib/job-queue' 28import { JobQueue } from '../../../lib/job-queue'
25import { Redis } from '../../../lib/redis' 29import { Redis } from '../../../lib/redis'
26import { 30import {
27 asyncMiddleware, 31 asyncMiddleware,
28 authenticate, 32 authenticate,
33 optionalAuthenticate,
29 paginationValidator, 34 paginationValidator,
30 setDefaultPagination, 35 setDefaultPagination,
31 setDefaultSort, 36 setDefaultSort,
@@ -44,6 +49,9 @@ import { blacklistRouter } from './blacklist'
44import { videoChannelRouter } from './channel' 49import { videoChannelRouter } from './channel'
45import { videoCommentRouter } from './comment' 50import { videoCommentRouter } from './comment'
46import { rateVideoRouter } from './rate' 51import { rateVideoRouter } from './rate'
52import { User } from '../../../../shared/models/users'
53import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
54import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
47 55
48const videosRouter = express.Router() 56const videosRouter = express.Router()
49 57
@@ -81,6 +89,7 @@ videosRouter.get('/',
81 videosSortValidator, 89 videosSortValidator,
82 setDefaultSort, 90 setDefaultSort,
83 setDefaultPagination, 91 setDefaultPagination,
92 optionalAuthenticate,
84 asyncMiddleware(listVideos) 93 asyncMiddleware(listVideos)
85) 94)
86videosRouter.get('/search', 95videosRouter.get('/search',
@@ -89,6 +98,7 @@ videosRouter.get('/search',
89 videosSortValidator, 98 videosSortValidator,
90 setDefaultSort, 99 setDefaultSort,
91 setDefaultPagination, 100 setDefaultPagination,
101 optionalAuthenticate,
92 asyncMiddleware(searchVideos) 102 asyncMiddleware(searchVideos)
93) 103)
94videosRouter.put('/:id', 104videosRouter.put('/:id',
@@ -391,7 +401,13 @@ async function getVideoDescription (req: express.Request, res: express.Response)
391} 401}
392 402
393async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 403async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
394 const resultList = await VideoModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.filter) 404 const resultList = await VideoModel.listForApi(
405 req.query.start as number,
406 req.query.count as number,
407 req.query.sort as VideoSortField,
408 isNSFWHidden(res),
409 req.query.filter as VideoFilter
410 )
395 411
396 return res.json(getFormattedObjects(resultList.data, resultList.total)) 412 return res.json(getFormattedObjects(resultList.data, resultList.total))
397} 413}
@@ -419,11 +435,21 @@ async function removeVideo (req: express.Request, res: express.Response) {
419 435
420async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { 436async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
421 const resultList = await VideoModel.searchAndPopulateAccountAndServer( 437 const resultList = await VideoModel.searchAndPopulateAccountAndServer(
422 req.query.search, 438 req.query.search as string,
423 req.query.start, 439 req.query.start as number,
424 req.query.count, 440 req.query.count as number,
425 req.query.sort 441 req.query.sort as VideoSortField,
442 isNSFWHidden(res)
426 ) 443 )
427 444
428 return res.json(getFormattedObjects(resultList.data, resultList.total)) 445 return res.json(getFormattedObjects(resultList.data, resultList.total))
429} 446}
447
448function isNSFWHidden (res: express.Response) {
449 if (res.locals.oauth) {
450 const user: User = res.locals.oauth.token.User
451 if (user) return user.nsfwPolicy === 'do_not_list'
452 }
453
454 return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
455}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 3e384c48a..27ebecc40 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -6,6 +6,7 @@ import * as Feed from 'pfeed'
6import { ResultList } from '../../shared/models' 6import { ResultList } from '../../shared/models'
7import { AccountModel } from '../models/account/account' 7import { AccountModel } from '../models/account/account'
8import { cacheRoute } from '../middlewares/cache' 8import { cacheRoute } from '../middlewares/cache'
9import { VideoSortField } from '../../client/src/app/shared/video/sort-field.type'
9 10
10const feedsRouter = express.Router() 11const feedsRouter = express.Router()
11 12
@@ -31,20 +32,22 @@ async function generateFeed (req: express.Request, res: express.Response, next:
31 32
32 let resultList: ResultList<VideoModel> 33 let resultList: ResultList<VideoModel>
33 const account: AccountModel = res.locals.account 34 const account: AccountModel = res.locals.account
35 const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
34 36
35 if (account) { 37 if (account) {
36 resultList = await VideoModel.listAccountVideosForApi( 38 resultList = await VideoModel.listAccountVideosForApi(
37 account.id, 39 account.id,
38 start, 40 start,
39 FEEDS.COUNT, 41 FEEDS.COUNT,
40 req.query.sort, 42 req.query.sort as VideoSortField,
41 true 43 hideNSFW
42 ) 44 )
43 } else { 45 } else {
44 resultList = await VideoModel.listForApi( 46 resultList = await VideoModel.listForApi(
45 start, 47 start,
46 FEEDS.COUNT, 48 FEEDS.COUNT,
47 req.query.sort, 49 req.query.sort as VideoSortField,
50 hideNSFW,
48 req.query.filter, 51 req.query.filter,
49 true 52 true
50 ) 53 )
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts
index bbc7cc199..c0acb8218 100644
--- a/server/helpers/custom-validators/users.ts
+++ b/server/helpers/custom-validators/users.ts
@@ -1,9 +1,10 @@
1import 'express-validator' 1import 'express-validator'
2import * as validator from 'validator' 2import * as validator from 'validator'
3import { UserRole } from '../../../shared' 3import { UserRole } from '../../../shared'
4import { CONSTRAINTS_FIELDS } from '../../initializers' 4import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers'
5 5
6import { exists, isFileValid } from './misc' 6import { exists, isFileValid } from './misc'
7import { values } from 'lodash'
7 8
8const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS 9const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
9 10
@@ -29,8 +30,9 @@ function isBoolean (value: any) {
29 return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) 30 return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
30} 31}
31 32
32function isUserDisplayNSFWValid (value: any) { 33const nsfwPolicies = values(NSFW_POLICY_TYPES)
33 return isBoolean(value) 34function isUserNSFWPolicyValid (value: any) {
35 return exists(value) && nsfwPolicies.indexOf(value) !== -1
34} 36}
35 37
36function isUserAutoPlayVideoValid (value: any) { 38function isUserAutoPlayVideoValid (value: any) {
@@ -56,7 +58,7 @@ export {
56 isUserRoleValid, 58 isUserRoleValid,
57 isUserVideoQuotaValid, 59 isUserVideoQuotaValid,
58 isUserUsernameValid, 60 isUserUsernameValid,
59 isUserDisplayNSFWValid, 61 isUserNSFWPolicyValid,
60 isUserAutoPlayVideoValid, 62 isUserAutoPlayVideoValid,
61 isUserDescriptionValid, 63 isUserDescriptionValid,
62 isAvatarFile 64 isAvatarFile
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts
index 71f303963..739f623c6 100644
--- a/server/initializers/checker.ts
+++ b/server/initializers/checker.ts
@@ -5,12 +5,12 @@ import { ApplicationModel } from '../models/application/application'
5import { OAuthClientModel } from '../models/oauth/oauth-client' 5import { OAuthClientModel } from '../models/oauth/oauth-client'
6 6
7// Some checks on configuration files 7// Some checks on configuration files
8// Return an error message, or null if everything is okay
8function checkConfig () { 9function checkConfig () {
9 if (config.has('webserver.host')) { 10 const defaultNSFWPolicy = config.get<string>('instance.default_nsfw_policy')
10 let errorMessage = '`host` config key was renamed to `hostname` but it seems you still have a `host` key in your configuration files!'
11 errorMessage += ' Please ensure to rename your `host` configuration to `hostname`.'
12 11
13 return errorMessage 12 if ([ 'do_not_list', 'blur', 'display' ].indexOf(defaultNSFWPolicy) === -1) {
13 return 'NSFW policy setting should be "do_not_list" or "blur" or "display" instead of ' + defaultNSFWPolicy
14 } 14 }
15 15
16 return null 16 return null
@@ -28,7 +28,8 @@ function checkMissedConfig () {
28 'log.level', 28 'log.level',
29 'user.video_quota', 29 'user.video_quota',
30 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', 30 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
31 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route' 31 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
32 'instance.default_nsfw_policy'
32 ] 33 ]
33 const miss: string[] = [] 34 const miss: string[] = []
34 35
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 5ee13389d..d1915586a 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -6,13 +6,14 @@ import { FollowState } from '../../shared/models/actors'
6import { VideoPrivacy } from '../../shared/models/videos' 6import { VideoPrivacy } from '../../shared/models/videos'
7// Do not use barrels, remain constants as independent as possible 7// Do not use barrels, remain constants as independent as possible
8import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' 8import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
9import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
9 10
10// Use a variable to reload the configuration if we need 11// Use a variable to reload the configuration if we need
11let config: IConfig = require('config') 12let config: IConfig = require('config')
12 13
13// --------------------------------------------------------------------------- 14// ---------------------------------------------------------------------------
14 15
15const LAST_MIGRATION_VERSION = 200 16const LAST_MIGRATION_VERSION = 205
16 17
17// --------------------------------------------------------------------------- 18// ---------------------------------------------------------------------------
18 19
@@ -167,6 +168,7 @@ const CONFIG = {
167 get DESCRIPTION () { return config.get<string>('instance.description') }, 168 get DESCRIPTION () { return config.get<string>('instance.description') },
168 get TERMS () { return config.get<string>('instance.terms') }, 169 get TERMS () { return config.get<string>('instance.terms') },
169 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') }, 170 get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
171 get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
170 CUSTOMIZATIONS: { 172 CUSTOMIZATIONS: {
171 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') }, 173 get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
172 get CSS () { return config.get<string>('instance.customizations.css') } 174 get CSS () { return config.get<string>('instance.customizations.css') }
@@ -378,6 +380,12 @@ const BCRYPT_SALT_SIZE = 10
378 380
379const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes 381const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
380 382
383const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = {
384 DO_NOT_LIST: 'do_not_list',
385 BLUR: 'blur',
386 DISPLAY: 'display'
387}
388
381// --------------------------------------------------------------------------- 389// ---------------------------------------------------------------------------
382 390
383// Express static paths (router) 391// Express static paths (router)
@@ -474,6 +482,7 @@ export {
474 PRIVATE_RSA_KEY_SIZE, 482 PRIVATE_RSA_KEY_SIZE,
475 SORTABLE_COLUMNS, 483 SORTABLE_COLUMNS,
476 FEEDS, 484 FEEDS,
485 NSFW_POLICY_TYPES,
477 STATIC_MAX_AGE, 486 STATIC_MAX_AGE,
478 STATIC_PATHS, 487 STATIC_PATHS,
479 ACTIVITY_PUB, 488 ACTIVITY_PUB,
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts
index 09c6d5473..b0084b368 100644
--- a/server/initializers/installer.ts
+++ b/server/initializers/installer.ts
@@ -120,6 +120,7 @@ async function createOAuthAdminIfNotExist () {
120 email, 120 email,
121 password, 121 password,
122 role, 122 role,
123 nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
123 videoQuota: -1 124 videoQuota: -1
124 } 125 }
125 const user = new UserModel(userData) 126 const user = new UserModel(userData)
diff --git a/server/initializers/migrations/0205-user-nsfw-policy.ts b/server/initializers/migrations/0205-user-nsfw-policy.ts
new file mode 100644
index 000000000..d0f6e8962
--- /dev/null
+++ b/server/initializers/migrations/0205-user-nsfw-policy.ts
@@ -0,0 +1,46 @@
1import * as Sequelize from 'sequelize'
2
3async function up (utils: {
4 transaction: Sequelize.Transaction,
5 queryInterface: Sequelize.QueryInterface,
6 sequelize: Sequelize.Sequelize
7}): Promise<void> {
8
9 {
10 const data = {
11 type: Sequelize.ENUM('do_not_list', 'blur', 'display'),
12 allowNull: true,
13 defaultValue: null
14 }
15 await utils.queryInterface.addColumn('user', 'nsfwPolicy', data)
16 }
17
18 {
19 const query = 'UPDATE "user" SET "nsfwPolicy" = \'do_not_list\''
20 await utils.sequelize.query(query)
21 }
22
23 {
24 const query = 'UPDATE "user" SET "nsfwPolicy" = \'display\' WHERE "displayNSFW" = true'
25 await utils.sequelize.query(query)
26 }
27
28 {
29 const query = 'ALTER TABLE "user" ALTER COLUMN "nsfwPolicy" SET NOT NULL'
30 await utils.sequelize.query(query)
31 }
32
33 {
34 await utils.queryInterface.removeColumn('user', 'displayNSFW')
35 }
36
37}
38
39function down (options) {
40 throw new Error('Not implemented.')
41}
42
43export {
44 up,
45 down
46}
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts
index 41a3fb718..a6f28dd5b 100644
--- a/server/middlewares/oauth.ts
+++ b/server/middlewares/oauth.ts
@@ -2,6 +2,7 @@ import * as express from 'express'
2import * as OAuthServer from 'express-oauth-server' 2import * as OAuthServer from 'express-oauth-server'
3import 'express-validator' 3import 'express-validator'
4import { OAUTH_LIFETIME } from '../initializers' 4import { OAUTH_LIFETIME } from '../initializers'
5import { logger } from '../helpers/logger'
5 6
6const oAuthServer = new OAuthServer({ 7const oAuthServer = new OAuthServer({
7 useErrorHandler: true, 8 useErrorHandler: true,
@@ -13,6 +14,8 @@ const oAuthServer = new OAuthServer({
13function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { 14function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
14 oAuthServer.authenticate()(req, res, err => { 15 oAuthServer.authenticate()(req, res, err => {
15 if (err) { 16 if (err) {
17 logger.warn('Cannot authenticate.', { err })
18
16 return res.status(err.status) 19 return res.status(err.status)
17 .json({ 20 .json({
18 error: 'Token is invalid.', 21 error: 'Token is invalid.',
@@ -25,6 +28,12 @@ function authenticate (req: express.Request, res: express.Response, next: expres
25 }) 28 })
26} 29}
27 30
31function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
32 if (req.header('authorization')) return authenticate(req, res, next)
33
34 return next()
35}
36
28function token (req: express.Request, res: express.Response, next: express.NextFunction) { 37function token (req: express.Request, res: express.Response, next: express.NextFunction) {
29 return oAuthServer.token()(req, res, err => { 38 return oAuthServer.token()(req, res, err => {
30 if (err) { 39 if (err) {
@@ -44,5 +53,6 @@ function token (req: express.Request, res: express.Response, next: express.NextF
44 53
45export { 54export {
46 authenticate, 55 authenticate,
56 optionalAuthenticate,
47 token 57 token
48} 58}
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts
index ee6f6efa4..f58c0676c 100644
--- a/server/middlewares/validators/config.ts
+++ b/server/middlewares/validators/config.ts
@@ -1,6 +1,6 @@
1import * as express from 'express' 1import * as express from 'express'
2import { body } from 'express-validator/check' 2import { body } from 'express-validator/check'
3import { isUserVideoQuotaValid } from '../../helpers/custom-validators/users' 3import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
4import { logger } from '../../helpers/logger' 4import { logger } from '../../helpers/logger'
5import { areValidationErrors } from './utils' 5import { areValidationErrors } from './utils'
6 6
@@ -9,6 +9,7 @@ const customConfigUpdateValidator = [
9 body('instance.description').exists().withMessage('Should have a valid instance description'), 9 body('instance.description').exists().withMessage('Should have a valid instance description'),
10 body('instance.terms').exists().withMessage('Should have a valid instance terms'), 10 body('instance.terms').exists().withMessage('Should have a valid instance terms'),
11 body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), 11 body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
12 body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
12 body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), 13 body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
13 body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), 14 body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
14 body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), 15 body('cache.previews.size').isInt().withMessage('Should have a valid previews size'),
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts
index 6ea3d0b6c..5dd8caa3f 100644
--- a/server/middlewares/validators/users.ts
+++ b/server/middlewares/validators/users.ts
@@ -8,7 +8,7 @@ import {
8 isAvatarFile, 8 isAvatarFile,
9 isUserAutoPlayVideoValid, 9 isUserAutoPlayVideoValid,
10 isUserDescriptionValid, 10 isUserDescriptionValid,
11 isUserDisplayNSFWValid, 11 isUserNSFWPolicyValid,
12 isUserPasswordValid, 12 isUserPasswordValid,
13 isUserRoleValid, 13 isUserRoleValid,
14 isUserUsernameValid, 14 isUserUsernameValid,
@@ -101,7 +101,7 @@ const usersUpdateMeValidator = [
101 body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), 101 body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
102 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), 102 body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
103 body('email').optional().isEmail().withMessage('Should have a valid email attribute'), 103 body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
104 body('displayNSFW').optional().custom(isUserDisplayNSFWValid).withMessage('Should have a valid display Not Safe For Work attribute'), 104 body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
105 body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), 105 body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
106 106
107 (req: express.Request, res: express.Response, next: express.NextFunction) => { 107 (req: express.Request, res: express.Response, next: express.NextFunction) => {
diff --git a/server/models/account/user.ts b/server/models/account/user.ts
index 8afd246b2..56af2f30a 100644
--- a/server/models/account/user.ts
+++ b/server/models/account/user.ts
@@ -21,7 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
21import { User, UserRole } from '../../../shared/models/users' 21import { User, UserRole } from '../../../shared/models/users'
22import { 22import {
23 isUserAutoPlayVideoValid, 23 isUserAutoPlayVideoValid,
24 isUserDisplayNSFWValid, 24 isUserNSFWPolicyValid,
25 isUserPasswordValid, 25 isUserPasswordValid,
26 isUserRoleValid, 26 isUserRoleValid,
27 isUserUsernameValid, 27 isUserUsernameValid,
@@ -32,6 +32,9 @@ import { OAuthTokenModel } from '../oauth/oauth-token'
32import { getSort, throwIfNotValid } from '../utils' 32import { getSort, throwIfNotValid } from '../utils'
33import { VideoChannelModel } from '../video/video-channel' 33import { VideoChannelModel } from '../video/video-channel'
34import { AccountModel } from './account' 34import { AccountModel } from './account'
35import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
36import { values } from 'lodash'
37import { NSFW_POLICY_TYPES } from '../../initializers'
35 38
36@DefaultScope({ 39@DefaultScope({
37 include: [ 40 include: [
@@ -83,10 +86,9 @@ export class UserModel extends Model<UserModel> {
83 email: string 86 email: string
84 87
85 @AllowNull(false) 88 @AllowNull(false)
86 @Default(false) 89 @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
87 @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean')) 90 @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
88 @Column 91 nsfwPolicy: NSFWPolicyType
89 displayNSFW: boolean
90 92
91 @AllowNull(false) 93 @AllowNull(false)
92 @Default(true) 94 @Default(true)
@@ -265,7 +267,7 @@ export class UserModel extends Model<UserModel> {
265 id: this.id, 267 id: this.id,
266 username: this.username, 268 username: this.username,
267 email: this.email, 269 email: this.email,
268 displayNSFW: this.displayNSFW, 270 nsfwPolicy: this.nsfwPolicy,
269 autoPlayVideo: this.autoPlayVideo, 271 autoPlayVideo: this.autoPlayVideo,
270 role: this.role, 272 role: this.role,
271 roleLabel: USER_ROLE_LABELS[ this.role ], 273 roleLabel: USER_ROLE_LABELS[ this.role ],
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index a7923b477..2e66f9aa7 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -95,7 +95,7 @@ enum ScopeNames {
95} 95}
96 96
97@Scopes({ 97@Scopes({
98 [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter, withFiles?: boolean) => { 98 [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean) => {
99 const query: IFindOptions<VideoModel> = { 99 const query: IFindOptions<VideoModel> = {
100 where: { 100 where: {
101 id: { 101 id: {
@@ -161,6 +161,11 @@ enum ScopeNames {
161 }) 161 })
162 } 162 }
163 163
164 // Hide nsfw videos?
165 if (hideNSFW === true) {
166 query.where['nsfw'] = false
167 }
168
164 return query 169 return query
165 }, 170 },
166 [ScopeNames.WITH_ACCOUNT_DETAILS]: { 171 [ScopeNames.WITH_ACCOUNT_DETAILS]: {
@@ -640,7 +645,7 @@ export class VideoModel extends Model<VideoModel> {
640 }) 645 })
641 } 646 }
642 647
643 static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { 648 static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
644 const query: IFindOptions<VideoModel> = { 649 const query: IFindOptions<VideoModel> = {
645 offset: start, 650 offset: start,
646 limit: count, 651 limit: count,
@@ -669,6 +674,12 @@ export class VideoModel extends Model<VideoModel> {
669 }) 674 })
670 } 675 }
671 676
677 if (hideNSFW === true) {
678 query.where = {
679 nsfw: false
680 }
681 }
682
672 return VideoModel.findAndCountAll(query).then(({ rows, count }) => { 683 return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
673 return { 684 return {
674 data: rows, 685 data: rows,
@@ -677,7 +688,7 @@ export class VideoModel extends Model<VideoModel> {
677 }) 688 })
678 } 689 }
679 690
680 static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter, withFiles = false) { 691 static async listForApi (start: number, count: number, sort: string, hideNSFW: boolean, filter?: VideoFilter, withFiles = false) {
681 const query = { 692 const query = {
682 offset: start, 693 offset: start,
683 limit: count, 694 limit: count,
@@ -685,8 +696,7 @@ export class VideoModel extends Model<VideoModel> {
685 } 696 }
686 697
687 const serverActor = await getServerActor() 698 const serverActor = await getServerActor()
688 699 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles ] })
689 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] })
690 .findAndCountAll(query) 700 .findAndCountAll(query)
691 .then(({ rows, count }) => { 701 .then(({ rows, count }) => {
692 return { 702 return {
@@ -696,7 +706,7 @@ export class VideoModel extends Model<VideoModel> {
696 }) 706 })
697 } 707 }
698 708
699 static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string) { 709 static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) {
700 const query: IFindOptions<VideoModel> = { 710 const query: IFindOptions<VideoModel> = {
701 offset: start, 711 offset: start,
702 limit: count, 712 limit: count,
@@ -724,7 +734,7 @@ export class VideoModel extends Model<VideoModel> {
724 734
725 const serverActor = await getServerActor() 735 const serverActor = await getServerActor()
726 736
727 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] }) 737 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW ] })
728 .findAndCountAll(query) 738 .findAndCountAll(query)
729 .then(({ rows, count }) => { 739 .then(({ rows, count }) => {
730 return { 740 return {
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts
index 3fe517fad..58b780f38 100644
--- a/server/tests/api/check-params/config.ts
+++ b/server/tests/api/check-params/config.ts
@@ -6,7 +6,7 @@ import { CustomConfig } from '../../../../shared/models/server/custom-config.mod
6 6
7import { 7import {
8 createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, 8 createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
9 setAccessTokensToServers, userLogin 9 setAccessTokensToServers, userLogin, immutableAssign
10} from '../../utils' 10} from '../../utils'
11 11
12describe('Test config API validators', function () { 12describe('Test config API validators', function () {
@@ -20,6 +20,7 @@ describe('Test config API validators', function () {
20 description: 'my super description', 20 description: 'my super description',
21 terms: 'my super terms', 21 terms: 'my super terms',
22 defaultClientRoute: '/videos/recently-added', 22 defaultClientRoute: '/videos/recently-added',
23 defaultNSFWPolicy: 'blur',
23 customizations: { 24 customizations: {
24 javascript: 'alert("coucou")', 25 javascript: 'alert("coucou")',
25 css: 'body { background-color: red; }' 26 css: 'body { background-color: red; }'
@@ -122,6 +123,22 @@ describe('Test config API validators', function () {
122 }) 123 })
123 }) 124 })
124 125
126 it('Should fail with a bad default NSFW policy', async function () {
127 const newUpdateParams = immutableAssign(updateParams, {
128 instance: {
129 defaultNSFWPolicy: 'hello'
130 }
131 })
132
133 await makePutBodyRequest({
134 url: server.url,
135 path,
136 fields: newUpdateParams,
137 token: server.accessToken,
138 statusCodeExpected: 400
139 })
140 })
141
125 it('Should success with the correct parameters', async function () { 142 it('Should success with the correct parameters', async function () {
126 await makePutBodyRequest({ 143 await makePutBodyRequest({
127 url: server.url, 144 url: server.url,
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts
index a3e415b94..e8a6ffd19 100644
--- a/server/tests/api/check-params/users.ts
+++ b/server/tests/api/check-params/users.ts
@@ -231,9 +231,9 @@ describe('Test users API validators', function () {
231 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 231 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
232 }) 232 })
233 233
234 it('Should fail with an invalid display NSFW attribute', async function () { 234 it('Should fail with an invalid NSFW policy attribute', async function () {
235 const fields = { 235 const fields = {
236 displayNSFW: -1 236 nsfwPolicy: 'hello'
237 } 237 }
238 238
239 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) 239 await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
@@ -266,7 +266,7 @@ describe('Test users API validators', function () {
266 it('Should succeed with the correct params', async function () { 266 it('Should succeed with the correct params', async function () {
267 const fields = { 267 const fields = {
268 password: 'my super password', 268 password: 'my super password',
269 displayNSFW: true, 269 nsfwPolicy: 'blur',
270 autoPlayVideo: false, 270 autoPlayVideo: false,
271 email: 'super_email@example.com' 271 email: 'super_email@example.com'
272 } 272 }
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts
index e17588142..3f1b1532c 100644
--- a/server/tests/api/server/config.ts
+++ b/server/tests/api/server/config.ts
@@ -59,6 +59,7 @@ describe('Test config', function () {
59 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') 59 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
60 expect(data.instance.terms).to.equal('No terms for now.') 60 expect(data.instance.terms).to.equal('No terms for now.')
61 expect(data.instance.defaultClientRoute).to.equal('/videos/trending') 61 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
62 expect(data.instance.defaultNSFWPolicy).to.equal('display')
62 expect(data.instance.customizations.css).to.be.empty 63 expect(data.instance.customizations.css).to.be.empty
63 expect(data.instance.customizations.javascript).to.be.empty 64 expect(data.instance.customizations.javascript).to.be.empty
64 expect(data.cache.previews.size).to.equal(1) 65 expect(data.cache.previews.size).to.equal(1)
@@ -83,6 +84,7 @@ describe('Test config', function () {
83 description: 'my super description', 84 description: 'my super description',
84 terms: 'my super terms', 85 terms: 'my super terms',
85 defaultClientRoute: '/videos/recently-added', 86 defaultClientRoute: '/videos/recently-added',
87 defaultNSFWPolicy: 'blur' as 'blur',
86 customizations: { 88 customizations: {
87 javascript: 'alert("coucou")', 89 javascript: 'alert("coucou")',
88 css: 'body { background-color: red; }' 90 css: 'body { background-color: red; }'
@@ -125,6 +127,7 @@ describe('Test config', function () {
125 expect(data.instance.description).to.equal('my super description') 127 expect(data.instance.description).to.equal('my super description')
126 expect(data.instance.terms).to.equal('my super terms') 128 expect(data.instance.terms).to.equal('my super terms')
127 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') 129 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
130 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
128 expect(data.instance.customizations.javascript).to.equal('alert("coucou")') 131 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
129 expect(data.instance.customizations.css).to.equal('body { background-color: red; }') 132 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
130 expect(data.cache.previews.size).to.equal(2) 133 expect(data.cache.previews.size).to.equal(2)
@@ -156,6 +159,7 @@ describe('Test config', function () {
156 expect(data.instance.description).to.equal('my super description') 159 expect(data.instance.description).to.equal('my super description')
157 expect(data.instance.terms).to.equal('my super terms') 160 expect(data.instance.terms).to.equal('my super terms')
158 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') 161 expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
162 expect(data.instance.defaultNSFWPolicy).to.equal('blur')
159 expect(data.instance.customizations.javascript).to.equal('alert("coucou")') 163 expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
160 expect(data.instance.customizations.css).to.equal('body { background-color: red; }') 164 expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
161 expect(data.cache.previews.size).to.equal(2) 165 expect(data.cache.previews.size).to.equal(2)
@@ -198,6 +202,7 @@ describe('Test config', function () {
198 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') 202 expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
199 expect(data.instance.terms).to.equal('No terms for now.') 203 expect(data.instance.terms).to.equal('No terms for now.')
200 expect(data.instance.defaultClientRoute).to.equal('/videos/trending') 204 expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
205 expect(data.instance.defaultNSFWPolicy).to.equal('display')
201 expect(data.instance.customizations.css).to.be.empty 206 expect(data.instance.customizations.css).to.be.empty
202 expect(data.instance.customizations.javascript).to.be.empty 207 expect(data.instance.customizations.javascript).to.be.empty
203 expect(data.cache.previews.size).to.equal(1) 208 expect(data.cache.previews.size).to.equal(1)
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts
index b6ab4f660..1192ef9e4 100644
--- a/server/tests/api/users/users.ts
+++ b/server/tests/api/users/users.ts
@@ -168,7 +168,7 @@ describe('Test users', function () {
168 168
169 expect(user.username).to.equal('user_1') 169 expect(user.username).to.equal('user_1')
170 expect(user.email).to.equal('user_1@example.com') 170 expect(user.email).to.equal('user_1@example.com')
171 expect(user.displayNSFW).to.be.false 171 expect(user.nsfwPolicy).to.equal('display')
172 expect(user.videoQuota).to.equal(2 * 1024 * 1024) 172 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
173 expect(user.roleLabel).to.equal('User') 173 expect(user.roleLabel).to.equal('User')
174 expect(user.id).to.be.a('number') 174 expect(user.id).to.be.a('number')
@@ -215,12 +215,12 @@ describe('Test users', function () {
215 const user = users[ 0 ] 215 const user = users[ 0 ]
216 expect(user.username).to.equal('user_1') 216 expect(user.username).to.equal('user_1')
217 expect(user.email).to.equal('user_1@example.com') 217 expect(user.email).to.equal('user_1@example.com')
218 expect(user.displayNSFW).to.be.false 218 expect(user.nsfwPolicy).to.equal('display')
219 219
220 const rootUser = users[ 1 ] 220 const rootUser = users[ 1 ]
221 expect(rootUser.username).to.equal('root') 221 expect(rootUser.username).to.equal('root')
222 expect(rootUser.email).to.equal('admin1@example.com') 222 expect(rootUser.email).to.equal('admin1@example.com')
223 expect(rootUser.displayNSFW).to.be.false 223 expect(user.nsfwPolicy).to.equal('display')
224 224
225 userId = user.id 225 userId = user.id
226 }) 226 })
@@ -239,7 +239,7 @@ describe('Test users', function () {
239 expect(user.username).to.equal('root') 239 expect(user.username).to.equal('root')
240 expect(user.email).to.equal('admin1@example.com') 240 expect(user.email).to.equal('admin1@example.com')
241 expect(user.roleLabel).to.equal('Administrator') 241 expect(user.roleLabel).to.equal('Administrator')
242 expect(user.displayNSFW).to.be.false 242 expect(user.nsfwPolicy).to.equal('display')
243 }) 243 })
244 244
245 it('Should list only the first user by username desc', async function () { 245 it('Should list only the first user by username desc', async function () {
@@ -254,7 +254,7 @@ describe('Test users', function () {
254 const user = users[ 0 ] 254 const user = users[ 0 ]
255 expect(user.username).to.equal('user_1') 255 expect(user.username).to.equal('user_1')
256 expect(user.email).to.equal('user_1@example.com') 256 expect(user.email).to.equal('user_1@example.com')
257 expect(user.displayNSFW).to.be.false 257 expect(user.nsfwPolicy).to.equal('display')
258 }) 258 })
259 259
260 it('Should list only the second user by createdAt desc', async function () { 260 it('Should list only the second user by createdAt desc', async function () {
@@ -269,7 +269,7 @@ describe('Test users', function () {
269 const user = users[ 0 ] 269 const user = users[ 0 ]
270 expect(user.username).to.equal('user_1') 270 expect(user.username).to.equal('user_1')
271 expect(user.email).to.equal('user_1@example.com') 271 expect(user.email).to.equal('user_1@example.com')
272 expect(user.displayNSFW).to.be.false 272 expect(user.nsfwPolicy).to.equal('display')
273 }) 273 })
274 274
275 it('Should list all the users by createdAt asc', async function () { 275 it('Should list all the users by createdAt asc', async function () {
@@ -283,11 +283,11 @@ describe('Test users', function () {
283 283
284 expect(users[ 0 ].username).to.equal('root') 284 expect(users[ 0 ].username).to.equal('root')
285 expect(users[ 0 ].email).to.equal('admin1@example.com') 285 expect(users[ 0 ].email).to.equal('admin1@example.com')
286 expect(users[ 0 ].displayNSFW).to.be.false 286 expect(users[ 0 ].nsfwPolicy).to.equal('display')
287 287
288 expect(users[ 1 ].username).to.equal('user_1') 288 expect(users[ 1 ].username).to.equal('user_1')
289 expect(users[ 1 ].email).to.equal('user_1@example.com') 289 expect(users[ 1 ].email).to.equal('user_1@example.com')
290 expect(users[ 1 ].displayNSFW).to.be.false 290 expect(users[ 1 ].nsfwPolicy).to.equal('display')
291 }) 291 })
292 292
293 it('Should update my password', async function () { 293 it('Should update my password', async function () {
@@ -305,7 +305,7 @@ describe('Test users', function () {
305 await updateMyUser({ 305 await updateMyUser({
306 url: server.url, 306 url: server.url,
307 accessToken: accessTokenUser, 307 accessToken: accessTokenUser,
308 displayNSFW: true 308 nsfwPolicy: 'do_not_list'
309 }) 309 })
310 310
311 const res = await getMyUserInformation(server.url, accessTokenUser) 311 const res = await getMyUserInformation(server.url, accessTokenUser)
@@ -313,7 +313,7 @@ describe('Test users', function () {
313 313
314 expect(user.username).to.equal('user_1') 314 expect(user.username).to.equal('user_1')
315 expect(user.email).to.equal('user_1@example.com') 315 expect(user.email).to.equal('user_1@example.com')
316 expect(user.displayNSFW).to.be.ok 316 expect(user.nsfwPolicy).to.equal('do_not_list')
317 expect(user.videoQuota).to.equal(2 * 1024 * 1024) 317 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
318 expect(user.id).to.be.a('number') 318 expect(user.id).to.be.a('number')
319 expect(user.account.description).to.be.null 319 expect(user.account.description).to.be.null
@@ -344,7 +344,7 @@ describe('Test users', function () {
344 344
345 expect(user.username).to.equal('user_1') 345 expect(user.username).to.equal('user_1')
346 expect(user.email).to.equal('updated@example.com') 346 expect(user.email).to.equal('updated@example.com')
347 expect(user.displayNSFW).to.be.ok 347 expect(user.nsfwPolicy).to.equal('do_not_list')
348 expect(user.videoQuota).to.equal(2 * 1024 * 1024) 348 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
349 expect(user.id).to.be.a('number') 349 expect(user.id).to.be.a('number')
350 expect(user.account.description).to.be.null 350 expect(user.account.description).to.be.null
@@ -377,7 +377,7 @@ describe('Test users', function () {
377 377
378 expect(user.username).to.equal('user_1') 378 expect(user.username).to.equal('user_1')
379 expect(user.email).to.equal('updated@example.com') 379 expect(user.email).to.equal('updated@example.com')
380 expect(user.displayNSFW).to.be.ok 380 expect(user.nsfwPolicy).to.equal('do_not_list')
381 expect(user.videoQuota).to.equal(2 * 1024 * 1024) 381 expect(user.videoQuota).to.equal(2 * 1024 * 1024)
382 expect(user.id).to.be.a('number') 382 expect(user.id).to.be.a('number')
383 expect(user.account.description).to.equal('my super description updated') 383 expect(user.account.description).to.equal('my super description updated')
@@ -398,7 +398,7 @@ describe('Test users', function () {
398 398
399 expect(user.username).to.equal('user_1') 399 expect(user.username).to.equal('user_1')
400 expect(user.email).to.equal('updated2@example.com') 400 expect(user.email).to.equal('updated2@example.com')
401 expect(user.displayNSFW).to.be.ok 401 expect(user.nsfwPolicy).to.equal('do_not_list')
402 expect(user.videoQuota).to.equal(42) 402 expect(user.videoQuota).to.equal(42)
403 expect(user.roleLabel).to.equal('Moderator') 403 expect(user.roleLabel).to.equal('Moderator')
404 expect(user.id).to.be.a('number') 404 expect(user.id).to.be.a('number')
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts
new file mode 100644
index 000000000..4e5ab11ce
--- /dev/null
+++ b/server/tests/api/videos/video-nsfw.ts
@@ -0,0 +1,197 @@
1/* tslint:disable:no-unused-expression */
2
3import * as chai from 'chai'
4import 'mocha'
5import { flushTests, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index'
6import { userLogin } from '../../utils/users/login'
7import { createUser } from '../../utils/users/users'
8import { getMyVideos } from '../../utils/videos/videos'
9import {
10 getConfig, getCustomConfig,
11 getMyUserInformation,
12 getVideosListWithToken,
13 runServer,
14 searchVideo,
15 searchVideoWithToken, updateCustomConfig,
16 updateMyUser
17} from '../../utils'
18import { ServerConfig } from '../../../../shared/models'
19import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
20
21const expect = chai.expect
22
23describe('Test video NSFW policy', function () {
24 let server: ServerInfo
25 let userAccessToken: string
26 let customConfig: CustomConfig
27
28 before(async function () {
29 this.timeout(50000)
30
31 await flushTests()
32 server = await runServer(1)
33
34 // Get the access tokens
35 await setAccessTokensToServers([ server ])
36
37 {
38 const attributes = { name: 'nsfw', nsfw: true }
39 await uploadVideo(server.url, server.accessToken, attributes)
40 }
41
42 {
43 const attributes = { name: 'normal', nsfw: false }
44 await uploadVideo(server.url, server.accessToken, attributes)
45 }
46
47 {
48 const res = await getCustomConfig(server.url, server.accessToken)
49 customConfig = res.body
50 }
51 })
52
53 describe('Instance default NSFW policy', function () {
54 it('Should display NSFW videos with display default NSFW policy', async function () {
55 const resConfig = await getConfig(server.url)
56 const serverConfig: ServerConfig = resConfig.body
57 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display')
58
59 for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) {
60 expect(res.body.total).to.equal(2)
61
62 const videos = res.body.data
63 expect(videos).to.have.lengthOf(2)
64 expect(videos[ 0 ].name).to.equal('normal')
65 expect(videos[ 1 ].name).to.equal('nsfw')
66 }
67 })
68
69 it('Should not display NSFW videos with do_not_list default NSFW policy', async function () {
70 customConfig.instance.defaultNSFWPolicy = 'do_not_list'
71 await updateCustomConfig(server.url, server.accessToken, customConfig)
72
73 const resConfig = await getConfig(server.url)
74 const serverConfig: ServerConfig = resConfig.body
75 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list')
76
77 for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) {
78 expect(res.body.total).to.equal(1)
79
80 const videos = res.body.data
81 expect(videos).to.have.lengthOf(1)
82 expect(videos[ 0 ].name).to.equal('normal')
83 }
84 })
85
86 it('Should display NSFW videos with blur default NSFW policy', async function () {
87 customConfig.instance.defaultNSFWPolicy = 'blur'
88 await updateCustomConfig(server.url, server.accessToken, customConfig)
89
90 const resConfig = await getConfig(server.url)
91 const serverConfig: ServerConfig = resConfig.body
92 expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur')
93
94 for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) {
95 expect(res.body.total).to.equal(2)
96
97 const videos = res.body.data
98 expect(videos).to.have.lengthOf(2)
99 expect(videos[ 0 ].name).to.equal('normal')
100 expect(videos[ 1 ].name).to.equal('nsfw')
101 }
102 })
103 })
104
105 describe('User NSFW policy', function () {
106
107 it('Should create a user having the default nsfw policy', async function () {
108 const username = 'user1'
109 const password = 'my super password'
110 await createUser(server.url, server.accessToken, username, password)
111
112 userAccessToken = await userLogin(server, { username, password })
113
114 const res = await getMyUserInformation(server.url, userAccessToken)
115 const user = res.body
116
117 expect(user.nsfwPolicy).to.equal('blur')
118 })
119
120 it('Should display NSFW videos with blur user NSFW policy', async function () {
121 const results = [
122 await getVideosListWithToken(server.url, userAccessToken),
123 await searchVideoWithToken(server.url, 'n', userAccessToken)
124 ]
125
126 for (const res of results) {
127 expect(res.body.total).to.equal(2)
128
129 const videos = res.body.data
130 expect(videos).to.have.lengthOf(2)
131 expect(videos[ 0 ].name).to.equal('normal')
132 expect(videos[ 1 ].name).to.equal('nsfw')
133 }
134 })
135
136 it('Should display NSFW videos with display user NSFW policy', async function () {
137 await updateMyUser({
138 url: server.url,
139 accessToken: server.accessToken,
140 nsfwPolicy: 'display'
141 })
142
143 const results = [
144 await getVideosListWithToken(server.url, server.accessToken),
145 await searchVideoWithToken(server.url, 'n', server.accessToken)
146 ]
147
148 for (const res of results) {
149 expect(res.body.total).to.equal(2)
150
151 const videos = res.body.data
152 expect(videos).to.have.lengthOf(2)
153 expect(videos[ 0 ].name).to.equal('normal')
154 expect(videos[ 1 ].name).to.equal('nsfw')
155 }
156 })
157
158 it('Should not display NSFW videos with do_not_list user NSFW policy', async function () {
159 await updateMyUser({
160 url: server.url,
161 accessToken: server.accessToken,
162 nsfwPolicy: 'do_not_list'
163 })
164
165 const results = [
166 await getVideosListWithToken(server.url, server.accessToken),
167 await searchVideoWithToken(server.url, 'n', server.accessToken)
168 ]
169 for (const res of results) {
170 expect(res.body.total).to.equal(1)
171
172 const videos = res.body.data
173 expect(videos).to.have.lengthOf(1)
174 expect(videos[ 0 ].name).to.equal('normal')
175 }
176 })
177
178 it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
179 const res = await getMyVideos(server.url, server.accessToken, 0, 5)
180 expect(res.body.total).to.equal(2)
181
182 const videos = res.body.data
183 expect(videos).to.have.lengthOf(2)
184 expect(videos[ 0 ].name).to.equal('normal')
185 expect(videos[ 1 ].name).to.equal('nsfw')
186 })
187 })
188
189 after(async function () {
190 killallServers([ server ])
191
192 // Keep the logs if the test failed
193 if (this['ok']) {
194 await flushTests()
195 }
196 })
197})
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts
index daf731a14..fc6b26c50 100644
--- a/server/tests/utils/users/users.ts
+++ b/server/tests/utils/users/users.ts
@@ -3,6 +3,7 @@ import * as request from 'supertest'
3import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../' 3import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../'
4 4
5import { UserRole } from '../../../../shared/index' 5import { UserRole } from '../../../../shared/index'
6import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
6 7
7function createUser ( 8function createUser (
8 url: string, 9 url: string,
@@ -128,7 +129,7 @@ function updateMyUser (options: {
128 url: string 129 url: string
129 accessToken: string, 130 accessToken: string,
130 newPassword?: string, 131 newPassword?: string,
131 displayNSFW?: boolean, 132 nsfwPolicy?: NSFWPolicyType,
132 email?: string, 133 email?: string,
133 autoPlayVideo?: boolean 134 autoPlayVideo?: boolean
134 description?: string 135 description?: string
@@ -137,7 +138,7 @@ function updateMyUser (options: {
137 138
138 const toSend = {} 139 const toSend = {}
139 if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword 140 if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword
140 if (options.displayNSFW !== undefined && options.displayNSFW !== null) toSend['displayNSFW'] = options.displayNSFW 141 if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy
141 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo 142 if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
142 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email 143 if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
143 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description 144 if (options.description !== undefined && options.description !== null) toSend['description'] = options.description
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts
index 01e7fa5a1..df9071c29 100644
--- a/server/tests/utils/videos/videos.ts
+++ b/server/tests/utils/videos/videos.ts
@@ -128,6 +128,18 @@ function getVideosList (url: string) {
128 .expect('Content-Type', /json/) 128 .expect('Content-Type', /json/)
129} 129}
130 130
131function getVideosListWithToken (url: string, token: string) {
132 const path = '/api/v1/videos'
133
134 return request(url)
135 .get(path)
136 .set('Authorization', 'Bearer ' + token)
137 .query({ sort: 'name' })
138 .set('Accept', 'application/json')
139 .expect(200)
140 .expect('Content-Type', /json/)
141}
142
131function getLocalVideos (url: string) { 143function getLocalVideos (url: string) {
132 const path = '/api/v1/videos' 144 const path = '/api/v1/videos'
133 145
@@ -202,6 +214,18 @@ function searchVideo (url: string, search: string) {
202 .expect('Content-Type', /json/) 214 .expect('Content-Type', /json/)
203} 215}
204 216
217function searchVideoWithToken (url: string, search: string, token: string) {
218 const path = '/api/v1/videos'
219 const req = request(url)
220 .get(path + '/search')
221 .set('Authorization', 'Bearer ' + token)
222 .query({ search })
223 .set('Accept', 'application/json')
224
225 return req.expect(200)
226 .expect('Content-Type', /json/)
227}
228
205function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { 229function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
206 const path = '/api/v1/videos' 230 const path = '/api/v1/videos'
207 231
@@ -490,6 +514,7 @@ export {
490 getVideoPrivacies, 514 getVideoPrivacies,
491 getVideoLanguages, 515 getVideoLanguages,
492 getMyVideos, 516 getMyVideos,
517 searchVideoWithToken,
493 getVideo, 518 getVideo,
494 getVideoWithToken, 519 getVideoWithToken,
495 getVideosList, 520 getVideosList,
@@ -499,6 +524,7 @@ export {
499 searchVideo, 524 searchVideo,
500 searchVideoWithPagination, 525 searchVideoWithPagination,
501 searchVideoWithSort, 526 searchVideoWithSort,
527 getVideosListWithToken,
502 uploadVideo, 528 uploadVideo,
503 updateVideo, 529 updateVideo,
504 rateVideo, 530 rateVideo,