Initial commit
Some checks failed
NPM Audit Check / Check NPM audit (push) Has been cancelled
App Image CI / Build app image (push) Has been cancelled

This commit is contained in:
2025-10-09 18:48:59 -05:00
commit 237d076668
17 changed files with 2357 additions and 0 deletions

103
src/Web.ts Normal file
View File

@@ -0,0 +1,103 @@
import * as http from "http";
import crypto from 'crypto';
import express from 'express';
import session from 'express-session';
import ky from 'ky';
import bodyParser from 'body-parser';
import type { Express } from 'express';
interface WebConfig {
sessionSecret?: string;
port?: number;
secure?: boolean;
}
/**
* I still hate typescript.
*/
function notStupidParseInt(v: string | undefined): number {
return v === undefined ? NaN : parseInt(v);
}
class Web {
private _webserver: http.Server | null = null;
private app: Express | null = null;
private port: number;
private options: WebConfig;
constructor(options: WebConfig = {}) {
this.options = options;
this.port = notStupidParseInt(process.env['PORT']) || options['port'] as number || 8080;
}
initialize = async () => {
const options = this.options;
const sessionSecret = process.env['SESSIONSECRET'] || options.sessionSecret;
const app: Express = this.app = express();
if(!sessionSecret) {
console.error('sessionSecret is required.');
throw new Error('sessionSecret is required.');
}
app.set('trust proxy', 1);
app.set('view engine', 'ejs');
app.set('view options', { outputFunctionName: 'echo' });
app.use('/assets', express.static('assets', { maxAge: '30 days' }));
app.use(session({
name: 'sessionId',
secret: sessionSecret,
resave: true,
saveUninitialized: false,
store: undefined,
cookie: {
maxAge: notStupidParseInt(process.env['COOKIETTL']) || 1000 * 60 * 60 * 24 * 30, // 30 days
httpOnly: true,
secure: !!options.secure
}
}));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use((_req, res, next) => {
crypto.randomBytes(32, (err, randomBytes) => {
if (err) {
console.error(err);
next(err);
} else {
res.locals['cspNonce'] = randomBytes.toString("hex");
next();
}
});
});
app.get('/healthcheck', (_, res) => {
res.send('Healthy');
});
app.get('/', (_, res) => {
res.render('index', {
page: {
title: 'Web',
titlesuffix: 'Home',
description: 'Homepage'
}
});
});
app.get('/ky', async (_, res) => {
res.send(await (await ky.get('https://sanin.dev')).text());
});
this._webserver = this.app.listen(this.port, () => console.log(`archery is running on port ${this.port}`));
}
close = () => {
if (this._webserver) {
this._webserver.close();
}
}
}
export default Web;
export { Web, notStupidParseInt };
export type { WebConfig };

28
src/index.ts Normal file
View File

@@ -0,0 +1,28 @@
import fs from 'fs';
import path from 'path';
import JSON5 from 'json5';
import { Web, type WebConfig } from './Web.js';
interface compositeConfig {
web?: WebConfig
}
async function readConfig(): Promise<compositeConfig> {
try {
return JSON5.parse(await fs.promises.readFile(process.env['config'] || process.env['CONFIG'] || path.join(process.cwd(), 'config', 'config.jsonc'), 'utf-8'));
}
catch (err) {
console.error('No config file found, using default config');
console.error(err);
return {};
}
}
const config: compositeConfig = await readConfig();
const web = new Web(config.web);
await web.initialize();
process.on('SIGTERM', () => {
web.close();
});