diff --git a/backend/config/default.json b/backend/config/default.json new file mode 100644 index 0000000..785b2a1 --- /dev/null +++ b/backend/config/default.json @@ -0,0 +1,24 @@ +{ + "domain": "localhost", + "port": 8080, + "db_path": "./ucs.db", + "sites": { + "localhost": { + "info": { + "id": 2, + "url": "localhost", + "comment_token": "שלום" + }, + "cors": ["localhost"], + "limits_per_second": { + "get": 500, + "post": 20 + }, + "max_lengths": { + "user": 20, + "website": 100, + "message": 200 + } + } + } +} diff --git a/backend/package-lock.json b/backend/package-lock.json index 5a82f61..b5af640 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -12,6 +12,7 @@ "@blackglory/better-sqlite3-migrations": "^0.1.19", "better-sqlite3": "^11.8.1", "compression": "^1.8.0", + "config": "^3.3.12", "cors": "^2.8.5", "express": "^4.21.2", "express-rate-limit": "^7.5.0", @@ -378,6 +379,18 @@ "dev": true, "license": "MIT" }, + "node_modules/config": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", + "integrity": "sha512-Vmx389R/QVM3foxqBzXO8t2tUikYZP64Q6vQxGrsMpREeJc/aWRnPRERXWsYzOHAumx/AOoILWe6nU3ZJL+6Sw==", + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -977,6 +990,18 @@ "node": ">=0.12.0" } }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/justypes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/justypes/-/justypes-3.1.2.tgz", diff --git a/backend/package.json b/backend/package.json index fd5d1fb..0dda1d2 100644 --- a/backend/package.json +++ b/backend/package.json @@ -18,6 +18,7 @@ "@blackglory/better-sqlite3-migrations": "^0.1.19", "better-sqlite3": "^11.8.1", "compression": "^1.8.0", + "config": "^3.3.12", "cors": "^2.8.5", "express": "^4.21.2", "express-rate-limit": "^7.5.0", diff --git a/backend/src/api.mjs b/backend/src/api.mjs index 1fa940e..44f9a8f 100644 --- a/backend/src/api.mjs +++ b/backend/src/api.mjs @@ -14,7 +14,7 @@ router.use(express.json()); // POST router.post('/:site/*', utils.limiter(20), (req, res) => { - const site = req.params.site; + const site_url = req.params.site; const path = req.params[0]; if (!req.body.token || !req.body.message) { @@ -22,30 +22,27 @@ router.post('/:site/*', utils.limiter(20), (req, res) => { return; } - let object = { + let comment = { user: utils.escapeHtml(req.body.name) || "Anonymous", user_website: utils.escapeHtml(req.body.website) || null, message: utils.escapeHtml(req.body.message), reply_to: req.body.reply_to || null, - site, - path, }; // validation const user_token = req.body.token; - const site_info = db.siteInfo(site); - console.log(site_info.comment_token, site_info.message_length_limit); + const site = db.siteInfo(site_url); - if (user_token !== site_info.comment_token) { + if (user_token !== site.info.comment_token) { res.status(403).json("תשובת סינון שגויה."); - } else if (object.user.length > utils.MAX_LENGTHS.username) { + } else if (object.user.length > site.max_lengths.user) { res.status(400).json("שם משתמש ארוך מדי."); - } else if (object.user_website > utils.MAX_LENGTHS.user_website) { + } else if (object.user_website > site.max_lengths.website) { res.status(400).json("כתובת אתר ארוכה מדי."); - } else if (object.message > site_info.message_length_limit) { + } else if (object.message > site.max_lengths.message) { res.status(400).json("הודעה ארוכה מדי."); } else { - const comment = db.insertPageComment(object); + const comment = db.insertPageComment(site_url, path, comment); res.json(comment); } diff --git a/backend/src/config.mjs b/backend/src/config.mjs new file mode 100644 index 0000000..6fb2ad7 --- /dev/null +++ b/backend/src/config.mjs @@ -0,0 +1,12 @@ +import config from 'config'; + +const configuration = config.util.toObject(); + +// console.log(JSON.stringify(configuration)); + +export default { + config: configuration, + getSite: (site_url) => { + return config.sites[site_url]; + } +}; diff --git a/backend/src/db.mjs b/backend/src/db.mjs index d4ada7c..2394ffa 100644 --- a/backend/src/db.mjs +++ b/backend/src/db.mjs @@ -2,6 +2,7 @@ import Database from 'better-sqlite3'; import { migrate } from '@blackglory/better-sqlite3-migrations'; import utils from './utils.mjs'; +import config from './config.mjs'; // class @@ -12,14 +13,14 @@ export class DB { siteInfo(site) { return getSiteInfo(this.my_db, site); } - siteComments(site) { - return getSiteComments(this.my_db, site); + siteComments(site_url) { + return getSiteComments(this.my_db, site_url); } - pageComments(site, page) { - return getPageComments(this.my_db, site, page); + pageComments(site_url, page) { + return getPageComments(this.my_db, site_url, page); } - insertPageComment(comment) { - insertPageComment(this.my_db, comment); + insertPageComment(site_url, path, comment) { + insertPageComment(this.my_db, site_url, path, comment); } } @@ -32,13 +33,6 @@ function migrations() { return [ { version: 1, up: ` -CREATE TABLE site ( - id integer primary key autoincrement, - url text not null, - comment_token text not null, - length_limit integer -); - CREATE TABLE comment ( id integer not null, site integer not null, @@ -48,13 +42,11 @@ CREATE TABLE comment ( message text not null, published text default (datetime('now')), reply_to integer, - FOREIGN KEY(site) REFERENCES site(id), PRIMARY KEY (site, path, id) ); `, down: ` DROP TABLE comment; -DROP TABLE site; ` } ]; @@ -63,7 +55,7 @@ DROP TABLE site; // setup function createDB() { - const db = new Database(process.env.DB || utils.DEFAULT_DB_PATH); + const db = new Database(utils.db_path); db.pragma('journal_mode = WAL'); migrate(db, migrations(), 1); return db; @@ -71,25 +63,31 @@ function createDB() { // queries -function getSiteInfo(db, site) { - return db.prepare(`SELECT comment_token, length_limit as message_length_limit FROM site WHERE url = ?`).get(site); +function getSiteInfo(db, site_url) { + const site = config.getSite(site_url); + if (!site || !site.info) { throw "Unknown site" } + return site; } -function insertPageComment(db, object) { +function insertPageComment(db, site_url, path, comment) { + const site = config.getSite(site_url); + if (!site || !site.info.id) { throw "Unknown site" } + + let object = {...comment, site_id: site.info.id, path: path }; + const stmt = db.prepare(` INSERT INTO comment(id, site, path, user, user_website, message, reply_to) SELECT ( SELECT count(*) - FROM (SELECT * FROM comment WHERE path = @path) c - JOIN (SELECT id FROM site WHERE url = @site) s - ON s.id = c.site + FROM comment + WHERE site = @site_id AND path = @path ), - ( SELECT id FROM site WHERE url = @site ), - @path, - @user, - @user_website, - @message, - @reply_to + @site_id, + @path, + @user, + @user_website, + @message, + @reply_to RETURNING id as id, user, @@ -102,40 +100,41 @@ function insertPageComment(db, object) { return stmt.all(object); } -function getPageComments(db, site, path) { +function getPageComments(db, site_url, path) { + const site = config.getSite(site_url); + if (!site || !site.info.id) { return []; } const stmt = db.prepare(` SELECT - c.id as id, + id, user, user_website, message, published, reply_to - FROM - (SELECT id from site where url = @site) s - JOIN (SELECT * FROM comment WHERE path = @path) c - ON c.site = s.id - ORDER BY c.id + FROM comment + WHERE site = @site_id AND path = @path + ORDER BY id ;`); - return stmt.all({ site, path }); + return stmt.all({ site_id: site.info.id, path }); } -function getSiteComments(db, site) { +function getSiteComments(db, site_url) { + const site = config.getSite(site_url); + if (!site || !site.info.id) { return []; } const stmt = db.prepare(` SELECT - c.id, - s.url as site, - c.path, - c.user, - c.user_website, - c.message, - c.published + id, + @site_url as site, + path, + user, + user_website, + message, + published FROM - (SELECT id, url from site where url = @site) s - JOIN comment c - ON c.site = s.id - ORDER BY c.published DESC + comment + WHERE site = @site_id + ORDER BY published DESC ; `); - return stmt.all({ site }); + return stmt.all({ site_id: site.info.id, site_url }); } diff --git a/backend/src/feed.mjs b/backend/src/feed.mjs index e4d6e10..fac83e6 100644 --- a/backend/src/feed.mjs +++ b/backend/src/feed.mjs @@ -12,13 +12,14 @@ export default router; router.use(express.json()); -const domain = process.env.DOMAIN || utils.DEFAULT_DOMAIN; +const domain = utils.domain; router.get('/:site', utils.limiter(500), (req, res) => { const site = req.params.site; var feed = new Feed({ title: 'UCS', + generator: 'UCS', description: 'תגובות עבור האתר ' + site, id: domain + '/feed/' + site, link: domain, @@ -49,6 +50,7 @@ router.get('/:site/*', utils.limiter(500), (req, res) => { var feed = new Feed({ title: 'UCS', + generator: 'UCS', description: 'תגובות עבור הדף ' + site + '/' + path, id: domain + '/feed/' + site, link: domain, diff --git a/backend/src/main.mjs b/backend/src/main.mjs index 2fca963..3c959f7 100644 --- a/backend/src/main.mjs +++ b/backend/src/main.mjs @@ -19,7 +19,7 @@ app.use('/atom', feed); // Listen -const port = process.env.PORT || utils.DEFAULT_PORT; +const port = utils.port; app.listen(port, () => { console.log(`Listening on port ${port}`); diff --git a/backend/src/server.mjs b/backend/src/server.mjs index c94cd2f..c707cbc 100644 --- a/backend/src/server.mjs +++ b/backend/src/server.mjs @@ -22,7 +22,7 @@ app.use( app.use(morgan('combined')); const corsOptions = { - origin: utils.DEFAULT_CORS, + origin: utils.cors, optionsSuccessStatus: 200 }; diff --git a/backend/src/utils.mjs b/backend/src/utils.mjs index b3b5f1b..bb3a441 100644 --- a/backend/src/utils.mjs +++ b/backend/src/utils.mjs @@ -1,24 +1,26 @@ import RateLimit from 'express-rate-limit'; +import config from './config.mjs'; + // Constants -export const MAX_LENGTHS = { - username: 100, - user_website: 200, - message_body: 1000 -}; +const domain = process.env.DOMAIN || config.domain || "localhost"; -export const DEFAULT_PORT = 8080; +const port = process.env.PORT || config.port || 8080; -export const DEFAULT_CORS = ["alloca.space", "www.alloca.space"]; +const db_path = process.env.DB || config.db_path || "ucs.db"; -export const DEFAULT_DOMAIN = "comments.alloca.space"; - -export const DEFAULT_DB_PATH = "ucs.db"; +const cors = (function() { + let origins = new Set(); + for (const site in config.sites) { + config.sites[site].cors.forEach(origin => {origins.add(origin) }); + } + return Array.from(origins); +})(); // functions -export function escapeHtml(unsafe) { +function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(/ RateLimit({ +const limiter = limit => RateLimit({ windowMs: 1 * 60 * 1000, max: limit, }); export default { - MAX_LENGTHS, - DEFAULT_PORT, - DEFAULT_CORS, - DEFAULT_DOMAIN, - DEFAULT_DB_PATH, + domain, + port, + db_path, + cors, escapeHtml, limiter }; diff --git a/bundle.sh b/bundle.sh index 4daa373..4e31f26 100755 --- a/bundle.sh +++ b/bundle.sh @@ -20,7 +20,7 @@ cp ../frontend/dist/assets/* public/ PACKAGE="ucs-$(cat package.json | jq -r .version).tar.gz" -tar czvf "$PACKAGE" package.json package-lock.json src/ node_modules/ public/ +tar czvf "$PACKAGE" package.json package-lock.json node_modules/ src/ config/ public/ cd .. mv backend/"$PACKAGE" . echo "$PACKAGE created."