aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers/client.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2023-07-31 14:34:36 +0200
committerChocobozzz <me@florianbigard.com>2023-08-11 15:02:33 +0200
commit3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch)
treee4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/controllers/client.ts
parent04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff)
downloadPeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst
PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge conflicts, but it's a major step forward: * Server can be faster at startup because imports() are async and we can easily lazy import big modules * Angular doesn't seem to support ES import (with .js extension), so we had to correctly organize peertube into a monorepo: * Use yarn workspace feature * Use typescript reference projects for dependencies * Shared projects have been moved into "packages", each one is now a node module (with a dedicated package.json/tsconfig.json) * server/tools have been moved into apps/ and is now a dedicated app bundled and published on NPM so users don't have to build peertube cli tools manually * server/tests have been moved into packages/ so we don't compile them every time we want to run the server * Use isolatedModule option: * Had to move from const enum to const (https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums) * Had to explictely specify "type" imports when used in decorators * Prefer tsx (that uses esbuild under the hood) instead of ts-node to load typescript files (tests with mocha or scripts): * To reduce test complexity as esbuild doesn't support decorator metadata, we only test server files that do not import server models * We still build tests files into js files for a faster CI * Remove unmaintained peertube CLI import script * Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/controllers/client.ts')
-rw-r--r--server/controllers/client.ts236
1 files changed, 0 insertions, 236 deletions
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
deleted file mode 100644
index 2d0c49904..000000000
--- a/server/controllers/client.ts
+++ /dev/null
@@ -1,236 +0,0 @@
1import express from 'express'
2import { constants, promises as fs } from 'fs'
3import { readFile } from 'fs-extra'
4import { join } from 'path'
5import { logger } from '@server/helpers/logger'
6import { CONFIG } from '@server/initializers/config'
7import { Hooks } from '@server/lib/plugins/hooks'
8import { root } from '@shared/core-utils'
9import { buildFileLocale, getCompleteLocale, is18nLocale, LOCALE_FILES } from '@shared/core-utils/i18n'
10import { HttpStatusCode } from '@shared/models'
11import { STATIC_MAX_AGE } from '../initializers/constants'
12import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html'
13import { asyncMiddleware, buildRateLimiter, embedCSP } from '../middlewares'
14
15const clientsRouter = express.Router()
16
17const clientsRateLimiter = buildRateLimiter({
18 windowMs: CONFIG.RATES_LIMIT.CLIENT.WINDOW_MS,
19 max: CONFIG.RATES_LIMIT.CLIENT.MAX
20})
21
22const distPath = join(root(), 'client', 'dist')
23const testEmbedPath = join(distPath, 'standalone', 'videos', 'test-embed.html')
24
25// Special route that add OpenGraph and oEmbed tags
26// Do not use a template engine for a so little thing
27clientsRouter.use([ '/w/p/:id', '/videos/watch/playlist/:id' ],
28 clientsRateLimiter,
29 asyncMiddleware(generateWatchPlaylistHtmlPage)
30)
31
32clientsRouter.use([ '/w/:id', '/videos/watch/:id' ],
33 clientsRateLimiter,
34 asyncMiddleware(generateWatchHtmlPage)
35)
36
37clientsRouter.use([ '/accounts/:nameWithHost', '/a/:nameWithHost' ],
38 clientsRateLimiter,
39 asyncMiddleware(generateAccountHtmlPage)
40)
41
42clientsRouter.use([ '/video-channels/:nameWithHost', '/c/:nameWithHost' ],
43 clientsRateLimiter,
44 asyncMiddleware(generateVideoChannelHtmlPage)
45)
46
47clientsRouter.use('/@:nameWithHost',
48 clientsRateLimiter,
49 asyncMiddleware(generateActorHtmlPage)
50)
51
52const embedMiddlewares = [
53 clientsRateLimiter,
54
55 CONFIG.CSP.ENABLED
56 ? embedCSP
57 : (req: express.Request, res: express.Response, next: express.NextFunction) => next(),
58
59 // Set headers
60 (req: express.Request, res: express.Response, next: express.NextFunction) => {
61 res.removeHeader('X-Frame-Options')
62
63 // Don't cache HTML file since it's an index to the immutable JS/CSS files
64 res.setHeader('Cache-Control', 'public, max-age=0')
65
66 next()
67 },
68
69 asyncMiddleware(generateEmbedHtmlPage)
70]
71
72clientsRouter.use('/videos/embed', ...embedMiddlewares)
73clientsRouter.use('/video-playlists/embed', ...embedMiddlewares)
74
75const testEmbedController = (req: express.Request, res: express.Response) => res.sendFile(testEmbedPath)
76
77clientsRouter.use('/videos/test-embed', clientsRateLimiter, testEmbedController)
78clientsRouter.use('/video-playlists/test-embed', clientsRateLimiter, testEmbedController)
79
80// Dynamic PWA manifest
81clientsRouter.get('/manifest.webmanifest', clientsRateLimiter, asyncMiddleware(generateManifest))
82
83// Static client overrides
84// Must be consistent with static client overrides redirections in /support/nginx/peertube
85const staticClientOverrides = [
86 'assets/images/logo.svg',
87 'assets/images/favicon.png',
88 'assets/images/icons/icon-36x36.png',
89 'assets/images/icons/icon-48x48.png',
90 'assets/images/icons/icon-72x72.png',
91 'assets/images/icons/icon-96x96.png',
92 'assets/images/icons/icon-144x144.png',
93 'assets/images/icons/icon-192x192.png',
94 'assets/images/icons/icon-512x512.png',
95 'assets/images/default-playlist.jpg',
96 'assets/images/default-avatar-account.png',
97 'assets/images/default-avatar-account-48x48.png',
98 'assets/images/default-avatar-video-channel.png',
99 'assets/images/default-avatar-video-channel-48x48.png'
100]
101
102for (const staticClientOverride of staticClientOverrides) {
103 const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride)
104 clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath)))
105}
106
107clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations)
108clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT }))
109
110// 404 for static files not found
111clientsRouter.use('/client/*', (req: express.Request, res: express.Response) => {
112 res.status(HttpStatusCode.NOT_FOUND_404).end()
113})
114
115// Always serve index client page (the client is a single page application, let it handle routing)
116// Try to provide the right language index.html
117clientsRouter.use('/(:language)?',
118 clientsRateLimiter,
119 asyncMiddleware(serveIndexHTML)
120)
121
122// ---------------------------------------------------------------------------
123
124export {
125 clientsRouter
126}
127
128// ---------------------------------------------------------------------------
129
130function serveServerTranslations (req: express.Request, res: express.Response) {
131 const locale = req.params.locale
132 const file = req.params.file
133
134 if (is18nLocale(locale) && LOCALE_FILES.includes(file)) {
135 const completeLocale = getCompleteLocale(locale)
136 const completeFileLocale = buildFileLocale(completeLocale)
137
138 const path = join(__dirname, `../../../client/dist/locale/${file}.${completeFileLocale}.json`)
139 return res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
140 }
141
142 return res.status(HttpStatusCode.NOT_FOUND_404).end()
143}
144
145async function generateEmbedHtmlPage (req: express.Request, res: express.Response) {
146 const hookName = req.originalUrl.startsWith('/video-playlists/')
147 ? 'filter:html.embed.video-playlist.allowed.result'
148 : 'filter:html.embed.video.allowed.result'
149
150 const allowParameters = { req }
151
152 const allowedResult = await Hooks.wrapFun(
153 isEmbedAllowed,
154 allowParameters,
155 hookName
156 )
157
158 if (!allowedResult || allowedResult.allowed !== true) {
159 logger.info('Embed is not allowed.', { allowedResult })
160
161 return sendHTML(allowedResult?.html || '', res)
162 }
163
164 const html = await ClientHtml.getEmbedHTML()
165
166 return sendHTML(html, res)
167}
168
169async function generateWatchHtmlPage (req: express.Request, res: express.Response) {
170 // Thread link is '/w/:videoId;threadId=:threadId'
171 // So to get the videoId we need to remove the last part
172 let videoId = req.params.id + ''
173
174 const threadIdIndex = videoId.indexOf(';threadId')
175 if (threadIdIndex !== -1) videoId = videoId.substring(0, threadIdIndex)
176
177 const html = await ClientHtml.getWatchHTMLPage(videoId, req, res)
178
179 return sendHTML(html, res, true)
180}
181
182async function generateWatchPlaylistHtmlPage (req: express.Request, res: express.Response) {
183 const html = await ClientHtml.getWatchPlaylistHTMLPage(req.params.id + '', req, res)
184
185 return sendHTML(html, res, true)
186}
187
188async function generateAccountHtmlPage (req: express.Request, res: express.Response) {
189 const html = await ClientHtml.getAccountHTMLPage(req.params.nameWithHost, req, res)
190
191 return sendHTML(html, res, true)
192}
193
194async function generateVideoChannelHtmlPage (req: express.Request, res: express.Response) {
195 const html = await ClientHtml.getVideoChannelHTMLPage(req.params.nameWithHost, req, res)
196
197 return sendHTML(html, res, true)
198}
199
200async function generateActorHtmlPage (req: express.Request, res: express.Response) {
201 const html = await ClientHtml.getActorHTMLPage(req.params.nameWithHost, req, res)
202
203 return sendHTML(html, res, true)
204}
205
206async function generateManifest (req: express.Request, res: express.Response) {
207 const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest')
208 const manifestJson = await readFile(manifestPhysicalPath, 'utf8')
209 const manifest = JSON.parse(manifestJson)
210
211 manifest.name = CONFIG.INSTANCE.NAME
212 manifest.short_name = CONFIG.INSTANCE.NAME
213 manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION
214
215 res.json(manifest)
216}
217
218function serveClientOverride (path: string) {
219 return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
220 try {
221 await fs.access(path, constants.F_OK)
222 // Serve override client
223 res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
224 } catch {
225 // Serve dist client
226 next()
227 }
228 }
229}
230
231type AllowedResult = { allowed: boolean, html?: string }
232function isEmbedAllowed (_object: {
233 req: express.Request
234}): AllowedResult {
235 return { allowed: true }
236}