PronounsPage/server/index.ts
2024-06-26 13:59:53 +02:00

195 lines
6.3 KiB
TypeScript

import './setup.ts';
import express from 'express';
import type { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import type { StartSpanOptions } from '@sentry/types';
import authenticate from '../src/authenticate.ts';
import dbConnection from './db.ts';
import type { Database, SQLQuery } from './db.ts';
import cookieParser from 'cookie-parser';
import grant from 'grant';
import router from './routes/user.ts';
import { isGranted } from '../src/helpers.ts';
import buildLocaleList from '../src/buildLocaleList.ts';
import { longtimeCookieSetting } from '../src/cookieSettings.ts';
import SQL from 'sql-template-strings';
import formatError from '../src/error.ts';
import type { User } from '../src/user.ts';
import session from 'express-session';
import memorystore from 'memorystore';
import './globals.ts';
import { config } from './social.ts';
import grantOverridesRoute from './routes/grantOverrides.ts';
import homeRoute from './routes/home.ts';
import bannerRoute from './routes/banner.ts';
import userRoute from './routes/user.ts';
import profileRoute from './routes/profile.ts';
import adminRoute from './routes/admin.ts';
import mfaRoute from './routes/mfa.ts';
import pronounsRoute from './routes/pronouns.ts';
import sourcesRoute from './routes/sources.ts';
import nounsRoute from './routes/nouns.ts';
import inclusiveRoute from './routes/inclusive.ts';
import termsRoute from './routes/terms.ts';
import pronounceRoute from './routes/pronounce.ts';
import censusRoute from './routes/census.ts';
import namesRoute from './routes/names.ts';
import imagesRoute from './routes/images.ts';
import blogRoute from './routes/blog.ts';
import calendarRoute from './routes/calendar.ts';
import translationsRoute from './routes/translations.ts';
import subscriptionRoute from './routes/subscription.ts';
import discord from './routes/discord.ts';
const MemoryStore = memorystore(session);
const app = express();
app.enable('trust proxy');
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(express.json({
verify: (req, res, buf) => {
if (buf.includes(Buffer.from('narodowcy.net', 'utf-8'))) {
req.socket.end();
throw 'fuck off';
}
},
}));
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.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
}),
}));
// app.use(csurf({ cookie: true }));
// app.use(csrfHandleError())
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);
}
}
}
}
app.use(async function (req, res, next) {
try {
req.rawUser = authenticate(req);
req.user = req.rawUser && req.rawUser.authenticated ? req.rawUser : null;
req.isGranted = (area: string = '', locale = global.config.locale): boolean => {
return !!req.user && isGranted(req.user, locale, area);
};
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();
});
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Headers', 'authorization,content-type');
next();
} catch (err) {
next(err);
}
});
app.use(grantOverridesRoute);
router.use(grant.express()(config));
app.use(homeRoute);
app.use(bannerRoute);
app.use(userRoute);
app.use(profileRoute);
app.use(adminRoute);
app.use(mfaRoute);
app.use(pronounsRoute);
app.use(sourcesRoute);
app.use(nounsRoute);
app.use(inclusiveRoute);
app.use(termsRoute);
app.use(pronounceRoute);
app.use(censusRoute);
app.use(namesRoute);
app.use(imagesRoute);
app.use(blogRoute);
app.use(calendarRoute);
app.use(translationsRoute);
app.use(subscriptionRoute);
app.use(discord);
app.use(Sentry.Handlers.errorHandler());
app.use((err: Error, req: Request, res: Response, _next: NextFunction) => {
console.error(formatError(err, req));
res.status(500).send('Unexpected server error');
req.db.close();
});
export default {
path: '/api',
handler: app,
};