diff --git a/.env.sample b/.env.sample index 25c2fd1a..98b69fda 100644 --- a/.env.sample +++ b/.env.sample @@ -1,14 +1,15 @@ # Copy this file to .env, remove this comment and change the keys # -# Please use `openssl rand -hex 32` to create SEQUELIZE_SECRET +# Please use `openssl rand -hex 32` to create SECRET_KEY DATABASE_URL=postgres://user:pass@example.com:5432/outline DATABASE_URL_TEST=postgres://user:pass@example.com:5432/outline-test +SECRET_KEY=F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B PORT=3000 REDIS_URL=redis://localhost:6379 -SEQUELIZE_SECRET=F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B SLACK_KEY=71315967491.XXXXXXXXXX SLACK_SECRET=d2dc414f9953226bad0a356c794XXXXX URL=http://localhost:3000 DEPLOYMENT=hosted +ENABLE_UPDATES=true GOOGLE_ANALYTICS_ID= diff --git a/README.md b/README.md index 0f6308e0..d7e470a0 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,28 @@ To install and run the application: 1. Register a Slack app at https://api.slack.com/apps 1. Copy the file `.env.sample` to `.env` and fill out the keys 1. Run DB migrations `yarn sequelize db:migrate` - 1. Start the development server `yarn start` + + To run Outline in development mode with server and frontend code reloading: +```shell +yarn dev +``` + +To run Outline in production mode: + +```shell +yarn start +``` + +## Development + +### Server + +To enable debugging statements, set the following env vars: + +``` +DEBUG=sql,cache,presenters +``` ## Migrations diff --git a/app.json b/app.json index 92659689..ea52a1ec 100644 --- a/app.json +++ b/app.json @@ -28,7 +28,7 @@ "REDIS_URL": { "required": true }, - "SEQUELIZE_SECRET": { + "SECRET_KEY": { "required": true }, "SLACK_KEY": { @@ -51,10 +51,13 @@ } }, "formation": {}, - "addons": ["heroku-postgresql", "heroku-redis"], + "addons": [ + "heroku-postgresql", + "heroku-redis" + ], "buildpacks": [ { "url": "heroku/nodejs" } ] -} +} \ No newline at end of file diff --git a/circle.yml b/circle.yml index 875af71c..99947a85 100644 --- a/circle.yml +++ b/circle.yml @@ -6,7 +6,7 @@ machine: environment: ENVIRONMENT: test PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" - SEQUELIZE_SECRET: F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B + SECRET_KEY: F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B DATABASE_URL_TEST: postgres://ubuntu@localhost:5432/circle_test DATABASE_URL: postgres://ubuntu@localhost:5432/circle_test diff --git a/flow-typed/globals.js b/flow-typed/globals.js index 37ccbbce..b5fbc176 100644 --- a/flow-typed/globals.js +++ b/flow-typed/globals.js @@ -6,3 +6,8 @@ declare var BASE_URL: string; declare var BUGSNAG_KEY: ?string; declare var DEPLOYMENT: string; declare var Bugsnag: any; +declare var process: { + env: { + [string]: string, + }, +}; diff --git a/index.js b/index.js index 9f822e6c..d9f045b1 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,30 @@ require('./init'); + +if (process.env.NODE_ENV === 'production') { + console.log( + '\n\x1b[33m%s\x1b[0m', + 'Running Outline in production mode. Use `yarn dev` to run in development with live code reloading' + ); +} else if (process.env.NODE_ENV === 'development') { + console.log( + '\n\x1b[33m%s\x1b[0m', + 'Running Outline in development mode with React hot reloading. To run Outline in production mode, use `yarn start`' + ); +} + const app = require('./server').default; const http = require('http'); +if ( + process.env.SECRET_KEY === + 'F0E5AD933D7F6FD8F4DBB3E038C501C052DC0593C686D21ACB30AE205D2F634B' +) { + console.error( + 'Please set SECRET_KEY env variable with output of `openssl rand -hex 32`' + ); + process.exit(1); +} + const server = http.createServer(app.callback()); server.listen(process.env.PORT || '3000'); server.on('error', err => { @@ -9,5 +32,5 @@ server.on('error', err => { }); server.on('listening', () => { const address = server.address(); - console.log(`Listening on http://localhost:${address.port}`); + console.log(`\n> Listening on http://localhost:${address.port}\n`); }); diff --git a/package.json b/package.json index 53361d1a..21c2cac3 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,8 @@ "build:analyze": "NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer", "build": "npm run clean && npm run build:webpack", - "start": "node index.js", - "dev": - "NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --inspect --watch server index.js", + "start": "NODE_ENV=production node index.js", + "dev": "NODE_ENV=development nodemon --inspect --watch server index.js", "lint": "npm run lint:flow && npm run lint:js", "lint:js": "eslint app", "lint:flow": "flow", diff --git a/server/index.js b/server/index.js index 92eb0738..6c593f24 100644 --- a/server/index.js +++ b/server/index.js @@ -5,6 +5,7 @@ import logger from 'koa-logger'; import mount from 'koa-mount'; import Koa from 'koa'; import bugsnag from 'bugsnag'; +import updates from './utils/updates'; import api from './api'; import routes from './routes'; @@ -82,4 +83,17 @@ app.use( }) ); +/** + * Production updates and anonymous analytics. + * + * Set ENABLE_UPDATES=false to disable them for your installation + */ +if ( + process.env.ENABLE_UPDATES !== 'false' && + process.env.NODE_ENV === 'production' +) { + updates(); + setInterval(updates, 24 * 3600 * 1000); +} + export default app; diff --git a/server/sequelize.js b/server/sequelize.js index c1633c94..4c0275ab 100644 --- a/server/sequelize.js +++ b/server/sequelize.js @@ -3,7 +3,7 @@ import Sequelize from 'sequelize'; import EncryptedField from 'sequelize-encrypted'; import debug from 'debug'; -const secretKey = process.env.SEQUELIZE_SECRET; +const secretKey = process.env.SECRET_KEY; export const encryptedFields = EncryptedField(Sequelize, secretKey); diff --git a/server/utils/updates.js b/server/utils/updates.js new file mode 100644 index 00000000..18d950b5 --- /dev/null +++ b/server/utils/updates.js @@ -0,0 +1,71 @@ +// @flow +import crypto from 'crypto'; +import invariant from 'invariant'; +import fetch from 'isomorphic-fetch'; +import { client } from '../redis'; +import packageInfo from '../../package.json'; + +import { User, Team, Collection, Document } from '../models'; + +const UPDATES_URL = 'https://updates.getoutline.com'; +const UPDATES_KEY = 'UPDATES_KEY'; + +export default async () => { + invariant( + process.env.SECRET_KEY && process.env.URL, + 'SECRET_KEY or URL env var is not set' + ); + const secret = process.env.SECRET_KEY.slice(0, 6) + process.env.URL; + const id = crypto.createHash('sha256').update(secret).digest('hex'); + + const [ + userCount, + teamCount, + collectionCount, + documentCount, + ] = await Promise.all([ + User.count(), + Team.count(), + Collection.count(), + Document.count(), + ]); + + const body = JSON.stringify({ + id, + version: 1, + clientVersion: packageInfo.version, + analytics: { + userCount, + teamCount, + collectionCount, + documentCount, + }, + }); + + await client.del('UPDATES_KEY'); + + try { + const response = await fetch(UPDATES_URL, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + }); + + const data = await response.json(); + if (data.severity) { + await client.set( + UPDATES_KEY, + JSON.stringify({ + severity: data.severity, + message: data.message, + url: data.url, + }) + ); + } + } catch (_e) { + // no-op + } +};