From f0e8b91e78fd8c840cc4ee859c196e0ad9e3bda5 Mon Sep 17 00:00:00 2001 From: me Date: Sat, 15 Mar 2025 17:39:10 +0200 Subject: [PATCH] some docs on backend --- backend/config/default.json | 2 +- backend/src/api.mjs | 35 +++++++++++++++++++++++------------ backend/src/config.mjs | 9 ++++++--- backend/src/db.mjs | 37 +++++++++++++++++++++++++++++++++---- backend/src/feed.mjs | 13 ++++++++++--- backend/src/main.mjs | 2 +- backend/src/server.mjs | 15 ++++++++++++++- backend/src/utils.mjs | 21 ++++++++++++--------- 8 files changed, 100 insertions(+), 34 deletions(-) diff --git a/backend/config/default.json b/backend/config/default.json index 6edd2fe..d04e709 100644 --- a/backend/config/default.json +++ b/backend/config/default.json @@ -2,7 +2,7 @@ "domain": "localhost", "port": 8080, "db_path": "./ucs.db", - "limits_per_second": { + "limits_per_minute": { "get": 500, "post": 20 }, diff --git a/backend/src/api.mjs b/backend/src/api.mjs index a01cd33..f3dd7f5 100644 --- a/backend/src/api.mjs +++ b/backend/src/api.mjs @@ -1,22 +1,40 @@ +/** + * Comment fetch and submit APIs. + */ + import express from "express"; import utils from "./utils.mjs"; import db from "./db.mjs"; -// server +// Router const router = express.Router(); - export default router; - router.use(express.json()); +// GET + +/** + * Get comments on site :site and page * as JSON. + */ +router.get("/:site/*", utils.get_limiter, (req, res) => { + const site = req.params.site; + const path = req.params[0]; + const comments = db.pageComments(site, path); + res.json(comments); +}); + // POST +/** + * Handles the submission of a new comment into :site with page *. + */ router.post("/:site/*", utils.post_limiter, (req, res) => { const site_url = req.params.site; const path = req.params[0]; + // Validate token and message are not empty. if (!req.body.token || !req.body.message) { res.status(400).json("הודעה ריקה."); return; @@ -29,10 +47,10 @@ router.post("/:site/*", utils.post_limiter, (req, res) => { reply_to: req.body.reply_to || null, }; - // validation const user_token = req.body.token; const site = db.siteInfo(site_url); + // Other validations. if (user_token !== site.info.comment_token) { res.status(403).json("תשובת סינון שגויה."); } else if (comment.user.length > site.max_lengths.user) { @@ -42,14 +60,7 @@ router.post("/:site/*", utils.post_limiter, (req, res) => { } else if (comment.message > site.max_lengths.message) { res.status(400).json("הודעה ארוכה מדי."); } else { + // If all validations pass, insert the comment. res.json(db.insertPageComment(site_url, path, comment)); } }); - -// GET -router.get("/:site/*", utils.get_limiter, (req, res) => { - const site = req.params.site; - const path = req.params[0]; - const comments = db.pageComments(site, path); - res.json(comments); -}); diff --git a/backend/src/config.mjs b/backend/src/config.mjs index 55b48fe..17281ef 100644 --- a/backend/src/config.mjs +++ b/backend/src/config.mjs @@ -1,11 +1,14 @@ +/** + * Read relevant information from configuration file. + * + * See the `config/default.json` file for an example config. + */ import config from "config"; const configuration = config.util.toObject(); -// console.log(JSON.stringify(configuration)); - export default { - config: configuration, + ...config, getSite: (site_url) => { return configuration.sites[site_url]; }, diff --git a/backend/src/db.mjs b/backend/src/db.mjs index 3fbd717..033d2a3 100644 --- a/backend/src/db.mjs +++ b/backend/src/db.mjs @@ -1,24 +1,53 @@ +/** + * Database interactions. + */ import Database from "better-sqlite3"; import { migrate } from "@blackglory/better-sqlite3-migrations"; import utils from "./utils.mjs"; import config from "./config.mjs"; -// class +// Interface. export class DB { + /** + * Connect to a database and perform migrations. + */ constructor() { this.my_db = createDB(); } - siteInfo(site) { - return getSiteInfo(this.my_db, site); + /** + * Fetch information about a site. + * @param {!string} site url. + * @return {!ObjType} site information. + */ + siteInfo(site_url) { + return getSiteInfo(this.my_db, site_url); } + /** + * Fetch all comments from a specific site. + * @param {!string} site url. + * @return {!Array} comment objects. + */ siteComments(site_url) { return getSiteComments(this.my_db, site_url); } + /** + * Fetch comments from a specific page in a specific site. + * @param {!string} site url. + * @param {!string} page path. + * @return {!Array} comment objects. + */ pageComments(site_url, page) { return getPageComments(this.my_db, site_url, page); } + /** + * Insert a comment into the database. + * @param {!string} site url. + * @param {!string} page path. + * @param {!ObjType} comment. + * @return {!ObjType} inserted comment. + */ insertPageComment(site_url, path, comment) { return insertPageComment(this.my_db, site_url, path, comment); } @@ -27,7 +56,7 @@ export class DB { const db = new DB(); export default db; -// migrations +// Migrations function migrations() { return [ diff --git a/backend/src/feed.mjs b/backend/src/feed.mjs index af0e70b..e6bb87d 100644 --- a/backend/src/feed.mjs +++ b/backend/src/feed.mjs @@ -1,19 +1,23 @@ +/** + * Serve Atom feeds for comments on sites. + */ import express from "express"; import { Feed } from "feed"; import utils from "./utils.mjs"; import db from "./db.mjs"; -// Feed +// Router const router = express.Router(); - export default router; - router.use(express.json()); const domain = utils.domain; +/** + * Serve feed for all comments in a specific :site as XML Atom feed. + */ router.get("/:site", utils.get_limiter, (req, res) => { const site = req.params.site; @@ -44,6 +48,9 @@ router.get("/:site", utils.get_limiter, (req, res) => { res.send(xml); }); +/** + * Serve feed comments in a specific :site and page as XML Atom feed. + */ router.get("/:site/*", utils.get_limiter, (req, res) => { const site = req.params.site; const path = req.params[0]; diff --git a/backend/src/main.mjs b/backend/src/main.mjs index a95b0e6..26ae490 100644 --- a/backend/src/main.mjs +++ b/backend/src/main.mjs @@ -2,7 +2,7 @@ import express from "express"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { app } from "./server.mjs"; +import app from "./server.mjs"; import api from "./api.mjs"; import feed from "./feed.mjs"; import utils from "./utils.mjs"; diff --git a/backend/src/server.mjs b/backend/src/server.mjs index fb024ed..01b464f 100644 --- a/backend/src/server.mjs +++ b/backend/src/server.mjs @@ -1,3 +1,6 @@ +/** + * Setup the server app and apply common middlewares. + */ import express from "express"; import compression from "compression"; import helmet from "helmet"; @@ -8,10 +11,15 @@ import utils from "./utils.mjs"; // server -export const app = express(); +const app = express(); +// get form data as json. app.use(express.urlencoded({ extended: true })); + +// compress responses. app.use(compression()); + +// set security policies. app.use( helmet.contentSecurityPolicy({ directives: { @@ -19,8 +27,10 @@ app.use( }, }), ); +// add logging. app.use(morgan("combined")); +// set cors options const corsOptions = { origin: utils.cors, optionsSuccessStatus: 200, @@ -28,4 +38,7 @@ const corsOptions = { app.use(cors(corsOptions)); +// trust reverse proxies. app.set("trust proxy", "127.0.0.1"); + +export default app; diff --git a/backend/src/utils.mjs b/backend/src/utils.mjs index c47e947..0f6fc84 100644 --- a/backend/src/utils.mjs +++ b/backend/src/utils.mjs @@ -1,26 +1,29 @@ +/** + * Utilities and constants. + */ import RateLimit from "express-rate-limit"; import config from "./config.mjs"; // Constants -const domain = process.env.DOMAIN || config.config.domain || "localhost"; +const domain = process.env.DOMAIN || config.domain || "localhost"; -const port = process.env.PORT || config.config.port || 8080; +const port = process.env.PORT || config.port || 8080; -const db_path = process.env.DB || config.config.db_path || "ucs.db"; +const db_path = process.env.DB || config.db_path || "ucs.db"; const cors = (function () { let origins = new Set(); - for (const site in config.config.sites) { - config.config.sites[site].cors.forEach((origin) => { + for (const site in config.sites) { + config.sites[site].cors.forEach((origin) => { origins.add(origin); }); } return Array.from(origins); })(); -// functions +// Functions function escapeHtml(unsafe) { return unsafe @@ -31,7 +34,7 @@ function escapeHtml(unsafe) { .replace(/'/g, "'"); } -// limiters +// Limiters const limiter = (limit) => RateLimit({ @@ -45,6 +48,6 @@ export default { db_path, cors, escapeHtml, - get_limiter: limiter(config.config.limits_per_second.get), - post_limiter: limiter(config.config.limits_per_second.post), + get_limiter: limiter(config.limits_per_minute.get), + post_limiter: limiter(config.limits_per_minute.post), };