mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-07 14:28:13 -04:00

the #shared alias used by Nuxt cannot be easily disabled and to prevent breackage with jiti, we make use of it
174 lines
5.9 KiB
TypeScript
174 lines
5.9 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 './dotenv.ts';
|
|
|
|
import dbConnection from './db.ts';
|
|
import type { Database, SQLQuery } from './db.ts';
|
|
import adminRoute from './express/admin.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 imagesRoute from './express/images.ts';
|
|
import mfaRoute from './express/mfa.ts';
|
|
import profileRoute from './express/profile.ts';
|
|
import pronounceRoute from './express/pronounce.ts';
|
|
import sentryRoute from './express/sentry.ts';
|
|
import subscriptionRoute from './express/subscription.ts';
|
|
import translationsRoute from './express/translations.ts';
|
|
import userRoute from './express/user.ts';
|
|
import { config } from './social.ts';
|
|
|
|
import buildLocaleList from '#shared/buildLocaleList.ts';
|
|
import { longtimeCookieSetting } from '#shared/cookieSettings.ts';
|
|
import formatError from '#shared/error.ts';
|
|
import { closeAuditLogConnection } from '~~/server/audit.ts';
|
|
import { getLocale } from '~~/server/data.ts';
|
|
|
|
class StorageStore extends session.Store {
|
|
get(sid: string, callback: (err: unknown, session?: (session.SessionData | null)) => void): void {
|
|
useStorage('session').getItem<session.SessionData>(sid)
|
|
.then((session) => callback(null, session))
|
|
.catch((error) => callback(error));
|
|
}
|
|
|
|
set(sid: string, session: session.SessionData, callback?: (err?: unknown) => void): void {
|
|
// unwrap session to make it a primitive object (otherwise unstorage will reject serializing the object)
|
|
useStorage('session').setItem(sid, { ...session }, { ttl: 24 * 60 * 60 })
|
|
.then(() => callback?.())
|
|
.catch((error) => callback?.(error));
|
|
}
|
|
|
|
destroy(sid: string, callback?: (err?: unknown) => void): void {
|
|
useStorage('session').removeItem(sid)
|
|
.then(() => callback?.())
|
|
.catch((error) => callback?.(error));
|
|
}
|
|
}
|
|
|
|
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 StorageStore(),
|
|
}));
|
|
|
|
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();
|
|
this.db = null;
|
|
} catch (error) {
|
|
Sentry.captureException(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
router.use(async function (req, res, next) {
|
|
try {
|
|
const locale = getLocale(getH3Event(req));
|
|
|
|
const authentication = await useAuthentication(getH3Event(req));
|
|
req.rawUser = authentication.rawUser;
|
|
req.user = authentication.user;
|
|
req.isGranted = authentication.isGranted;
|
|
req.locales = buildLocaleList(locale, locale === '_');
|
|
req.db = new LazyDatabase();
|
|
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(sentryRoute);
|
|
|
|
router.use(userRoute);
|
|
router.use(profileRoute);
|
|
router.use(adminRoute);
|
|
router.use(mfaRoute);
|
|
router.use(pronounceRoute);
|
|
router.use(censusRoute);
|
|
router.use(imagesRoute);
|
|
router.use(calendarRoute);
|
|
router.use(translationsRoute);
|
|
router.use(subscriptionRoute);
|
|
router.use(discord);
|
|
|
|
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();
|
|
});
|
|
|
|
router.use((_req, res) => res.status(404).send('Not found'));
|
|
|
|
export default useBase('/api', defineExpressHandler(router));
|