Make Terms of Service editable at runtime by admins (closes #39)
tos.md was baked into the repo and read once at startup, so changing the terms required a code change and deploy. It's now a Redis-backed singleton (models/tos.js), editable from a new "Terms of Service" card on the admin Dashboard, with the bundled tos.md used only as a one-time seed for new deployments. - routes/tos.js: GET (any authenticated user) / PUT (app_sso_admin only) via /api/tos. Saving can optionally reset every user's tos_accepted flag so they're asked to re-accept -- off by default, since a wording fix shouldn't re-prompt everyone. - routes/index.js: /tos and /onboarding now render the live content instead of a module-level constant computed once at process start.
This commit is contained in:
+15
-7
@@ -1,17 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
const moment = require('moment');
|
||||
const {marked} = require('marked');
|
||||
const {InviteToken, PasswordResetToken} = require('./../models/token');
|
||||
const {Tos} = require('../models/tos');
|
||||
const conf = require('@simpleworkjs/conf');
|
||||
const buildInfo = require('../utils/build_info');
|
||||
|
||||
const tosHtml = marked(fs.readFileSync(path.join(__dirname, '../../tos.md'), 'utf8'));
|
||||
|
||||
const values ={
|
||||
title: conf.environment !== 'production' ? `dev` : '',
|
||||
titleIcon: conf.environment !== 'production' ? `<i class="fa-brands fa-dev"></i>` : '',
|
||||
@@ -44,8 +42,13 @@ router.get('/health', function(req, res) {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
router.get('/tos', function(req, res) {
|
||||
res.render('tos', {...values, tosHtml});
|
||||
router.get('/tos', async function(req, res, next) {
|
||||
try {
|
||||
const tos = await Tos.getCurrent();
|
||||
res.render('tos', {...values, tosHtml: marked(tos.content), tosUpdatedOnFmt: moment(tos.updated_on, 'x').format('MMMM YYYY')});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Admin dashboard (stats + recent/inactive users) and Notifications
|
||||
@@ -61,8 +64,13 @@ router.get('/invites', function(req, res) {
|
||||
res.render('invites', {...values});
|
||||
});
|
||||
|
||||
router.get('/onboarding', function(req, res) {
|
||||
res.render('onboarding', {...values, tosHtml});
|
||||
router.get('/onboarding', async function(req, res, next) {
|
||||
try {
|
||||
const tos = await Tos.getCurrent();
|
||||
res.render('onboarding', {...values, tosHtml: marked(tos.content)});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/', async function(req, res, next) {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const router = require('express').Router();
|
||||
const {Tos} = require('../models/tos');
|
||||
const {UserVerification} = require('../models/verification');
|
||||
const permission = require('../utils/permission');
|
||||
|
||||
// Any authenticated user may read the current ToS (it's what they already
|
||||
// see on /tos and during onboarding, and it isn't sensitive) -- only saving
|
||||
// an edit is admin-gated.
|
||||
router.get('/', async function(req, res, next) {
|
||||
try {
|
||||
const tos = await Tos.getCurrent();
|
||||
return res.json(tos);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/', async function(req, res, next) {
|
||||
try {
|
||||
await permission.byGroup(req.user, ['app_sso_admin']);
|
||||
|
||||
const {content, resetAcceptance} = req.body;
|
||||
if (!content || !content.trim()) {
|
||||
return res.status(400).json({name: 'ValidationError', message: 'content is required'});
|
||||
}
|
||||
|
||||
const tos = await Tos.getCurrent();
|
||||
await tos.update({content, updated_by: req.user.uid, updated_on: Date.now()});
|
||||
|
||||
// Opt-in: a substantive change may need everyone to agree again, but a
|
||||
// wording/typo fix shouldn't re-prompt every user, so this only runs
|
||||
// when the admin explicitly asks for it.
|
||||
let resetCount = 0;
|
||||
if (resetAcceptance) {
|
||||
const verifications = await UserVerification.listDetail();
|
||||
for (const v of verifications) {
|
||||
if (v.tos_accepted) {
|
||||
// Leave tos_accepted_at as the last acceptance time (a
|
||||
// historical fact) -- only the boolean flips, driving
|
||||
// onboardingNeeds back to including 'tos'.
|
||||
await v.update({tos_accepted: false});
|
||||
resetCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({results: tos, resetCount});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user