Node API attempt #1

Merged
danieljsummers merged 4 commits from node-api-attempt into master 2017-09-21 23:47:59 +00:00
12 changed files with 1179 additions and 293 deletions
Showing only changes of commit 794a365f08 - Show all commits

22
src/api/app.js Normal file
View File

@ -0,0 +1,22 @@
'use strict'
const chalk = require('chalk')
const env = process.env.NODE_ENV || 'dev'
if ('dev' === env) require('babel-register')
const fullEnv = ('dev' === env) ? 'Development' : 'Production'
/** Configuration for the application */
const appConfig = require('./appsettings.json')
/** Express app */
const app = require('./index').default
// Ensure the database exists...
require('./db').default.verify().then(() =>
// ...and start it up!
app.listen(appConfig.port, () => {
console.log(chalk`{reset myPrayerJournal | Port: {bold ${appConfig.port}} | Mode: {bold ${fullEnv}}}`)
}))

View File

@ -1,6 +1,6 @@
'use strict' 'use strict'
const { Pool } = require('pg') import { Pool } from 'pg'
/** /**
* SQL to check the existence of a table in the mpj schema * SQL to check the existence of a table in the mpj schema
@ -14,7 +14,7 @@ const tableSql = table => `SELECT 1 FROM pg_tables WHERE schemaname='mpj' AND ta
* @param {string} index The name of the index * @param {string} index The name of the index
*/ */
const indexSql = (table, index) => const indexSql = (table, index) =>
`SELECT 1 FROM pg_indexes WHERE schemaname='mpj' AND tablename='${table}' AND indexname='${index}'` `SELECT 1 FROM pg_indexes WHERE schemaname='mpj' AND tablename='${table}' AND indexname='${index}'`
const ddl = [ const ddl = [
{ {
@ -55,7 +55,7 @@ const ddl = [
} }
] ]
module.exports = query => { export default function (query) {
return { return {
/** /**
* Ensure that the database schema, tables, and indexes exist * Ensure that the database schema, tables, and indexes exist

View File

@ -1,9 +1,13 @@
'use strict' 'use strict'
const { Pool } = require('pg') import { Pool } from 'pg'
import appConfig from '../appsettings.json'
import ddl from './ddl'
import request from './request'
/** Pooled PostgreSQL instance */ /** Pooled PostgreSQL instance */
const pool = new Pool(require('../appsettings.json').pgPool) const pool = new Pool(appConfig.pgPool)
/** /**
* Run a SQL query * Run a SQL query
@ -12,8 +16,8 @@ const pool = new Pool(require('../appsettings.json').pgPool)
*/ */
const query = (text, params) => pool.query(text, params) const query = (text, params) => pool.query(text, params)
module.exports = { export default {
query: query, query: query,
request: require('./request')(query), request: request(query),
verify: require('./ddl')(query).ensureDatabase verify: ddl(query).ensureDatabase
} }

View File

@ -1,8 +1,9 @@
'use strict' 'use strict'
const { Pool } = require('pg') import { Pool } from 'pg'
import cuid from 'cuid'
module.exports = query => { export default function (query) {
return { return {
/** /**
* Get the current requests for a user (i.e., their complete current journal) * Get the current requests for a user (i.e., their complete current journal)
@ -10,6 +11,24 @@ module.exports = query => {
* @return The requests that make up the current journal * @return The requests that make up the current journal
*/ */
journal: async userId => journal: async userId =>
(await query('SELECT "requestId" FROM request WHERE "userId" = $1', [userId])).rows (await query('SELECT "requestId" FROM request WHERE "userId" = $1', [userId])).rows,
/**
* Add a new prayer request
* @param {string} userId The Id of the user
* @param {string} requestText The text of the request
* @return {string} The Id of the created request
*/
addNew: async (userId, requestText) => {
const id = cuid()
const enteredOn = Date.now()
await query(`
BEGIN;
INSERT INTO request ("requestId", "enteredOn", "userId") VALUES ($1, $2, $3);
INSERT INTO history ("requestId", "asOf", "status", "text") VALUES ($1, $2, 'Created', $4);
COMMIT;`,
[ id, enteredOn, userId, requestText ])
return id
}
} }
} }

View File

@ -1,35 +1,33 @@
'use strict' 'use strict'
const express = require('express') import Koa from 'koa'
import morgan from 'koa-morgan'
import send from 'koa-send'
import serveFrom from 'koa-static'
/** Configuration for the application */ import appConfig from './appsettings.json'
const appConfig = require('./appsettings.json') import router from './routes'
/** Express app */ /** Koa app */
const app = express() const app = new Koa()
// Serve the Vue files from /public export default app
app.use(express.static('public')) // Logging FTW!
.use(morgan('dev'))
// Logging FTW! // Serve the Vue files from /public
app.use(require('morgan')('dev')) .use(serveFrom('public'))
// Tie in all the routes
// Tie in all the API routes .use(router.routes())
require('./routes').mount(app) .use(router.allowedMethods())
// Send the index.html file for what would normally get a 404
// Send the index.html file for what would normally get a 404 .use(async (ctx, next) => {
app.use(async (req, res, next) => { if (ctx.url.indexOf('/api') === -1) {
try { try {
await res.sendFile('index.html', { root: __dirname + '/public/', dotfiles: 'deny' }) await send(ctx, 'index.html', { root: __dirname + '/public/' })
} }
catch (err) { catch (err) {
return next(err) return await next(err)
} }
}) }
return await next()
// Ensure the database exists... })
require('./db').verify().then(() =>
// ...and start it up!
app.listen(appConfig.port, () => {
console.log(`Listening on port ${appConfig.port}`)
}))

View File

@ -7,16 +7,37 @@
"author": "Daniel J. Summers <daniel@djs-consulting.com>", "author": "Daniel J. Summers <daniel@djs-consulting.com>",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chalk": "^2.1.0",
"cuid": "^1.3.8", "cuid": "^1.3.8",
"express": "^4.15.4", "jwks-rsa-koa": "^1.1.3",
"express-jwt": "^5.3.0", "koa": "^2.3.0",
"express-promise-router": "^2.0.0", "koa-bodyparser": "^4.2.0",
"jwks-rsa": "^1.2.0", "koa-jwt": "^3.2.2",
"morgan": "^1.8.2", "koa-morgan": "^1.0.1",
"koa-router": "^7.2.1",
"koa-send": "^4.1.0",
"koa-static": "^4.0.1",
"pg": "^7.3.0" "pg": "^7.3.0"
}, },
"scripts": { "scripts": {
"start": "node index.js", "start": "node app.js",
"vue": "cd ../app && node build/build.js prod && cd ../api && node index.js" "vue": "cd ../app && node build/build.js prod && cd ../api && node app.js"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-preset-env": "^1.6.0",
"babel-register": "^6.26.0"
},
"babel": {
"presets": [
[
"env",
{
"targets": {
"node": "current"
}
}
]
]
} }
} }

View File

@ -1,31 +1,39 @@
'use strict' 'use strict'
const jwt = require('express-jwt') import jwt from 'koa-jwt'
const jwksRsa = require('jwks-rsa') import jwksRsa from 'jwks-rsa-koa'
const config = require('../appsettings.json') import Router from 'koa-router'
// Authentication middleware. When used, the import appConfig from '../appsettings.json'
// access token must exist and be verified against import journal from './journal'
// the Auth0 JSON Web Key Set import request from './request'
/** Authentication middleware to verify the access token against the Auth0 JSON Web Key Set */
const checkJwt = jwt({ const checkJwt = jwt({
// Dynamically provide a signing key // Dynamically provide a signing key
// based on the kid in the header and // based on the kid in the header and
// the singing keys provided by the JWKS endpoint. // the singing keys provided by the JWKS endpoint.
secret: jwksRsa.expressJwtSecret({ secret: jwksRsa.koaJwt2Key({
cache: true, cache: true,
rateLimit: true, rateLimit: true,
jwksRequestsPerMinute: 5, jwksRequestsPerMinute: 5,
jwksUri: `https://${config.auth0.domain}/.well-known/jwks.json` jwksUri: `https://${appConfig.auth0.domain}/.well-known/jwks.json`
}), }),
// Validate the audience and the issuer. // Validate the audience and the issuer.
audience: config.auth0.clientId, audience: appConfig.auth0.clientId,
issuer: `https://${config.auth0.domain}/`, issuer: `https://${appConfig.auth0.domain}/`,
algorithms: ['RS256'] algorithms: ['RS256']
}) })
module.exports = { /** /api/journal routes */
mount: app => { const journalRoutes = journal(checkJwt)
app.use('/api/journal', require('./journal')(checkJwt)) /** /api/request routes */
} const requestRoutes = request(checkJwt)
}
/** Combined router */
const router = new Router({ prefix: '/api' })
router.use('/journal', journalRoutes.routes(), journalRoutes.allowedMethods())
router.use('/request', requestRoutes.routes(), requestRoutes.allowedMethods())
export default router

View File

@ -1,15 +1,16 @@
'use strict' 'use strict'
const Router = require('express-promise-router') import Router from 'koa-router'
const db = require('../db') import db from '../db'
module.exports = checkJwt => { const router = new Router()
let router = new Router()
router.get('/', checkJwt, async (req, res) => { export default function (checkJwt) {
const reqs = await db.request.journal(req.user.sub)
res.json(reqs) router.get('/', checkJwt, async (ctx, next) => {
const reqs = await db.request.journal(ctx.state.user.sub)
ctx.body = reqs
return await next()
}) })
return router return router
} }

18
src/api/routes/request.js Normal file
View File

@ -0,0 +1,18 @@
'use strict'
import Router from 'koa-router'
import db from '../db'
const router = new Router()
export default function (checkJwt) {
router.post('/', checkJwt, async (ctx, next) => {
const newId = await db.request.addNew(ctx.state.user.sub, ctx.body.requestText)
ctx.body = { id: newId }
await next()
})
return router
}

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,16 @@
<template lang="pug"> <template lang="pug">
article article
page-title(:title="title") page-title(:title="title")
p here you are!
p(v-if="isLoadingJournal") journal is loading... p(v-if="isLoadingJournal") journal is loading...
p(v-if="!isLoadingJournal") journal has {{ journal.length }} entries template(v-if="!isLoadingJournal")
new-request
p journal has {{ journal.length }} entries
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
import PageTitle from './PageTitle' import PageTitle from './PageTitle'
import NewRequest from './request/NewRequest'
import * as actions from '@/store/action-types' import * as actions from '@/store/action-types'
@ -19,7 +21,8 @@ export default {
return {} return {}
}, },
components: { components: {
PageTitle PageTitle,
NewRequest
}, },
computed: { computed: {
title () { title () {

View File

@ -0,0 +1,26 @@
<template lang="pug">
div
el-button(@click='showNewVisible = true') Add a New Request
el-dialog(title='Add a New Prayer Request' :visible.sync='showNewVisible')
el-form(:model='form' :label-position='top')
el-form-item(label='Prayer Request')
el-input(type='textarea' v-model.trim='form.requestText' :rows='10')
span.dialog-footer(slot='footer')
el-button(@click='showNewVisible = false') Cancel
el-button(type='primary' @click='showNewVisible = false') Confirm
</template>
<script>
export default {
name: 'new-request',
data () {
return {
showNewVisible: false,
form: {
requestText: ''
},
formLabelWidth: '120px'
}
}
}
</script>