diff --git a/README.md b/README.md index 79cc6f0..4f313a8 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ adapt the behavior to your needs. to `/`). The value should end with a trailing slash. * `THUNDERFOREST_API_KEY=` to pass an API key server to use for [Thunderforest](http://thunderforest.com/) tiles (OpenCycleMap, etc). +* `API_TOKEN=` to pass a token required to access the server side API (check + below in the server part environment variables for more details). You should also have a look at the build variables under the `config/` subdirectory. @@ -79,6 +81,7 @@ adapt its behavior: * `DATABASE=` to specify a [database URL](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#db-url) to connect to (defaults to `sqlite:///reports.db` which means a SQLite database named `reports.db` in the current working directory). +* `API_TOKEN=` to specify a token required to `POST` data to the API. #### Serving in production diff --git a/config/prod.env.js b/config/prod.env.js index 6f56a08..d2123af 100644 --- a/config/prod.env.js +++ b/config/prod.env.js @@ -1,6 +1,7 @@ 'use strict' module.exports = { - NODE_ENV: '"production"', - API_BASE_URL: JSON.stringify(process.env.API_BASE_URL), - THUNDERFOREST_API_KEY: JSON.stringify(process.env.THUNDERFOREST_API_KEY), + NODE_ENV: '"production"', + API_BASE_URL: JSON.stringify(process.env.API_BASE_URL), + API_TOKEN: JSON.stringify(process.env.API_TOKEN), + THUNDERFOREST_API_KEY: JSON.stringify(process.env.THUNDERFOREST_API_KEY), } diff --git a/server/jsonapi.py b/server/jsonapi.py index 58498bb..86c1a4b 100644 --- a/server/jsonapi.py +++ b/server/jsonapi.py @@ -34,7 +34,8 @@ def enable_cors(): 'PUT, GET, POST, DELETE, OPTIONS, PATCH' ) bottle.response.headers[str('Access-Control-Allow-Headers')] = str( - 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' + 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token, ' + 'Authorization' ) diff --git a/server/routes.py b/server/routes.py index bae2db4..124a92c 100644 --- a/server/routes.py +++ b/server/routes.py @@ -5,6 +5,7 @@ Routes definitions """ import arrow import json +import os import bottle @@ -12,6 +13,29 @@ from server.models import Report from server import jsonapi +class AuthenticationError(Exception): + pass + + +def check_auth(): + """ + Check authentication. + + :return: Abort and return a HTTP 403 page if authentication is not ok. + """ + if not os.getenv('API_TOKEN'): + return + + auth = bottle.request.headers.get('Authorization', None) + if not auth: + raise AuthenticationError() + parts = auth.split() + if parts[0].lower() != 'bearer' or parts[1] != os.getenv('API_TOKEN'): + raise AuthenticationError() + + return + + def get_reports(only_active=False): """ Get reports for the reports getting routes. @@ -151,6 +175,12 @@ def post_report(): if bottle.request.method == 'OPTIONS': return {} + # Check authentication + try: + check_auth() + except AuthenticationError: + return jsonapi.JsonApiError(403, "Invalid authentication.") + try: payload = json.load(bottle.request.body) except ValueError as exc: @@ -189,6 +219,12 @@ def upvote_report(id): if bottle.request.method == 'OPTIONS': return {} + # Check authentication + try: + check_auth() + except AuthenticationError: + return jsonapi.JsonApiError(403, "Invalid authentication.") + r = Report.get(Report.id == id) if not r: return jsonapi.JsonApiError(404, "Invalid report id.") @@ -213,6 +249,12 @@ def downvote_report(id): if bottle.request.method == 'OPTIONS': return {} + # Check authentication + try: + check_auth() + except AuthenticationError: + return jsonapi.JsonApiError(403, "Invalid authentication.") + r = Report.get(Report.id == id) if not r: return jsonapi.JsonApiError(404, "Invalid report id.") diff --git a/src/api/index.js b/src/api/index.js index f8afbe3..4cb0a73 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -3,6 +3,10 @@ require('isomorphic-fetch'); // With trailing slash export const BASE_URL = process.env.API_BASE_URL || '/'; +const AUTHORIZATION_HEADERS = new Headers({}); +if (process.env.API_TOKEN) { + AUTHORIZATION_HEADERS.set('Authorization', `Bearer ${process.env.API_TOKEN}`); +} export function saveReport(type, lat, lng) { return fetch(`${BASE_URL}api/v1/reports`, { @@ -12,6 +16,7 @@ export function saveReport(type, lat, lng) { lat, lng, }), + headers: AUTHORIZATION_HEADERS, }) .then(response => response.json()) .then(response => response.data) @@ -34,6 +39,7 @@ export function getActiveReports() { export function downvote(id) { return fetch(`${BASE_URL}api/v1/reports/${id}/downvote`, { method: 'POST', + headers: AUTHORIZATION_HEADERS, }) .then(response => response.json()) .then(response => response.data) @@ -46,6 +52,7 @@ export function downvote(id) { export function upvote(id) { return fetch(`${BASE_URL}api/v1/reports/${id}/upvote`, { method: 'POST', + headers: AUTHORIZATION_HEADERS, }) .then(response => response.json()) .then(response => response.data)