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 01/21] 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",
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 02/21] 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
From b917800eece394ad71f0b5f331b4a609200b9f50 Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Fri, 18 Apr 2025 13:16:49 -0700
Subject: [PATCH 03/21] implement new token checking function and modify
email.js to use it
---
liberals/auth.js | 12 ++++++++++--
liberals/logging.js | 5 +++--
routes/email.js | 14 +++++++-------
todo.md | 4 +++-
4 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/liberals/auth.js b/liberals/auth.js
index eb60dc8..6348230 100644
--- a/liberals/auth.js
+++ b/liberals/auth.js
@@ -28,7 +28,15 @@ async function checkToken(token,scope) {
*/
async function checkTokenNew(token,scope) {
-
+ 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 { result: false, owner: response[0]?.username}
+ } else if (response[0]?.scopes.split(",").includes(scope)) {
+ return { result: true, owner: response[0]?.username}
+ } else {
+ return { result: false, owner: response[0]?.username}
+ }
+ })
}
-export {checkToken}
\ No newline at end of file
+export {checkToken, checkTokenNew}
\ No newline at end of file
diff --git a/liberals/logging.js b/liberals/logging.js
index 3586855..ecdb8e2 100644
--- a/liberals/logging.js
+++ b/liberals/logging.js
@@ -4,9 +4,10 @@
* @param {object} request Parent request object
* @param {number} code Status code to log, should be same as sent to client
* @param {string} extra Optional extra details to add to log, ideal for caught errors
+ * @param {object} authresponse Optionally include result of auth response to include owner information for a token
*/
-function logRequest(response,request,code,extra) {
- console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`)
+function logRequest(response,request,code,extra,authresponse) {
+ console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${authresponse.owner ?? ""}/${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`)
}
export { logRequest }
\ No newline at end of file
diff --git a/routes/email.js b/routes/email.js
index 4119776..3d916ca 100644
--- a/routes/email.js
+++ b/routes/email.js
@@ -1,5 +1,5 @@
import { app, globalConfig } from "../index.js" // Get globals from index
-import { checkToken } from "../liberals/auth.js"
+import { checkTokenNew } from "../liberals/auth.js"
import { logRequest } from "../liberals/logging.js"
import * as nodemailer from 'nodemailer'
@@ -14,10 +14,10 @@ const transporter = nodemailer.createTransport({
})
app.post("/api/sendemail", (rreq,rres) => {
- checkToken(rreq.get("Authorization"),"email").then(authRes => {
- if (authRes === false) {
+ checkTokenNew(rreq.get("Authorization"),"email").then(authRes => {
+ if (authRes.result === false) {
rres.sendStatus(401)
- } else if (authRes === true) {
+ } else if (authRes.result === true) {
if (rreq.body == undefined || rreq.body.recipient == undefined) { // 2024-05-11: Turbo bodge check to make sure request JSON is valid, probably wont work but whatever
rres.sendStatus(400)
} else {
@@ -29,14 +29,14 @@ app.post("/api/sendemail", (rreq,rres) => {
text: rreq.body.message ?? "Message Not Set"
}).then(transportResponse => {
if (transportResponse.response.slice(0,1) === "2") {
- logRequest(rres,rreq,200,transportResponse.response)
+ logRequest(rres,rreq,200,transportResponse.response,authRes)
rres.status(200).send(transportResponse.response)
} else {
- logRequest(rres,rreq,400,transportResponse.response)
+ logRequest(rres,rreq,400,transportResponse.response,authRes)
rres.status(400).send(transportResponse.response)
}
}).catch(transportError => {
- logRequest(rres,rreq,500,transportError)
+ logRequest(rres,rreq,500,transportError,authRes)
rres.sendStatus(500)
})
diff --git a/todo.md b/todo.md
index 71f8b01..27281b2 100644
--- a/todo.md
+++ b/todo.md
@@ -2,4 +2,6 @@
- [ ] 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
+- [ ] DELETE /api/token - Invalidate a token
+- [ ] liberals/libnowplaying - Implement queryCider()
+- [ ] routes/nowplaying - Reimplement query order to Cider and then Jellyfin
From 9f96d82e53d34ba191b68c312848455f5c0332d8 Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Fri, 18 Apr 2025 22:36:15 -0700
Subject: [PATCH 04/21] auth modification
---
liberals/auth.js | 9 +++++----
routes/auth.js | 25 +++++++++++++++++++++++++
2 files changed, 30 insertions(+), 4 deletions(-)
create mode 100644 routes/auth.js
diff --git a/liberals/auth.js b/liberals/auth.js
index 6348230..0db2b29 100644
--- a/liberals/auth.js
+++ b/liberals/auth.js
@@ -25,16 +25,17 @@ async function checkToken(token,scope) {
* @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
+ * @property {number} ownerId Database ID of the token owner
*/
async function checkTokenNew(token,scope) {
- 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 => {
+ return await db`select s.*, u.username from sessions s join users u on s.owner = u.id where s.token = ${token}`.then(response => {
if (response.length === 0) {
- return { result: false, owner: response[0]?.username}
+ return { result: false, owner: response[0]?.username, ownerId: response[0]?.owner}
} else if (response[0]?.scopes.split(",").includes(scope)) {
- return { result: true, owner: response[0]?.username}
+ return { result: true, owner: response[0]?.username, ownerId: response[0]?.owner}
} else {
- return { result: false, owner: response[0]?.username}
+ return { result: false, owner: response[0]?.username, ownerId: response[0]?.owner}
}
})
}
diff --git a/routes/auth.js b/routes/auth.js
new file mode 100644
index 0000000..fce1deb
--- /dev/null
+++ b/routes/auth.js
@@ -0,0 +1,25 @@
+import { app, db, globalConfig } from "../index.js" // Get globals from index
+import { checkTokenNew } from "../liberals/auth.js"
+import { logRequest } from "../liberals/logging.js"
+
+app.get("/api/auth/whoami", (rreq,rres) => {
+ rres.send("Non functional endpoint")
+})
+
+app.get("/api/auth/login", (rreq,rres) => {
+ rres.send("Non functional endpoint")
+})
+
+app.get("/api/auth/callback", (rreq,rres) => {
+ rres.send("Non functional endpoint")
+})
+
+app.post("/api/auth/token", (rreq,rres) => {
+ rres.send("Non functional endpoint")
+})
+
+app.delete("/api/auth/token", (rreq,rres) => {
+ rres.send("Non functional endpoint")
+})
+
+export { app }
\ No newline at end of file
From 74cf834699084844577d61276d384114494edf00 Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Sun, 20 Apr 2025 13:52:49 -0700
Subject: [PATCH 05/21] fix problem with logging & add misc functions
---
liberals/logging.js | 2 +-
liberals/misc.js | 23 +++++++++++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
create mode 100644 liberals/misc.js
diff --git a/liberals/logging.js b/liberals/logging.js
index ecdb8e2..ebbe529 100644
--- a/liberals/logging.js
+++ b/liberals/logging.js
@@ -7,7 +7,7 @@
* @param {object} authresponse Optionally include result of auth response to include owner information for a token
*/
function logRequest(response,request,code,extra,authresponse) {
- console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${authresponse.owner ?? ""}/${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`)
+ console.log(`${request.get("cf-connecting-ip") ?? request.ip} ${authresponse?.owner ?? ""}/${request.get("Authorization") ?? ""} ${request.method} ${request.path} returned ${code} ${extra ?? ""}`)
}
export { logRequest }
\ No newline at end of file
diff --git a/liberals/misc.js b/liberals/misc.js
new file mode 100644
index 0000000..b12a8f6
--- /dev/null
+++ b/liberals/misc.js
@@ -0,0 +1,23 @@
+function randomStringBase16(length) {
+ let characters = "0123456789abcdef"
+ let remaining = length
+ let returnstring = ""
+ while (remaining > 0) {
+ returnstring = returnstring + characters.charAt(Math.floor(Math.random() * characters.length))
+ remaining = remaining - 1
+ }
+ return returnstring
+}
+
+function randomStringBase62(length) {
+ let characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJIJKLMNOPQRSTUVWXYZ"
+ let remaining = length
+ let returnstring = ""
+ while (remaining > 0) {
+ returnstring = returnstring + characters.charAt(Math.floor(Math.random() * characters.length))
+ remaining = remaining - 1
+ }
+ return returnstring
+}
+
+export { randomStringBase16, randomStringBase62 }
\ No newline at end of file
From d07d75c994415321b868703915f28cb257bbdc5a Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Sun, 20 Apr 2025 18:30:29 -0700
Subject: [PATCH 06/21] main groundwork for oidc handling
---
routes/auth.js | 34 ++++++++++++++++++++++++++++++++--
routes/debug.js | 4 ++++
2 files changed, 36 insertions(+), 2 deletions(-)
diff --git a/routes/auth.js b/routes/auth.js
index fce1deb..b78b00e 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -7,11 +7,41 @@ app.get("/api/auth/whoami", (rreq,rres) => {
})
app.get("/api/auth/login", (rreq,rres) => {
- rres.send("Non functional endpoint")
+ rres.redirect(`${globalConfig.oidc.authorizeUrl}?client_id=${globalConfig.oidc.clientId}&response_type=code&scope=openid enstrayedapi&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback`)
})
app.get("/api/auth/callback", (rreq,rres) => {
- rres.send("Non functional endpoint")
+ fetch(globalConfig.oidc.tokenUrl, { // Call token endpoint at IdP using code provdided during callback
+ method: "POST",
+ headers: { "Content-Type": "application/x-www-form-urlencoded"},
+ body: `grant_type=authorization_code&code=${rreq.query.code}&redirect_uri=${rreq.protocol}://${rreq.get("Host")}/api/auth/callback&client_id=${globalConfig.oidc.clientId}&client_secret=${globalConfig.oidc.clientSecret}`
+ }).then(fetchRes1 => {
+ fetchRes1.json().then(fetchRes1 => { // Convert response to JSON then continue
+ if (fetchRes1.error) { // Fetch to token endpoint succeded but resulted in error, usually because the provided code is invalid
+ logRequest(rres,rreq,500,`Callback-Token-${fetchRes1.error}`)
+ rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Token-${fetchRes1.error}`)
+ } else { // Assumed success
+ fetch(globalConfig.oidc.userinfoUrl, { // Call userinfo endpoint at IdP using token provided during previous step
+ headers: { "Authorization": `Bearer ${fetchRes1.access_token}`}
+ }).then(fetchRes2 => {
+ if (fetchRes2.ok === false) { // Fetch to userinfo endpoint succeded but resulted in error (usually 401)
+ logRequest(rres,rreq,500,`Callback-Userinfo-${fetchRes2.status}`)
+ rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Userinfo-${fetchRes2.status}`)
+ } else {
+ fetchRes2.json().then(fetchRes2 => {
+ rres.send(fetchRes2)
+ })
+ }
+ }).catch(fetchErr2 => { // Fetch to userinfo endpoint failed for some other reason
+ logRequest(rres,rreq,500,`Callback-Fetch2-${fetchErr2}`)
+ rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Fetch2-${fetchErr2}`)
+ })
+ }
+ })
+ }).catch(fetchErr1 => { // Fetch to token endpoint failed for some other reason
+ logRequest(rres,rreq,500,`Callback-Fetch1-${fetchErr1}`)
+ rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Fetch1-${fetchErr1}`)
+ })
})
app.post("/api/auth/token", (rreq,rres) => {
diff --git a/routes/debug.js b/routes/debug.js
index c155ad5..c5c350e 100644
--- a/routes/debug.js
+++ b/routes/debug.js
@@ -12,4 +12,8 @@ app.get("/api/debugtokencheck", (rreq,rres) => {
})
})
+app.get("/api/debugurl", (rreq,rres) => {
+ rres.send(`${rreq.protocol}://${rreq.get("Host")}`)
+})
+
export { app }
\ No newline at end of file
From c868c9bc09821966baa69e1aa0ba31192559202a Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Sun, 20 Apr 2025 20:15:02 -0700
Subject: [PATCH 07/21] groundwork for writing new tokens to db
---
liberals/misc.js | 34 +++++++++++++++++++++++++++++++++-
routes/auth.js | 27 ++++++++++++++++++---------
2 files changed, 51 insertions(+), 10 deletions(-)
diff --git a/liberals/misc.js b/liberals/misc.js
index b12a8f6..5fafd90 100644
--- a/liberals/misc.js
+++ b/liberals/misc.js
@@ -20,4 +20,36 @@ function randomStringBase62(length) {
return returnstring
}
-export { randomStringBase16, randomStringBase62 }
\ No newline at end of file
+function getHumanReadableUserAgent(useragent) {
+ let formattedua = useragent.replace(/[\/()]/g," ").split(" ")
+ let os = ""
+ let browser = ""
+
+ if (formattedua.includes("Windows")) {
+ os += "Windows"
+ } else if (formattedua.includes("Macintosh")) {
+ os += "macOS"
+ } else if (formattedua.includes("iPhone")) {
+ os += "iOS"
+ } else if (formattedua.includes("Android")) {
+ os += "Android"
+ } else if (formattedua.includes("Linux")) {
+ os += "Linux"
+ } else {
+ os += "Other"
+ }
+
+ if (formattedua.includes("Firefox")) {
+ browser += "Firefox"
+ } else if (formattedua.includes("Chrome")) {
+ browser += "Chrome"
+ } else if (formattedua.includes("Safari")) {
+ browser += "Safari"
+ } else {
+ browser += "Other"
+ }
+
+ return `${os} ${browser}`
+}
+
+export { randomStringBase16, randomStringBase62, getHumanReadableUserAgent }
\ No newline at end of file
diff --git a/routes/auth.js b/routes/auth.js
index b78b00e..cec9dc7 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -1,6 +1,7 @@
import { app, db, globalConfig } from "../index.js" // Get globals from index
import { checkTokenNew } from "../liberals/auth.js"
import { logRequest } from "../liberals/logging.js"
+import { randomStringBase62, getHumanReadableUserAgent } from "../liberals/misc.js"
app.get("/api/auth/whoami", (rreq,rres) => {
rres.send("Non functional endpoint")
@@ -18,30 +19,38 @@ app.get("/api/auth/callback", (rreq,rres) => {
}).then(fetchRes1 => {
fetchRes1.json().then(fetchRes1 => { // Convert response to JSON then continue
if (fetchRes1.error) { // Fetch to token endpoint succeded but resulted in error, usually because the provided code is invalid
- logRequest(rres,rreq,500,`Callback-Token-${fetchRes1.error}`)
- rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Token-${fetchRes1.error}`)
+ localError500(`Callback-Token-${fetchRes1.error}`)
} else { // Assumed success
fetch(globalConfig.oidc.userinfoUrl, { // Call userinfo endpoint at IdP using token provided during previous step
headers: { "Authorization": `Bearer ${fetchRes1.access_token}`}
}).then(fetchRes2 => {
if (fetchRes2.ok === false) { // Fetch to userinfo endpoint succeded but resulted in error (usually 401)
- logRequest(rres,rreq,500,`Callback-Userinfo-${fetchRes2.status}`)
- rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Userinfo-${fetchRes2.status}`)
+ localError500(`Callback-Userinfo-${fetchRes2.status}`)
} else {
fetchRes2.json().then(fetchRes2 => {
- rres.send(fetchRes2)
+ let newToken = randomStringBase62(64)
+ let newExpiration = Date.now() + 86400
+ let newComment = `Login token for ${getHumanReadableUserAgent(rreq.get("User-Agent"))} on ${rreq.get("cf-connecting-ip") ?? rreq.ip}`
+ db`select * from users where oidc_username = ${fetchRes2.username};`.then(dbRes1 => {
+ db`insert into sessions (token,owner,scopes,expires,comment) values (${newToken},${dbRes1[0]?.id},${fetchRes2.enstrayedapi_scopes},${newExpiration},${newComment})`.then(dbRes2 => {
+ rres.send(dbRes2)
+ })
+ })
})
}
}).catch(fetchErr2 => { // Fetch to userinfo endpoint failed for some other reason
- logRequest(rres,rreq,500,`Callback-Fetch2-${fetchErr2}`)
- rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Fetch2-${fetchErr2}`)
+ localError500(`Callback-Fetch2-${fetchErr2}`)
})
}
})
}).catch(fetchErr1 => { // Fetch to token endpoint failed for some other reason
- logRequest(rres,rreq,500,`Callback-Fetch1-${fetchErr1}`)
- rres.status(500).send(`An error occured during login, a token was not created.
500 Callback-Fetch1-${fetchErr1}`)
+ localError500(`Callback-Fetch-${fetchErr1}`)
})
+
+ function localError500(code) {
+ logRequest(rres,rreq,500,code)
+ rres.status(500).send(`An error occured during login, a token was not created.
500 ${code}`)
+ }
})
app.post("/api/auth/token", (rreq,rres) => {
From 2ad8d04c9961922a21d43ffb74d26cdf6e16b13d Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Tue, 22 Apr 2025 09:22:54 -0700
Subject: [PATCH 08/21] readme will need rewritten later
---
README.md | 53 ++---------------------------------------------------
1 file changed, 2 insertions(+), 51 deletions(-)
diff --git a/README.md b/README.md
index f3b9c36..903d3f7 100644
--- a/README.md
+++ b/README.md
@@ -8,49 +8,12 @@ This file contains documentation relevant for development and deployment, but no
If you would like to report a bug or security issue, please open a GitHub issue. If you are the operator of a service this application accesses, use the contact information provided during registration with your service to contact me directly.
## Configuration
-The configuration is downloaded from CouchDB on startup, however two environment variables must be set to specify the URL of the CouchDB server and the credentials for accessing it:
-| Variable | Required? | Purpose |
-|--------------|----------------------|-----------------------------------------------------------------------------------------------------|
-| `API_PORT` | No, defaults to 8081 | Sets the port the server will listen on |
-| `API_DBHOST` | Yes | Complete URL of the CouchDB instance, including port and protocol |
-| `API_DBCRED` | Yes | Credentials to access the CouchDB instance, in Basic Authentication format e.g. `username:password` |
-Configuration Example
-* `frontpage.directory`: Directory of frontpage, will be served at root with modifications.
-* `mailjet.apiKey`: Mailjet API Key.
-* `mailjet.senderAddress`: Email address that emails will be received from, must be verified in Mailjet admin panel.
-* `nowplaying.*.apiKey`: API key of respective service.
-* `nowplaying.*.target`: User that should be queried to retrieve playback information.
+Configuration Template
```json
-{
- "frontpage": {
- "directory": ""
- },
- "mailjet": {
- "apiKey": "",
- "senderAddress": ""
- },
-
- "nowplaying": {
- "lastfm": {
- "apiKey": "",
- "target": ""
- },
- "jellyfin": {
- "apiKey": "",
- "host": "",
- "target": ""
- },
- "cider": {
- "apiKeys": [],
- "hosts": []
- }
- }
-
-}
```
@@ -58,9 +21,6 @@ The configuration is downloaded from CouchDB on startup, however two environment
## Docker
In production, this application is designed to be run in Docker, and the container built by pulling the latest commit from the main branch. As such, deploying this application is just a matter of creating a directory and copying the Dockerfile:
-> [!IMPORTANT]
-> Please review the Configuration section of this document for important information. By default, the `config.json` file is expected to be mounted into the container at `/app/config.json`.
-
```dockerfile
FROM node:22
WORKDIR /app
@@ -75,16 +35,7 @@ ENTRYPOINT [ "node", "index.js" ]
Docker Compose File
```yaml
----
-services:
- enstrayedapi:
- build:
- context: .
- image: enstrayedapi
- container_name: enstrayedapi
- restart: unless-stopped
- volumes:
- - ./config.json:/app/config.json
+
```
From a92d3c7a68082631345db4820d2fa012688fffe1 Mon Sep 17 00:00:00 2001
From: Enstrayed <48845980+Enstrayed@users.noreply.github.com>
Date: Thu, 24 Apr 2025 19:30:46 -0700
Subject: [PATCH 09/21] begin new website
---
website/idea.txt | 9 +++++++++
website/posts/20240409-API-Documentation.html | 8 +++++---
website/static/posts.css | 3 ++-
website/static/snow-leopard.svg | 20 +++++++++++++++++++
4 files changed, 36 insertions(+), 4 deletions(-)
create mode 100644 website/idea.txt
create mode 100644 website/static/snow-leopard.svg
diff --git a/website/idea.txt b/website/idea.txt
new file mode 100644
index 0000000..8ed9c10
--- /dev/null
+++ b/website/idea.txt
@@ -0,0 +1,9 @@
+Welcome to Enstrayed.com | @enstrayed
+I received an Email from @enstrayed.com | Twitter Bluesky Github
+↗️ Click here for information about EOES | API Documentation
+I need help with an enstrayed.com web service |
+↗️ Click here to visit the Helpdesk |
+I'm looking for something else
+➡️ URL Toolbox
+➡️ API Documentation
+➡️ Downloads
\ No newline at end of file
diff --git a/website/posts/20240409-API-Documentation.html b/website/posts/20240409-API-Documentation.html
index c030ee6..d419713 100644
--- a/website/posts/20240409-API-Documentation.html
+++ b/website/posts/20240409-API-Documentation.html
@@ -34,9 +34,11 @@
I'm listening to${response.recenttracks.track[0].name} by ${response.recenttracks.track[0].artist["#text"]}from ${response.recenttracks.track[0].album["#text"]}View on Last.fm