├── .nvmrc ├── config └── jwt │ └── .gitignore ├── .eslintrc ├── .gitignore ├── .env.default ├── docker-compose.yml ├── schemas └── user.js ├── LICENSE.md ├── README.MD ├── auth └── jwt-auth.js ├── package.json ├── routes ├── indexRoutes.js └── userRoutes.js └── index.js /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /config/jwt/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | config/jwt/public.pem 3 | config/jwt/private.pem 4 | node_modules -------------------------------------------------------------------------------- /.env.default: -------------------------------------------------------------------------------- 1 | MONGODB_URI='mongodb://localhost:27017/fastify' 2 | SERVER_PORT=3000 3 | JWT_PASSPHRASE=empty -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | services: 3 | mongo: 4 | image: mongo:latest 5 | restart: always 6 | environment: 7 | MONGO_INITDB_DATABASE: fastify 8 | ports: 9 | ['27017:27017'] -------------------------------------------------------------------------------- /schemas/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose') 2 | 3 | module.exports = function (fastify, opts, next) { 4 | const Schema = mongoose.Schema 5 | 6 | const User = new Schema({ 7 | username: String, 8 | email: String, 9 | roles: [String], 10 | password: String, 11 | created: { type: Date, default: Date.now } 12 | }) 13 | 14 | fastify.mongo.db.model('User', User) 15 | 16 | next() 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Frank Jogeleit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ``` 4 | npm install 5 | ``` 6 | 7 | ## Configuration 8 | 9 | * Copy and rename .env.default to .env 10 | * Add your parameters 11 | 12 | ## Create your PEM files 13 | 14 | Create your public and private key for the JWT signature: 15 | 16 | ``` 17 | $ openssl genrsa -out config/jwt/private.pem -aes256 4096 18 | $ openssl rsa -pubout -in config/jwt/private.pem -out config/jwt/public.pem 19 | ``` 20 | 21 | ## Use docker to start your test mongoDB 22 | 23 | ``` 24 | docker-compose up -d 25 | ``` 26 | 27 | ## Start 28 | 29 | Use nodemon to restart the application after any change (includes the .env file) 30 | 31 | ``` 32 | npm start 33 | ``` 34 | 35 | # Swagger UI 36 | 37 | Overview of all existing routes via Swagger UI. Port 3000 is the default, it could be different from your .env Configuration 38 | 39 | ``` 40 | http://localhost:3000/documentation 41 | ``` 42 | 43 | # Features 44 | 45 | * JWT Authentification with PEM file based signature and passphrase 46 | * Base User Model 47 | * API Routes for registration and login 48 | * Example for Authorized and Unauthorized API access 49 | * Swagger UI 50 | * Mongoose as ODM -------------------------------------------------------------------------------- /auth/jwt-auth.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const fastifyPlugin = require('fastify-plugin') 3 | const fs = require('fs') 4 | 5 | const jwtAuth = function (fastify, opts, next) { 6 | fastify.decorate('jwtAuth', function (request, reply, done) { 7 | if (!request.req.headers.authorization) { 8 | return reply.code(401).send({ message: ' Unauthorized' }) 9 | } 10 | 11 | jwt.verify(request.req.headers.authorization.replace('Bearer', '').trim(), fs.readFileSync('./config/jwt/public.pem'), (err, decoded) => { 12 | if (err || !decoded.username) { 13 | return reply.code(401).send({ message: 'Unauthorized' }) 14 | } 15 | 16 | fastify.mongo.db.model('User').findOne({ username: decoded.username }, (error, user) => { 17 | if (error) { 18 | return reply.code(500).send({ message: error.message }) 19 | } 20 | 21 | if (!user) { 22 | return reply.code(401).send({ message: 'Unauthorized' }) 23 | } 24 | 25 | request.user = user 26 | 27 | done() 28 | }) 29 | }) 30 | }) 31 | 32 | next() 33 | } 34 | 35 | module.exports = fastifyPlugin(jwtAuth, '>=0.13.1') 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-base-project", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "nodemon --watch ./ --watch .env", 9 | "start:prod": "node index.js", 10 | "lint": "npm run lint:standard", 11 | "lint:fix": "standard --fix", 12 | "lint:standard": "standard --verbose" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "bcryptjs": "^2.4.3", 18 | "dotenv": "^8.2.0", 19 | "fastify": "^2.15.3", 20 | "fastify-auth": "^0.5.0", 21 | "fastify-helmet": "^3.0.1", 22 | "fastify-mongoose": "^0.2.1", 23 | "fastify-swagger": "^5.2.0", 24 | "jsonwebtoken": "^9.0.0", 25 | "mongoose": "^5.12.2" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^7.11.0", 29 | "eslint-config-standard": "^16.0.1", 30 | "eslint-import-resolver-node": "^0.3.2", 31 | "eslint-plugin-import": "^2.20.2", 32 | "eslint-plugin-node": "^11.1.0", 33 | "eslint-plugin-promise": "^4.2.1", 34 | "nodemon": "^2.0.20", 35 | "snazzy": "^9.0.0", 36 | "standard": "^16.0.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /routes/indexRoutes.js: -------------------------------------------------------------------------------- 1 | module.exports = function (fastify, opts, next) { 2 | const indexSchema = { 3 | schema: { 4 | description: 'Unauthenticate access', 5 | tags: ['Index'], 6 | summary: 'Hello World', 7 | response: { 8 | 200: { 9 | type: 'object', 10 | properties: { 11 | hello: { type: 'string' } 12 | } 13 | } 14 | } 15 | } 16 | } 17 | fastify.get('/', indexSchema, async (request, reply) => { 18 | reply.type('application/json').code(200) 19 | 20 | return { hello: 'hello world' } 21 | }) 22 | 23 | const authSchema = { 24 | schema: { 25 | description: 'Only authenticate access', 26 | tags: ['Index'], 27 | summary: 'Hello Username', 28 | security: [{ apiKey: [] }], 29 | response: { 30 | 200: { 31 | type: 'object', 32 | properties: { 33 | hello: { type: 'string' } 34 | } 35 | } 36 | } 37 | }, 38 | preHandler: fastify.auth([fastify.jwtAuth]) 39 | } 40 | 41 | fastify.get('/auth', authSchema, async (request, reply) => { 42 | reply.type('application/json').code(200) 43 | 44 | return { hello: 'hello ' + request.user.username } 45 | }) 46 | next() 47 | } 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | const fastify = require('fastify')() 4 | const port = process.env.SERVER_PORT || 3000 5 | 6 | fastify 7 | .register(require('fastify-swagger'), { 8 | routePrefix: '/documentation', 9 | swagger: { 10 | info: { 11 | title: 'Fastify Basic Project', 12 | description: 'API Overview', 13 | version: '0.1.0' 14 | }, 15 | host: `localhost:${port}`, 16 | tags: [ 17 | { name: 'User', description: 'User related end-points' } 18 | ], 19 | schemes: ['http'], 20 | consumes: ['application/json'], 21 | produces: ['application/json'], 22 | securityDefinitions: { 23 | apiKey: { 24 | type: 'apiKey', 25 | name: 'Authorization', 26 | in: 'header' 27 | } 28 | } 29 | }, 30 | exposeRoute: true 31 | }) 32 | .register(require('fastify-helmet')) 33 | .register(require('fastify-mongoose'), { 34 | uri: process.env.MONGODB_URI 35 | }, err => { 36 | if (err) throw err 37 | }) 38 | .register(require('./schemas/user')) 39 | .register(require('fastify-auth')) 40 | .register(require('./auth/jwt-auth')) 41 | .register(require('./routes/userRoutes'), { prefix: '/users' }) 42 | .register(require('./routes/indexRoutes')) 43 | 44 | fastify.listen(port, function (err) { 45 | if (err) throw err 46 | console.log(`server listening on ${fastify.server.address().port}`) 47 | }) 48 | -------------------------------------------------------------------------------- /routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs') 2 | const jwt = require('jsonwebtoken') 3 | const fs = require('fs') 4 | 5 | module.exports = function (fastify, opts, next) { 6 | const registerSchema = { 7 | schema: { 8 | description: 'Register a new User', 9 | tags: ['User'], 10 | summary: 'Register a new User', 11 | body: { 12 | type: 'object', 13 | required: ['username', 'email', 'password'], 14 | properties: { 15 | username: { type: 'string' }, 16 | email: { type: 'string' }, 17 | password: { type: 'string' } 18 | } 19 | }, 20 | response: { 21 | 200: { 22 | type: 'object', 23 | properties: { 24 | _id: { type: 'string' }, 25 | username: { type: 'string' }, 26 | email: { type: 'string' }, 27 | roles: { type: 'array', items: { type: 'string' } } 28 | } 29 | }, 30 | 409: { 31 | type: 'object', 32 | properties: { 33 | errorCode: { type: 'string' }, 34 | errorMessage: { type: 'string' } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | fastify.post('/register', registerSchema, async (req, reply) => { 42 | const User = fastify.mongo.db.model('User') 43 | 44 | User.findOne({ username: req.body.username }, async (error, user) => { 45 | if (error) throw error 46 | 47 | if (user) { 48 | return reply.code(409).send({ errorCode: 409, errorMessage: 'Username already exists' }) 49 | } 50 | 51 | const password = await bcrypt.hash(req.body.password, await bcrypt.genSalt(10)) 52 | const newUser = new User(Object.assign(req.body, { password, roles: ['ROLE_USER'] })) 53 | const { _id, username, email, roles } = await newUser.save() 54 | 55 | reply.send({ _id, username, email, roles }) 56 | }) 57 | }) 58 | 59 | const loginSchema = { 60 | schema: { 61 | description: 'Login User API', 62 | tags: ['User'], 63 | summary: 'Create a JWT for authenticate API calls', 64 | body: { 65 | type: 'object', 66 | required: ['username', 'password'], 67 | properties: { 68 | username: { type: 'string' }, 69 | password: { type: 'string' } 70 | } 71 | }, 72 | response: { 73 | 200: { 74 | type: 'object', 75 | properties: { 76 | token: { type: 'string' } 77 | } 78 | }, 79 | 401: { 80 | type: 'object', 81 | properties: { 82 | errorCode: { type: 'string' }, 83 | errorMessage: { type: 'string' } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | fastify.post('/login', loginSchema, async (req, reply) => { 91 | const User = fastify.mongo.db.model('User') 92 | 93 | User.findOne({ username: req.body.username }, async (error, user) => { 94 | if (error) throw error 95 | 96 | if (!user) { 97 | return reply.code(409).send({ errorCode: 409, errorMessage: 'Login failed' }) 98 | } 99 | 100 | if (await bcrypt.compare(req.body.password, user.password) === false) { 101 | return reply.code(409).send({ errorCode: 409, errorMessage: 'Login failed' }) 102 | } 103 | 104 | const { _id, username, email, roles } = user 105 | 106 | jwt.sign({ _id, username, email, roles }, { key: fs.readFileSync('./config/jwt/private.pem'), passphrase: process.env.JWT_PASSPHRASE }, { algorithm: 'RS256' }, (error, token) => { 107 | if (error) throw error 108 | 109 | reply.send({ token }) 110 | }) 111 | }) 112 | }) 113 | 114 | next() 115 | } 116 | --------------------------------------------------------------------------------