#66 develop

Merge aplicado
radivoje.milutinovic aplicou merge dos 57 commits de develop em master 3 anos atrás

+ 2
- 0
.dockerignore Ver arquivo

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

+ 4
- 0
.gitignore Ver arquivo

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

+ 16
- 0
Dockerfile Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 35
- 0
package.json Ver arquivo

@@ -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 Ver arquivo


+ 6
- 0
src/config/default.json Ver arquivo

@@ -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 Ver arquivo

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

+ 70
- 0
src/database/models/token.js Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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 Ver arquivo

@@ -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}

Carregando…
Cancelar
Salvar