205 lines
7.3 KiB
TypeScript
205 lines
7.3 KiB
TypeScript
import Fastify from 'fastify'
|
|
import prismaPlugin from './plugins/prisma'
|
|
import vodsRoutes from './plugins/vods'
|
|
import uploadsRoutes from './plugins/uploads'
|
|
import vtubersRoutes from './plugins/vtubers'
|
|
import usersRoutes from './plugins/users'
|
|
import indexRoutes from './plugins/index'
|
|
import streamsRoutes from './plugins/streams'
|
|
import adminRoutes from './plugins/admin'
|
|
import hls from './plugins/hls.ts'
|
|
import fastifyStatic from '@fastify/static'
|
|
import fastifySecureSession from '@fastify/secure-session'
|
|
import path, { basename } from 'node:path'
|
|
import fastifyFormbody from '@fastify/formbody'
|
|
import fastifyView from "@fastify/view"
|
|
import { env } from './config/env'
|
|
import { constants } from './config/constants'
|
|
import authRoutes from './plugins/auth'
|
|
import Handlebars from 'handlebars'
|
|
import graphileWorker from './plugins/graphileWorker'
|
|
import fastifySwagger from "@fastify/swagger"
|
|
import fastifySwaggerUi from "@fastify/swagger-ui"
|
|
import { join } from 'node:path'
|
|
import { format } from 'date-fns'
|
|
import * as jdenticon from 'jdenticon'
|
|
import { Role } from '../generated/prisma'
|
|
import fastifyFlash from '@fastify/flash'
|
|
import { isModerator, hasRole } from './utils/privs'
|
|
import { signUrl } from './utils/cdn'
|
|
import { extractBasePath } from './utils/filesystem'
|
|
import { truncate } from './utils/formatters.ts'
|
|
import { icons } from './utils/icons.ts'
|
|
import logger from './utils/logger.ts'
|
|
import fastifyCaching from '@fastify/caching'
|
|
|
|
export function buildApp() {
|
|
const app = Fastify()
|
|
|
|
Handlebars.registerHelper('formatDate', function (dateString) {
|
|
if (!dateString) return ''
|
|
return format(new Date(dateString), 'yyyy-MM-dd')
|
|
})
|
|
Handlebars.registerHelper('identicon', function (str, size = 48) {
|
|
return jdenticon.toSvg(str, size)
|
|
})
|
|
Handlebars.registerHelper('safeJson', function (context) {
|
|
return new Handlebars.SafeString(JSON.stringify(context));
|
|
});
|
|
Handlebars.registerHelper('json', function (context) {
|
|
return JSON.stringify(context)
|
|
})
|
|
Handlebars.registerHelper('patron', function (user) {
|
|
if (!user.roles) {
|
|
throw new Error(
|
|
'patron hbs helper was called without roles. This usually means you forgot to include roles relationship in the query.'
|
|
);
|
|
}
|
|
return user.roles.some((r: Role) => r.name.startsWith('supporter'));
|
|
});
|
|
Handlebars.registerHelper('notEqual', function (a, b) {
|
|
return a !== b;
|
|
});
|
|
Handlebars.registerHelper('isEqual', function (a, b) {
|
|
logger.trace(`isEqual a=${a} b=${b}`)
|
|
return a == b
|
|
});
|
|
Handlebars.registerHelper('isModerator', function (user) {
|
|
return isModerator(user)
|
|
})
|
|
Handlebars.registerHelper('hasRole', hasRole)
|
|
Handlebars.registerHelper('breaklines', function (text) {
|
|
text = Handlebars.Utils.escapeExpression(text);
|
|
text = text.replace(/(\r\n|\n|\r)/gm, '<br>');
|
|
return new Handlebars.SafeString(text);
|
|
});
|
|
Handlebars.registerHelper('getCdnUrl', function (s3Key) {
|
|
// Before you remove this log, find a way to memoize this function!
|
|
logger.info(`getCdnUrl called with CDN_ORIGIN=${env.CDN_ORIGIN} and CDN_TOKEN_SECRET=${env.CDN_TOKEN_SECRET}`)
|
|
return signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
|
|
securityKey: env.CDN_TOKEN_SECRET,
|
|
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
|
})
|
|
})
|
|
/**
|
|
* @see https://github.com/video-dev/hls.js/issues/2152
|
|
*/
|
|
Handlebars.registerHelper('signedHlsUrl', function (s3Key) {
|
|
if (!s3Key) throw new Error(`signedHlsUrl called with falsy s3Key=${s3Key}`);
|
|
const pathAllowed = extractBasePath(s3Key)
|
|
const url = signUrl(`${env.CDN_ORIGIN}/${s3Key}`, {
|
|
securityKey: env.CDN_TOKEN_SECRET,
|
|
pathAllowed,
|
|
isDirectory: true,
|
|
expirationTime: constants.timeUnits.sevenDaysInSeconds,
|
|
})
|
|
logger.debug(`pathAllowed=${pathAllowed} url=${url}`)
|
|
return url
|
|
})
|
|
Handlebars.registerHelper('basename', function (url: string) {
|
|
return basename(url)
|
|
})
|
|
Handlebars.registerHelper('trunc', function (str, length = 6) {
|
|
return truncate(str, length)
|
|
});
|
|
Handlebars.registerHelper('icon', function (name: string, size = 20) {
|
|
const svg = icons[name];
|
|
|
|
if (!svg) {
|
|
return new Handlebars.SafeString(`<!-- icon "${name}" not found -->`);
|
|
}
|
|
|
|
// Inject width/height if not already set
|
|
const sizedSvg = svg
|
|
.replace(/<svg([^>]*)>/, `<svg$1 width="${size}" height="${size}">`);
|
|
|
|
return new Handlebars.SafeString(sizedSvg);
|
|
});
|
|
|
|
const __dirname = import.meta.dirname;
|
|
const swaggerOptions = {
|
|
swagger: {
|
|
info: {
|
|
title: constants.site.title,
|
|
description: constants.site.description,
|
|
version: constants.site.version,
|
|
},
|
|
host: env.ORIGIN,
|
|
schemes: ["http", "https"],
|
|
consumes: ["application/json"],
|
|
produces: ["application/json"],
|
|
},
|
|
};
|
|
|
|
const swaggerUiOptions = {
|
|
routePrefix: "/api/docs",
|
|
exposeRoute: true,
|
|
};
|
|
|
|
app.register(fastifySwagger, swaggerOptions);
|
|
app.register(fastifySwaggerUi, swaggerUiOptions);
|
|
|
|
app.register(fastifyStatic, {
|
|
root: path.join(__dirname, 'assets'),
|
|
prefix: '/', // optional: default '/'
|
|
constraints: {} // optional: default {}
|
|
})
|
|
app.register(fastifyFormbody)
|
|
// app.register(fastifyMultipart, {
|
|
// limits: {
|
|
// fileSize: 30 * 1024 * 1024 // 30MB
|
|
// }
|
|
// })
|
|
app.register(fastifySecureSession, {
|
|
// the name of the attribute decorated on the request-object, defaults to 'session'
|
|
sessionName: 'session',
|
|
cookieName: 'fp-session',
|
|
// adapt this to point to the directory where secret-key is located
|
|
key: Buffer.from(env.COOKIE_SECRET, 'hex'),
|
|
// the amount of time the session is considered valid; this is different from the cookie options
|
|
// and based on value within the session.
|
|
expiry: 24 * 60 * 60, // Default 1 day
|
|
cookie: {
|
|
path: '/'
|
|
// options for setCookie, see https://github.com/fastify/fastify-cookie
|
|
}
|
|
})
|
|
app.register(fastifyFlash)
|
|
app.register(fastifyView, {
|
|
engine: {
|
|
handlebars: Handlebars,
|
|
},
|
|
templates: join(__dirname, '..', 'src', 'views'),
|
|
viewExt: 'hbs',
|
|
options: {
|
|
partials: {
|
|
navbar: 'partials/navbar.hbs',
|
|
footer: 'partials/footer.hbs',
|
|
commentForm: 'partials/commentForm.hbs'
|
|
}
|
|
}
|
|
})
|
|
|
|
// @todo we are going to need to use redis or similar https://github.com/fastify/fastify-caching
|
|
// app.register(fastifyCaching, {
|
|
// expiresIn: 300,
|
|
// privacy: 'private',
|
|
// serverExpiresIn: 300,
|
|
// })
|
|
|
|
app.register(graphileWorker)
|
|
app.register(prismaPlugin)
|
|
app.register(hls)
|
|
app.register(vodsRoutes)
|
|
|
|
app.register(streamsRoutes)
|
|
app.register(vtubersRoutes)
|
|
app.register(uploadsRoutes)
|
|
app.register(usersRoutes)
|
|
app.register(indexRoutes)
|
|
app.register(adminRoutes)
|
|
app.register(authRoutes)
|
|
|
|
return app
|
|
}
|