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 { 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(sql: SQLQuery, ...args: unknown[]): Promise { await this.init(); return Sentry.startSpan(this.buildSpanOptions(sql), () => this.db!.get(sql, ...args)); } async each(sql: SQLQuery, callback: (err: unknown, row: T) => void): Promise { await this.init(); return Sentry.startSpan(this.buildSpanOptions(sql), () => this.db!.each(sql, callback)); } async all(sql: SQLQuery, ...args: unknown[]): Promise { await this.init(); return Sentry.startSpan(this.buildSpanOptions(sql), () => this.db!.all(sql, ...args)); } async close(): Promise { 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 => { if (!req.user) { return false; } const user = await req.db.get(SQL`SELECT bannedReason FROM users WHERE id = ${req.user.id}`) as Pick; 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));