diff --git a/src/branding.js b/src/branding.js index 02e630e77..6637ad8ad 100644 --- a/src/branding.js +++ b/src/branding.js @@ -1,18 +1,74 @@ 'use strict'; exports = module.exports = { + getCloudronName, + setCloudronName, + + getCloudronAvatar, + setCloudronAvatar, + + getFooter, + setFooter, + renderFooter }; const assert = require('assert'), - constants = require('./constants.js'); + BoxError = require('./boxerror.js'), + constants = require('./constants.js'), + paths = require('./paths.js'), + safe = require('safetydance'), + settings = require('./settings.js'); -function renderFooter(footer) { - assert.strictEqual(typeof footer, 'string'); +async function getCloudronName() { + const name = await settings.get(settings.CLOUDRON_NAME_KEY); + return name || 'Cloudron'; +} +async function setCloudronName(name) { + assert.strictEqual(typeof name, 'string'); + + if (!name) throw new BoxError(BoxError.BAD_FIELD, 'name is empty'); + + // some arbitrary restrictions (for sake of ui layout) + // if this is changed, adjust dashboard/branding.html + if (name.length > 64) throw new BoxError(BoxError.BAD_FIELD, 'name cannot exceed 64 characters'); + + await settings.set(settings.CLOUDRON_NAME_KEY, name); +} + +async function getCloudronAvatar() { + let avatar = await settings.getBlob(settings.CLOUDRON_AVATAR_KEY); + if (avatar) return avatar; + + // try default fallback + avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE); + if (avatar) return avatar; + + throw new BoxError(BoxError.FS_ERROR, `Could not read avatar: ${safe.error.message}`); +} + +async function setCloudronAvatar(avatar) { + assert(Buffer.isBuffer(avatar)); + + await settings.setBlob(settings.CLOUDRON_AVATAR_KEY, avatar); +} + +async function renderFooter() { + const footer = await getFooter(); const year = new Date().getFullYear(); return footer.replace(/%YEAR%/g, year) .replace(/%VERSION%/g, constants.VERSION); } +async function getFooter() { + const value = await settings.get(settings.FOOTER_KEY); + return value || constants.FOOTER; +} + +async function setFooter(footer) { + assert.strictEqual(typeof footer, 'string'); + + await settings.set(settings.FOOTER_KEY, footer); +} diff --git a/src/cloudron.js b/src/cloudron.js index 81286dadb..088d1e483 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -158,8 +158,8 @@ async function getConfig() { version: constants.VERSION, ubuntuVersion, isDemo: settings.isDemo(), - cloudronName: allSettings[settings.CLOUDRON_NAME_KEY], - footer: branding.renderFooter(allSettings[settings.FOOTER_KEY]), + cloudronName: await branding.getCloudronName(), + footer: await branding.renderFooter(), features: appstore.getFeatures(), profileLocked: allSettings[settings.PROFILE_CONFIG_KEY].lockUserProfiles, mandatory2FA: allSettings[settings.PROFILE_CONFIG_KEY].mandatory2FA, diff --git a/src/mailer.js b/src/mailer.js index 2d6740f6b..48722d18c 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -17,6 +17,7 @@ exports = module.exports = { const assert = require('assert'), BoxError = require('./boxerror.js'), + branding = require('./branding.js'), debug = require('debug')('box:mailer'), ejs = require('ejs'), mail = require('./mail.js'), @@ -31,7 +32,7 @@ const MAIL_TEMPLATES_DIR = path.join(__dirname, 'mail_templates'); // This will collect the most common details required for notification emails async function getMailConfig() { - const cloudronName = await settings.getCloudronName(); + const cloudronName = await branding.getCloudronName(); const supportConfig = await settings.getSupportConfig(); return { diff --git a/src/oidc.js b/src/oidc.js index 77e51857d..06cd657c5 100644 --- a/src/oidc.js +++ b/src/oidc.js @@ -17,6 +17,7 @@ const assert = require('assert'), apps = require('./apps.js'), BoxError = require('./boxerror.js'), blobs = require('./blobs.js'), + branding = require('./branding.js'), constants = require('./constants.js'), database = require('./database.js'), debug = require('debug')('box:oidc'), @@ -447,7 +448,7 @@ function renderInteractionPage(provider) { const options = { submitUrl: `${ROUTE_PREFIX}/interaction/${uid}/login`, iconUrl: '/api/v1/cloudron/avatar', - name: client?.name || await settings.getCloudronName() + name: client?.name || await branding.getCloudronName() }; if (app) { diff --git a/src/provision.js b/src/provision.js index d1e929dc3..7fc1feaf4 100644 --- a/src/provision.js +++ b/src/provision.js @@ -249,8 +249,8 @@ async function getStatus() { version: constants.VERSION, apiServerOrigin: settings.apiServerOrigin(), // used by CaaS tool webServerOrigin: settings.webServerOrigin(), // used by CaaS tool - cloudronName: allSettings[settings.CLOUDRON_NAME_KEY], - footer: branding.renderFooter(allSettings[settings.FOOTER_KEY]), + cloudronName: await branding.getCloudronName(), + footer: await branding.renderFooter(), adminFqdn: settings.dashboardDomain() ? settings.dashboardFqdn() : null, language: allSettings[settings.LANGUAGE_KEY], activated: activated, diff --git a/src/routes/branding.js b/src/routes/branding.js index 689af8da8..50b8b4cf0 100644 --- a/src/routes/branding.js +++ b/src/routes/branding.js @@ -1,21 +1,23 @@ 'use strict'; exports = module.exports = { - get, - set, - - getCloudronAvatar + getCloudronName, + setCloudronName, + getCloudronAvatar, + setCloudronAvatar, + getFooter, + setFooter, }; const assert = require('assert'), BoxError = require('../boxerror.js'), + branding = require('../branding.js'), HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess, - safe = require('safetydance'), - settings = require('../settings.js'); + safe = require('safetydance'); async function getFooter(req, res, next) { - const [error, footer] = await safe(settings.getFooter()); + const [error, footer] = await safe(branding.getFooter()); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { footer })); @@ -26,7 +28,7 @@ async function setFooter(req, res, next) { if (typeof req.body.footer !== 'string') return next(new HttpError(400, 'footer is required')); - const [error] = await safe(settings.setFooter(req.body.footer)); + const [error] = await safe(branding.setFooter(req.body.footer)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); @@ -37,14 +39,14 @@ async function setCloudronName(req, res, next) { if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required')); - const [error] = await safe(settings.setCloudronName(req.body.name)); + const [error] = await safe(branding.setCloudronName(req.body.name)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function getCloudronName(req, res, next) { - const [error, name] = await safe(settings.getCloudronName()); + const [error, name] = await safe(branding.getCloudronName()); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { name })); @@ -57,14 +59,14 @@ async function setCloudronAvatar(req, res, next) { const avatar = safe.fs.readFileSync(req.files.avatar.path); if (!avatar) return next(500, safe.error.message); - const [error] = await safe(settings.setCloudronAvatar(avatar)); + const [error] = await safe(branding.setCloudronAvatar(avatar)); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, {})); } async function getCloudronAvatar(req, res, next) { - const [error, avatar] = await safe(settings.getCloudronAvatar()); + const [error, avatar] = await safe(branding.getCloudronAvatar()); if (error) return next(BoxError.toHttpError(error)); // avoid caching the avatar on the client to see avatar changes immediately @@ -73,27 +75,3 @@ async function getCloudronAvatar(req, res, next) { res.set('Content-Type', 'image/png'); res.status(200).send(avatar); } - -async function get(req, res, next) { - assert.strictEqual(typeof req.params.setting, 'string'); - - switch (req.params.setting) { - case settings.CLOUDRON_AVATAR_KEY: return await getCloudronAvatar(req, res, next); - case settings.CLOUDRON_NAME_KEY: return await getCloudronName(req, res, next); - case settings.FOOTER_KEY: return await getFooter(req, res, next); - - default: return next(new HttpError(404, 'No such setting')); - } -} - -async function set(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - switch (req.params.setting) { - case settings.CLOUDRON_AVATAR_KEY: return await setCloudronAvatar(req, res, next); - case settings.CLOUDRON_NAME_KEY: return await setCloudronName(req, res, next); - case settings.FOOTER_KEY: return await setFooter(req, res, next); - - default: return next(new HttpError(404, 'No such branding')); - } -} diff --git a/src/routes/test/branding-test.js b/src/routes/test/branding-test.js index c6e8ea5ae..31d53c16c 100644 --- a/src/routes/test/branding-test.js +++ b/src/routes/test/branding-test.js @@ -26,7 +26,7 @@ describe('Branding API', function () { .query({ access_token: owner.token }); expect(response.statusCode).to.equal(200); - expect(response.body.name).to.be.ok(); + expect(response.body.name).to.be('Cloudron'); }); it('cannot set without name', async function () { @@ -49,7 +49,7 @@ describe('Branding API', function () { it('set succeeds', async function () { const response = await superagent.post(`${serverUrl}/api/v1/branding/cloudron_name`) .query({ access_token: owner.token }) - .send({ name: name }); + .send({ name }); expect(response.statusCode).to.equal(200); }); diff --git a/src/server.js b/src/server.js index 88dbce0b1..d3cecf104 100644 --- a/src/server.js +++ b/src/server.js @@ -278,10 +278,12 @@ async function initializeExpressSync() { router.get ('/api/v1/applinks/:id/icon', token, authorizeUser, routes.applinks.getIcon); // branding routes - router.get ('/api/v1/branding/:setting', token, authorizeOwner, routes.branding.get); - router.post('/api/v1/branding/:setting', json, token, authorizeOwner, (req, res, next) => { - return req.params.setting === 'cloudron_avatar' ? multipart(req, res, next) : next(); - }, routes.branding.set); + router.get ('/api/v1/branding/cloudron_name', token, authorizeOwner, routes.branding.getCloudronName); + router.post('/api/v1/branding/cloudron_name', json, token, authorizeOwner, routes.branding.setCloudronName); + router.get ('/api/v1/branding/cloudron_avatar', token, authorizeOwner, routes.branding.getCloudronAvatar); + router.post('/api/v1/branding/cloudron_avatar', json, token, authorizeOwner, multipart, routes.branding.setCloudronAvatar); + router.get ('/api/v1/branding/footer', token, authorizeOwner, routes.branding.getFooter); + router.post('/api/v1/branding/footer', json, token, authorizeOwner, routes.branding.setFooter); // network routes router.get ('/api/v1/network/blocklist', token, authorizeOwner, routes.network.getBlocklist); diff --git a/src/settings.js b/src/settings.js index b7e9d2887..151436631 100644 --- a/src/settings.js +++ b/src/settings.js @@ -7,12 +7,6 @@ exports = module.exports = { getTimeZone, setTimeZone, - getCloudronName, - setCloudronName, - - getCloudronAvatar, - setCloudronAvatar, - getDynamicDnsConfig, setDynamicDnsConfig, @@ -46,9 +40,6 @@ exports = module.exports = { getSysinfoConfig, setSysinfoConfig, - getFooter, - setFooter, - getProfileConfig, setProfileConfig, @@ -161,7 +152,6 @@ const gDefaults = (function () { result[exports.AUTOUPDATE_PATTERN_KEY] = cron.DEFAULT_AUTOUPDATE_PATTERN; result[exports.TIME_ZONE_KEY] = 'UTC'; result[exports.CLOUDRON_COOKIE_SECRET_KEY] = ''; - result[exports.CLOUDRON_NAME_KEY] = 'Cloudron'; result[exports.DYNAMIC_DNS_KEY] = false; result[exports.IPV6_CONFIG_KEY] = { provider: 'noop' @@ -222,8 +212,6 @@ const gDefaults = (function () { submitTickets: true }; - result[exports.FOOTER_KEY] = constants.FOOTER; - return result; })(); @@ -304,42 +292,6 @@ async function getTimeZone() { return tz; } -async function getCloudronName() { - const name = await get(exports.CLOUDRON_NAME_KEY); - if (name === null) return gDefaults[exports.CLOUDRON_NAME_KEY]; - return name; -} - -async function setCloudronName(name) { - assert.strictEqual(typeof name, 'string'); - - if (!name) throw new BoxError(BoxError.BAD_FIELD, 'name is empty'); - - // some arbitrary restrictions (for sake of ui layout) - // if this is changed, adjust dashboard/branding.html - if (name.length > 64) throw new BoxError(BoxError.BAD_FIELD, 'name cannot exceed 64 characters'); - - await set(exports.CLOUDRON_NAME_KEY, name); - notifyChange(exports.CLOUDRON_NAME_KEY, name); -} - -async function getCloudronAvatar() { - let avatar = await getBlob(exports.CLOUDRON_AVATAR_KEY); - if (avatar) return avatar; - - // try default fallback - avatar = safe.fs.readFileSync(paths.CLOUDRON_DEFAULT_AVATAR_FILE); - if (avatar) return avatar; - - throw new BoxError(BoxError.FS_ERROR, `Could not read avatar: ${safe.error.message}`); -} - -async function setCloudronAvatar(avatar) { - assert(Buffer.isBuffer(avatar)); - - await setBlob(exports.CLOUDRON_AVATAR_KEY, avatar); -} - async function getDynamicDnsConfig() { const enabled = await get(exports.DYNAMIC_DNS_KEY); if (enabled === null) return gDefaults[exports.DYNAMIC_DNS_KEY]; @@ -694,19 +646,6 @@ async function setApiServerOrigin(origin) { notifyChange(exports.API_SERVER_ORIGIN_KEY, origin); } -async function getFooter() { - const value = await get(exports.FOOTER_KEY); - if (value === null) return gDefaults[exports.FOOTER_KEY]; - return value; -} - -async function setFooter(footer) { - assert.strictEqual(typeof footer, 'string'); - - await set(exports.FOOTER_KEY, footer); - notifyChange(exports.FOOTER_KEY, footer); -} - function provider() { return gCache.provider; } function apiServerOrigin() { return gCache.apiServerOrigin; } function webServerOrigin() { return gCache.webServerOrigin; } diff --git a/src/test/branding-test.js b/src/test/branding-test.js index e541271b5..71f99ab30 100644 --- a/src/test/branding-test.js +++ b/src/test/branding-test.js @@ -7,7 +7,6 @@ const branding = require('../branding.js'), common = require('./common.js'), - constants = require('../constants.js'), expect = require('expect.js'); describe('Branding', function () { @@ -16,15 +15,27 @@ describe('Branding', function () { before(setup); after(cleanup); + it ('can get default cloudron name', async function () { + const name = await branding.getCloudronName(); + expect(name).to.be('Cloudron'); + }); + + it('can get default cloudron avatar', async function () { + const avatar = await branding.getCloudronAvatar(); + expect(avatar).to.be.a(Buffer); + }); + it('can render default footer', async function () { - expect(branding.renderFooter(constants.FOOTER)).to.contain('(https://cloudron.io)'); + expect(await branding.renderFooter()).to.contain('(https://cloudron.io)'); }); it('can render footer', async function () { - expect(branding.renderFooter('BigFoot Inc')).to.be('BigFoot Inc'); + await branding.setFooter('BigFoot Inc'); + expect(await branding.renderFooter()).to.be('BigFoot Inc'); }); it('can render footer with YEAR', async function () { - expect(branding.renderFooter('BigFoot Inc %YEAR%')).to.be('BigFoot Inc 2023'); + await branding.setFooter('BigFoot Inc %YEAR%'); + expect(await branding.renderFooter()).to.be('BigFoot Inc 2023'); }); }); diff --git a/src/test/settings-test.js b/src/test/settings-test.js index 11563fbed..0ca477988 100644 --- a/src/test/settings-test.js +++ b/src/test/settings-test.js @@ -28,16 +28,6 @@ describe('Settings', function () { expect(pattern).to.be('00 00 1,3,5,23 * * *'); }); - it ('can get default cloudron name', async function () { - const name = await settings.getCloudronName(); - expect(name).to.be('Cloudron'); - }); - - it('can get default cloudron avatar', async function () { - const avatar = await settings.getCloudronAvatar(); - expect(avatar).to.be.a(Buffer); - }); - it('can get backup config', async function () { const backupConfig = await settings.getBackupConfig(); expect(backupConfig.provider).to.be('filesystem'); @@ -104,7 +94,6 @@ describe('Settings', function () { const allSettings = await settings.list(); expect(allSettings[settings.TIME_ZONE_KEY]).to.be.a('string'); expect(allSettings[settings.AUTOUPDATE_PATTERN_KEY]).to.be.a('string'); - expect(allSettings[settings.CLOUDRON_NAME_KEY]).to.be.a('string'); expect(allSettings[settings.IPV6_CONFIG_KEY]).to.be.an('object'); }); });