aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers')
-rw-r--r--server/controllers/api/users/index.ts4
-rw-r--r--server/controllers/api/users/token.ts7
-rw-r--r--server/controllers/api/users/two-factor.ts95
-rw-r--r--server/controllers/feeds.ts78
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/live.ts32
-rw-r--r--server/controllers/well-known.ts7
7 files changed, 159 insertions, 65 deletions
diff --git a/server/controllers/api/users/index.ts b/server/controllers/api/users/index.ts
index 0b27d5277..a8677a1d3 100644
--- a/server/controllers/api/users/index.ts
+++ b/server/controllers/api/users/index.ts
@@ -51,6 +51,7 @@ import { myVideosHistoryRouter } from './my-history'
51import { myNotificationsRouter } from './my-notifications' 51import { myNotificationsRouter } from './my-notifications'
52import { mySubscriptionsRouter } from './my-subscriptions' 52import { mySubscriptionsRouter } from './my-subscriptions'
53import { myVideoPlaylistsRouter } from './my-video-playlists' 53import { myVideoPlaylistsRouter } from './my-video-playlists'
54import { twoFactorRouter } from './two-factor'
54 55
55const auditLogger = auditLoggerFactory('users') 56const auditLogger = auditLoggerFactory('users')
56 57
@@ -66,6 +67,7 @@ const askSendEmailLimiter = buildRateLimiter({
66}) 67})
67 68
68const usersRouter = express.Router() 69const usersRouter = express.Router()
70usersRouter.use('/', twoFactorRouter)
69usersRouter.use('/', tokensRouter) 71usersRouter.use('/', tokensRouter)
70usersRouter.use('/', myNotificationsRouter) 72usersRouter.use('/', myNotificationsRouter)
71usersRouter.use('/', mySubscriptionsRouter) 73usersRouter.use('/', mySubscriptionsRouter)
@@ -343,7 +345,7 @@ async function askResetUserPassword (req: express.Request, res: express.Response
343 345
344 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id) 346 const verificationString = await Redis.Instance.setResetPasswordVerificationString(user.id)
345 const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString 347 const url = WEBSERVER.URL + '/reset-password?userId=' + user.id + '&verificationString=' + verificationString
346 await Emailer.Instance.addPasswordResetEmailJob(user.username, user.email, url) 348 Emailer.Instance.addPasswordResetEmailJob(user.username, user.email, url)
347 349
348 return res.status(HttpStatusCode.NO_CONTENT_204).end() 350 return res.status(HttpStatusCode.NO_CONTENT_204).end()
349} 351}
diff --git a/server/controllers/api/users/token.ts b/server/controllers/api/users/token.ts
index 012a49791..c6afea67c 100644
--- a/server/controllers/api/users/token.ts
+++ b/server/controllers/api/users/token.ts
@@ -1,8 +1,9 @@
1import express from 'express' 1import express from 'express'
2import { logger } from '@server/helpers/logger' 2import { logger } from '@server/helpers/logger'
3import { CONFIG } from '@server/initializers/config' 3import { CONFIG } from '@server/initializers/config'
4import { OTP } from '@server/initializers/constants'
4import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth' 5import { getAuthNameFromRefreshGrant, getBypassFromExternalAuth, getBypassFromPasswordGrant } from '@server/lib/auth/external-auth'
5import { handleOAuthToken } from '@server/lib/auth/oauth' 6import { handleOAuthToken, MissingTwoFactorError } from '@server/lib/auth/oauth'
6import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model' 7import { BypassLogin, revokeToken } from '@server/lib/auth/oauth-model'
7import { Hooks } from '@server/lib/plugins/hooks' 8import { Hooks } from '@server/lib/plugins/hooks'
8import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares' 9import { asyncMiddleware, authenticate, buildRateLimiter, openapiOperationDoc } from '@server/middlewares'
@@ -79,6 +80,10 @@ async function handleToken (req: express.Request, res: express.Response, next: e
79 } catch (err) { 80 } catch (err) {
80 logger.warn('Login error', { err }) 81 logger.warn('Login error', { err })
81 82
83 if (err instanceof MissingTwoFactorError) {
84 res.set(OTP.HEADER_NAME, OTP.HEADER_REQUIRED_VALUE)
85 }
86
82 return res.fail({ 87 return res.fail({
83 status: err.code, 88 status: err.code,
84 message: err.message, 89 message: err.message,
diff --git a/server/controllers/api/users/two-factor.ts b/server/controllers/api/users/two-factor.ts
new file mode 100644
index 000000000..e6ae9e4dd
--- /dev/null
+++ b/server/controllers/api/users/two-factor.ts
@@ -0,0 +1,95 @@
1import express from 'express'
2import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
3import { encrypt } from '@server/helpers/peertube-crypto'
4import { CONFIG } from '@server/initializers/config'
5import { Redis } from '@server/lib/redis'
6import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
7import {
8 confirmTwoFactorValidator,
9 disableTwoFactorValidator,
10 requestOrConfirmTwoFactorValidator
11} from '@server/middlewares/validators/two-factor'
12import { HttpStatusCode, TwoFactorEnableResult } from '@shared/models'
13
14const twoFactorRouter = express.Router()
15
16twoFactorRouter.post('/:id/two-factor/request',
17 authenticate,
18 asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
19 asyncMiddleware(requestOrConfirmTwoFactorValidator),
20 asyncMiddleware(requestTwoFactor)
21)
22
23twoFactorRouter.post('/:id/two-factor/confirm-request',
24 authenticate,
25 asyncMiddleware(requestOrConfirmTwoFactorValidator),
26 confirmTwoFactorValidator,
27 asyncMiddleware(confirmRequestTwoFactor)
28)
29
30twoFactorRouter.post('/:id/two-factor/disable',
31 authenticate,
32 asyncMiddleware(usersCheckCurrentPasswordFactory(req => req.params.id)),
33 asyncMiddleware(disableTwoFactorValidator),
34 asyncMiddleware(disableTwoFactor)
35)
36
37// ---------------------------------------------------------------------------
38
39export {
40 twoFactorRouter
41}
42
43// ---------------------------------------------------------------------------
44
45async function requestTwoFactor (req: express.Request, res: express.Response) {
46 const user = res.locals.user
47
48 const { secret, uri } = generateOTPSecret(user.email)
49
50 const encryptedSecret = await encrypt(secret, CONFIG.SECRETS.PEERTUBE)
51 const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, encryptedSecret)
52
53 return res.json({
54 otpRequest: {
55 requestToken,
56 secret,
57 uri
58 }
59 } as TwoFactorEnableResult)
60}
61
62async function confirmRequestTwoFactor (req: express.Request, res: express.Response) {
63 const requestToken = req.body.requestToken
64 const otpToken = req.body.otpToken
65 const user = res.locals.user
66
67 const encryptedSecret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken)
68 if (!encryptedSecret) {
69 return res.fail({
70 message: 'Invalid request token',
71 status: HttpStatusCode.FORBIDDEN_403
72 })
73 }
74
75 if (await isOTPValid({ encryptedSecret, token: otpToken }) !== true) {
76 return res.fail({
77 message: 'Invalid OTP token',
78 status: HttpStatusCode.FORBIDDEN_403
79 })
80 }
81
82 user.otpSecret = encryptedSecret
83 await user.save()
84
85 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
86}
87
88async function disableTwoFactor (req: express.Request, res: express.Response) {
89 const user = res.locals.user
90
91 user.otpSecret = null
92 await user.save()
93
94 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
95}
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts
index 241715fb9..772fe734d 100644
--- a/server/controllers/feeds.ts
+++ b/server/controllers/feeds.ts
@@ -4,7 +4,8 @@ import { 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 { MAccountDefault, MChannelBannerAccountDefault, MVideoFullLight } from '@server/types/models'
8import { ActorImageType, VideoInclude } from '@shared/models'
8import { buildNSFWFilter } from '../helpers/express-utils' 9import { buildNSFWFilter } from '../helpers/express-utils'
9import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
10import { MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 11import { MIMETYPES, PREVIEWS_SIZE, ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
@@ -82,22 +83,12 @@ async function generateVideoCommentsFeed (req: express.Request, res: express.Res
82 videoChannelId: videoChannel ? videoChannel.id : undefined 83 videoChannelId: videoChannel ? videoChannel.id : undefined
83 }) 84 })
84 85
85 let name: string 86 const { name, description, imageUrl } = buildFeedMetadata({ video, account, videoChannel })
86 let description: string
87 87
88 if (videoChannel) {
89 name = videoChannel.getDisplayName()
90 description = videoChannel.description
91 } else if (account) {
92 name = account.getDisplayName()
93 description = account.description
94 } else {
95 name = video ? video.name : CONFIG.INSTANCE.NAME
96 description = video ? video.description : CONFIG.INSTANCE.DESCRIPTION
97 }
98 const feed = initFeed({ 88 const feed = initFeed({
99 name, 89 name,
100 description, 90 description,
91 imageUrl,
101 resourceType: 'video-comments', 92 resourceType: 'video-comments',
102 queryString: new URL(WEBSERVER.URL + req.originalUrl).search 93 queryString: new URL(WEBSERVER.URL + req.originalUrl).search
103 }) 94 })
@@ -137,23 +128,12 @@ async function generateVideoFeed (req: express.Request, res: express.Response) {
137 const videoChannel = res.locals.videoChannel 128 const videoChannel = res.locals.videoChannel
138 const nsfw = buildNSFWFilter(res, req.query.nsfw) 129 const nsfw = buildNSFWFilter(res, req.query.nsfw)
139 130
140 let name: string 131 const { name, description, imageUrl } = buildFeedMetadata({ videoChannel, account })
141 let description: string
142
143 if (videoChannel) {
144 name = videoChannel.getDisplayName()
145 description = videoChannel.description
146 } else if (account) {
147 name = account.getDisplayName()
148 description = account.description
149 } else {
150 name = CONFIG.INSTANCE.NAME
151 description = CONFIG.INSTANCE.DESCRIPTION
152 }
153 132
154 const feed = initFeed({ 133 const feed = initFeed({
155 name, 134 name,
156 description, 135 description,
136 imageUrl,
157 resourceType: 'videos', 137 resourceType: 'videos',
158 queryString: new URL(WEBSERVER.URL + req.url).search 138 queryString: new URL(WEBSERVER.URL + req.url).search
159 }) 139 })
@@ -190,12 +170,13 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
190 const start = 0 170 const start = 0
191 const account = res.locals.account 171 const account = res.locals.account
192 const nsfw = buildNSFWFilter(res, req.query.nsfw) 172 const nsfw = buildNSFWFilter(res, req.query.nsfw)
193 const name = account.getDisplayName() 173
194 const description = account.description 174 const { name, description, imageUrl } = buildFeedMetadata({ account })
195 175
196 const feed = initFeed({ 176 const feed = initFeed({
197 name, 177 name,
198 description, 178 description,
179 imageUrl,
199 resourceType: 'videos', 180 resourceType: 'videos',
200 queryString: new URL(WEBSERVER.URL + req.url).search 181 queryString: new URL(WEBSERVER.URL + req.url).search
201 }) 182 })
@@ -229,11 +210,12 @@ async function generateVideoFeedForSubscriptions (req: express.Request, res: exp
229function initFeed (parameters: { 210function initFeed (parameters: {
230 name: string 211 name: string
231 description: string 212 description: string
213 imageUrl: string
232 resourceType?: 'videos' | 'video-comments' 214 resourceType?: 'videos' | 'video-comments'
233 queryString?: string 215 queryString?: string
234}) { 216}) {
235 const webserverUrl = WEBSERVER.URL 217 const webserverUrl = WEBSERVER.URL
236 const { name, description, resourceType, queryString } = parameters 218 const { name, description, resourceType, queryString, imageUrl } = parameters
237 219
238 return new Feed({ 220 return new Feed({
239 title: name, 221 title: name,
@@ -241,7 +223,7 @@ function initFeed (parameters: {
241 // updated: TODO: somehowGetLatestUpdate, // optional, default = today 223 // updated: TODO: somehowGetLatestUpdate, // optional, default = today
242 id: webserverUrl, 224 id: webserverUrl,
243 link: webserverUrl, 225 link: webserverUrl,
244 image: webserverUrl + '/client/assets/images/icons/icon-96x96.png', 226 image: imageUrl,
245 favicon: webserverUrl + '/client/assets/images/favicon.png', 227 favicon: webserverUrl + '/client/assets/images/favicon.png',
246 copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` + 228 copyright: `All rights reserved, unless otherwise specified in the terms specified at ${webserverUrl}/about` +
247 ` and potential licenses granted by each content's rightholder.`, 229 ` and potential licenses granted by each content's rightholder.`,
@@ -369,3 +351,39 @@ function sendFeed (feed: Feed, req: express.Request, res: express.Response) {
369 351
370 return res.send(feed.rss2()).end() 352 return res.send(feed.rss2()).end()
371} 353}
354
355function buildFeedMetadata (options: {
356 videoChannel?: MChannelBannerAccountDefault
357 account?: MAccountDefault
358 video?: MVideoFullLight
359}) {
360 const { video, videoChannel, account } = options
361
362 let imageUrl = WEBSERVER.URL + '/client/assets/images/icons/icon-96x96.png'
363 let name: string
364 let description: string
365
366 if (videoChannel) {
367 name = videoChannel.getDisplayName()
368 description = videoChannel.description
369
370 if (videoChannel.Actor.hasImage(ActorImageType.AVATAR)) {
371 imageUrl = WEBSERVER.URL + videoChannel.Actor.Avatars[0].getStaticPath()
372 }
373 } else if (account) {
374 name = account.getDisplayName()
375 description = account.description
376
377 if (account.Actor.hasImage(ActorImageType.AVATAR)) {
378 imageUrl = WEBSERVER.URL + account.Actor.Avatars[0].getStaticPath()
379 }
380 } else if (video) {
381 name = video.name
382 description = video.description
383 } else {
384 name = CONFIG.INSTANCE.NAME
385 description = CONFIG.INSTANCE.DESCRIPTION
386 }
387
388 return { name, description, imageUrl }
389}
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index e8833d58c..8574a9e7b 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -6,7 +6,6 @@ export * from './feeds'
6export * from './services' 6export * from './services'
7export * from './static' 7export * from './static'
8export * from './lazy-static' 8export * from './lazy-static'
9export * from './live'
10export * from './misc' 9export * from './misc'
11export * from './webfinger' 10export * from './webfinger'
12export * from './tracker' 11export * from './tracker'
diff --git a/server/controllers/live.ts b/server/controllers/live.ts
deleted file mode 100644
index 81008f120..000000000
--- a/server/controllers/live.ts
+++ /dev/null
@@ -1,32 +0,0 @@
1import cors from 'cors'
2import express from 'express'
3import { mapToJSON } from '@server/helpers/core-utils'
4import { LiveSegmentShaStore } from '@server/lib/live'
5import { HttpStatusCode } from '@shared/models'
6
7const liveRouter = express.Router()
8
9liveRouter.use('/segments-sha256/:videoUUID',
10 cors(),
11 getSegmentsSha256
12)
13
14// ---------------------------------------------------------------------------
15
16export {
17 liveRouter
18}
19
20// ---------------------------------------------------------------------------
21
22function getSegmentsSha256 (req: express.Request, res: express.Response) {
23 const videoUUID = req.params.videoUUID
24
25 const result = LiveSegmentShaStore.Instance.getSegmentsSha256(videoUUID)
26
27 if (!result) {
28 return res.status(HttpStatusCode.NOT_FOUND_404).end()
29 }
30
31 return res.json(mapToJSON(result))
32}
diff --git a/server/controllers/well-known.ts b/server/controllers/well-known.ts
index f467bd629..ce5883571 100644
--- a/server/controllers/well-known.ts
+++ b/server/controllers/well-known.ts
@@ -5,6 +5,7 @@ import { root } from '@shared/core-utils'
5import { CONFIG } from '../initializers/config' 5import { CONFIG } from '../initializers/config'
6import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants' 6import { ROUTE_CACHE_LIFETIME, WEBSERVER } from '../initializers/constants'
7import { cacheRoute } from '../middlewares/cache/cache' 7import { cacheRoute } from '../middlewares/cache/cache'
8import { handleStaticError } from '@server/middlewares'
8 9
9const wellKnownRouter = express.Router() 10const wellKnownRouter = express.Router()
10 11
@@ -69,6 +70,12 @@ wellKnownRouter.use('/.well-known/host-meta',
69 } 70 }
70) 71)
71 72
73wellKnownRouter.use('/.well-known/',
74 cacheRoute(ROUTE_CACHE_LIFETIME.WELL_KNOWN),
75 express.static(CONFIG.STORAGE.WELL_KNOWN_DIR, { fallthrough: false }),
76 handleStaticError
77)
78
72// --------------------------------------------------------------------------- 79// ---------------------------------------------------------------------------
73 80
74export { 81export {