import * as http from "http"; import crypto from 'crypto'; import express, { type Express } from 'express'; import bodyParser from 'body-parser'; import { type GrocyUser, type Grocy } from './Grocy.js'; 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; private grocy: Grocy; constructor(options: WebConfig = {}, grocy: Grocy) { // this.options = options; this.port = notStupidParseInt(process.env['PORT']) || options['port'] as number || 8080; this.grocy = grocy } initialize = async () => { const app: Express = this.app = express(); 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(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('app', { page: { title: 'Chore Chart', titlesuffix: 'View Grocy Chores', description: 'chore-chart displays chores from a Grocy instance.' } }); }); app.get('/api/chores', async (_, res) => { const chores = await this.grocy.chores(); const users: Record = {}; const missing: Set = new Set(); chores.forEach(chore => { if ('next_execution_assigned_user' in chore) { users[chore.next_execution_assigned_to_user_id] = chore.next_execution_assigned_user } }); chores.forEach(chore => { if (!(chore.next_execution_assigned_to_user_id in users) && typeof chore.next_execution_assigned_to_user_id === 'number') { missing.add(chore.next_execution_assigned_to_user_id); } }); (await Promise.all([...missing].map(id => this.grocy.user(id)))).forEach(user => { if (!user) { return; } users[user.id] = user; }); chores.forEach(chore => { if (!('next_execution_assigned_user' in chore) && chore.next_execution_assigned_to_user_id in users) { chore.next_execution_assigned_user = users[chore.next_execution_assigned_to_user_id] as GrocyUser; } }); res.send({ chores }); }); app.post('/api/chores/:id', async (req, res) => { const doer = req.body?.done_by; const chore = req.params?.id; if (typeof doer != 'number' || doer < 0) { res.status(400).send({ error: 'done_by value is invalid.' }); return; } if (typeof chore != 'number' || chore < 0) { res.status(400).send({ error: 'chore id value is invalid.' }); return; } try { const resp = await this.grocy.doChore(chore, doer); res.send(resp); } catch (err) { console.error(err); res.status(400).send({ error: 'grocy didn\'t like that.' }); } }); app.get('/api/users/:id', async (req, res) => { const user = await this.grocy.user(parseInt(req.params.id)) res.send({ user }); }); this._webserver = this.app.listen(this.port, () => console.log(`chore-chart is running on port ${this.port}`)); } close = () => { if (this._webserver) { this._webserver.close(); } } } export default Web; export { Web, notStupidParseInt }; export type { WebConfig };