some docs on backend

This commit is contained in:
me 2025-03-15 17:39:10 +02:00
parent f7918b45f1
commit f0e8b91e78
8 changed files with 100 additions and 34 deletions

View File

@ -2,7 +2,7 @@
"domain": "localhost", "domain": "localhost",
"port": 8080, "port": 8080,
"db_path": "./ucs.db", "db_path": "./ucs.db",
"limits_per_second": { "limits_per_minute": {
"get": 500, "get": 500,
"post": 20 "post": 20
}, },

View File

@ -1,22 +1,40 @@
/**
* Comment fetch and submit APIs.
*/
import express from "express"; import express from "express";
import utils from "./utils.mjs"; import utils from "./utils.mjs";
import db from "./db.mjs"; import db from "./db.mjs";
// server // Router
const router = express.Router(); const router = express.Router();
export default router; export default router;
router.use(express.json()); 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 // POST
/**
* Handles the submission of a new comment into :site with page *.
*/
router.post("/:site/*", utils.post_limiter, (req, res) => { router.post("/:site/*", utils.post_limiter, (req, res) => {
const site_url = req.params.site; const site_url = req.params.site;
const path = req.params[0]; const path = req.params[0];
// Validate token and message are not empty.
if (!req.body.token || !req.body.message) { if (!req.body.token || !req.body.message) {
res.status(400).json("הודעה ריקה."); res.status(400).json("הודעה ריקה.");
return; return;
@ -29,10 +47,10 @@ router.post("/:site/*", utils.post_limiter, (req, res) => {
reply_to: req.body.reply_to || null, reply_to: req.body.reply_to || null,
}; };
// validation
const user_token = req.body.token; const user_token = req.body.token;
const site = db.siteInfo(site_url); const site = db.siteInfo(site_url);
// Other validations.
if (user_token !== site.info.comment_token) { if (user_token !== site.info.comment_token) {
res.status(403).json("תשובת סינון שגויה."); res.status(403).json("תשובת סינון שגויה.");
} else if (comment.user.length > site.max_lengths.user) { } 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) { } else if (comment.message > site.max_lengths.message) {
res.status(400).json("הודעה ארוכה מדי."); res.status(400).json("הודעה ארוכה מדי.");
} else { } else {
// If all validations pass, insert the comment.
res.json(db.insertPageComment(site_url, path, 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);
});

View File

@ -1,11 +1,14 @@
/**
* Read relevant information from configuration file.
*
* See the `config/default.json` file for an example config.
*/
import config from "config"; import config from "config";
const configuration = config.util.toObject(); const configuration = config.util.toObject();
// console.log(JSON.stringify(configuration));
export default { export default {
config: configuration, ...config,
getSite: (site_url) => { getSite: (site_url) => {
return configuration.sites[site_url]; return configuration.sites[site_url];
}, },

View File

@ -1,24 +1,53 @@
/**
* Database interactions.
*/
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import { migrate } from "@blackglory/better-sqlite3-migrations"; import { migrate } from "@blackglory/better-sqlite3-migrations";
import utils from "./utils.mjs"; import utils from "./utils.mjs";
import config from "./config.mjs"; import config from "./config.mjs";
// class // Interface.
export class DB { export class DB {
/**
* Connect to a database and perform migrations.
*/
constructor() { constructor() {
this.my_db = createDB(); 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<!ObjType>} comment objects.
*/
siteComments(site_url) { siteComments(site_url) {
return getSiteComments(this.my_db, 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<!ObjType>} comment objects.
*/
pageComments(site_url, page) { pageComments(site_url, page) {
return getPageComments(this.my_db, 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) { insertPageComment(site_url, path, comment) {
return insertPageComment(this.my_db, site_url, path, comment); return insertPageComment(this.my_db, site_url, path, comment);
} }
@ -27,7 +56,7 @@ export class DB {
const db = new DB(); const db = new DB();
export default db; export default db;
// migrations // Migrations
function migrations() { function migrations() {
return [ return [

View File

@ -1,19 +1,23 @@
/**
* Serve Atom feeds for comments on sites.
*/
import express from "express"; import express from "express";
import { Feed } from "feed"; import { Feed } from "feed";
import utils from "./utils.mjs"; import utils from "./utils.mjs";
import db from "./db.mjs"; import db from "./db.mjs";
// Feed // Router
const router = express.Router(); const router = express.Router();
export default router; export default router;
router.use(express.json()); router.use(express.json());
const domain = utils.domain; 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) => { router.get("/:site", utils.get_limiter, (req, res) => {
const site = req.params.site; const site = req.params.site;
@ -44,6 +48,9 @@ router.get("/:site", utils.get_limiter, (req, res) => {
res.send(xml); res.send(xml);
}); });
/**
* Serve feed comments in a specific :site and page as XML Atom feed.
*/
router.get("/:site/*", utils.get_limiter, (req, res) => { router.get("/:site/*", utils.get_limiter, (req, res) => {
const site = req.params.site; const site = req.params.site;
const path = req.params[0]; const path = req.params[0];

View File

@ -2,7 +2,7 @@ import express from "express";
import path from "node:path"; import path from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { app } from "./server.mjs"; import app from "./server.mjs";
import api from "./api.mjs"; import api from "./api.mjs";
import feed from "./feed.mjs"; import feed from "./feed.mjs";
import utils from "./utils.mjs"; import utils from "./utils.mjs";

View File

@ -1,3 +1,6 @@
/**
* Setup the server app and apply common middlewares.
*/
import express from "express"; import express from "express";
import compression from "compression"; import compression from "compression";
import helmet from "helmet"; import helmet from "helmet";
@ -8,10 +11,15 @@ import utils from "./utils.mjs";
// server // server
export const app = express(); const app = express();
// get form data as json.
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// compress responses.
app.use(compression()); app.use(compression());
// set security policies.
app.use( app.use(
helmet.contentSecurityPolicy({ helmet.contentSecurityPolicy({
directives: { directives: {
@ -19,8 +27,10 @@ app.use(
}, },
}), }),
); );
// add logging.
app.use(morgan("combined")); app.use(morgan("combined"));
// set cors options
const corsOptions = { const corsOptions = {
origin: utils.cors, origin: utils.cors,
optionsSuccessStatus: 200, optionsSuccessStatus: 200,
@ -28,4 +38,7 @@ const corsOptions = {
app.use(cors(corsOptions)); app.use(cors(corsOptions));
// trust reverse proxies.
app.set("trust proxy", "127.0.0.1"); app.set("trust proxy", "127.0.0.1");
export default app;

View File

@ -1,26 +1,29 @@
/**
* Utilities and constants.
*/
import RateLimit from "express-rate-limit"; import RateLimit from "express-rate-limit";
import config from "./config.mjs"; import config from "./config.mjs";
// Constants // 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 () { const cors = (function () {
let origins = new Set(); let origins = new Set();
for (const site in config.config.sites) { for (const site in config.sites) {
config.config.sites[site].cors.forEach((origin) => { config.sites[site].cors.forEach((origin) => {
origins.add(origin); origins.add(origin);
}); });
} }
return Array.from(origins); return Array.from(origins);
})(); })();
// functions // Functions
function escapeHtml(unsafe) { function escapeHtml(unsafe) {
return unsafe return unsafe
@ -31,7 +34,7 @@ function escapeHtml(unsafe) {
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }
// limiters // Limiters
const limiter = (limit) => const limiter = (limit) =>
RateLimit({ RateLimit({
@ -45,6 +48,6 @@ export default {
db_path, db_path,
cors, cors,
escapeHtml, escapeHtml,
get_limiter: limiter(config.config.limits_per_second.get), get_limiter: limiter(config.limits_per_minute.get),
post_limiter: limiter(config.config.limits_per_second.post), post_limiter: limiter(config.limits_per_minute.post),
}; };