├── bin └── bksh ├── docs ├── .nojekyll ├── web │ ├── scripts │ │ ├── prettify │ │ │ └── lang-css.js │ │ ├── nav.js │ │ ├── commonNav.js │ │ ├── collapse.js │ │ └── search.js │ └── styles │ │ └── prettify.css ├── jsdoc.js └── src │ └── start.md ├── web ├── img │ ├── index.html │ ├── 1.png │ ├── logo.png │ └── loading.gif ├── webfonts │ ├── index.html │ ├── fa-solid-900.eot │ ├── fa-solid-900.ttf │ ├── fa-brands-400.eot │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-regular-400.eot │ ├── fa-regular-400.ttf │ ├── fa-solid-900.woff │ ├── fa-solid-900.woff2 │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.woff │ └── fa-regular-400.woff2 ├── css │ └── index.html ├── js │ ├── index.html │ ├── webpush.js │ ├── app-passkey.js │ ├── app-dom.js │ ├── app-sanitizer.js │ ├── app-send.js │ └── app-ws.js └── index.html ├── lib ├── modules.js ├── metrics │ ├── FakeTrace.js │ ├── Counter.js │ ├── ExponentiallyMovingWeightedAverage.js │ ├── Timer.js │ ├── Histogram.js │ ├── Meter.js │ ├── Trace.js │ └── TokenBucket.js ├── queue │ ├── local.js │ ├── sns.js │ ├── worker.js │ ├── eventbridge.js │ └── json.js ├── cache │ ├── local.js │ └── worker.js ├── shell │ └── users.js ├── index.js ├── lib │ ├── hash.js │ ├── uuid.js │ └── lru.js ├── api │ ├── routing.js │ ├── redirect.js │ └── acl.js ├── metrics.js ├── util │ ├── shell.js │ └── redis.js ├── aws │ ├── sqs.js │ ├── ses.js │ └── other.js ├── push │ └── webpush.js └── db │ └── pg.js ├── examples ├── alpine │ ├── web │ │ ├── test.html │ │ ├── index.js │ │ └── index.html │ ├── bkjs.conf │ ├── README.md │ └── package.json ├── config │ ├── web │ │ ├── index.js │ │ ├── index.html │ │ ├── config.html │ │ └── config.js │ ├── README.md │ ├── bkjs.conf │ ├── package.json │ └── modules │ │ └── config.js ├── kanban │ ├── bkjs.conf │ ├── web │ │ ├── index.js │ │ └── index.html │ ├── package.json │ ├── README.md │ └── modules │ │ └── kanban.js └── modules │ ├── bk_file.js │ ├── bk_data.js │ └── bk_system.js ├── .gitignore ├── tools ├── bkjs-get ├── docker │ └── Dockerfile.abuild ├── alpine │ └── APKBUILD.cloudwatch ├── bkjs-install ├── bkjs-dynamodb ├── bkjs-ec2-ami ├── bkjs-nats ├── bkjs-ecs-agent ├── bkjs-docker ├── bkjs-ec2-cwagent ├── bkjs-ecr ├── bkjs-monit └── bkjs-redis ├── tests ├── access.test.js ├── bkjs.conf ├── pool.test.js ├── limiter.test.js ├── logwatcher.test.js ├── jobs.test.js ├── acl.test.js ├── jwt.test.js ├── flow.test.js └── config.test.js ├── LICENSE ├── package.json └── README.md /bin/bksh: -------------------------------------------------------------------------------- 1 | bkjs -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/img/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/webfonts/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/css/index.html: -------------------------------------------------------------------------------- 1 | nothing here 2 | 3 | -------------------------------------------------------------------------------- /web/js/index.html: -------------------------------------------------------------------------------- 1 | nothing here 2 | 3 | -------------------------------------------------------------------------------- /web/img/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/img/1.png -------------------------------------------------------------------------------- /web/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/img/logo.png -------------------------------------------------------------------------------- /web/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/img/loading.gif -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /web/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /web/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /web/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vseryakov/backendjs/HEAD/web/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /lib/modules.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This module contains all internal and dynamicaly loaded modules 4 | * @module modules 5 | */ 6 | 7 | module.exports = {} 8 | -------------------------------------------------------------------------------- /examples/alpine/web/test.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | Rendering text template inside component with params: 4 |
5 | -------------------------------------------------------------------------------- /examples/alpine/bkjs.conf: -------------------------------------------------------------------------------- 1 | 2 | log=info 3 | 4 | # connect to node repl to access web process 5 | repl-port-web=2090 6 | 7 | # redirect /app endpoints to the main template 8 | api-routing-^/app=/index.html 9 | 10 | -------------------------------------------------------------------------------- /examples/alpine/README.md: -------------------------------------------------------------------------------- 1 | # Backend.js sample application with Alpine.js 2 | 3 | 1. Run the app 4 | 5 | npm run start 6 | 7 | 2. Point browser to http://localhost:8000 8 | 9 | 10 | # Authors 11 | vlad 12 | 13 | -------------------------------------------------------------------------------- /examples/config/web/index.js: -------------------------------------------------------------------------------- 1 | 2 | app.debug = 1; 3 | 4 | app.$ready(() => { 5 | app.user = Alpine.reactive({}); 6 | Alpine.magic('user', (el) => app.user); 7 | 8 | app.ui.setColorScheme(); 9 | app.start(); 10 | 11 | }); 12 | -------------------------------------------------------------------------------- /examples/config/README.md: -------------------------------------------------------------------------------- 1 | # Backend.js sample config application with Alpine.js 2 | 3 | 1. Create tables 4 | 5 | npm run init 6 | 7 | 3. Run the app 8 | 9 | npm run start 10 | 11 | 4. Point browser to http://localhost:8000 12 | 13 | 14 | # Authors 15 | vlad 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .DS_Store 3 | *.[aod] 4 | *.gch 5 | *.so 6 | *.sqlext 7 | *.dSYM 8 | *.node 9 | *.swp 10 | *.*~ 11 | *.log 12 | *.local 13 | *.gz 14 | *.br 15 | *.tgz 16 | *.tar.gz 17 | *.zip 18 | *.js.map 19 | *.db 20 | .history 21 | .env* 22 | .bkjsrc 23 | .bkjs_history 24 | nodejs 25 | node_modules 26 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Backendjs 8 | 9 | 10 | Hello world! 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/alpine/web/index.js: -------------------------------------------------------------------------------- 1 | 2 | app.debug = 1; 3 | 4 | app.templates.empty = `
Empty
`; 5 | 6 | app.components.index = class extends app.AlpineComponent { 7 | template; 8 | 9 | toggle() { 10 | this.template = !this.template ? "/test.html" : this.template == "config" ? "empty" : ""; 11 | } 12 | }; 13 | 14 | app.$ready(() => { 15 | app.start(); 16 | }); 17 | -------------------------------------------------------------------------------- /examples/kanban/bkjs.conf: -------------------------------------------------------------------------------- 1 | 2 | log=info 3 | 4 | # connect to node repl to access web process 5 | repl-port-web=2090 6 | 7 | # redirect /app endpoints to the main index 8 | api-routing-path-^/app=/index.html 9 | 10 | # allow public access 11 | api-acl-add-public=^/ 12 | 13 | # make sqlite database as primary 14 | db-pool=sqlite 15 | db-sqlite-pool=kanban 16 | 17 | # PostgreSQL 18 | #db-pool=pg 19 | #db-pg-pool=default 20 | 21 | -------------------------------------------------------------------------------- /lib/metrics/FakeTrace.js: -------------------------------------------------------------------------------- 1 | const lib = require("../lib"); 2 | 3 | module.exports = class FakeTrace { 4 | 5 | /** 6 | * Noop trace class 7 | * @implements {Trace} 8 | * 9 | * @class FakeTrace 10 | */ 11 | constructor() 12 | { 13 | this.start = () => (new FakeTrace()); 14 | this.stop = lib.noop; 15 | this.send = lib.noop; 16 | this.toString = () => (""); 17 | this.destroy = lib.noop; 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /lib/metrics/Counter.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = class Counter { 4 | 5 | /** 6 | * Counter that resets itself after each read 7 | * @param {object} [options] 8 | * @class Counter 9 | */ 10 | constructor(options) { 11 | this.count = 0; 12 | } 13 | 14 | toJSON() { 15 | const n = this.count; 16 | this.count = 0; 17 | return n; 18 | } 19 | 20 | incr(count) { 21 | this.count += typeof count == "number" ? count : 1; 22 | return this.count; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tools/bkjs-get: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$BKJS_CMD" in 4 | 5 | help|get-help) 6 | echo "" 7 | echo " get-json - read JSON from the input and show it nicely formatted" 8 | echo " get-jsval FILE PROP [DFLT] [realpath] - return a value from a JSON file by property name" 9 | ;; 10 | 11 | get-json) 12 | exec node -e "console.log(util.inspect(JSON.parse(fs.readFileSync(0).toString()),null,null))" 13 | ;; 14 | 15 | get-jsval) 16 | echo $(get_json_flat "$BKJS_ARGV0" "$BKJS_ARGV1" "$BKJS_ARGV2" "$BKJS_ARGV3") 17 | exit 18 | ;; 19 | 20 | esac 21 | 22 | -------------------------------------------------------------------------------- /web/js/webpush.js: -------------------------------------------------------------------------------- 1 | // Handle Push events to display messages 2 | console.log("ServiceWorker loaded: Push Notifications"); 3 | self.addEventListener("push", (e) => { 4 | const options = {}; 5 | const data = e.data && e.data.json() || {}; 6 | for (const p of ["actions", "badge", "body" ,"data", "dir", "icon", "image", "lang", "renotify", "requireInteraction", "silent", "tag", "timestamp", "vibrate"]) { 7 | if (typeof data[p] != "undefined") options[p] = data[p]; 8 | } 9 | if (data.body && !data.title) delete options.body; 10 | self.registration.showNotification(data.title || data.body || "New message!", options); 11 | }); 12 | -------------------------------------------------------------------------------- /tools/docker/Dockerfile.abuild: -------------------------------------------------------------------------------- 1 | ARG ALPINE=3.22 2 | 3 | FROM alpine:$ALPINE 4 | 5 | RUN apk --no-cache add \ 6 | alpine-conf \ 7 | alpine-sdk \ 8 | apk-tools \ 9 | coreutils \ 10 | cmake \ 11 | doas-sudo-shim \ 12 | ccache \ 13 | mc \ 14 | nodejs \ 15 | npm \ 16 | python3 \ 17 | go \ 18 | curl \ 19 | file \ 20 | zip \ 21 | rsync \ 22 | zlib-dev \ 23 | zimg-dev \ 24 | fontconfig-dev \ 25 | freetype-dev \ 26 | imlib2-dev \ 27 | nasm && \ 28 | apk -U upgrade -a 29 | 30 | RUN adduser -D alpine && \ 31 | addgroup alpine abuild && \ 32 | echo 'alpine ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers && \ 33 | node-gyp install 34 | -------------------------------------------------------------------------------- /examples/config/bkjs.conf: -------------------------------------------------------------------------------- 1 | 2 | log=info 3 | 4 | # connect to node repl to access web process 5 | repl-port-web=2090 6 | 7 | # redirect /app endpoints to the main index 8 | api-routing-path-^/app=/index.html 9 | 10 | # allow public access by /app route 11 | api-acl-add-public=^/app 12 | 13 | # create ACL named admins with access to our config endpoint 14 | api-acl-add-admins=^/config 15 | 16 | # allow users with admin role to access the admins ACL 17 | api-acl-allow-admin=admins 18 | 19 | # make sqlite database as primary 20 | db-pool=sqlite 21 | db-sqlite-pool=config 22 | 23 | # Local DynamoDB 24 | #db-pool=dynamodb 25 | #db-dynamodb-pool=http://localhost:8181 26 | 27 | -------------------------------------------------------------------------------- /examples/alpine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "author": "vlad", 4 | "name": "app", 5 | "description": "The backend", 6 | "repository": { "type": "git", "url": "http://github.com/vseryakov/backendjs.git" }, 7 | "main": "app.js", 8 | "dependencies": { 9 | "backendjs": "latest" 10 | }, 11 | "engines": { "node": ">= 24.0" }, 12 | "eslintConfig": { 13 | "env": { 14 | "browser": true 15 | }, 16 | "globals": { 17 | "app": false, 18 | "Alpine": false, 19 | "bootstrap": false, 20 | "bootpopup": false, 21 | "window": false, 22 | "document": false 23 | } 24 | }, 25 | "scripts": { 26 | "start": "bkjs run -api -watch" 27 | }, 28 | "license": "BSD-3-Clause" 29 | } 30 | -------------------------------------------------------------------------------- /examples/kanban/web/index.js: -------------------------------------------------------------------------------- 1 | 2 | app.debug = 1; 3 | 4 | app.components.index = class extends app.AlpineComponent { 5 | 6 | boards = []; 7 | 8 | async onCreate() { 9 | const { err, data } = await app.afetch("/api/boards"); 10 | if (err) return app.ui.showToast("error", err); 11 | this.boards = data; 12 | } 13 | } 14 | 15 | app.components.board = class extends app.AlpineComponent { 16 | 17 | cards = [] 18 | 19 | async onCreate() { 20 | const { err, data } = await app.afetch("/api/board/" + this.params.id); 21 | if (err) return app.ui.showToast("error", err); 22 | this.cards = data; 23 | } 24 | } 25 | 26 | app.$ready(() => { 27 | app.ui.setColorScheme(); 28 | app.start(); 29 | }); 30 | -------------------------------------------------------------------------------- /lib/metrics/ExponentiallyMovingWeightedAverage.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = class ExponentiallyMovingWeightedAverage { 4 | 5 | constructor(rateUnit, tickInterval) 6 | { 7 | this._rateUnit = rateUnit || 60000; 8 | this._tickInterval = tickInterval || 5000; 9 | this._alpha = 1 - Math.exp(-this._tickInterval / this._rateUnit); 10 | this._count = 0; 11 | this._rate = 0; 12 | } 13 | 14 | update(n) 15 | { 16 | this._count += n; 17 | } 18 | 19 | tick() 20 | { 21 | this._rate += this._alpha * ((this._count / this._tickInterval) - this._rate); 22 | this._count = 0; 23 | } 24 | 25 | rate(timeUnit) 26 | { 27 | return this._rate * timeUnit || 0; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/kanban/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "author": "vlad", 4 | "name": "app", 5 | "description": "The backend", 6 | "repository": { "type": "git", "url": "http://github.com/vseryakov/backendjs.git" }, 7 | "main": "app.js", 8 | "dependencies": { 9 | "backendjs": "latest" 10 | }, 11 | "engines": { "node": ">= 24.0" }, 12 | "eslintConfig": { 13 | "env": { 14 | "browser": true 15 | }, 16 | "globals": { 17 | "app": false, 18 | "Alpine": false, 19 | "bootstrap": false, 20 | "bootpopup": false, 21 | "window": false, 22 | "document": false 23 | } 24 | }, 25 | "scripts": { 26 | "setup": "rm -f kanban.db && bksh -db-create-tables -run-file tools/seed", 27 | "start": "bkjs run -api -watch" 28 | }, 29 | "license": "BSD-3-Clause" 30 | } 31 | -------------------------------------------------------------------------------- /tests/access.test.js: -------------------------------------------------------------------------------- 1 | /* global */ 2 | 3 | const { describe, it, before, after } = require('node:test'); 4 | const { checkAccess, init } = require("./utils"); 5 | const { app } = require("../"); 6 | 7 | const config = [ 8 | { get: "/ping" }, 9 | { url: "/auth", status: 417 }, 10 | { url: "/login", data: { login: "test", secret: "test1" }, status: 401 }, 11 | { url: "/login", data: { login: "test", secret: "test" } }, 12 | { url: "/auth" }, 13 | ]; 14 | 15 | describe('Access Tests', (t) => { 16 | 17 | before((t, done) => { 18 | init({ api: 1, nodb: 1, noipc: 1, roles: "users" }, done) 19 | }); 20 | 21 | it("checks basic endpoints", (t, done) => { 22 | checkAccess({ config }, done); 23 | }); 24 | 25 | after((t, done) => { 26 | app.stop(done) 27 | }) 28 | }) 29 | 30 | -------------------------------------------------------------------------------- /examples/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "author": "vlad", 4 | "name": "app", 5 | "description": "The backend", 6 | "repository": { "type": "git", "url": "http://github.com/vseryakov/backendjs.git" }, 7 | "main": "app.js", 8 | "dependencies": { 9 | "backendjs": "latest" 10 | }, 11 | "engines": { "node": ">= 24.0" }, 12 | "eslintConfig": { 13 | "env": { 14 | "browser": true 15 | }, 16 | "globals": { 17 | "app": false, 18 | "Alpine": false, 19 | "bootstrap": false, 20 | "bootpopup": false, 21 | "window": false, 22 | "document": false 23 | } 24 | }, 25 | "scripts": { 26 | "init": "bksh -db-create-tables -user-add roles admin login admin secret admin name admin", 27 | "start": "bkjs run -api -watch" 28 | }, 29 | "license": "BSD-3-Clause" 30 | } 31 | -------------------------------------------------------------------------------- /docs/web/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /tests/bkjs.conf: -------------------------------------------------------------------------------- 1 | 2 | api-port=8999 3 | api-ws-port=8999 4 | 5 | app-no-restart=1 6 | app-no-events=1 7 | 8 | jobs-workers=1 9 | 10 | [roles=users] 11 | api-acl-authenticated=auth 12 | api-acl-add-auth=^/auth$ 13 | api-csrf-set-path=^/login$ 14 | api-csrf-check-path=^/auth$ 15 | api-users-users={ "test": { "id": "test", "name": "test", "login": "test", "secret": "test" } } 16 | 17 | [roles=redis] 18 | cache-redis=redis:// 19 | queue-redis=redis://?bk-visibilityTimeout=5000&bk-interval=50&bk-retryInterval=50 20 | ipc-system-queue=redis 21 | jobs-worker-queue=redis 22 | 23 | [roles=sqlite] 24 | db-pool=sqlite 25 | db-config=sqlite 26 | db-sqlite-pool=/tmp/test 27 | 28 | [roles=dynamodb] 29 | db-pool=dynamodb 30 | db-config=dynamodb 31 | db-dynamodb-pool=http://localhost:8181 32 | 33 | [roles=elasticsearch] 34 | db-pool=elasticsearch 35 | db-config=elasticsearch 36 | db-elasticsearch-pool=default 37 | db-elasticsearch-pool-tables=test1,test2,test3 38 | db-elasticsearch-pool-options-defaultParams={"refresh":true} 39 | 40 | [global] 41 | -------------------------------------------------------------------------------- /docs/jsdoc.js: -------------------------------------------------------------------------------- 1 | // BigInt JSON serialization. 2 | BigInt.prototype.toJSON = function() { 3 | return this.toString() + 'n'; 4 | } 5 | 6 | module.exports = { 7 | plugins: ['plugins/markdown'], 8 | source: { 9 | include: [ "lib" ], 10 | includePattern: ".+\\.js(doc|x)?$", 11 | excludePattern: "(^|\\/|\\\\)_" 12 | }, 13 | templates: { 14 | default: { 15 | includeDate: false, 16 | outputSourceFiles: true 17 | } 18 | }, 19 | opts: { 20 | template: "node_modules/docdash", 21 | destination: "./docs/web", 22 | recurse: true, 23 | readme: "README.md", 24 | tutorials: "docs/src", 25 | }, 26 | docdash: { 27 | sort: true, 28 | search: true, 29 | collapse: true, 30 | typedefs: true, 31 | meta: { 32 | title: "Backendjs Documentation", 33 | description: "A Node.js library to create Web backends with minimal dependencies.", 34 | }, 35 | tutorialsOrder: ["start", "modules", "reference", "bkjs", "config"] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /lib/queue/local.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2024 4 | */ 5 | 6 | const modules = require(__dirname + '/../modules'); 7 | const lib = require(__dirname + "/../lib"); 8 | const QueueClient = require(__dirname + "/client"); 9 | 10 | const localClient = { 11 | name: "local", 12 | 13 | create: function(options) { 14 | if (/^local:/.test(options?.url)) return new LocalClient(options); 15 | } 16 | }; 17 | module.exports = localClient; 18 | 19 | /** 20 | * Client that uses the local process or server process for jobs. 21 | * @memberOf module:queue 22 | */ 23 | 24 | class LocalClient extends QueueClient { 25 | 26 | constructor(options) { 27 | super(options); 28 | this.name = localClient.name; 29 | 30 | this.applyOptions(); 31 | this.emit("ready"); 32 | } 33 | 34 | listen(options, callback) {} 35 | 36 | submit(msg, options, callback) { 37 | setTimeout(modules.jobs.processJobMessage.bind(modules.jobs, "#local", msg), options?.delay); 38 | lib.tryCall(callback); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /docs/web/scripts/nav.js: -------------------------------------------------------------------------------- 1 | function scrollToNavItem() { 2 | var path = window.location.href.split('/').pop().replace(/\.html/, ''); 3 | document.querySelectorAll('nav a').forEach(function(link) { 4 | var href = link.attributes.href.value.replace(/\.html/, ''); 5 | if (path === href) { 6 | link.scrollIntoView({block: 'center'}); 7 | return; 8 | } 9 | }) 10 | } 11 | 12 | function lineNumbers() { 13 | var source = document.getElementsByClassName('prettyprint source linenums'); 14 | if (!source || !source[0]) return; 15 | var anchorHash = document.location.hash.substring(1); 16 | var lines = source[0].getElementsByTagName('li'); 17 | 18 | for (var i = 0 ; i < lines.length; i++) { 19 | var lineId = 'line' + (i + 1); 20 | lines[i].id = lineId; 21 | if (lineId === anchorHash) { 22 | lines[i].className += ' selected'; 23 | lines[i].scrollIntoView({block: 'center'}); 24 | } 25 | } 26 | } 27 | setTimeout(() => { 28 | scrollToNavItem(); 29 | prettyPrint(); 30 | lineNumbers(); 31 | }, 100); 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-2025 Vlad Seryakov. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/web/scripts/commonNav.js: -------------------------------------------------------------------------------- 1 | if (typeof fetch === 'function') { 2 | const init = () => { 3 | if (typeof scrollToNavItem !== 'function') return false 4 | scrollToNavItem() 5 | // hideAllButCurrent not always loaded 6 | if (typeof hideAllButCurrent === 'function') hideAllButCurrent() 7 | return true 8 | } 9 | fetch('./nav.inc.html') 10 | .then(response => response.ok ? response.text() : `${response.url} => ${response.status} ${response.statusText}`) 11 | .then(body => { 12 | document.querySelector('nav').innerHTML += body 13 | // nav.js should be quicker to load than nav.inc.html, a fallback just in case 14 | return init() 15 | }) 16 | .then(done => { 17 | if (done) return 18 | let i = 0 19 | ;(function waitUntilNavJs () { 20 | if (init()) return 21 | if (i++ < 100) return setTimeout(waitUntilNavJs, 300) 22 | console.error(Error('nav.js not loaded after 30s waiting for it')) 23 | })() 24 | }) 25 | .catch(error => console.error(error)) 26 | } else { 27 | console.error(Error('Browser too old to display commonNav (remove commonNav docdash option)')) 28 | } 29 | -------------------------------------------------------------------------------- /docs/web/styles/prettify.css: -------------------------------------------------------------------------------- 1 | .pln { 2 | color: #ddd; 3 | } 4 | 5 | /* string content */ 6 | .str { 7 | color: #61ce3c; 8 | } 9 | 10 | /* a keyword */ 11 | .kwd { 12 | color: #fbde2d; 13 | } 14 | 15 | /* a comment */ 16 | .com { 17 | color: #aeaeae; 18 | } 19 | 20 | /* a type name */ 21 | .typ { 22 | color: #8da6ce; 23 | } 24 | 25 | /* a literal value */ 26 | .lit { 27 | color: #fbde2d; 28 | } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #ddd; 33 | } 34 | 35 | /* lisp open bracket */ 36 | .opn { 37 | color: #000000; 38 | } 39 | 40 | /* lisp close bracket */ 41 | .clo { 42 | color: #000000; 43 | } 44 | 45 | /* a markup tag name */ 46 | .tag { 47 | color: #8da6ce; 48 | } 49 | 50 | /* a markup attribute name */ 51 | .atn { 52 | color: #fbde2d; 53 | } 54 | 55 | /* a markup attribute value */ 56 | .atv { 57 | color: #ddd; 58 | } 59 | 60 | /* a declaration */ 61 | .dec { 62 | color: #EF5050; 63 | } 64 | 65 | /* a variable name */ 66 | .var { 67 | color: #c82829; 68 | } 69 | 70 | /* a function name */ 71 | .fun { 72 | color: #4271ae; 73 | } 74 | 75 | /* Specify class=linenums on a pre to get line numbering */ 76 | ol.linenums { 77 | margin-top: 0; 78 | margin-bottom: 0; 79 | padding-bottom: 2px; 80 | } 81 | -------------------------------------------------------------------------------- /tests/pool.test.js: -------------------------------------------------------------------------------- 1 | const { describe, it } = require('node:test'); 2 | const assert = require('node:assert/strict'); 3 | const { lib, dbPool } = require("../"); 4 | 5 | describe("Pool tests", async (t) => { 6 | 7 | var options = { 8 | min: 1, max: 5, idle: 50, 9 | create: function(cb) { cb(null,{ id: Date.now() }) } 10 | } 11 | var list = [], pool; 12 | 13 | await it("use 5 connections", async () => { 14 | pool = new dbPool(options); 15 | for (var i = 0; i < 5; i++) { 16 | pool.use((err, obj) => { list.push(obj) }); 17 | } 18 | assert.strictEqual(list.length, 5); 19 | }); 20 | 21 | await it("release all", async () => { 22 | while (list.length) { 23 | pool.release(list.shift()); 24 | } 25 | assert.strictEqual(list.length, 0); 26 | }); 27 | 28 | await it("take 1 connection", async () => { 29 | pool.use((err, obj) => { list.push(obj) }); 30 | assert.strictEqual(list.length, 1); 31 | pool.release(list.shift()); 32 | }); 33 | 34 | await it("destroy idle connections", async () => { 35 | await lib.sleep(options.idle*2); 36 | assert.strictEqual(pool.stats().avail, 1); 37 | pool.shutdown(); 38 | }); 39 | 40 | }) 41 | 42 | -------------------------------------------------------------------------------- /lib/queue/sns.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const logger = require(__dirname + '/../logger'); 7 | const aws = require(__dirname + '/../aws'); 8 | const QueueClient = require(__dirname + "/client"); 9 | 10 | 11 | const snsClient = { 12 | name: "sns", 13 | 14 | create: function(options) { 15 | if (/^sns:\/\//.test(options?.url)) return new SNSClient(options); 16 | } 17 | }; 18 | 19 | module.exports = snsClient; 20 | 21 | /** 22 | * Queue client using AWS SNS. 23 | * 24 | * The URL must look like: `sns://ARN`. 25 | * 26 | * @example 27 | * 28 | * queue-events=sns:// 29 | * queue-events=sns://topic 30 | * 31 | * @memberOf module:queue 32 | */ 33 | 34 | class SNSClient extends QueueClient { 35 | 36 | constructor(options) { 37 | super(options); 38 | this.name = snsClient.name; 39 | this.applyOptions(); 40 | this.emit("ready"); 41 | } 42 | 43 | submit(event, options, callback) { 44 | logger.dev("submit:", this.url, event, options); 45 | 46 | var arn = `arn:aws:sns:${options.region || aws.region}:${this.options.account || aws.accountId}:${options.topic || this.hostname}`; 47 | aws.snsPublish(arn, event, callback); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /examples/modules/bk_file.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const { api } = require('backendjs'); 7 | 8 | const mod = { 9 | name: "bk_file", 10 | }; 11 | module.exports = mod; 12 | 13 | // Create API endpoints and routes 14 | mod.configureWeb = function(options, callback) 15 | { 16 | api.app.all(/^\/file\/([a-z]+)$/, function(req, res) { 17 | var query = api.toParams(req, { 18 | name: { required: 1 }, 19 | prefix: { required: 1 }, 20 | }, { query: 1 }); 21 | if (typeof query == "string") return api.sendReply(res, 400, query); 22 | 23 | var file = query.prefix.replace("/", "") + "/" + query.name.replace("/", ""); 24 | 25 | switch (req.params[0]) { 26 | case "get": 27 | api.files.send(req, file); 28 | break; 29 | 30 | case "put": 31 | api.files.put(req, "file", query, (err) => { 32 | api.sendReply(res, err); 33 | }); 34 | break; 35 | 36 | case "del": 37 | api.files.del(file, (err) => { 38 | api.sendReply(res, err); 39 | }); 40 | break; 41 | 42 | default: 43 | api.sendReply(res, 400, "Invalid command"); 44 | } 45 | }); 46 | 47 | callback(); 48 | } 49 | -------------------------------------------------------------------------------- /lib/cache/local.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2024 4 | */ 5 | 6 | const modules = require(__dirname + '/../modules'); 7 | const CacheClient = require(__dirname + "/client"); 8 | 9 | const localClient = { 10 | name: "local", 11 | 12 | create: function(options) { 13 | if (/^local:/.test(options?.url)) return new LocalClient(options); 14 | } 15 | }; 16 | module.exports = localClient; 17 | 18 | /** 19 | * Client that uses the local process or server process for jobs. 20 | * @memberOf module:cache 21 | */ 22 | 23 | class LocalClient extends CacheClient { 24 | 25 | constructor(options) { 26 | super(options); 27 | this.name = localClient.name; 28 | this.applyOptions(); 29 | this.emit("ready"); 30 | } 31 | 32 | limiter(options, callback) { 33 | var opts = { 34 | name: options.name, 35 | rate: options.rate, 36 | max: options.max, 37 | interval: options.interval, 38 | expire: options.ttl > 0 ? Date.now() + options.ttl : 0, 39 | reset: options.reset, 40 | multiplier: options.multiplier, 41 | cacheName: this.cacheName, 42 | }; 43 | const msg = modules.cache.localLimiter(opts); 44 | callback(msg.consumed ? 0 : msg.delay, msg); 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /lib/cache/worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2024 4 | */ 5 | 6 | const modules = require(__dirname + '/../modules'); 7 | const CacheClient = require(__dirname + "/client"); 8 | 9 | const workerClient = { 10 | name: "worker", 11 | 12 | create: function(options) { 13 | if (/^worker:/.test(options?.url)) return new WorkerClient(options); 14 | } 15 | }; 16 | module.exports = workerClient; 17 | 18 | /** 19 | * Client that uses server process rate limiter and workers for jobs. 20 | * @memberOf module:cache 21 | */ 22 | 23 | class WorkerClient extends CacheClient { 24 | 25 | constructor(options) { 26 | super(options); 27 | this.name = workerClient.name; 28 | this.applyOptions(); 29 | this.emit("ready"); 30 | } 31 | 32 | limiter(options, callback) { 33 | var opts = { 34 | name: options.name, 35 | rate: options.rate, 36 | max: options.max, 37 | interval: options.interval, 38 | expire: options.ttl > 0 ? Date.now() + options.ttl : 0, 39 | reset: options.reset, 40 | multiplier: options.multiplier, 41 | cacheName: this.cacheName, 42 | }; 43 | modules.ipc.sendMsg("ipc:limiter", opts, (msg) => { 44 | callback(msg.consumed ? 0 : msg.delay, msg); 45 | }); 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /tools/alpine/APKBUILD.cloudwatch: -------------------------------------------------------------------------------- 1 | # Maintainer: Vlad Seryakov 2 | pkgname=amazon-cloudwatch-agent 3 | pkgver=1.300058.0 4 | pkgrel=0 5 | pkgdesc="Amazon Cloudwatch Agent" 6 | url="https://github.com/aws/amazon-cloudwatch-agent" 7 | arch="all" 8 | license="MIT" 9 | makedepends=" 10 | go 11 | " 12 | source="$pkgname-$pkgver.tar.gz::https://github.com/aws/amazon-cloudwatch-agent/archive/refs/tags/v$pkgver.tar.gz" 13 | 14 | options="!check !fhs" 15 | 16 | build() { 17 | cd $srcdir/$pkgname-$pkgver 18 | go mod download -x 19 | export CWARCH=$(uname -m) 20 | [ "$CWARCH" = "x86_64" ] && export CWARCH=amd64 21 | [ "$CWARCH" = "aarch64" ] && export CWARCH=arm64 22 | echo $pkgver > CWAGENT_VERSION 23 | make build-for-docker-$CWARCH 24 | } 25 | 26 | package() { 27 | cd $srcdir/$pkgname-$pkgver 28 | CWAGENT=amazon-cloudwatch-agent 29 | destdir=$pkgdir/opt/aws/$CWAGENT 30 | mkdir -p $destdir/bin $destdir/etc/$CWAGENT.d $destdir/logs $destdir/var $destdir/doc 31 | cp build/bin/linux_$CWARCH/* $destdir/bin 32 | rm -f $destdir/bin/start-$CWAGENT 33 | cp licensing/* $destdir 34 | cp translator/config/schema.json $destdir/doc/$CWAGENT-schema.json 35 | } 36 | 37 | sha512sums=" 38 | 0c4cb2f684dfdfc57a4e26c91509db57bf7c78fb7992f9efc376a952de7fb9f4515d2362b6ae19e9763581a5150bae85f8dfe09a2d6c6c3efaafc13701022a49 amazon-cloudwatch-agent-1.300058.0.tar.gz 39 | " 40 | 41 | 42 | -------------------------------------------------------------------------------- /lib/metrics/Timer.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const Meter = require("./Meter"); 4 | const Histogram = require("./Histogram"); 5 | 6 | function noop() {} 7 | 8 | class Timer { 9 | 10 | /** 11 | * Timers are a combination of Meters and Histograms. 12 | * They measure the rate as well as distribution of scalar events. 13 | * @class Timer 14 | */ 15 | constructor(options) { 16 | this.meter = new Meter(options); 17 | this.histogram = new Histogram(options); 18 | } 19 | 20 | start(value) { 21 | var timer = { start: Date.now(), count: value }; 22 | timer.end = this._end.bind(this, timer); 23 | return timer; 24 | } 25 | 26 | _end(timer) { 27 | timer.elapsed = Date.now() - timer.start; 28 | this.update(timer.elapsed, timer.count); 29 | timer.end = noop; 30 | return timer.elapsed; 31 | } 32 | 33 | update(time, count) { 34 | this.lastUpdate = Date.now(); 35 | this.histogram.update(time); 36 | this.meter.mark(count); 37 | } 38 | 39 | reset() { 40 | this.meter.reset(); 41 | this.histogram.reset(); 42 | } 43 | 44 | end() { 45 | this.meter.end(); 46 | } 47 | 48 | toJSON(options) { 49 | return { 50 | meter: this.meter.toJSON(options), 51 | histogram: this.histogram.toJSON(options), 52 | } 53 | } 54 | } 55 | 56 | module.exports = Timer; 57 | 58 | -------------------------------------------------------------------------------- /examples/modules/bk_data.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const { db, api } = require('backendjs'); 7 | 8 | // Data management 9 | const mod = { 10 | name: "bk_data", 11 | args: [ 12 | ], 13 | }; 14 | module.exports = mod; 15 | 16 | // Create API endpoints and routes 17 | mod.configureWeb = function(options, callback) 18 | { 19 | // Return table columns 20 | api.app.get("/data/columns/{:table}", (req, res) => { 21 | if (req.params.table) { 22 | return res.json(db.getColumns(req.params.table)); 23 | } 24 | res.json(db.tables); 25 | }); 26 | 27 | // Return table keys 28 | api.app.get("/data/keys/:table", (req, res) => { 29 | res.json({ data: db.getKeys(req.params.table) }); 30 | }); 31 | 32 | // Basic operations on a table 33 | api.app.post("/data/:op/:table", (req, res) => { 34 | 35 | if (!["select", "search", "get", "add", "put", "update", "del", "incr"].includes(req.params.op)) { 36 | return api.sendReply(res, 400, "invalid op"); 37 | } 38 | if (!db.getColumns(req.params.table)) { 39 | return api.sendReply(res, 404, "Unknown table"); 40 | } 41 | 42 | db[req.params.op](req.params.table, req.body, (err, rows, info) => { 43 | api.sendJSON(req, err, api.getResultPage(req, rows, info)); 44 | }); 45 | }); 46 | 47 | callback(); 48 | } 49 | 50 | -------------------------------------------------------------------------------- /lib/shell/users.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const { api, lib, shell } = require('../modules'); 7 | 8 | shell.help.push( 9 | "-user-add login LOGIN secret SECRET [name NAME] [type TYPE] ... - add a new user record for API access", 10 | "-user-update login LOGIN [name NAME] [type TYPE] ... - update existing user record", 11 | "-user-del ID|LOGIN... - delete a user record", 12 | "-user-get ID|LOGIN ... - show user records", 13 | ); 14 | 15 | 16 | // Show user records by id or login 17 | shell.commands.userGet = function(options) 18 | { 19 | lib.forEachSeries(process.argv.slice(2).filter((x) => (x[0] != "-")), function(login, next) { 20 | api.users.get(login, (err, row) => { 21 | if (row) shell.jsonFormat(row); 22 | next(); 23 | }); 24 | }, shell.exit); 25 | } 26 | 27 | // Add a user login 28 | shell.commands.userAdd = function(options) 29 | { 30 | api.users.add(shell.getQuery(), lib.objExtend(shell.getArgs(), { isInternal: 1 }), shell.exit); 31 | } 32 | 33 | // Update a user login 34 | shell.commands.userUpdate = function(options) 35 | { 36 | api.users.update(shell.getQuery(), lib.objExtend(shell.getArgs(), { isInternal: 1 }), shell.exit); 37 | } 38 | 39 | // Delete a user login 40 | shell.commands.userDel = function(options) 41 | { 42 | lib.forEachSeries(process.argv.slice(2).filter((x) => (x[0] != "-")), (login, next) => { 43 | api.users.del(login, next); 44 | }, shell.exit); 45 | } 46 | -------------------------------------------------------------------------------- /tools/bkjs-install: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|install-help) 10 | echo "" 11 | echo " install-node [-path PATH] [-version V] [-force] [-clean] [-tgz TGZ] - install binary release of the node into ./nodejs or specified path" 12 | ;; 13 | 14 | install-node) 15 | path=$(get_arg -path nodejs) 16 | if [ -n "$(get_flag -force)" -a -f $path/bin/node ]; then 17 | echo "Uninstalling node from $path ..." 18 | rm -rf $path/bin/node $path/bin/npm $path/bin/npx $path/lib/node_modules/npm $path/include/node 19 | [ -n "$(get_flag -clean)" ] && rm -rf $path/lib/node_modules 20 | fi 21 | [ -f $path/bin/node ] && echo "already installed as $path/bin/node" && exit 22 | 23 | mkdir -p $path 24 | [ "$?" != "0" ] && exit "echo failed to create $path" && exit 1 25 | echo "Installing node into $path ..." 26 | 27 | tgz=$(get_arg -tgz) 28 | if [ -n "$tgz" ]; then 29 | tar -C $path --strip-components=1 -xzf $tgz 30 | [ "$?" != "0" ] && exit 1 31 | else 32 | ver=$(get_arg -version v24.10.0) 33 | [ "$OS_ARCH" = "amd64" ] && arch=x64 || arch=$OS_ARCH 34 | platform=$(to_lower $PLATFORM) 35 | curl -L -o node.tgz https://nodejs.org/dist/$ver/node-$ver-$platform-$arch.tar.gz 36 | [ "$?" != "0" ] && exit 1 37 | tar -C $path --strip-components=1 -xzf node.tgz 38 | rm -rf node.tgz 39 | fi 40 | mv $path/README.md $path/LICENSE $path/CHANGELOG.md $path/share/doc 41 | exit 42 | ;; 43 | 44 | esac 45 | -------------------------------------------------------------------------------- /lib/queue/worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2024 4 | */ 5 | 6 | const cluster = require("cluster"); 7 | const lib = require(__dirname + "/../lib"); 8 | const QueueClient = require(__dirname + "/client"); 9 | 10 | const workerClient = { 11 | name: "worker", 12 | 13 | create: function(options) { 14 | if (/^worker:/.test(options?.url)) return new WorkerClient(options); 15 | } 16 | }; 17 | module.exports = workerClient; 18 | 19 | /** 20 | * Client that uses primary process rate limiter and workers for jobs. 21 | * @memberOf module:queue 22 | */ 23 | 24 | class WorkerClient extends QueueClient { 25 | 26 | constructor(options) { 27 | super(options); 28 | this.name = workerClient.name; 29 | this.qworker = 0; 30 | this.applyOptions(); 31 | this.emit("ready"); 32 | } 33 | 34 | listen(options, callback) {} 35 | 36 | submit(msg, options, callback) { 37 | var err; 38 | 39 | if (cluster.isPrimary) { 40 | var workers = lib.getWorkers({ worker_type: null }) 41 | if (workers.length) { 42 | msg.__op = "worker:job"; 43 | try { 44 | workers[this.qworker++ % workers.length].send(msg); 45 | } catch (e) { err = e } 46 | } else { 47 | err = { status: 404, message: "no workers available" } 48 | } 49 | } else { 50 | err = { status: 400, message: "not a primary process" }; 51 | } 52 | if (typeof callback == "function") callback(err); 53 | } 54 | 55 | } 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/web/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | //but we need to rehide them for search 4 | document.querySelectorAll("nav > ul").forEach(function(parent) { 5 | if (parent.className.indexOf("collapse_top") !== -1) { 6 | parent.style.display = "none"; 7 | } 8 | }); 9 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { 10 | parent.style.display = "none"; 11 | }); 12 | document.querySelectorAll("nav > h3").forEach(function(section) { 13 | if (section.className.indexOf("collapsed_header") !== -1) { 14 | section.addEventListener("click", function(){ 15 | if (section.nextSibling.style.display === "none") { 16 | section.nextSibling.style.display = "block"; 17 | } else { 18 | section.nextSibling.style.display = "none"; 19 | } 20 | }); 21 | } 22 | }); 23 | 24 | //only current page (if it exists) should be opened 25 | var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); 26 | document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { 27 | var href = parent.attributes.href.value.replace(/\.html/, ''); 28 | if (file === href) { 29 | if (parent.parentNode.parentNode.className.indexOf("collapse_top") !== -1) { 30 | parent.parentNode.parentNode.style.display = "block"; 31 | } 32 | parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { 33 | elem.style.display = "block"; 34 | }); 35 | } 36 | }); 37 | } 38 | 39 | hideAllButCurrent(); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | exports.modules = require(__dirname + '/modules'); 7 | exports.logger = require(__dirname + '/logger'); 8 | exports.lib = require(__dirname + '/lib'); 9 | exports.metrics = require(__dirname + '/metrics'); 10 | exports.app = require(__dirname + '/app'); 11 | exports.cache = require(__dirname + '/cache'); 12 | exports.queue = require(__dirname + '/queue'); 13 | exports.ipc = require(__dirname + '/ipc'); 14 | exports.aws = require(__dirname + '/aws'); 15 | exports.db = require(__dirname + '/db'); 16 | exports.sql = require(__dirname + '/db/sql'); 17 | exports.push = require(__dirname + '/push'); 18 | exports.api = require(__dirname + '/api'); 19 | exports.jobs = require(__dirname + '/jobs'); 20 | exports.events = require(__dirname + '/events'); 21 | exports.stats = require(__dirname + '/stats'); 22 | exports.sendmail = require(__dirname + '/sendmail'); 23 | exports.logwatcher = require(__dirname + '/util/logwatcher'); 24 | exports.DbPool = require(__dirname + '/db/pool'); 25 | exports.DbRequest = exports.db.Request; 26 | exports.shell = { name: "shell", help: [], commands: {} }; 27 | 28 | for (const p in exports) { 29 | if (p != "modules") exports.app.addModule(exports[p]); 30 | } 31 | 32 | const mods = [ 33 | "/api/hooks", "/api/routing", "/api/redirect", 34 | "/api/access", "/api/acl", "/api/csrf", "/api/session", "/api/signature", 35 | "/api/users", "/api/passkeys", "/api/ws", 36 | "/api/images", "/api/files", 37 | ]; 38 | 39 | for (const p of mods) { 40 | exports.app.addModule(require(__dirname + p)) 41 | } 42 | 43 | // Run the main server if we execute this as a standalone program 44 | if (!module.parent) { 45 | exports.app.start(); 46 | } 47 | -------------------------------------------------------------------------------- /examples/alpine/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | App Components Playground 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 48 | 49 | -------------------------------------------------------------------------------- /lib/metrics/Histogram.js: -------------------------------------------------------------------------------- 1 | 2 | const perf_hooks = require("perf_hooks"); 3 | 4 | module.exports = class Histogram { 5 | 6 | /** 7 | * - Histogram - Keeps a resevoir of statistically relevant values biased towards the last 5 minutes to explore their distribution 8 | * - count: The number of observed values. 9 | * - min: The lowest observed value. 10 | * - max: The highest observed value. 11 | * - mean: The average of all observed values. 12 | * - dev: The standard deviation of all observed values. 13 | * - med: median, 50% of all values in the resevoir are at or below this value. 14 | * - p75: See median, 75% percentile. 15 | * - p95: See median, 95% percentile. 16 | * - p99: See median, 99% percentile. 17 | * - p999: See median, 99.9% percentile. 18 | * @param {object} [options] 19 | * @class Histogram 20 | */ 21 | constructor(options) { 22 | if (options?.reset) this._reset = options?.reset; 23 | this._handle = perf_hooks.createHistogram(); 24 | } 25 | 26 | update(value) { 27 | this.lastUpdate = Date.now(); 28 | this._handle.record(typeof value == "number" && value > 0 ? value : 1); 29 | } 30 | 31 | reset() { 32 | this._handle.reset(); 33 | } 34 | 35 | toJSON(options) { 36 | this.lastJSON = Date.now(); 37 | 38 | const rc = { 39 | count: this._handle.count, 40 | min: this._handle.min, 41 | max: this._handle.max, 42 | mean: this._handle.mean, 43 | dev: this._handle.stddev, 44 | med: this._handle.percentile(50), 45 | p25: this._handle.percentile(25), 46 | p75: this._handle.percentile(75), 47 | p95: this._handle.percentile(95), 48 | p99: this._handle.percentile(99), 49 | }; 50 | if (this._reset || options?.reset) this.reset(); 51 | return rc; 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.200.0", 3 | "type": "commonjs", 4 | "author": "Vlad Seryakov", 5 | "name": "backendjs", 6 | "description": "A library for building backends", 7 | "main": "lib/index", 8 | "homepage": "https://github.com/vseryakov/backendjs", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/vseryakov/backendjs.git" 12 | }, 13 | "dependencies": { 14 | "express": "5.2.1", 15 | "formidable": "3.5.4", 16 | "mime-types": "3.0.2", 17 | "qs": "6.14.0", 18 | "ws": "8.18.3", 19 | "fast-xml-parser": "5.3.3" 20 | }, 21 | "devDependencies": { 22 | "eslint": "8.57.1", 23 | "esbuild": "0.27.2", 24 | "docdash": "github:vseryakov/docdash" 25 | }, 26 | "peerDependencies": { 27 | "croner": "9.1.0", 28 | "nodemailer": "7.0.12", 29 | "nats": "2.29.3", 30 | "pg": "8.16.3", 31 | "redis": "3.1.2", 32 | "sharp": "0.34.5", 33 | "unix-dgram": "2.0.7", 34 | "web-push": "3.6.7" 35 | }, 36 | "keywords": [ 37 | "webservice", 38 | "websockets", 39 | "aws", 40 | "database", 41 | "API", 42 | "DynamoDB", 43 | "Sqlite", 44 | "Elasticsearch", 45 | "PostgreSQL", 46 | "NATS", 47 | "Redis", 48 | "pubsub", 49 | "jobs", 50 | "cron" 51 | ], 52 | "engines": { 53 | "node": ">=22.0" 54 | }, 55 | "license": "BSD-3-Clause", 56 | "bin": { 57 | "bkjs": "./bin/bkjs", 58 | "bksh": "./bin/bkjs" 59 | }, 60 | "files": [ 61 | "LICENSE", 62 | "bin", 63 | "lib/", 64 | "web/", 65 | "tools/" 66 | ], 67 | "scripts": { 68 | "build": "npm run docs", 69 | "devbuild": "npm run build", 70 | "node": "./bkjs install-node", 71 | "deps": "PATH=$(pwd)/nodejs/bin:$PATH npm install", 72 | "test": "node --test --test-concurrency=1 tests/**/*.test.js tests/**/*-test.js", 73 | "docs": "rm -rf docs/web && bksh -help-config > docs/src/config.md && jsdoc -c docs/jsdoc.js" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tools/bkjs-dynamodb: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|dynamodb-help) 10 | echo "" 11 | echo " dynamodb-init - download and install local DynamoDB, start the server" 12 | echo " dynamodb-get [-force] - install local DynamoDB server in $BKJS_HOME/dynamodb" 13 | echo " dynamodb-run [-memmax SZ] - run local DynamoDB server installed in $BKJS_HOME/dynamodb, data files in $BKJS_HOME/var" 14 | echo " dynamodb-stop - stop local DynamoDB server" 15 | echo " dynamodb-reset - remove local DynamoDB database and restart the server" 16 | ;; 17 | 18 | dynamodb-init) 19 | ($0 dynamodb-get $(get_all_args)) 20 | ($0 dynamodb-run $(get_all_args)) 21 | exit 22 | ;; 23 | 24 | dynamodb-get) 25 | [ "$DYNAMODB_PREFIX" = "" ] && DYNAMODB_PREFIX=$BKJS_HOME/dynamodb 26 | [ "$(get_flag -force)" != "" -a "$DYNAMODB_PREFIX" != "" ] && rm -rf $DYNAMODB_PREFIX 27 | if [ ! -d $DYNAMODB_PREFIX ]; then 28 | mkdir -p $DYNAMODB_PREFIX 29 | curl -L -o ddb.tgz http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz 30 | tar -C $DYNAMODB_PREFIX -xzf ddb.tgz 31 | rm -f ddb.tgz 32 | fi 33 | exit 34 | ;; 35 | 36 | dynamodb-run) 37 | [ "$DYNAMODB_PREFIX" = "" ] && DYNAMODB_PREFIX=$BKJS_HOME/dynamodb 38 | mkdir -p $BKJS_HOME/var $BKJS_HOME/log 39 | params="-Xmx$(get_arg -memmax 256M)" 40 | export DDB_LOCAL_TELEMETRY=0 41 | (cd $BKJS_HOME/var && exec nohup java $params -Djava.library.path=$DYNAMODB_PREFIX/DynamoDBLocal_lib -jar $DYNAMODB_PREFIX/DynamoDBLocal.jar -sharedDb -disableTelemetry -dbPath $BKJS_HOME/var -port 8181 >>$BKJS_HOME/log/ddb.log 2>&1 &) 42 | exit 43 | ;; 44 | 45 | dynamodb-stop) 46 | pkill -f DynamoDBLocal 47 | exit 48 | ;; 49 | 50 | dynamodb-reset) 51 | $0 dynamodb-stop 52 | rm -rf $BKJS_HOME/var/*_us-east-1.db 53 | $0 dynamodb-run 54 | exit 55 | ;; 56 | 57 | esac 58 | 59 | -------------------------------------------------------------------------------- /web/js/app-passkey.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // alpinejs-app 2024 4 | // 5 | 6 | // Passkey support 7 | 8 | (() => { 9 | 10 | var app = window.app; 11 | 12 | app.passkey = { 13 | 14 | reg_path: "/passkey/register", 15 | login_path: "/passkey/login", 16 | 17 | init(callback) { 18 | if (this.client) return; 19 | import("/js/webauthn.min.mjs").then(mod => { 20 | this.client = mod.client; 21 | app.call(callback) 22 | }).catch(err => { 23 | app.call(callback, err); 24 | }); 25 | }, 26 | 27 | start(options, callback) { 28 | app.send({ url: this.reg_path, data: options?.data }, callback); 29 | }, 30 | 31 | finish(config, options, callback) { 32 | this.client.register(options?.name || app.user?.name, config?.challenge, { 33 | attestation: true, 34 | userHandle: config?.id, 35 | domain: config?.domain, 36 | }).then(data => { 37 | app.send({ url: this.reg_path, body: Object.assign(data || {}, options?.body) }, callback); 38 | }).catch(err => { 39 | app.call(callback, err); 40 | }); 41 | }, 42 | 43 | register(options, callback) { 44 | this.start(options, (err, config) => { 45 | if (err) return app.call(callback, err); 46 | this.finish(config, options, callback); 47 | }); 48 | }, 49 | 50 | login(options, callback) { 51 | app.fetch({ url: this.login_path }, (err, config) => { 52 | if (err) return app.call(callback, err); 53 | 54 | this.client.authenticate(app.util.split(options?.ids), config.challenge, { 55 | domain: config.domain, 56 | }).then(data => { 57 | app.send({ url: this.login_path, body: Object.assign(data, options?.body) }, callback); 58 | }).catch(err => { 59 | app.call(callback, err); 60 | }); 61 | }); 62 | }, 63 | } 64 | 65 | })(); 66 | -------------------------------------------------------------------------------- /examples/config/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | Backendjs Config Example 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 46 | 47 | 60 | -------------------------------------------------------------------------------- /web/js/app-dom.js: -------------------------------------------------------------------------------- 1 | /* 2 | * app client 3 | * Vlad Seryakov vseryakov@gmail.com 2018 4 | */ 5 | 6 | /* global document window */ 7 | 8 | (() => { 9 | var app = window.app; 10 | 11 | app.util = { 12 | 13 | // Inject CSS/Script resources into the current page, all urls are loaded at the same time by default. 14 | // - `options.series` - load urls one after another 15 | // - `options.async` if set then scripts executed as soon as loaded otherwise executing scripts will be in the order provided 16 | // - `options.callback` will be called with (el, opts) args for customizations after loading each url or on error 17 | // - `options.attrs` is an object with attributes to set like nonce, ... 18 | // - `options.timeout` - call the callback after timeout 19 | loadResources(urls, options, callback) 20 | { 21 | if (typeof options == "function") callback = options, options = null; 22 | if (typeof urls == "string") urls = [urls]; 23 | app[`forEach${options?.series ? "Series" : ""}`](urls, (url, next) => { 24 | let el; 25 | const ev = () => { app.call(options?.callback, el, options); next() } 26 | if (/\.css/.test(url)) { 27 | el = app.$elem("link", "rel", "stylesheet", "type", "text/css", "href", url, "load", ev, "error", ev) 28 | } else { 29 | el = app.$elem('script', "async", !!options?.async, "src", url, "load", ev, "error", ev) 30 | } 31 | for (const p in options?.attrs) app.$attr(el, p, options.attrs[p]); 32 | document.head.appendChild(el); 33 | }, options?.timeout > 0 ? () => { setTimeout(callback, options.timeout) } : callback); 34 | }, 35 | 36 | // Return a random hex string 37 | random(size) 38 | { 39 | var s = "", u = new Uint8Array(size || 16), h = "0123456789abcdef"; 40 | window.crypto.getRandomValues(u); 41 | for (let i = 0; i < u.length; i++) s += h.charAt(u[i] >> 4) + h.charAt(u[i] & 0x0F); 42 | return s; 43 | }, 44 | } 45 | 46 | })(); 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![logo](https://vseryakov.github.io/backendjs/web/img/logo.png) Backend library for Node.js 2 | 3 | A Node.js library to create Web apps with minimal dependencies. 4 | 5 | ## Features: 6 | 7 | * Express 5 is used for the API access. 8 | * Authentication is based on signed requests using API key and secret, similar to Amazon AWS signing requests. 9 | * Web sessions with CSRF protection, Webauthn/Passkeys. 10 | * Web server runs in separate processes to utilize multiple CPU cores. 11 | * WebSocket connections are processed by the same Express routes. 12 | * Rate limiter based on TokenBucket algorithm can be run in-process or in Redis. 13 | * PUB/SUB modes of operations using Redis, NATS. 14 | * Async jobs processing using queue implementation on top of SQS, Redis, NATS, jobs can be scheduled or run on-demand in separate processes. 15 | * REPL (command line) interface for debugging and looking into server internals. 16 | * Push notifications via Webpush. 17 | * Can be used with any MVC, MVVC or other types of frameworks that work on top of, or with the included Express server. 18 | * Simple DB layer for CRUD operations like Get/Select/Put/Del/Update for supported databases (SQLite, PostreSQL, DynamoDB, ElasticSearch). 19 | * AWS support is very well integrated by using APIs directly without AWS SDK, includes EC2, ECS, S3, DynamoDB, SQS, CloudWatch and more. 20 | * Includes a simple log watcher to monitor the log files and CloudWatch logs. 21 | * Runtime metrics about CPU, DB, requests, cache, memory, rate limits, AWS X-Ray spans. 22 | * The backend process can be used directly in Docker containers without shim init. 23 | 24 | ## NOTE: work in progress, major refactor, the NPM version is stable 25 | 26 | ## Documentation 27 | 28 | Visit the [documentation](https://vseryakov.github.io/backendjs/docs/web/index.html). 29 | 30 | ## Installation 31 | 32 | npm install backendjs --save 33 | 34 | or clone the repo 35 | 36 | git clone https://github.com/vseryakov/backendjs.git 37 | 38 | ## License 39 | [MIT](/LICENSE) 40 | 41 | ## Author 42 | Vlad Seryakov 43 | 44 | -------------------------------------------------------------------------------- /lib/queue/eventbridge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const logger = require(__dirname + '/../logger'); 7 | const lib = require(__dirname + '/../lib'); 8 | const aws = require(__dirname + '/../aws'); 9 | const QueueClient = require(__dirname + "/client"); 10 | 11 | const eventbridgeClient = { 12 | name: "eventbridge", 13 | 14 | create: function(options) { 15 | if (/^eventbridge:\/\//.test(options?.url)) return new EventBridgeClient(options); 16 | } 17 | }; 18 | module.exports = eventbridgeClient; 19 | 20 | /** 21 | * Queue client using AWS EventBridge. 22 | * 23 | * The URL must look like: `eventbridge://?[params]`. 24 | * 25 | * @example 26 | * 27 | * -queue-events=eventbridge:// 28 | * -queue-events=eventbridge://?bk-endpoint=12345 29 | * -queue-events=eventbridge://?bk-bus=mybus 30 | * -queue-events=eventbridge://?bk-source=mysource 31 | * @memberOf module:queue 32 | */ 33 | 34 | class EventBridgeClient extends QueueClient { 35 | 36 | constructor(options) { 37 | super(options); 38 | this.name = eventbridgeClient.name; 39 | this.applyOptions(); 40 | this.emit("ready"); 41 | } 42 | 43 | submit(events, options, callback) { 44 | logger.dev("submit:", this.url, events, options); 45 | 46 | var entries = []; 47 | if (!Array.isArray(events)) events = [events]; 48 | for (const event of events) { 49 | if (!event) continue; 50 | entries.push({ 51 | Source: options.source || this.options.source || this.queueName, 52 | DetailType: event.subject || options.subject || this.options.subject, 53 | Details: lib.stringify(event), 54 | EventBusName: options.eventBusName || this.options.bus, 55 | TraceHeader: options.traceHeader, 56 | }); 57 | } 58 | 59 | aws.queryEvents("PutEvents", { EndpointId: this.options.endpoint, Entries: entries }, options, callback); 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /lib/lib/hash.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const crypto = require('node:crypto'); 7 | const logger = require(__dirname + '/../logger'); 8 | const lib = require(__dirname + '/../lib'); 9 | 10 | /** 11 | * Hash wrapper without exceptions for node crypto createHash 12 | * @param {string|Buffer} data - data to hash 13 | * @param {string} [algorithm] - sha1 is default, any supported hash algorithm by node:crypto 14 | * @param {string} [encode] - encoding, base64 by default 15 | * @return {string} calculated hash or empty string on error 16 | * @memberof module:lib 17 | * @method hash 18 | */ 19 | lib.hash = function (data, algorithm, encode) 20 | { 21 | encode = encode === "binary" ? undefined : encode || "base64"; 22 | try { 23 | return crypto.createHash(algorithm || "sha1").update(data || "").digest(encode); 24 | } catch (e) { 25 | logger.error('hash:', algorithm, encode, e.stack); 26 | return ""; 27 | } 28 | } 29 | 30 | /** 31 | * 32-bit MurmurHash3 implemented by bryc (github.com/bryc) 32 | * @param {string} key input string 33 | * @return {number} hash number 34 | * @memberof module:lib 35 | * @method murmurHash3 36 | */ 37 | lib.murmurHash3 = function(key, seed = 0) 38 | { 39 | if (typeof key != "string") return 0; 40 | 41 | var k, p1 = 3432918353, p2 = 461845907, h = seed | 0; 42 | 43 | for (var i = 0, b = key.length & -4; i < b; i += 4) { 44 | k = key[i+3] << 24 | key[i+2] << 16 | key[i+1] << 8 | key[i]; 45 | k = Math.imul(k, p1); k = k << 15 | k >>> 17; 46 | h ^= Math.imul(k, p2); h = h << 13 | h >>> 19; 47 | h = Math.imul(h, 5) + 3864292196 | 0; 48 | } 49 | 50 | k = 0; 51 | switch (key.length & 3) { 52 | case 3: k ^= key[i+2] << 16; 53 | case 2: k ^= key[i+1] << 8; 54 | case 1: k ^= key[i]; 55 | k = Math.imul(k, p1); k = k << 15 | k >>> 17; 56 | h ^= Math.imul(k, p2); 57 | } 58 | 59 | h ^= key.length; 60 | h ^= h >>> 16; h = Math.imul(h, 2246822507); 61 | h ^= h >>> 13; h = Math.imul(h, 3266489909); 62 | h ^= h >>> 16; 63 | 64 | return h >>> 0; 65 | } 66 | -------------------------------------------------------------------------------- /lib/api/routing.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | /** 7 | * @module api/routing 8 | */ 9 | 10 | const lib = require(__dirname + '/../lib'); 11 | const api = require(__dirname + '/../api'); 12 | const logger = require(__dirname + '/../logger'); 13 | 14 | const mod = { 15 | name: "api.routing", 16 | args: [ 17 | { name: "err-(.+)", descr: "Error messages for various cases" }, 18 | { name: "path-(.+)", type: "regexpobj", reverse: 1, nocamel: 1, obj: 'path', descr: "Locations to be re-routed to other path, this is done inside the server at the beginning, only the path is replaced, same format and placeholders as in redirect-url, use ! in front of regexp to remove particular redirect from the list", example: "-api-routing-path-^/user/get /user/read" }, 19 | { name: "auth-(.+)", type: "regexpobj", reverse: 1, nocamel: 1, obj: 'auth', descr: "URL path to be re-routed to other path after the authentication is successful, this is done inside the server, only the path is replaced, same format and placeholders as in redirect-url", example: "-api-routing-auth-^/user/get /user/read" }, 20 | { name: "reset", type: "callback", callback: function(v) { if (v) this.reset() }, descr: "Reset all rules" }, 21 | ], 22 | }; 23 | 24 | /** 25 | * Routing rewriting 26 | */ 27 | module.exports = mod; 28 | 29 | mod.reset = function() 30 | { 31 | delete this.path; 32 | delete this.auth; 33 | } 34 | 35 | /** 36 | * Check if the current request must be re-routed to another endpoint, 37 | * returns true if the url has been replaced 38 | */ 39 | mod.check = function(req, name) 40 | { 41 | var rules = this[name]; 42 | if (!rules) return; 43 | 44 | const location = req.options.host + req.options.path; 45 | for (const p in rules) { 46 | if (lib.testRegexpObj(req.options.path, rules[p]) || lib.testRegexpObj(location, rules[p])) { 47 | req.signatureUrl = req.url; 48 | api.replacePath(req, api.checkRedirectPlaceholders(req, p)); 49 | logger.debug("check:", this.name, name, location, "switch to:", p, req.url); 50 | return true; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/limiter.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { describe, it, before, after } = require('node:test'); 3 | const assert = require('node:assert/strict'); 4 | const { app, lib, cache } = require("../"); 5 | const { ainit } = require("./utils"); 6 | 7 | var opts = { 8 | name: "test", 9 | rate: 1, 10 | max: 1, 11 | interval: 100, 12 | cacheName: process.env.BKJS_ROLES || "local", 13 | pace: 5, 14 | count: 5, 15 | delays: 4, 16 | }; 17 | 18 | describe("Limiter tests", async () => { 19 | 20 | before(async () => { 21 | await ainit({ nodb: 1, cache: 1, roles: process.env.BKJS_ROLES }); 22 | }); 23 | 24 | it("should delay the pace", async () => ( 25 | new Promise((resolve, reject) => { 26 | var list = [], delays = 0; 27 | for (let i = 0; i < opts.count; i++) list.push(i); 28 | 29 | lib.forEachSeries(list, (i, next2) => { 30 | lib.doWhilst( 31 | function(next3) { 32 | cache.limiter(opts, (delay) => { 33 | opts.delay = delay; 34 | setTimeout(next3, delay); 35 | }); 36 | }, 37 | function() { 38 | if (opts.delay) delays++; 39 | return opts.delay; 40 | }, 41 | function() { 42 | setTimeout(next2, opts.pace); 43 | }); 44 | }, () => { 45 | assert.strictEqual(delays, opts.delays); 46 | resolve(); 47 | }); 48 | }) 49 | )); 50 | 51 | it("should wait and continue", async () => { 52 | opts.retry = 2; 53 | await cache.alimiter(opts); 54 | const { delay } = await cache.acheckLimiter(opts); 55 | assert.ok(!delay && opts._retries == 2); 56 | }); 57 | 58 | it("should fail after first run", async () => ( 59 | new Promise((resolve, reject) => { 60 | opts.retry = 1; 61 | delete opts._retries; 62 | cache.limiter(opts, (delay, info) => { 63 | cache.checkLimiter(opts, (delay, info) => { 64 | assert.ok(delay && opts._retries == 1); 65 | resolve(); 66 | }); 67 | }); 68 | }) 69 | )); 70 | 71 | after(async () => { 72 | await app.astop(); 73 | }); 74 | 75 | }); 76 | -------------------------------------------------------------------------------- /tests/logwatcher.test.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require("fs"); 3 | const { describe, it } = require('node:test'); 4 | const assert = require('node:assert/strict'); 5 | const { app, lib, logwatcher } = require("../"); 6 | 7 | describe("Logwatcher tests", () => { 8 | 9 | var argv = ["-logwatcher-send-error", "none://", 10 | "-logwatcher-send-test", "none://", 11 | "-logwatcher-send-ignore", "none://", 12 | "-logwatcher-send-warning", "none://", 13 | "-logwatcher-send-any", "none://", 14 | "-logwatcher-matches-test", "TEST: ", 15 | "-logwatcher-ignore-error", "error2", 16 | "-logwatcher-ignore-warning", "warning2", 17 | "-logwatcher-once-test2", "test2", 18 | "-logwatcher-matches-any", "line:[0-9]+", 19 | "-logwatcher-interval", "1", 20 | "-app-log-file", "/tmp/message.log", 21 | "-app-err-file", "/tmp/error.log", 22 | "-db-pool", "none", 23 | ]; 24 | var lines = [ 25 | " ERROR: error1", 26 | " continue error1", 27 | "]: WARN: warning1", 28 | "]: WARN: warning2", 29 | " backtrace test line:123", 30 | "[] TEST: test1", 31 | "[] TEST: test2 shown", 32 | "[] TEST: test2 skipped", 33 | "[] TEST: test2 skipped", 34 | "[] ERROR: error2", 35 | "no error string", 36 | "no error string", 37 | "no error string", 38 | "no error string", 39 | "no error string", 40 | "no error string", 41 | " backtrace test line:456", 42 | ]; 43 | 44 | logwatcher.files = logwatcher.files.filter((x) => (x.name)); 45 | 46 | app.parseArgs(argv); 47 | fs.writeFileSync(app.errFile, lines.join("\n")); 48 | fs.writeFileSync(app.logFile, lines.join("\n")); 49 | 50 | it("process log files", (t, callback) => { 51 | logwatcher.run((err, rc) => { 52 | assert.ifError(err); 53 | assert.equal(lib.objKeys(rc?.errors).length, 5); 54 | callback(err); 55 | }); 56 | }); 57 | }) 58 | 59 | -------------------------------------------------------------------------------- /examples/modules/bk_system.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2018 4 | // 5 | 6 | const { modules, api, ipc, core, lib, logger } = require('backendjs'); 7 | 8 | // System management 9 | const mod = { 10 | name: "bk_system", 11 | }; 12 | module.exports = mod; 13 | 14 | // Create API endpoints and routes 15 | mod.configureWeb = function(options, callback) 16 | { 17 | api.app.post(/^\/system\/([^/]+)\/?(.+)?/, (req, res) => { 18 | 19 | switch (req.params[0]) { 20 | case "restart": 21 | ipc.sendMsg(`${req.params[1] || "api"}:restart`); 22 | res.json({}); 23 | break; 24 | 25 | case "init": 26 | if (req.params[1]) { 27 | ipc.broadcast(app.id + ":server", req.params[1] + ":" + req.params[0]); 28 | } 29 | res.json({}); 30 | break; 31 | 32 | case "params": 33 | var args = []; 34 | Object.keys(modules).forEach((n) => { 35 | if (modules[n].args) args.push([n, app.modules[n].args]); 36 | }); 37 | switch (req.params[1]) { 38 | case 'get': 39 | res.json(args.reduce((data, x) => { 40 | x[1].forEach((y) => { 41 | if (!y._name) return; 42 | var val = lib.objGet(modules[x[0]], y._name); 43 | if (val == null && !options.total) return; 44 | data[y._key] = typeof val == "undefined" ? null : val; 45 | }); 46 | return data; 47 | }, { "-home": app.home, "-log": logger.level })); 48 | break; 49 | 50 | case "info": 51 | res.json(args.reduce((data, x) => { 52 | x[1].forEach((y) => { 53 | data[(x[0] ? x[0] + "-" : "") + y.name] = y; 54 | }); 55 | return data; 56 | }, {})); 57 | break; 58 | default: 59 | api.sendReply(res, 400, "Invalid command"); 60 | } 61 | break; 62 | 63 | default: 64 | api.sendReply(res, 400, "Invalid command"); 65 | } 66 | }); 67 | } 68 | 69 | -------------------------------------------------------------------------------- /examples/config/web/config.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 | 6 | System Config 7 |
8 | 9 | 14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 | 41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /web/js/app-sanitizer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * app client 3 | * Vlad Seryakov vseryakov@gmail.com 2018 4 | */ 5 | 6 | /* global window */ 7 | 8 | (() => { 9 | var app = window.app; 10 | 11 | function isattr(attr, list) 12 | { 13 | const name = attr.nodeName.toLowerCase(); 14 | if (list.includes(name)) { 15 | if (sanitizer._attrs.has(name)) { 16 | return sanitizer._urls.test(attr.nodeValue) || sanitizer._data.test(attr.nodeValue); 17 | } 18 | return true; 19 | } 20 | return list.some((x) => (x instanceof RegExp && x.test(name))); 21 | } 22 | 23 | // Based on Bootstrap internal sanitizer 24 | function sanitizer(html, list) 25 | { 26 | if (!html || typeof html != "string") return list ? [] : html; 27 | const body = app.$parse(html); 28 | const elements = [...body.querySelectorAll('*')]; 29 | for (const el of elements) { 30 | const name = el.nodeName.toLowerCase(); 31 | if (sanitizer._tags[name]) { 32 | const allow = [...sanitizer._tags['*'], ...sanitizer._tags[name] || []]; 33 | for (const attr of [...el.attributes]) { 34 | if (!isattr(attr, allow)) el.removeAttribute(attr.nodeName); 35 | } 36 | } else { 37 | el.remove(); 38 | } 39 | } 40 | return list ? Array.from(body.childNodes) : body.innerHTML; 41 | } 42 | 43 | sanitizer._attrs = new Set(['background','cite','href','itemtype','longdesc','poster','src','xlink:href']) 44 | sanitizer._urls = /^(?:(?:https?|mailto|ftp|tel|file|sms):|[^#&/:?]*(?:[#/?]|$))/i 45 | sanitizer._data = /^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i 46 | sanitizer._tags = { 47 | '*': ['class', 'dir', 'id', 'lang', 'role', /^aria-[\w-]*$/i, 48 | 'data-bs-toggle', 'data-bs-target', 'data-bs-dismiss', 'data-bs-parent'], 49 | a: ['target', 'href', 'title', 'rel'], area: [], 50 | b: [], blockquote: [], br: [], button: [], 51 | col: [], code: [], 52 | div: [], em: [], hr: [], 53 | img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'style'], 54 | h1: [], h2: [], h3: [], h4: [], h5: [], h6: [], 55 | i: [], li: [], ol: [], p: [], pre: [], 56 | s: [], small: [], span: [], sub: [], sup: [], strong: [], 57 | table: [], thead: [], tbody: [], th: [], tr: [], td: [], 58 | u: [], ul: [], 59 | } 60 | 61 | app.sanitizer = sanitizer; 62 | 63 | })(); 64 | -------------------------------------------------------------------------------- /examples/kanban/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | Boards - Kanban 3 | 4 | 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 68 | 69 | -------------------------------------------------------------------------------- /tools/bkjs-ec2-ami: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$BKJS_CMD" in 4 | 5 | help|ec2-help) 6 | echo 7 | echo " ec2-create-ami [-tag NAME] [-prefix PREFIX] - create a new AMI from the given running instance by tag or the current instance" 8 | echo " ec2-create-launch-template-version [-name NAME] [-tag TAG] [-skip A B] [-image-name *] - create new launch template verson with the latest AMI, if no -name is given all existing templates matched by tag if given will be updated" 9 | echo " ec2-build-ami - [-image-id ID] [-image-name N] [-ssh-user alpine] - build a new AMI for Alpine Linux, runs alpine-build-ami-hook for actual setup, uses -aws-launch-instance, either -image-id or -image-name is required" 10 | echo " ec2-build-ami-hook - actual new AMI build script after it is launched and ssh is ready: INSTANCE_USER, INSTANCE_HOST, INSTANCE_ID, INSTANCE_IP, INSTANCE_ARCH are set" 11 | ;; 12 | 13 | ec2-create-ami) 14 | tag=$(get_arg -tag) 15 | [ -n "$tag" ] && instance_id=$(bkjs ec2-show -tag $tag -fmt id | head -1) 16 | instance_id=$(get_arg -instance-id $instance_id) 17 | $BKJS_BIN shell -no db,ipc -aws-create-image -wait -instance-id $instance_id $(get_all_args "-instance-id -tag") 18 | exit 19 | ;; 20 | 21 | ec2-create-launch-template-version) 22 | name=$(get_arg -name) 23 | skip=$(get_arg -skip) 24 | if [ -z "$name" ]; then 25 | tag=$(get_arg -tag) 26 | key=$(get_arg -key Name) 27 | [ "$tag" != "" ] && filter="--filter Name=tag:$key,Values=${tag}" 28 | name=$(aws ec2 describe-launch-templates $filter --query 'LaunchTemplates[*].LaunchTemplateName' --output text|sed 's/\t/\n/g'|sort|uniq) 29 | fi 30 | for c in $name; do 31 | if list_has $c $skip; then continue; fi 32 | $BKJS_BIN shell -no db,ipc -aws-create-launch-template-version -name $c $(get_all_args "-name -skip -tag -key") 33 | done 34 | exit 35 | ;; 36 | 37 | ec2-build-ami) 38 | run_bkjs_cmd ec2-launch-instance 1 -return -ssh-user alpine 39 | [ "$?" != "0" ] && exit 1 40 | 41 | # Run the hooks to do the actual work now 42 | run_bkjs_cmd $BKJS_CMD-hook 1 43 | exit 44 | ;; 45 | 46 | ec2-build-ami-hook) 47 | # Default hooks to build Alpine image 48 | ssh $INSTANCE_USER@$INSTANCE_HOST "doas apk add git && git clone --depth=1 https://github.com/vseryakov/backendjs.git && doas backendjs/bkjs setup-ec2 && doas reboot" 49 | echo 50 | echo "now you can ssh into $INSTANCE_ID AS ec2-user@$INSTANCE_HOST" 51 | exit 52 | ;; 53 | 54 | esac 55 | -------------------------------------------------------------------------------- /tests/jobs.test.js: -------------------------------------------------------------------------------- 1 | 2 | const cluster = require("node:cluster"); 3 | const { app, lib, jobs, cache } = require("../"); 4 | const { ainit, astop, testJob } = require("./utils"); 5 | 6 | jobs.testJob = testJob; 7 | 8 | if (cluster.isWorker) { 9 | return app.start({ worker: 1, roles: process.env.BKJS_ROLES || "redis", config: __dirname + "/bkjs.conf" }, () => { 10 | process.exit(); 11 | }); 12 | } 13 | 14 | const { describe, it, before, after } = require('node:test'); 15 | const assert = require('node:assert/strict'); 16 | const { promisify } = require("util"); 17 | 18 | const submitJob = promisify(jobs.submitJob.bind(jobs)); 19 | 20 | describe("Jobs tests", async () => { 21 | 22 | var opts = { 23 | queueName: process.env.BKJS_ROLES || "redis", 24 | }; 25 | 26 | before(async () => { 27 | await ainit({ jobs: 1, roles: process.env.BKJS_ROLES || "redis" }) 28 | await cache.adel(opts.queueName); 29 | await cache.adel("#" + opts.queueName); 30 | }); 31 | 32 | after(async () => { 33 | await astop(); 34 | }); 35 | 36 | 37 | await it("run simple job", async () => { 38 | var file = "/tmp/job1.test"; 39 | lib.unlinkSync(file); 40 | 41 | await jobs.submitJob({ job: { "jobs.testJob": { file, data: "job" } } }, opts); 42 | await lib.sleep(200) 43 | 44 | var data = lib.readFileSync(file); 45 | assert.match(data, /job/); 46 | }); 47 | 48 | await it("run cancel job", async () => { 49 | const file = "/tmp/job2.test"; 50 | lib.unlinkSync(file); 51 | 52 | await submitJob({ job: { "jobs.testJob": { file, cancel: "job2", timeout: 5000 } } }, opts); 53 | await lib.sleep(500) 54 | 55 | jobs.cancelJob("job2"); 56 | await lib.sleep(500); 57 | 58 | var data = lib.readFileSync(file); 59 | assert.match(data, /cancelled/); 60 | }); 61 | 62 | it("run local job", async () => { 63 | const file = "/tmp/job3.test"; 64 | lib.unlinkSync(file); 65 | 66 | await submitJob({ job: { "jobs.testJob": { file, data: "local" } } }, { queueName: "local" }); 67 | await lib.sleep(100) 68 | 69 | var data = lib.readFileSync(file); 70 | assert.match(data, /local/); 71 | 72 | }); 73 | 74 | it("run worker job", async() => { 75 | var file = "/tmp/job4.test"; 76 | lib.unlinkSync(file); 77 | 78 | await submitJob({ job: { "jobs.testJob": { file, data: "worker" } } }, { queueName: "worker" }); 79 | await lib.sleep(500) 80 | 81 | var data = lib.readFileSync(file); 82 | assert.match(data, /worker/); 83 | }); 84 | }); 85 | 86 | 87 | -------------------------------------------------------------------------------- /tools/bkjs-nats: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|nats-help) 10 | echo "" 11 | echo " nats-get - install local NATS server in $BKJS_HOME/bin" 12 | echo " nats-getcli - install NATS command line tool in $BKJS_HOME/bin" 13 | echo " nats-run - run local NATS server installed in $BKJS_HOME/bin" 14 | echo " nats-stop - stop local NATS server" 15 | ;; 16 | 17 | nats-get) 18 | platform=$(to_lower $PLATFORM) 19 | curl -L -o /tmp/nats.tgz https://github.com/nats-io/nats-server/releases/download/v2.9.15/nats-server-v2.9.15-$platform-$OS_ARCH.tar.gz 20 | tar --strip-components=1 -C /tmp -xzf /tmp/nats.tgz 21 | mv /tmp/nats-server $BKJS_HOME/bin 22 | rm -rf /tmp/nats.tgz 23 | if [ ! -f $BKJS_HOME/etc/nats.conf ]; then 24 | echo 'listen: localhost:4222' >> $BKJS_HOME/etc/nats.conf 25 | echo 'http: localhost:8222' >> $BKJS_HOME/etc/nats.conf 26 | echo 'syslog: true' >> $BKJS_HOME/etc/nats.conf 27 | echo 'logtime: false' >> $BKJS_HOME/etc/nats.conf 28 | echo "pid_file: $BKJS_HOME/var/nats.pid" >> $BKJS_HOME/etc/nats.conf 29 | echo 'jetstream: {' >> $BKJS_HOME/etc/nats.conf 30 | echo " store_dir: \"$BKJS_HOME/var\"" >> $BKJS_HOME/etc/nats.conf 31 | echo ' max_file: 1G' >> $BKJS_HOME/etc/nats.conf 32 | echo '}' >> $BKJS_HOME/etc/nats.conf 33 | echo 'cluster {' >> $BKJS_HOME/etc/nats.conf 34 | echo ' name: nats' >> $BKJS_HOME/etc/nats.conf 35 | echo ' #listen: 0.0.0.0:6222' >> $BKJS_HOME/etc/nats.conf 36 | echo ' routes: [' >> $BKJS_HOME/etc/nats.conf 37 | echo ' nats-route://nats:6222' >> $BKJS_HOME/etc/nats.conf 38 | echo ' ]' >> $BKJS_HOME/etc/nats.conf 39 | echo '}' >> $BKJS_HOME/etc/nats.conf 40 | fi 41 | exit 42 | ;; 43 | 44 | nats-getcli) 45 | platform=$(to_lower $PLATFORM) 46 | curl -L -o /tmp/nats.zip https://github.com/nats-io/natscli/releases/download/v0.0.35/nats-0.0.35-$platform-$OS_ARCH.zip 47 | unzip -j /tmp/nats.zip -d /tmp '*/nats' 48 | mv /tmp/nats $BKJS_HOME/bin 49 | rm -rf /tmp/nats.zip 50 | exit 51 | ;; 52 | 53 | nats-run) 54 | name=$(get_arg -name $(uname -n|cut -f1 -d.)) 55 | mkdir -p $BKJS_HOME/var $BKJS_HOME/log 56 | exec nohup nats-server -n $name -c $BKJS_HOME/etc/nats.conf >>$BKJS_HOME/log/message.log 2>&1 & 57 | exit 58 | ;; 59 | 60 | nats-stop) 61 | pkill -f nats 62 | exit 63 | ;; 64 | 65 | nats-init-monit) 66 | $0 monit-init -name nats -start "$BKJS_BIN nats-run" -stop "$BKJS_BIN nats-stop" 67 | exit 68 | ;; 69 | 70 | esac 71 | 72 | -------------------------------------------------------------------------------- /examples/config/modules/config.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2025 4 | // 5 | 6 | const { db, api, lib } = require('../../../lib/index'); 7 | 8 | // Config module 9 | const mod = { 10 | name: "config", 11 | args: [ 12 | { name: "roles", type: "list", descr: "List of roles that can access this module, ex: -config-roles admin,user" }, 13 | ], 14 | tables: { 15 | bk_config: { 16 | name: { primary: 1 }, 17 | ctime: { type: "now", primary: 2 }, 18 | type: {}, 19 | value: {}, 20 | mtime: { type: "now" }, 21 | }, 22 | }, 23 | roles: ["admin"], 24 | }; 25 | module.exports = mod; 26 | 27 | // Create API endpoints 28 | mod.configureWeb = function(options, callback) 29 | { 30 | api.app.use("/config", 31 | api.express.Router(). 32 | use(perms). 33 | get("/list", select). 34 | post("/put", put). 35 | post("/update", update). 36 | post("/del", del)); 37 | 38 | callback(); 39 | } 40 | 41 | function perms(req, res, next) 42 | { 43 | if (!lib.isFlag(mod.roles, req.user?.roles)) { 44 | return api.sendReply(res, 403, "access denied"); 45 | } 46 | next(); 47 | } 48 | 49 | function select(req, res) 50 | { 51 | var data = []; 52 | db.scan("bk_config", {}, { sync: 1 }, (rows) => { 53 | data.push(...rows); 54 | }, (err) => { 55 | api.sendJSON(req, err, { count: data.length, data }); 56 | }); 57 | } 58 | 59 | function put(req, res) 60 | { 61 | var query = api.toParams(req, { 62 | name: { required: 1 }, 63 | type: {}, 64 | value: {}, 65 | }); 66 | if (typeof query == "string") return api.sendReply(res, 400, query); 67 | 68 | db.put("bk_config", query, (err) => { 69 | api.sendJSON(req, err); 70 | }); 71 | } 72 | 73 | function update(req, res) 74 | { 75 | var query = api.toParams(req, { 76 | ctime: { type: "int", required: 1 }, 77 | name: { required: 1 }, 78 | type: {}, 79 | value: {}, 80 | }); 81 | if (typeof query == "string") return api.sendReply(res, 400, query); 82 | 83 | db.update("bk_config", query, (err) => { 84 | api.sendJSON(req, err); 85 | }); 86 | } 87 | 88 | function del(req, res) 89 | { 90 | var query = api.toParams(req, { 91 | ctime: { type: "int", required: 1 }, 92 | name: { required: 1 }, 93 | }); 94 | if (typeof query == "string") return api.sendReply(res, 400, query); 95 | 96 | db.del("bk_config", query, (err) => { 97 | api.sendJSON(req, err); 98 | }); 99 | } 100 | -------------------------------------------------------------------------------- /examples/kanban/README.md: -------------------------------------------------------------------------------- 1 | # Kanban Board - Alpine.js Implementation 2 | 3 | A full-featured Kanban board application built with Alpine.js and Alpinejs-app, featuring drag-and-drop, real-time updates, and SQLite/PostgreSQL persistence. 4 | 5 | ## Tech Stack 6 | 7 | - **Framework**: Backendjs with Alpinejs-app 8 | - **Database**: SQLite (local) / PostgreSQL (production) 9 | - **Deployment**: none 10 | - **Styling**: Bootstrap 5 11 | - **Drag & Drop**: native 12 | - **Animations**: Alpine.js 13 | - **Charts**: charts.css 14 | - **Validation**: backendjs 15 | 16 | ## Local Development 17 | 18 | The app uses a dual-database setup: 19 | - **Local Development**: node:sqlite 20 | - **Production**: PostgreSQL 21 | 22 | ### First Time Setup 23 | 24 | ```bash 25 | npm install 26 | npm run setup # Resets, migrates, and seeds the SQLite database 27 | npm run dev 28 | ``` 29 | 30 | Visit [http://localhost:8000](http://localhost:8000) 31 | 32 | > **Note**: Migration files are included in the repo, so you don't need to generate them. 33 | 34 | ### Subsequent Runs 35 | 36 | ```bash 37 | npm run dev 38 | ``` 39 | 40 | ## Database Setup 41 | 42 | The app uses a dual-database setup: 43 | - **Local Development**: node:sqlite 44 | - **Production**: PostgreSQL 45 | 46 | ### Local Database (SQLite) 47 | 48 | ```bash 49 | npm run setup # This automatically resets, migrates, and seeds 50 | ``` 51 | 52 | ## Project Structure 53 | 54 | ``` 55 | src/ 56 | ├── web/ 57 | │ ├── card.js # Card component 58 | │ ├── board.js # Board component 59 | │ └── index.html # Home page 60 | ├── modules/ 61 | │ ├── board.js # Board API 62 | │ └── card.js # Card API 63 | tools/ 64 | └── seed.js # DB seed file 65 | kanban.db # Local SQLite database 66 | ``` 67 | 68 | ## Features 69 | 70 | - ✅ Create and manage multiple boards 71 | - ✅ Four fixed lists per board (Todo, In-Progress, QA, Done) 72 | - ✅ Create, edit, and delete cards 73 | - ✅ Drag-and-drop cards within and between lists 74 | - ✅ Assign users to cards 75 | - ✅ Add tags to cards with color coding 76 | - ✅ Add comments to cards 77 | - ✅ Mark cards as complete 78 | - ✅ Responsive design with Bootstrap theme 79 | - ✅ Board overview charts 80 | - ✅ Alpinejs-app for dynamic updates 81 | - ✅ Locality of behavior with inline event handlers 82 | - ✅ Native HTML dialogs 83 | - ✅ Error handling with loading states 84 | - ✅ Dual-database support (SQLite + PostgreSQL) 85 | 86 | ## Learn More 87 | 88 | - [Backendjs Documentation](https://vseryakov.github.io/backendjs/docs/web/index.html) 89 | - [Alpinejs-app Documentation](https://github.com/vseryakov/alpinejs-app) 90 | - [Alpine.js Documentation](https://alpinejs.dev/) 91 | - [Boostrap Documentation](https://getboostrap.com/) 92 | 93 | ## License 94 | 95 | MIT 96 | -------------------------------------------------------------------------------- /tools/bkjs-ecs-agent: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Aug 2024 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|docker-help) 10 | echo "" 11 | echo " ecs-setup-agent - setup ECS agent to start in the current runlevel later, updates iptables" 12 | echo " ecs-start-agent - start ECS agent as a docker container if not running, start docker if not running" 13 | echo " ecs-stop-agent - stop ECS agent" 14 | ;; 15 | 16 | ecs-setup-agent) 17 | sysctl -w net.ipv4.conf.all.route_localnet=1 18 | iptables -t nat -A PREROUTING -p tcp -d 169.254.170.2 --dport 80 -j DNAT --to-destination 127.0.0.1:51679 19 | iptables -t nat -A OUTPUT -d 169.254.170.2 -p tcp -m tcp --dport 80 -j REDIRECT --to-ports 51679 20 | 21 | mkdir -p /etc/ecs/ /var/log/ecs /var/lib/ecs/data 22 | touch /etc/ecs/ecs.config 23 | 24 | conf=/etc/monit.d/ecs-agent.conf 25 | if [ ! -f $conf ]; then 26 | msg Setup ECS agent... 27 | 28 | $BKJS_BIN setup-rsyslog-docker -tag ID 29 | killall -HUP rsyslogd 30 | 31 | mkdir -p /etc/monit.d 32 | cycles=$(get_arg -cycles 2) 33 | cycles2=$(( $cycles*2 )) 34 | echo "check program ecs-agent with path \"$BKJS_BIN ecs-start-agent\" every $cycles cycles" >$conf 35 | echo " if status != 0 for 2 times within ${cycles2} cycles then alert" >> $conf 36 | 37 | rc-update add docker 38 | fi 39 | exit 40 | ;; 41 | 42 | ecs-start-agent) 43 | if ! rc-service docker status; then 44 | rc-service docker start 45 | exit 0 46 | fi 47 | 48 | if docker top ecs-agent; then 49 | exit 0 50 | fi 51 | 52 | cluster=$(imds meta-data/tags/instance/ECS_CLUSTER) 53 | 54 | exec docker run -d --name ecs-agent \ 55 | --restart=unless-stopped \ 56 | --volume=/var/run:/var/run \ 57 | --volume=/var/log/ecs/:/log \ 58 | --volume=/var/lib/ecs/data:/data \ 59 | --volume=/etc/ecs:/etc/ecs \ 60 | --volume=/sbin:/host/sbin \ 61 | --volume=/lib:/lib \ 62 | --volume=/usr/lib:/usr/lib \ 63 | --volume=/proc:/host/proc \ 64 | --volume=/sys/fs/cgroup:/sys/fs/cgroup \ 65 | --net=host \ 66 | --env-file=/etc/ecs/ecs.config \ 67 | --env=ECS_CLUSTER=${cluster:-default} \ 68 | --env=ECS_LOGFILE=/log/ecs-agent.log \ 69 | --env=ECS_DATADIR=/data \ 70 | --env=ECS_ENABLE_TASK_IAM_ROLE=true \ 71 | --env=ECS_ENABLE_TASK_IAM_ROLE_NETWORK_HOST=true \ 72 | --env=ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs","syslog","none"]' \ 73 | --env=ECS_ENABLE_AWSLOGS_EXECUTIONROLE_OVERRIDE=true \ 74 | --cap-add=sys_admin \ 75 | --cap-add=net_admin \ 76 | public.ecr.aws/ecs/amazon-ecs-agent:latest 77 | ;; 78 | 79 | ecs-stop-agent) 80 | exec docker rm -f ecs-agent 81 | ;; 82 | 83 | esac 84 | -------------------------------------------------------------------------------- /lib/metrics.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | * 5 | * Based on https://github.com/felixge/node-measured 6 | */ 7 | 8 | /** 9 | * Metrics library 10 | * @module metrics 11 | */ 12 | 13 | exports.name = "metrics"; 14 | 15 | /** @var {Counter} */ 16 | exports.Counter = require('./metrics/Counter'); 17 | 18 | /** @var {Histogram} */ 19 | exports.Histogram = require('./metrics/Histogram'); 20 | 21 | /** @var {Meter} */ 22 | exports.Meter = require('./metrics/Meter'); 23 | 24 | /** @var {Timer} */ 25 | exports.Timer = require('./metrics/Timer'); 26 | 27 | /** @var {TokenBucket} */ 28 | exports.TokenBucket = require('./metrics/TokenBucket'); 29 | 30 | /** @var {Trace} */ 31 | exports.Trace = require('./metrics/Trace'); 32 | 33 | /** @var {FakeTrace} */ 34 | exports.FakeTrace = require('./metrics/FakeTrace'); 35 | 36 | /** 37 | * Convert all metrics for all propeties. 38 | * Options: 39 | * - reset - true to reset all metrics 40 | * - take - regexp for variable that should use `take` i.e. resetable counters 41 | * - skip - regexp of properties to ignore 42 | * @memberof module:metrics 43 | * @method toJSON 44 | */ 45 | exports.toJSON = function(obj, options) 46 | { 47 | var rc = {}; 48 | for (const p in obj) { 49 | const type = typeof obj[p]; 50 | if (!obj[p] || type == "function") continue; 51 | if (options?.skip?.test && options.skip.test(p)) continue; 52 | 53 | if (typeof obj[p].toJSON == "function") { 54 | rc[p] = obj[p].toJSON(options); 55 | } else 56 | if (type == "object") { 57 | rc[p] = this.toJSON(obj[p], options); 58 | } else 59 | if (type == "number") { 60 | rc[p] = obj[p]; 61 | if (options?.take?.test && options.take.test(p)) { 62 | obj[p] = 0; 63 | } 64 | } 65 | } 66 | return rc; 67 | } 68 | 69 | /** 70 | * Increments a counter in an object, creates a new var if not exist or not a number 71 | * 72 | * @param {object} obj 73 | * @param {string} name 74 | * @param {number} [coount=1] 75 | * @return {number} 76 | * @memberof module:metrics 77 | * @method incr 78 | */ 79 | exports.incr = function(obj, name, count) 80 | { 81 | if (typeof obj[name] != "number") obj[name] = 0; 82 | obj[name] += typeof count == "number" ? count : 1; 83 | return obj[name]; 84 | } 85 | 86 | /** 87 | * Return the value for the given var and resets it to 0 88 | * @param {object} obj 89 | * @param {string} name 90 | * @return {number} 91 | * @memberof module:metrics 92 | * @method take 93 | */ 94 | exports.take = function(obj, name) 95 | { 96 | if (typeof obj[name] !== "number") return 0; 97 | const n = obj[name]; 98 | obj[name] = 0; 99 | return n; 100 | } 101 | 102 | -------------------------------------------------------------------------------- /lib/queue/json.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | const logger = require(__dirname + '/../logger'); 8 | const QueueClient = require(__dirname + "/client"); 9 | const fs = require("fs"); 10 | 11 | const jsonClient = { 12 | name: "json", 13 | 14 | create: function(options) { 15 | if (/^json:\/\//.test(options?.url)) return new JSONClient(options); 16 | } 17 | }; 18 | module.exports = jsonClient; 19 | 20 | /** 21 | * Queue client using JSON files, one event per line. 22 | * 23 | * File name format: queueName-YYYY-MM-DDTHH:MM:SS.MSECZ-SEQNUM 24 | * 25 | * The URL must look like: `json://...`. 26 | * If no path is given it is placed in the current directory. 27 | * 28 | * Files are rotated by size or number of lines whatever is met first, 29 | * use cache config options `bk-size` and `bk-count` to set 30 | * 31 | * @example 32 | * -queue-events json:// 33 | * -queue-events json:///path/accesslog-?bk-count=1000&bk-size=1000000 34 | * @memberOf module:queue 35 | */ 36 | 37 | class JSONClient extends QueueClient { 38 | 39 | file 40 | name 41 | seq = 0 42 | size = 0; 43 | count = 0; 44 | 45 | constructor(options) { 46 | super(options); 47 | this.name = jsonClient.name; 48 | this.applyOptions(); 49 | this.emit("ready"); 50 | } 51 | 52 | submit(events, options, callback) { 53 | logger.dev("submit:", this.url, events, options); 54 | 55 | lib.series([ 56 | (next) => { 57 | if (this.file) return next(); 58 | this.name = `${this.pathname || ""}-${this.queueName}-${new Date().toISOString()}-${this.seq++}.json`; 59 | fs.open(this.name, 'w', (err, handle) => { 60 | if (!err) this.file = handle; 61 | next(err); 62 | }); 63 | }, 64 | 65 | (next) => { 66 | if (!Array.isArray(events)) events = [events]; 67 | fs.write(this.file, events.map(x => { 68 | var line = JSON.stringify(x) + "\n"; 69 | this.count++; 70 | this.size += line.length; 71 | return line; 72 | }).join(""), next); 73 | }, 74 | 75 | (next) => { 76 | if ((this.options.size > 0 && this.size >= this.options.size) || 77 | (this.options.count > 0 && this.count >= this.options.count)) { 78 | logger.debug("submit:", this.url, "closing", this.name, this.size, this.count); 79 | this.file = null; 80 | this.size = this.count = 0; 81 | } 82 | next(); 83 | }, 84 | 85 | ], callback, true); 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /lib/api/redirect.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | const util = require("util"); 6 | const lib = require(__dirname + '/../lib'); 7 | const logger = require(__dirname + '/../logger'); 8 | 9 | /** 10 | * @module api/redirect 11 | */ 12 | 13 | const mod = { 14 | name: "api.redirect", 15 | args: [ 16 | { name: "err-(.+)", descr: "Error messages for various cases" }, 17 | { name: "url", type: "regexpmap", descr: "Add to the list a JSON object with property name defining a location regexp to be matched early against in order to redirect using the value of the property, if the regexp starts with !, that means it must be removed from the list, variables can be used for substitution: @HOST@, @PATH@, @URL@, @BASE@, @DIR@, @QUERY@, status code can be prepended to the location", example: "{ '^[^/]+/path/$': '/path2/index.html', '.+/$': '301:@PATH@/index.html' }" }, 18 | { name: "login-(.+)", type: "regexpobj", reverse: 1, nocamel: 1, obj: "login", descr: "Define a location where to redirect if no login is provided, same format and placeholders as in redirect-url", example: "api-redirect-login-^/admin/=/login.html" }, 19 | { name: "reset", type: "callback", callback: function(v) { if (v) this.reset() }, descr: "Reset all rules" }, 20 | ], 21 | 22 | }; 23 | 24 | /** 25 | * configuration based request redirection 26 | */ 27 | 28 | module.exports = mod; 29 | 30 | mod.reset = function() 31 | { 32 | delete this.url; 33 | delete this.login; 34 | } 35 | 36 | /** 37 | * Check a request for possible redirection condition based on the configuration. 38 | * This is used by API servers for early redirections. It returns null 39 | * if no redirects or errors happend, otherwise an object with status that is expected by the `api.sendStatus` method. 40 | * The options is expected to contain the following cached request properties: 41 | * - path - from req.path or the request pathname only 42 | * - host - from req.hostname or the hostname part only 43 | * - port - port from the host: header if specified 44 | * - secure - if the protocol is https 45 | */ 46 | mod.check = function(req, name = "url") 47 | { 48 | var url = req.url, location = req.options.host + url; 49 | var rules = this[name]; 50 | for (var i in rules) { 51 | const rx = util.types.isRegExp(rules[i]?.rx) ? rules[i].rx : util.types.isRegExp(rules[i]) ? rules[i] : null; 52 | if (rx && (rx.test(url) || rx.test(location))) { 53 | let loc = !lib.isNumeric(i) ? i : rules[i].value || ""; 54 | if (!loc) continue; 55 | var status = 302; 56 | if (loc[0]== "3" && loc[1] == "0" && loc[3] == ":") { 57 | status = lib.toNumber(loc.substr(0, 3), { dflt: 302 }); 58 | loc = loc.substr(4); 59 | } 60 | loc = this.checkRedirectPlaceholders(req, loc); 61 | logger.debug("checkRedirectRules:", name, location, req.options.path, "=>", status, loc, "rule:", i, rules[i]); 62 | return { status: status, url: loc }; 63 | } 64 | } 65 | return null; 66 | } 67 | -------------------------------------------------------------------------------- /lib/metrics/Meter.js: -------------------------------------------------------------------------------- 1 | 2 | const ExponentiallyMovingWeightedAverage = require('./ExponentiallyMovingWeightedAverage'); 3 | 4 | 5 | module.exports = class Meter { 6 | 7 | /** 8 | * - Meter - Things that are measured as events / interval. 9 | * - count: The total of all values added to the meter. 10 | * - rate: The rate of the meter since the last toJSON() call. 11 | * - mean: The average rate since the meter was started. 12 | * - m1: The rate of the meter biased towards the last 1 minute. 13 | * - m5: The rate of the meter biased towards the last 5 minutes. 14 | * - m15: The rate of the meter biased towards the last 15 minutes. 15 | * @param {object} [options] 16 | * @class Meter 17 | */ 18 | constructor(options) { 19 | if (options?.reset) this._reset = options?.reset; 20 | this._unit = options?.unit || 1000; 21 | this._interval = options?.interval || 5000; 22 | this._init(); 23 | } 24 | 25 | _init() { 26 | this._m1Rate = new ExponentiallyMovingWeightedAverage(60000, this._interval); 27 | this._m5Rate = new ExponentiallyMovingWeightedAverage(5 * 60000, this._interval); 28 | this._m15Rate = new ExponentiallyMovingWeightedAverage(15 * 60000, this._interval); 29 | this._count = this._sum = 0; 30 | } 31 | 32 | mark(value) { 33 | if (!this._timer) this.start(); 34 | value = typeof value == "number" && value || 1; 35 | this._count += value; 36 | this._sum += value; 37 | this._m1Rate.update(value); 38 | this._m5Rate.update(value); 39 | this._m15Rate.update(value); 40 | this.lastMark = Date.now(); 41 | } 42 | 43 | start() { 44 | clearInterval(this._timer); 45 | this._timer = setInterval(this._tick.bind(this), this._interval); 46 | this.startTime = this.lastJSON = this.lastMark = Date.now(); 47 | } 48 | 49 | end() { 50 | clearInterval(this._timer); 51 | delete this._timer; 52 | } 53 | 54 | _tick() { 55 | this._m1Rate.tick(); 56 | this._m5Rate.tick(); 57 | this._m15Rate.tick(); 58 | } 59 | 60 | reset() { 61 | this.end(); 62 | this._init(); 63 | } 64 | 65 | meanRate() { 66 | if (this._count === 0) return 0; 67 | return this._count / (Date.now() - this.startTime) * this._unit; 68 | } 69 | 70 | currentRate() { 71 | var now = Date.now(); 72 | var duration = now - this.lastJSON; 73 | var rate = duration ? this._sum / duration * this._unit : 0; 74 | this._sum = 0; 75 | this.lastJSON = now; 76 | return rate; 77 | } 78 | 79 | toJSON(options) { 80 | const rc = { 81 | count: this._count, 82 | rate: this.currentRate(), 83 | mean: this.meanRate(), 84 | m1: this._m1Rate.rate(this._unit), 85 | m5: this._m5Rate.rate(this._unit), 86 | m15: this._m15Rate.rate(this._unit), 87 | }; 88 | if (this._reset || options?.reset) this.reset(); 89 | return rc; 90 | } 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /lib/lib/uuid.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const crypto = require('crypto'); 7 | const lib = require(__dirname + '/../lib'); 8 | 9 | /** 10 | * Unique Id (UUID v4) without any special characters and in lower case 11 | * @param {string} [prefix] - prepend with prefix 12 | * @return {string} 13 | * @memberof module:lib 14 | * @method uuid 15 | */ 16 | lib.uuid = function(prefix) 17 | { 18 | return (prefix || "") + crypto.randomUUID().replace(/[-]/g, '').toLowerCase(); 19 | } 20 | 21 | /** 22 | * Generate a 22 chars slug from an UUID 23 | * @param {object} [options] 24 | * @param {string} [options.alphabet] - chars allowed in hashids, default is lib.uriSafe 25 | * @param {string} [options.prefix] - repend with prefix 26 | * @return {string} 27 | * @memberof module:lib 28 | * @method slug 29 | */ 30 | lib.slug = function(options) 31 | { 32 | var bits = "0000" + BigInt("0x" + lib.uuid()).toString(2); 33 | var bytes = []; 34 | for (let i = 0; i < bits.length; i += 6) bytes.push(bits.substr(i, 6)); 35 | const alphabet = options?.alphabet || lib.uriSafe; 36 | return (options?.prefix || "") + bytes.map((x) => alphabet[parseInt(x, 2) % alphabet.length]).join(""); 37 | } 38 | 39 | /** 40 | * Short unique id within a microsecond 41 | * @param {string} [prefix] - prepend with prefix 42 | * @param {object} [options] - same options as for {@link module:lib.getHashid} 43 | * @return {string} generated hash 44 | * @memberof module:lib 45 | * @method suuid 46 | */ 47 | lib.suuid = function(prefix, options) 48 | { 49 | var hashid = this.getHashid(options); 50 | var s = hashid.encode(lib.clock(), hashid._counter); 51 | return prefix ? prefix + s : s; 52 | } 53 | 54 | /** 55 | * Generate a SnowFlake unique id as 64-bit number 56 | * Format: time - 41 bit, node - 10 bit, counter - 12 bit 57 | * Properties can be provided: 58 | * - now - time, if not given local epoch clock is used in microseconds 59 | * - epoch - local epoch type, default is milliseconds, `m` for microseconds, `s` for seconds 60 | * - node - node id, limited to max 1024 61 | * - radix - default is 10, use any value between 2 - 36 for other numeric encoding 62 | */ 63 | lib.sfuuid = function(options) 64 | { 65 | var node = options?.node || lib.sfuuidNode; 66 | if (node === undefined) { 67 | var intf = lib.networkInterfaces()[0]; 68 | if (intf) lib.sfuuidNode = node = lib.murmurHash3(intf.mac); 69 | } 70 | var now = options?.now || lib.localEpoch(options?.epoch); 71 | var n = BigInt(now) << 22n | (BigInt(node % 1024) << 12n) | BigInt(lib.sfuuidCounter++ % 4096); 72 | return n.toString(options?.radix || 10); 73 | } 74 | 75 | lib.sfuuidCounter = 0; 76 | 77 | // Parse an id into original components: now, node, counter 78 | lib.sfuuidParse = function(id) 79 | { 80 | const _map = { now: [22n, 64n], node: [12n, 10n], counter: [0n, 12n] }; 81 | const rc = {}; 82 | try { 83 | id = rc.id = BigInt(id); 84 | for (const p in _map) { 85 | rc[p] = Number((id & (((1n << _map[p][1]) - 1n) << _map[p][0])) >> _map[p][0]); 86 | } 87 | } catch (e) {} 88 | return rc; 89 | } 90 | 91 | 92 | -------------------------------------------------------------------------------- /tools/bkjs-docker: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Nov 2021 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|docker-help) 10 | echo "" 11 | echo " docker-build-tag -t TAG [-version V] [-path .] [-root H] - build an image for the specified tag" 12 | echo " docker-run-tag -t TAG [-path .] [-force] - run a container for the specified tag" 13 | echo " docker-restart NAME - restart a container, for use with bkrsync" 14 | echo " docker-compose-get - install docker compose plugin in /usr/local/bin" 15 | echo " docker-ecr-login-get - install ECR login utility for docker in ~/bin" 16 | echo " docker-init-binfmt - initialize local docker to support multiple platforms in the default builder by using binfmt" 17 | ;; 18 | 19 | docker-build-tag) 20 | tag=$(get_arg -tag) 21 | version=$(get_arg -version) 22 | path=$(get_arg -path .) 23 | root=$(get_arg -root .) 24 | 25 | [ ! -f $path/Dockerfile.$tag ] && echo "$BKJS_CMD: no Dockerfile.$tag found in $path" && exit 1 26 | 27 | [ -f $path/path.$tag ] && path="$root$(head -1 $path/path.$tag)" 28 | [ ! -d $root ] && echo "$BKJS_CMD: invalid $tag context path: $root" && exit 1 29 | 30 | if [ ! -f $root/.dockerignore -a -f $path/dockerignore.$tag ]; then 31 | cp $path/dockerignore.$tag $root/.dockerignore 32 | dockerignore=yes 33 | fi 34 | 35 | [ -f $path/build.$tag ] && cmd=$(cat $path/build.$tag|tr '\n' ' ') 36 | [ ! -z $version ] && cmd="$cmd -t $tag:$version" 37 | 38 | cmd="docker build --rm --progress=plain -t $tag -f $path/Dockerfile.$tag $cmd $root" 39 | debug $cmd 40 | 41 | $cmd 42 | rc=$? 43 | 44 | [ "$dockerignore" = "yes" ] && rm -f $root/.dockerignore 45 | exit $rc 46 | ;; 47 | 48 | docker-run-tag) 49 | tag=$(get_arg -tag) 50 | path=$(get_arg -path .) 51 | 52 | [ -z $tag ] && echo "$BKJS_CMD: -tag must be provided" && exit 1 53 | [ -f $path/run.$tag ] && cmd=$(cat $path/run.$tag|tr '\n' ' ') 54 | 55 | if [ "$(get_flag -force)" != "" ]; then 56 | pids=$(docker ps -aq -f name=$tag) 57 | [ ! -z $pids ] && docker rm -f $pids 58 | fi 59 | 60 | cmd="docker run -d --name $tag ${cmd:-$tag} $(get_all_args "-tag -path -force")" 61 | debug $cmd 62 | 63 | $cmd 64 | exit 65 | ;; 66 | 67 | docker-restart) 68 | exec docker restart $BKJS_ARGV0 69 | ;; 70 | 71 | docker-init-binfmt) 72 | docker run --rm --privileged linuxkit/binfmt:312ed1cb899fae229b5303ac6c0510ac58f331c8 73 | exit 74 | ;; 75 | 76 | docker-compose-get) 77 | wget -L -O /usr/local/bin/docker-compose https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m) 78 | chmod 755 /usr/local/bin/docker-compose 79 | ln -s /usr/local/bin/docker-compose /usr/libexec/docker/cli-plugins/docker-compose 80 | exit 81 | ;; 82 | 83 | docker-ecr-login-get) 84 | go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@latest 85 | mv ~/go/bin/docker-credential-ecr-login $BKJS_HOME/bin 86 | echo "{ \"auths\": {}, \"credsStore\": \"ecr-login\" }" > ~/.docker/config.json 87 | exit 88 | ;; 89 | 90 | esac 91 | -------------------------------------------------------------------------------- /web/js/app-send.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * alpinejs-app client 3 | * Vlad Seryakov vseryakov@gmail.com 2018 4 | */ 5 | 6 | (() => { 7 | var app = window.app; 8 | 9 | // Send a request using app.fetch, call the callback with error and result. 10 | // - options can be a string with url or an object with options.url, options.body and options.method properties 11 | // - POST is default 12 | // 13 | app.send = function(options, callback) 14 | { 15 | if (app.isS(options)) options = { url: options }; 16 | if (!options.headers) options.headers = {}; 17 | options.headers["bk-tz"] = (new Date()).getTimezoneOffset(); 18 | for (const p in app.headers) { 19 | options.headers[p] ??= app.headers[p]; 20 | } 21 | for (const p in options.body) { 22 | if (options.body[p] === undefined) delete options.body[p]; 23 | } 24 | options.method ??= 'POST'; 25 | app.emit("send:start"); 26 | 27 | app.fetch(options, (err, data, info) => { 28 | app.emit("send:stop"); 29 | 30 | var h = info?.headers["bk-csrf"] || ""; 31 | switch (h) { 32 | case "": 33 | break; 34 | case "0": 35 | if (!app.headers) break; 36 | delete app.headers["bk-csrf"]; 37 | break; 38 | default: 39 | if (!app.headers) app.headers = {}; 40 | app.headers["bk-csrf"] = h; 41 | } 42 | 43 | if (err) { 44 | if (options.alert) { 45 | var a = app.isS(options.alert); 46 | app.emit("alert", "error", a || err, { safe: !a }); 47 | } 48 | } else { 49 | if (options.info_msg || options.success_msg) { 50 | app.emit("alert", options.info_msg ? "info" : "success", options.info_msg || options.success_msg); 51 | } 52 | } 53 | app.call(callback, err, data, info); 54 | }); 55 | } 56 | 57 | // Send file(s) as multi-part upload in the `options.files` object. Optional form inputs 58 | // can be specified in the `options.body` object. 59 | app.sendFile = function(options, callback) 60 | { 61 | var body = new FormData(); 62 | for (const p in options.files) { 63 | const file = options.files[p]; 64 | if (!file?.files?.length) continue; 65 | body.append(p, file.files[0]); 66 | } 67 | 68 | const add = (k, v) => { 69 | body.append(k, app.isF(v) ? v() : v === null || v === true ? "" : v); 70 | } 71 | 72 | const build = (key, val) => { 73 | if (val === undefined) return; 74 | if (Array.isArray(val)) { 75 | for (const i in val) build(`${key}[${app.isO(val[i]) ? i : ""}]`, val[i]); 76 | } else 77 | if (app.isObject(val)) { 78 | for (const n in val) build(`${key}[${n}]`, val[n]); 79 | } else { 80 | add(key, val); 81 | } 82 | } 83 | for (const p in options.body) { 84 | build(p, options.body[p]); 85 | } 86 | for (const p in options.json) { 87 | const blob = new Blob([JSON.stringify(options.json[p])], { type: "application/json" }); 88 | body.append(p, blob); 89 | } 90 | 91 | var req = { url: options.url, body }; 92 | for (const p in options) { 93 | req[p] ??= options[p]; 94 | } 95 | app.send(req, callback); 96 | } 97 | 98 | })(); 99 | -------------------------------------------------------------------------------- /lib/util/shell.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const { app, shell, lib, logger, ipc } = require('../modules'); 7 | 8 | /** 9 | * @module shell 10 | */ 11 | 12 | /** 13 | * Shell command interface for `bksh` 14 | * 15 | * Run `bksh -help` to see all registered shell commands. 16 | * 17 | * Special global command-line arguments: 18 | * 19 | * - `-noexit` - keep the shell running after executing the command 20 | * - `-exit` - exit with error if no shell command found 21 | * - `-exit-timeout MS` - will be set to ms to wait before exit for async actions to finish 22 | * - `-shell-delay MS` - will wait before running the command to allow initialization complete 23 | * 24 | * Shell functions must be defined in `shell.commands` object, 25 | * where `myCommand` is the command name in camel case for `-my-command` 26 | * 27 | * The function may return special values: 28 | * - `stop` - stop processing commands and create REPL 29 | * - `continue` - do not exit and continue processing other commands or end with REPL 30 | * 31 | * all other values will result in returning from the run assuming the command will decide what to do, exit or continue running, no REPL is created 32 | * 33 | * @example 34 | * const { shell } = require("backendjs"); 35 | * 36 | * shell.commands.myCommand = function(options) { 37 | * console.log("hello"); 38 | * return "continue" 39 | * } 40 | * // Calling `bksh -my-command` it will run this command. 41 | */ 42 | module.exports = Shell; 43 | 44 | function Shell(options) 45 | { 46 | require("../shell/aws"); 47 | require("../shell/db"); 48 | require("../shell/shell"); 49 | require("../shell/users"); 50 | 51 | shell.exitTimeout = lib.getArgInt("-exit-timeout", 1000); 52 | var delay = lib.getArgInt("-shell-delay"); 53 | 54 | app.runMethods("configureShell", options, (err) => { 55 | if (options.done) process.exit(); 56 | 57 | if (app.isPrimary) { 58 | ipc.initServer(); 59 | } else { 60 | ipc.initWorker(); 61 | } 62 | var cmd; 63 | 64 | for (var i = 1; i < process.argv.length; i++) { 65 | if (process.argv[i][0] != '-') continue; 66 | var name = lib.toCamel(process.argv[i].substr(1)); 67 | if (typeof shell.commands[name] != "function") continue; 68 | cmd = name; 69 | if (delay) { 70 | return setTimeout(shell.commands[name].bind(shell, options), delay); 71 | } 72 | var rc = shell.commands[name](options); 73 | logger.debug("start:", shell.name, name, rc); 74 | 75 | if (rc == "stop") break; 76 | if (rc == "continue") continue; 77 | if (lib.isArg("-noexit")) continue; 78 | return; 79 | } 80 | if (!cmd && lib.isArg("-exit")) { 81 | return shell.exit("no shell command found"); 82 | } 83 | if (app.isPrimary) { 84 | const repl = app.createRepl({ file: app.repl.file, size: app.repl.size }); 85 | repl.on('exit', () => { 86 | app.runMethods("shutdownShell", { sync: 1 }, () => { 87 | setTimeout(() => { process.exit() }, shell.exitTimeout); 88 | }); 89 | }); 90 | } 91 | }); 92 | } 93 | 94 | -------------------------------------------------------------------------------- /lib/util/redis.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2025 4 | */ 5 | 6 | const net = require('net'); 7 | const logger = require(__dirname + '/../logger'); 8 | const lib = require(__dirname + '/../lib'); 9 | 10 | const mod = { 11 | name: "redis", 12 | args: [ 13 | { name: "host", descr: "Default Redis host" }, 14 | { name: "port", type: "int", descr: "Default Redis port" }, 15 | ], 16 | port: 6379, 17 | Client, 18 | }; 19 | module.exports = mod; 20 | 21 | var Parser; 22 | 23 | class Client { 24 | callbacks = []; 25 | 26 | constructor(options) 27 | { 28 | this.options = Object.assign({ port: mod.port, host: mod.host }, options); 29 | 30 | Parser = Parser || require('redis-parser'); 31 | 32 | this.parser = new Parser({ 33 | returnBuffers: this.options.returnBuffers, 34 | returnReply: data => { 35 | const cb = this.callbacks[0]; 36 | if (typeof cb != "function" || cb(null, data) !== -1999) this.callbacks.shift(); 37 | }, 38 | returnError: err => { 39 | const cb = this.callbacks[0]; 40 | if (typeof cb != "function" || cb(err) !== -1999) this.callbacks.shift(); 41 | } 42 | }); 43 | 44 | this.connect(); 45 | } 46 | 47 | connect() 48 | { 49 | this.connecting = true; 50 | this.callbacks = []; 51 | 52 | this.socket = net.createConnection(this.options, () => { 53 | this.ready = true; 54 | delete this.connecting; 55 | }); 56 | 57 | this.socket.on('data', (data) => { 58 | this.parser.execute(data); 59 | }).on('error', (err) => { 60 | const cb = this.callbacks.shift(); 61 | if (typeof cb == "function") cb(err); 62 | }).on('close', () => { 63 | delete this.ready; 64 | delete this.socket; 65 | }); 66 | } 67 | 68 | call(...args) 69 | { 70 | if (!this.ready) return; 71 | this.callbacks.push(args.at(-1)); 72 | this.socket.write(this.encode(args)); 73 | } 74 | 75 | multi(...args) 76 | { 77 | if (!this.ready) return; 78 | var replies = [], error, callback = args.at(-1); 79 | var cmds = args.filter(lib.isArray); 80 | this.callbacks.push((err, reply) => { 81 | error = error || err; 82 | replies.push(err || reply); 83 | if (replies.length != cmds.length) return -1999; 84 | if (typeof callback == "function") callback(error, replies); 85 | }); 86 | this.socket.write(cmds.map(this.encode).join("") + "\r\n"); 87 | } 88 | 89 | destroy() 90 | { 91 | this.destroyed = true; 92 | if (this.socket) this.socket.destroy(); 93 | } 94 | 95 | encode(cmd) 96 | { 97 | if (!lib.isArray(cmd)) return ""; 98 | var rc = `*${cmd.length}\r\n`; 99 | for (let arg of cmd) { 100 | if (typeof arg == "function") continue; 101 | if (typeof arg != "string") arg = String(arg); 102 | rc += `$${Buffer.byteLength(arg)}\r\n${arg}\r\n`; 103 | } 104 | return rc; 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /lib/lib/lru.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const lib = require(__dirname + "/../lib"); 7 | 8 | lib.LRUCache = class LRUCache { 9 | 10 | /** 11 | * Simple LRU cache in memory, supports get,put,del operations only, TTL can be specified in milliseconds as future time 12 | * @param {number} [max] - max number of items in the cache 13 | * @class LRUCache 14 | * @example 15 | * const { lib } = require("backendjs") 16 | * 17 | * const lru = new lib.LRUCache(1000) 18 | * 19 | */ 20 | constructor(max) { 21 | this.max = max || 10000; 22 | this.map = new Map(); 23 | this.head = {}; 24 | this.tail = this.head.next = { prev: this.head }; 25 | } 26 | 27 | /** 28 | * Return an item by key 29 | * @param {string} key 30 | * @return {any|undefined} an item if found 31 | * @memberof LRUCache 32 | * @method get 33 | */ 34 | get(key) { 35 | const node = this.map.get(key); 36 | if (node === undefined) return; 37 | if (node.ttl && node.ttl < Date.now()) { 38 | this.del(key); 39 | return; 40 | } 41 | node.prev.next = node.next; 42 | node.next.prev = node.prev; 43 | this.tail.prev.next = node; 44 | node.prev = this.tail.prev; 45 | node.next = this.tail; 46 | this.tail.prev = node; 47 | return node.value; 48 | } 49 | 50 | /** 51 | * Put an item into cache, if total number of items exceed the max then the oldest item is removed 52 | * @param {string} key 53 | * @param {any} value 54 | * @param {number} [ttl] in milliseconds 55 | * @memberof LRUCache 56 | * @method put 57 | */ 58 | put(key, value, ttl) { 59 | if (this.get(key) !== undefined) { 60 | this.tail.prev.value = value; 61 | return true; 62 | } 63 | if (this.map.size === this.max) { 64 | this.map.delete(this.head.next.key); 65 | this.head.next = this.head.next.next; 66 | this.head.next.prev = this.head; 67 | } 68 | const node = { value, ttl }; 69 | this.map.set(key, node); 70 | this.tail.prev.next = node; 71 | node.prev = this.tail.prev; 72 | node.next = this.tail; 73 | this.tail.prev = node; 74 | } 75 | 76 | /** 77 | * Remove a item from cache 78 | * @param {string} key 79 | * @return {boolean} true if removed 80 | * @memberof LRUCache 81 | * @method del 82 | */ 83 | del(key) { 84 | const node = this.map.get(key); 85 | if (node === undefined) return false; 86 | node.prev.next = node.next; 87 | node.next.prev = node.prev; 88 | if (node == this.head) this.head = node.next; 89 | if (node == this.tail) this.tail = node.prev; 90 | this.map.delete(key); 91 | return true; 92 | } 93 | 94 | /** 95 | * @memberof LRUCache 96 | * @method clean 97 | */ 98 | clean() { 99 | const now = Date.now(), s = this.map.size; 100 | for (const [key, val] of this.map) { 101 | if (val.ttl && val.ttl < now) this.del(key); 102 | } 103 | return s - this.map.size; 104 | } 105 | } 106 | 107 | -------------------------------------------------------------------------------- /examples/config/web/config.js: -------------------------------------------------------------------------------- 1 | 2 | app.components.config = class extends app.AlpineComponent { 3 | 4 | rows = []; 5 | types = []; 6 | query = ""; 7 | 8 | onCreate() { 9 | this.show(); 10 | 11 | app.$("input", this.$el)?.focus(); 12 | } 13 | 14 | refresh() { 15 | this.show(); 16 | } 17 | 18 | close() { 19 | app.render("index") 20 | } 21 | 22 | filter() { 23 | var list = this.rows, q = this.query; 24 | if (q) list = this.rows.filter((x) => (x.type.includes(q) || x.name.includes(q) || x.value.includes(q))); 25 | list.sort((a,b) => (a.type < b.type ? -1 : a.type > b.type ? 1 : app.util.toNumber(a.sort) - app.util.toNumber(b.sort) || (a.ctime - b.ctime))); 26 | 27 | var types = {}; 28 | list.forEach((x) => { 29 | x.icon = x.status == "ok" ? "fa-check" : "fa-ban"; 30 | x.text = x.name + " = " + x.value; 31 | if (!types[x.type]) types[x.type] = []; 32 | types[x.type].push(x); 33 | }); 34 | return Object.keys(types).map(x => ({ type: x, rows: types[x], q })); 35 | } 36 | 37 | show() { 38 | app.fetch('/config/list', (err, rc) => { 39 | if (err) return app.emit("alert", "error", err); 40 | this.rows = app.util.isArray(rc.data, []); 41 | }); 42 | } 43 | 44 | edit(data) { 45 | var popup; 46 | popup = bootpopup({ 47 | title: "Config Details", 48 | horizontal: 1, 49 | backdrop: false, 50 | keyboard: false, 51 | alert: 1, 52 | empty: 1, 53 | size: "xlarge", 54 | size_label: "col-sm-3", 55 | size_input: "col-sm-9", 56 | class_content: "modal-content border-3 border-blue bg-light min-vh-70", 57 | content: [ 58 | { input: { name: "type", label: "Type:", value: data.type } }, 59 | { input: { name: "name", label: "Name:", value: data.name, } }, 60 | { textarea: { name: "value", class: "form-control", label: "Value:", rows: 5, value: data.value } }, 61 | ], 62 | buttons: ["cancel", "Save", data.name && "Copy", data.name && "Delete"], 63 | 64 | Save: (d) => { 65 | if (!d.type || !d.name || !d.value) return popup.showAlert("Type, name and value are required"); 66 | d.ctime = data.ctime; 67 | app.fetch({ url: `/config/${d.ctime ? "update" : "put"}`, body: d, post: 1 }, (err) => { 68 | if (err) return popup.showAlert(err); 69 | this.show(); 70 | popup.close(); 71 | }); 72 | return null; 73 | }, 74 | 75 | Copy: (d) => { 76 | app.fetch({ url: '/config/put', body: d, post: 1 }, (err) => { 77 | if (err) return popup.showAlert(err); 78 | this.show(); 79 | popup.close(); 80 | }); 81 | return null; 82 | }, 83 | 84 | Delete: (d) => { 85 | app.ui.showConfirm.call(this, "Delete this parameter?", () => { 86 | app.fetch({ url: '/config/del', body: { ctime: data.ctime, name: data.name }, post: 1 }, (err) => { 87 | if (err) return popup.showAlert(err); 88 | this.show(); 89 | popup.close(); 90 | }); 91 | }); 92 | return null; 93 | }, 94 | }); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/acl.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { describe, it } = require('node:test'); 3 | const assert = require('node:assert/strict'); 4 | const { api, app, logger, lib } = require("../"); 5 | 6 | describe("Auth tests", () => { 7 | 8 | var argv = [ 9 | "-api-acl-allow-admin", "auth, admin, manager, -userallow, -manageronly", 10 | "-api-acl-allow-manager", "manager, user, -useronly", 11 | "-api-acl-allow-user", "user, userallow", 12 | "-api-acl-authenticated", "auth", 13 | 14 | "-api-acl-deny-manager", "useronly", 15 | "-api-acl-deny-user", "userdeny", 16 | 17 | "-api-acl-add-auth", "^/auth", 18 | "-api-acl-add-admin", "^/admin", 19 | "-api-acl-add-manager", "^/manager", 20 | "-api-acl-add-user", "^/user", 21 | "-api-acl-add-userallow", "^/allow", 22 | "-api-acl-add-userdeny", "^/userdeny", 23 | "-api-acl-add-useronly", "^/useronly", 24 | "-api-acl-add-manageronly", "^/manageronly", 25 | ]; 26 | 27 | var checks = [ 28 | { status: 403, path: "/system" }, 29 | { status: 403, path: "/system", roles: "admin" }, 30 | 31 | { status: 200, path: "/auth" }, 32 | { status: 200, path: "/auth", roles: "admin" }, 33 | { status: 200, path: "/auth", roles: "user" }, 34 | 35 | { status: 403, path: "/admin" }, 36 | { status: 403, path: "/admin", roles: "user" }, 37 | { status: 403, path: "/admin", roles: "manager" }, 38 | { status: 200, path: "/admin", roles: "admin" }, 39 | 40 | { status: 403, path: "/allow" }, 41 | { status: 200, path: "/allow", roles: "user" }, 42 | { status: 403, path: "/allow", roles: "manager" }, 43 | { status: 403, path: "/allow", roles: "admin" }, 44 | 45 | { status: 200, path: "/user", roles: "user" }, 46 | { status: 403, path: "/user", roles: "admin" }, 47 | { status: 200, path: "/user", roles: "manager" }, 48 | 49 | { status: 200, path: "/useronly", roles: "user", code: "DENY" }, 50 | { status: 403, path: "/useronly", roles: "manager", code: "DENY" }, 51 | 52 | { status: 200, path: "/userdeny", roles: "manager" }, 53 | { status: 403, path: "/userdeny", roles: "user", code: "DENY" }, 54 | 55 | { status: 403, path: "/manager" }, 56 | { status: 403, path: "/manager", roles: "user" }, 57 | { status: 200, path: "/manager", roles: "manager" }, 58 | { status: 200, path: "/manager", roles: "admin" }, 59 | 60 | { status: 200, path: "/manageronly", roles: "manager" }, 61 | { status: 403, path: "/manageronly", roles: "user" }, 62 | { status: 403, path: "/manageronly", roles: "admin" }, 63 | 64 | ]; 65 | 66 | api.acl.reset(); 67 | app.parseArgs(argv); 68 | var req = { user: {}, options: {} }; 69 | 70 | logger.setLevel(process.env.BKJS_TEST_LOG) 71 | 72 | it("checks all acls", (t, callback) => { 73 | 74 | lib.forEachSeries(checks, (check, next) => { 75 | req.user.id = check.roles || "anon"; 76 | req.user.roles = lib.strSplit(check.roles); 77 | req.options.path = check.path; 78 | logger.debug("checking:", check); 79 | api.access.authorize(req, (err) => { 80 | logger.debug("checked:", err); 81 | assert.ok((err?.status || 200) === check.status, lib.objDescr({ check, err })); 82 | if (err && check.code !== undefined) { 83 | assert.ok((err.code || "") === check.code, lib.objDescr({ check, err })); 84 | } 85 | next(); 86 | }); 87 | }, callback); 88 | }); 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /lib/aws/sqs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | const aws = require(__dirname + '/../aws'); 8 | 9 | /** 10 | * AWS SQS API request 11 | * @memberOf module:aws 12 | */ 13 | aws.querySQS = function(action, obj, options, callback) 14 | { 15 | this.queryEndpoint("sqs", '2012-11-05', action, obj, options, callback); 16 | } 17 | 18 | /** 19 | * Receive message(s) from the SQS queue, the callback will receive a list with messages if no error. 20 | * The following options can be specified: 21 | * - count - how many messages to receive 22 | * - timeout - how long to wait, in milliseconds, this is for Long Poll 23 | * - visibilityTimeout - the duration (in milliseconds) that the received messages are hidden from subsequent retrieve requests 24 | * - attempt - request attempt id for FIFO queues 25 | * after being retrieved by a ReceiveMessage request. 26 | * @memberOf module:aws 27 | */ 28 | aws.sqsReceiveMessage = function(url, options, callback) 29 | { 30 | if (typeof options == "function") callback = options, options = null; 31 | 32 | var params = { QueueUrl: url }; 33 | if (options) { 34 | if (options.count) params.MaxNumberOfMessages = options.count; 35 | if (options.visibilityTimeout > 999) params.VisibilityTimeout = Math.round(options.visibilityTimeout/1000); 36 | if (options.timeout > 999) params.WaitTimeSeconds = Math.round(options.timeout/1000); 37 | if (options.attempt) params.ReceiveRequestAttemptId = options.attempt; 38 | } 39 | this.querySQS("ReceiveMessage", params, options, function(err, obj) { 40 | var rows = []; 41 | if (!err) rows = lib.objGet(obj, "ReceiveMessageResponse.ReceiveMessageResult.Message", { list: 1 }); 42 | if (typeof callback == "function") callback(err, rows); 43 | }); 44 | } 45 | 46 | /** 47 | * Send a message to the SQS queue. 48 | * The options can specify the following: 49 | * - delay - how long to delay this message in milliseconds 50 | * - group - a group id for FIFO queues 51 | * - unique - deduplication id for FIFO queues 52 | * - attrs - an object with additional message attributes to send, use only string, numbers or binary values, 53 | * all other types will be converted into strings 54 | * @memberOf module:aws 55 | */ 56 | aws.sqsSendMessage = function(url, body, options, callback) 57 | { 58 | if (typeof options == "function") callback = options, options = null; 59 | 60 | var params = { QueueUrl: url, MessageBody: body }; 61 | if (options) { 62 | if (options.delay > 999) params.DelaySeconds = Math.round(options.delay/1000); 63 | if (options.group) params.MessageGroupId = options.group; 64 | if (options.unique) params.MessageDeduplicationId = options.unique; 65 | if (options.attrs) { 66 | var n = 1; 67 | for (var p in options.attrs) { 68 | var type = typeof options.attrs[p] == "number" ? "Number" : typeof options.attrs[p] == "string" ? "String" : "Binary"; 69 | params["MessageAttribute." + n + ".Name"] = p; 70 | params["MessageAttribute." + n + ".Value." + type + "Value"] = options.attrs[p]; 71 | params["MessageAttribute." + n + ".Value.DataType"] = type; 72 | n++; 73 | } 74 | } 75 | } 76 | this.querySQS("SendMessage", params, options, function(err, obj) { 77 | var rows = []; 78 | if (!err) rows = lib.objGet(obj, "ReceiveMessageResponse.ReceiveMessageResult.Message", { list: 1 }); 79 | if (typeof callback == "function") callback(err, rows); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /tests/jwt.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { describe, it } = require('node:test'); 3 | const assert = require('node:assert/strict'); 4 | const { lib } = require("../"); 5 | 6 | const JWT = lib.JWT; 7 | 8 | const secret = 'a-secret' 9 | 10 | describe("JWT tests", async () => { 11 | 12 | it('Issuer (correct - string)', async () => { 13 | const tok = 14 | 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MzMwNDY0MDAsImlzcyI6ImNvcnJlY3QtaXNzdWVyIn0.gF8S6M2QcfTTscgxeyihNk28JAOa8mfL1bXPb3_E3rk' 15 | 16 | const rc = await JWT.verify(tok, secret, { alg: "HS256", iss: 'correct-issuer' }) 17 | 18 | assert.partialDeepStrictEqual(rc, { 19 | payload: { 20 | iss: 'correct-issuer' 21 | } 22 | }); 23 | }) 24 | 25 | it('Token Expired', async () => { 26 | const tok = 27 | 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2MzMwNDYxMDAsImV4cCI6MTYzMzA0NjQwMH0.H-OI1TWAbmK8RonvcpPaQcNvOKS9sxinEOsgKwjoiVo' 28 | const { err } = await JWT.verify(tok, secret) 29 | assert.match(err?.message, /expired/); 30 | }) 31 | 32 | it('HS512 sign & verify & decode', async () => { 33 | var payload = { message: 'hello world' } 34 | const tok = await JWT.sign(payload, secret, "HS512") 35 | const expected = 36 | 'eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJtZXNzYWdlIjoiaGVsbG8gd29ybGQifQ.RqVLgExB_GXF1-9T-k4V4HjFmiuQKTEjVSiZd-YL0WERIlywZ7PfzAuTZSJU4gg8cscGamQa030cieEWrYcywg' 37 | 38 | assert.strictEqual(tok.token, expected) 39 | 40 | const rc = await JWT.verify(tok.token, secret) 41 | 42 | assert.partialDeepStrictEqual(rc, { 43 | header: { 44 | alg: 'HS512', 45 | typ: 'JWT', 46 | }, 47 | payload: { 48 | message: 'hello world', 49 | }, 50 | }) 51 | }) 52 | 53 | it('EdDSA sign & verify w/ CryptoKey', async () => { 54 | const alg = 'EdDSA' 55 | const payload = { message: 'hello world' } 56 | 57 | const keyPair = await crypto.subtle.generateKey({ name: 'Ed25519', namedCurve: 'Ed25519' }, true, ['sign', 'verify']) 58 | 59 | const tok = await JWT.sign(payload, keyPair.privateKey, alg) 60 | 61 | const rc1 = await JWT.verify(tok.token, keyPair.privateKey, alg) 62 | assert.partialDeepStrictEqual(rc1, { payload }) 63 | 64 | const rc2 = await JWT.verify(tok.token, keyPair.publicKey, alg) 65 | assert.partialDeepStrictEqual(rc2, { payload }) 66 | }) 67 | 68 | it(`PS384 sign & verify`, async () => { 69 | const alg = "PS384" 70 | const payload = { message: 'hello world' } 71 | 72 | const keyPair = await crypto.subtle.generateKey({ 73 | hash: JWT.algorithms[alg].hash.name, 74 | modulusLength: 2048, 75 | publicExponent: new Uint8Array([1, 0, 1]), 76 | name: 'RSA-PSS', 77 | }, true, ['sign', 'verify']); 78 | 79 | var exported = await crypto.subtle.exportKey('pkcs8', keyPair.privateKey) 80 | const pemPrivateKey = `-----BEGIN PRIVATE KEY-----\n${Buffer.from(exported).toString("base64")}\n-----END PRIVATE KEY-----` 81 | 82 | exported = await crypto.subtle.exportKey('spki', keyPair.publicKey) 83 | const pemPublicKey = `-----BEGIN PUBLIC KEY-----\n${Buffer.from(exported).toString("base64")}\n-----END PUBLIC KEY-----` 84 | 85 | const jwkPublicKey = await crypto.subtle.exportKey('jwk', keyPair.publicKey) 86 | 87 | const tok = await JWT.sign(payload, pemPrivateKey, alg) 88 | 89 | const rc1 = await JWT.verify(tok.token, pemPublicKey, alg) 90 | assert.partialDeepStrictEqual(rc1, { payload }) 91 | 92 | const rc2 = await JWT.verify(tok.token, pemPrivateKey, alg) 93 | assert.partialDeepStrictEqual(rc2, { payload }) 94 | 95 | const rc3 = await JWT.verify(tok.token, jwkPublicKey, alg) 96 | assert.partialDeepStrictEqual(rc3, { payload }) 97 | 98 | }) 99 | 100 | }) 101 | -------------------------------------------------------------------------------- /lib/push/webpush.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const logger = require(__dirname + '/../logger'); 7 | const lib = require(__dirname + '/../lib'); 8 | const api = require(__dirname + '/../api'); 9 | 10 | const mod = { 11 | name: "webpush", 12 | 13 | create: function(options) { 14 | if (!webpush) webpush = require("web-push"); 15 | if (["webpush", "wp"].includes(options?.type)) return new WebpushClient(options); 16 | }, 17 | 18 | configure: function(options) { 19 | api.hooks.add('access', '', '/js/webpush.js', function(req, status, cb) { 20 | req.res.header("Service-Worker-Allowed", "/"); 21 | cb(); 22 | }); 23 | }, 24 | 25 | properties: ["actions", "badge", "body", "dir", "icon", "image", "lang", "renotify", "requireInteraction", "silent", "tag", "timestamp", "vibrate"], 26 | }; 27 | 28 | module.exports = mod; 29 | 30 | var webpush; 31 | 32 | /** 33 | * Send a Web push notification using the `web-push` npm module, referer to it for details how to generate VAPID credentials to 34 | * configure this module with 3 required parameters: 35 | * 36 | * - `key` - VAPID private key 37 | * - `pubkey` - VAPID public key 38 | * - `email` - an admin email for the VAPID subject 39 | * 40 | * The device token must be generated in the browser after successful subscription: 41 | * 42 | * navigator.serviceWorker.register("/js/webpush.js", { scope: "/" }).then(function(registration) { 43 | * registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: vapidKeyPublic }).then(function(subscription) { 44 | * bkjs.send({ url: '/user/update', data: { pushkey: "wp://" + window.btoa(JSON.stringify(subscription)) }, type: "POST" }); 45 | * }).catch((err) => {}) 46 | * }); 47 | */ 48 | 49 | class WebpushClient { 50 | 51 | constructor(options) { 52 | this.type = options.type; 53 | this.key = options.key; 54 | this.pubkey = options.pubkey; 55 | this.subject = `mailto:${options.email}`; 56 | this.app = options.app; 57 | this.queue = 0; 58 | } 59 | 60 | close(callback) { 61 | lib.tryCall(callback); 62 | } 63 | 64 | send(dev, options, callback) { 65 | if (!dev?.id) return lib.tryCall(callback, lib.newError("invalid device:" + dev.id)); 66 | 67 | var to = lib.jsonParse(Buffer.from(dev.id, "base64").toString()); 68 | if (!to) return lib.tryCall(callback, lib.newError("invalid device id:" + dev.id)); 69 | 70 | var msg = { title: options.title, body: options.msg, data: {} }; 71 | for (const p of mod.properties) { 72 | if (typeof options[p] != "undefined") msg[p] = options[p]; 73 | } 74 | 75 | if (options.id) msg.data.id = String(options.id); 76 | if (options.url) msg.data.url = String(options.url); 77 | if (options.type) msg.data.type = String(options.type); 78 | if (options.user_id) msg.data.user_id = options.user_id; 79 | for (const p in options.payload) msg.data[p] = options.payload[p]; 80 | 81 | const opts = { 82 | vapidDetails: { 83 | subject: this.subject, 84 | publicKey: this.pubkey, 85 | privateKey: this.key, 86 | } 87 | } 88 | this.queue++; 89 | webpush.sendNotification(to, lib.stringify(msg), opts). 90 | then(() => { 91 | this.queue--; 92 | logger.debug("send:", mod.name, dev, msg); 93 | lib.tryCall(callback); 94 | }). 95 | catch((err) => { 96 | this.queue--; 97 | logger.error("send:", mod.name, err, dev, msg); 98 | lib.tryCall(callback, err); 99 | }); 100 | return true; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/web/scripts/search.js: -------------------------------------------------------------------------------- 1 | 2 | var searchAttr = 'data-search-mode'; 3 | function contains(a,m){ 4 | return (a.textContent || a.innerText || "").toUpperCase().indexOf(m) !== -1; 5 | }; 6 | 7 | //on search 8 | document.getElementById("nav-search").addEventListener("keyup", (event) => { 9 | var search = this.value.toUpperCase(); 10 | 11 | if (!search) { 12 | //no search, show all results 13 | document.documentElement.removeAttribute(searchAttr); 14 | 15 | document.querySelectorAll("nav > ul > li:not(.level-hide)").forEach(function(elem) { 16 | elem.style.display = "block"; 17 | }); 18 | 19 | if (typeof hideAllButCurrent === "function"){ 20 | //let's do what ever collapse wants to do 21 | hideAllButCurrent(); 22 | } else { 23 | //menu by default should be opened 24 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 25 | elem.style.display = "block"; 26 | }); 27 | } 28 | } else { 29 | //we are searching 30 | document.documentElement.setAttribute(searchAttr, ''); 31 | 32 | //show all parents 33 | document.querySelectorAll("nav > ul > li").forEach(function(elem) { 34 | elem.style.display = "block"; 35 | }); 36 | document.querySelectorAll("nav > ul").forEach(function(elem) { 37 | elem.style.display = "block"; 38 | }); 39 | //hide all results 40 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 41 | elem.style.display = "none"; 42 | }); 43 | //show results matching filter 44 | document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { 45 | if (!contains(elem.parentNode, search)) { 46 | return; 47 | } 48 | elem.parentNode.style.display = "block"; 49 | }); 50 | //hide parents without children 51 | document.querySelectorAll("nav > ul > li").forEach(function(parent) { 52 | var countSearchA = 0; 53 | parent.querySelectorAll("a").forEach(function(elem) { 54 | if (contains(elem, search)) { 55 | countSearchA++; 56 | } 57 | }); 58 | 59 | var countUl = 0; 60 | var countUlVisible = 0; 61 | parent.querySelectorAll("ul").forEach(function(ulP) { 62 | // count all elements that match the search 63 | if (contains(ulP, search)) { 64 | countUl++; 65 | } 66 | 67 | // count all visible elements 68 | var children = ulP.children 69 | for (i=0; i ul.collapse_top").forEach(function(parent) { 86 | var countVisible = 0; 87 | parent.querySelectorAll("li").forEach(function(elem) { 88 | if (elem.style.display !== "none") { 89 | countVisible++; 90 | } 91 | }); 92 | 93 | if (countVisible == 0) { 94 | //has no child at all and does not contain text 95 | parent.style.display = "none"; 96 | } 97 | }); 98 | } 99 | }); -------------------------------------------------------------------------------- /lib/metrics/Trace.js: -------------------------------------------------------------------------------- 1 | 2 | const lib = require("../lib"); 3 | const logger = require("../logger"); 4 | 5 | module.exports = class Trace { 6 | 7 | static #sock; 8 | 9 | /** 10 | * AWS X-Ray trace support 11 | * 12 | * Only supports local daemon UDP port 2000, to test locally 13 | * 14 | * socat -U -v PIPE udp-recv:2000 15 | * 16 | * @example 17 | * 18 | * var trace = new metrics.Trace({ _host: "127.0.0.1", annotations: { tag: app.onstance.tag, role: app.role } }); 19 | * var sub1 = trace.start("subsegment1"); 20 | * sub1.stop(); 21 | * var sub2 = trace.start("subsegment2"); 22 | * trace.stop(req); 23 | * trace.send(); 24 | * trace.destroy(); 25 | * @param {object} [options] 26 | * @param {Trqce} [parent] 27 | * @class Trace 28 | */ 29 | 30 | constructor(options, parent) 31 | { 32 | if (parent instanceof Trace) { 33 | this._parent = parent; 34 | } else { 35 | this.trace_id = `1-${Math.round(new Date().getTime() / 1000).toString(16)}-${lib.randomBytes(12)}`; 36 | } 37 | this.id = lib.randomBytes(8); 38 | 39 | this._start = Date.now(); 40 | this.start_time = this._start / 1000; 41 | 42 | if (typeof options == "string") { 43 | this.name = options; 44 | } else { 45 | for (const p in options) { 46 | if (this[p] === undefined) this[p] = options[p]; 47 | } 48 | } 49 | if (!this.name) this.name = process.title.split(/[^a-z0-9_-]/i)[0]; 50 | } 51 | 52 | /** 53 | * Closes a segment or subsegment, for segments it sends it right away 54 | * @param {IncomingRequest} [req] 55 | * @memberOf Trace 56 | * @method stop 57 | */ 58 | stop(req) 59 | { 60 | if (!this.end_time) { 61 | this._end = Date.now(); 62 | this.end_time = this._end / 1000; 63 | } 64 | 65 | if (req?.res?.statusCode) { 66 | this.http = { 67 | request: { 68 | method: req.method || "GET", 69 | url: `http${req.options.secure}://${req.options.host}${req.url}`, 70 | }, 71 | response: { 72 | status: req.res.statusCode 73 | } 74 | } 75 | } 76 | for (const i in this.subsegments) this.subsegments[i].stop(); 77 | } 78 | 79 | /** 80 | * destroy all traces and subsegments 81 | * @method destroy 82 | * @memberOf Trace 83 | */ 84 | destroy() 85 | { 86 | for (const i in this.subsegments) { 87 | this.subsegments[i].destroy(); 88 | } 89 | for (const p in this) { 90 | if (typeof this[p] == "object") delete this[p]; 91 | } 92 | } 93 | 94 | /** 95 | * @method toString 96 | * @memberOf Trace 97 | */ 98 | toString(msg) 99 | { 100 | return lib.stringify(msg || this, (key, val) => (key[0] == "_" ? undefined : val)) 101 | } 102 | 103 | 104 | /** 105 | * Sends a segment to local daemon 106 | * @memberOf Trace 107 | * @method send 108 | */ 109 | send(msg) 110 | { 111 | if (!Trace.#sock) { 112 | Trace.#sock = require("node:dgram").createSocket('udp4').unref(); 113 | } 114 | 115 | var json = this.toString(msg); 116 | 117 | Trace.#sock.send(`{"format":"json","version":1}\n${json}`, this._port || 2000, this._host, (err) => { 118 | logger.logger(err ? "error": "debug", "trace", "send:", err, json); 119 | }); 120 | } 121 | 122 | /** 123 | * Starts a new subsegment 124 | * @param {object} [options] 125 | * @memberOf Trace 126 | * @method start 127 | */ 128 | start(options) 129 | { 130 | var sub = new Trace(options, this); 131 | if (!this.subsegments) this.subsegments = []; 132 | this.subsegments.push(sub); 133 | return sub; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /docs/src/start.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Overview 4 | 5 | This tutorial will show how to set up a basic backendjs server that displays "Hello World!" in your browser. 6 | 7 | ## Installing backendjs 8 | 9 | Create a new directory myproject, and from there run: 10 | 11 | ``` 12 | cd myproject 13 | npm install vseryakov/backendjs --save 14 | ``` 15 | 16 | this will install the latest version of backendjs as a dependency in your package.json. 17 | 18 | ## Creating a Web Server 19 | 20 | A very basic backendjs Web server looks like the following: 21 | 22 | ```js 23 | const { app, api } = require('backendjs'); 24 | app.start({ api: 1 }); 25 | console.log('Server running on http://%s:%s', api.bind, api.port); 26 | ``` 27 | 28 | First, you require backendjs. Then you start the server with default settings and log that it's running, the **api** property 29 | tells to start Express web server, i.e. the api mode. There are more server modes available. 30 | 31 | Run `node` and then paste the three lines above into it. 32 | 33 | Now visit _http://localhost:8000_ in your browser, you'll see the text 'Hello, World!'. 34 | 35 | This is default empty index.html bundled with the server. 36 | 37 | ## Creating a module 38 | 39 | Now let's create a simple module that will increase a counter every time you refresh the page and persist it in the Sqlite database. 40 | 41 | Save the lines below as **index.js** 42 | 43 | ```js 44 | const { app, api, db } = require('backendjs'); 45 | 46 | const counter = { 47 | name: "counter", 48 | 49 | tables: { 50 | counter: { 51 | id: { type: "int", primary: 1 }, 52 | value: { type: "counter" }, 53 | mtime: { type: "now" }, 54 | } 55 | }, 56 | 57 | configureWeb(options, callback) 58 | { 59 | api.app.get("/counter", (req, res) => { 60 | 61 | db.incr("counter", { id: 1, value: 1 }, { returning: "*", first: 1 }, (err, row) => { 62 | api.sendJSON(req, err, row); 63 | }); 64 | }); 65 | callback(); 66 | } 67 | 68 | } 69 | app.addModule(counter); 70 | 71 | app.start({ api: 1 }); 72 | ``` 73 | 74 | Then save the following lines as **bkjs.conf** 75 | 76 | ``` 77 | api-acl-add-public=^/counter 78 | db-sqlite-pool=counter 79 | db-pool=sqlite 80 | ``` 81 | 82 | Now run the command 83 | 84 | ``` 85 | node index.js -db-create-tables 86 | ``` 87 | 88 | Go to _http://localhost:8000/counter_ in your browser, you'll see the current counter value, 89 | refresh it and see the counter value incrementing with mtime timestamp in milliseconds. 90 | 91 | Explanations about this example: 92 | 93 | - the __counter__ module is just an object with a name, here we created it inside the same index.js but 94 | modules usually placed in its own separate files 95 | - the __tables__ object describes SQL table "counter" with 3 columns 96 | - the __configureWeb__ method is called by the server on start, this method is reserved for adding your Express routes, 97 | it is called only in the "api" mode where __api.app__ is the Express application. 98 | - we just created a single GET route __/counter__ using Express middlware syntax, inside we directly increment 99 | the counter in the record with id=1, it is created automatically on first call 100 | - then return the whole record back as JSON, it is ok to do it for such a silly example. 101 | - the __bkjs.conf__ file is created for convenience, we could pass all params via the command-line 102 | - the config defines Sqlite database pool with file named "counter" and adds our __/counter__ endpoint to the 103 | public access list, by default all endpoints require some kind of access permissions 104 | - and lastly we pass __-db-create-tables__ to the node to initialize the database, this is usually need only once or every time 105 | the schema changes, so next time it is fine to run the demo as `node index.js` 106 | 107 | ## Next steps 108 | 109 | **backendjs** has many, many other capabilities, please explore the documentation and 110 | examples on https://github.com/vseryakov/backendjs/examples 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /tools/bkjs-ec2-cwagent: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | CWAGENT=amazon-cloudwatch-agent 8 | CWAROOT=/opt/aws/$CWAGENT 9 | BKJS_USER=$(get_arg -user ${BKJS_USER:-$(whoami)}) 10 | 11 | case "$BKJS_CMD" in 12 | 13 | help|ec2-help) 14 | echo "" 15 | echo " ec2-cwagent-get - install AWS Cloudwatch agent" 16 | echo " ec2-cwagent-check-config [-root D] - check AWS Cloudwatch agent config for changes, exists with code 2 if changed" 17 | echo " ec2-cwagent-start [-root D] - start AWS Cloudwatch agent" 18 | echo " ec2-cwagent-init-monit [-root D] - setup agent to be run on start and to be monitored" 19 | ;; 20 | 21 | ec2-cwagent-get) 22 | root=$(get_arg -root $CWAROOT) 23 | if [ ! -d $root ]; then 24 | case "$OS_NAME" in 25 | *Alpine*) 26 | mkdir -p $root/bin $root/etc/$CWAGENT.d $root/logs $root/var 27 | (cd $root/bin && 28 | curl -OL https://amazoncloudwatch-agent.s3.amazonaws.com/nightly-build/latest/linux_$OS_ARCH/amazon-cloudwatch-agent && 29 | curl -OL https://amazoncloudwatch-agent.s3.amazonaws.com/nightly-build/latest/linux_$OS_ARCH/config-translator && 30 | chmod 755 $root/bin/*) 31 | ;; 32 | 33 | *Amazon*) 34 | curl -OL https://s3.amazonaws.com/amazoncloudwatch-agent/amazon_linux/$OS_ARCH/latest/amazon-cloudwatch-agent.rpm 35 | rpm -i amazon-cloudwatch-agent.rpm 36 | rm amazon-cloudwatch-agent.rpm 37 | ;; 38 | esac 39 | fi 40 | exit 41 | ;; 42 | 43 | ec2-cwagent-start) 44 | root=$(get_arg -root $CWAROOT) 45 | [ ! -d $root ] && exit 2 46 | 47 | tag=$(get_arg -tag) 48 | [ -z "$tag" ] && tag=$($BKJS_BIN ec2-tag) 49 | 50 | config="\"agent\": { \"usage_data\": false, \"logfile\": \"$CWAROOT/logs/cwagent.log\" }" 51 | 52 | files="access.log message.log error.log docker.log $BKJS_CLOUDWATCH_LOGS" 53 | for file in $files; do 54 | [ -n "$logs" ] && logs="$logs," 55 | logs="$logs{ \"file_path\": \"$BKJS_HOME/log/$file\", \"log_group_name\": \"$file\", \"multi_line_start_pattern\": \"^[^ \\t]\", \"timestamp_format\": \"%Y-%m-%dT%H:%M:%S.%f\" }" 56 | done 57 | 58 | # Streams format: tag_group.log 59 | streams="$BKJS_CLOUDWATCH_STREAMS" 60 | for file in $streams; do 61 | [ -n "$logs" ] && logs="$logs," 62 | log=$(echo $file|awk -F_ '{print $2}') 63 | str=$(echo $file|awk -F_ '{print $1}') 64 | logs="$logs{ \"file_path\": \"$BKJS_HOME/log/$file\", \"log_group_name\": \"$log\", \"log_stream_name\": \"$str\", \"multi_line_start_pattern\": \"^[^ \\t]\", \"timestamp_format\": \"%Y-%m-%dT%H:%M:%S.%f\" }" 65 | done 66 | 67 | config="$config, \"logs\": { \"logs_collected\": { \"files\": { \"collect_list\": [ $logs ] } }, \"log_stream_name\": \"$tag\" }" 68 | 69 | xray_bind=$(get_arg -xray-bind) 70 | [ -n "$xray_bind" ] && xray_bind="\"bind_address\": \"$xray_bind\"" 71 | config="$config, \"traces\": { \"local_mode\": true, \"traces_collected\": { \"xray\": {$xray_bind} } }" 72 | 73 | tmp=$root/etc/cwagent.json 74 | echo "{ $config }" > $tmp 75 | 76 | json=$root/etc/$CWAGENT.json 77 | toml=$root/etc/$CWAGENT.toml 78 | 79 | cmp $json $tmp 80 | if [ "$?" != "0" ]; then 81 | mv $tmp $json 82 | $root/bin/config-translator --input $json --output $toml --mode auto 83 | [ "$?" != "0" ] && exit 1 84 | fi 85 | 86 | toml=$root/etc/$CWAGENT.toml 87 | env=$root/etc/env-config.json 88 | echo "{ \"CWAGENT_LOG_LEVEL\": \"$(get_arg -log ERROR)\" }" > $env 89 | 90 | exec nohup $root/bin/$CWAGENT -config $toml -envconfig $env -pidfile $root/logs/cwagent.pid >> $CWAROOT/logs/cwagent.log 2>&1 & 91 | exit 0 92 | ;; 93 | 94 | ec2-cwagent-init-monit) 95 | root=$(get_arg -root $CWAROOT) 96 | echo -e "check process cwagent with pidfile $root/logs/cwagent.pid start program = \"$BKJS_BIN ec2-cwagent-start\" as uid $BKJS_USER with timeout 60 seconds stop program = \"/usr/bin/pkill -f $CWAGENT\"" > /etc/monit.d/cwagent.conf 97 | exit 98 | ;; 99 | 100 | esac 101 | -------------------------------------------------------------------------------- /lib/aws/ses.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const app = require(__dirname + '/../app'); 7 | const lib = require(__dirname + '/../lib'); 8 | const aws = require(__dirname + '/../aws'); 9 | const logger = require(__dirname + '/../logger'); 10 | 11 | // AWS SES API request 12 | aws.querySES = function(action, obj, options, callback) 13 | { 14 | this.queryEndpoint("email", '2010-12-01', action, obj, options, callback); 15 | } 16 | 17 | /** 18 | * Send an email via SES 19 | * The following options supported: 20 | * - from - an email to use in the From: header 21 | * - cc - list of email to use in CC: header 22 | * - bcc - list of emails to use in Bcc: header 23 | * - replyTo - list of emails to ue in ReplyTo: header 24 | * - returnPath - email where to send bounces 25 | * - charset - charset to use, default is UTF-8 26 | * - html - if set the body is sent as MIME HTML 27 | * - config - configuration set name 28 | */ 29 | aws.sesSendEmail = function(to, subject, body, options, callback) 30 | { 31 | if (typeof options == "function") callback = options, options = null; 32 | if (!options) options = lib.empty; 33 | 34 | var params = { "Message.Subject.Data": subject, "Message.Subject.Charset": options.charset || "UTF-8" }; 35 | params["Message.Body." + (options.html ? "Html" : "Text") + ".Data"] = body; 36 | params["Message.Body." + (options.html ? "Html" : "Text") + ".Charset"] = options.charset || "UTF-8"; 37 | params.Source = options.from || app.emailFrom || ("admin@" + app.domain); 38 | lib.strSplit(to).forEach((x, i) => { params["Destination.ToAddresses.member." + (i + 1)] = x; }) 39 | if (options.cc) lib.strSplit(options.cc).forEach((x, i) => { params["Destination.CcAddresses.member." + (i + 1)] = x; }) 40 | if (options.bcc) lib.strSplit(options.bcc).forEach((x, i) => { params["Destination.BccAddresses.member." + (i + 1)] = x; }) 41 | if (options.replyTo) lib.strSplit(options.replyTo).forEach((x, i) => { params["ReplyToAddresses.member." + (i + 1)] = x; }) 42 | if (options.returnPath) params.ReturnPath = options.returnPath; 43 | if (options.config) params.ConfigurationSetName = options.config; 44 | this.querySES("SendEmail", params, options, callback); 45 | } 46 | 47 | /** 48 | * Send raw email 49 | * The following options accepted: 50 | * - to - list of email addresses to use in RCPT TO 51 | * - from - an email to use in from header 52 | * - config - configuration set name 53 | */ 54 | aws.sesSendRawEmail = function(body, options, callback) 55 | { 56 | if (typeof options == "function") callback = options, options = null; 57 | 58 | var params = { "RawMessage.Data": body }; 59 | if (options) { 60 | if (options.from) params.Source = options.from; 61 | if (options.to) lib.strSplit(options.to).forEach((x, i) => { params["Destinations.member." + (i + 1)] = x; }) 62 | if (options.config) params.ConfigurationSetName = options.config; 63 | } 64 | this.querySES("SendRawEmail", params, options, callback); 65 | } 66 | 67 | // SES V2 version 68 | aws.sesSendRawEmail2 = function(body, options, callback) 69 | { 70 | if (typeof options == "function") callback = options, options = null; 71 | var params = { 72 | Content: { Raw: { Data: body } } 73 | }; 74 | if (options) { 75 | if (options.from) params.FromEmailAddress = options.from; 76 | if (options.to) params.Destination = { ToAddresses: lib.strSplit(options.to) } 77 | if (options.config) params.ConfigurationSetName = options.config; 78 | } 79 | 80 | var headers = { 'content-type': 'application/x-amz-json-1.1' }; 81 | var opts = this.queryOptions("POST", lib.stringify(params), headers, options); 82 | opts.region = this.getServiceRegion("email", options?.region || this.region || 'us-east-1'); 83 | opts.endpoint = "ses"; 84 | opts.action = "SendEmail"; 85 | opts.signer = this.querySigner; 86 | logger.debug(opts.action, opts); 87 | aws.fetch(`https://email.${opts.region}.amazonaws.com/v2/email/outbound-emails`, opts, (err, params) => { 88 | if (params.status != 200) err = aws.parseError(params, options); 89 | if (typeof callback == "function") callback(err, params.obj); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /tools/bkjs-ecr: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Nov 2021 5 | # 6 | 7 | case "$BKJS_CMD" in 8 | 9 | help|ecr-help) 10 | echo "" 11 | echo " ecr-get [-account ID] [-region R] [-login] - login into ECR if required or return ECR host name" 12 | echo " ecr-push -repo REPO -image IMAGE [-tag latest|TAG|VERSION] [-latest] [-create] [-login 0|1] - tag and push a docker image to ECR, create if needed, -latest also pushes it as :latest if -tag is different" 13 | echo " ecr-tag -repo REPO -image IMAGE [-ecr ECR] [-tag latest] - tag and push a docker image to ECR, repository must exist" 14 | echo " ecr-create -repo REPO [-region R] - create a repo if not exist" 15 | echo " ecr-del -tag NAME -repo REPO - delete an image from repo by tag" 16 | echo " ecr-manifest -tag NAME -repo REPO -arm ARMTAG -amd AMDTAG - combine ARM and AMD platforms into one container using manifest" 17 | ;; 18 | 19 | ecr-get) 20 | id=$(get_arg -account $BKJS_ECR_ACCOUNT) 21 | [ -z $id ] && id=$($BKJS_BIN ec2-account) 22 | region=$(get_arg -region $BKJS_ECR_REGION) 23 | [ -z $region ] && region=$($BKJS_BIN ec2-region) 24 | 25 | ecr="$id.dkr.ecr.$region.amazonaws.com" 26 | echo $ecr 27 | 28 | login=$(get_flag -login ${BKJS_ECR_LOGIN:-1}) 29 | [ "$login" != "1" ] && exit 0 30 | 31 | aws ecr get-login-password| docker login --username AWS --password-stdin $ecr 32 | [ "$?" != "0" ] && exit 1 33 | exit 0 34 | ;; 35 | 36 | ecr-push) 37 | repo=$(get_arg -repo) 38 | image=$(get_arg -image) 39 | [ "$repo" = "" -o "$image" = "" ] && echo "-repo and -image are required" && exit 1 40 | 41 | ecr=$($BKJS_BIN ecr-get $(get_all_args)) 42 | [ "$?" != "0" ] && exit 1 43 | 44 | if [ -n "$(get_flag -create)" ]; then 45 | $BKJS_BIN ecr-create -repo $repo 46 | [ "$?" != "0" ] && exit 1 47 | fi 48 | 49 | $BKJS_BIN ecr-tag -ecr $ecr $(get_all_args) 50 | [ "$?" != "0" ] && exit 1 51 | 52 | if [ -n "$(get_flag -latest)" -a "$(get_arg -tag)" != "latest" ]; then 53 | $BKJS_BIN ecr-tag $(get_all_args "-tag") -ecr $ecr 54 | [ "$?" != "0" ] && exit 1 55 | fi 56 | exit 0 57 | ;; 58 | 59 | ecr-tag) 60 | repo=$(get_arg -repo) 61 | image=$(get_arg -image) 62 | [ "$repo" = "" -o "$image" = "" ] && echo "-repo and -image are required" && exit 1 63 | tag=$(get_arg -tag latest) 64 | 65 | ecr=$(get_arg -ecr) 66 | if [ -z "$ecr" ]; then 67 | ecr=$($BKJS_BIN ecr-get $(get_all_args)) 68 | [ "$?" != "0" ] && exit 1 69 | fi 70 | 71 | docker tag $image $ecr/$repo:$tag 72 | docker push $ecr/$repo:$tag 73 | exit 74 | ;; 75 | 76 | ecr-create) 77 | repo=$(get_arg -repo) 78 | [ "$repo" = "" ] && echo "-repo is required" && exit 1 79 | region=$(get_arg -region $BKJS_ECR_REGION) 80 | [ -z $region ] && region=$($BKJS_BIN ec2-region) 81 | 82 | aws ecr describe-repositories --region $region --repository-names $repo --query repositories[*].repositoryUri --output text 2>/dev/null 83 | if [ "$?" != "0" ]; then 84 | aws ecr create-repository --region $region --repository-name $repo 85 | [ "$?" != "0" ] && exit 1 86 | fi 87 | exit 0 88 | ;; 89 | 90 | ecr-del) 91 | repo=$(get_arg -repo) 92 | tag=$(get_arg -tag) 93 | [ "$repo" = "" -o $tag == "" ] && echo "-repo and -tag are required" && exit 1 94 | 95 | aws ecr batch-delete-image --repository-name $repo --image-ids imageTag=$tag 96 | exit 97 | ;; 98 | 99 | ecr-manifest) 100 | repo=$(get_arg -repo) 101 | tag=$(get_arg -tag) 102 | arm64=$(get_arg -arm64) 103 | amd64=$(get_arg -amd64) 104 | [ "$repo" = "" -o $tag == "" ] && echo "-repo and -tag are required" && exit 1 105 | [ "$arm64" = "" -o $amd64 == "" ] && echo "-arm64 and -amd64 are required" && exit 1 106 | 107 | set -e 108 | docker manifest create $repo:$tag $repo:$arm64 $repo:$amd64 109 | docker manifest annotate --arch arm64 $repo:$tag $repo:$arm64 110 | docker manifest annotate --arch amd64 $repo:$tag $repo:$amd64 111 | docker manifest inspect $repo:$tag 112 | docker manifest push $repo:$tag 113 | exit 114 | ;; 115 | 116 | esac 117 | -------------------------------------------------------------------------------- /lib/db/pg.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const lib = require(__dirname + '/../lib'); 7 | const db = require(__dirname + '/../db'); 8 | const logger = require(__dirname + '/../logger'); 9 | const SqlPool = require(__dirname + '/sqlpool'); 10 | 11 | /** 12 | * @module db/pg 13 | */ 14 | const pool = { 15 | name: "pg", 16 | type: "pg", 17 | configOptions: { 18 | typesMap: { list: "text[]" }, 19 | upsert: 1, 20 | noListOps: 0, 21 | noListTypes: 0, 22 | schema: ['public'], 23 | }, 24 | createPool: function(options) { 25 | return new PostgresPool(options); 26 | } 27 | }; 28 | module.exports = pool; 29 | 30 | db.modules.push(pool); 31 | 32 | class PgClient { 33 | constructor(client) { 34 | this.pg = client; 35 | } 36 | 37 | query(req, callback) { 38 | if (typeof req == "string") req = { text: req }; 39 | logger.dev("pgQuery:", req.text, req.values); 40 | 41 | this.pg.query(req.text, req.values, (err, result) => { 42 | callback(err, result?.rows || [], { affected_rows: result?.rowCount }); 43 | }); 44 | } 45 | } 46 | 47 | class PostgresPool extends SqlPool { 48 | 49 | constructor(options) 50 | { 51 | pool.pg = require("pg"); 52 | super(options, pool); 53 | } 54 | 55 | openDb(callback) 56 | { 57 | if (this.url == "default") this.url = "postgresql://postgres@127.0.0.1/" + db.dbName; 58 | const client = new pool.pg.Client(/:\/\//.test(this.url) ? { connectionString: this.url } : this.configOptions); 59 | client.connect((err) => { 60 | if (err) { 61 | logger.error('openDb:', this.name, err); 62 | callback(err); 63 | } else { 64 | client.on('error', logger.error.bind(logger, this.name)); 65 | client.on('notice', logger.log.bind(logger, this.name)); 66 | client.on('notification', logger.info.bind(logger, this.name)); 67 | callback(err, new PgClient(client)); 68 | } 69 | }); 70 | } 71 | 72 | closeDb(client, callback) 73 | { 74 | client.pg.end(callback); 75 | } 76 | 77 | // Cache indexes using the information_schema 78 | cacheIndexes(client, options, callback) 79 | { 80 | client.query("SELECT t.relname as table, i.relname as index, indisprimary as pk, array_agg(a.attname ORDER BY a.attnum) as cols "+ 81 | "FROM pg_class t, pg_class i, pg_index ix, pg_attribute a, pg_catalog.pg_namespace n "+ 82 | "WHERE t.oid = ix.indrelid and i.oid = ix.indexrelid and a.attrelid = t.oid and n.oid = t.relnamespace and " + 83 | " a.attnum = ANY(ix.indkey) and t.relkind = 'r' and n.nspname not in ('pg_catalog', 'pg_toast') " + 84 | "GROUP BY t.relname, i.relname, ix.indisprimary ORDER BY t.relname, i.relname", (err, rows) => { 85 | if (err) logger.error('cacheIndexes:', this.name, err); 86 | this.dbkeys = {}; 87 | this.dbindexes = {}; 88 | for (const i in rows) { 89 | if (rows[i].pk) { 90 | this.dbkeys[rows[i].table] = rows[i].cols; 91 | } else { 92 | this.dbindexes[rows[i].index] = rows[i].cols; 93 | } 94 | } 95 | this.release(client); 96 | lib.tryCall(callback, err, []); 97 | }); 98 | } 99 | 100 | prepareUpdateExpr(req, expr) 101 | { 102 | switch (expr.op) { 103 | case "add": 104 | // Add to a list 105 | if (expr.type != "list") break; 106 | expr.text = `${expr.column}=${expr.column}||${expr.placeholder}`; 107 | return; 108 | 109 | case "del": 110 | // Delete from a list, only one item at a time 111 | if (expr.type != "list") break; 112 | expr.text = `${expr.column}=array_remove(${expr.column},${expr.placeholder})`; 113 | expr.value = Array.isArray(expr.value) ? expr.value[0] : expr.value; 114 | return; 115 | } 116 | 117 | super.prepareUpdateExpr(req, expr); 118 | } 119 | 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /tools/bkjs-monit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | BKJS_USER=$(get_arg -user ${BKJS_USER:-$(whoami)}) 8 | 9 | case "$BKJS_CMD" in 10 | 11 | help|monit-help) 12 | echo "" 13 | echo " monit-init-system - setup system monitoring with monit, CPU, disk" 14 | echo " monit-init-bkjs - setup monit to keep bkjs service running without using any other services and monitor" 15 | echo " monit-stop-bkjs - remove and stop bkjs, reload monit" 16 | echo " monit-init-alerts - setup monit mail alerts" 17 | echo " monit-init -name NAME [-gid G] [-timeout 30] [-cycles N] -start SCRIPT -stop SCRIPT - generate a monit service config" 18 | ;; 19 | 20 | monit-init-system) 21 | interval=$(get_arg -interval 15) 22 | delay=$(get_arg -delay 0) 23 | load=$(get_arg -load 7) 24 | lcycles=$(get_arg -lcycles 4) 25 | space=$(get_arg -space 90) 26 | fscycles=$(get_arg -fscycles 50) 27 | path=$(get_arg -path /) 28 | mkdir -p /etc/monit.d 29 | if [ -z "$(egrep -Es '^include /etc/monit.d' /etc/monitrc)" ]; then 30 | echo 'include /etc/monit.d/*' >> /etc/monitrc 31 | fi 32 | echo "set logfile syslog" > /etc/monit.d/system.conf 33 | echo "set daemon $interval with start delay $delay" > /etc/monit.d/system.conf 34 | echo "check system \$HOST every $lcycles cycles if loadavg(5min) > $load then alert" >> /etc/monit.d/system.conf 35 | echo "check filesystem rootfs with path $path every $fscycles cycles if space usage > ${space}% then alert" >> /etc/monit.d/system.conf 36 | exit 37 | ;; 38 | 39 | monit-start-instance) 40 | file=~/.monit.uptime 41 | uptime=$(stat -c %Z /proc/1/cmdline) 42 | [ -f $file -a "$(cat $file)" = "$uptime" ] && exit 43 | echo $uptime > $file 44 | run_bkjs_cmd start-hook 45 | exit 46 | ;; 47 | 48 | monit-init-start-instance) 49 | bin=$(get_arg -bin $BKJS_BIN) 50 | mkdir -p /etc/monit.d 51 | echo -e "check program start-instance with path \"$bin monit-start-instance\" if status > -1 then unmonitor" > /etc/monit.d/start-instance.conf 52 | exit 53 | ;; 54 | 55 | monit-init-bkjs) 56 | timeout=$(get_arg -timeout 30) 57 | bin=$(get_arg -bin $BKJS_BIN) 58 | echo -e "check process bkjs with pidfile \"$BKJS_HOME/var/server.pid\" start program = \"$bin run-server $(get_all_args)\" as uid $BKJS_USER with timeout $timeout seconds stop program = \"$bin stop\"" > /etc/monit.d/bkjs.conf 59 | exit 60 | ;; 61 | 62 | monit-stop-bkjs) 63 | rm -f /etc/monit.d/bkjs.conf 64 | killall -HUP monit 65 | $BKJS_BIN stop 66 | exit 67 | ;; 68 | 69 | monit-init-alerts) 70 | [ -n "$(get_flag -force)" ] && rm -f /etc/monit.d/alert.conf 71 | [ -f /etc/monit.d/alert.conf ] && exit 72 | email=$(get_arg -email) 73 | [ -z "$email" ] && exit 74 | user=$(get_arg -user) 75 | host=$(get_arg -host) 76 | password=$(get_arg -password) 77 | events=$(get_arg -events "action,connection,data,pid,ppid,exec,content,resource,status,timeout") 78 | echo "Init monit alert: $email $events, $host, $user" 79 | [ "$events" != "" ] && events="only on { $events }" 80 | echo -e "set alert $email $events" > /etc/monit.d/alert.conf 81 | echo -e "set mail-format { from: $email }" >> /etc/monit.d/alert.conf 82 | [ -z "$host" ] && exit 83 | server="set mailserver $host" 84 | if match $host amazonaws; then server="$server port 465"; fi 85 | [ -n "$user" ] && server="$server username $user" 86 | [ -n "$password" ] && server="$server password $password" 87 | if match $host amazonaws; then server="$server using tlsv13"; fi 88 | echo -e $server >> /etc/monit.d/alert.conf 89 | exit 90 | ;; 91 | 92 | monit-init) 93 | name=$(get_arg -name) 94 | start=$(get_arg -start) 95 | stop=$(get_arg -stop) 96 | [ "$name" = "" -o "$start" = "" -o "$stop" = "" ] && echo "invalid init-monit arguments" && exit 97 | timeout=$(get_arg -timeout 30) 98 | cycles=$(get_arg -cycles) 99 | [ -n "$cycles" ] && cycles="for $cycles cycles" 100 | gid=$(get_arg -gid) 101 | [ -n "$gid" ] && gid="and gid $gid" 102 | echo -e "check process $name with pidfile \"$BKJS_HOME/var/$name.pid\" start program = \"$start\" as uid $BKJS_USER $gid with timeout $timeout seconds $cycles stop program = \"$stop\"" > /etc/monit.d/$name.conf 103 | exit 104 | ;; 105 | 106 | esac 107 | -------------------------------------------------------------------------------- /tools/bkjs-redis: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # Author: Vlad Seryakov vseryakov@gmail.com 4 | # Sep 2013 5 | # 6 | 7 | BKJS_USER=$(get_arg -user ${BKJS_USER:-$(whoami)}) 8 | 9 | case "$BKJS_CMD" in 10 | 11 | help|redis-help) 12 | echo "" 13 | echo " redis-get - install Redis server into $BKJS_HOME" 14 | echo " redis-init - install and setup Redis server to be run on start and to be monitored (Linux only)" 15 | echo " redis-run [-memsize PERCENT] [-memmax SIZE] [-slave-host HOST] - run local Redis server, uses config file $BKJS_HOME/etc/redis.conf" 16 | echo " redis-stop - stop local Redis server" 17 | echo " redis-init-monit [-memsize PERCENT] [-memmax SIZE] - setup Redis server to be run on start and to be monitored (Linux only)" 18 | echo "" 19 | ;; 20 | 21 | redis-init) 22 | ($0 redis-get $(get_all_args)) 23 | ($0 redis-run $(get_all_args)) 24 | if [ "$PLATFORM" = "Linux" ]; then 25 | sudo $BKJS_BIN redis-init-monit 26 | fi 27 | exit 28 | ;; 29 | 30 | redis-get) 31 | # Install redis server 32 | if [ -f $BKJS_HOME/bin/redis-server ]; then 33 | [ "$(get_flag -force)" = "" ] && exit 34 | echo "Uninstalling redis from $BKJS_HOME..." 35 | rm -f $BKJS_HOME/bin/redis-* 36 | cp $BKJS_HOME/etc/redis.local.conf $BKJS_HOME/etc/redis.local.conf.old 37 | fi 38 | 39 | ver=$(get_arg -version 7.2.4) 40 | curl -L -o redis.tgz http://download.redis.io/releases/redis-$ver.tar.gz 41 | 42 | mkdir -p redis $BKJS_HOME/etc 43 | tar -C redis --strip-components=1 -xzf redis.tgz 44 | 45 | [ -n "$(get_flag -tls)" ] && tls="BUILD_TLS=yes" 46 | make -C redis $tls install PREFIX=$BKJS_HOME 47 | 48 | cp redis/redis.conf $BKJS_HOME/etc 49 | rm -rf redis redis.tgz 50 | $BKJS_BIN redis-config 51 | exit 52 | ;; 53 | 54 | redis-config) 55 | conf=$BKJS_HOME/etc/redis.conf 56 | [ ! -f $conf ] && conf=/etc/redis.conf 57 | local=$BKJS_HOME/etc/redis.local.conf 58 | 59 | if [ -z "$(grep -s $local $conf)" ]; then 60 | echo "include $local" >> $conf 61 | fi 62 | 63 | echo 'syslog-enabled yes' > $local 64 | echo "dir $BKJS_HOME/var/" >> $local 65 | echo "timeout 3600" >> $local 66 | echo "bind *" >> $local 67 | echo "protected-mode no" >> $local 68 | echo "unixsocket $BKJS_HOME/var/redis.sock" >> $local 69 | echo "pidfile $BKJS_HOME/var/redis.pid" >> $local 70 | echo "logfile $BKJS_HOME/log/redis.log" >> $local 71 | echo "tcp-keepalive 60" >> $local 72 | echo "maxmemory-policy volatile-lru" >> $local 73 | echo 'daemonize yes' >> $local 74 | 75 | if [ "$(whoami)" = "root" ]; then 76 | [ -n "$BKJS_USER" ] && chown $BKJS_USER $local 77 | 78 | if [ "$PLATFORM" = "Linux" ]; then 79 | echo 1 > /proc/sys/vm/overcommit_memory 80 | echo never > /sys/kernel/mm/transparent_hugepage/enabled 81 | fi 82 | fi 83 | exit 84 | ;; 85 | 86 | redis-run) 87 | # Percent from the total memory 88 | memsize=$(get_arg -memsize) 89 | [ "$memsize" != "" ] && memmax="$(( ($(free -m|grep Mem:|awk '{print $2}') * $memsize) / 100 ))mb" 90 | memmax=$(get_arg -memmax $memmax) 91 | if [ "$memmax" != "" ]; then 92 | conf=$BKJS_HOME/etc/redis.local.conf 93 | if [ -z "$(grep -s "maxmemory $memmax" $conf)" ]; then 94 | echo "maxmemory $memmax" >> $conf 95 | fi 96 | fi 97 | 98 | conf=$BKJS_HOME/etc/redis.conf 99 | [ ! -f $conf ] && conf=/etc/redis.conf 100 | 101 | touch $BKJS_HOME/log/redis.log 102 | redis-server $conf 103 | 104 | slavehost=$(get_arg -slave-host) 105 | slaveport=$(get_arg -slave-port 6379) 106 | if [ "$slavehost" != "" ]; then 107 | redis-cli slaveof $slavehost $slaveport 108 | fi 109 | exit 110 | ;; 111 | 112 | redis-stop) 113 | pkill -f redis-server 114 | exit 115 | ;; 116 | 117 | redis-init-monit) 118 | echo -e "$BKJS_HOME/log/redis.log {\n weekly\n rotate 10\n copytruncate\n delaycompress\n compress\n notifempty\n missingok\n}" > /etc/logrotate.d/redis 119 | echo -e "check process redis-server with pidfile \"$BKJS_HOME/var/redis.pid\" start program = \"$BKJS_BIN redis-run $(get_all_args)\" as uid $BKJS_USER stop program = \"$BKJS_BIN redis-stop\" if failed host 127.0.0.1 port 6379 for 2 cycles then restart if total memory usage > 50% for 2 cycles then alert" > /etc/monit.d/redis.conf 120 | exit 121 | ;; 122 | 123 | esac 124 | 125 | -------------------------------------------------------------------------------- /examples/kanban/modules/kanban.js: -------------------------------------------------------------------------------- 1 | // 2 | // Author: Vlad Seryakov vseryakov@gmail.com 3 | // backendjs 2025 4 | // 5 | 6 | const { db, api, lib } = require('backendjs'); 7 | 8 | module.exports = { 9 | 10 | tables: { 11 | users: { 12 | id: { primary: 1 }, 13 | name: { not_null: 1 }, 14 | }, 15 | 16 | boards: { 17 | id: { primary: 1 }, 18 | title: { not_null: 1 }, 19 | description: { not_null: 1 }, 20 | created_at: { type: "bigint", not_null: 1 }, 21 | }, 22 | 23 | lists: { 24 | id: { primary: 1 }, 25 | board_id: { not_null: 1, foreign: { table: "boards", name: "id", ondelete: "cascade" } }, 26 | title: { not_null: 1 }, 27 | position: { type: "int", not_null: 1 }, 28 | created_at: { type: "bigint", not_null: 1 }, 29 | }, 30 | 31 | cards: { 32 | id: { primary: 1 }, 33 | list_id: { not_null: 1, foreign: { table: "lists", name: "id", ondelete: "cascade" } }, 34 | title: { not_null: 1 }, 35 | description: {}, 36 | assignee_id: { foreign: { table: "users", name: "id", ondelete: "cascade" } }, 37 | position: { type: "int", not_null: 1 }, 38 | completed: { type: "int", value: false }, 39 | created_at: { type: "bigint", not_null: 1 }, 40 | }, 41 | 42 | card_tags: { 43 | card_id: { primary: 1, foreign: { table: "cards", name: "id", ondelete: "cascade" } }, 44 | tag_id: { primary: 2, foreign: { table: "tags", name: "id", ondelete: "cascade" } }, 45 | }, 46 | 47 | comments: { 48 | id: { primary: 1 }, 49 | card_id: { not_null: 1, foreign: { table: "cards", name: "id", ondelete: "cascade" } }, 50 | user_id: { not_null: 1, foreign: { table: "users", name: "id", ondelete: "cascade" } }, 51 | text: { not_null: 1 }, 52 | created_at: { type: "bigint", not_null: 1 }, 53 | }, 54 | 55 | tags: { 56 | id: { primary: 1 }, 57 | name: { not_null: 1 }, 58 | color: { not_null: 1 }, 59 | created_at: { type: "bigint", not_null: 1 }, 60 | }, 61 | }, 62 | 63 | configureWeb(options, callback) { 64 | 65 | api.app.use("/api", 66 | api.express.Router(). 67 | get("/boards", getBoards). 68 | post("/boards", createBoard). 69 | get("/board/:id", getBoard). 70 | post("/board/:id", updateBoard). 71 | delete("/board/:id", delBoard)); 72 | 73 | callback(); 74 | } 75 | }; 76 | 77 | function getBoards(req, res) 78 | { 79 | var data = []; 80 | db.scan("boards", {}, { sync: 1 }, (rows) => { 81 | data.push(...rows); 82 | }, (err) => { 83 | api.sendJSON(req, err, data); 84 | }); 85 | } 86 | 87 | function getBoard(req, res) 88 | { 89 | db.get("boards", { id: req.params.id }, (err, row) => { 90 | if (!err && !row) err = { status: 404, message: "Board not found" }; 91 | api.sendJSON(req, err, row); 92 | }); 93 | } 94 | 95 | function createBoard(req, res) 96 | { 97 | var query = api.toParams(req, { 98 | title: { required: 1, max: 128 }, 99 | description: { required: 1, max: 256 }, 100 | id: { value: lib.uuid() }, 101 | created_at: { value: Date.now() }, 102 | }); 103 | if (typeof query == "string") return api.sendReply(res, 400, query); 104 | 105 | db.put("boards", query, (err) => { 106 | api.sendJSON(req, err, query); 107 | }); 108 | } 109 | 110 | function updateBoard(req, res) 111 | { 112 | var query = api.toParams(req, { 113 | id: { required: 1, value: req.params.id }, 114 | title: { required: 1, max: 128 }, 115 | description: { required: 1, max: 256 }, 116 | }); 117 | if (typeof query == "string") return api.sendReply(res, 400, query); 118 | 119 | db.update("boards", query, (err) => { 120 | api.sendJSON(req, err, query); 121 | }); 122 | } 123 | 124 | function delBoard(req, res) 125 | { 126 | var query = api.toParams(req, { 127 | id: { required: 1, value: req.params.id }, 128 | }); 129 | if (typeof query == "string") return api.sendReply(res, 400, query); 130 | 131 | db.del("boards", query, (err) => { 132 | api.sendJSON(req, err); 133 | }); 134 | } 135 | -------------------------------------------------------------------------------- /lib/aws/other.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const app = require(__dirname + '/../app'); 9 | const lib = require(__dirname + '/../lib'); 10 | const aws = require(__dirname + '/../aws'); 11 | 12 | // Assume a role and return new credentials that can be used in other API calls 13 | aws.stsAssumeRole = function(options, callback) 14 | { 15 | var params = { 16 | RoleSessionName: options.name || app.id, 17 | RoleArn: options.role, 18 | }; 19 | this.querySTS("AssumeRole", params, options, (err, obj) => { 20 | if (!err) { 21 | obj = lib.objGet(obj, "AssumeRoleResponse.AssumeRoleResult"); 22 | obj.credentials = { 23 | key: obj.Credentials.AccessKeyId, 24 | secret: obj.Credentials.SecretAccessKey, 25 | token: obj.Credentials.SessionToken, 26 | expiration: lib.toDate(obj.Credentials.Expiration).getTime(), 27 | }; 28 | delete obj.Credentials; 29 | } 30 | if (typeof callback == "function") callback(err, obj); 31 | }); 32 | } 33 | 34 | /** 35 | * Detect image features using AWS Rekognition service, the `name` can be a Buffer, a local file or an url to the S3 bucket. In the latter case 36 | * the url can be just apath to the file inside a bucket if `options.bucket` is specified, otherwise it must be a public S3 url with the bucket name 37 | * to be the first part of the host name. For CDN/CloudFront cases use the `option.bucket` option. 38 | */ 39 | aws.detectLabels = function(name, options, callback) 40 | { 41 | if (typeof options == "function") callback = options, options = null; 42 | 43 | if (Buffer.isBuffer(name)) { 44 | const req = { 45 | Image: { 46 | Bytes: name.toString("base64") 47 | } 48 | }; 49 | aws.queryRekognition("DetectLabels", req, options, callback); 50 | } else 51 | if (name && options && options.bucket) { 52 | const req = { 53 | Image: { 54 | S3Object: { 55 | Bucket: options.bucket, 56 | Name: name[0] == "/" ? name.substr(1) : name 57 | } 58 | } 59 | }; 60 | aws.queryRekognition("DetectLabels", req, options, callback); 61 | } else 62 | if (name && name[0] == "/") { 63 | fs.readFile(path.normalize(name), function(err, data) { 64 | if (err) return callback && callback(err); 65 | const req = { 66 | Image: { 67 | Bytes: data.toString("base64") 68 | } 69 | }; 70 | aws.queryRekognition("DetectLabels", req, options, callback); 71 | }); 72 | } else { 73 | name = URL.parse(String(name)); 74 | if (!name) return callback && callback({ status: 400, message: "invalid url" }) 75 | if (name.pathname && name.pathname[0] == "/") name.pathname = name.pathname.substr(1); 76 | const req = { 77 | Image: { 78 | S3Object: { 79 | Bucket: name.hostname && name.hostname.split(".")[0], 80 | Name: name.pathname 81 | } 82 | } 83 | }; 84 | if (!req.Image.S3Object.Bucket || !req.Image.S3Object.Name) return callback && callback({ status: 404, message: "invalid image" }); 85 | aws.queryRekognition("DetectLabels", req, options, callback); 86 | } 87 | } 88 | 89 | /** 90 | * Return a list of certificates, 91 | * - `status` can limit which certs to return, PENDING_VALIDATION | ISSUED | INACTIVE | EXPIRED | VALIDATION_TIMED_OUT | REVOKED | FAILED 92 | */ 93 | aws.listCertificates = function(options, callback) 94 | { 95 | var token, list = []; 96 | 97 | lib.doWhilst( 98 | function(next) { 99 | aws.queryACM("ListCertificates", { CertificateStatuses: options.status, MaxItems: 1000, NextToken: token }, (err, rc) => { 100 | if (err) return next(err); 101 | token = rc.NextToken; 102 | for (const i in rc.CertificateSummaryList) { 103 | list.push(rc.CertificateSummaryList[i]); 104 | } 105 | next(); 106 | }); 107 | }, 108 | function() { 109 | return token; 110 | }, 111 | function(err) { 112 | lib.tryCall(callback, err, list); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /tests/flow.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { describe, it } = require('node:test'); 3 | const assert = require('node:assert/strict'); 4 | const { lib } = require("../"); 5 | 6 | describe("Flow tests", () => { 7 | 8 | var direct = lib.isArg("-direct"); 9 | 10 | var c1 = 0; 11 | lib.forEach([ 1, 2, 3 ], (i, next) => { 12 | c1++; next() 13 | }, (err) => { 14 | assert.strictEqual(c1, 3); 15 | }, direct) 16 | 17 | 18 | var c2 = 0; 19 | lib.forEach([ 1, 2, 3 ], (i, next) => { 20 | c2++; next(i == 2 ? "error" : null) 21 | }, (err) => { 22 | assert.ok(c2 == 2 && err); 23 | }, direct) 24 | 25 | 26 | var c3 = 0; 27 | lib.forEvery([ 1, 2, 3 ], (i, next) => { 28 | c3++; next("ignore") 29 | }, (err) => { 30 | assert.ok(c3 == 3 && err == "ignore") 31 | }, direct) 32 | 33 | lib.forEachSeries([ 1, 2, 3 ], (i, next, n) => { 34 | next(null, lib.toNumber(n) + i); 35 | }, (err, n) => { 36 | assert.strictEqual(n, 6); 37 | }, direct); 38 | 39 | lib.forEachSeries([ 1, 2, 3 ], (i, next, n) => { 40 | next(i == 2 ? "error" : null, lib.toNumber(n) + i); 41 | }, (err, n) => { 42 | assert.ok(n == 3 && err == "error"); 43 | }, direct); 44 | 45 | lib.forEverySeries([ 1, 2, 3 ], (i, next, err, n) => { 46 | next("ignore", lib.toNumber(n) + i); 47 | }, (err, n) => { 48 | assert.ok(n == 6 && err == "ignore"); 49 | }, direct); 50 | 51 | var c4 = 0; 52 | lib.forEachLimit([ 1, 2, 3 ], 2, (i, next) => { 53 | c4++; next(); 54 | }, (err) => { 55 | assert.strictEqual(c4, 3); 56 | }, direct); 57 | 58 | var c5 = 0; 59 | lib.forEachLimit([ 1, 2, 3 ], 2, (i, next) => { 60 | c5++; next(i == 2 ? "error" : null); 61 | }, (err) => { 62 | assert.ok(c5 == 2 && err == "error"); 63 | }, direct); 64 | 65 | var c6 = 0; 66 | lib.forEveryLimit([ 1, 2, 3 ], 2, (i, next) => { 67 | c6++; next("ignore"); 68 | }, (err) => { 69 | assert.ok(c6 == 3 && String(err) == "ignore,ignore,ignore"); 70 | }, direct); 71 | 72 | var c7 = 0; 73 | lib.whilst( 74 | function() { 75 | return c7 < 5; 76 | }, 77 | function (next) { 78 | c7++; 79 | next(null, c7); 80 | }, 81 | function (err, d) { 82 | assert.strictEqual(c7, 5); 83 | }, direct); 84 | 85 | var c8 = 0; 86 | lib.doWhilst( 87 | function (next) { 88 | c8++; 89 | next(null, c8); 90 | }, 91 | function() { 92 | return c8 < 5; 93 | }, 94 | function (err, d) { 95 | assert.strictEqual(c8, 5); 96 | }, direct); 97 | 98 | var c9 = 0; 99 | lib.series([ 100 | (next) => { 101 | c9++ 102 | next(null, 1) 103 | }, 104 | (next, data) => { 105 | c9++ 106 | next(null, data + 1) 107 | } 108 | ], (err, d) => { 109 | assert.ok(c9 == 2 && d === 2); 110 | }, direct) 111 | 112 | var c10 = 0; 113 | lib.series([ 114 | (next) => { 115 | c10++ 116 | next("error", 1); 117 | }, 118 | (next, data) => { 119 | c10++ 120 | next("error", data + 1) 121 | } 122 | ], (err, d) => { 123 | assert.ok(c10 == 1 && d == 1 && err == "error"); 124 | }, direct) 125 | 126 | var c11 = 0; 127 | lib.parallel([ 128 | (next) => { 129 | c11++; 130 | next() 131 | }, 132 | (next) => { 133 | c11++; 134 | next() 135 | } 136 | ], (err) => { 137 | assert.strictEqual(c11, 2); 138 | }, direct) 139 | 140 | var c12 = 0; 141 | lib.parallel([ 142 | (next) => { 143 | c12++ 144 | next("error"); 145 | }, 146 | (next) => { 147 | c12++; 148 | next(); 149 | } 150 | ], (err) => { 151 | assert.ok(c12 >= 1 && err); 152 | }, direct) 153 | 154 | var c13 = 0; 155 | lib.everySeries([ 156 | (next) => { 157 | c13++; 158 | next("ignore", 1); 159 | }, 160 | (next, err, data) => { 161 | c13++; 162 | next(err, data + 1) 163 | } 164 | ], (err, d) => { 165 | assert.ok(c13 == 2 && d === 2 && err == "ignore"); 166 | }, direct) 167 | 168 | var c14 = 0; 169 | lib.everyParallel([ 170 | (next) => { 171 | c14++ 172 | next("ignore") 173 | }, 174 | (next) => { 175 | c14++ 176 | next() 177 | } 178 | ], (err) => { 179 | assert.strictEqual(c14, 2); 180 | }, direct) 181 | 182 | }) 183 | -------------------------------------------------------------------------------- /lib/metrics/TokenBucket.js: -------------------------------------------------------------------------------- 1 | 2 | const lib = require("../lib"); 3 | 4 | 5 | module.exports = class TokenBucket { 6 | 7 | /** 8 | * Create a Token Bucket object for rate limiting as per http://en.wikipedia.org/wiki/Token_bucket 9 | * - rate - the rate to refill tokens 10 | * - max - the maximum burst capacity 11 | * - interval - interval for the bucket refills, default 1000 ms 12 | * 13 | * Store as an array for easier serialization into JSON when keep it in the shared cache. 14 | * 15 | * Based on https://github.com/thisandagain/micron-throttle 16 | * @param {number|object|number[]} rate 17 | * @param {number} max 18 | * @param {number} interval 19 | * 20 | * @class TokenBucket 21 | */ 22 | constructor(rate, max, interval) 23 | { 24 | this.configure(rate, max, interval); 25 | } 26 | 27 | /** 28 | * Initialize existing token with numbers for rate calculations 29 | * 30 | * @memberOf TokenBucket 31 | * @method configure 32 | */ 33 | configure(rate, max, interval, total) 34 | { 35 | if (Array.isArray(rate)) { 36 | this._rate = lib.toNumber(rate[0]); 37 | this._max = lib.toNumber(rate[1]); 38 | this._count = lib.toNumber(rate[2]); 39 | this._time = lib.toNumber(rate[3]); 40 | this._interval = lib.toNumber(rate[4]); 41 | this._total = lib.toNumber(rate[5]); 42 | } else 43 | if (typeof rate == "object" && rate?.rate) { 44 | this._rate = lib.toNumber(rate.rate); 45 | this._max = lib.toNumber(rate.max); 46 | this._count = lib.toNumber(rate.count); 47 | this._time = lib.toNumber(rate.time); 48 | this._interval = lib.toNumber(rate.interval); 49 | this._total = lib.toNumber(rate.total); 50 | } else { 51 | this._rate = lib.toNumber(rate, { min: 0 }); 52 | this._max = lib.toNumber(max, { min: 0 }) || this._rate; 53 | this._count = this._max; 54 | this._time = Date.now(); 55 | this._interval = lib.toNumber(interval, { min: 0 }) || 1000; 56 | this._total = lib.toNumber(total, { min: 0 }); 57 | } 58 | } 59 | 60 | /** 61 | * Return a JSON object to be serialized/saved, can be used to construct new object as the `rate` param 62 | * @memberOf TokenBucket 63 | * @method toJSON 64 | */ 65 | toJSON() 66 | { 67 | return { rate: this._rate, max: this._max, count: this._count, time: this._time, interval: this._interval, total: this._total }; 68 | } 69 | 70 | /** 71 | * Return a string to be serialized/saved, can be used to construct new object as the `rate` param 72 | * @memberOf TokenBucket 73 | * @method toString 74 | */ 75 | toString() 76 | { 77 | return this.toArray().join(","); 78 | } 79 | 80 | /** 81 | * Return an array object to be serialized/saved, can be used to construct new object as the `rate` param 82 | * @memberOf TokenBucket 83 | * @method toArray 84 | */ 85 | toArray() 86 | { 87 | return [this._rate, this._max, this._count, this._time, this._interval, this._total]; 88 | } 89 | 90 | /** 91 | * Return true if this bucket uses the same rates in arguments 92 | * @memberOf TokenBucket 93 | * @method equal 94 | */ 95 | equal(rate, max, interval) 96 | { 97 | rate = lib.toNumber(rate, { min: 0 }); 98 | max = lib.toNumber(max || rate, { min: 0 }); 99 | interval = lib.toNumber(interval || 1000, { min: 1 }); 100 | return this._rate === rate && this._max === max && this._interval == interval; 101 | } 102 | 103 | /** 104 | * Consume N tokens from the bucket, if no capacity, the tokens are not pulled from the bucket. 105 | * 106 | * Refill the bucket by tracking elapsed time from the last time we touched it. 107 | * 108 | * min(totalTokens, current + (fillRate * elapsedTime)) 109 | * @memberOf TokenBucket 110 | * @method consume 111 | */ 112 | consume(tokens) 113 | { 114 | var now = Date.now(); 115 | if (now < this._time) this._time = now - this._interval; 116 | this._elapsed = now - this._time; 117 | if (this._count < this._max) this._count = Math.min(this._max, this._count + this._rate * (this._elapsed / this._interval)); 118 | this._time = now; 119 | if (typeof tokens != "number" || tokens < 0) tokens = 0; 120 | this._total += tokens; 121 | if (tokens > this._count) return false; 122 | this._count -= tokens; 123 | return true; 124 | } 125 | 126 | /** 127 | * Returns number of milliseconds to wait till number of tokens can be available again 128 | */ 129 | delay(tokens) 130 | { 131 | return Math.max(0, this._interval - (tokens >= this._max ? 0 : this._elapsed)); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /tests/config.test.js: -------------------------------------------------------------------------------- 1 | 2 | const util = require("util"); 3 | const { describe, it, after } = require('node:test'); 4 | const assert = require('node:assert/strict'); 5 | const { app, logger, db, lib, cache, logwatcher, api } = require("../"); 6 | 7 | describe("Config tests", async () => { 8 | 9 | it("test sections", async () => { 10 | 11 | var data = ` 12 | line1=line1 13 | [tag=test1] 14 | line3=line3 15 | [runMode=test2] 16 | line4=line4 17 | [tag=test] 18 | line5=line5 19 | [instance.tag=tag] 20 | line6=line6 21 | [roles=dev,staging] 22 | line7=line7 23 | [aws.key=] 24 | line8=line8 25 | [instance.tag!=] 26 | line9=line9 27 | [none.test=1] 28 | line10=line10 29 | [instance.tag!=aaa] 30 | line11=line11 31 | [roles=dev] 32 | line12=line12 33 | [roles=beta] 34 | line13=line13 35 | [global] 36 | line2=line2 37 | `; 38 | 39 | var args = lib.configParse(data); 40 | assert.deepStrictEqual(args, ["-line1", "line1", "-line2", "line2"]) 41 | 42 | args = lib.configParse(data, { tag: "test1" }); 43 | assert.deepStrictEqual(args, ["-line1", "line1", "-line3", "line3", "-line2", "line2"]) 44 | 45 | args = lib.configParse(data, { tag: "test", runMode: "test2" }); 46 | assert.deepStrictEqual(args, ['-line1', 'line1','-line4', 'line4','-line5', 'line5','-line2', 'line2' ]) 47 | 48 | app.instance.tag = "tag"; 49 | app.instance.roles = ["dev", "staging", "prod"]; 50 | args = lib.configParse(data, app); 51 | assert.deepStrictEqual(args, ['-line1', 'line1', '-line6', 'line6', '-line9', 'line9', '-line11', 'line11', '-line2', 'line2' ]) 52 | }) 53 | 54 | it("test parameters", async () => { 55 | 56 | var argv = [ 57 | "-api-redirect-url", '{ "^a/$": "a", "^b": "b" }', 58 | "-logwatcher-send-error", "a", 59 | "-logwatcher-files-error", "a", 60 | "-logwatcher-files", "b", 61 | "-logwatcher-matches-error", "a", 62 | "-db-create-tables", 63 | "-db-sqlite-pool-max", "10", 64 | "-db-sqlite1-pool", "a", 65 | "-db-sqlite1-pool-max", "10", 66 | "-db-sqlite1-pool-options-test", "test", 67 | "-db-sqlite-pool-options-discovery-interval", "30000", 68 | "-db-sqlite-pool-options-map.test", "test", 69 | "-db-sqlite-pool-options", "arg1:1,arg2:2", 70 | "-db-aliases-Test6", "t", 71 | "-cache-default", "local://default?bk-count=2", 72 | "-cache-q", "local://queue?bk-test=10", 73 | "-cache-q-options", "count:10,interval:100", 74 | "-cache-q-options-visibilityTimeout", "1000", 75 | "-api-cleanup-rules-aaa", "one:1,two:2", 76 | "-api-cleanup-rules-aaa", "three:3", 77 | "-app-log-inspect-map", "length:222,b:true,s:s%20%3a%2c,ignore:^/test/$", 78 | ]; 79 | 80 | cache._config = {}; 81 | db._config = {}; 82 | 83 | app.parseArgs(argv); 84 | logger.debug("config:", db._config); 85 | 86 | assert.ok(!(!app.workerId && !db._createTables)); 87 | 88 | assert.strictEqual(db.aliases.t, "test6"); 89 | 90 | assert.strictEqual(db._config.sqlite?.max, 10); 91 | 92 | assert.partialDeepStrictEqual(db._config.sqlite.configOptions, { arg1: 1, arg2: 2 }); 93 | 94 | assert.partialDeepStrictEqual(db._config.sqlite1, { url: "a", max: 10, configOptions: { test: "test" } }) 95 | 96 | assert.partialDeepStrictEqual(db._config.sqlite, { configOptions: { discoveryInterval: 30000, 'map.test': "test" } }); 97 | 98 | logger.debug("config:", cache._config); 99 | assert.partialDeepStrictEqual(cache._config.q, { count: 10, interval: 100, visibilityTimeout: 1000 }) 100 | 101 | cache.shutdown(); 102 | cache.initClients(); 103 | var q = cache.getClient(""); 104 | assert.strictEqual(q.options.count, 2) 105 | 106 | app.parseArgs(["-cache-default-options-visibilityTimeout", "99", "-cache-default", "local://default?bk-count=10"]); 107 | 108 | assert.partialDeepStrictEqual(q.options, { visibilityTimeout: 99, count: 10 }); 109 | 110 | app.parseArgs(["-cache-fake-options-visibilityTimeout", "11"]); 111 | 112 | assert.partialDeepStrictEqual(q.options, { visibilityTimeout: 99 }) 113 | 114 | q = cache.getClient("q"); 115 | assert.partialDeepStrictEqual(q.options, { test: 10 }) 116 | 117 | app.parseArgs(["-cache-q-options-visibilityTimeout", "99", "-cache-q-options", "count:99"]); 118 | assert.partialDeepStrictEqual(q.options, { visibilityTimeout: 99, count: 99 }) 119 | 120 | assert.strictEqual(logwatcher.send.error, "a") 121 | assert.partialDeepStrictEqual(logwatcher.matches.error, ["a"]); 122 | assert.partialDeepStrictEqual(logwatcher.files, [{ file: "a", type: "error" }]); 123 | assert.partialDeepStrictEqual(logwatcher.files, [{ file: "b" }]); 124 | 125 | assert.partialDeepStrictEqual(api.cleanupRules.aaa, { one: 1, two: 2, three: 3 }) 126 | 127 | assert.partialDeepStrictEqual(app.logInspect, { length: 222, b: true, s: ["s :"], ignore: /test/ }) 128 | }) 129 | 130 | after(async () => { 131 | await app.astop(); 132 | }) 133 | 134 | }); 135 | -------------------------------------------------------------------------------- /lib/api/acl.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Author: Vlad Seryakov vseryakov@gmail.com 3 | * backendjs 2018 4 | */ 5 | 6 | /** 7 | * @module api/acl 8 | */ 9 | 10 | const lib = require(__dirname + '/../lib'); 11 | const logger = require(__dirname + '/../logger'); 12 | 13 | const mod = { 14 | name: "api.acl", 15 | args: [ 16 | { name: "err-(.+)", descr: "Error messages for various cases" }, 17 | { name: "add-([a-z0-9_]+)", type: "regexpobj", obj: "acl", make: "$1", descr: "Add URLs to the named ACL which can be used in allow/deny rules per role", example: "-api-acl-add-admins ^/admin" }, 18 | { name: "deny-([a-z0-9_]+)", type: "list", obj: "deny", array: 1, sort: 1, descr: "Match all regexps from the specified acls to deny access for the specified role", example: "-api-acl-deny-user admins,billing" }, 19 | { name: "allow-([a-z0-9_]+)", type: "list", obj: "allow", array: 1, sort: 1, descr: "Match all regexps from the specified acls for allow access for the specified role", example: "-api-acl-allow-staff admins,support,-billing" }, 20 | { name: "public", type: "list", array: 1, sort: 1, descr: "Match all regexps from the specified acls for public access", example: "-api-acl-public pub,docs,-intdocs" }, 21 | { name: "anonymous", type: "list", array: 1, sort: 1, descr: "Match all regexps from the specified acls to allow access with or without authentication", example: "-api-acl-anonymous pub,docs" }, 22 | { name: "authenticated", type: "list", array: 1, sort: 1, descr: "Match all regexps from the specified acls to allow access only with authentication any role", example: "-api-acl-authenticated stats,profile" }, 23 | { name: "reset", type: "callback", callback: function(v) { if (v) this.reset() }, descr: "Reset all rules" }, 24 | ], 25 | 26 | allow: {}, 27 | deny: {}, 28 | acl: {}, 29 | 30 | errDeny: "Access denied", 31 | }; 32 | 33 | /** 34 | * ACL for access permissions. Each ACL is a list of RegExps with a name. 35 | * ACLs are grouped by a role, at least one must match in order to succeed. 36 | * 37 | * The `public` ACL exists with default list of files and endpoints to allow access without authentication. 38 | */ 39 | 40 | module.exports = mod; 41 | 42 | const _public = [ 43 | "^/$", 44 | "\\.htm$", "\\.html$", 45 | "\\.ico$", "\\.gif$", "\\.png$", "\\.jpg$", "\\.jpeg$", "\\.svg$", 46 | "\\.ttf$", "\\.eot$", "\\.woff$", "\\.woff2$", 47 | "\\.js$", "\\.css$", 48 | "^/js/", 49 | "^/css/", 50 | "^/img", 51 | "^/webfonts/", 52 | "^/public/", 53 | "^/ping", 54 | ]; 55 | 56 | mod.reset = function() 57 | { 58 | this.acl = { 59 | public: lib.toRegexpObj(null, _public) 60 | }; 61 | this.allow = {}; 62 | this.deny = {}; 63 | this.public = ["public"]; 64 | this.authenticated = this.anonymous = null; 65 | } 66 | mod.reset(); 67 | 68 | // Check the path agains given ACL list, if an ACL starts with `-` it means negative match, the check fails immediately 69 | mod.isMatched = function(path, acls) 70 | { 71 | if (!path || !Array.isArray(acls)) return; 72 | 73 | for (const acl of acls) { 74 | if (typeof acl != "string") continue; 75 | if (lib.testRegexpObj(path, mod.acl[acl[0] == "-" ? acl.substr(1) : acl])) { 76 | return acl[0] != "-"; 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * For the current user check allowed ACLs 83 | * return true if matched 84 | */ 85 | mod.isAllowed = function(req) 86 | { 87 | for (const i in req.user?.roles) { 88 | var p = req.user.roles[i]; 89 | if (this.isMatched(req.options.path, this.allow[p])) { 90 | logger.debug("isAllowed:", this.name, 403, req.options, p, this.allow[p]); 91 | return true; 92 | } 93 | } 94 | logger.debug("isAllowed:", this.name, 403, req.options, "nomatch", req.user?.roles); 95 | } 96 | 97 | /** 98 | * For the current user check not-allowed ACLs 99 | * return true if matched 100 | */ 101 | mod.isDenied = function(req) 102 | { 103 | for (const i in req.user?.roles) { 104 | var p = req.user.roles[i]; 105 | if (this.isMatched(req.options.path, this.deny[p])) { 106 | logger.debug("isDenied:", this.name, 403, req.options, p, this.deny[p]); 107 | return true; 108 | } 109 | } 110 | } 111 | 112 | // Returns true if the current request is allowed for public access 113 | mod.isPublic = function(req, callback) 114 | { 115 | if (this.isMatched(req.options.path, this.public)) return true; 116 | 117 | logger.debug("isPublic:", this.name, 403, req.options, this.public); 118 | } 119 | 120 | // Returns true if the current request is must be authenticated 121 | mod.isAuthenticated = function(req) 122 | { 123 | if (req.user?.id && this.isMatched(req.options.path, this.authenticated)) return true; 124 | 125 | logger.debug("isAuthenticated:", this.name, 403, req.options, this.authenticated); 126 | } 127 | 128 | // Returns true if the current request is allowed for public or authenticated access 129 | mod.isAnonymous = function(req) 130 | { 131 | if (this.isMatched(req.options.path, this.anonymous)) return true; 132 | 133 | logger.debug("isAnonymous:", this.name, 403, req.options, this.anonymous); 134 | } 135 | 136 | -------------------------------------------------------------------------------- /web/js/app-ws.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * alpinejs-app client 3 | * Vlad Seryakov vseryakov@gmail.com 2018 4 | */ 5 | 6 | (() => { 7 | var app = window.app; 8 | 9 | class WS { 10 | path = "/" 11 | query = null 12 | retry_timeout = 500 13 | retry_factor = 2 14 | max_timeout = 30000 15 | max_retries = Infinity 16 | max_pending = 10 17 | ping_interval = 300000 18 | _retries = 0 19 | _pending = [] 20 | 21 | // Open a new websocket connection 22 | connect(options) 23 | { 24 | if (this._timer) { 25 | clearTimeout(this._timer); 26 | delete this._timer; 27 | } 28 | if (this.disabled) return; 29 | 30 | for (const p in options) this[p] = options[p]; 31 | var host = this.host || window.location.hostname; 32 | 33 | if (navigator.onLine === false && !/^(localhost|127.0.0.1)$/.test(host)) { 34 | return this.timer(0); 35 | } 36 | 37 | if (!this.query) this.query = {}; 38 | for (const p in this.headers) if (this.query[p] === undefined) this.query[p] = this.headers[p]; 39 | 40 | var port = this.port || window.location.port; 41 | var proto = this.protocol || window.location.protocol.replace("http", "ws"); 42 | var url = `${proto}//${host}:${port}${this.path}?${this.query ? new URLSearchParams(this.query).toString() : ""}`; 43 | 44 | var ws = this.ws = new WebSocket(url); 45 | ws.onopen = () => { 46 | if (this.debug) app.log("ws.open:", url); 47 | app.emit("ws:open", url); 48 | this._ctime = Date.now(); 49 | this._timeout = this.retry_timeout; 50 | this._retries = 0; 51 | while (this._pending.length) { 52 | this.send(this.pending.shift()); 53 | } 54 | this.ping(); 55 | } 56 | ws.onclose = () => { 57 | if (this.debug) app.log("ws.closed:", url, this._timeout, this._retries); 58 | this.ws = null; 59 | app.emit("ws:close", url); 60 | if (++this._retries < this.max_retries) this.timer(); 61 | } 62 | ws.onmessage = (msg) => { 63 | var data = msg.data; 64 | if (data === "bye") return this.close(1); 65 | if (typeof data == "string" && (data[0] == "{" || data[0] == "[")) data = JSON.parse(data); 66 | if (this.debug) app.log('ws.message:', data); 67 | app.emit("ws:message", data); 68 | } 69 | ws.onerror = (err) => { 70 | if (this.debug) app.log('ws.error:', url, err); 71 | } 72 | } 73 | 74 | // Restart websocket reconnect timer, increase timeout according to reconnect policy (retry_factor, max_timeout) 75 | timer(timeout) 76 | { 77 | clearTimeout(this._timer); 78 | if (this.disabled) return; 79 | if (typeof timeout == "number") this._timeout = timeout; 80 | this._timer = setTimeout(this.connect.bind(this), this._timeout); 81 | this._timeout *= this._timeout == this.max_timeout ? 0 : this.retry_factor; 82 | this._timeout = app.util.toClamp(this._timeout, this.retry_timeout, this.max_timeout); 83 | } 84 | 85 | // Send a ping and shcedule next one 86 | ping() 87 | { 88 | clearTimeout(this._ping); 89 | if (this.disabled || !this.ping_interval) return; 90 | if (app.ws?.readyState === WebSocket.OPEN) { 91 | app.ws.send(this.ping_path || "/ping"); 92 | } 93 | this._ping = setTimeout(this.ping.bind(this), this.ping_interval); 94 | } 95 | 96 | // Closes and possibly disables WS connection, to reconnect again must delete .disabled property 97 | close(disable) 98 | { 99 | this.disabled = disable; 100 | if (this.ws) { 101 | this.ws.close(); 102 | delete this.ws; 103 | } 104 | } 105 | 106 | // Send a string data or an object in jQuery ajax format { url:.., data:.. } or as an object to be stringified 107 | send(data) 108 | { 109 | if (this.ws?.readyState != WebSocket.OPEN) { 110 | if (!this.max_pending || this._pending.length < this.max_pending) { 111 | this._pending.push(data); 112 | } 113 | return; 114 | } 115 | if (app.isO(data)) { 116 | if (data.url && data.url[0] == "/") { 117 | data = data.url; 118 | if (app.isO(data.data)) { 119 | data += "?" + new URLSearchParams(data.data).toString(); 120 | } 121 | } else { 122 | data = JSON.stringified(data); 123 | } 124 | } 125 | this.send(data); 126 | } 127 | 128 | // Check the status of websocket connection, reconnect if needed 129 | online() 130 | { 131 | if (this.debug) app.log('ws.online:', navigator.onLine, this.ws?.readyState, this.path, this._ctime); 132 | if (this.ws?.readyState !== WebSocket.OPEN && this._ctime) { 133 | this.connect(); 134 | } 135 | } 136 | } 137 | 138 | app.ws = new WS(); 139 | 140 | app.$ready(() => { 141 | app.$on(window, "online", app.ws.online.bind(app.ws)); 142 | }); 143 | 144 | 145 | })(); 146 | 147 | 148 | --------------------------------------------------------------------------------