Files
chore-chart/src/Web.ts
Cory Sanin f653af501f
All checks were successful
App Image CI / Build app image (push) Successful in 35s
NPM Audit Check / Check NPM audit (push) Successful in -2m9s
minor html changes
2025-10-15 00:12:39 -05:00

146 lines
4.7 KiB
TypeScript

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<string, GrocyUser> = {};
const missing: Set<number> = 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 };