Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
729a6ee
mv kansa/ server/
eemeli Sep 8, 2018
0173a01
Refactor hugo as a module, dropping its own server instance
eemeli Sep 8, 2018
dad66d5
Rename service `kansa` as `server`
eemeli Sep 8, 2018
0a93649
Rename `nginx` directory & service as `proxy`
eemeli Sep 8, 2018
2ee8e93
Add image tags to docker-compose config
eemeli Sep 8, 2018
aa93b88
raami: Refactor routes as router
eemeli Sep 8, 2018
6242ebe
raami: Refactor queries as Class, dropping dependency on req.app.locals
eemeli Sep 8, 2018
8fc428f
Refactor raami as a module, dropping its own server instance
eemeli Sep 8, 2018
e80ff78
Move hugo & raami under modules/
eemeli Sep 8, 2018
cfbd440
Move hugo & raami postgres init under modules/
eemeli Sep 8, 2018
80df28d
Simplify module router shape: now called as router(pgp)
eemeli Sep 8, 2018
b5dc376
server: Split lib/router.js from app.js
eemeli Sep 8, 2018
fea10d2
Fix WebSocket routing (dropping obsolete fix for monkeypatch bug)
eemeli Sep 8, 2018
9e41e62
Add modules to config/kansa.yaml
eemeli Sep 8, 2018
942be72
Update README
eemeli Sep 8, 2018
142f58e
Wrap up Slack inviter as a module
eemeli Sep 8, 2018
3c5369b
Run `npm install` for enabled modules at server start
eemeli Sep 8, 2018
c31ab84
Add & publish @kansa/errors at common/errors/
eemeli Sep 8, 2018
3fe4ee8
Get error objects from @kansa/errors (via npm)
eemeli Sep 8, 2018
aa2aa5c
server: Require user.authenticate only for specific paths
eemeli Sep 9, 2018
54a6262
server: Improve module install log prints
eemeli Sep 9, 2018
06d57be
Repackage @kansa/errors as @kansa/common & add README
eemeli Sep 9, 2018
a3b7fd0
Add @kansa/common/split-name & use it for badge names
eemeli Sep 9, 2018
ef42f61
common: Add auth-user
eemeli Sep 9, 2018
b12549c
Bundle @kansa/common in server with make rules
eemeli Sep 9, 2018
84a8944
Use @kansa/common/auth-user for (nearly) all authentication
eemeli Sep 9, 2018
29dc297
Add tests for badge & barcode endpoints
eemeli Sep 8, 2018
99b8fff
Fix Travis CI config to include kansa-common build
eemeli Sep 9, 2018
8c143a0
Add content-type hack for badge tests failing on CI
eemeli Sep 9, 2018
7f8c4d6
Move config from server to common
eemeli Sep 9, 2018
82c3b89
Move mail.js from server/lib/ to common/, refactoring it a bit
eemeli Sep 9, 2018
db82d29
Add common/trueish & drop server/lib/util
eemeli Sep 9, 2018
72e03e8
Add auth.admin_roles to config & drop server/lib/types/admin.js
eemeli Sep 9, 2018
80de660
Move LogEntry from server to common
eemeli Sep 9, 2018
d2437b5
Split admin actions into their own module
eemeli Sep 10, 2018
892a417
Refactor server build: fixes `make update-server`
eemeli Sep 10, 2018
543681b
Drop pgp as first param from modules; use db.$config.pgp instead
eemeli Sep 10, 2018
73fd564
Add modules/README.md
eemeli Sep 10, 2018
1d50260
server: Fix some async postgres task bugs
eemeli Sep 10, 2018
9a7abfa
postgres: Fix for parameter length limit in name_match()
eemeli Sep 10, 2018
fd3064f
Add tests for /people endpoints, fixing route for updates WebSocket
eemeli Sep 10, 2018
f9cab0e
server: Gather people files under lib/people/
eemeli Sep 10, 2018
a5f987c
server: Split people/router.js from router.js
eemeli Sep 10, 2018
285a251
server: Split people/admin-info from people/index, refactoring
eemeli Sep 10, 2018
aed666f
server: Inline user.verifyPeopleAccess in people/router
eemeli Sep 10, 2018
50bb312
Split people/lookup from public
eemeli Sep 11, 2018
2ff25b7
server: Distribute log functions to user & people/index
eemeli Sep 11, 2018
7930862
Inline authAddPerson & authUpgradePerson into people/router
eemeli Sep 11, 2018
cf10fcd
Refactor people/upgrade exports
eemeli Sep 11, 2018
3e9edb0
Split admin from people
eemeli Sep 11, 2018
07987d9
Split people/update from people/index
eemeli Sep 11, 2018
c1d43ca
Split people/add from people/index
eemeli Sep 11, 2018
6c04f93
Refactor people/index as people/get
eemeli Sep 11, 2018
2578c6f
Refactor setKey
eemeli Sep 11, 2018
45a4561
Split public-data tests into their own file, expand them
eemeli Sep 11, 2018
4a1bca1
Split server/lib/public into modules/public & server/lib/get-config
eemeli Sep 11, 2018
f520bac
mv server/app.js server/lib/
eemeli Sep 11, 2018
1b4f11c
server: Split modules from app, refactor purchase/payments pgp depend…
eemeli Sep 11, 2018
36129f6
Add shared context parameter to module init
eemeli Sep 11, 2018
0e30076
Add people functions to context
eemeli Sep 11, 2018
2a935d2
key: Rename setKey -> sendKey, setKeyChecked -> setKey
eemeli Sep 11, 2018
e636db5
Split key into key/{max-age,refresh,reset,send,set}
eemeli Sep 11, 2018
637614c
Split user into user/{info,log,login,logout,router}
eemeli Sep 11, 2018
4192f38
Move send-key route to user/router
eemeli Sep 11, 2018
efcd313
@kansa/common v1.1.0
eemeli Sep 13, 2018
656a16d
Update README
eemeli Sep 13, 2018
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: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/node_modules/
**/package-lock.json
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.*
config/docker-compose.prod.yaml
config/docker-compose.prod.yml
kansa-common*.tgz
node_modules/
package-lock.json

Expand Down
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ node_js:
- 'node'

before_install:
- mv common/$(cd common && npm pack) server/kansa-common.tgz
- docker-compose
-f config/docker-compose.base.yaml
-f config/docker-compose.dev.yaml
Expand Down
24 changes: 24 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM node:10

ARG NODE_ENV
ENV NODE_ENV $NODE_ENV

WORKDIR /kansa
COPY server/package.json .
COPY common/package.json ./common/
RUN npm install && \
npm install ./common && \
npm cache clean --force

COPY modules/raami/package.json modules/raami/
RUN cd modules/raami && npm install && npm cache clean --force

COPY modules/slack/package.json modules/slack/
RUN cd modules/slack && npm install && npm cache clean --force

COPY common ./common
COPY modules ./modules
COPY server .

EXPOSE 80
CMD [ "npm", "start" ]
51 changes: 31 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@
<h1>Kansa</h1>
</div>

Kansa is a convention member management system originally developed for [Worldcon 75](http://www.worldcon.fi),
Kansa is a convention member management system originally developed for [Worldcon 75],
the World Science Fiction Convention organised in Helsinki in 2017. It is also used by
[Dublin 2019: An Irish Worldcon](https://dublin2019.com/).
[Dublin 2019: An Irish Worldcon] and [CoNZealand], the 2020 Worldcon.

The system is modular and extensible. Together with its
[front-end client](https://github.com/maailma/kansa-client) it provides the following services:
The system is modular and extensible. Together with its [front-end client] it provides
the following services:

- Member admin services, including an easy-to use admin front-end
- Support for multiple membership types, as well as for non-member accounts
- [Stripe](https://stripe.com/) integration for membership and other purchases (via credit cards or
- [Stripe] integration for membership and other purchases (via credit cards or
SEPA direct debit)
- Individual and bulk import of member data and transactions from other systems
- Member-facing front-end for e.g. name and address changes
- Passwordless authentication using login links sent by email
- Emails sent using [Sendgrid](https://sendgrid.com/) and customisable
[templates](config/message-templates/)
- Emails sent using [Sendgrid] and customisable [templates](config/message-templates/)
- Synchronisation of contact info to Sendgrid for mass mailings

[worldcon 75]: http://www.worldcon.fi
[dublin 2019: an irish worldcon]: https://dublin2019.com/
[conzealand]: https://conzealand.nz/
[front-end client]: https://github.com/maailma/kansa-client
[stripe]: https://stripe.com/
[sendgrid]: https://sendgrid.com/

To help with at-con registration, Kansa has:

- Pre-con badge preview and customisation (with Unicode support)
Expand Down Expand Up @@ -101,21 +107,26 @@ Email messages are based on message templates, which are

### Directory Overview

- **`config`** - System configuration
- **`hugo`** - Provides the Hugo Nominations and Awards parts of the [REST API](docs/index.md)
- **`integration-tests`** - Tests for the REST API, targeting the Stripe and Sendgrid interfaces in particular
- **`kansa`** - Provides the core parts of the [REST API](docs/index.md)
- **`kyyhky`** - Internal mailing service & [SendGrid](https://sendgrid.com/) integration for hugo & kansa
- **`nginx`** - An SSL-terminating reverse proxy & file server, using [OpenResty](https://openresty.org/)
- **`common`** - Shared utilities for the server & modules, published on npm as [`@kansa/common`][kc]
- **`config`** - System configuration, see in particular [`config/kansa.yaml`](config/kansa.yaml)
- **`integration-tests`** - Tests for the REST API endpoints
- **`kyyhky`** - Internal mailing service & [SendGrid] integration for hugo & kansa
- **`modules`** - Optional server modules providing additional functionality
- **`postgres`** - Configuration & schemas for our database
- **`raami`** - Art show management [REST API](docs/raami.md)
- **`proxy`** - An SSL-terminating reverse proxy & file server, using [OpenResty]
- **`server`** - Provides the core parts of the [REST API](docs/index.md)
- **`tools`** - Semi-automated tools for importing data, and for other tasks
- **`tuohi`** - Fills out a PDF form, for `GET /people/:id/ballot`

[Kansa](https://en.wiktionary.org/wiki/kansa#Finnish) is Finnish for "people" or "tribe", and it's
the name for our member registry. The [Hugo Awards](http://www.thehugoawards.org/) are awards that
are nominated and selected by the members of each year's Worldcon. Kyyhky is Finnish for "pigeon",
Raami is "frame", and Tuohi is the bark of a birch tree.
The [Hugo Awards] are awards that are nominated and selected by the members of each year's Worldcon.
[Kansa] is Finnish for "people" or "tribe", Kyyhky is "pigeon", Raami is "frame", and Tuohi is the
bark of a birch tree.

[kc]: https://www.npmjs.com/package/@kansa/common
[sendgrid]: https://sendgrid.com/
[openresty]: https://openresty.org/
[hugo awards]: http://www.thehugoawards.org/
[kansa]: https://en.wiktionary.org/wiki/kansa#Finnish

### Common Issues

Expand All @@ -125,7 +136,7 @@ The particular places that may need manual adjustment are:
[self-signed certificate](http://www.selfsignedcertificate.com/) for `localhost`. This will not
be automatically accepted by browsers or other clients. If you have a signed certificate you can
use (and therefore a publicly visible address), you'll want to add the certificate files to
`nginx/ssl/` and adjust the environment values set for the `nginx` service in
`proxy/ssl/` and adjust the environment values set for the `proxy` service in
[docker-compose.override.yaml](config/docker-compose.override.yaml) and/or your
`docker-compose.prod.yaml`.

Expand All @@ -134,7 +145,7 @@ The particular places that may need manual adjustment are:
default, the value should match the `http://localhost:8080` address of the client Webpack dev
servers.

- If you're running the server on a separate machine or if you've changed the `nginx` port
- If you're running the server on a separate machine or if you've changed the `proxy` port
configuration, you may need to tell clients where to find the server, using something like
`export API_HOST='remote.example.com'` before running `npm start`.

Expand Down
123 changes: 123 additions & 0 deletions common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# @kansa/common

Common objects and utilities shared across the
[Kansa](https://github.com/maailma/kansa) server & modules.

## User authentication

```js
const { isSignedIn, hasRole, matchesId } = require('@kansa/common/auth-user')
```

### `function isSignedIn(req, res, next)`

Express.js middleware function that verifies that the current user is signed in.
On failure, calls `next(new AuthError())`.

### `function hasRole(role: string | string[]): function(req, res, next)`

Function returning an Express.js middleware function that verifies that the
current user is signed in and has the specified `role` (or one of the roles, if
an array). On failure, calls `next(new AuthError())`.

### `function matchesId(db, req, role?: string | string[]): Promise<number>`

Verifies that the `id` parameter of the request `req` grants access to the
session user's `email`. If set, `role` may define one or multiple roles that the
user may have that grant access without a matching `email`.

`db` should be a `pg-promise` instance, or e.g. a `task` extended from such an
instance. Returns a promise that either resolves to the `id` value, or rejects
with either `AuthError`, `InputError`, or a database error.

### `new AuthError(message: string)`

## Configuration

```js
const config = require('@kansa/common/config')
```

The server configuration, sourced from `config/kansa.yaml` and read during
server start.

## Errors

```js
const { AuthError, InputError } = require('@kansa/common/errors')
```

### `new AuthError(message: string)`

### `new InputError(message: string)`

Handled by the server's error handling. May also have their `status` set.

## Log entries

```js
const LogEntry = require('@kansa/common/log-entry')
new LogEntry(req, 'Log message').write(db)
```

### `new LogEntry(req, message: string)`

Creates a new log entry for tracking changes. The entry's fields such as
`author`, `description` and `subject` are modifiable before the entry gets
stored with its `write(db): Promise` method.

## Mail

### `function sendMail(type: string, data: any, delay?: number): Promise`

Schedule an email message of `type` with `data` to be sent, with an optional
`delay` in minutes. The message should have a correspondingly named template
available under `config/message-templates/`.

### `function updateMailRecipient(db, email: string): Promise`

Using the `pg-promise` instance `db`, fetch the appropriate data for `email`
and forward it to be sent to Sendgrid. Returns a promise that will never reject.

## Split names

```js
const splitName = require('@kansa/common/split-name')

splitName('Bob Tucker')
// ['', 'Bob Tucker']

splitName('Bob Tucker', 8)
// ['Bob', 'Tucker']

splitName('Arthur Wilson "Bob" Tucker')
// [ 'Arthur Wilson', '"Bob" Tucker' ]
```

### `function splitName(name: string, maxLength = 14): [string, string]`

Splits a name (or other string) prettily on two lines.

If the name already includes newlines, the first will be used to split the
string and the others replaced with spaces. If the name is at most
`maxLength` characters, it'll be returned as `['', name]`. Otherwise, we
find the most balanced white space to use as a split point.

## Trueish

```js
const isTrueish = require('@kansa/common/trueish')

isTrueish() // false
isTrueish('0') // false
isTrueish(' False') // false
isTrueish(-1) // true
```

### `function isTrueish(v: any): boolean`

Casts input into boolean values. In addition to normal JavaScript casting
rules, also trims and lower-cases strings before comparing them to the
following: `''`, `'0'`, `'false'`, `'null'`. Matches to these result in a
`false` value. The primary intent is to enable human-friendly handling of query
parameter values.
39 changes: 39 additions & 0 deletions common/auth-user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const { AuthError, InputError } = require('./errors')

module.exports = { isSignedIn, hasRole, matchesId }

function isSignedIn(req, res, next) {
const { user } = req.session
if (user && user.email) next()
else next(new AuthError())
}

function hasRole(role) {
return function _hasRole(req, res, next) {
const { user } = req.session
if (user && user.email) {
if (Array.isArray(role)) {
if (role.some(r => user[r])) return next()
} else {
if (user[role]) return next()
}
}
next(new AuthError())
}
}

function matchesId(db, req, role) {
const id = parseInt(req.params.id)
const { user } = req.session
if (isNaN(id) || id < 0)
return Promise.reject(new InputError('Bad id number'))
if (!user || !user.email) return Promise.reject(new AuthError())
if ((Array.isArray(role) && role.some(r => user[r])) || (role && user[role]))
return Promise.resolve(id)
return db
.oneOrNone('SELECT email FROM people WHERE id = $1', id)
.then(data => {
if (data && user.email === data.email) return id
throw new AuthError()
})
}
13 changes: 12 additions & 1 deletion kansa/lib/config.js → common/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ const shape = {
name: 'string',
paid_paper_pubs: 'boolean',
auth: {
admin_roles: ['string'],
key_timeout: {
admin: 'duration',
normal: 'duration'
},
session_timeout: 'duration'
}
},
modules: 'object'
}

const getIn = (config, path) => {
Expand All @@ -40,6 +42,15 @@ function parseConfig(config, path, shape) {
`Expected value for '${key}' to match regular expression ${shape}`
)
}
} else if (Array.isArray(shape)) {
if (!Array.isArray(value)) {
throw new Error(
`Expected array value for '${key}', but found ${typeof value}`
)
}
value.forEach((v, i) => {
parseConfig(config, path.concat(i), shape[0])
})
} else if (typeof shape === 'object') {
if (typeof value !== 'object') {
throw new Error(
Expand Down
File renamed without changes.
39 changes: 39 additions & 0 deletions common/log-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const logFields = [
// id SERIAL PRIMARY KEY
'timestamp', // timestamptz NOT NULL DEFAULT now()
'client_ip', // text NOT NULL
'client_ua', // text
'author', // text
'subject', // integer REFERENCES People
'action', // text NOT NULL
'parameters', // jsonb NOT NULL
'description' // text NOT NULL
]

class LogEntry {
constructor(req, desc = '') {
const { user } = req.session
this.timestamp = null
if (user && user.member_admin && req.body && req.body.timestamp) {
const ts = new Date(req.body.timestamp)
if (ts > 0) this.timestamp = ts.toISOString()
}
this.client_ip = req.ip
this.client_ua = req.headers['user-agent'] || null
this.author = (user && user.email) || null
this.subject = null
this.action = req.method + ' ' + req.baseUrl + req.path
this.parameters = Object.assign({}, req.query, req.body)
delete this.parameters.key
this.description = desc
}

write(db) {
const fields = logFields.filter(fn => this[fn] !== null)
const values = fields.map(fn => `$(${fn})`).join(', ')
const sqlValues = `(${fields.join(', ')}) VALUES(${values})`
return db.none(`INSERT INTO log ${sqlValues}`, this)
}
}

module.exports = LogEntry
Loading