Skip to content

Commit

Permalink
Update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
danneu committed Apr 20, 2018
1 parent b8df32e commit 30ccf95
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 79 deletions.
149 changes: 78 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@

An example Koa application that glues together Koa + Postgres + good defaults + common abstractions that I frequently use to create web applications.

Also used as a test bed for my own libraries.

Originally this project was intended to be forked and modified, but it's grown to the point
that it's better left as a demonstration of how one can structure a Koa + Postgres application.

- Live Demo: https://koa-skeleton.danneu.com/
* Live Demo: https://koa-skeleton.danneu.com/

[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/danneu/koa-skeleton)

## The Stack

Depends on Node v8.x+:

- **Micro-framework**: [Koa 2.x](http://koajs.com/). It's very similar to [Express](http://expressjs.com/) except it supports async/await.
- **Database**: [Postgres](http://www.postgresql.org/).
- **User-input validation**: [koa-bouncer](https://github.com/danneu/koa-bouncer).
- **HTML templating**: [React/JSX](https://github.com/danneu/react-template-render). HTML is rendered on the server via React JSX templates.
- **Deployment**: [Heroku](https://heroku.com/). Keeps things easy while you focus on coding your webapp. Forces you to write your webapp statelessly and horizontally-scalably.
* **Micro-framework**: [Koa 2.x](http://koajs.com/). It's very similar to [Express](http://expressjs.com/) except it supports async/await.
* **Database**: [Postgres](http://www.postgresql.org/).
* **User-input validation**: [koa-bouncer](https://github.com/danneu/koa-bouncer).
* **HTML templating**: [React/JSX](https://github.com/danneu/react-template-render). HTML is rendered on the server via React JSX templates.
* **Deployment**: [Heroku](https://heroku.com/). Keeps things easy while you focus on coding your webapp. Forces you to write your webapp statelessly and horizontally-scalably.

## Setup

Expand Down Expand Up @@ -58,92 +60,97 @@ like on Heroku).

You can look at `src/config.js` to view these and their defaults.

| Evironment Variable | Type | Default | Description |
| --- | --- | --- | --- |
| <code>NODE_ENV</code> | String | "development" | Set to `"production"` on the production server to enable some optimizations and security checks that are turned off in development for convenience. |
| <code>PORT</code> | Integer | 3000 | Overriden by Heroku in production. |
| <code>DATABASE_URL</code> | String | "postgres://localhost:5432/koa-skeleton" | Overriden by Heroku in production if you use its Heroku Postgres addon. |
| <code>TRUST_PROXY</code> | Boolean | false | Set it to the string `"true"` to turn it on. Turn it on if you're behind a proxy like Cloudflare which means you can trust the IP address supplied in the `X-Forwarded-For` header. If so, then `ctx.request.ip` will use that header if it's set. |
| <code>HOSTNAME</code> | String | undefined | Set it to your hostname in production to enable basic CSRF protection. i.e. `example.com`, `subdomain.example.com`. If set, then any requests not one of `GET | HEAD | OPTIONS` must have a `Referer` header set that originates from the given HOSTNAME. The referer is always set for `<form>` submissions, for example. Very crude protection. |
| <code>RECAPTCHA_SITEKEY</code> | String | undefined | Must be set to enable the Recaptcha system. <https://www.google.com/recaptcha> |
| <code>RECAPTCHA_SITESECRET</code> | String | undefined | Must be set to enable the Recaptcha system. <https://www.google.com/recaptcha> |
| <code>MESSAGES_PER_PAGE</code> | Integer | 10 | Determines how many messages to show per page when viewing paginated lists |
| <code>USERS_PER_PAGE</code> | Integer | 10 | Determines how many users to show per page when viewing paginated lists |
| Evironment Variable | Type | Default | Description |
| --------------------------------- | ------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <code>NODE_ENV</code> | String | "development" | Set to `"production"` on the production server to enable some optimizations and security checks that are turned off in development for convenience. |
| <code>PORT</code> | Integer | 3000 | Overriden by Heroku in production. |
| <code>DATABASE_URL</code> | String | "postgres://localhost:5432/koa-skeleton" | Overriden by Heroku in production if you use its Heroku Postgres addon. |
| <code>TRUST_PROXY</code> | Boolean | false | Set it to the string `"true"` to turn it on. Turn it on if you're behind a proxy like Cloudflare which means you can trust the IP address supplied in the `X-Forwarded-For` header. If so, then `ctx.request.ip` will use that header if it's set. |
| <code>HOSTNAME</code> | String | undefined | Set it to your hostname in production to enable basic CSRF protection. i.e. `example.com`, `subdomain.example.com`. If set, then any requests not one of `GET | HEAD | OPTIONS` must have a `Referer` header set that originates from the given HOSTNAME. The referer is always set for `<form>` submissions, for example. Very crude protection. |
| <code>RECAPTCHA_SITEKEY</code> | String | undefined | Must be set to enable the Recaptcha system. <https://www.google.com/recaptcha> |
| <code>RECAPTCHA_SITESECRET</code> | String | undefined | Must be set to enable the Recaptcha system. <https://www.google.com/recaptcha> |
| <code>MESSAGES_PER_PAGE</code> | Integer | 10 | Determines how many messages to show per page when viewing paginated lists |
| <code>USERS_PER_PAGE</code> | Integer | 10 | Determines how many users to show per page when viewing paginated lists |

Don't access `process.env.*` directly in the app.
Instead, require the `src/config.js` and access them there.

## Features/Demonstrations

- **User authentication** (/register, /login). Sessions are backed by the database and persisted on the client with a cookie `session_id=<UUID v4>`.
- **Role-based user authorization** (i.e. the permission system). Core abstraction is basically `can(currUser, action, target) -> Boolean`, which is implemented as one big switch statement. For example: `can(currUser, 'READ_TOPIC', topic)`. Inspired by [ryanb/cancan](https://github.com/ryanb/cancan), though my abstraction is an ultra simplification. My user table often has a `role` column that's either one of `ADMIN` | `MOD` | `MEMBER` (default) | `BANNED`.
- **Recaptcha integration**. Protect any route with Recaptcha by adding the `ensureRecaptcha` middleware to the route.
- Flash message cookie. You often want to display simple messages like "Topic created successfully" to the user, but you want the message to last just one request and survive any redirects.
- Relative human-friendly timestamps like 'Created 4 hours ago' that are updated live via Javascript as the user stays on the page. I accomplish this with the [timeago](http://timeago.yarp.com/) jQuery plugin.
- Comes with Bootstrap v3.x. I start almost every project with Bootstrap so that I can focus on the back-end code and have a decent looking front-end with minimal effort.
- `npm run reset-db`. During early development, I like to have a `reset-db` command that I can spam that will delete the schema, recreate it, and insert any sample data I put in a `seeds.sql` file.
- Ratelimit middleware. An IP address can only insert a message every X seconds.
* **User authentication** (/register, /login). Sessions are backed by the database and persisted on the client with a cookie `session_id=<UUID v4>`.
* **Role-based user authorization** (i.e. the permission system). Core abstraction is basically `can(currUser, action, target) -> Boolean`, which is implemented as one big switch statement. For example: `can(currUser, 'READ_TOPIC', topic)`. Inspired by [ryanb/cancan](https://github.com/ryanb/cancan), though my abstraction is an ultra simplification. My user table often has a `role` column that's either one of `ADMIN` | `MOD` | `MEMBER` (default) | `BANNED`.
* **Recaptcha integration**. Protect any route with Recaptcha by adding the `ensureRecaptcha` middleware to the route.
* Flash message cookie. You often want to display simple messages like "Topic created successfully" to the user, but you want the message to last just one request and survive any redirects.
* Relative human-friendly timestamps like 'Created 4 hours ago' that are updated live via Javascript as the user stays on the page. I accomplish this with the [timeago](http://timeago.yarp.com/) jQuery plugin.
* Comes with Bootstrap v3.x. I start almost every project with Bootstrap so that I can focus on the back-end code and have a decent looking front-end with minimal effort.
* `npm run reset-db`. During early development, I like to have a `reset-db` command that I can spam that will delete the schema, recreate it, and insert any sample data I put in a `seeds.sql` file.
* Ratelimit middleware. An IP address can only insert a message every X seconds.

## Philosophy/Opinions

- It's better to write explicit glue code between small libraries than credentializing in larger libraries/frameworks that try to do everything for you. When you return to a project in eight months, it's generally easier to catch up by reading explicit glue code then library idiosyncrasies. Similarly, it's easier to catch up by reading SQL strings than your clever ORM backflips.
- Just write SQL. When you need more complex/composable queries (like a /search endpoint with various filter options), consider using a SQL query building library like [knex.js](http://knexjs.org/).
- Use whichever Javascript features that are supported by the lastest stable version of Node. I don't think Babel compilation and the resulting idiosyncrasies are worth the build step.
* It's better to write explicit glue code between small libraries than credentializing in larger libraries/frameworks that try to do everything for you. When you return to a project in eight months, it's generally easier to catch up by reading explicit glue code then library idiosyncrasies. Similarly, it's easier to catch up by reading SQL strings than your clever ORM backflips.
* Just write SQL. When you need more complex/composable queries (like a /search endpoint with various filter options), consider using a SQL query building library like [knex.js](http://knexjs.org/).
* Use whichever Javascript features that are supported by the lastest stable version of Node. I don't think Babel compilation and the resulting idiosyncrasies are worth the build step.

## Conventions

- Aside from validation, never access query/body/url params via the Koa default like `ctx.request.body.username`. Instead, use koa-bouncer to move these to the `ctx.vals` object and access them there. This forces you to self-document what params you expect at the top of your route and prevents the case where you forget to validate params.
* Aside from validation, never access query/body/url params via the Koa default like `ctx.request.body.username`. Instead, use koa-bouncer to move these to the `ctx.vals` object and access them there. This forces you to self-document what params you expect at the top of your route and prevents the case where you forget to validate params.

``` javascript
```javascript
router.post('/users', async (ctx, next) => {

// Validation

ctx.validateBody('uname')
.isString('Username required')
.trim()
.isLength(3, 15, 'Username must be 3-15 chars')
ctx.validateBody('email')
.optional()
.isString()
.trim()
.isEmail()
ctx.validateBody('password1')
.isString('Password required')
.isLength(6, 100, 'Password must be 6-100 chars')
ctx.validateBody('password2')
.isString('Password confirmation required')
.eq(ctx.vals.password1, 'Passwords must match')

// Validation passed. Access the above params via `ctx.vals` for
// the remainder of the route to ensure you're getting the validated
// version.

const user = await db.insertUser(
ctx.vals.uname, ctx.vals.password1, ctx.vals.email
)

ctx.redirect(`/users/${user.uname}`)
// Validation

ctx
.validateBody('uname')
.isString('Username required')
.trim()
.isLength(3, 15, 'Username must be 3-15 chars')
ctx
.validateBody('email')
.optional()
.isString()
.trim()
.isEmail()
ctx
.validateBody('password1')
.isString('Password required')
.isLength(6, 100, 'Password must be 6-100 chars')
ctx
.validateBody('password2')
.isString('Password confirmation required')
.eq(ctx.vals.password1, 'Passwords must match')

// Validation passed. Access the above params via `ctx.vals` for
// the remainder of the route to ensure you're getting the validated
// version.

const user = await db.insertUser(
ctx.vals.uname,
ctx.vals.password1,
ctx.vals.email
)

ctx.redirect(`/users/${user.uname}`)
})
```

## Changelog

The following version numbers are meaningless.

- `4.0.0` 26 Nov 2017
- Replace Pug with React/JSX for HTML templating.
- `3.1.0` 14 Nov 2017
- Replace Nunjucks with Pug for HTML templating.
- `3.0.0` 25 Apr 2017
- Removed Babel since the features we want are now supported by Node 7.x.
- `2.0.0` 29 Oct 2015
- Refactored from Koa 1.x to Koa 2.x.
- `0.1.0` 29 Oct 2016
- koa-skeleton is a year old, but I just started versioning it
started at v0.1.0.
- Extracted `src/db/util.js` into `pg-extra` npm module.
Now util.js just exports the connection pool for other modules to use.
* `4.0.0` 26 Nov 2017
* Replace Pug with React/JSX for HTML templating.
* `3.1.0` 14 Nov 2017
* Replace Nunjucks with Pug for HTML templating.
* `3.0.0` 25 Apr 2017
* Removed Babel since the features we want are now supported by Node 7.x.
* `2.0.0` 29 Oct 2015
* Refactored from Koa 1.x to Koa 2.x.
* `0.1.0` 29 Oct 2016
* koa-skeleton is a year old, but I just started versioning it
started at v0.1.0.
* Extracted `src/db/util.js` into `pg-extra` npm module.
Now util.js just exports the connection pool for other modules to use.

## License

Expand Down
17 changes: 11 additions & 6 deletions src/cancan.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
const debug = require('debug')('app:cancan')
const assert = require('better-assert')

module.exports = {
can,
isAdmin,
}

// Is `user` an admin?
//
// This convenience function exists so that we don't have to first check
// if `user` is defined before passing it in.
exports.isAdmin = function(user) {
function isAdmin(user) {
if (!user) return false
return user.role === 'ADMIN'
}
Expand All @@ -21,7 +26,7 @@ exports.isAdmin = function(user) {
//
// can(this.currUser, 'READ_TOPIC', topic)
// can(this.currUser, 'CREATE_TOPIC')
exports.can = function(user, action, target) {
function can(user, action, target) {
assert(typeof action === 'string')

switch (action) {
Expand All @@ -35,8 +40,8 @@ exports.can = function(user, action, target) {
return false
case 'UPDATE_USER_*': // target is other user
return (
exports.can(user, 'UPDATE_USER_SETTINGS', target) ||
exports.can(user, 'UPDATE_USER_ROLE', target)
can(user, 'UPDATE_USER_SETTINGS', target) ||
can(user, 'UPDATE_USER_ROLE', target)
)
case 'UPDATE_USER_SETTINGS': // target is other user
assert(target)
Expand Down Expand Up @@ -80,8 +85,8 @@ exports.can = function(user, action, target) {
case 'UPDATE_MESSAGE': // target is message
assert(target)
return (
exports.can(user, 'UPDATE_MESSAGE_STATE', target) ||
exports.can(user, 'UPDATE_MESSAGE_MARKUP', target)
can(user, 'UPDATE_MESSAGE_STATE', target) ||
can(user, 'UPDATE_MESSAGE_MARKUP', target)
)
// Can user change message.is_hidden?
case 'UPDATE_MESSAGE_STATE': // target is message
Expand Down
2 changes: 1 addition & 1 deletion src/db/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const pg = extend(require('pg'))
// 1st
const { DATABASE_URL } = require('../config')

// This is the connection pool the rest of our db namespace
// This is the connection pool singleton the rest of our db namespace
// should import and use
const pool = new pg.Pool({ connectionString: DATABASE_URL })

Expand Down
2 changes: 1 addition & 1 deletion src/middleware/react-render.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const makeRender = require('react-template-render')

// Adds ctx.render() method to koa context for rendering our pug templates
// Adds ctx.render() method to koa context for rendering our .jsx templates
module.exports = function reactRender(root, opts) {
return async (ctx, next) => {
ctx.renderer = makeRender(root, opts)
Expand Down
3 changes: 3 additions & 0 deletions src/middleware/remove-trailing-slash.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// If request path ends in '/', redirect to path without slash.
//
// Avoids dupe-content URLs.
module.exports = function removeTrailingSlash() {
return async (ctx, next) => {
if (ctx.path.length > 1 && ctx.path.endsWith('/')) {
Expand Down

0 comments on commit 30ccf95

Please sign in to comment.