From 68be646f37d7d8509dcd65b6acb9435d8597c232 Mon Sep 17 00:00:00 2001 From: Chewbaccalakis Date: Thu, 18 Jun 2026 17:27:08 -0700 Subject: [PATCH] Add multi-user support via token-based routing and users.json config Each user gets a unique secret token embedded in their webhook URL (/webhook/:token). Credentials are loaded from users.json at startup instead of environment variables, making it easy to add or remove users without redeploying. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 ++- README.md | 62 +++++++++++++++++++++++++++++++++++++--------- server.js | 33 ++++++++++++++++-------- users.json.example | 14 +++++++++++ 4 files changed, 88 insertions(+), 24 deletions(-) create mode 100644 users.json.example diff --git a/.gitignore b/.gitignore index 3ec544c..79bc4db 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.env \ No newline at end of file +.env +users.json \ No newline at end of file diff --git a/README.md b/README.md index b5c392f..d79a927 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,65 @@ # AudioPen Nextcloud Webhook -A small Node.js webhook service that receives notes from AudioPen and uploads them to a folder in Nextcloud using WebDAV. +A small Node.js webhook service that receives notes from AudioPen and uploads them to a folder in Nextcloud using WebDAV. Supports multiple users, each with their own Nextcloud credentials and a unique webhook token. When AudioPen sends a webhook request, the service: -1. Receives the note content -2. Creates a timestamped Markdown file -3. Uploads the file to the configured Nextcloud folder +1. Validates the token in the URL +2. Receives the note content +3. Creates a timestamped Markdown file +4. Uploads the file to the configured Nextcloud folder Example filename: ```text -note-2026-05-19T22-15-10.123Z.md +2026-05-19T22-15-10.123Z-my-note-title.md ``` -## Example `.env` +## Setup -Create a file named `.env` in the project root directory: +### 1. Configure users + +Copy `users.json.example` to `users.json` and fill in credentials for each user: + +```json +{ + "abc123secrettoken": { + "nextcloudUrl": "https://nextcloud.example.com", + "username": "alice", + "password": "alice-app-password", + "folder": "AudioPen" + } +} +``` + +Each key is a secret token that identifies a user. Generate one with: + +```bash +node -e "console.log(crypto.randomUUID())" +``` + +### 2. Configure the server + +Optionally create a `.env` file to set the port or a custom path to `users.json`: ```env -NEXTCLOUD_URL=https://nextcloud.example.com -NEXTCLOUD_USERNAME=myuser -NEXTCLOUD_PASSWORD=my-app-password -NEXTCLOUD_FOLDER=AudioPen PORT=3000 -``` \ No newline at end of file +USERS_FILE=users.json +``` + +### 3. Run + +```bash +npm install +node server.js +``` + +### 4. Configure AudioPen + +In AudioPen's webhook settings, set the URL to: + +``` +https://your-server/webhook/ +``` + +Each user gets their own URL containing their unique token. diff --git a/server.js b/server.js index 1dc438f..84db296 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,7 @@ import 'dotenv/config' import express from 'express' import axios from 'axios' +import { readFileSync } from 'fs' const app = express() @@ -10,14 +11,24 @@ app.use(express.text({ type: '*/*' })) const PORT = process.env.PORT || 3000 -const NEXTCLOUD_URL = process.env.NEXTCLOUD_URL -const NEXTCLOUD_USERNAME = process.env.NEXTCLOUD_USERNAME -const NEXTCLOUD_PASSWORD = process.env.NEXTCLOUD_PASSWORD -const NEXTCLOUD_FOLDER = process.env.NEXTCLOUD_FOLDER || 'AudioPen' +const USERS_FILE = process.env.USERS_FILE || 'users.json' +let users = {} +try { + users = JSON.parse(readFileSync(USERS_FILE, 'utf8')) + console.log(`Loaded ${Object.keys(users).length} user(s) from ${USERS_FILE}`) +} catch (err) { + console.error(`Failed to load ${USERS_FILE}: ${err.message}`) + process.exit(1) +} + +app.post('/webhook/:token', async (req, res) => { + const user = users[req.params.token] + if (!user) { + return res.status(401).json({ error: 'Invalid token' }) + } -app.post('/webhook', async (req, res) => { try { - console.log('Received webhook') + console.log(`Received webhook for user: ${user.username}`) console.log('HEADERS:', req.headers) console.log('RAW BODY:', req.body) @@ -63,16 +74,16 @@ ${transcript} ` const webdavUrl = - `${process.env.NEXTCLOUD_URL}` + + `${user.nextcloudUrl}` + `/remote.php/dav/files/` + - `${process.env.NEXTCLOUD_USERNAME}/` + - `${process.env.NEXTCLOUD_FOLDER}/` + + `${user.username}/` + + `${user.folder}/` + `${filename}` await axios.put(webdavUrl, markdown, { auth: { - username: process.env.NEXTCLOUD_USERNAME, - password: process.env.NEXTCLOUD_PASSWORD + username: user.username, + password: user.password }, headers: { 'Content-Type': 'text/markdown' diff --git a/users.json.example b/users.json.example new file mode 100644 index 0000000..7fa3290 --- /dev/null +++ b/users.json.example @@ -0,0 +1,14 @@ +{ + "abc123secrettoken": { + "nextcloudUrl": "https://nextcloud.example.com", + "username": "alice", + "password": "alice-app-password", + "folder": "AudioPen" + }, + "xyz789anothertoken": { + "nextcloudUrl": "https://nextcloud.example.com", + "username": "bob", + "password": "bob-app-password", + "folder": "AudioPen" + } +}