The whole forum schema lands at once — threads, posts, the moderation ledger and upload attribution — including the columns only phase 5's discussion threads use. That is TEAMS.md 5.1's split BY LAYER rather than by feature: phase 5 opens paths instead of migrating data. Three settings keys, and only one of them is ordinary. `teams_forums_enabled` and `teams_forum_images` are enum keys on the existing admin settings endpoint; `teams_forum_images` also carries a server-side PRECONDITION, which is why the three live in their own model rather than in the generic setMany() loop where a reader would never find it. The gate is the server's. `PUT teams_forum_images = 'uploads'` is rejected 400 unless the same request carries the acknowledgement version — the admin checkbox is how the gate is presented, never the gate. What is stored is the TEXT VERSION, so "which wording did they agree to" is answerable later; settings already record updated_by/updated_at, and an activity_log row puts it in the staff audit trail. A reworded notice makes a stored acknowledgement stale, and neither obvious answer is right: uploads KEEP WORKING, and no other forum setting may be saved until it is re-given. Non-destructive, and impossible to ignore. Both reads fail closed. A DB fault reports the forum off and images disabled — a forum that 404s for a minute is the cheap failure; a policy that is not a policy is not. Co-Authored-By: Claude <noreply@anthropic.com>
48 lines
1.8 KiB
JavaScript
48 lines
1.8 KiB
JavaScript
const { query } = require('../../utils/db')
|
|
|
|
async function getAll() {
|
|
return query('SELECT `key`, value, updated_at FROM settings ORDER BY `key`')
|
|
}
|
|
|
|
async function get(key) {
|
|
const rows = await query('SELECT value FROM settings WHERE `key` = ? LIMIT 1', [key])
|
|
return rows[0] ? rows[0].value : null
|
|
}
|
|
|
|
async function set(key, value, updatedBy = null) {
|
|
await query(
|
|
'INSERT INTO settings (`key`, value, updated_by) VALUES (?, ?, ?) ' +
|
|
'ON DUPLICATE KEY UPDATE value = VALUES(value), updated_by = VALUES(updated_by)',
|
|
[key, value, updatedBy],
|
|
)
|
|
}
|
|
|
|
// One row WITH its provenance. `updated_by`/`updated_at` are already stored for
|
|
// every key; this is the only reader that needs them, because TEAMS.md §5.5.5
|
|
// makes the uploads acknowledgement a RECORDED consent rather than a displayed
|
|
// one, and "which admin accepted it, and when" is the question that has to be
|
|
// answerable afterwards.
|
|
async function getRow(key) {
|
|
const rows = await query(
|
|
'SELECT s.`key`, s.value, s.updated_by, s.updated_at, u.username AS updated_by_username '
|
|
+ 'FROM settings s LEFT JOIN users u ON u.id = s.updated_by WHERE s.`key` = ? LIMIT 1',
|
|
[key],
|
|
)
|
|
return rows[0] || null
|
|
}
|
|
|
|
// Insert a default only if the key does not already exist.
|
|
async function seedDefault(key, value) {
|
|
await query('INSERT IGNORE INTO settings (`key`, value) VALUES (?, ?)', [key, value])
|
|
}
|
|
|
|
// Delete a settings row. "Reset to defaults" for the theming/nav keys is the
|
|
// *absence* of a row, not a stored copy of the defaults — see
|
|
// docs/website/THEMING_AND_NAV.md §2. Deleting a key that was never set is a
|
|
// no-op, so reset is idempotent.
|
|
async function remove(key) {
|
|
await query('DELETE FROM settings WHERE `key` = ?', [key])
|
|
}
|
|
|
|
module.exports = { getAll, get, getRow, set, seedDefault, remove }
|