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,84 @@
const { query } = require('../../utils/db')
const COLS =
'id, category, title, slug, excerpt, body, image_url, published, author_id, created_at, updated_at, published_at'
// Published posts for a category, newest first — public feed.
async function listPublished(category) {
return query(
`SELECT ${COLS} FROM posts WHERE category = ? AND published = 1 ` +
'ORDER BY COALESCE(published_at, created_at) DESC, id DESC',
[category],
)
}
// All posts for a category (admin), newest first.
async function listAll(category) {
if (category) {
return query(`SELECT ${COLS} FROM posts WHERE category = ? ORDER BY id DESC`, [category])
}
return query(`SELECT ${COLS} FROM posts ORDER BY id DESC`)
}
async function findById(id) {
const rows = await query(`SELECT ${COLS} FROM posts WHERE id = ? LIMIT 1`, [id])
return rows[0] || null
}
async function findPublished(category, id, slug) {
const rows = await query(
`SELECT ${COLS} FROM posts WHERE category = ? AND published = 1 AND (id = ? OR slug = ?) LIMIT 1`,
[category, id, slug],
)
return rows[0] || null
}
async function insert(post) {
const res = await query(
'INSERT INTO posts (category, title, slug, excerpt, body, image_url, published, author_id, published_at) ' +
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[
post.category,
post.title,
post.slug || null,
post.excerpt || null,
post.body || null,
post.image_url || null,
post.published ? 1 : 0,
post.author_id || null,
post.published ? new Date() : null,
],
)
return res.insertId
}
async function update(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 posts SET ${cols.join(', ')} WHERE id = ?`, params)
}
async function remove(id) {
return query('DELETE FROM posts WHERE id = ?', [id])
}
async function countByCategory() {
return query('SELECT category, COUNT(*) AS c FROM posts GROUP BY category')
}
module.exports = {
listPublished,
listAll,
findById,
findPublished,
insert,
update,
remove,
countByCategory,
}

View File

@@ -0,0 +1,89 @@
const postsDb = require('./posts.db')
// URL category (kebab) <-> DB enum value.
const CATEGORY_MAP = {
news: 'news',
'five-on-friday': 'five_on_friday',
newsletter: 'newsletter',
screenshots: 'screenshot',
}
const URL_CATEGORIES = Object.keys(CATEGORY_MAP)
const DB_CATEGORIES = Object.values(CATEGORY_MAP)
function toDbCategory(urlCategory) {
return CATEGORY_MAP[urlCategory] || null
}
function isValidUrlCategory(urlCategory) {
return Boolean(CATEGORY_MAP[urlCategory])
}
function isValidDbCategory(dbCategory) {
return DB_CATEGORIES.includes(dbCategory)
}
async function listPublished(urlCategory) {
return postsDb.listPublished(toDbCategory(urlCategory))
}
async function getPublished(urlCategory, idOrSlug) {
const id = Number.isInteger(Number(idOrSlug)) ? Number(idOrSlug) : -1
return postsDb.findPublished(toDbCategory(urlCategory), id, String(idOrSlug))
}
async function listAll(dbCategory) {
return postsDb.listAll(dbCategory || null)
}
async function getById(id) {
return postsDb.findById(id)
}
async function create(post) {
const id = await postsDb.insert(post)
return postsDb.findById(id)
}
async function update(id, fields) {
await postsDb.update(id, fields)
return postsDb.findById(id)
}
async function setPublished(id, published) {
const current = await postsDb.findById(id)
if (!current) return null
const fields = { published: published ? 1 : 0 }
// Stamp published_at the first time a post goes live.
if (published && !current.published_at) fields.published_at = new Date()
await postsDb.update(id, fields)
return postsDb.findById(id)
}
async function remove(id) {
return postsDb.remove(id)
}
async function counts() {
const rows = await postsDb.countByCategory()
return rows.reduce((acc, row) => {
acc[row.category] = Number(row.c)
return acc
}, {})
}
module.exports = {
URL_CATEGORIES,
DB_CATEGORIES,
toDbCategory,
isValidUrlCategory,
isValidDbCategory,
listPublished,
getPublished,
listAll,
getById,
create,
update,
setPublished,
remove,
counts,
}