Initial commit: UOMysticmoon backend (Express + MariaDB + JWT)

- Layered API (router -> controller -> model -> db), serverlinkr pattern
- Public / auth / admin route groups; posts, wiki, settings, users, activity models
- JWT httpOnly-cookie auth (Secure auto-detected: LAN HTTP + Pangolin HTTPS)
- Site LIVE/MAINTENANCE mode with admin preview bypass
- Dual file+console logging (info/warn/error/debug) + HTTP access logs
- Docker Compose (app + MariaDB), schema.sql + seed, .env.example
- Verified end-to-end against MariaDB (27/27 smoke checks)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 20:58:32 -05:00
commit eef79e2403
41 changed files with 4195 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
const { query } = require('../../utils/db')
const PUBLIC_COLS = 'id, username, role, created_at, last_login_at'
async function insertUser({ username, passwordHash, role = 'admin' }) {
const res = await query(
'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)',
[username, passwordHash, role],
)
return res.insertId
}
async function findByUsername(username) {
const rows = await query('SELECT * FROM users WHERE username = ? LIMIT 1', [username])
return rows[0] || null
}
async function findById(id) {
const rows = await query('SELECT * FROM users WHERE id = ? LIMIT 1', [id])
return rows[0] || null
}
async function listUsers() {
return query(`SELECT ${PUBLIC_COLS} FROM users ORDER BY id ASC`)
}
async function updateUser(id, fields) {
const cols = []
const params = []
for (const [key, val] of Object.entries(fields)) {
cols.push(`${key} = ?`)
params.push(val)
}
if (cols.length === 0) return
params.push(id)
await query(`UPDATE users SET ${cols.join(', ')} WHERE id = ?`, params)
}
async function deleteUser(id) {
return query('DELETE FROM users WHERE id = ?', [id])
}
async function countUsers() {
const rows = await query('SELECT COUNT(*) AS c FROM users')
return Number(rows[0].c)
}
async function countAdmins() {
const rows = await query("SELECT COUNT(*) AS c FROM users WHERE role = 'admin'")
return Number(rows[0].c)
}
async function touchLastLogin(id) {
return query('UPDATE users SET last_login_at = NOW() WHERE id = ?', [id])
}
module.exports = {
insertUser,
findByUsername,
findById,
listUsers,
updateUser,
deleteUser,
countUsers,
countAdmins,
touchLastLogin,
}

View File

@@ -0,0 +1,73 @@
const bcrypt = require('bcryptjs')
const usersDb = require('./users.db')
const SALT_ROUNDS = 10
// Strip the password hash before sending a user anywhere.
function sanitize(user) {
if (!user) return null
const { password_hash, ...safe } = user
return safe
}
async function createUser({ username, password, role = 'admin' }) {
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS)
const id = await usersDb.insertUser({ username, passwordHash, role })
return sanitize(await usersDb.findById(id))
}
// Returns the raw row (incl. hash) — used by login only.
async function getRawByUsername(username) {
return usersDb.findByUsername(username)
}
async function getById(id) {
return sanitize(await usersDb.findById(id))
}
async function validatePassword(user, password) {
if (!user || !user.password_hash) return false
return bcrypt.compare(password, user.password_hash)
}
async function list() {
return usersDb.listUsers()
}
async function update(id, { username, password, role }) {
const fields = {}
if (username !== undefined) fields.username = username
if (role !== undefined) fields.role = role
if (password) fields.password_hash = await bcrypt.hash(password, SALT_ROUNDS)
await usersDb.updateUser(id, fields)
return getById(id)
}
async function remove(id) {
return usersDb.deleteUser(id)
}
async function count() {
return usersDb.countUsers()
}
async function countAdmins() {
return usersDb.countAdmins()
}
async function recordLogin(id) {
return usersDb.touchLastLogin(id)
}
module.exports = {
createUser,
getRawByUsername,
getById,
validatePassword,
list,
update,
remove,
count,
countAdmins,
recordLogin,
}