Add self-service web UI for user registration and settings management
Users visit the home page, enter their Nextcloud credentials and an admin password, and receive a unique webhook URL for AudioPen. A bookmarkable settings page (/?token=...) lets them update credentials later without needing the admin password. New users.json entries are written at runtime via POST/PUT API endpoints. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
1 parent
68be646f37
commit
611a4a6da3
3 files changed
+519
-43
No files matched your search
@@ -1,26 +1,102 @@
|
||||
import 'dotenv/config'
|
||||
import express from 'express'
|
||||
import axios from 'axios'
|
||||
import { readFileSync } from 'fs'
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
import { randomUUID } from 'crypto'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
const app = express()
|
||||
|
||||
app.use(express.urlencoded({ extended: false }))
|
||||
app.use(express.json())
|
||||
app.use(express.text({ type: '*/*' }))
|
||||
app.use(express.static(path.join(__dirname, 'public')))
|
||||
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
const USERS_FILE = process.env.USERS_FILE || 'users.json'
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD
|
||||
|
||||
if (!ADMIN_PASSWORD) {
|
||||
console.warn('Warning: ADMIN_PASSWORD is not set. New user registration is disabled.')
|
||||
}
|
||||
|
||||
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)
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(`${USERS_FILE} not found, starting with empty user list`)
|
||||
} else {
|
||||
console.error(`Failed to load ${USERS_FILE}: ${err.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function saveUsers() {
|
||||
writeFileSync(USERS_FILE, JSON.stringify(users, null, 2))
|
||||
}
|
||||
|
||||
// --- Setup API ---
|
||||
|
||||
app.post('/api/users', (req, res) => {
|
||||
if (!ADMIN_PASSWORD) {
|
||||
return res.status(503).json({ error: 'Registration is disabled: ADMIN_PASSWORD is not configured.' })
|
||||
}
|
||||
if (req.body.adminPassword !== ADMIN_PASSWORD) {
|
||||
return res.status(403).json({ error: 'Invalid admin password.' })
|
||||
}
|
||||
|
||||
const { nextcloudUrl, username, password, folder } = req.body
|
||||
if (!nextcloudUrl || !username || !password) {
|
||||
return res.status(400).json({ error: 'nextcloudUrl, username, and password are required.' })
|
||||
}
|
||||
|
||||
const token = randomUUID()
|
||||
users[token] = {
|
||||
nextcloudUrl: nextcloudUrl.replace(/\/$/, ''),
|
||||
username,
|
||||
password,
|
||||
folder: folder || 'AudioPen'
|
||||
}
|
||||
saveUsers()
|
||||
|
||||
const webhookUrl = `${req.protocol}://${req.get('host')}/webhook/${token}`
|
||||
console.log(`New user registered: ${username}`)
|
||||
res.json({ token, webhookUrl })
|
||||
})
|
||||
|
||||
app.get('/api/users/:token', (req, res) => {
|
||||
const user = users[req.params.token]
|
||||
if (!user) return res.status(404).json({ error: 'Not found.' })
|
||||
res.json(user)
|
||||
})
|
||||
|
||||
app.put('/api/users/:token', (req, res) => {
|
||||
if (!users[req.params.token]) return res.status(404).json({ error: 'Not found.' })
|
||||
|
||||
const { nextcloudUrl, username, password, folder } = req.body
|
||||
if (!nextcloudUrl || !username || !password) {
|
||||
return res.status(400).json({ error: 'nextcloudUrl, username, and password are required.' })
|
||||
}
|
||||
|
||||
users[req.params.token] = {
|
||||
nextcloudUrl: nextcloudUrl.replace(/\/$/, ''),
|
||||
username,
|
||||
password,
|
||||
folder: folder || 'AudioPen'
|
||||
}
|
||||
saveUsers()
|
||||
|
||||
console.log(`User updated: ${username}`)
|
||||
res.json({ success: true })
|
||||
})
|
||||
|
||||
// --- Webhook ---
|
||||
|
||||
app.post('/webhook/:token', async (req, res) => {
|
||||
const user = users[req.params.token]
|
||||
if (!user) {
|
||||
@@ -38,9 +114,7 @@ app.post('/webhook/:token', async (req, res) => {
|
||||
// If body comes in as raw string (VERY likely here)
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
// try parsing as URL encoded manually
|
||||
const params = new URLSearchParams(data)
|
||||
|
||||
data = Object.fromEntries(params.entries())
|
||||
} catch (e) {
|
||||
console.log('Failed to parse raw body')
|
||||
@@ -59,7 +133,6 @@ app.post('/webhook/:token', async (req, res) => {
|
||||
.toLowerCase()
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-')
|
||||
|
||||
const filename = `${timestamp}-${safeTitle}.md`
|
||||
|
||||
const markdown = `# ${title}
|
||||
@@ -99,4 +172,4 @@ ${transcript}
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Webhook server listening on ${PORT}`)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user