From 8a2bd6ecc377d36c52e3a9d94f004976274f3b0b Mon Sep 17 00:00:00 2001 From: Enstrayed <48845980+Enstrayed@users.noreply.github.com> Date: Wed, 16 Apr 2025 19:50:12 -0700 Subject: [PATCH 1/3] update index & auth to use postgres --- index.js | 22 ++++++++++------------ liberals/auth.js | 33 +++++++++++---------------------- package-lock.json | 40 +++++++++++++++++++++++++++++----------- package.json | 3 ++- 4 files changed, 52 insertions(+), 46 deletions(-) diff --git a/index.js b/index.js index e0b2a20..dd4d492 100644 --- a/index.js +++ b/index.js @@ -1,28 +1,26 @@ import * as fs from 'fs' import { execSync } from 'child_process' +import postgres from 'postgres' import express, { json } from 'express' const app = express() -if (!process.env.API_DBHOST || !process.env.API_DBCRED) { - console.log("FATAL: API_DBHOST and API_DBCRED must be set") +if (!process.env.DATABASE_URI) { + console.log("FATAL: DATABASE_URI must be set") process.exit(1) } -const globalConfig = await fetch(`${process.env.API_DBHOST}/config/${process.env.API_DBCRED.split(":")[0]}`,{ - headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -}).then(response => { - if (response.status !== 200) { - console.log(`FATAL: Failed to download configuration: ${response.status} ${response.statusText}`) - process.exit(1) - } else { - return response.json() - } +const db = postgres(process.env.DATABASE_URI) + +const globalConfig = await db`select content from config where id = ${process.env.CONFIG_OVERRIDE ?? 'production'}`.then(response => {return response[0]["content"]}).catch(error => { + console.log(`FATAL: Error occured in downloading configuration: ${error}`) + process.exit(1) }) + const globalVersion = execSync(`git show --oneline -s`).toString().split(" ")[0] // Returns ISO 8601 Date & 24hr time for UTC-7/PDT const startTime = new Date(new Date().getTime() - 25200000).toISOString().slice(0,19).replace('T',' ') -export { app, fs, globalConfig, globalVersion } +export { app, fs, db, globalConfig, globalVersion } app.use(json()) // Allows receiving JSON bodies // see important note: https://expressjs.com/en/api.html#express.json diff --git a/liberals/auth.js b/liberals/auth.js index 171db72..e7ac0ca 100644 --- a/liberals/auth.js +++ b/liberals/auth.js @@ -1,31 +1,20 @@ -import { globalConfig } from "../index.js" +import { globalConfig, db } from "../index.js" /** - * Checks if a token exists in the sessions file (authentication) and if it has the correct permissions (authorization) + * (DEPRECATED) Checks if a token exists in the sessions file (authentication) and if it has the correct permissions (authorization) * @param {string} token Token as received by client * @param {string} scope Scope the token will need to have in order to succeed - * @returns True for successful authentication and authorization, false if either fail + * @returns {boolean} True for successful authentication and authorization, false if either fail */ async function checkToken(token,scope) { - return await fetch(`${process.env.API_DBHOST}/auth/sessions`, { - headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} - }).then(fetchRes => { - - return fetchRes.json().then(dbRes => { - - if (dbRes.sessions[token] == undefined) { // If the token is not on the sessions list then reject - return false - } else if (dbRes.sessions[token].scopes.includes(scope)) { // If the token is on the seesions list and includes the scope then accept - return true - } else { // Otherwise reject - return false - } - - }) - - }).catch(error => { - console.log(`ERROR: auth.js: Fetch failed: ${error}`) - return false + return await db`select s.token, s.scopes, s.expires, u.username from sessions s join users u on s.owner = u.id where s.token = ${token}`.then(response => { + if (response.length === 0) { + return false + } else if (response[0]?.scopes.split(",").includes(scope)) { + return true + } else { + return false + } }) } diff --git a/package-lock.json b/package-lock.json index 0088f1a..6374e23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "express": "^4.18.2", "marked": "^14.1.3", - "nodemailer": "^6.9.15" + "nodemailer": "^6.9.15", + "postgres": "^3.4.5" }, "devDependencies": { "@types/bun": "^1.0.12", @@ -259,9 +260,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -283,7 +284,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -298,6 +299,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler": { @@ -466,9 +471,9 @@ } }, "node_modules/marked": { - "version": "14.1.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.3.tgz", - "integrity": "sha512-ZibJqTULGlt9g5k4VMARAktMAjXoVnnr+Y3aCqW1oDftcV4BA3UmrBifzXoZyenHRk75csiPu9iwsTj4VNBT0g==", + "version": "14.1.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", + "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", "license": "MIT", "bin": { "marked": "bin/marked.js" @@ -591,11 +596,24 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, + "node_modules/postgres": { + "version": "3.4.5", + "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz", + "integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/package.json b/package.json index 8f9de25..ee5577d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "dependencies": { "express": "^4.18.2", "marked": "^14.1.3", - "nodemailer": "^6.9.15" + "nodemailer": "^6.9.15", + "postgres": "^3.4.5" }, "name": "enstrayedapi", "version": "1.0.0", -- 2.49.1 From 4c0f140e9edfd28b71587152ae8ea9b0777ec8a0 Mon Sep 17 00:00:00 2001 From: Enstrayed <48845980+Enstrayed@users.noreply.github.com> Date: Thu, 17 Apr 2025 22:41:08 -0700 Subject: [PATCH 2/3] working changes --- liberals/auth.js | 13 ++++ routes/debug.js | 15 ++++ routes/etyd.js | 197 +++++++++++++++++++++++------------------------ todo.md | 5 ++ 4 files changed, 128 insertions(+), 102 deletions(-) create mode 100644 routes/debug.js create mode 100644 todo.md diff --git a/liberals/auth.js b/liberals/auth.js index e7ac0ca..eb60dc8 100644 --- a/liberals/auth.js +++ b/liberals/auth.js @@ -18,4 +18,17 @@ async function checkToken(token,scope) { }) } +/** + * New function to check if a token exists in the sessions table (authentication) and if it has the desired scope (authorization) + * @param {string} token Token as received by client + * @param {string} scope Desired scope for action + * @typedef {Object} Object containing the result and the username of the token owner + * @property {boolean} result Boolean result of if the check passed + * @property {string} owner Username of the token owner + */ + +async function checkTokenNew(token,scope) { + +} + export {checkToken} \ No newline at end of file diff --git a/routes/debug.js b/routes/debug.js new file mode 100644 index 0000000..c155ad5 --- /dev/null +++ b/routes/debug.js @@ -0,0 +1,15 @@ +import { app, globalConfig } from "../index.js" // Get globals from index +import { checkToken } from "../liberals/auth.js" +import { logRequest } from "../liberals/logging.js" + +app.get("/api/debugtokencheck", (rreq,rres) => { + checkToken(rreq.get("Authorization"),"etyd").then(authRes => { + if (authRes) { + rres.sendStatus(200) + } else { + rres.sendStatus(401) + } + }) +}) + +export { app } \ No newline at end of file diff --git a/routes/etyd.js b/routes/etyd.js index f185745..653a4ea 100644 --- a/routes/etyd.js +++ b/routes/etyd.js @@ -1,130 +1,123 @@ -import { app, globalConfig } from "../index.js" // Get globals from index +import { app, db, globalConfig } from "../index.js" // Get globals from index import { checkToken } from "../liberals/auth.js" import { logRequest } from "../liberals/logging.js" app.get("/api/etyd*", (rreq,rres) => { - fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd","")}`,{ - headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} - }).then(dbRes => { - if (dbRes.status == 404) { - rres.sendStatus(404) + let userRequest = rreq.path.replace("/api/etyd/","") + db`select content from etyd where url = ${userRequest}`.then(response => { + if (response.length == 0) { + rres.status(404).send(`etyd.cc: URL "${userRequest}" was not found`) } else { - dbRes.json().then(dbRes => { - try { - rres.redirect(dbRes.content.url) // Node will crash if the Database entry is malformed - } catch (responseError) { - logRequest(rres,rreq,500,responseError) - rres.sendStatus(500) - } - }) + rres.redirect(response[0].content) } - }).catch(fetchError => { - logRequest(rres,rreq,500,fetchError) - rres.sendStatus(500) + }).catch(dbError => { + logRequest(rres,rreq,500,dbError) + rres.status(500).send(`etyd.cc: An internal error occured`) }) }) -// app.delete("/api/etyd*", (rreq,rres) => { +app.delete("/api/etyd*", (rreq,rres) => { -// if (rreq.get("Authorization") === undefined) { -// rres.sendStatus(400) -// } else { -// checkToken(rreq.get("Authorization"),"etyd").then(authRes => { -// if (authRes === false) { -// rres.sendStatus(401) -// } else if (authRes === true) { // Authorization successful + if (rreq.get("Authorization") === undefined) { + rres.sendStatus(400) + } else { + checkToken(rreq.get("Authorization"),"etyd").then(authRes => { + if (authRes === false) { + rres.sendStatus(401) + } else if (authRes === true) { // Authorization successful -// fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", "")}`,{ -// headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -// }).then(dbRes => { + fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", "")}`,{ + headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} + }).then(dbRes => { -// if (dbRes.status == 404) { -// rres.sendStatus(404) // Entry does not exist -// } else { -// dbRes.json().then(dbRes => { + if (dbRes.status == 404) { + rres.sendStatus(404) // Entry does not exist + } else { + dbRes.json().then(dbRes => { -// fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", ""),{ -// headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -// }}`, { -// method: "DELETE", -// headers: { -// "If-Match": dbRes["_rev"] // Using the If-Match header is easiest for deleting entries in couchdb -// } -// }).then(fetchRes => { -// if (fetchRes.status == 200) { -// // console.log(`${rres.get("cf-connecting-ip")} DELETE ${rreq.path} returned 200 KEY: ${rreq.get("Authorization")}`) -// logRequest(rres,rreq,200) -// rres.sendStatus(200) -// } -// }).catch(fetchError => { -// // console.log(`${rres.get("cf-connecting-ip")} DELETE ${rreq.path} returned 500: ${fetchError}`) -// logRequest(rres,rreq,500,fetchError) -// rres.sendStatus(500) -// }) + fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", "")}`,{ + headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`}, + method: "DELETE", + headers: { + "If-Match": dbRes["_rev"] // Using the If-Match header is easiest for deleting entries in couchdb + } + }).then(fetchRes => { + + if (fetchRes.status == 200) { + // console.log(`${rres.get("cf-connecting-ip")} DELETE ${rreq.path} returned 200 KEY: ${rreq.get("Authorization")}`) + logRequest(rres,rreq,200) + rres.sendStatus(200) + } else { + rres.send(`Received status ${fetchRes.status}`) + } + }).catch(fetchError => { + // console.log(`${rres.get("cf-connecting-ip")} DELETE ${rreq.path} returned 500: ${fetchError}`) + logRequest(rres,rreq,500,fetchError) + rres.sendStatus(500) + }) -// }) -// } + }) + } -// }).catch(fetchError => { -// logRequest(rres,rreq,500,fetchError) -// rres.sendStatus(500) -// }) + }).catch(fetchError => { + logRequest(rres,rreq,500,fetchError) + rres.sendStatus(500) + }) -// } -// }) -// } + } + }) + } -// }) +}) -// app.post("/api/etyd*", (rreq,rres) => { +app.post("/api/etyd*", (rreq,rres) => { -// if (rreq.get("Authorization") === undefined) { -// rres.sendStatus(400) -// } else { -// checkToken(rreq.get("Authorization"),"etyd").then(authRes => { -// if (authRes === false) { -// rres.sendStatus(401) -// } else if (authRes === true) { // Authorization successful + if (rreq.get("Authorization") === undefined) { + rres.sendStatus(400) + } else { + checkToken(rreq.get("Authorization"),"etyd").then(authRes => { + if (authRes === false) { + rres.sendStatus(401) + } else if (authRes === true) { // Authorization successful -// if (rreq.body["url"] == undefined) { -// rres.sendStatus(400) -// } else { -// fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", ""),{ -// headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`} -// }}`, { -// method: "PUT", -// body: JSON.stringify({ -// "content": { -// "url": rreq.body["url"] -// } -// }) -// }).then(dbRes => { + if (rreq.body["url"] == undefined) { + rres.sendStatus(400) + } else { + fetch(`${process.env.API_DBHOST}/etyd${rreq.path.replace("/api/etyd", "")}`,{ + headers: { "Authorization": `Basic ${btoa(process.env.API_DBCRED)}`}, + method: "PUT", + body: JSON.stringify({ + "content": { + "url": rreq.body["url"] + } + }) + }).then(dbRes => { -// switch(dbRes.status) { -// case 409: -// rres.sendStatus(409) -// break; + switch(dbRes.status) { + case 409: + rres.sendStatus(409) + break; -// case 201: -// rres.status(200).send(rreq.path.replace("/api/etyd", "")) -// break; + case 201: + rres.status(200).send(rreq.path.replace("/api/etyd", "")) + break; -// default: -// logRequest(rres,rreq,500,`CouchDB PUT did not return expected code: ${dbRes.status} ${dbRes.statusText}`) -// rres.sendStatus(500) -// break; -// } + default: + logRequest(rres,rreq,500,`CouchDB PUT did not return expected code: ${dbRes.status} ${dbRes.statusText}`) + rres.sendStatus(500) + break; + } -// }).catch(fetchError => { -// logRequest(rres,rreq,500,fetchError) -// rres.sendStatus(500) -// }) -// } + }).catch(fetchError => { + logRequest(rres,rreq,500,fetchError) + rres.sendStatus(500) + }) + } -// } -// }) -// } + } + }) + } -// }) +}) export {app} // export routes to be imported by index for execution \ No newline at end of file diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..71f8b01 --- /dev/null +++ b/todo.md @@ -0,0 +1,5 @@ +- [ ] GET /api/whoami - Returns owner of token and what scopes it has +- [ ] GET /api/login - OIDC login redirect to ECLS +- [ ] GET /api/callback - Creates new token that is intended to be local to browser; e.g. can be used in turn to make longer lasting more specific tokens +- [ ] POST /api/token - Allows owner to create a new token with customized scopes, comments & expiration date +- [ ] DELETE /api/token - Invalidate a token \ No newline at end of file -- 2.49.1 From 71ffa47fa83aecaec2462f0fa591cc637decdb05 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 17 Apr 2025 23:29:30 -0700 Subject: [PATCH 3/3] run npm audit fix (???) --- package-lock.json | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0088f1a..c53f465 100644 --- a/package-lock.json +++ b/package-lock.json @@ -259,9 +259,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -283,7 +283,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -298,6 +298,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler": { @@ -591,9 +595,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, "node_modules/proxy-addr": { -- 2.49.1