Skip to content
This repository has been archived by the owner on Sep 2, 2023. It is now read-only.

Commit

Permalink
Add a hidden demo option to allow public demos (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
rowanmanning authored Dec 9, 2020
1 parent 9d576e5 commit d0a5199
Show file tree
Hide file tree
Showing 12 changed files with 129 additions and 17 deletions.
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,26 @@ This application is configured using environment variables, or an [`.env` file](

You can also change more configurations through the settings page of a running copy of Audrey, these additional configurations are stored in the database.

### Demo mode

**Do not use demo mode on your main feed reader – it will result in loss of data**. Audrey can be placed into "demo mode" by using setting a hidden config option in the database. It's important to note that placing an installation in demo mode will do the following things:

- You will no longer need to log in with a password, all data (including settings) will be viewable by anyone who visits the site

- You will no longer be able to change site settings or feed settings

- You will no longer be able to subscribe to new feeds

- Every 15 minutes, all entries in the database will be removed and all feeds will be refreshed

These changes are designed to allow Audrey to be publicly viewable without the risk of unwanted data being added to your database.

The only way to enable demo mode is to manually change the settings in your database. You can do so using the following while connected to MongoDB:

```js
db.settings.updateMany({}, {$set: {demoMode: true}})
```


## Beta notice

Expand Down
14 changes: 13 additions & 1 deletion client/sass/components/_notification.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,16 @@
.notification--success {
--notification-border-color: var(--colorvalue-green);
--notification-background-color: var(--colorvalue-green-light);
}
}

// Notification block, "demo" modifier
.notification--demo {
--notification-border-color: var(--colorvalue-yellow-dark);
--notification-background-color: var(--colorvalue-yellow);
border-bottom: calc(var(--spacing-rule) * 0.25) solid var(--notification-border-color);
border-left: none;
font-size: var(--font-size-small);
line-height: var(--font-size-body);
margin-bottom: var(--spacing-half-line) !important;
padding: var(--spacing-rule);
}
7 changes: 7 additions & 0 deletions server/controller/feeds-by-id.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const demoError = require('../lib/demo-error');
const paginate = require('../middleware/paginate');
const render = require('../middleware/render');
const requireAuth = require('../middleware/require-auth');
Expand Down Expand Up @@ -121,6 +122,9 @@ module.exports = function mountFeedsByIdController(app) {
try {
// On POST, attempt to save the feed
if (request.method === 'POST') {
if (request.settings.demoMode) {
throw demoError();
}

// We use a fresh feed object so that we don't interfere
// with displayed properties outside of the form
Expand Down Expand Up @@ -156,6 +160,9 @@ module.exports = function mountFeedsByIdController(app) {
try {
// On POST, attempt to unsubscribe from the feed
if (request.method === 'POST') {
if (request.settings.demoMode) {
throw demoError();
}
if (unsubscribeForm.data.confirm) {
const title = request.feed.displayTitle;
await request.feed.unsubscribe();
Expand Down
4 changes: 4 additions & 0 deletions server/controller/settings.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const demoError = require('../lib/demo-error');
const render = require('../middleware/render');
const requireAuth = require('../middleware/require-auth');
const {ValidationError} = require('@rowanmanning/app');
Expand Down Expand Up @@ -55,6 +56,9 @@ module.exports = function mountSettingsController(app) {
try {
// On POST, attempt to save the settings
if (request.method === 'POST') {
if (request.settings.demoMode) {
throw demoError();
}

// We use a fresh settings object so that we don't interfere
// with displayed properties outside of the form
Expand Down
4 changes: 4 additions & 0 deletions server/controller/subscribe.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';

const demoError = require('../lib/demo-error');
const render = require('../middleware/render');
const requireAuth = require('../middleware/require-auth');
const {ValidationError} = require('@rowanmanning/app');
Expand Down Expand Up @@ -35,6 +36,9 @@ module.exports = function mountSubscribeController(app) {
try {
// On POST, attempt to create a feed
if (request.method === 'POST') {
if (request.settings.demoMode) {
throw demoError();
}
const feed = await Feed.subscribe(subscribeForm.data.xmlUrl);
await feed.refresh();
request.flash('subscribed', true);
Expand Down
50 changes: 39 additions & 11 deletions server/lib/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,51 @@ module.exports = class AudreyApp extends App {
super.setupControllers();
}

setupScheduledJobs() {
async setupScheduledJobs() {
try {
this.scheduledJob = schedule.scheduleJob(this.options.updateSchedule, async () => {
try {
await this.models.Entry.performScheduledJobs();
await this.models.Feed.performScheduledJobs();
this.log.info(`[sheduler]: scheduled jobs complete`);
} catch (error) {
this.log.error(`[sheduler]: scheduled jobs failed: ${error.message}`);
}
});
this.log.info(`[setup:sheduler]: started with cron "${this.options.updateSchedule}"`);
const settings = await this.models.Settings.get();

// If the app is in demo mode, wipe the entries every 15 minutes
// and refresh all of the feeds so that entries are freshly loaded
if (settings.demoMode) {
this.setupScheduledDemoJobs();

// Otherwise refresh feeds on a schedule
} else {
this.setupScheduledStandardJobs();
}

} catch (error) {
this.log.error(`[setup:sheduler]: setup failed, ${error.message}`);
}
}

setupScheduledStandardJobs() {
this.scheduledJob = schedule.scheduleJob(this.options.updateSchedule, async () => {
try {
await this.models.Entry.performScheduledJobs();
await this.models.Feed.performScheduledJobs();
this.log.info(`[sheduler]: scheduled jobs complete`);
} catch (error) {
this.log.error(`[sheduler]: scheduled jobs failed: ${error.message}`);
}
});
this.log.info(`[setup:sheduler]: started with cron "${this.options.updateSchedule}"`);
}

setupScheduledDemoJobs() {
this.scheduledJob = schedule.scheduleJob('0,15,30,45 * * * *', async () => {
try {
await this.models.Entry.deleteMany({});
await this.models.Feed.refreshAll();
this.log.info(`[sheduler]: scheduled jobs complete`);
} catch (error) {
this.log.error(`[sheduler]: scheduled jobs failed: ${error.message}`);
}
});
this.log.info(`[setup:sheduler]: started in demo mode, resetting entries every 15 minutes`);
}

teardown() {
if (this.scheduledJob) {
this.scheduledJob.cancel();
Expand Down
9 changes: 9 additions & 0 deletions server/lib/demo-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

const {ValidationError} = require('@rowanmanning/app');

module.exports = function demoError(message) {
const error = new ValidationError();
error.errors.demo = new Error(message || 'Sorry, this feature is not available in demo mode.');
return error;
};
3 changes: 3 additions & 0 deletions server/middleware/require-auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
*/
module.exports = function requireAuth() {
return (request, response, next) => {
if (request.settings.demoMode) {
return next();
}
if (!request.settings.hasPassword()) {
return response.redirect('/');
}
Expand Down
4 changes: 4 additions & 0 deletions server/model/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ module.exports = function defineSettingsSchema(app) {
passwordHash: {
type: String,
minlength: [8, 'Password must be 8 or more characters in length']
},
demoMode: {
type: Boolean,
default: false
}
}, {
timestamps: true,
Expand Down
9 changes: 9 additions & 0 deletions server/view/layout/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ const Main = require('../partial/section/main');
*/
module.exports = function renderMainLayout(context, content) {
return layout(context, html`
${context.settings.demoMode ? html`
<div class="notification notification--demo page-layout">
<p class="page-layout__main">
<strong>This app is running in demo mode</strong>, any changes will be reset
every 15 minutes. Some features have been disabled for the demo to prevent
inappropriate content from being displayed.
</p>
</div>
` : ''}
<${Header}
title=${context.settings.siteTitle}
currentPath=${context.currentPath}
Expand Down
3 changes: 2 additions & 1 deletion test/integration/seed/settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ module.exports = async function seedDatabase(models) {
daysToRetainOldEntries: 60,
autoMarkAsRead: true,
showHelpText: true,
passwordHash: 'password'
passwordHash: 'password',
demoMode: false
});

};
19 changes: 15 additions & 4 deletions test/integration/setup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,21 @@ before(done => {
done(error);
});

// Once the app starts, we're ready to test
global.app.once('server:started', () => {
done();
});
// Once the app starts and the database is connected, we're ready to test
let isReady = false;
async function handleReadyEvent() {
if (isReady) {

// This makes sure that settings are created before
// any requests are made
await global.app.models.Settings.get();

return done();
}
isReady = true;
}
global.app.once('server:started', handleReadyEvent);
global.app.once('database:connected', handleReadyEvent);

// Start the application
global.app.setup();
Expand Down

0 comments on commit d0a5199

Please sign in to comment.