PronounsPage/server/index.ts
Valentyne Stigloher 9335a1bd48 Merge branch 'admin-blog-preview' into 'main'
admin blog preview

See merge request PronounsPage/PronounsPage!548
2025-01-02 13:27:45 +00:00

177 lines
6.0 KiB
TypeScript

import * as Sentry from '@sentry/node';
import type { StartSpanOptions } from '@sentry/types';
import express from 'express';
import type { Request, Response, NextFunction } from 'express';
import session from 'express-session';
import grant from 'grant';
import { useBase } from 'h3';
import { defineExpressHandler, getH3Event } from 'h3-express';
import memorystore from 'memorystore';
import SQL from 'sql-template-strings';
import buildLocaleList from '../src/buildLocaleList.ts';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import formatError from '../src/error.ts';
import type { User } from '../src/user.ts';
import './globals.ts';
import dbConnection from './db.ts';
import type { Database, SQLQuery } from './db.ts';
import adminRoute from './express/admin.ts';
import bannerRoute from './express/banner.ts';
import calendarRoute from './express/calendar.ts';
import censusRoute from './express/census.ts';
import discord from './express/discord.ts';
import grantOverridesRoute from './express/grantOverrides.ts';
import homeRoute from './express/home.ts';
import imagesRoute from './express/images.ts';
import inclusiveRoute from './express/inclusive.ts';
import mfaRoute from './express/mfa.ts';
import namesRoute from './express/names.ts';
import nounsRoute from './express/nouns.ts';
import profileRoute from './express/profile.ts';
import pronounceRoute from './express/pronounce.ts';
import pronounsRoute from './express/pronouns.ts';
import sentryRoute from './express/sentry.ts';
import sourcesRoute from './express/sources.ts';
import subscriptionRoute from './express/subscription.ts';
import termsRoute from './express/terms.ts';
import translationsRoute from './express/translations.ts';
import userRoute from './express/user.ts';
import { config } from './social.ts';
import { closeAuditLogConnection } from '~/server/audit.ts';
import useAuthentication from '~/server/utils/useAuthentication.ts';
const MemoryStore = memorystore(session);
const router = express.Router();
router.use(session({
secret: process.env.SECRET!,
cookie: { ...longtimeCookieSetting, sameSite: undefined }, // somehow, sameSite=lax breaks sign-in with apple 🙄
resave: false,
saveUninitialized: false,
store: new MemoryStore({
checkPeriod: 86400000, // 24h
}),
}));
export class LazyDatabase implements Database {
db: Database | null;
constructor() {
this.db = null;
}
async init(): Promise<void> {
if (this.db === null) {
this.db = await dbConnection();
// https://kerkour.com/sqlite-for-servers
await this.db.get('PRAGMA journal_mode = WAL;');
await this.db.get('PRAGMA busy_timeout = 5000;');
await this.db.get('PRAGMA synchronous = NORMAL;');
await this.db.get('PRAGMA cache_size = 1000000000;');
await this.db.get('PRAGMA foreign_keys = true;');
await this.db.get('PRAGMA temp_store = memory;');
}
}
buildSpanOptions(sql: SQLQuery): StartSpanOptions {
return {
name: typeof sql === 'string' ? sql : sql.sql,
op: 'db',
attributes: {
'db.system': 'sqlite',
},
};
}
async get<T = unknown>(sql: SQLQuery, ...args: unknown[]): Promise<T | undefined> {
await this.init();
return Sentry.startSpan(this.buildSpanOptions(sql), () => this.db!.get(sql, ...args));
}
async each<T = unknown>(sql: SQLQuery, callback: (err: unknown, row: T) => void): Promise<number> {
await this.init();
return Sentry.startSpan(this.buildSpanOptions(sql), () => this.db!.each(sql, callback));
}
async all<T = unknown>(sql: SQLQuery, ...args: unknown[]): Promise<T[]> {
await this.init();
return Sentry.startSpan(this.buildSpanOptions(sql), () => this.db!.all(sql, ...args));
}
async close(): Promise<void> {
if (this.db !== null) {
try {
await this.db.close();
} catch (error) {
Sentry.captureException(error);
}
}
}
}
router.use(async function (req, res, next) {
try {
const authentication = await useAuthentication(getH3Event(req));
req.rawUser = authentication.rawUser;
req.user = authentication.user;
req.isGranted = authentication.isGranted;
req.locales = buildLocaleList(global.config.locale, global.config.locale === '_');
req.db = new LazyDatabase();
req.isUserAllowedToPost = async (): Promise<boolean> => {
if (!req.user) {
return false;
}
const user = await req.db.get(SQL`SELECT bannedReason FROM users WHERE id = ${req.user.id}`) as Pick<User, 'bannedReason'>;
return user && !user.bannedReason;
};
res.on('finish', async () => {
await req.db.close();
await closeAuditLogConnection();
});
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Headers', 'authorization,content-type');
// necessary for grant and not added by h3-express
res.locals ??= {};
next();
} catch (err) {
next(err);
}
});
router.use(grantOverridesRoute);
router.use(grant.express()(config));
router.use(homeRoute);
router.use(bannerRoute);
router.use(userRoute);
router.use(profileRoute);
router.use(adminRoute);
router.use(mfaRoute);
router.use(pronounsRoute);
router.use(sourcesRoute);
router.use(nounsRoute);
router.use(inclusiveRoute);
router.use(termsRoute);
router.use(pronounceRoute);
router.use(censusRoute);
router.use(namesRoute);
router.use(imagesRoute);
router.use(calendarRoute);
router.use(translationsRoute);
router.use(subscriptionRoute);
router.use(discord);
router.use(sentryRoute);
router.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error(formatError(err, req));
res.status(500).send('Unexpected server error');
req.db.close();
closeAuditLogConnection();
});
export default useBase('/api', defineExpressHandler(router));