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,357 @@
const posts = require('../../../model/posts/posts.model')
const wiki = require('../../../model/wiki/wiki.model')
const settings = require('../../../model/settings/settings.model')
const users = require('../../../model/users/users.model')
const activity = require('../../../model/activity/activity.model')
const log = require('../../../utils/logger')('admin')
// ── Dashboard & site mode ─────────────────────────────────────────────
async function dashboard(req, res) {
try {
return res.json({
site_mode: (await settings.get('site_mode')) || 'live',
last_change: {
at: await settings.get('site_mode_changed_at'),
by: await settings.get('site_mode_changed_by'),
},
counts: {
posts: await posts.counts(),
users: await users.count(),
},
recent_activity: await activity.list({ limit: 10 }),
})
} catch (err) {
log.error('dashboard', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function setSiteMode(req, res) {
const { mode } = req.body
try {
const changedAt = new Date().toISOString()
await settings.setMany(
{
site_mode: mode,
site_mode_changed_at: changedAt,
site_mode_changed_by: req.user.username,
},
req.user.id,
)
await activity.log({ req, action: 'site_mode.change', detail: { mode } })
log.info('site mode changed', { mode, by: req.user.username, ip: req.ip })
return res.json({ site_mode: mode, changed_at: changedAt, changed_by: req.user.username })
} catch (err) {
log.error('setSiteMode', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// ── Posts ─────────────────────────────────────────────────────────────
async function listPosts(req, res) {
try {
let dbCategory = null
if (req.query.category) {
dbCategory = posts.toDbCategory(req.query.category)
if (!dbCategory) return res.status(400).json({ message: 'Unknown category' })
}
return res.json(await posts.listAll(dbCategory))
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function getPost(req, res) {
try {
const post = await posts.getById(Number(req.params.id))
if (!post) return res.status(404).json({ message: 'Not found' })
return res.json(post)
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function createPost(req, res) {
const dbCategory = posts.toDbCategory(req.body.category)
if (!dbCategory) return res.status(400).json({ message: 'Unknown category' })
if (dbCategory === 'screenshot' && !req.body.image_url) {
return res.status(400).json({ message: 'Screenshots require an image_url' })
}
try {
const created = await posts.create({
category: dbCategory,
title: req.body.title,
slug: req.body.slug || null,
excerpt: req.body.excerpt || null,
body: req.body.body || null,
image_url: req.body.image_url || null,
published: Boolean(req.body.published),
author_id: req.user.id,
})
await activity.log({ req, action: 'post.create', detail: { id: created.id, category: dbCategory } })
return res.status(201).json(created)
} catch (err) {
log.error('createPost', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function updatePost(req, res) {
const id = Number(req.params.id)
try {
const current = await posts.getById(id)
if (!current) return res.status(404).json({ message: 'Not found' })
const fields = {}
for (const key of ['title', 'slug', 'excerpt', 'body', 'image_url']) {
if (key in req.body) fields[key] = req.body[key] || null
}
if ('category' in req.body) {
const dbCategory = posts.toDbCategory(req.body.category)
if (!dbCategory) return res.status(400).json({ message: 'Unknown category' })
fields.category = dbCategory
}
if ('published' in req.body) {
fields.published = req.body.published ? 1 : 0
if (req.body.published && !current.published_at) fields.published_at = new Date()
}
const updated = await posts.update(id, fields)
await activity.log({ req, action: 'post.update', detail: { id } })
return res.json(updated)
} catch (err) {
log.error('updatePost', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function publishPost(req, res) {
const id = Number(req.params.id)
try {
const updated = await posts.setPublished(id, Boolean(req.body.published))
if (!updated) return res.status(404).json({ message: 'Not found' })
await activity.log({
req,
action: 'post.publish',
detail: { id, published: Boolean(req.body.published) },
})
return res.json(updated)
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function deletePost(req, res) {
const id = Number(req.params.id)
try {
await posts.remove(id)
await activity.log({ req, action: 'post.delete', detail: { id } })
return res.json({ id })
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function uploadImage(req, res) {
if (!req.file) return res.status(400).json({ message: 'No image uploaded' })
const imageUrl = `/uploads/${req.file.filename}`
await activity.log({ req, action: 'post.upload', detail: { image_url: imageUrl } })
return res.status(201).json({ image_url: imageUrl })
}
// ── Wiki ──────────────────────────────────────────────────────────────
async function listWiki(req, res) {
try {
return res.json(await wiki.list())
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function getWiki(req, res) {
try {
const page = await wiki.getBySlug(req.params.slug)
if (!page) return res.status(404).json({ message: 'Not found' })
return res.json(page)
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function createWiki(req, res) {
try {
if (await wiki.getBySlug(req.body.slug)) {
return res.status(409).json({ message: 'A page with that slug already exists' })
}
const page = await wiki.create({
slug: req.body.slug,
title: req.body.title,
body: req.body.body || null,
updatedBy: req.user.id,
})
await activity.log({ req, action: 'wiki.create', detail: { slug: page.slug } })
return res.status(201).json(page)
} catch (err) {
log.error('createWiki', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function updateWiki(req, res) {
try {
const existing = await wiki.getBySlug(req.params.slug)
if (!existing) return res.status(404).json({ message: 'Not found' })
const page = await wiki.update(req.params.slug, {
title: req.body.title,
body: req.body.body || null,
updatedBy: req.user.id,
})
await activity.log({ req, action: 'wiki.update', detail: { slug: req.params.slug } })
return res.json(page)
} catch (err) {
log.error('updateWiki', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function deleteWiki(req, res) {
try {
await wiki.remove(req.params.slug)
await activity.log({ req, action: 'wiki.delete', detail: { slug: req.params.slug } })
return res.json({ slug: req.params.slug })
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// ── Settings ──────────────────────────────────────────────────────────
async function getSettings(req, res) {
try {
return res.json(await settings.getAll())
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function updateSettings(req, res) {
const updates = req.body
if (!updates || typeof updates !== 'object' || Array.isArray(updates)) {
return res.status(400).json({ message: 'Expected an object of key/value settings' })
}
try {
await settings.setMany(updates, req.user.id)
await activity.log({ req, action: 'settings.update', detail: { keys: Object.keys(updates) } })
return res.json(await settings.getAll())
} catch (err) {
log.error('updateSettings', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// ── Activity log ──────────────────────────────────────────────────────
async function listActivity(req, res) {
const limit = Math.min(Number(req.query.limit) || 50, 200)
const offset = Number(req.query.offset) || 0
try {
return res.json(await activity.list({ limit, offset }))
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
// ── User management ───────────────────────────────────────────────────
async function listUsers(req, res) {
try {
return res.json(await users.list())
} catch (err) {
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function createUser(req, res) {
try {
if (await users.getRawByUsername(req.body.username)) {
return res.status(409).json({ message: 'Username already taken' })
}
const user = await users.createUser({
username: req.body.username,
password: req.body.password,
role: req.body.role || 'admin',
})
await activity.log({ req, action: 'user.create', detail: { id: user.id, username: user.username } })
return res.status(201).json(user)
} catch (err) {
log.error('createUser', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function updateUser(req, res) {
const id = Number(req.params.id)
try {
const target = await users.getById(id)
if (!target) return res.status(404).json({ message: 'Not found' })
// Don't let the last admin demote themselves out of admin access.
if (target.role === 'admin' && req.body.role && req.body.role !== 'admin') {
if ((await users.countAdmins()) <= 1) {
return res.status(400).json({ message: 'Cannot demote the last admin' })
}
}
const user = await users.update(id, {
username: req.body.username,
password: req.body.password,
role: req.body.role,
})
await activity.log({ req, action: 'user.update', detail: { id } })
return res.json(user)
} catch (err) {
log.error('updateUser', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
async function deleteUser(req, res) {
const id = Number(req.params.id)
try {
if (id === req.user.id) {
return res.status(400).json({ message: 'You cannot delete your own account' })
}
const target = await users.getById(id)
if (!target) return res.status(404).json({ message: 'Not found' })
if (target.role === 'admin' && (await users.countAdmins()) <= 1) {
return res.status(400).json({ message: 'Cannot delete the last admin' })
}
await users.remove(id)
await activity.log({ req, action: 'user.delete', detail: { id } })
return res.json({ id })
} catch (err) {
log.error('deleteUser', err)
return res.status(500).json({ message: 'Internal Server Error' })
}
}
module.exports = {
dashboard,
setSiteMode,
listPosts,
getPost,
createPost,
updatePost,
publishPost,
deletePost,
uploadImage,
listWiki,
getWiki,
createWiki,
updateWiki,
deleteWiki,
getSettings,
updateSettings,
listActivity,
listUsers,
createUser,
updateUser,
deleteUser,
}