Skip to content

Commit

Permalink
Merge pull request #7 from Eomm/to-production
Browse files Browse the repository at this point in the history
Third post: to production
  • Loading branch information
Eomm authored Oct 24, 2020
2 parents d5c3e96 + 6143f2a commit 61b2a17
Show file tree
Hide file tree
Showing 17 changed files with 837 additions and 142 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
NODE_ENV=development
BASE_URL=http://localhost:3000
DISCORD_CLIENT_ID=12345678
DISCORD_SECRET=XXXXXXXXXXXXXXXXX
DISCORD_SECRET=XXXXXXXXXXXXXXXXX
DB_URI=mongodb+srv://<user>:<password>@playground.xxxxx.mongodb.net/<dbname>?retryWrites=true&w=majority
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}

- name: Start MongoDB
uses: supercharge/mongodb-github-action@1.3.0
with:
mongodb-version: 4.2

- name: Install
run: npm install --ignore-scripts
- name: Run tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,4 @@ dist
.tern-port

package-lock.json
.vscode/
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ and here you will find the source code and the commit history!

1. [A Discord app with Fastify!](https://dev.to/eomm/a-discord-app-with-fastify-3h8c) - [📝](./posts/01-init-application.md)
2. [Project Automation](https://dev.to/eomm/project-automation-2bee) - [📝](./posts/02-project-automation.md)
3. COOMING SOON
3. [To Production](https://dev.to/eomm/fastify-demo-goes-to-production-499c) - [📝](./posts/03-to-production.md)

## Contributing

Expand Down
67 changes: 67 additions & 0 deletions lib/api/api.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

import schema from './schema.mjs'
import casual from 'casual'

export default function api (fastify, opts, next) {
fastify.setErrorHandler(function (error, request, reply) {
reply.send(error)
})

fastify.put('/users/:userId', {
handler: createUser,
schema: schema.createUser
})

fastify.get('/users', {
handler: searchUsers,
schema: schema.searchUsers
})

next()
}

async function createUser (request, reply) {
const { userId } = request.params

await this.mongo.client.db()
.collection('Users')
.updateOne(
{ id: userId },
{
$set: fakeData(request.body),
$push: { visits: new Date() },
$setOnInsert: { created: new Date() }
},
{ upsert: true })

request.log.debug('Track user %s', userId)
reply.code(201)
return { userId }
}

async function searchUsers (request, reply) {
const { offset, limit } = request.query

const query = await this.mongo.client.db().collection('Users')
.find({}, { projection: { _id: 0, visits: { $slice: -1 } } })
.sort({ 'visits.$0': 1 })
.skip(offset)
.limit(limit)

const total = await query.count()
const rows = await query.toArray()

return { rows, total }
}

/**
* The GDPR don't let us to store personal information
* without adding burden to this tutorial, so we fake the
* data
*/
function fakeData (user) {
user.id = casual.integer(0, 401404362695639040)
user.username = casual.username
user.email = casual.email
return user
}
64 changes: 64 additions & 0 deletions lib/api/schema.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@

export default {
createUser: {
params: {
userId: { type: 'integer' }
},
body: {
type: 'object',
additionalProperties: false,
properties: {
id: { type: 'integer' },
username: { type: 'string', maxLength: 32 },
avatar: { type: 'string', maxLength: 50 },
discriminator: { type: 'string', maxLength: 5 },
email: { type: 'string', format: 'email' },
verified: { type: 'boolean' },
locale: { type: 'string', maxLength: 2 }
},
required: ['id', 'username']
},
response: {
200: {
type: 'object',
properties: {
userId: { type: 'integer' }
}
}
}
},
searchUsers: {
query: {
type: 'object',
additionalProperties: false,
properties: {
offset: { type: 'integer', minimum: 0, default: 0 },
limit: { type: 'integer', minimum: 1, maximum: 40, default: 10 }
}
},
response: {
200: {
type: 'object',
properties: {
total: { type: 'integer' },
rows: {
type: 'array',
items: {
type: 'object',
properties: {
username: { type: 'string', maxLength: 32 },
visits: {
type: 'array',
items: {
type: 'string',
format: 'date-time'
}
}
}
}
}
}
}
}
}
}
36 changes: 29 additions & 7 deletions lib/app.mjs
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@

import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'

import Fastify from 'fastify'
import env from 'fastify-env'
import helmet from 'fastify-helmet'
import fastifyStatic from 'fastify-static'
import fastifyMongo from 'fastify-mongodb'
import pointOfView from 'point-of-view'
import handlebars from 'handlebars'
import { fileURLToPath } from 'url'

import authRoutes from './auth.mjs'
import apiEndpoints from './api/api.mjs'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

const appVersion = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'))).version

const schema = {
type: 'object',
required: ['PORT', 'DISCORD_CLIENT_ID', 'DISCORD_SECRET'],
required: ['PORT', 'DISCORD_CLIENT_ID', 'DISCORD_SECRET', 'DB_URI'],
properties: {
NODE_ENV: { type: 'string' },
BASE_URL: { type: 'string' },
PORT: { type: 'integer', default: 3000 },
DISCORD_CLIENT_ID: { type: 'string' },
DISCORD_SECRET: { type: 'string' }
DISCORD_SECRET: { type: 'string' },
DB_URI: { type: 'string', format: 'uri' }
}
}

export default function app (fastify, opts, next) {
export default function build (opts) {
const fastify = Fastify(opts.fastify)

// will load fastify.config
fastify.register(env, { schema, dotenv: true })
fastify.register(env, {
data: opts,
schema,
dotenv: false
})
.after((err) => {
if (err) throw err // if the config file has some issue, we must bubble up them
fastify.register(fastifyMongo, { url: fastify.config.DB_URI })

fastify.register(apiEndpoints, { ...fastify.config, prefix: '/api' })
})

// the auth management
fastify.register(authRoutes, { prefix: '/auth' })
Expand All @@ -48,6 +66,10 @@ export default function app (fastify, opts, next) {
reply.view('/pages/homepage.hbs', { version: appVersion })
})

fastify.get('/list', function (request, reply) {
reply.view('/pages/list.hbs')
})

fastify.get('/health', function (request, reply) {
reply.send('I am fine thanks')
})
Expand All @@ -70,12 +92,12 @@ export default function app (fastify, opts, next) {
frameAncestors: ["'self'"],
imgSrc: ["'self'", 'data:', 'via.placeholder.com', 'cdn.discordapp.com'],
objectSrc: ["'none'"],
scriptSrc: ["'self'", 'kit.fontawesome.com'],
scriptSrc: ['stackpath.bootstrapcdn.com', 'unpkg.com', 'code.jquery.com', 'kit.fontawesome.com', "'nonce-itShouldBeGenerated'"],
scriptSrcAttr: ["'none'"],
styleSrc: ["'self'", 'https:', "'unsafe-inline'"]
}
}
})

next()
return fastify
}
50 changes: 42 additions & 8 deletions lib/auth.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,36 @@

import oauth2 from 'fastify-oauth2'
import cookie from 'fastify-cookie'
import caching from 'fastify-caching'
import serverSession from 'fastify-server-session'
import got from 'got'

export default function auth (fastify, opts, next) {
const baseUrl = new URL(fastify.config.BASE_URL)

fastify
.register(cookie)
.register(caching)
.register(serverSession, {
secretKey: 'some-secret-password-at-least-32-characters-long',
sessionMaxAge: 604800,
cookie: {
domain: baseUrl.hostname,
httpOnly: true,
secure: fastify.config.NODE_ENV !== 'development',
maxAge: 60
}
})

fastify.addHook('onRequest', function userAlreadyLogged (req, reply, done) {
if (req.session.token) {
viewUserProfile(req.session.token, reply)
.catch(done) // don't forget to manage errors!
return // do not call `done` to stop the flow
}
done()
})

fastify.register(oauth2, {
name: 'discordOAuth2',
credentials: {
Expand All @@ -19,15 +47,21 @@ export default function auth (fastify, opts, next) {

fastify.get('/discord/callback', async function (request, reply) {
const token = await this.discordOAuth2.getAccessTokenFromAuthorizationCodeFlow(request)
const userData = await got.get('https://discord.com/api/users/@me', {
responseType: 'json',
headers: {
authorization: `${token.token_type} ${token.access_token}`
}
})

reply.view('/pages/profile.hbs', userData.body)
// server stored: the token object must not be sent to the client
request.session.token = token
return viewUserProfile(token, reply)
})

next()
}

async function viewUserProfile (token, reply) {
const userData = await got.get('https://discord.com/api/users/@me', {
responseType: 'json',
headers: {
authorization: `${token.token_type} ${token.access_token}`
}
})

return reply.view('/pages/profile.hbs', userData.body)
}
29 changes: 13 additions & 16 deletions lib/start.mjs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@

import Fastify from 'fastify'
import app from './app.mjs'

const server = Fastify({
logger: true,
pluginTimeout: 10000
})

server.register(app)

server.listen(process.env.PORT || 3000, '0.0.0.0', (err) => {
if (err) {
server.log.error(err)
process.exit(1)
}
})
import configurationLoader from './utils/configuration-loader.mjs'
import appFactory from './app.mjs'

;(async function () {
const config = await configurationLoader()
const server = appFactory(config)
server.listen(config.PORT || 3000, '0.0.0.0', (err) => {
if (err) {
server.log.error(err)
process.exit(1)
}
})
})()
18 changes: 18 additions & 0 deletions lib/utils/configuration-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@

import dotenv from 'dotenv'

export default async function load () {
const configuration = dotenv.config()

/**
* This function is async because we could
* load some KEYS from external services (like AWS Secrets Manager)
* in future
*/

configuration.fastify = {
logger: configuration.NODE_ENV !== 'production'
}

return configuration
}
Loading

0 comments on commit 61b2a17

Please sign in to comment.