diff --git a/.env-template b/.env-template
index bb1d803..d7fc23a 100644
--- a/.env-template
+++ b/.env-template
@@ -4,3 +4,4 @@ PGPASSWORD=
PGDATABASE=timesheet
PGPORT=5432
PORT=3000
+TESTAPIKEY=secret
diff --git a/GC.md b/GOOGLE_CLOUD.md
similarity index 100%
rename from GC.md
rename to GOOGLE_CLOUD.md
diff --git a/HEROKU.md b/HEROKU.md
new file mode 100644
index 0000000..f8a6cd2
--- /dev/null
+++ b/HEROKU.md
@@ -0,0 +1,61 @@
+# Heroku Deployment
+
+## Manage Domain
+
+### CNAME Setup for Heroku app
+
+1. Get CNAME from heroku: `heroku domains -a coderbunker-timesheet`
+
+2. add CNAME to google domains
+
+ | NAME | TYPE | TTL | DATA |
+ |--------|:--------------:|------:|--------------------------------------:|
+ | data | CNAME | 1h | data.coderbunker.com.herokudns.com. |
+
+
+### SSL Setup
+
+Enable SSL automatically managed by heroku.
+
+## troubleshooting
+
+want to push an amended history with subtree push? sadly, does not support push.
+
+create a local branch and force push that first:
+
+```
+git subtree split --prefix server -b backup-branch
+git push -f heroku backup-branch:master
+```
+
+should now be back to normal...
+
+## deploying to Heroku
+
+Creating/updating schema on Heroku instance:
+
+```bash
+psql -v "ON_ERROR_STOP=1" -b -1 -e -f sql/PSQL.sql `heroku pg:credentials:url | tail -1`
+```
+
+Restarting the dyno (to load changes to the database for example)
+
+```bash
+heroku restart -a coderbunker-timesheet
+```
+
+## data transfer to/from heroku database
+
+Pushing the local database:
+
+```bash
+heroku pg:push timesheet postgresql-rigid-65921 --app coderbunker-timesheet
+```
+
+Pulling the Heroku database locally and making a copy before changing the pulled version
+(adjust date):
+
+```bash
+heroku pg:pull postgresql-rigid-65921 heroku-timesheet --app coderbunker-timesheet
+psql -c 'CREATE DATABASE "heroku-timesheet-20180416" TEMPLATE "heroku-timesheet";' postgres
+```
diff --git a/INSTALL.md b/INSTALL.md
index 76c186a..86fd045 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -1,4 +1,4 @@
-# Integration with Google Spreadsheet
+# Running the system locally
## setup NodeJS latest LTS
@@ -12,7 +12,7 @@ nvm install --lts
```bash
npm install
-node app.js
+npm start
```
install PostgreSQL extensions:
@@ -21,6 +21,7 @@ install PostgreSQL extensions:
pip install pgxnclient
pgxn install pgtap
```
+
create schema of DB
```bash
@@ -37,28 +38,9 @@ brew install fswatch
./watch-test.sh timesheet
```
-## Apache hook
-
-Apache config:
-
-```text
-
- ProxyPass "http://localhost:3000/spreadsheet"
-
-```
-
-restart:
-
-```bash
-systemctl restart apache2
-```
-
-## Setup
-
-creates two routes:
+## Exposing local as a web service
-* /spreadsheet/snapshot/SPREADSHEET_ID
-* /spreadsheet/change/SPREADSHEET_ID
+Use http://ngrok.io
## Testing
@@ -71,7 +53,7 @@ Sample file:
"apptype": "Spreadsheet",
"category": "Timesheet"
},
- "apikey": "f98dbe87-5749-47c6-8e39-47ae7ff401ac"
+ "apikey": "STRING_CONFIGURED_IN_ENV"
}
```
diff --git a/app.js b/app.js
index 81371b3..efefc25 100644
--- a/app.js
+++ b/app.js
@@ -1,42 +1,130 @@
require('dotenv').config()
const express = require('express')
-const mountRoutes = require('./routes')
+const Router = require('express-promise-router')
const bodyParser = require('body-parser')
const { postgraphile } = require("postgraphile");
+const cookieParser = require('cookie-parser');
+const { Pool } = require('pg')
-const app = express()
+function createQuery(dburl) {
+ console.log('connecting to DATABASE_URL=%s', dburl);
-app.use(postgraphile(
- process.env.DATABASE_URL || "postgres://localhost/heroku-timesheet",
- "postgraphql",
- {
+ const pool = new Pool({
+ connectionString: dburl
+ });
+
+ return function(text, params) {
+ pool.query(text, params);
+ }
+}
+
+function postgraphileDefaultConfig(config) {
+ config = config || {};
+ return Object.assign({
dynamicJson: true,
- disableDefaultMutations: true,
+ disableDefaultMutations: false,
graphiql: true,
watchPg: true,
- enableCors: true
+ enableCors: true,
+ extendedErrors: ['hint', 'detail', 'errcode']
+ }, config);
+}
+
+function validateApiKey(apikey) {
+ return function(req, res, next) {
+ function err(e) {
+ res.writeHead(400);
+ res.end(JSON.stringify({error: e}));
+ }
+ if(req.originalUrl.match(/internal/)) {
+ if(!((req.body && req.body.apikey === apikey) || req.cookies.apikey === apikey)) {
+ return err(`invalid api key for ${req.originalUrl}`);
+ }
+ }
+ next();
}
-));
+}
+
+// create a new express-promise-router
+// this has the same API as the normal express router except
+// it allows you to use async functions as route handlers
+function createRouter(router, query) {
+ router.post('/snapshot', async (req, res) => {
+ function err(e) {
+ res.writeHead(400);
+ res.end(JSON.stringify({error: e}));
+ }
+
+ if(!req.body.id) {
+ return err("id is not defined in body")
+ }
+ if(!req.body.doc) {
+ return err("doc is not defined in body")
+ }
+ const { rows, fields } = await query(
+ `SELECT api.snapshot_json($1, $2::json)`,
+ [
+ req.body.id,
+ JSON.stringify(req.body.doc)
+ ])
+ var json = JSON.stringify(rows[0].snapshot_json);
+ res.writeHead(200, {'content-type':'application/json', 'content-length': Buffer.byteLength(json)});
+ res.end(json);
+ });
+
+ return router;
+}
+
+function registerEndpoint(app, schema, endpoints) {
+ if(!endpoints) {
+ endpoints = {
+ graphqlRoute: `/${schema}/graphql`,
+ graphiqlRoute: `/${schema}/graphiql`
+ }
+ }
+
+ app.use(postgraphile(
+ process.env.DATABASE_URL || "postgres://localhost/heroku-timesheet",
+ schema,
+ postgraphileDefaultConfig(endpoints)
+ ));
+}
+
+function createServer(app, router, mountPoint, apikey, port) {
+ app.use(mountPoint, router);
+ app.use(cookieParser());
+ app.use(validateApiKey(apikey));
+
+ registerEndpoint(app, 'postgraphql', {});
+ registerEndpoint(app, 'internal');
+
+ // parse application/x-www-form-urlencoded
+ app.use(bodyParser.urlencoded({
+ extended: false
+ }))
-// parse application/x-www-form-urlencoded
-app.use(bodyParser.urlencoded({
- extended: false
-}))
+ // parse application/json
+ app.use(bodyParser.json({
+ limit: "5mb"
+ }))
-// parse application/json
-app.use(bodyParser.json({
- limit: "5mb"
-}))
+ app.use(express.static(__dirname + '/public'));
-app.use(express.static(__dirname + '/public'));
+ var server = app.listen(port, function() {
+ console.log(JSON.stringify(server.address()))
+ const host = server.address().address;
+ const port = server.address().port;
+ console.log('timesheet app listening at http://%s:%s', host, port);
+ });
-mountRoutes(app)
+ return server;
+}
-console.log('port: %s', process.env.PORT)
-var server = app.listen(process.env.PORT, () => {
- console.log(JSON.stringify(server.address()))
- const host = server.address().address;
- const port = server.address().port;
- console.log('timesheet app listening at http://%s:%s', host, port);
-});
+if (require.main === module) {
+ const query = createQuery(process.env.DATABASE_URL);
+ const router = createRouter(new Router(), query);
+ const server = createServer(express(), router, '/gsuite', process.env.APIKEY, process.env.PORT);
+} else {
+ module.exports = { createServer, createRouter, createQuery };
+}
\ No newline at end of file
diff --git a/db/index.js b/db/index.js
deleted file mode 100644
index e1588bc..0000000
--- a/db/index.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const { Pool } = require('pg')
-
-console.log('connecting to DATABASE_URL=%s', process.env.DATABASE_URL)
-const pool = new Pool({
- connectionString: process.env.DATABASE_URL
-});
-
-module.exports = {
- query: (text, params) => pool.query(text, params)
-}
\ No newline at end of file
diff --git a/package.json b/package.json
index 4545e78..35beed2 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,8 @@
"description": "First version of the timesheet backend",
"main": "index.js",
"scripts": {
- "start": "node app.js"
+ "start": "node app.js",
+ "test": "mocha"
},
"repository": {
"type": "git",
@@ -17,12 +18,19 @@
},
"homepage": "https://github.com/coderbunker/timesheet-backend#readme",
"dependencies": {
+ "base64url": "^3.0.0",
"body-parser": "^1.18.2",
+ "buffer-equal-constant-time": "^1.0.1",
+ "cookie-parser": "^1.4.3",
"dotenv": "^5.0.0",
"express": "^4.16.2",
"express-promise-router": "^3.0.1",
+ "find-free-port": "^2.0.0",
"googleapis": "^26.0.1",
"pg": "^7.4.1",
- "postgraphile": "^4.0.0-beta.2"
+ "postgraphile": "^4.0.0-rc.3"
+ },
+ "devDependencies": {
+ "isomorphic-fetch": "^2.2.1"
}
}
diff --git a/routes/index.js b/routes/index.js
deleted file mode 100644
index de4873f..0000000
--- a/routes/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-const snapshot = require('./snapshot')
-
-module.exports = (app) => {
- app.use('/gsuite', snapshot)
-}
\ No newline at end of file
diff --git a/routes/snapshot.js b/routes/snapshot.js
deleted file mode 100644
index 45ef2fd..0000000
--- a/routes/snapshot.js
+++ /dev/null
@@ -1,37 +0,0 @@
-const Router = require('express-promise-router')
-
-const db = require('../db')
-
-// create a new express-promise-router
-// this has the same API as the normal express router except
-// it allows you to use async functions as route handlers
-const router = new Router()
-
-// export our router to be mounted by the parent application
-module.exports = router
-
-router.post('/snapshot', async (req, res) => {
- function err(e) {
- res.writeHead(400);
- res.end(JSON.stringify({error: e}));
- }
- if(req.body.apikey !== process.env.APIKEY) {
- return err("invalid api key")
- }
- if(!req.body.id) {
- return err("id is not defined in body")
- }
- if(!req.body.doc) {
- return err("doc is not defined in body")
- }
-
- const { rows, fields } = await db.query(
- `SELECT api.snapshot_json($1, $2::json)`,
- [
- req.body.id,
- JSON.stringify(req.body.doc)
- ])
- var json = JSON.stringify(rows[0].snapshot_json);
- res.writeHead(200, {'content-type':'application/json', 'content-length': Buffer.byteLength(json)});
- res.end(json);
-});
\ No newline at end of file
diff --git a/sql/010-psql-create-incoming.sql b/sql/010-psql-create-incoming.sql
index 78b3d59..22e235f 100644
--- a/sql/010-psql-create-incoming.sql
+++ b/sql/010-psql-create-incoming.sql
@@ -10,6 +10,7 @@
\ir incoming/profile.sql
\ir incoming/entry.sql
\ir incoming/people.sql
+\ir incoming/group.sql
\ir incoming/transfer.sql
\ir incoming/waveapps.sql
diff --git a/sql/070-psql-create-internal.sql b/sql/070-psql-create-internal.sql
new file mode 100644
index 0000000..1f9d98a
--- /dev/null
+++ b/sql/070-psql-create-internal.sql
@@ -0,0 +1,4 @@
+
+-- internal interface
+\ir internal/schema.sql
+\ir internal/group.sql
diff --git a/sql/PSQL.sql b/sql/PSQL.sql
index 9348b72..8b4b182 100644
--- a/sql/PSQL.sql
+++ b/sql/PSQL.sql
@@ -9,6 +9,7 @@
\ir 030-psql-incoming-to-model.sql
\ir 040-psql-create-reports.sql
\ir 050-psql-create-postgraphql.sql
+\ir 070-psql-create-internal.sql
-- \ir 900-psql-testsuite.sql
\echo "Success. Please don't forget to run tests using watch-test.sh"
\ No newline at end of file
diff --git a/sql/api/update_from_server.sql b/sql/api/update_from_server.sql
index 5a24440..4ca372b 100644
--- a/sql/api/update_from_server.sql
+++ b/sql/api/update_from_server.sql
@@ -1,6 +1,6 @@
-- Usage:
-- SELECT * FROM api.update_from_server(
---