Parcourir la source

Merge branch 'develop' of stefan.stamenovic/diligent-node-api into master

master
radivoje.milutinovic il y a 3 ans
Parent
révision
311f1b1404

+ 2
- 0
.dockerignore Voir le fichier

@@ -0,0 +1,2 @@
node_modules
npm-debug.log

+ 4
- 0
.gitignore Voir le fichier

@@ -0,0 +1,4 @@
node_modules/
loggerFiles/
public/loggerFiles
.idea

+ 16
- 0
Dockerfile Voir le fichier

@@ -0,0 +1,16 @@
FROM node:16

WORKDIR ./src

COPY package*.json ./
COPY src ./

RUN npm install

ENV NODE_ENV='docker'

# Bundle app source
COPY . .

EXPOSE 3000
CMD [ "node", "server.js" ]

+ 32
- 0
README.md Voir le fichier

@@ -0,0 +1,32 @@
# Node API template

This is template of web API with mongo db as database node.js as runtime and express as framework. We have user model and JWT tokens setup as well as mediator pattern and winston as logger.

## Setup

Here we will show you how to set up project and run it in localhost. If you want to run it in [docker]() or [portainer]() you can find setup on links.

- [Project setup](#project-setup)
- [Database setup](#database-setup)

### Database setup
Please follow the official tutorial on installing and running the latest Mongo database which can be found on this link:
> https://docs.mongodb.com/manual/tutorial/install-mongodb-on-windows/

### Project setup
In order to run our project you need to clone it from [git](https://git.dilig.net/stefan.stamenovic/diligent-node-api) first. Open terminal (cmd and powershell work as well) in folder that you want this project to be and run command
> git clone http://git.dilig.net/stefan.stamenovic/diligent-node-api.git

After cloning project you can open it with your preferred IDE/Code editor if you want to see the code. Before running project you need to open terminal and run command
> npm install

Running that command will download all necessary npm packages to run the project.

After that you want to move to src directory
> cd src

Now you can run the project using
> node server.js

Congratulations! You now ran backend api for template. You can see swagger documentation in the browser with the url
> http://localhost:3001/swagger

+ 15
- 0
docker-compose.yml Voir le fichier

@@ -0,0 +1,15 @@
version: "2"
services:
app:
container_name: app
restart: always
build: .
ports:
- "3000:3000"
links:
- mongo
mongo:
container_name: mongo
image: mongo
ports:
- "27017:27017"

+ 5880
- 0
package-lock.json
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 35
- 0
package.json Voir le fichier

@@ -0,0 +1,35 @@
{
"name": "diligent-node-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.dilig.net/stefan.stamenovic/diligent-node-api"
},
"author": "",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"config": "^3.3.7",
"cors": "^2.8.5",
"express": "^4.18.1",
"express-jwt": "^7.7.2",
"helmet": "^5.1.0",
"joi": "^17.6.0",
"joi-objectid": "^4.0.2",
"jsonwebtoken": "^8.5.1",
"migrate-mongo": "^9.0.0",
"mongodb": "^4.6.0",
"mongoose": "^6.3.4",
"nodemon": "^2.0.16",
"request": "^2.88.2",
"swagger-jsdoc": "^6.2.1",
"swagger-ui-express": "^4.4.0",
"validator": "^13.7.0",
"winston": "^3.7.2"
}
}

+ 0
- 0
src/app.js Voir le fichier


+ 6
- 0
src/config/default.json Voir le fichier

@@ -0,0 +1,6 @@
{
"Test": "Diligent",
"DbLocalConnection": "mongodb://127.0.0.1:27017/trampa-dev",
"DbDockerConnection": "mongodb://mongo:27017/trampa-dev",
"NODE_ENV": "development"
}

+ 4
- 0
src/database/models/roles.js Voir le fichier

@@ -0,0 +1,4 @@
module.exports = {
Admin : 'Admin',
User: 'User'
}

+ 70
- 0
src/database/models/token.js Voir le fichier

@@ -0,0 +1,70 @@
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const User = require('./user')

const tokenSchema = new mongoose.Schema({
token: {
type: String,
required: true
},
userId: {
type: String,
required: true
}
})

tokenSchema.statics.findByCredentials = async (email, password) => {
const user = await User.findOne({email})
if(!user) {
return
}
const checkMatch = await bcrypt.compare(password, user.password)
if(checkMatch) {
return user
}
return null
}

tokenSchema.statics.generateAuthToken = async function(userArg) {
console.log('aaa')
const user = userArg
const token = jwt.sign({ _id: user._id.toString() }, 'ovoJeSecret', { expiresIn: 60 * 20 })
console.log(token)

user.tokens = user.tokens.concat({ token })
await user.save()

return token
}

tokenSchema.statics.refreshAuthToken = async function(token) {
console.log(token)
try {
const payload = jwt.verify(token, 'ovoJeSecret')
console.log(payload)
delete payload.iat
delete payload.exp
delete payload.nbf
delete payload.jti
return jwt.sign(payload, 'ovoJeSecret', { expiresIn: 60 * 20 })
} catch(e) {
return null
}
}

tokenSchema.statics.destroyToken = async function(token) {
const findUser = await User.findOne({ 'tokens.token': token })
if(!findUser) {
return null
}
findUser.tokens = findUser.tokens.filter((currToken) => {
return currToken.token !== token
})
await findUser.save()
return true
}

const Token = mongoose.model('Token', tokenSchema)

module.exports = Token

+ 40
- 0
src/database/models/user.js Voir le fichier

@@ -0,0 +1,40 @@
const mongoose = require('mongoose')

const userSchema = new mongoose.Schema({
name: {
type: String
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
roles: {
type: String
},
tokens: [{
token: {
type: String,
required: true
}
}]
})

// userSchema.pre('save', async function(next) {
// const user = this
// console.log('pre hash: ' + user.password)

// user.password = await bcrypt.hash(user.password, 8)

// console.log('posle hash: ' + user.password)

// console.log('Middleware before password hash')
// next()
// })

const User = mongoose.model('User', userSchema)

module.exports = User

+ 29
- 0
src/database/mongoose.js Voir le fichier

@@ -0,0 +1,29 @@
const mongoose = require('mongoose')
const logger = require('../logging/loggerDbCon')
const config = require('config')

if (config.util.getEnv('NODE_ENV') === 'development') {
mongoose.connect(config.get('DbLocalConnection'), {
useNewUrlParser: true
})
}
else if (config.util.getEnv('NODE_ENV') === 'docker'){
mongoose.connect(config.get('DbDockerConnection'), {
useNewUrlParser: true
})
}

mongoose.connection.on('error', err => {
logger.silly('DB connection failed')
})

mongoose.connection.on('disconnected', () => {
logger.silly('DB disconnected')
})
mongoose.connection.on('disconnecting', () => {
logger.silly('DB connection closed by user')
})

mongoose.connection.on('reconnected', () => {
logger.silly('DB reconnected')
})

+ 41
- 0
src/endpoints/auth.js Voir le fichier

@@ -0,0 +1,41 @@
const Auth = require('../database/models/token')
const authMediator = require('../mediator/authMediator')

const token = async (req, res, next) => {
try {
if (!req.body.email || !req.body.password){
return res.status(400).send('Pass credentials')
}

const result = await authMediator.token()
if(!result.succeeded){
res.status(401).send(result.error)
}

return res.status(200).send(result.value)
} catch (e) {
next(e)
}
}

const logout = async (req, res) => {
const result = await Auth.destroyToken(req.body.token)
if (!result) {
return res.status(404).send('No user has the token provided!')
}
return res.send('Auth ' + req.body.token + ' invalidated!')
}

const refreshUserToken = async (req, res) => {
const form = {
token: req.body.token
}
const result = await Auth.refreshAuthToken(form.token)
if (!result) {
return res.status(404).send('Auth not valid!')
}

return res.status(201).send('Auth ' + result + ' refreshed successfully!')
}

module.exports = { loginUser: token, logout, refreshUserToken }

+ 105
- 0
src/endpoints/user.js Voir le fichier

@@ -0,0 +1,105 @@
const User = require("../database/models/user")
const { getUserValidator, getIdValidator, getUpdatedUserValidator } = require("../validators/users")
const mediator = require('../mediator/userMediator')

const getUsers = async (req, res, next) => {
const allUsers = await User.find({})
return res.status(200).send(allUsers)
}

const getUser = async (req, res, next) => {
try {
const id = req.params.id
const errId = getIdValidator.validate(id).error
if (errId) {
return res.status(404).send(errId.message)
}
const result = await mediator.getUser(id)

if (!result.succeeded) {
return res.status(400).send(result.error)
}
return res.status(200).json(result.value)
} catch (e) {
next(e)
}
}

const createUser = async (req, res, next) => {
try {
const userModel = req.body

const err = getUserValidator.validate(userModel).error
if (err) {
return res.status(404).send(err.message)
}
const result = await mediator.createUser(userModel)
if (!result.succeeded) {
return res.status(400).send(result.error)
}

return res.status(201).json(result.value)
} catch (e) {
next(e)
}
}

const updateUser = async (req, res, next) => {
try {
const id = req.params.id
const errId = getIdValidator.validate(id).error
if (errId) {
return res.status(400).send(errId.message)
}

const objBody = req.body

const err = getUpdatedUserValidator.validate(objBody).error
if (err) {
return res.status(400).send(err.message)
}
const result = await mediator.updateUser(id, objBody)
if (!result.succeeded) {
return res.status(400).send(result.error)
}
return res.status(200).send(result.value)
} catch (e) {
next(e)
}
}

const deleteUser = async (req, res, next) => {
try {
const id = req.params.id
const errId = getIdValidator.validate(id).error
if (errId) {
return res.status(400).send(errId.message)
}

const result = await mediator.deleteUser(id)
if (!result.succeeded) {
return res.status(400).send(result.error)
}
return res.status(204).send(result.value)
} catch (e) {
next(e)
}
}

const updateUserContacts = async (req, res, next) => {
try {
const userFound = true
if (!userFound) {
return res.status(404).send('user not found')
}
if (!req.body) {
return res.status(400).send('invalid input parameters')
}
return res.status(200).send('user contacts updated successfully')
} catch (e) {
next(e)
}
}

module.exports = { getUsers, getUser, createUser, updateUserContacts, updateUser, deleteUser }


+ 21
- 0
src/logging/logger.js Voir le fichier

@@ -0,0 +1,21 @@
const winston = require('winston')
const config = require('config')

const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.File({ filename: '../public/loggerFiles/error.log', level: 'error' }),
new winston.transports.File({ filename: '../public/loggerFiles/all.log', level: 'silly' }),
new winston.transports.Console({level: 'silly'}),
],
});

if (config.util.getEnv('NODE_ENV') !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}

module.exports = logger

+ 19
- 0
src/logging/loggerDbCon.js Voir le fichier

@@ -0,0 +1,19 @@
const winston = require('winston')
const config = require('config')
const loggerWinston = winston.createLogger({
level: 'info',
format: winston.format.json(),
defaultMeta: { service: 'user-service' },
transports: [
new winston.transports.File({ filename: '../public/loggerFiles/dbCon.log', level: 'silly' })
],
});
if (config.util.getEnv('NODE_ENV') !== 'production') {
loggerWinston.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
module.exports = loggerWinston

+ 31
- 0
src/mediator/authMediator.js Voir le fichier

@@ -0,0 +1,31 @@
const Auth = require("../database/models/token");
const bcrypt = require("bcryptjs");

const token = async (email, password) => {
const foundUser = await Auth.findByCredentials(email, password)

if (!foundUser) {
return {
succeeded: false,
value: null,
error: 'Wrong credentials!'
}
}
const isValidPassword = await bcrypt.compare(password, foundUser.password)
if (!isValidPassword) {
return {
succeeded: false,
value: null,
error: 'Wrong credentials!'
}
}
const token = await Auth.generateAuthToken(foundUser)

return {
succeeded: true,
value: token,
error: null
}
}

module.exports = {token}

+ 80
- 0
src/mediator/userMediator.js Voir le fichier

@@ -0,0 +1,80 @@
const bcrypt = require("bcryptjs/dist/bcrypt")
const User = require("../database/models/user")
const { getUserValidator } = require("../validators/users")

const getUser = async (id) => {
const user = await User.findById(id)
if (!user) {
return {
succeeded: false,
value: null,
error: 'User not found!'
}
}
return {
succeeded: true,
value: user,
error: null
}
}

const createUser = async (user) => {
const foundMail = await User.find({ email: user.email })
if (foundMail.length) {
return {
succeeded: false,
value: null,
error: 'User with email already exists!'
}
}

const newUser = new User(user)
newUser.password = await bcrypt.hash(newUser.password, 8)
await User.create(newUser)
return {
succeeded: true,
value: newUser,
error: null
}
}

const updateUser = async (id, body) => {
let user = await User.findById(id);
if (!user) {
return {
succeeded: false,
value: null,
error: 'User with id ' + id + ' does not exist!'
}
}

const updatedUser = {
name: body.name,
roles: body.roles
}
await User.updateOne({ _id: id }, updatedUser)
return {
succeeded: true,
value: 'User updated successfully!',
error: null
}
}

const deleteUser = async (id) => {
let user = await User.findById(id)
if (!user) {
return {
succeeded: false,
value: null,
error: 'User with id ' + id + ' does not exist!'
}
}
await User.deleteOne(user)
return {
succeeded: true,
value: null,
error: null
}
}

module.exports = {getUser, createUser, updateUser, deleteUser}

+ 40
- 0
src/middleware/auth.js Voir le fichier

@@ -0,0 +1,40 @@
const jwt = require('jsonwebtoken')
const User = require('../database/models/user')
const Role = require('../database/models/roles')

const auth = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '')
const decoded = jwt.verify(token, 'ovoJeSecret')
console.log(decoded)
} catch (e) {
return res.send(e)
}
console.log('auth middleware')
next()
}

const authRole = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ', '')
if(!token) {
return res.status(401).send('Invalid token!')
}

const findUser = await User.findOne({ 'tokens.token': token })
if(!findUser) {
return res.status(401).send('No user has the token provided!')
}
if(findUser['role'] === Role.Admin) {
console.log('User is admin!')
next()
}
else {
return res.status(403).send('Access forbidden!')
}
} catch(e) {
next(e)
}
}

module.exports = { auth, authRole }

+ 15
- 0
src/middleware/errorHandling.js Voir le fichier

@@ -0,0 +1,15 @@
const logger = require('../logging/logger')
const config = require('config')
const errorLogger = (err, req, res, next) => {
if (config.util.getEnv('NODE_ENV') === 'development')
logger.error(err)
next(err)
}
const errorResponder = (err, req, res, next) => {
res.status(err.statusCode).send(err)
}
module.exports = { errorLogger, errorResponder }

+ 10
- 0
src/middleware/requestLogging.js Voir le fichier

@@ -0,0 +1,10 @@
const logger = require("../logging/logger")
const requestLogging = async (req, res, next) => {
res.header("Content-Type", 'application/json');
logger.silly(req)
next()
}
module.exports = requestLogging

+ 10
- 0
src/routes/auth.js Voir le fichier

@@ -0,0 +1,10 @@
const express = require('express')
const router = new express.Router()
const endpoints = require('../endpoints/auth')


router.post('/auth/token', endpoints.loginUser)
router.post('/auth/logout', endpoints.logout)
router.post('/auth/refresh', endpoints.refreshUserToken)

module.exports = router

+ 14
- 0
src/routes/user.js Voir le fichier

@@ -0,0 +1,14 @@
const express = require('express')
const endpoints = require('../endpoints/user')
const router = new express.Router()
const auth = require('../middleware/auth')


router.get('/users', endpoints.getUsers)
router.get('/users/:id', endpoints.getUser)
router.post('/users', endpoints.createUser)
router.put('/users/:id', endpoints.updateUser)
router.patch('/users/:id/contacts', endpoints.updateUserContacts)
router.delete('/users/:id', endpoints.deleteUser)

module.exports = router

+ 41
- 0
src/server.js Voir le fichier

@@ -0,0 +1,41 @@
const config = require('config') //Default configuration file
const express = require('express')
const app = express()
const port = process.env.NODE_ENV === 'production' ? 80 : 3001
require('./database/mongoose')
const docs = require('./swagger.js')
const swaggerUI = require('swagger-ui-express')
const { errorLogger, errorResponder } = require('./middleware/errorHandling.js')
const requestLogging = require('./middleware/requestLogging.js')
const cors = require('cors') //Cross-origin resource sharing
const helmet = require('helmet') //Basic protection against attacks like XSS
const fs = require('fs')
const path = require('path')
const routesDirectory = path.resolve(__dirname) + '/routes/'
// console.log(config.util.getEnv('NODE_ENV'))

app.use(errorLogger);
app.use(errorResponder);
app.use(express.json())
app.use('/swagger', swaggerUI.serve, swaggerUI.setup(docs))
app.use(requestLogging)
app.use(cors())
app.use(helmet())

fs.readdirSync(routesDirectory).forEach(route => {
app.use(require(routesDirectory + route))
})


app.get('/', (req, res) => {
try {
res.send('Wello Horld!')
} catch (e) {
res.status(500).send(e)
}
})


app.listen(port, () => {
console.log('Server is up on port ' + port)
})

+ 351
- 0
src/swagger.js Voir le fichier

@@ -0,0 +1,351 @@
module.exports = {
servers: [
{
url: "http://localhost:3001/",
description: "Local server",
},
],
tags: [
{
name: "User"
},
],
openapi: "3.0.3",
info: {
title: "Trampa",
description: "Trampa api"
},
paths: {
'/users': {
get: {
tags: ["User"],
description: "Get all users",
parameters: [],
responses: {
200: {
description: "Success",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/User",
}
}
}
},
200: {
description: "Users returned successfully"
},
500: {
description: "Internal server error"
}
}
},
post: {
tags: ["User"],
description: "Create user",
parameters: [],
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: {
type: "string"
},
email: {
type: "string"
},
password: {
type: "string"
}
}
}
}
}
},
responses: {
201: {
description: "User created successfully",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/User"
},
},
},
},
400: {
description: "Object cant be empty",
},
401: {
description: "Invalid input parameters",
},
500: {
description: "Internal server error",
}
}
}
},
'/users/{id}': {
get: {
tags: ["User"],
description: "Get user by id",
parameters: [
{
name: "id",
in: "path",
schema: {
$ref: "#/components/schemas/id",
},
required: true,
description: "A single user id",
}
],
responses: {
200: {
description: "Success",
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/User",
}
}
}
},
400: {
description: "Bad request"
},
404: {
description: "User with specified id does not exist"
},
500: {
description: "Internal server error"
}
}
},
put: {
tags: ["User"],
description: "Update user",
parameters: [
{
name: "id",
in: "path",
schema: {
$ref: "#/components/schemas/id", // data model of the param
},
required: true,
description: "A single user id",
}
],
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: {
type: "string"
},
email: {
type: "string"
},
password: {
type: "string"
}
}
}
}
}
},
responses: {
200: {
description: "User updated successfully",
},
400: {
description: "Invalid input parameters",
},
500: {
description: "Server error",
}
}
},
patch: {
tags: ["User"],
description: "Update user",
parameters: [
{
name: "id",
in: "path",
// schema: {
// $ref: "#/components/schemas/id", // data model of the param
// },
required: true,
description: "A single user id",
}
],
requestBody: {
content: {
}
},
responses: {
}
},
delete: {
tags: ["User"],
description: "Delete user",
parameters: [
{
name: "id",
in: "path",
schema: {
$ref: "#/components/schemas/id",
},
required: true,
},
],
responses: {
204: {
description: "User deleted successfully",
},
400: {
description: "You need to provide valid Id'",
},
404: {
description: "User not found",
},
500: {
description: "Internal server error",
},
},
}
},
'/auth/token': {
post: {
tags: ["Token"],
description: "Log in user",
parameters: [],
requestBody: {
content: {
"application/json": {
schema: {
type: "object",
properties: {
email: {
type: "string"
},
password: {
type: "string"
}
}
}
}
}
},
responses: {
201: {
description: "User logged in successfully!"
},
400: {
description: "Wrong credentials!"
},
500: {
description: "Internal server error"
}
}
}
},
'/auth/logout': {
post: {
tags: ["Token"],
description: "Log out user",
parameters: [],
requestBody: {
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/Token"
}
}
}
},
responses: {
201: {
description: "User logged out successfully"
},
404: {
description: "No user has the token provided!"
},
500: {
description: "Internal server error"
}
}
}
},
'/auth/refresh': {
post: {
tags: ["Token"],
description: "Log out user",
parameters: [],
requestBody: {
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/Token"
}
}
}
},
responses: {
201: {
description: "User refreshed successfully"
},
404: {
description: "Token not valid!"
}
}
}
}
},
components: {
schemas: {
id: {
type: "string",
description: "An id of a user"
},
User: {
type: "object",
properties: {
name: {
type: "string"
},
email: {
type: "string"
},
password: {
type: "string"
},
tokens: {
type: "array",
items: {
type: "string"
}
}
}
},
Token: {
type: "object",
properties: {
token: {
type: "string"
},
userId: {
type: "string"
}
}
}
}
}
}

+ 19
- 0
src/validators/users.js Voir le fichier

@@ -0,0 +1,19 @@
const Joi = require("joi");
Joi.objectId = require('joi-objectid')(Joi)


const getIdValidator = Joi.objectId().required();

const getUserValidator = Joi.object({
name: Joi.string().min(2).required(),
password: Joi.string().min(8).regex(/[a-zA-Z0-9]{3,30}/).required(),
email: Joi.string().email().required(),
roles: Joi.string()
})

const getUpdatedUserValidator = Joi.object({
name: Joi.string().min(2).required(),
roles: Joi.string()
})

module.exports = {getUserValidator, getIdValidator, getUpdatedUserValidator}

Chargement…
Annuler
Enregistrer