Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-to-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
echo ${{ secrets.EMAIL_USER }} > email_user
echo ${{ secrets.EMAIL_PASS }} > email_pass
git pull
docker-compose build --build-arg OPTIMISM_API_CLIENT_SIDE_URL=${{ secrets.API_BASE_URL }}
docker-compose build --build-arg OPTIMISM_API_CLIENT_SIDE_URL=${{ secrets.API_BASE_URL }} --build-arg OPTIMISM_WEBSITE_BASE_URL=${{ secrets.WEBSITE_BASE_URL }} --build-arg OPTIMISM_EMAIL_ORG_FROM_ADDR=${{ secrets.EMAIL_ORG_FROM_ADDR }} --build-arg OPTIMISM_EMAIL_ORG_NOTIFY_ADDR=${{ secrets.EMAIL_ORG_NOTIFY_ADDR }} --build-arg OPTIMISM_SMTP_HOST=${{ secrets.SMTP_HOST }} --build-arg OPTIMISM_SMTP_PORT=${{ secrets.SMTP_PORT }} --build-arg OPTIMISM_SMTP_SECURE=${{ secrets.SMTP_SECURE }}
docker compose down
docker compose up -d
rm email_user
Expand Down
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ NOTE: You'll need to be using `npm` version 7 or above for this to work, **and**
OPTIMISM_API_PORT = 3001
OPTIMISM_WEBSITE_PORT = 3000
OPTIMISM_API_URL = 'http://localhost:3001/api'
OPTIMISM_WEBSITE_BASE_URL = 'http://localhost:3000'
OPTIMISM_EMAIL_ORG_FROM_ADDR = "YOUR EMAIL ADDRESS"
OPTIMISM_EMAIL_ORG_NOTIFY_ADDR = "YOUR EMAIL ADDRESS SO YOU GET THE NOTIFICATIONS"
OPTIMISM_EMAIL_USER = "A SENDAMATIC OR OTHER SMTP USER"
OPTIMISM_EMAIL_PASS = "A SENDAMATIC OR OTHER SMTP USER PASSWORD"
OPTIMISM_SMTP_HOST = "in.smtp.sendamatic.net"
OPTIMISM_SMTP_PORT = '587'
OPTIMISM_SMTP_SECURE = 'false'

OPTIMISM_ENABLE_DETAILED_ERROR_MESSAGES = 1
````
Expand Down Expand Up @@ -70,14 +78,16 @@ The `bootstrap` folder contains the site scss file (optimism.scss) and a subfold

For production we're running the `api` and `website` components as separate Docker containers, with an off-the-shelf Postgres container to provide the database.

Their interactions are orchestrated with `docker-compose`, so getting it running should just be a case of running:
Their interactions are orchestrated with `docker compose`, so getting it running should just be a case of running:
* `docker-compose build`
* `docker-compose up`

For the email username and password, the docker compose setup expects two files `email_user` and `email_pass` in the root folder.

To run any database migrations, once things are running then run: `docker-compose exec api npx knex migrate:latest --env production`

Any time things are pushed to the `master` branch, the [live site will automatically deploy the new version](https://github.com/DoESLiverpool/optimism/issues/48).

## Also see

[Testing](./documentation/testing)
[Testing](./documentation/testing)
17 changes: 17 additions & 0 deletions api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ ENV NPM_CONFIG_LOGLEVEL=info

ENV NODE_ENV=$NODE_ENVIRONMENT

# Set up environment from the args passed in by docker compose
ARG OPTIMISM_API_URL=http://optimism_api:3001/api
ENV OPTIMISM_API_URL=${OPTIMISM_API_URL}
ARG OPTIMISM_WEBSITE_BASE_URL=http://localhost:3000
ENV OPTIMISM_WEBSITE_BASE_URL=${OPTIMISM_WEBSITE_BASE_URL}
ARG OPTIMISM_EMAIL_ORG_FROM_ADDR=bookings@example.com
ENV OPTIMISM_EMAIL_ORG_FROM_ADDR=${OPTIMISM_EMAIL_ORG_FROM_ADDR}
ARG OPTIMISM_EMAIL_ORG_NOTIFY_ADDR=organisers@example.com
ENV OPTIMISM_EMAIL_ORG_NOTIFY_ADDR=${OPTIMISM_EMAIL_ORG_NOTIFY_ADDR}
ARG OPTIMISM_SMTP_HOST=smtp.example.com
ENV OPTIMISM_SMTP_HOST=${OPTIMISM_SMTP_HOST}
ARG OPTIMISM_SMTP_PORT=587
ENV OPTIMISM_SMTP_PORT=${OPTIMISM_SMTP_PORT}
ARG OPTIMISM_SMTP_SECURE=false
ENV OPTIMISM_SMTP_SECURE=${OPTIMISM_SMTP_SECURE}

# Install global npm modules to the non-root user (called `node` in the default Node container)
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH=$PATH:/home/node/.npm-global/bin
Expand All @@ -32,6 +48,7 @@ COPY model model
COPY model.js model.js
COPY db.js db.js
COPY routes routes
COPY services services
COPY migrations migrations
COPY seeds seeds
COPY knexfile.js knexfile.js
Expand Down
8 changes: 4 additions & 4 deletions api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ require('dotenv').config();
const port = process.env.OPTIMISM_API_PORT || 3001;

// Read in the email config from the secrets
var email_user = "NEED TO SET EMAIL USER";
var email_pass = "NEED TO SET EMAIL PASS";
process.env.OPTIMISM_EMAIL_USER || (process.env.OPTIMISM_EMAIL_USER = "NEED TO SET EMAIL USER");
process.env.OPTIMISM_EMAIL_PASS || (process.env.OPTIMISM_EMAIL_PASS = "NEED TO SET EMAIL PASS");
const email_user_path = "/run/secrets/email_user";
const email_pass_path = "/run/secrets/email_pass";
if (fs.existsSync(email_user_path))
{
email_user = fs.readFileSync(email_user_path, { encoding: 'utf8' }).trim();
process.env.OPTIMISM_EMAIL_USER = fs.readFileSync(email_user_path, { encoding: 'utf8' }).trim();
}
if (fs.existsSync(email_pass_path))
{
email_pass = fs.readFileSync(email_pass_path, { encoding: 'utf8' }).trim();
process.env.OPTIMISM_EMAIL_PASS = fs.readFileSync(email_pass_path, { encoding: 'utf8' }).trim();
}

const resourceRoutes = require('./routes/resources');
Expand Down
2 changes: 0 additions & 2 deletions api/db.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
var environment = process.env.NODE_ENV || 'development';
var config = require('./knexfile.js')[environment];

useNullAsDefault: true;

module.exports = require('knex')(config);
12 changes: 10 additions & 2 deletions api/knexfile.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
module.exports = {

development: {
client: 'sqlite3',
client: 'better-sqlite3',

connection: {
filename: __dirname + '/../databases/optimism_development.sqlite3'
},
useNullAsDefault: true,
pool: {
min: 1,
max: 1,
acquireTimeoutMillis: 30000,
idleTimeoutMillis: 30000
},
migrations: {
directory: __dirname + '/migrations'
},
Expand All @@ -14,11 +21,12 @@ module.exports = {
}
},
testing: {
client: 'sqlite3',
client: 'better-sqlite3',

connection: {
filename: ':memory:'
},
useNullAsDefault: true,
migrations: {
directory: __dirname + '/migrations'
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

exports.up = function (knex) {
return knex.schema.table('bookings', function (t) {
t.string('token').unique().nullable();
t.boolean('cancelled').notNullable().defaultTo(false);
t.index('token');
t.index('cancelled');
});
};

exports.down = function (knex) {
return knex.schema.table('bookings', function (t) {
t.dropIndex('cancelled');
t.dropIndex('token');
t.dropColumn('cancelled');
t.dropColumn('token');
});
};

51 changes: 48 additions & 3 deletions api/model/bookingItems.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const ModelItemsBase = require('./modelItemsBase');
const crypto = require('crypto');
/**
* Provides access to data in the bookings table.
*/
Expand All @@ -16,26 +17,70 @@ class BookingItems extends ModelItemsBase {
'name',
'notes',
'starts',
'ends'
'ends',
'token',
'cancelled'
]);
}

/**
* Generates a unique token for a booking.
* @returns {string} URL-safe base64 encoded random token (32 bytes)
*/
_generateToken () {
return crypto.randomBytes(32).toString('base64url')+Date.now().toString();
}

/**
* Inserts a new booking with a generated token.
* @param {Object<string, any>} item - The booking to insert.
* @param {Function} trx - Optional knex function to be supplied when using a transaction.
* @returns {Promise} When resolved returns an array with the inserted booking id.
*/
insert (item, trx = null) {
// Generate token if not provided
if (!item.token) {
item.token = this._generateToken();
}
// Set cancelled to false if not provided
if (item.cancelled === undefined) {
item.cancelled = false;
}
return super.insert(item, trx);
}

/**
* Gets bookings items for a specified resource for a provided date range.
* Only returns non-cancelled bookings.
*
* @param {object} start - The start date.
* @param {object} end - The inclusive end date.
* @param {number} resourceId - The resource id.
* @returns {Promise} When resolved returns an array of bookings for the resource where starts < date <= end.
* @returns {Promise} When resolved returns an array of non-cancelled bookings for the resource where starts < date <= end.
*/
getByDate (start, end, resourceId) {
const query = this.getSelectQuery(this.knex)
.join('resources', 'resources.id', '=', 'bookings.resource_id')
.where('bookings.resource_id', '=', resourceId)
.where('starts', '>=', start.toISOString())
.where('ends', '<=', end.toISOString());
.where('ends', '<=', end.toISOString())
.where('cancelled', '=', false);
return query.then((bookings) => { return bookings; });
}

/**
* Gets a booking by its token.
* @param {string} token - The booking token.
* @param {Function} trx - Optional knex function to be supplied when using a transaction.
* @returns {Promise} When resolved returns the booking with the supplied token, or null if it doesn't exist.
*/
getByToken (token, trx = null) {
const knexOrTrx = trx == null ? this.knex : trx;
const query = this.getSelectQuery(knexOrTrx).where(`${this.tableName}.token`, token);
return query.then((results) => {
return results.length === 0 ? null : results[0];
});
}
}

module.exports = BookingItems;
8 changes: 4 additions & 4 deletions api/model/modelItemsBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class ModelItemsBase {
* @param {Object<string, any>} item - The item to insert. It must have all the columns except the id column.
* @param {Function} trx - Optional knex function to be supplied when using a transaction.
* @returns {Promise} When resolved returns an array of the form [{id: 123}] where the id is set to the inserted
* row id. If the knex client is not one of 'pg' or 'sqlite3' then a rejected promise is returned.
* row id. If the knex client is not one of 'pg' or 'better-sqlite3' then a rejected promise is returned.
*/
insert (item, trx = null) {
const knexOrTrx = trx == null ? this.knex : trx;
Expand All @@ -91,10 +91,10 @@ class ModelItemsBase {
this value. So specific code is needed for the different
clients that Optimism supports.

Currently these are pg (postgres) and sqlite3 (sqlite).
Currently these are pg (postgres) and better-sqlite3 (sqlite).
*/
const client = knexOrTrx.client.config.client;
if (client === 'sqlite3') {
if (client === 'better-sqlite3' || client === 'sqlite3') {
return knexOrTrx(this.tableName)
.insert(itemWithColumnNames)
.then(() => {
Expand All @@ -105,7 +105,7 @@ class ModelItemsBase {
return knexOrTrx(this.tableName).returning(this.primaryKeyColumn).insert(itemWithColumnNames);
}
return Promise.reject(
new Error('The knex client is not supported. It must be one of \'pg\' or \'sqlite3\'')
new Error('The knex client is not supported. It must be one of \'pg\' or \'better-sqlite3\'')
);
}

Expand Down
18 changes: 15 additions & 3 deletions api/model/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,27 @@ function checkDate (date) {
function checkPostItemFields (item, modelItems) {
// Remove primary key.
const targetKeys = Object.keys(modelItems.jsonToTableNames).filter(key => key !== modelItems.primaryKeyColumn);

// Define optional fields that don't need to be provided in POST requests (auto-generated/defaulted)
const optionalFields = ['token', 'cancelled'];
const requiredKeys = targetKeys.filter(key => !optionalFields.includes(key));

const itemKeys = Object.keys(item);
if (targetKeys.length !== itemKeys.length) {
return false;
}

// Check that all provided keys are valid
for (const key of itemKeys) {
if (!targetKeys.includes(key)) {
return false;
}
}

// Check that all required keys (excluding optional ones) are present
for (const key of requiredKeys) {
if (!itemKeys.includes(key)) {
return false;
}
}

return true;
}

Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
},
"homepage": "https://github.com/DoESLiverpool/optimism#readme",
"dependencies": {
"@vscode/sqlite3": "^5.0.8",
"axios": "^0.21.1",
"better-sqlite3": "^11.10.0",
"chai": "^4.3.4",
Expand All @@ -33,6 +32,7 @@
"mustache-express": "^1.3.0",
"nodemailer": "^6.8.0",
"nunjucks": "^3.2.2",
"optimism": "file:..",
"pg": "^8.6.0",
"supertest": "^6.1.3"
},
Expand Down
Loading
Loading