├── public
└── .gitkeep
├── app
├── services
│ ├── users
│ │ ├── index.js
│ │ └── walletAddressExists.js
│ └── crypto
│ │ ├── index.js
│ │ ├── getCoseSign1Bech32Address.js
│ │ ├── verifyCoseSign1Signature.js
│ │ ├── verifyCoseSign1Address.js
│ │ ├── verifyCoseSign1Signature.test.js
│ │ └── verifyCoseSign1Address.test.js
├── controllers
│ ├── users
│ │ ├── helpers
│ │ │ ├── index.js
│ │ │ └── createItemInDb.js
│ │ ├── index.js
│ │ ├── validators
│ │ │ ├── index.js
│ │ │ ├── validateGetUser.js
│ │ │ ├── validateDeleteUser.js
│ │ │ ├── validateUpdateUser.js
│ │ │ └── validateCreateUser.js
│ │ ├── getUsers.js
│ │ ├── getUser.js
│ │ ├── deleteUser.js
│ │ ├── updateUser.js
│ │ └── createUser.js
│ ├── profile
│ │ ├── validators
│ │ │ ├── index.js
│ │ │ └── validateUpdateProfile.js
│ │ ├── index.js
│ │ ├── helpers
│ │ │ ├── index.js
│ │ │ ├── findUser.js
│ │ │ ├── getProfileFromDB.js
│ │ │ └── updateProfileInDB.js
│ │ ├── getProfile.js
│ │ └── updateProfile.js
│ ├── cities
│ │ ├── helpers
│ │ │ ├── index.js
│ │ │ ├── getAllItemsFromDB.js
│ │ │ ├── cityExists.js
│ │ │ └── cityExistsExcludingItself.js
│ │ ├── validators
│ │ │ ├── index.js
│ │ │ ├── validateGetCity.js
│ │ │ ├── validateDeleteCity.js
│ │ │ ├── validateCreateCity.js
│ │ │ └── validateUpdateCity.js
│ │ ├── index.js
│ │ ├── getAllCities.js
│ │ ├── getCities.js
│ │ ├── getCity.js
│ │ ├── deleteCity.js
│ │ ├── createCity.js
│ │ └── updateCity.js
│ └── auth
│ │ ├── logout.js
│ │ ├── helpers
│ │ ├── blockIsExpired.js
│ │ ├── changeWalletResponse.js
│ │ ├── verifyUser.js
│ │ ├── findUserById.js
│ │ ├── updateWallet.js
│ │ ├── setUserInfo.js
│ │ ├── findUserToResetWallet.js
│ │ ├── returnRegisterToken.js
│ │ ├── registerUser.js
│ │ ├── findUser.js
│ │ ├── verificationExists.js
│ │ ├── blockUser.js
│ │ ├── findChangeWallet.js
│ │ ├── findUserByWalletAddress.js
│ │ ├── generateAccessToken.js
│ │ ├── generateRefreshToken.js
│ │ ├── checkPermissions.js
│ │ ├── getUserIdFromToken.js
│ │ ├── saveChangeWallet.js
│ │ ├── markChangeWalletAsUsed.js
│ │ ├── saveUserAccessAndReturnToken.js
│ │ └── index.js
│ │ ├── validators
│ │ ├── index.js
│ │ ├── validateVerify.js
│ │ ├── validateChangeWallet.js
│ │ ├── validateLogin.js
│ │ ├── validateResetWallet.js
│ │ └── validateRegister.js
│ │ ├── index.js
│ │ ├── roleAuthorization.js
│ │ ├── verify.js
│ │ ├── login.js
│ │ ├── resetWallet.js
│ │ ├── getRefreshToken.js
│ │ ├── changeWallet.js
│ │ └── register.js
├── middleware
│ ├── auth
│ │ ├── index.js
│ │ ├── encrypt.js
│ │ └── decrypt.js
│ ├── utils
│ │ ├── getBrowserInfo.js
│ │ ├── getIP.js
│ │ ├── buildSuccObject.js
│ │ ├── getCountry.js
│ │ ├── removeExtensionFromFile.js
│ │ ├── buildErrObject.js
│ │ ├── isIDGood.js
│ │ ├── handleError.js
│ │ ├── itemNotFound.js
│ │ ├── validateResult.js
│ │ ├── index.js
│ │ └── handleError.test.js
│ ├── db
│ │ ├── buildSort.js
│ │ ├── cleanPaginationID.js
│ │ ├── createItem.js
│ │ ├── getItem.js
│ │ ├── deleteItem.js
│ │ ├── index.js
│ │ ├── updateItem.js
│ │ ├── getItems.js
│ │ ├── listInitOptions.js
│ │ └── checkQueryString.js
│ └── emailer
│ │ ├── index.js
│ │ ├── sendRegistrationEmailMessage.js
│ │ ├── sendChangeWalletEmailMessage.js
│ │ ├── emailExists.js
│ │ ├── prepareToSendEmail.js
│ │ ├── emailExistsExcludingMyself.js
│ │ └── sendEmail.js
├── models
│ ├── city.js
│ ├── index.js
│ ├── userAccess.js
│ ├── changeWallet.js
│ └── user.js
└── routes
│ ├── profile.js
│ ├── index.js
│ ├── auth.js
│ ├── users.js
│ └── cities.js
├── .prettierrc.json
├── CONTRIBUTING.md
├── jsconfig.json
├── .editorconfig
├── test
├── helpers
│ └── auth
│ │ ├── createFakePrivateKey.js
│ │ ├── createRewardAddress.js
│ │ ├── createCOSEKey.js
│ │ ├── index.js
│ │ ├── createCOSESign1Signature.js
│ │ ├── getUserLoginDetails.js
│ │ └── getAdminLoginDetails.js
├── profile.js
├── cities.js
├── users.js
└── auth.js
├── .travis.yml
├── jest.config.js
├── locales
├── en.json
└── es.json
├── .env.example
├── CHANGELOG.md
├── seed.js
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── build-and-test.yml
├── views
└── index.html
├── clean.js
├── LICENSE
├── .vscode
└── launch.json
├── data
├── 2.cities
│ └── city.js
└── 1.users
│ └── user.js
├── .gitignore
├── config
├── mongo.js
└── passport.js
├── server.js
├── .eslintrc.json
├── postmanCrypto.js
├── mockWalletSignatures.js
├── CODE_OF_CONDUCT.md
├── package.json
└── README.md
/public/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/services/users/index.js:
--------------------------------------------------------------------------------
1 | const { walletAddressExists } = require('./walletAddressExists')
2 |
3 | module.exports = { walletAddressExists }
4 |
--------------------------------------------------------------------------------
/app/controllers/users/helpers/index.js:
--------------------------------------------------------------------------------
1 | const { createItemInDb } = require('./createItemInDb')
2 |
3 | module.exports = {
4 | createItemInDb
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "semi": false,
4 | "singleQuote": true,
5 | "printWidth": 80,
6 | "bracketSpacing": true
7 | }
8 |
--------------------------------------------------------------------------------
/app/controllers/profile/validators/index.js:
--------------------------------------------------------------------------------
1 | const { validateUpdateProfile } = require('./validateUpdateProfile')
2 |
3 | module.exports = {
4 | validateUpdateProfile
5 | }
6 |
--------------------------------------------------------------------------------
/app/middleware/auth/index.js:
--------------------------------------------------------------------------------
1 | const { decrypt } = require('./decrypt')
2 | const { encrypt } = require('./encrypt')
3 |
4 | module.exports = {
5 | decrypt,
6 | encrypt
7 | }
8 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Awesome! We're happy that you want to contribute.
4 |
5 | Make sure that you're read and understand the [Code of Conduct](CODE_OF_CONDUCT.md).
6 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es5"
5 | },
6 | "include": ["app/**/*", "test/**/*"],
7 | "exclude": ["node_modules"]
8 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/app/controllers/profile/index.js:
--------------------------------------------------------------------------------
1 | const { getProfile } = require('./getProfile')
2 | const { updateProfile } = require('./updateProfile')
3 |
4 | module.exports = {
5 | getProfile,
6 | updateProfile
7 | }
8 |
--------------------------------------------------------------------------------
/app/middleware/utils/getBrowserInfo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets browser info from user
3 | * @param {*} req - request object
4 | */
5 | const getBrowserInfo = ({ headers }) => headers['user-agent']
6 |
7 | module.exports = { getBrowserInfo }
8 |
--------------------------------------------------------------------------------
/app/middleware/utils/getIP.js:
--------------------------------------------------------------------------------
1 | const requestIp = require('request-ip')
2 |
3 | /**
4 | * Gets IP from user
5 | * @param {*} req - request object
6 | */
7 | const getIP = (req) => requestIp.getClientIp(req)
8 |
9 | module.exports = { getIP }
10 |
--------------------------------------------------------------------------------
/app/middleware/utils/buildSuccObject.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds success object
3 | * @param {string} message - success text
4 | */
5 | const buildSuccObject = (message = '') => {
6 | return {
7 | msg: message
8 | }
9 | }
10 |
11 | module.exports = { buildSuccObject }
12 |
--------------------------------------------------------------------------------
/app/middleware/utils/getCountry.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Gets country from user using CloudFlare header 'cf-ipcountry'
3 | * @param {*} req - request object
4 | */
5 | const getCountry = ({ headers }) =>
6 | headers['cf-ipcountry'] ? headers['cf-ipcountry'] : 'XX'
7 |
8 | module.exports = { getCountry }
9 |
--------------------------------------------------------------------------------
/app/middleware/utils/removeExtensionFromFile.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Removes extension from file
3 | * @param {string} file - filename
4 | */
5 | const removeExtensionFromFile = (file) => {
6 | return file.split('.').slice(0, -1).join('.').toString()
7 | }
8 |
9 | module.exports = { removeExtensionFromFile }
10 |
--------------------------------------------------------------------------------
/app/controllers/profile/helpers/index.js:
--------------------------------------------------------------------------------
1 | const { findUser } = require('./findUser')
2 | const { getProfileFromDB } = require('./getProfileFromDB')
3 | const { updateProfileInDB } = require('./updateProfileInDB')
4 |
5 | module.exports = {
6 | findUser,
7 | getProfileFromDB,
8 | updateProfileInDB
9 | }
10 |
--------------------------------------------------------------------------------
/app/middleware/utils/buildErrObject.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds error object
3 | * @param {number} code - error code
4 | * @param {string} message - error text
5 | */
6 | const buildErrObject = (code = '', message = '') => {
7 | return {
8 | code,
9 | message
10 | }
11 | }
12 |
13 | module.exports = { buildErrObject }
14 |
--------------------------------------------------------------------------------
/app/middleware/db/buildSort.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds sorting
3 | * @param {string} sort - field to sort from
4 | * @param {number} order - order for query (1,-1)
5 | */
6 | const buildSort = (sort = '', order = 1) => {
7 | const sortBy = {}
8 | sortBy[sort] = order
9 | return sortBy
10 | }
11 |
12 | module.exports = { buildSort }
13 |
--------------------------------------------------------------------------------
/app/middleware/db/cleanPaginationID.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Hack for mongoose-paginate, removes 'id' from results
3 | * @param {Object} result - result object
4 | */
5 | const cleanPaginationID = (result = {}) => {
6 | result.docs.map((element) => delete element.id)
7 | return result
8 | }
9 |
10 | module.exports = { cleanPaginationID }
11 |
--------------------------------------------------------------------------------
/app/controllers/cities/helpers/index.js:
--------------------------------------------------------------------------------
1 | const { cityExists } = require('./cityExists')
2 | const { cityExistsExcludingItself } = require('./cityExistsExcludingItself')
3 | const { getAllItemsFromDB } = require('./getAllItemsFromDB')
4 |
5 | module.exports = {
6 | cityExists,
7 | cityExistsExcludingItself,
8 | getAllItemsFromDB
9 | }
10 |
--------------------------------------------------------------------------------
/app/controllers/auth/logout.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Login function called by route
3 | * @param {import('express').Request} req - request object
4 | * @param {import('express').Response} res - response object
5 | */
6 | const logout = async (_, res) => {
7 | res.clearCookie('jwt')
8 | res.status(200).send('Logout successful')
9 | }
10 |
11 | module.exports = { logout }
12 |
--------------------------------------------------------------------------------
/app/controllers/users/index.js:
--------------------------------------------------------------------------------
1 | const { createUser } = require('./createUser')
2 | const { deleteUser } = require('./deleteUser')
3 | const { getUser } = require('./getUser')
4 | const { getUsers } = require('./getUsers')
5 | const { updateUser } = require('./updateUser')
6 |
7 | module.exports = {
8 | createUser,
9 | deleteUser,
10 | getUser,
11 | getUsers,
12 | updateUser
13 | }
14 |
--------------------------------------------------------------------------------
/app/services/crypto/index.js:
--------------------------------------------------------------------------------
1 | const { verifyCoseSign1Address } = require('./verifyCoseSign1Address.js')
2 | const { verifyCoseSign1Signature } = require('./verifyCoseSign1Signature.js')
3 | const { getCoseSign1Bech32Address } = require('./getCoseSign1Bech32Address.js')
4 |
5 | module.exports = {
6 | verifyCoseSign1Address,
7 | verifyCoseSign1Signature,
8 | getCoseSign1Bech32Address
9 | }
10 |
--------------------------------------------------------------------------------
/test/helpers/auth/createFakePrivateKey.js:
--------------------------------------------------------------------------------
1 | const CSL = require('@emurgo/cardano-serialization-lib-nodejs')
2 |
3 | /**
4 | *
5 | * @param {number} accountNumber - Number between 0 and 255 for mocking private key
6 | * @returns
7 | */
8 | const createFakePrivateKey = (accountNumber) => {
9 | return CSL.PrivateKey.from_normal_bytes(new Array(32).fill(accountNumber))
10 | }
11 |
12 | module.exports = { createFakePrivateKey }
13 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/blockIsExpired.js:
--------------------------------------------------------------------------------
1 | const LOGIN_ATTEMPTS = 5
2 |
3 | /**
4 | * Checks that login attempts are greater than specified in constant and also that blockexpires is less than now
5 | * @param {Object} user - user object
6 | */
7 | const blockIsExpired = ({ loginAttempts = 0, blockExpires = '' }) =>
8 | loginAttempts > LOGIN_ATTEMPTS && blockExpires <= new Date()
9 |
10 | module.exports = { blockIsExpired }
11 |
--------------------------------------------------------------------------------
/app/controllers/cities/validators/index.js:
--------------------------------------------------------------------------------
1 | const { validateCreateCity } = require('./validateCreateCity')
2 | const { validateDeleteCity } = require('./validateDeleteCity')
3 | const { validateGetCity } = require('./validateGetCity')
4 | const { validateUpdateCity } = require('./validateUpdateCity')
5 |
6 | module.exports = {
7 | validateCreateCity,
8 | validateDeleteCity,
9 | validateGetCity,
10 | validateUpdateCity
11 | }
12 |
--------------------------------------------------------------------------------
/app/controllers/users/validators/index.js:
--------------------------------------------------------------------------------
1 | const { validateCreateUser } = require('./validateCreateUser')
2 | const { validateDeleteUser } = require('./validateDeleteUser')
3 | const { validateGetUser } = require('./validateGetUser')
4 | const { validateUpdateUser } = require('./validateUpdateUser')
5 |
6 | module.exports = {
7 | validateCreateUser,
8 | validateDeleteUser,
9 | validateGetUser,
10 | validateUpdateUser
11 | }
12 |
--------------------------------------------------------------------------------
/app/models/city.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const mongoosePaginate = require('mongoose-paginate-v2')
3 |
4 | const CitySchema = new mongoose.Schema(
5 | {
6 | name: {
7 | type: String,
8 | required: true
9 | }
10 | },
11 | {
12 | versionKey: false,
13 | timestamps: true
14 | }
15 | )
16 | CitySchema.plugin(mongoosePaginate)
17 | module.exports = mongoose.model('City', CitySchema)
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: xenial
2 | language: node_js
3 | node_js:
4 | - '12'
5 | cache:
6 | npm: true
7 | services:
8 | - mongodb
9 | before_script:
10 | - cp .env.example .env
11 | env:
12 | - NODE_ENV=test
13 | if: tag IS present
14 | deploy:
15 | provider: npm
16 | email: $NPM_EMAIL
17 | api_key: $NPM_DEPLOY_TOKEN
18 | on:
19 | branch: master
20 | tags: true
21 | repo: davellanedam/node-express-mongodb-jwt-rest-api-skeleton
22 |
--------------------------------------------------------------------------------
/app/controllers/cities/index.js:
--------------------------------------------------------------------------------
1 | const { createCity } = require('./createCity')
2 | const { deleteCity } = require('./deleteCity')
3 | const { getAllCities } = require('./getAllCities')
4 | const { getCity } = require('./getCity')
5 | const { getCities } = require('./getCities')
6 | const { updateCity } = require('./updateCity')
7 |
8 | module.exports = {
9 | createCity,
10 | deleteCity,
11 | getAllCities,
12 | getCity,
13 | getCities,
14 | updateCity
15 | }
16 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | verbose: true,
3 | testEnvironment: 'node',
4 | collectCoverage: true,
5 | collectCoverageFrom: [
6 | '**/*.js',
7 | '!jest.config.js',
8 | '!**/data/**',
9 | '!**/node_modules/**',
10 | '!**/.history/**',
11 | '!**/test/**',
12 | '!**/coverage/**',
13 | '!**/tmp/**'
14 | ],
15 | coverageDirectory: 'coverage/unit',
16 | coverageReporters: ['json', 'text', 'lcov'],
17 | testPathIgnorePatterns: ['.history/']
18 | }
19 |
--------------------------------------------------------------------------------
/app/controllers/auth/validators/index.js:
--------------------------------------------------------------------------------
1 | const { validateChangeWallet } = require('./validateChangeWallet')
2 | const { validateLogin } = require('./validateLogin')
3 | const { validateRegister } = require('./validateRegister')
4 | const { validateResetWallet } = require('./validateResetWallet')
5 | const { validateVerify } = require('./validateVerify')
6 |
7 | module.exports = {
8 | validateChangeWallet,
9 | validateLogin,
10 | validateRegister,
11 | validateResetWallet,
12 | validateVerify
13 | }
14 |
--------------------------------------------------------------------------------
/app/controllers/auth/validators/validateVerify.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates verify request
6 | */
7 | const validateVerify = [
8 | check('id')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | (req, res, next) => {
15 | validateResult(req, res, next)
16 | }
17 | ]
18 |
19 | module.exports = { validateVerify }
20 |
--------------------------------------------------------------------------------
/app/controllers/users/validators/validateGetUser.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates get item request
6 | */
7 | const validateGetUser = [
8 | check('id')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | (req, res, next) => {
15 | validateResult(req, res, next)
16 | }
17 | ]
18 |
19 | module.exports = { validateGetUser }
20 |
--------------------------------------------------------------------------------
/app/middleware/db/createItem.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('../../middleware/utils')
2 |
3 | /**
4 | * Creates a new item in database
5 | * @param {Object} req - request object
6 | */
7 | const createItem = (req = {}, model = {}) => {
8 | return new Promise((resolve, reject) => {
9 | model.create(req, (err, item) => {
10 | if (err) {
11 | reject(buildErrObject(422, err.message))
12 | }
13 | resolve(item)
14 | })
15 | })
16 | }
17 |
18 | module.exports = { createItem }
19 |
--------------------------------------------------------------------------------
/app/controllers/cities/validators/validateGetCity.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates get item request
6 | */
7 | const validateGetCity = [
8 | check('id')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | (req, res, next) => {
15 | validateResult(req, res, next)
16 | }
17 | ]
18 |
19 | module.exports = { validateGetCity }
20 |
--------------------------------------------------------------------------------
/app/middleware/utils/isIDGood.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const { buildErrObject } = require('./buildErrObject')
3 |
4 | /**
5 | * Checks if given ID is good for MongoDB
6 | * @param {string} id - id to check
7 | */
8 | const isIDGood = async (id = '') => {
9 | return new Promise((resolve, reject) => {
10 | const goodID = mongoose.Types.ObjectId.isValid(id)
11 | return goodID ? resolve(id) : reject(buildErrObject(422, 'ID_MALFORMED'))
12 | })
13 | }
14 |
15 | module.exports = { isIDGood }
16 |
--------------------------------------------------------------------------------
/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "registration": {
3 | "SUBJECT": "Verify your email at myProject",
4 | "MESSAGE": "
Hello %s.
Welcome! To verify your email, please click in this link:
%s/verify/%s
Thank you.
"
5 | },
6 | "changeWallet": {
7 | "SUBJECT": "Password recovery at myProject",
8 | "MESSAGE": "To recover the password for user: %s
click the following link:
%s/reset/%s
If this was a mistake, you can ignore this message.
Thank you.
"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | PORT=3000
2 | JWT_SECRET=MyUltraSecurePassWordIWontForgetToChange
3 | JWT_ACCESS_TOKEN_EXPIRATION_IN_SECONDS=60
4 | JWT_REFRESH_TOKEN_EXPIRATION_IN_SECONDS=300
5 | PAYLOAD_VALIDITY_IN_SECONDS=120
6 | MONGO_URI=mongodb://localhost:27017/myprojectdbname
7 | EMAIL_FROM_NAME=My Project
8 | EMAIL_FROM_ADDRESS=info@myproject.com
9 | EMAIL_SMTP_DOMAIN_MAILGUN=myproject.com
10 | EMAIL_SMTP_API_MAILGUN=123456
11 | FRONTEND_URL=http://localhost:8080
12 | USE_REDIS=false
13 | REDIS_HOST=127.0.0.1
14 | REDIS_PORT=6379
15 | HOST=HOST
16 |
--------------------------------------------------------------------------------
/app/controllers/cities/validators/validateDeleteCity.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates delete item request
6 | */
7 | const validateDeleteCity = [
8 | check('id')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | (req, res, next) => {
15 | validateResult(req, res, next)
16 | }
17 | ]
18 |
19 | module.exports = { validateDeleteCity }
20 |
--------------------------------------------------------------------------------
/app/controllers/users/validators/validateDeleteUser.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates delete item request
6 | */
7 | const validateDeleteUser = [
8 | check('id')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | (req, res, next) => {
15 | validateResult(req, res, next)
16 | }
17 | ]
18 |
19 | module.exports = { validateDeleteUser }
20 |
--------------------------------------------------------------------------------
/app/controllers/cities/getAllCities.js:
--------------------------------------------------------------------------------
1 | const { handleError } = require('../../middleware/utils')
2 | const { getAllItemsFromDB } = require('./helpers')
3 |
4 | /**
5 | * Get all items function called by route
6 | * @param {Object} req - request object
7 | * @param {Object} res - response object
8 | */
9 | const getAllCities = async (req, res) => {
10 | try {
11 | res.status(200).json(await getAllItemsFromDB())
12 | } catch (error) {
13 | handleError(res, error)
14 | }
15 | }
16 |
17 | module.exports = { getAllCities }
18 |
--------------------------------------------------------------------------------
/test/helpers/auth/createRewardAddress.js:
--------------------------------------------------------------------------------
1 | const CSL = require('@emurgo/cardano-serialization-lib-nodejs')
2 |
3 | /**
4 | *
5 | * @param {CSL.PrivateKey} privateKey
6 | * @param {CSL.NetworkId} networkId
7 | * @returns
8 | */
9 | const createRewardAddress = (
10 | privateKey,
11 | networkId = CSL.NetworkId.mainnet()
12 | ) => {
13 | return CSL.RewardAddress.new(
14 | networkId.kind(),
15 | CSL.StakeCredential.from_keyhash(privateKey.to_public().hash())
16 | )
17 | }
18 |
19 | module.exports = { createRewardAddress }
20 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## v1.1.0 (Jan 07, 2023)
2 |
3 | * Passport Cardano web3 implementation
4 |
5 | ## v1.0.0 (Dec 23, 2022)
6 |
7 | * Wallet address uniqueness
8 | * Payload expiration checking
9 |
10 | ## v0.2.0 (Dec 21, 2022)
11 |
12 | * Logout implementation
13 | * React frontend documentation
14 |
15 | ## v0.1.0 (Dec 08, 2022)
16 |
17 | * Login request without user's email
18 | * Refresh token with HTTPOnly Cookie
19 | * Payload verificiation
20 |
21 | ## v0.0.2 (Nov 30, 2022)
22 |
23 | * Cardano message signing spec integration
24 |
--------------------------------------------------------------------------------
/app/controllers/cities/validators/validateCreateCity.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates create new item request
6 | */
7 | const validateCreateCity = [
8 | check('name')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY')
14 | .trim(),
15 | (req, res, next) => {
16 | validateResult(req, res, next)
17 | }
18 | ]
19 |
20 | module.exports = { validateCreateCity }
21 |
--------------------------------------------------------------------------------
/app/middleware/db/getItem.js:
--------------------------------------------------------------------------------
1 | const { itemNotFound } = require('../../middleware/utils')
2 |
3 | /**
4 | * Gets item from database by id
5 | * @param {string} id - item id
6 | */
7 | const getItem = (id = '', model = {}) => {
8 | return new Promise((resolve, reject) => {
9 | model.findById(id, async (err, item) => {
10 | try {
11 | await itemNotFound(err, item, 'NOT_FOUND')
12 | resolve(item)
13 | } catch (error) {
14 | reject(error)
15 | }
16 | })
17 | })
18 | }
19 |
20 | module.exports = { getItem }
21 |
--------------------------------------------------------------------------------
/locales/es.json:
--------------------------------------------------------------------------------
1 | {
2 | "registration": {
3 | "SUBJECT": "Verificar tu Email en myProject",
4 | "MESSAGE": "Hola %s.
¡Bienvenido! Para verificar tu Email, por favor haz click en este enlace:
%s/verify/%s
Gracias.
"
5 | },
6 | "changeWallet": {
7 | "SUBJECT": "Recuperar contraseña en myProject",
8 | "MESSAGE": "Para recuperar la contraseña para el usuario: %s
haz click en el siguiente enlace:
%s/reset/%s
Si esto fue un error, puedes ignorar este mensaje.
Gracias.
"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/app/middleware/utils/handleError.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Handles error by printing to console in development env and builds and sends an error response
3 | * @param {Object} res - response object
4 | * @param {Object} err - error object
5 | */
6 | const handleError = (res = {}, err = {}) => {
7 | // Prints error in console
8 | if (process.env.NODE_ENV === 'development') {
9 | console.log(err)
10 | }
11 | // Sends error to user
12 | res.status(err.code).json({
13 | errors: {
14 | msg: err.message
15 | }
16 | })
17 | }
18 |
19 | module.exports = { handleError }
20 |
--------------------------------------------------------------------------------
/app/controllers/profile/getProfile.js:
--------------------------------------------------------------------------------
1 | const { getProfileFromDB } = require('./helpers')
2 | const { isIDGood, handleError } = require('../../middleware/utils')
3 |
4 | /**
5 | * Get profile function called by route
6 | * @param {Object} req - request object
7 | * @param {Object} res - response object
8 | */
9 | const getProfile = async (req, res) => {
10 | try {
11 | const id = await isIDGood(req.user._id)
12 | res.status(200).json(await getProfileFromDB(id))
13 | } catch (error) {
14 | handleError(res, error)
15 | }
16 | }
17 |
18 | module.exports = { getProfile }
19 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/changeWalletResponse.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds an object with created forgot password object, if env is development or testing exposes the verification
3 | * @param {Object} item - created forgot password object
4 | */
5 | const changeWalletResponse = ({ email = '', verification = '' }) => {
6 | let data = {
7 | msg: 'RESET_EMAIL_SENT',
8 | email
9 | }
10 | if (process.env.NODE_ENV !== 'production') {
11 | data = {
12 | ...data,
13 | verification
14 | }
15 | }
16 | return data
17 | }
18 |
19 | module.exports = { changeWalletResponse }
20 |
--------------------------------------------------------------------------------
/app/controllers/auth/validators/validateChangeWallet.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates change wallet request
6 | */
7 | const validateChangeWallet = [
8 | check('email')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY')
14 | .isEmail()
15 | .withMessage('EMAIL_IS_NOT_VALID'),
16 | (req, res, next) => {
17 | validateResult(req, res, next)
18 | }
19 | ]
20 |
21 | module.exports = { validateChangeWallet }
22 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/verifyUser.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('../../../middleware/utils')
2 |
3 | /**
4 | * Verifies an user
5 | * @param {Object} user - user object
6 | */
7 | const verifyUser = (user = {}) => {
8 | return new Promise((resolve, reject) => {
9 | user.verified = true
10 | user.save((err, item) => {
11 | if (err) {
12 | return reject(buildErrObject(422, err.message))
13 | }
14 | resolve({
15 | email: item.email,
16 | verified: item.verified
17 | })
18 | })
19 | })
20 | }
21 |
22 | module.exports = { verifyUser }
23 |
--------------------------------------------------------------------------------
/app/controllers/auth/index.js:
--------------------------------------------------------------------------------
1 | const { changeWallet } = require('./changeWallet')
2 | const { getRefreshToken } = require('./getRefreshToken')
3 | const { login } = require('./login')
4 | const { logout } = require('./logout')
5 | const { register } = require('./register')
6 | const { resetWallet } = require('./resetWallet')
7 | const { roleAuthorization } = require('./roleAuthorization')
8 | const { verify } = require('./verify')
9 |
10 | module.exports = {
11 | changeWallet,
12 | getRefreshToken,
13 | login,
14 | logout,
15 | register,
16 | resetWallet,
17 | roleAuthorization,
18 | verify
19 | }
20 |
--------------------------------------------------------------------------------
/app/controllers/auth/roleAuthorization.js:
--------------------------------------------------------------------------------
1 | const { checkPermissions } = require('./helpers')
2 |
3 | const { handleError } = require('../../middleware/utils')
4 |
5 | /**
6 | * Roles authorization function called by route
7 | * @param {Array} roles - roles specified on the route
8 | */
9 | const roleAuthorization = (roles) => async (req, res, next) => {
10 | try {
11 | const data = {
12 | id: req.user._id,
13 | roles
14 | }
15 | await checkPermissions(data, next)
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { roleAuthorization }
22 |
--------------------------------------------------------------------------------
/app/models/index.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const modelsPath = `${__dirname}/`
3 | const { removeExtensionFromFile } = require('../middleware/utils')
4 |
5 | module.exports = () => {
6 | /*
7 | * Load models dynamically
8 | */
9 |
10 | // Loop models path and loads every file as a model except this file
11 | fs.readdirSync(modelsPath).filter((file) => {
12 | // Take filename and remove last part (extension)
13 | const modelFile = removeExtensionFromFile(file)
14 | // Prevents loading of this file
15 | return modelFile !== 'index' ? require(`./${modelFile}`) : ''
16 | })
17 | }
18 |
--------------------------------------------------------------------------------
/app/middleware/db/deleteItem.js:
--------------------------------------------------------------------------------
1 | const { buildSuccObject, itemNotFound } = require('../../middleware/utils')
2 |
3 | /**
4 | * Deletes an item from database by id
5 | * @param {string} id - id of item
6 | */
7 | const deleteItem = (id = '', model = {}) => {
8 | return new Promise((resolve, reject) => {
9 | model.findByIdAndRemove(id, async (err, item) => {
10 | try {
11 | await itemNotFound(err, item, 'NOT_FOUND')
12 | resolve(buildSuccObject('DELETED'))
13 | } catch (error) {
14 | reject(error)
15 | }
16 | })
17 | })
18 | }
19 |
20 | module.exports = { deleteItem }
21 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/findUserById.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Finds user by ID
6 | * @param {string} id - user´s id
7 | */
8 | const findUserById = (userId = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findById(userId, async (err, item) => {
11 | try {
12 | await itemNotFound(err, item, 'USER_DOES_NOT_EXIST')
13 | resolve(item)
14 | } catch (error) {
15 | reject(error)
16 | }
17 | })
18 | })
19 | }
20 |
21 | module.exports = { findUserById }
22 |
--------------------------------------------------------------------------------
/app/controllers/profile/helpers/findUser.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Finds user by id
6 | * @param {string} id - user id
7 | */
8 | const findUser = (id = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findById(id, 'password email', async (err, user) => {
11 | try {
12 | await itemNotFound(err, user, 'USER_DOES_NOT_EXIST')
13 | resolve(user)
14 | } catch (error) {
15 | reject(error)
16 | }
17 | })
18 | })
19 | }
20 |
21 | module.exports = { findUser }
22 |
--------------------------------------------------------------------------------
/app/middleware/utils/itemNotFound.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('./buildErrObject')
2 |
3 | /**
4 | * Item not found
5 | * @param {Object} err - error object
6 | * @param {Object} item - item result object
7 | * @param {string} message - message
8 | */
9 | const itemNotFound = (err = {}, item = {}, message = 'NOT_FOUND') => {
10 | return new Promise((resolve, reject) => {
11 | if (err) {
12 | return reject(buildErrObject(422, err.message))
13 | }
14 | if (!item) {
15 | return reject(buildErrObject(404, message))
16 | }
17 | resolve()
18 | })
19 | }
20 |
21 | module.exports = { itemNotFound }
22 |
--------------------------------------------------------------------------------
/seed.js:
--------------------------------------------------------------------------------
1 | require('dotenv-safe').config()
2 | const { Seeder } = require('mongo-seeding')
3 | const path = require('path')
4 | const config = {
5 | database: process.env.MONGO_URI,
6 | inputPath: path.resolve(__dirname, './data'),
7 | dropDatabase: false
8 | }
9 | const seeder = new Seeder(config)
10 | const collections = seeder.readCollectionsFromPath(path.resolve('./data'))
11 |
12 | const main = async () => {
13 | try {
14 | await seeder.import(collections)
15 | console.log('Seed complete!')
16 | process.exit(0)
17 | } catch (err) {
18 | console.log(err)
19 | process.exit(0)
20 | }
21 | }
22 |
23 | main()
24 |
--------------------------------------------------------------------------------
/test/helpers/auth/createCOSEKey.js:
--------------------------------------------------------------------------------
1 | const MSG = require('@emurgo/cardano-message-signing-nodejs')
2 |
3 | /**
4 | * Create a COSE Key structure from a private key
5 | *
6 | * @param {CSL.PrivateKey} privateKey - private key to extract the public key
7 | * @returns
8 | */
9 | const createCOSEKey = (privateKey) => {
10 | const coseKey = MSG.COSEKey.new(MSG.Label.new_int(MSG.Int.new_i32(1)))
11 | coseKey.set_header(
12 | MSG.Label.new_int(MSG.Int.new_negative(MSG.BigNum.from_str('2'))),
13 | MSG.CBORValue.new_bytes(privateKey.to_public().as_bytes())
14 | )
15 | return coseKey
16 | }
17 |
18 | module.exports = { createCOSEKey }
19 |
--------------------------------------------------------------------------------
/app/controllers/users/getUsers.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models/user')
2 | const { handleError } = require('../../middleware/utils')
3 | const { getItems, checkQueryString } = require('../../middleware/db')
4 |
5 | /**
6 | * Get items function called by route
7 | * @param {Object} req - request object
8 | * @param {Object} res - response object
9 | */
10 | const getUsers = async (req, res) => {
11 | try {
12 | const query = await checkQueryString(req.query)
13 | res.status(200).json(await getItems(req, User, query))
14 | } catch (error) {
15 | handleError(res, error)
16 | }
17 | }
18 |
19 | module.exports = { getUsers }
20 |
--------------------------------------------------------------------------------
/test/helpers/auth/index.js:
--------------------------------------------------------------------------------
1 | const { createFakePrivateKey } = require('./createFakePrivateKey.js')
2 | const { createRewardAddress } = require('./createRewardAddress')
3 | const { createCOSEKey } = require('./createCOSEKey.js')
4 | const { createCOSESign1Signature } = require('./createCOSESign1Signature.js')
5 | const { getAdminLoginDetails } = require('./getAdminLoginDetails.js')
6 | const { getUserLoginDetails } = require('./getUserLoginDetails.js')
7 |
8 | module.exports = {
9 | createFakePrivateKey,
10 | createRewardAddress,
11 | createCOSEKey,
12 | createCOSESign1Signature,
13 | getAdminLoginDetails,
14 | getUserLoginDetails
15 | }
16 |
--------------------------------------------------------------------------------
/app/controllers/cities/getCities.js:
--------------------------------------------------------------------------------
1 | const City = require('../../models/city')
2 | const { checkQueryString, getItems } = require('../../middleware/db')
3 | const { handleError } = require('../../middleware/utils')
4 |
5 | /**
6 | * Get items function called by route
7 | * @param {Object} req - request object
8 | * @param {Object} res - response object
9 | */
10 | const getCities = async (req, res) => {
11 | try {
12 | const query = await checkQueryString(req.query)
13 | res.status(200).json(await getItems(req, City, query))
14 | } catch (error) {
15 | handleError(res, error)
16 | }
17 | }
18 |
19 | module.exports = { getCities }
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/app/controllers/auth/verify.js:
--------------------------------------------------------------------------------
1 | const { matchedData } = require('express-validator')
2 | const { verificationExists, verifyUser } = require('./helpers')
3 |
4 | const { handleError } = require('../../middleware/utils')
5 |
6 | /**
7 | * Verify function called by route
8 | * @param {Object} req - request object
9 | * @param {Object} res - response object
10 | */
11 | const verify = async (req, res) => {
12 | try {
13 | req = matchedData(req)
14 | const user = await verificationExists(req.id)
15 | res.status(200).json(await verifyUser(user))
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { verify }
22 |
--------------------------------------------------------------------------------
/app/controllers/profile/helpers/getProfileFromDB.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Gets profile from database by id
6 | * @param {string} id - user id
7 | */
8 | const getProfileFromDB = (id = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findById(id, '-_id -updatedAt -createdAt', async (err, user) => {
11 | try {
12 | await itemNotFound(err, user, 'NOT_FOUND')
13 | resolve(user)
14 | } catch (error) {
15 | reject(error)
16 | }
17 | })
18 | })
19 | }
20 |
21 | module.exports = { getProfileFromDB }
22 |
--------------------------------------------------------------------------------
/app/controllers/cities/validators/validateUpdateCity.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates update item request
6 | */
7 | const validateUpdateCity = [
8 | check('name')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | check('id')
15 | .exists()
16 | .withMessage('MISSING')
17 | .not()
18 | .isEmpty()
19 | .withMessage('IS_EMPTY'),
20 | (req, res, next) => {
21 | validateResult(req, res, next)
22 | }
23 | ]
24 |
25 | module.exports = { validateUpdateCity }
26 |
--------------------------------------------------------------------------------
/app/middleware/emailer/index.js:
--------------------------------------------------------------------------------
1 | const { emailExists } = require('./emailExists')
2 | const { emailExistsExcludingMyself } = require('./emailExistsExcludingMyself')
3 | const { prepareToSendEmail } = require('./prepareToSendEmail')
4 | const { sendEmail } = require('./sendEmail')
5 | const {
6 | sendRegistrationEmailMessage
7 | } = require('./sendRegistrationEmailMessage')
8 | const {
9 | sendChangeWalletEmailMessage
10 | } = require('./sendChangeWalletEmailMessage')
11 |
12 | module.exports = {
13 | emailExists,
14 | emailExistsExcludingMyself,
15 | prepareToSendEmail,
16 | sendEmail,
17 | sendRegistrationEmailMessage,
18 | sendChangeWalletEmailMessage
19 | }
20 |
--------------------------------------------------------------------------------
/app/controllers/profile/updateProfile.js:
--------------------------------------------------------------------------------
1 | const { isIDGood, handleError } = require('../../middleware/utils')
2 | const { matchedData } = require('express-validator')
3 | const { updateProfileInDB } = require('./helpers')
4 |
5 | /**
6 | * Update profile function called by route
7 | * @param {Object} req - request object
8 | * @param {Object} res - response object
9 | */
10 | const updateProfile = async (req, res) => {
11 | try {
12 | const id = await isIDGood(req.user._id)
13 | req = matchedData(req)
14 | res.status(200).json(await updateProfileInDB(req, id))
15 | } catch (error) {
16 | handleError(res, error)
17 | }
18 | }
19 |
20 | module.exports = { updateProfile }
21 |
--------------------------------------------------------------------------------
/app/controllers/cities/getCity.js:
--------------------------------------------------------------------------------
1 | const { matchedData } = require('express-validator')
2 | const City = require('../../models/city')
3 | const { getItem } = require('../../middleware/db')
4 | const { isIDGood, handleError } = require('../../middleware/utils')
5 |
6 | /**
7 | * Get item function called by route
8 | * @param {Object} req - request object
9 | * @param {Object} res - response object
10 | */
11 | const getCity = async (req, res) => {
12 | try {
13 | req = matchedData(req)
14 | const id = await isIDGood(req.id)
15 | res.status(200).json(await getItem(id, City))
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { getCity }
22 |
--------------------------------------------------------------------------------
/app/controllers/cities/helpers/getAllItemsFromDB.js:
--------------------------------------------------------------------------------
1 | const City = require('../../../models/city')
2 | const { buildErrObject } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Gets all items from database
6 | */
7 | const getAllItemsFromDB = () => {
8 | return new Promise((resolve, reject) => {
9 | City.find(
10 | {},
11 | '-updatedAt -createdAt',
12 | {
13 | sort: {
14 | name: 1
15 | }
16 | },
17 | (err, items) => {
18 | if (err) {
19 | return reject(buildErrObject(422, err.message))
20 | }
21 | resolve(items)
22 | }
23 | )
24 | })
25 | }
26 |
27 | module.exports = { getAllItemsFromDB }
28 |
--------------------------------------------------------------------------------
/app/controllers/users/getUser.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models/user')
2 | const { matchedData } = require('express-validator')
3 | const { isIDGood, handleError } = require('../../middleware/utils')
4 | const { getItem } = require('../../middleware/db')
5 |
6 | /**
7 | * Get item function called by route
8 | * @param {Object} req - request object
9 | * @param {Object} res - response object
10 | */
11 | const getUser = async (req, res) => {
12 | try {
13 | req = matchedData(req)
14 | const id = await isIDGood(req.id)
15 | res.status(200).json(await getItem(id, User))
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { getUser }
22 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/updateWallet.js:
--------------------------------------------------------------------------------
1 | const { itemNotFound } = require('../../../middleware/utils')
2 |
3 | /**
4 | * Updates a user wallet address in database
5 | * @param {string} walletAddress - new wallet address
6 | * @param {Object} user - user object
7 | */
8 | const updateWallet = (walletAddress = '', user = {}) => {
9 | return new Promise((resolve, reject) => {
10 | user.walletAddress = walletAddress
11 | user.save(async (err, item) => {
12 | try {
13 | await itemNotFound(err, item, 'NOT_FOUND')
14 | resolve(item)
15 | } catch (error) {
16 | reject(error)
17 | }
18 | })
19 | })
20 | }
21 |
22 | module.exports = { updateWallet }
23 |
--------------------------------------------------------------------------------
/app/controllers/auth/validators/validateLogin.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates register request
6 | */
7 | /**
8 | * Validates login request
9 | */
10 | const validateLogin = [
11 | check('key')
12 | .exists()
13 | .withMessage('MISSING')
14 | .not()
15 | .isEmpty()
16 | .withMessage('IS_EMPTY'),
17 | check('signature')
18 | .exists()
19 | .withMessage('MISSING')
20 | .not()
21 | .isEmpty()
22 | .withMessage('IS_EMPTY'),
23 | (req, res, next) => {
24 | validateResult(req, res, next)
25 | }
26 | ]
27 |
28 | module.exports = { validateLogin }
29 |
--------------------------------------------------------------------------------
/app/middleware/db/index.js:
--------------------------------------------------------------------------------
1 | const { buildSort } = require('./buildSort')
2 | const { checkQueryString } = require('./checkQueryString')
3 | const { cleanPaginationID } = require('./cleanPaginationID')
4 | const { createItem } = require('./createItem')
5 | const { deleteItem } = require('./deleteItem')
6 | const { getItem } = require('./getItem')
7 | const { getItems } = require('./getItems')
8 | const { listInitOptions } = require('./listInitOptions')
9 | const { updateItem } = require('./updateItem')
10 |
11 | module.exports = {
12 | buildSort,
13 | checkQueryString,
14 | cleanPaginationID,
15 | createItem,
16 | deleteItem,
17 | getItem,
18 | getItems,
19 | listInitOptions,
20 | updateItem
21 | }
22 |
--------------------------------------------------------------------------------
/app/middleware/emailer/sendRegistrationEmailMessage.js:
--------------------------------------------------------------------------------
1 | const i18n = require('i18n')
2 | const { prepareToSendEmail } = require('./prepareToSendEmail')
3 |
4 | /**
5 | * Sends registration email
6 | * @param {string} locale - locale
7 | * @param {Object} user - user object
8 | */
9 | const sendRegistrationEmailMessage = (locale = '', user = {}) => {
10 | i18n.setLocale(locale)
11 | const subject = i18n.__('registration.SUBJECT')
12 | const htmlMessage = i18n.__(
13 | 'registration.MESSAGE',
14 | user.name,
15 | process.env.FRONTEND_URL,
16 | user.verification
17 | )
18 | prepareToSendEmail(user, subject, htmlMessage)
19 | }
20 |
21 | module.exports = { sendRegistrationEmailMessage }
22 |
--------------------------------------------------------------------------------
/app/controllers/cities/deleteCity.js:
--------------------------------------------------------------------------------
1 | const City = require('../../models/city')
2 | const { matchedData } = require('express-validator')
3 | const { isIDGood, handleError } = require('../../middleware/utils')
4 | const { deleteItem } = require('../../middleware/db')
5 |
6 | /**
7 | * Delete item function called by route
8 | * @param {Object} req - request object
9 | * @param {Object} res - response object
10 | */
11 | const deleteCity = async (req, res) => {
12 | try {
13 | req = matchedData(req)
14 | const id = await isIDGood(req.id)
15 | res.status(200).json(await deleteItem(id, City))
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { deleteCity }
22 |
--------------------------------------------------------------------------------
/app/middleware/emailer/sendChangeWalletEmailMessage.js:
--------------------------------------------------------------------------------
1 | const i18n = require('i18n')
2 | const { prepareToSendEmail } = require('./prepareToSendEmail')
3 |
4 | /**
5 | * Sends reset password email
6 | * @param {string} locale - locale
7 | * @param {Object} user - user object
8 | */
9 | const sendChangeWalletEmailMessage = (locale = '', user = {}) => {
10 | i18n.setLocale(locale)
11 | const subject = i18n.__('changeWallet.SUBJECT')
12 | const htmlMessage = i18n.__(
13 | 'changeWallet.MESSAGE',
14 | user.email,
15 | process.env.FRONTEND_URL,
16 | user.verification
17 | )
18 | prepareToSendEmail(user, subject, htmlMessage)
19 | }
20 |
21 | module.exports = { sendChangeWalletEmailMessage }
22 |
--------------------------------------------------------------------------------
/app/controllers/users/deleteUser.js:
--------------------------------------------------------------------------------
1 | const model = require('../../models/user')
2 | const { matchedData } = require('express-validator')
3 | const { isIDGood, handleError } = require('../../middleware/utils')
4 | const { deleteItem } = require('../../middleware/db')
5 |
6 | /**
7 | * Delete item function called by route
8 | * @param {Object} req - request object
9 | * @param {Object} res - response object
10 | */
11 | const deleteUser = async (req, res) => {
12 | try {
13 | req = matchedData(req)
14 | const id = await isIDGood(req.id)
15 | res.status(200).json(await deleteItem(id, model))
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { deleteUser }
22 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/setUserInfo.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates an object with user info
3 | * @param {Object} req - request object
4 | */
5 | const setUserInfo = (req = {}) => {
6 | return new Promise((resolve) => {
7 | let user = {
8 | _id: req._id,
9 | name: req.name,
10 | email: req.email,
11 | role: req.role,
12 | verified: req.verified,
13 | walletAddress: req.walletAddress
14 | }
15 | // Adds verification for testing purposes
16 | if (process.env.NODE_ENV !== 'production') {
17 | user = {
18 | ...user,
19 | verification: req.verification
20 | }
21 | }
22 | resolve(user)
23 | })
24 | }
25 |
26 | module.exports = { setUserInfo }
27 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/findUserToResetWallet.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Finds user by email to reset password
6 | * @param {string} email - user email
7 | */
8 | const findUserToResetWallet = (email = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findOne(
11 | {
12 | email
13 | },
14 | async (err, user) => {
15 | try {
16 | await itemNotFound(err, user, 'NOT_FOUND')
17 | resolve(user)
18 | } catch (error) {
19 | reject(error)
20 | }
21 | }
22 | )
23 | })
24 | }
25 |
26 | module.exports = { findUserToResetWallet }
27 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/returnRegisterToken.js:
--------------------------------------------------------------------------------
1 | const { generateAccessToken } = require('./generateAccessToken')
2 |
3 | /**
4 | * Builds the registration token
5 | * @param {Object} item - user object that contains created id
6 | * @param {Object} userInfo - user object
7 | */
8 | const returnRegisterToken = (
9 | { _id = '', verification = '' },
10 | userInfo = {}
11 | ) => {
12 | return new Promise((resolve) => {
13 | if (process.env.NODE_ENV !== 'production') {
14 | userInfo.verification = verification
15 | }
16 | const data = {
17 | accessToken: generateAccessToken(_id),
18 | user: userInfo
19 | }
20 | resolve(data)
21 | })
22 | }
23 |
24 | module.exports = { returnRegisterToken }
25 |
--------------------------------------------------------------------------------
/app/middleware/auth/encrypt.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto')
2 |
3 | const secret = process.env.JWT_SECRET
4 | const algorithm = 'aes-256-cbc'
5 | // Key length is dependent on the algorithm. In this case for aes256, it is
6 | // 32 bytes (256 bits).
7 | const key = crypto.scryptSync(secret, 'salt', 32)
8 | const iv = Buffer.alloc(16, 0) // Initialization crypto vector
9 |
10 | /**
11 | * Encrypts text
12 | * @param {string} text - text to encrypt
13 | */
14 | const encrypt = (text = '') => {
15 | const cipher = crypto.createCipheriv(algorithm, key, iv)
16 |
17 | let encrypted = cipher.update(text, 'utf8', 'hex')
18 | encrypted += cipher.final('hex')
19 |
20 | return encrypted
21 | }
22 |
23 | module.exports = { encrypt }
24 |
--------------------------------------------------------------------------------
/app/middleware/utils/validateResult.js:
--------------------------------------------------------------------------------
1 | const { validationResult } = require('express-validator')
2 | const { handleError } = require('./handleError')
3 | const { buildErrObject } = require('./buildErrObject')
4 |
5 | /**
6 | * Builds error for validation files
7 | * @param {Object} req - request object
8 | * @param {Object} res - response object
9 | * @param {Object} next - next object
10 | */
11 | const validateResult = (req, res, next) => {
12 | try {
13 | validationResult(req).throw()
14 | if (req.body.email) {
15 | req.body.email = req.body.email.toLowerCase()
16 | }
17 | return next()
18 | } catch (err) {
19 | return handleError(res, buildErrObject(422, err.array()))
20 | }
21 | }
22 |
23 | module.exports = { validateResult }
24 |
--------------------------------------------------------------------------------
/app/controllers/auth/login.js:
--------------------------------------------------------------------------------
1 | const { saveUserAccessAndReturnToken } = require('./helpers')
2 |
3 | const { handleError } = require('../../middleware/utils')
4 | const { findUserByWalleAddress } = require('./helpers/findUserByWalletAddress')
5 |
6 | /**
7 | * Login function called by route
8 | * @param {import('express').Request} req - request object
9 | * @param {import('express').Response} res - response object
10 | */
11 | const login = async (req, res) => {
12 | try {
13 | const walletAddress = req.user.id
14 | const user = await findUserByWalleAddress(walletAddress)
15 | res.status(200).json(await saveUserAccessAndReturnToken(req, res, user))
16 | } catch (error) {
17 | handleError(res, error)
18 | }
19 | }
20 |
21 | module.exports = { login }
22 |
--------------------------------------------------------------------------------
/app/controllers/cities/helpers/cityExists.js:
--------------------------------------------------------------------------------
1 | const City = require('../../../models/city')
2 | const { buildErrObject } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Checks if a city already exists in database
6 | * @param {string} name - name of item
7 | */
8 | const cityExists = (name = '') => {
9 | return new Promise((resolve, reject) => {
10 | City.findOne(
11 | {
12 | name
13 | },
14 | (err, item) => {
15 | if (err) {
16 | return reject(buildErrObject(422, err.message))
17 | }
18 |
19 | if (item) {
20 | return reject(buildErrObject(422, 'CITY_ALREADY_EXISTS'))
21 | }
22 | resolve(false)
23 | }
24 | )
25 | })
26 | }
27 |
28 | module.exports = { cityExists }
29 |
--------------------------------------------------------------------------------
/app/middleware/db/updateItem.js:
--------------------------------------------------------------------------------
1 | const { itemNotFound } = require('../../middleware/utils')
2 |
3 | /**
4 | * Updates an item in database by id
5 | * @param {string} id - item id
6 | * @param {Object} req - request object
7 | */
8 | const updateItem = (id = '', model = {}, req = {}) => {
9 | return new Promise((resolve, reject) => {
10 | model.findByIdAndUpdate(
11 | id,
12 | req,
13 | {
14 | new: true,
15 | runValidators: true
16 | },
17 | async (err, item) => {
18 | try {
19 | await itemNotFound(err, item, 'NOT_FOUND')
20 | resolve(item)
21 | } catch (error) {
22 | reject(error)
23 | }
24 | }
25 | )
26 | })
27 | }
28 |
29 | module.exports = { updateItem }
30 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/registerUser.js:
--------------------------------------------------------------------------------
1 | const uuid = require('uuid')
2 | const User = require('../../../models/user')
3 | const { buildErrObject } = require('../../../middleware/utils')
4 |
5 | /**
6 | * Registers a new user in database
7 | * @param {Object} req - request object
8 | */
9 | const registerUser = (req = {}) => {
10 | return new Promise((resolve, reject) => {
11 | const user = new User({
12 | name: req.name,
13 | email: req.email,
14 | walletAddress: req.walletAddress,
15 | verification: uuid.v4()
16 | })
17 | user.save((err, item) => {
18 | if (err) {
19 | reject(buildErrObject(422, err.message))
20 | }
21 | resolve(item)
22 | })
23 | })
24 | }
25 |
26 | module.exports = { registerUser }
27 |
--------------------------------------------------------------------------------
/app/middleware/emailer/emailExists.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models/user')
2 | const { buildErrObject } = require('../../middleware/utils')
3 |
4 | /**
5 | * Checks User model if user with an specific email exists
6 | * @param {string} email - user email
7 | */
8 | const emailExists = (email = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findOne(
11 | {
12 | email
13 | },
14 | (err, item) => {
15 | if (err) {
16 | return reject(buildErrObject(422, err.message))
17 | }
18 |
19 | if (item) {
20 | return reject(buildErrObject(422, 'EMAIL_ALREADY_EXISTS'))
21 | }
22 | resolve(false)
23 | }
24 | )
25 | })
26 | }
27 |
28 | module.exports = { emailExists }
29 |
--------------------------------------------------------------------------------
/app/middleware/db/getItems.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('../../middleware/utils')
2 |
3 | const { listInitOptions } = require('./listInitOptions')
4 | const { cleanPaginationID } = require('./cleanPaginationID')
5 |
6 | /**
7 | * Gets items from database
8 | * @param {Object} req - request object
9 | * @param {Object} query - query object
10 | */
11 | const getItems = async (req = {}, model = {}, query = {}) => {
12 | const options = await listInitOptions(req)
13 | return new Promise((resolve, reject) => {
14 | model.paginate(query, options, (err, items) => {
15 | if (err) {
16 | return reject(buildErrObject(422, err.message))
17 | }
18 | resolve(cleanPaginationID(items))
19 | })
20 | })
21 | }
22 |
23 | module.exports = { getItems }
24 |
--------------------------------------------------------------------------------
/app/models/userAccess.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const validator = require('validator')
3 |
4 | const UserAccessSchema = new mongoose.Schema(
5 | {
6 | email: {
7 | type: String,
8 | validate: {
9 | validator: validator.isEmail,
10 | message: 'EMAIL_IS_NOT_VALID'
11 | },
12 | lowercase: true,
13 | required: true
14 | },
15 | ip: {
16 | type: String,
17 | required: true
18 | },
19 | browser: {
20 | type: String,
21 | required: true
22 | },
23 | country: {
24 | type: String,
25 | required: true
26 | }
27 | },
28 | {
29 | versionKey: false,
30 | timestamps: true
31 | }
32 | )
33 | module.exports = mongoose.model('UserAccess', UserAccessSchema)
34 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/findUser.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Finds user by email
6 | * @param {string} email - user´s email
7 | */
8 | const findUser = (email = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findOne(
11 | {
12 | email
13 | },
14 | 'password loginAttempts blockExpires name email role verified verification walletAddress',
15 | async (err, item) => {
16 | try {
17 | await itemNotFound(err, item, 'USER_DOES_NOT_EXIST')
18 | resolve(item)
19 | } catch (error) {
20 | reject(error)
21 | }
22 | }
23 | )
24 | })
25 | }
26 |
27 | module.exports = { findUser }
28 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/verificationExists.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Checks if verification id exists for user
6 | * @param {string} id - verification id
7 | */
8 | const verificationExists = (id = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findOne(
11 | {
12 | verification: id,
13 | verified: false
14 | },
15 | async (err, user) => {
16 | try {
17 | await itemNotFound(err, user, 'NOT_FOUND_OR_ALREADY_VERIFIED')
18 | resolve(user)
19 | } catch (error) {
20 | reject(error)
21 | }
22 | }
23 | )
24 | })
25 | }
26 |
27 | module.exports = { verificationExists }
28 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/blockUser.js:
--------------------------------------------------------------------------------
1 | const { addHours } = require('date-fns')
2 | const HOURS_TO_BLOCK = 2
3 |
4 | const { buildErrObject } = require('../../../middleware/utils')
5 |
6 | /**
7 | * Blocks a user by setting blockExpires to the specified date based on constant HOURS_TO_BLOCK
8 | * @param {Object} user - user object
9 | */
10 | const blockUser = (user = {}) => {
11 | return new Promise((resolve, reject) => {
12 | user.blockExpires = addHours(new Date(), HOURS_TO_BLOCK)
13 | user.save((err, result) => {
14 | if (err) {
15 | return reject(buildErrObject(422, err.message))
16 | }
17 | if (result) {
18 | return resolve(buildErrObject(409, 'BLOCKED_USER'))
19 | }
20 | })
21 | })
22 | }
23 |
24 | module.exports = { blockUser }
25 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/findChangeWallet.js:
--------------------------------------------------------------------------------
1 | const ChangeWallet = require('../../../models/changeWallet')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Checks if a forgot password verification exists
6 | * @param {string} id - verification id
7 | */
8 | const findChangeWallet = (id = '') => {
9 | return new Promise((resolve, reject) => {
10 | ChangeWallet.findOne(
11 | {
12 | verification: id,
13 | used: false
14 | },
15 | async (err, item) => {
16 | try {
17 | await itemNotFound(err, item, 'NOT_FOUND_OR_ALREADY_USED')
18 | resolve(item)
19 | } catch (error) {
20 | reject(error)
21 | }
22 | }
23 | )
24 | })
25 | }
26 |
27 | module.exports = { findChangeWallet }
28 |
--------------------------------------------------------------------------------
/app/middleware/auth/decrypt.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto')
2 |
3 | const secret = process.env.JWT_SECRET
4 | const algorithm = 'aes-256-cbc'
5 | // Key length is dependent on the algorithm. In this case for aes256, it is
6 | // 32 bytes (256 bits).
7 | const key = crypto.scryptSync(secret, 'salt', 32)
8 | const iv = Buffer.alloc(16, 0) // Initialization crypto vector
9 |
10 | /**
11 | * Decrypts text
12 | * @param {string} text - text to decrypt
13 | */
14 | const decrypt = (text = '') => {
15 | const decipher = crypto.createDecipheriv(algorithm, key, iv)
16 |
17 | try {
18 | let decrypted = decipher.update(text, 'hex', 'utf8')
19 | decrypted += decipher.final('utf8')
20 | return decrypted
21 | } catch (err) {
22 | return err
23 | }
24 | }
25 |
26 | module.exports = { decrypt }
27 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Smartphone (please complete the following information):**
24 | - OS: [e.g. macOS High Sierra, Linux, Windows]
25 | - API client: [e.g. postman, cURL]
26 | - Version [e.g. 22]
27 |
28 | **Additional context**
29 | Add any other context about the problem here.
30 |
--------------------------------------------------------------------------------
/app/controllers/cities/createCity.js:
--------------------------------------------------------------------------------
1 | const City = require('../../models/city')
2 | const { createItem } = require('../../middleware/db')
3 | const { handleError } = require('../../middleware/utils')
4 | const { matchedData } = require('express-validator')
5 | const { cityExists } = require('./helpers')
6 |
7 | /**
8 | * Create item function called by route
9 | * @param {Object} req - request object
10 | * @param {Object} res - response object
11 | */
12 | const createCity = async (req, res) => {
13 | try {
14 | req = matchedData(req)
15 | const doesCityExists = await cityExists(req.name)
16 | if (!doesCityExists) {
17 | res.status(201).json(await createItem(req, City))
18 | }
19 | } catch (error) {
20 | handleError(res, error)
21 | }
22 | }
23 |
24 | module.exports = { createCity }
25 |
--------------------------------------------------------------------------------
/app/middleware/utils/index.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('./buildErrObject')
2 | const { buildSuccObject } = require('./buildSuccObject')
3 | const { getBrowserInfo } = require('./getBrowserInfo')
4 | const { getCountry } = require('./getCountry')
5 | const { getIP } = require('./getIP')
6 | const { handleError } = require('./handleError')
7 | const { isIDGood } = require('./isIDGood')
8 | const { itemNotFound } = require('./itemNotFound')
9 | const { removeExtensionFromFile } = require('./removeExtensionFromFile')
10 | const { validateResult } = require('./validateResult')
11 |
12 | module.exports = {
13 | buildErrObject,
14 | buildSuccObject,
15 | getBrowserInfo,
16 | getCountry,
17 | getIP,
18 | handleError,
19 | isIDGood,
20 | itemNotFound,
21 | removeExtensionFromFile,
22 | validateResult
23 | }
24 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/findUserByWalletAddress.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Finds user by ID
6 | * @param {string} id - user´s id
7 | */
8 | const findUserByWalleAddress = (walletAddress = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findOne(
11 | {
12 | walletAddress
13 | },
14 | 'loginAttempts blockExpires name email role verified verification walletAddress',
15 | async (err, item) => {
16 | try {
17 | await itemNotFound(err, item, 'USER_DOES_NOT_EXIST')
18 | resolve(item)
19 | } catch (error) {
20 | reject(error)
21 | }
22 | }
23 | )
24 | })
25 | }
26 |
27 | module.exports = { findUserByWalleAddress }
28 |
--------------------------------------------------------------------------------
/app/services/users/walletAddressExists.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models/user')
2 | const { buildErrObject } = require('../../middleware/utils')
3 |
4 | /**
5 | * Checks User model if user with an specific wallet address exists
6 | * @param {string} walletAddress - user wallet address
7 | */
8 | const walletAddressExists = (walletAddress = '') => {
9 | return new Promise((resolve, reject) => {
10 | User.findOne(
11 | {
12 | walletAddress
13 | },
14 | (err, item) => {
15 | if (err) {
16 | return reject(buildErrObject(422, err.message))
17 | }
18 |
19 | if (item) {
20 | return reject(buildErrObject(422, 'WALLET_ADDRESS_ALREADY_EXISTS'))
21 | }
22 | resolve(false)
23 | }
24 | )
25 | })
26 | }
27 |
28 | module.exports = { walletAddressExists }
29 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/generateAccessToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 | const { encrypt } = require('../../../middleware/auth')
3 |
4 | /**
5 | * Generates a token
6 | * @param {Object} user - user object
7 | */
8 | const generateAccessToken = (user = '') => {
9 | try {
10 | // Gets expiration time
11 | const expiration =
12 | Math.floor(Date.now() / 1000) +
13 | Number(process.env.JWT_ACCESS_TOKEN_EXPIRATION_IN_SECONDS)
14 |
15 | // returns signed and encrypted token
16 | return encrypt(
17 | jwt.sign(
18 | {
19 | data: {
20 | _id: user
21 | },
22 | exp: expiration
23 | },
24 | process.env.JWT_SECRET
25 | )
26 | )
27 | } catch (error) {
28 | throw error
29 | }
30 | }
31 |
32 | module.exports = { generateAccessToken }
33 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/generateRefreshToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 | const { encrypt } = require('../../../middleware/auth')
3 |
4 | /**
5 | * Generates a token
6 | * @param {Object} user - user object
7 | */
8 | const generateRefreshToken = (user = '') => {
9 | try {
10 | // Gets expiration time
11 | const expiration =
12 | Math.floor(Date.now() / 1000) +
13 | Number(process.env.JWT_REFRESH_TOKEN_EXPIRATION_IN_SECONDS)
14 |
15 | // returns signed and encrypted token
16 | return encrypt(
17 | jwt.sign(
18 | {
19 | data: {
20 | _id: user
21 | },
22 | exp: expiration
23 | },
24 | process.env.JWT_SECRET
25 | )
26 | )
27 | } catch (error) {
28 | throw error
29 | }
30 | }
31 |
32 | module.exports = { generateRefreshToken }
33 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/checkPermissions.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound, buildErrObject } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Checks against user if has quested role
6 | * @param {Object} data - data object
7 | * @param {*} next - next callback
8 | */
9 | const checkPermissions = ({ id = '', roles = [] }, next) => {
10 | return new Promise((resolve, reject) => {
11 | User.findById(id, async (err, result) => {
12 | try {
13 | await itemNotFound(err, result, 'USER_NOT_FOUND')
14 | if (roles.indexOf(result.role) > -1) {
15 | return resolve(next())
16 | }
17 | reject(buildErrObject(401, 'UNAUTHORIZED'))
18 | } catch (error) {
19 | reject(error)
20 | }
21 | })
22 | })
23 | }
24 |
25 | module.exports = { checkPermissions }
26 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/getUserIdFromToken.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken')
2 | const { buildErrObject } = require('../../../middleware/utils')
3 | const { decrypt } = require('../../../middleware/auth')
4 |
5 | /**
6 | * Gets user id from token
7 | * @param {string} token - Encrypted and encoded token
8 | */
9 | const getUserIdFromToken = (token = '') => {
10 | return new Promise((resolve, reject) => {
11 | // Decrypts, verifies and decode token
12 | jwt.verify(decrypt(token), process.env.JWT_SECRET, (err, decoded) => {
13 | if (err) {
14 | if (err.name === 'TokenExpiredError') {
15 | reject(buildErrObject(401, 'EXPIRED_TOKEN'))
16 | } else {
17 | reject(buildErrObject(401, 'INVALID_TOKEN'))
18 | }
19 | }
20 | resolve(decoded.data._id)
21 | })
22 | })
23 | }
24 |
25 | module.exports = { getUserIdFromToken }
26 |
--------------------------------------------------------------------------------
/app/controllers/profile/helpers/updateProfileInDB.js:
--------------------------------------------------------------------------------
1 | const User = require('../../../models/user')
2 | const { itemNotFound } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Updates profile in database
6 | * @param {Object} req - request object
7 | * @param {string} id - user id
8 | */
9 | const updateProfileInDB = (req = {}, id = '') => {
10 | return new Promise((resolve, reject) => {
11 | User.findByIdAndUpdate(
12 | id,
13 | req,
14 | {
15 | new: true,
16 | runValidators: true,
17 | select: '-role -_id -updatedAt -createdAt'
18 | },
19 | async (err, user) => {
20 | try {
21 | await itemNotFound(err, user, 'NOT_FOUND')
22 | resolve(user)
23 | } catch (error) {
24 | reject(error)
25 | }
26 | }
27 | )
28 | })
29 | }
30 |
31 | module.exports = { updateProfileInDB }
32 |
--------------------------------------------------------------------------------
/app/controllers/cities/updateCity.js:
--------------------------------------------------------------------------------
1 | const City = require('../../models/city')
2 | const { updateItem } = require('../../middleware/db')
3 | const { isIDGood, handleError } = require('../../middleware/utils')
4 | const { matchedData } = require('express-validator')
5 | const { cityExistsExcludingItself } = require('./helpers')
6 |
7 | /**
8 | * Update item function called by route
9 | * @param {Object} req - request object
10 | * @param {Object} res - response object
11 | */
12 | const updateCity = async (req, res) => {
13 | try {
14 | req = matchedData(req)
15 | const id = await isIDGood(req.id)
16 | const doesCityExists = await cityExistsExcludingItself(id, req.name)
17 | if (!doesCityExists) {
18 | res.status(200).json(await updateItem(id, City, req))
19 | }
20 | } catch (error) {
21 | handleError(res, error)
22 | }
23 | }
24 |
25 | module.exports = { updateCity }
26 |
--------------------------------------------------------------------------------
/app/controllers/users/updateUser.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models/user')
2 | const { matchedData } = require('express-validator')
3 | const { isIDGood, handleError } = require('../../middleware/utils')
4 | const { updateItem } = require('../../middleware/db')
5 | const { emailExistsExcludingMyself } = require('../../middleware/emailer')
6 |
7 | /**
8 | * Update item function called by route
9 | * @param {Object} req - request object
10 | * @param {Object} res - response object
11 | */
12 | const updateUser = async (req, res) => {
13 | try {
14 | req = matchedData(req)
15 | const id = await isIDGood(req.id)
16 | const doesEmailExists = await emailExistsExcludingMyself(id, req.email)
17 | if (!doesEmailExists) {
18 | res.status(200).json(await updateItem(id, User, req))
19 | }
20 | } catch (error) {
21 | handleError(res, error)
22 | }
23 | }
24 |
25 | module.exports = { updateUser }
26 |
--------------------------------------------------------------------------------
/app/controllers/cities/helpers/cityExistsExcludingItself.js:
--------------------------------------------------------------------------------
1 | const City = require('../../../models/city')
2 | const { buildErrObject } = require('../../../middleware/utils')
3 |
4 | /**
5 | * Checks if a city already exists excluding itself
6 | * @param {string} id - id of item
7 | * @param {string} name - name of item
8 | */
9 | const cityExistsExcludingItself = (id = '', name = '') => {
10 | return new Promise((resolve, reject) => {
11 | City.findOne(
12 | {
13 | name,
14 | _id: {
15 | $ne: id
16 | }
17 | },
18 | (err, item) => {
19 | if (err) {
20 | return reject(buildErrObject(422, err.message))
21 | }
22 |
23 | if (item) {
24 | return reject(buildErrObject(422, 'CITY_ALREADY_EXISTS'))
25 | }
26 |
27 | resolve(false)
28 | }
29 | )
30 | })
31 | }
32 |
33 | module.exports = { cityExistsExcludingItself }
34 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/saveChangeWallet.js:
--------------------------------------------------------------------------------
1 | const uuid = require('uuid')
2 | const ChangeWallet = require('../../../models/changeWallet')
3 | const {
4 | getIP,
5 | getBrowserInfo,
6 | getCountry,
7 | buildErrObject
8 | } = require('../../../middleware/utils')
9 |
10 | /**
11 | * Creates a new password forgot
12 | * @param {Object} req - request object
13 | */
14 | const saveChangeWallet = (req = {}) => {
15 | return new Promise((resolve, reject) => {
16 | const forgot = new ChangeWallet({
17 | email: req.body.email,
18 | verification: uuid.v4(),
19 | ipRequest: getIP(req),
20 | browserRequest: getBrowserInfo(req),
21 | countryRequest: getCountry(req)
22 | })
23 | forgot.save((err, item) => {
24 | if (err) {
25 | return reject(buildErrObject(422, err.message))
26 | }
27 | resolve(item)
28 | })
29 | })
30 | }
31 |
32 | module.exports = { saveChangeWallet }
33 |
--------------------------------------------------------------------------------
/app/middleware/emailer/prepareToSendEmail.js:
--------------------------------------------------------------------------------
1 | const { sendEmail } = require('./sendEmail')
2 |
3 | /**
4 | * Prepares to send email
5 | * @param {string} user - user object
6 | * @param {string} subject - subject
7 | * @param {string} htmlMessage - html message
8 | */
9 | const prepareToSendEmail = (user = {}, subject = '', htmlMessage = '') => {
10 | user = {
11 | name: user.name,
12 | email: user.email,
13 | verification: user.verification
14 | }
15 | const data = {
16 | user,
17 | subject,
18 | htmlMessage
19 | }
20 | if (process.env.NODE_ENV === 'production') {
21 | sendEmail(data, (messageSent) =>
22 | messageSent
23 | ? console.log(`Email SENT to: ${user.email}`)
24 | : console.log(`Email FAILED to: ${user.email}`)
25 | )
26 | } else if (process.env.NODE_ENV === 'development') {
27 | console.log(data)
28 | }
29 | }
30 |
31 | module.exports = { prepareToSendEmail }
32 |
--------------------------------------------------------------------------------
/views/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | cardano-express-web3-skeleton
10 |
11 |
12 |
13 | API home
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/app/controllers/auth/resetWallet.js:
--------------------------------------------------------------------------------
1 | const { matchedData } = require('express-validator')
2 | const {
3 | findChangeWallet,
4 | findUserToResetWallet,
5 | updateWallet,
6 | markChangeWalletAsUsed
7 | } = require('./helpers')
8 | const { handleError } = require('../../middleware/utils')
9 |
10 | /**
11 | * Reset password function called by route
12 | * @param {Object} req - request object
13 | * @param {Object} res - response object
14 | */
15 | const resetWallet = async (req, res) => {
16 | try {
17 | const data = matchedData(req)
18 | const changeWallet = await findChangeWallet(data.id)
19 | const user = await findUserToResetWallet(changeWallet.email)
20 |
21 | await updateWallet(data.walletAddress, user)
22 | const result = await markChangeWalletAsUsed(req, changeWallet)
23 | res.status(200).json(result)
24 | } catch (error) {
25 | handleError(res, error)
26 | }
27 | }
28 |
29 | module.exports = { resetWallet }
30 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/markChangeWalletAsUsed.js:
--------------------------------------------------------------------------------
1 | const {
2 | getIP,
3 | getBrowserInfo,
4 | getCountry,
5 | itemNotFound,
6 | buildSuccObject
7 | } = require('../../../middleware/utils')
8 |
9 | /**
10 | * Marks a request to reset password as used
11 | * @param {Object} req - request object
12 | * @param {Object} forgot - forgot object
13 | */
14 | const markChangeWalletAsUsed = (req = {}, forgot = {}) => {
15 | return new Promise((resolve, reject) => {
16 | forgot.used = true
17 | forgot.ipChanged = getIP(req)
18 | forgot.browserChanged = getBrowserInfo(req)
19 | forgot.countryChanged = getCountry(req)
20 | forgot.save(async (err, item) => {
21 | try {
22 | await itemNotFound(err, item, 'NOT_FOUND')
23 | resolve(buildSuccObject('WALLET_CHANGED'))
24 | } catch (error) {
25 | reject(error)
26 | }
27 | })
28 | })
29 | }
30 |
31 | module.exports = { markChangeWalletAsUsed }
32 |
--------------------------------------------------------------------------------
/app/middleware/emailer/emailExistsExcludingMyself.js:
--------------------------------------------------------------------------------
1 | const User = require('../../models/user')
2 | const { buildErrObject } = require('../../middleware/utils')
3 |
4 | /**
5 | * Checks User model if user with an specific email exists but excluding user id
6 | * @param {string} id - user id
7 | * @param {string} email - user email
8 | */
9 | const emailExistsExcludingMyself = (id = '', email = '') => {
10 | return new Promise((resolve, reject) => {
11 | User.findOne(
12 | {
13 | email,
14 | _id: {
15 | $ne: id
16 | }
17 | },
18 | async (err, item) => {
19 | if (err) {
20 | return reject(buildErrObject(422, err.message))
21 | }
22 |
23 | if (item) {
24 | return reject(buildErrObject(422, 'EMAIL_ALREADY_EXISTS'))
25 | }
26 |
27 | resolve(false)
28 | }
29 | )
30 | })
31 | }
32 |
33 | module.exports = { emailExistsExcludingMyself }
34 |
--------------------------------------------------------------------------------
/app/controllers/auth/validators/validateResetWallet.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates reset password request
6 | */
7 | const validateResetWallet = [
8 | check('id')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | check('walletAddress')
15 | .exists()
16 | .withMessage('MISSING')
17 | .not()
18 | .isEmpty()
19 | .withMessage('IS_EMPTY'),
20 | check('key')
21 | .exists()
22 | .withMessage('MISSING')
23 | .not()
24 | .isEmpty()
25 | .withMessage('IS_EMPTY'),
26 | check('signature')
27 | .exists()
28 | .withMessage('MISSING')
29 | .not()
30 | .isEmpty()
31 | .withMessage('IS_EMPTY'),
32 | (req, res, next) => {
33 | validateResult(req, res, next)
34 | }
35 | ]
36 |
37 | module.exports = { validateResetWallet }
38 |
--------------------------------------------------------------------------------
/app/controllers/auth/getRefreshToken.js:
--------------------------------------------------------------------------------
1 | const {
2 | getUserIdFromToken,
3 | findUserById,
4 | generateAccessToken,
5 | setUserInfo
6 | } = require('./helpers')
7 | const { isIDGood, handleError } = require('../../middleware/utils')
8 |
9 | /**
10 | * Refresh token function called by route
11 | * @param {import('express').Request} req - request object
12 | * @param {import('express').Response} res - response object
13 | */
14 | const getRefreshToken = async (req, res) => {
15 | try {
16 | const tokenEncrypted = req.cookies['jwt']
17 | let userId = await getUserIdFromToken(tokenEncrypted)
18 | userId = await isIDGood(userId)
19 | const user = await findUserById(userId)
20 | const userInfo = await setUserInfo(user)
21 | const token = { accessToken: generateAccessToken(user), user: userInfo }
22 | res.status(200).json(token)
23 | } catch (error) {
24 | handleError(res, error)
25 | }
26 | }
27 |
28 | module.exports = { getRefreshToken }
29 |
--------------------------------------------------------------------------------
/app/middleware/db/listInitOptions.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('../../middleware/utils')
2 | const { buildSort } = require('./buildSort')
3 |
4 | /**
5 | * Builds initial options for query
6 | * @param {Object} query - query object
7 | */
8 | const listInitOptions = (req = {}) => {
9 | return new Promise(async (resolve, reject) => {
10 | try {
11 | const order = req.query.order || -1
12 | const sort = req.query.sort || 'createdAt'
13 | const sortBy = buildSort(sort, order)
14 | const page = parseInt(req.query.page, 10) || 1
15 | const limit = parseInt(req.query.limit, 10) || 5
16 | const options = {
17 | sort: sortBy,
18 | lean: true,
19 | page,
20 | limit
21 | }
22 | resolve(options)
23 | } catch (error) {
24 | console.log(error.message)
25 | reject(buildErrObject(422, 'ERROR_WITH_INIT_OPTIONS'))
26 | }
27 | })
28 | }
29 |
30 | module.exports = { listInitOptions }
31 |
--------------------------------------------------------------------------------
/app/controllers/auth/changeWallet.js:
--------------------------------------------------------------------------------
1 | const { matchedData } = require('express-validator')
2 | const {
3 | findUser,
4 | changeWalletResponse,
5 | saveChangeWallet
6 | } = require('./helpers')
7 | const { handleError } = require('../../middleware/utils')
8 | const { sendChangeWalletEmailMessage } = require('../../middleware/emailer')
9 |
10 | /**
11 | * Change waller function called by route
12 | * @param {Object} req - request object
13 | * @param {Object} res - response object
14 | */
15 | const changeWallet = async (req, res) => {
16 | try {
17 | // Gets locale from header 'Accept-Language'
18 | const locale = req.getLocale()
19 | const data = matchedData(req)
20 | await findUser(data.email)
21 | const item = await saveChangeWallet(req)
22 | sendChangeWalletEmailMessage(locale, item)
23 | res.status(200).json(changeWalletResponse(item))
24 | } catch (error) {
25 | handleError(res, error)
26 | }
27 | }
28 |
29 | module.exports = { changeWallet }
30 |
--------------------------------------------------------------------------------
/app/middleware/utils/handleError.test.js:
--------------------------------------------------------------------------------
1 | const { handleError } = require('./handleError')
2 |
3 | const mockResponse = () => {
4 | const res = {}
5 | res.status = jest.fn().mockReturnValueOnce(res)
6 | res.json = jest.fn().mockReturnValueOnce(res)
7 | return res
8 | }
9 |
10 | const err = {
11 | message: 'error',
12 | code: 123
13 | }
14 |
15 | describe('handleError()', () => {
16 | it('should send the error object with the code and message provided and print the error code, message in development mode', async () => {
17 | process.env.NODE_ENV = 'development'
18 |
19 | const res = mockResponse()
20 |
21 | console.log = jest.fn()
22 |
23 | await handleError(res, err)
24 |
25 | expect(console.log).toHaveBeenCalledWith({
26 | code: 123,
27 | message: 'error'
28 | })
29 |
30 | expect(res.status).toHaveBeenCalledWith(123)
31 |
32 | expect(res.json).toHaveBeenCalledWith({
33 | errors: {
34 | msg: 'error'
35 | }
36 | })
37 | })
38 | })
39 |
--------------------------------------------------------------------------------
/app/routes/profile.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | require('../../config/passport')
4 | const passport = require('passport')
5 | const requireAuth = passport.authenticate('jwt', {
6 | session: false
7 | })
8 | const trimRequest = require('trim-request')
9 |
10 | const { roleAuthorization } = require('../controllers/auth')
11 |
12 | const { getProfile, updateProfile } = require('../controllers/profile')
13 |
14 | const { validateUpdateProfile } = require('../controllers/profile/validators')
15 |
16 | /*
17 | * Profile routes
18 | */
19 |
20 | /*
21 | * Get profile route
22 | */
23 | router.get(
24 | '/',
25 | requireAuth,
26 | roleAuthorization(['user', 'admin']),
27 | trimRequest.all,
28 | getProfile
29 | )
30 |
31 | /*
32 | * Update profile route
33 | */
34 | router.patch(
35 | '/',
36 | requireAuth,
37 | roleAuthorization(['user', 'admin']),
38 | trimRequest.all,
39 | validateUpdateProfile,
40 | updateProfile
41 | )
42 |
43 | module.exports = router
44 |
--------------------------------------------------------------------------------
/app/models/changeWallet.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const validator = require('validator')
3 |
4 | const ChangeWalletSchema = new mongoose.Schema(
5 | {
6 | email: {
7 | type: String,
8 | validate: {
9 | validator: validator.isEmail,
10 | message: 'EMAIL_IS_NOT_VALID'
11 | },
12 | lowercase: true,
13 | required: true
14 | },
15 | verification: {
16 | type: String
17 | },
18 | used: {
19 | type: Boolean,
20 | default: false
21 | },
22 | ipRequest: {
23 | type: String
24 | },
25 | browserRequest: {
26 | type: String
27 | },
28 | countryRequest: {
29 | type: String
30 | },
31 | ipChanged: {
32 | type: String
33 | },
34 | browserChanged: {
35 | type: String
36 | },
37 | countryChanged: {
38 | type: String
39 | }
40 | },
41 | {
42 | versionKey: false,
43 | timestamps: true
44 | }
45 | )
46 | module.exports = mongoose.model('ChangeWallet', ChangeWalletSchema)
47 |
--------------------------------------------------------------------------------
/app/middleware/emailer/sendEmail.js:
--------------------------------------------------------------------------------
1 | const nodemailer = require('nodemailer')
2 | const mg = require('nodemailer-mailgun-transport')
3 |
4 | /**
5 | * Sends email
6 | * @param {Object} data - data
7 | * @param {boolean} callback - callback
8 | */
9 | const sendEmail = async (data = {}, callback) => {
10 | const auth = {
11 | auth: {
12 | // eslint-disable-next-line camelcase
13 | api_key: process.env.EMAIL_SMTP_API_MAILGUN,
14 | domain: process.env.EMAIL_SMTP_DOMAIN_MAILGUN
15 | }
16 | // host: 'api.eu.mailgun.net' // THIS IS NEEDED WHEN USING EUROPEAN SERVERS
17 | }
18 | const transporter = nodemailer.createTransport(mg(auth))
19 | const mailOptions = {
20 | from: `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
21 | to: `${data.user.name} <${data.user.email}>`,
22 | subject: data.subject,
23 | html: data.htmlMessage
24 | }
25 | transporter.sendMail(mailOptions, (err) => {
26 | if (err) {
27 | return callback(false)
28 | }
29 | return callback(true)
30 | })
31 | }
32 |
33 | module.exports = { sendEmail }
34 |
--------------------------------------------------------------------------------
/test/helpers/auth/createCOSESign1Signature.js:
--------------------------------------------------------------------------------
1 | const MSG = require('@emurgo/cardano-message-signing-nodejs')
2 |
3 | /**
4 | *
5 | * @param {Object} payload
6 | * @param {CSL.RewardAddress} address
7 | * @param {CSL.PrivateKey} privateKey
8 | * @returns
9 | */
10 | const createCOSESign1Signature = (payload, address, privateKey) => {
11 | const protectedHeaders = MSG.HeaderMap.new()
12 | protectedHeaders.set_header(
13 | MSG.Label.new_text('address'),
14 | MSG.CBORValue.new_bytes(address.to_address().to_bytes())
15 | )
16 | const protectedHeadersSerialized =
17 | MSG.ProtectedHeaderMap.new(protectedHeaders)
18 | const headers = MSG.Headers.new(
19 | protectedHeadersSerialized,
20 | MSG.HeaderMap.new()
21 | )
22 | const builder = MSG.COSESign1Builder.new(
23 | headers,
24 | Buffer.from(JSON.stringify(payload)),
25 | false
26 | )
27 | const toSign = builder.make_data_to_sign().to_bytes()
28 | const signedSignature = privateKey.sign(toSign).to_bytes()
29 |
30 | return builder.build(signedSignature)
31 | }
32 |
33 | module.exports = { createCOSESign1Signature }
34 |
--------------------------------------------------------------------------------
/app/services/crypto/getCoseSign1Bech32Address.js:
--------------------------------------------------------------------------------
1 | const CSL = require('@emurgo/cardano-serialization-lib-nodejs')
2 | const MSG = require('@emurgo/cardano-message-signing-nodejs')
3 | const { buildErrObject } = require('../../middleware/utils')
4 |
5 | /**
6 | * Get the bech32 address from a COSE_Sign1 signature
7 | * @param {String} signature - Hex string represeantation of a COSE_Sign1 signature
8 | */
9 | const getCoseSign1Bech32Address = (signature) => {
10 | return new Promise((resolve, reject) => {
11 | try {
12 | const coseSignature = MSG.COSESign1.from_bytes(
13 | Buffer.from(signature, 'hex')
14 | )
15 |
16 | const bAddress = coseSignature
17 | .headers()
18 | .protected()
19 | .deserialized_headers()
20 | .header(MSG.Label.new_text('address'))
21 | .as_bytes()
22 |
23 | const address = CSL.Address.from_bytes(bAddress)
24 |
25 | resolve(address.to_bech32())
26 | } catch (err) {
27 | return reject(buildErrObject(422, err.message))
28 | }
29 | })
30 | }
31 |
32 | module.exports = { getCoseSign1Bech32Address }
33 |
--------------------------------------------------------------------------------
/app/controllers/users/helpers/createItemInDb.js:
--------------------------------------------------------------------------------
1 | const uuid = require('uuid')
2 | const User = require('../../../models/user')
3 | const { buildErrObject } = require('../../../middleware/utils')
4 |
5 | /**
6 | * Creates a new item in database
7 | * @param {Object} req - request object
8 | */
9 | const createItemInDb = ({
10 | name = '',
11 | email = '',
12 | walletAddress = '',
13 | role = '',
14 | phone = '',
15 | city = '',
16 | country = ''
17 | }) => {
18 | return new Promise((resolve, reject) => {
19 | const user = new User({
20 | name,
21 | email,
22 | walletAddress,
23 | role,
24 | phone,
25 | city,
26 | country,
27 | verification: uuid.v4()
28 | })
29 | user.save((err, item) => {
30 | if (err) {
31 | reject(buildErrObject(422, err.message))
32 | }
33 |
34 | item = JSON.parse(JSON.stringify(item))
35 |
36 | delete item.password
37 | delete item.blockExpires
38 | delete item.loginAttempts
39 |
40 | resolve(item)
41 | })
42 | })
43 | }
44 |
45 | module.exports = { createItemInDb }
46 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-test.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Build and test
5 |
6 | on:
7 | push:
8 | branches: [ "master" ]
9 | pull_request:
10 | branches: [ "master" ]
11 |
12 | jobs:
13 | build:
14 |
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | matrix:
19 | node-version: [14.x, 16.x, 18.x]
20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
21 |
22 | steps:
23 | - uses: actions/checkout@v3
24 | - name: MongoDB in GitHub Actions
25 | uses: supercharge/mongodb-github-action@1.8.0
26 | - name: Use Node.js ${{ matrix.node-version }}
27 | uses: actions/setup-node@v3
28 | with:
29 | node-version: ${{ matrix.node-version }}
30 | cache: 'npm'
31 | - run: cp .env.example .env
32 | - run: npm install
33 | - run: npm test
34 |
--------------------------------------------------------------------------------
/clean.js:
--------------------------------------------------------------------------------
1 | require('dotenv-safe').config()
2 | const initMongo = require('./config/mongo')
3 | const fs = require('fs')
4 | const modelsPath = `./app/models`
5 | const { removeExtensionFromFile } = require('./app/middleware/utils')
6 |
7 | initMongo()
8 |
9 | // Loop models path and loads every file as a model except index file
10 | const models = fs.readdirSync(modelsPath).filter((file) => {
11 | return removeExtensionFromFile(file) !== 'index'
12 | })
13 |
14 | const deleteModelFromDB = (model) => {
15 | return new Promise((resolve, reject) => {
16 | model = require(`./app/models/${model}`)
17 | model.deleteMany({}, (err, row) => {
18 | if (err) {
19 | reject(err)
20 | } else {
21 | resolve(row)
22 | }
23 | })
24 | })
25 | }
26 |
27 | const clean = async () => {
28 | try {
29 | const promiseArray = models.map(
30 | async (model) => await deleteModelFromDB(model)
31 | )
32 | await Promise.all(promiseArray)
33 | console.log('Cleanup complete!')
34 | process.exit(0)
35 | } catch (err) {
36 | console.log(err)
37 | process.exit(0)
38 | }
39 | }
40 |
41 | clean()
42 |
--------------------------------------------------------------------------------
/app/controllers/auth/validators/validateRegister.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const { check } = require('express-validator')
3 |
4 | /**
5 | * Validates register request
6 | */
7 | const validateRegister = [
8 | check('name')
9 | .exists()
10 | .withMessage('MISSING')
11 | .not()
12 | .isEmpty()
13 | .withMessage('IS_EMPTY'),
14 | check('email')
15 | .exists()
16 | .withMessage('MISSING')
17 | .not()
18 | .isEmpty()
19 | .withMessage('IS_EMPTY')
20 | .isEmail()
21 | .withMessage('EMAIL_IS_NOT_VALID'),
22 | check('key')
23 | .exists()
24 | .withMessage('MISSING')
25 | .not()
26 | .isEmpty()
27 | .withMessage('IS_EMPTY'),
28 | check('signature')
29 | .exists()
30 | .withMessage('MISSING')
31 | .not()
32 | .isEmpty()
33 | .withMessage('IS_EMPTY'),
34 | check('walletAddress')
35 | .exists()
36 | .withMessage('MISSING')
37 | .not()
38 | .isEmpty()
39 | .withMessage('IS_EMPTY'),
40 | (req, res, next) => {
41 | validateResult(req, res, next)
42 | }
43 | ]
44 |
45 | module.exports = { validateRegister }
46 |
--------------------------------------------------------------------------------
/app/controllers/users/createUser.js:
--------------------------------------------------------------------------------
1 | const { matchedData } = require('express-validator')
2 | const { handleError } = require('../../middleware/utils')
3 | const {
4 | emailExists,
5 | sendRegistrationEmailMessage
6 | } = require('../../middleware/emailer')
7 | const { createItemInDb } = require('./helpers')
8 | const { walletAddressExists } = require('../../services/users')
9 |
10 | /**
11 | * Create item function called by route
12 | * @param {Object} req - request object
13 | * @param {Object} res - response object
14 | */
15 | const createUser = async (req, res) => {
16 | try {
17 | // Gets locale from header 'Accept-Language'
18 | const locale = req.getLocale()
19 | req = matchedData(req)
20 | const doesEmailOrWalletAddressExists =
21 | (await emailExists(req.email)) ||
22 | (await walletAddressExists(req.walletAddress))
23 | if (!doesEmailOrWalletAddressExists) {
24 | const item = await createItemInDb(req)
25 | sendRegistrationEmailMessage(locale, item)
26 | res.status(201).json(item)
27 | }
28 | } catch (error) {
29 | handleError(res, error)
30 | }
31 | }
32 |
33 | module.exports = { createUser }
34 |
--------------------------------------------------------------------------------
/app/routes/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const fs = require('fs')
4 | const routesPath = `${__dirname}/`
5 | const { removeExtensionFromFile } = require('../middleware/utils')
6 |
7 | /*
8 | * Load routes statically and/or dynamically
9 | */
10 |
11 | // Load Auth route
12 | router.use('/', require('./auth'))
13 |
14 | // Loop routes path and loads every file as a route except this file and Auth route
15 | fs.readdirSync(routesPath).filter((file) => {
16 | // Take filename and remove last part (extension)
17 | const routeFile = removeExtensionFromFile(file)
18 | // Prevents loading of this file and auth file
19 | return routeFile !== 'index' && routeFile !== 'auth' && file !== '.DS_Store'
20 | ? router.use(`/${routeFile}`, require(`./${routeFile}`))
21 | : ''
22 | })
23 |
24 | /*
25 | * Setup routes for index
26 | */
27 | router.get('/', (req, res) => {
28 | res.render('index')
29 | })
30 |
31 | /*
32 | * Handle 404 error
33 | */
34 | router.use('*', (req, res) => {
35 | res.status(404).json({
36 | errors: {
37 | msg: 'URL_NOT_FOUND'
38 | }
39 | })
40 | })
41 |
42 | module.exports = router
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Daniel Avellaneda
4 | Copyright (c) 2022 Juan Salvador Magán Valero
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/app/services/crypto/verifyCoseSign1Signature.js:
--------------------------------------------------------------------------------
1 | const CSL = require('@emurgo/cardano-serialization-lib-nodejs')
2 | const MSG = require('@emurgo/cardano-message-signing-nodejs')
3 | const { buildErrObject } = require('../../middleware/utils')
4 |
5 | const verifyCoseSign1Signature = (key, signature) => {
6 | return new Promise((resolve, reject) => {
7 | try {
8 | const coseSignature = MSG.COSESign1.from_bytes(
9 | Buffer.from(signature, 'hex')
10 | )
11 |
12 | const coseKey = MSG.COSEKey.from_bytes(Buffer.from(key, 'hex'))
13 |
14 | const bKey = coseKey
15 | .header(
16 | MSG.Label.new_int(MSG.Int.new_negative(MSG.BigNum.from_str('2')))
17 | )
18 | .as_bytes()
19 |
20 | const publicKey = CSL.PublicKey.from_bytes(bKey)
21 |
22 | const signedPayload = coseSignature.signed_data().to_bytes()
23 |
24 | const ed25519Signature = CSL.Ed25519Signature.from_bytes(
25 | coseSignature.signature()
26 | )
27 |
28 | resolve(publicKey.verify(signedPayload, ed25519Signature))
29 | } catch (err) {
30 | return reject(buildErrObject(422, err.message))
31 | }
32 | })
33 | }
34 |
35 | module.exports = { verifyCoseSign1Signature }
36 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Launch nodemon in debug",
6 | "type": "node",
7 | "request": "launch",
8 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/nodemon",
9 | "program": "${workspaceFolder}/server.js",
10 | "env": { "NODE_ENV": "development"},
11 | "restart": true,
12 | "protocol": "inspector",
13 | "console": "integratedTerminal",
14 | "internalConsoleOptions": "neverOpen",
15 | "port": 9230
16 | },
17 | {
18 | "name": "Launch mocha test in debug",
19 | "request": "launch",
20 | "runtimeArgs": ["run", "mocha", "${relativeFile}"],
21 | "runtimeExecutable": "npm",
22 | "skipFiles": ["/**"],
23 | "type": "node",
24 | "env": {
25 | "PORT": "3111"
26 | }
27 | },
28 | {
29 | "name": "Launch jest in debug",
30 | "type": "node",
31 | "request": "launch",
32 | "cwd": "${workspaceFolder}",
33 | "runtimeArgs": [
34 | "--inspect-brk",
35 | "node_modules/.bin/jest",
36 | "--runInBand",
37 | "--config=jest.config.js",
38 | "${file}"
39 | ],
40 | "port": 9231
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/test/helpers/auth/getUserLoginDetails.js:
--------------------------------------------------------------------------------
1 | const { createCOSEKey } = require('./createCOSEKey')
2 | const { createCOSESign1Signature } = require('./createCOSESign1Signature')
3 | const { createFakePrivateKey } = require('./createFakePrivateKey')
4 | const { createRewardAddress } = require('./createRewardAddress')
5 |
6 | /**
7 | *
8 | * @param {String} host
9 | * @returns
10 | */
11 | const getUserLoginDetails = (host) => {
12 | /**
13 | *
14 | * @param {CSL.RewardAddress} address
15 | * @param {CSL.PrivateKey} PrivateKey
16 | * @returns
17 | */
18 | const createLoginUserSignature = (address, privateKey) => {
19 | const payload = {
20 | host,
21 | action: 'Login',
22 | uri: host + '/login',
23 | timestamp: Date.now()
24 | }
25 | return createCOSESign1Signature(payload, address, privateKey)
26 | }
27 |
28 | const userPrivateKey = createFakePrivateKey(1)
29 | const userStakeAddress = createRewardAddress(userPrivateKey)
30 |
31 | return {
32 | key: Buffer.from(createCOSEKey(userPrivateKey).to_bytes()).toString('hex'),
33 | signature: Buffer.from(
34 | createLoginUserSignature(userStakeAddress, userPrivateKey).to_bytes()
35 | ).toString('hex')
36 | }
37 | }
38 |
39 | module.exports = { getUserLoginDetails }
40 |
--------------------------------------------------------------------------------
/test/helpers/auth/getAdminLoginDetails.js:
--------------------------------------------------------------------------------
1 | const { createCOSEKey } = require('./createCOSEKey')
2 | const { createCOSESign1Signature } = require('./createCOSESign1Signature')
3 | const { createFakePrivateKey } = require('./createFakePrivateKey')
4 | const { createRewardAddress } = require('./createRewardAddress')
5 |
6 | /**
7 | *
8 | * @param {String} host
9 | * @returns
10 | */
11 | const getAdminLoginDetails = (host) => {
12 | /**
13 | *
14 | * @param {CSL.RewardAddress} address
15 | * @param {CSL.PrivateKey} PrivateKey
16 | * @returns
17 | */
18 | const createLoginUserSignature = (address, privateKey) => {
19 | const payload = {
20 | host,
21 | action: 'Login',
22 | uri: host + '/login',
23 | timestamp: Date.now()
24 | }
25 | return createCOSESign1Signature(payload, address, privateKey)
26 | }
27 |
28 | const adminPrivateKey = createFakePrivateKey(0)
29 | const adminStakeAddress = createRewardAddress(adminPrivateKey)
30 |
31 | return {
32 | key: Buffer.from(createCOSEKey(adminPrivateKey).to_bytes()).toString('hex'),
33 | signature: Buffer.from(
34 | createLoginUserSignature(adminStakeAddress, adminPrivateKey).to_bytes()
35 | ).toString('hex')
36 | }
37 | }
38 |
39 | module.exports = { getAdminLoginDetails }
40 |
--------------------------------------------------------------------------------
/data/2.cities/city.js:
--------------------------------------------------------------------------------
1 | const faker = require('faker')
2 |
3 | const json = [
4 | {
5 | name: 'San Francisco',
6 | createdAt: faker.date.past(),
7 | updatedAt: faker.date.recent()
8 | },
9 | {
10 | name: 'New York',
11 | createdAt: faker.date.past(),
12 | updatedAt: faker.date.recent()
13 | },
14 | {
15 | name: 'Chicago',
16 | createdAt: faker.date.past(),
17 | updatedAt: faker.date.recent()
18 | },
19 | {
20 | name: 'Bogotá',
21 | createdAt: faker.date.past(),
22 | updatedAt: faker.date.recent()
23 | },
24 | {
25 | name: 'Bucaramanga',
26 | createdAt: faker.date.past(),
27 | updatedAt: faker.date.recent()
28 | },
29 | {
30 | name: 'Oakland',
31 | createdAt: faker.date.past(),
32 | updatedAt: faker.date.recent()
33 | },
34 | {
35 | name: 'San Leandro',
36 | createdAt: faker.date.past(),
37 | updatedAt: faker.date.recent()
38 | },
39 | {
40 | name: 'Medellín',
41 | createdAt: faker.date.past(),
42 | updatedAt: faker.date.recent()
43 | },
44 | {
45 | name: 'Cali',
46 | createdAt: faker.date.past(),
47 | updatedAt: faker.date.recent()
48 | },
49 | {
50 | name: 'Barranquilla',
51 | createdAt: faker.date.past(),
52 | updatedAt: faker.date.recent()
53 | }
54 | ]
55 |
56 | module.exports = json
57 |
--------------------------------------------------------------------------------
/app/controllers/profile/validators/validateUpdateProfile.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const validator = require('validator')
3 | const { check } = require('express-validator')
4 |
5 | /**
6 | * Validates update profile request
7 | */
8 | const validateUpdateProfile = [
9 | check('name')
10 | .exists()
11 | .withMessage('MISSING')
12 | .not()
13 | .isEmpty()
14 | .withMessage('IS_EMPTY'),
15 | check('phone')
16 | .exists()
17 | .withMessage('MISSING')
18 | .not()
19 | .isEmpty()
20 | .withMessage('IS_EMPTY')
21 | .trim(),
22 | check('city')
23 | .exists()
24 | .withMessage('MISSING')
25 | .not()
26 | .isEmpty()
27 | .withMessage('IS_EMPTY')
28 | .trim(),
29 | check('country')
30 | .exists()
31 | .withMessage('MISSING')
32 | .not()
33 | .isEmpty()
34 | .withMessage('IS_EMPTY')
35 | .trim(),
36 | check('urlTwitter')
37 | .optional()
38 | .custom((v) => (v === '' ? true : validator.isURL(v)))
39 | .withMessage('NOT_A_VALID_URL'),
40 | check('urlGitHub')
41 | .optional()
42 | .custom((v) => (v === '' ? true : validator.isURL(v)))
43 | .withMessage('NOT_A_VALID_URL'),
44 | (req, res, next) => {
45 | validateResult(req, res, next)
46 | }
47 | ]
48 |
49 | module.exports = { validateUpdateProfile }
50 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .vscode/settings.json
3 |
4 | # Logs
5 | logs
6 | *.log
7 | npm-debug.log*
8 | yarn-debug.log*
9 | yarn-error.log*
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Directory for instrumented libs generated by jscoverage/JSCover
18 | lib-cov
19 |
20 | # Coverage directory used by tools like istanbul
21 | coverage
22 |
23 | # nyc test coverage
24 | .nyc_output
25 |
26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
27 | .grunt
28 |
29 | # Bower dependency directory (https://bower.io/)
30 | bower_components
31 |
32 | # node-waf configuration
33 | .lock-wscript
34 |
35 | # Compiled binary addons (https://nodejs.org/api/addons.html)
36 | build/Release
37 |
38 | # Dependency directories
39 | node_modules/
40 | jspm_packages/
41 |
42 | # TypeScript v1 declaration files
43 | typings/
44 |
45 | # Optional npm cache directory
46 | .npm
47 |
48 | # Optional eslint cache
49 | .eslintcache
50 |
51 | # Optional REPL history
52 | .node_repl_history
53 |
54 | # Output of 'npm pack'
55 | *.tgz
56 |
57 | # Yarn Integrity file
58 | .yarn-integrity
59 |
60 | # dotenv environment variables file
61 | .env
62 |
63 | # next.js build output
64 | .next
65 |
66 | # don't ignore keep files
67 | !.gitkeep
68 |
69 | # ignore production configuration
70 | .env.production
71 |
72 | .history/
73 |
--------------------------------------------------------------------------------
/config/mongo.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const DB_URL = process.env.MONGO_URI
3 | const loadModels = require('../app/models')
4 |
5 | module.exports = () => {
6 | const connect = () => {
7 | mongoose.Promise = global.Promise
8 |
9 | mongoose.connect(
10 | DB_URL,
11 | {
12 | keepAlive: true,
13 | useNewUrlParser: true,
14 | useUnifiedTopology: true
15 | },
16 | (err) => {
17 | let dbStatus = ''
18 | if (err) {
19 | dbStatus = `* Error connecting to DB: ${err}\n****************************\n`
20 | }
21 | dbStatus = `* DB Connection: OK\n****************************\n`
22 | if (process.env.NODE_ENV !== 'test') {
23 | // Prints initialization
24 | console.log('****************************')
25 | console.log('* Starting Server')
26 | console.log(`* Port: ${process.env.PORT || 3000}`)
27 | console.log(`* NODE_ENV: ${process.env.NODE_ENV}`)
28 | console.log(`* Database: MongoDB`)
29 | console.log(dbStatus)
30 | }
31 | }
32 | )
33 | mongoose.set('useCreateIndex', true)
34 | mongoose.set('useFindAndModify', false)
35 | }
36 | connect()
37 |
38 | mongoose.connection.on('error', console.log)
39 | mongoose.connection.on('disconnected', connect)
40 |
41 | loadModels()
42 | }
43 |
--------------------------------------------------------------------------------
/app/middleware/db/checkQueryString.js:
--------------------------------------------------------------------------------
1 | const { buildErrObject } = require('../../middleware/utils')
2 |
3 | /**
4 | * Checks the query string for filtering records
5 | * query.filter should be the text to search (string)
6 | * query.fields should be the fields to search into (array)
7 | * @param {Object} query - query object
8 | */
9 | const checkQueryString = (query = {}) => {
10 | return new Promise((resolve, reject) => {
11 | try {
12 | if (
13 | typeof query.filter !== 'undefined' &&
14 | typeof query.fields !== 'undefined'
15 | ) {
16 | const data = {
17 | $and: []
18 | }
19 | const array = []
20 | // Takes fields param and builds an array by splitting with ','
21 | const arrayFields = query.fields.split(',')
22 | const arrayFilter = query.filter.split(',')
23 | // Adds SQL Like %word% with regex
24 | arrayFields.map((item, index) => {
25 | array.push({
26 | [item]: {
27 | $regex: new RegExp(arrayFilter[index], 'i')
28 | }
29 | })
30 | })
31 | // Puts array result in data
32 | data.$and = array
33 | resolve(data)
34 | } else {
35 | resolve({})
36 | }
37 | } catch (err) {
38 | console.log(err.message)
39 | reject(buildErrObject(422, 'ERROR_WITH_FILTER'))
40 | }
41 | })
42 | }
43 |
44 | module.exports = { checkQueryString }
45 |
--------------------------------------------------------------------------------
/app/controllers/auth/register.js:
--------------------------------------------------------------------------------
1 | const { matchedData } = require('express-validator')
2 |
3 | const { registerUser, setUserInfo, returnRegisterToken } = require('./helpers')
4 |
5 | const { handleError, buildErrObject } = require('../../middleware/utils')
6 | const { walletAddressExists } = require('../../services/users')
7 | const {
8 | emailExists,
9 | sendRegistrationEmailMessage
10 | } = require('../../middleware/emailer')
11 | /**
12 | * Register function called by route
13 | * @param {Object} req - request object
14 | * @param {Object} res - response object
15 | */
16 | const register = async (req, res) => {
17 | try {
18 | // Gets locale from header 'Accept-Language'
19 | const locale = req.getLocale()
20 | const payload = req.authInfo
21 | req = matchedData(req)
22 | const doesEmailOrWalletAddressExists =
23 | (await emailExists(req.email)) ||
24 | (await walletAddressExists(req.walletAddress))
25 |
26 | if (payload.email !== req.email || payload.name !== req.name) {
27 | throw buildErrObject(422, 'INVALID_PAYLOAD')
28 | }
29 |
30 | if (!doesEmailOrWalletAddressExists) {
31 | const item = await registerUser(req)
32 | const userInfo = await setUserInfo(item)
33 | const response = await returnRegisterToken(item, userInfo)
34 | sendRegistrationEmailMessage(locale, item)
35 | res.status(201).json(response)
36 | }
37 | } catch (error) {
38 | handleError(res, error)
39 | }
40 | }
41 |
42 | module.exports = { register }
43 |
--------------------------------------------------------------------------------
/app/services/crypto/verifyCoseSign1Address.js:
--------------------------------------------------------------------------------
1 | const CSL = require('@emurgo/cardano-serialization-lib-nodejs')
2 | const MSG = require('@emurgo/cardano-message-signing-nodejs')
3 | const { buildErrObject } = require('../../middleware/utils')
4 |
5 | const verifyCoseSign1Address = (key, signature, bech32Address) => {
6 | return new Promise((resolve, reject) => {
7 | try {
8 | const coseSignature = MSG.COSESign1.from_bytes(
9 | Buffer.from(signature, 'hex')
10 | )
11 |
12 | const coseKey = MSG.COSEKey.from_bytes(Buffer.from(key, 'hex'))
13 |
14 | const bKey = coseKey
15 | .header(
16 | MSG.Label.new_int(MSG.Int.new_negative(MSG.BigNum.from_str('2')))
17 | )
18 | .as_bytes()
19 |
20 | const publicKey = CSL.PublicKey.from_bytes(bKey)
21 |
22 | const bAddress = coseSignature
23 | .headers()
24 | .protected()
25 | .deserialized_headers()
26 | .header(MSG.Label.new_text('address'))
27 | .as_bytes()
28 |
29 | const address = CSL.RewardAddress.from_address(
30 | CSL.Address.from_bytes(bAddress)
31 | )
32 |
33 | const signatureKeyHash = address.payment_cred().to_keyhash().to_hex()
34 | const publicKeyHash = publicKey.hash().to_hex()
35 |
36 | resolve(
37 | signatureKeyHash === publicKeyHash &&
38 | address.to_address().to_bech32() === bech32Address
39 | )
40 | } catch (err) {
41 | return reject(buildErrObject(422, err.message))
42 | }
43 | })
44 | }
45 |
46 | module.exports = { verifyCoseSign1Address }
47 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/saveUserAccessAndReturnToken.js:
--------------------------------------------------------------------------------
1 | const UserAccess = require('../../../models/userAccess')
2 | const { setUserInfo } = require('./setUserInfo')
3 | const { generateAccessToken } = require('./generateAccessToken.js')
4 | const {
5 | getIP,
6 | getBrowserInfo,
7 | getCountry,
8 | buildErrObject
9 | } = require('../../../middleware/utils')
10 | const { generateRefreshToken } = require('./generateRefreshToken')
11 |
12 | /**
13 | * Saves a new user access and then returns token
14 | * @param {Object} req - request object
15 | * @param {import('express').Response} res - response object
16 | * @param {Object} user - user object
17 | */
18 | const saveUserAccessAndReturnToken = (req = {}, res = {}, user = {}) => {
19 | return new Promise((resolve, reject) => {
20 | const userAccess = new UserAccess({
21 | email: user.email,
22 | ip: getIP(req),
23 | browser: getBrowserInfo(req),
24 | country: getCountry(req)
25 | })
26 | userAccess.save(async (err) => {
27 | try {
28 | if (err) {
29 | return reject(buildErrObject(422, err.message))
30 | }
31 | const userInfo = await setUserInfo(user)
32 |
33 | res.cookie('jwt', generateRefreshToken(user._id), {
34 | httpOnly: true,
35 | maxAge: 24 * 60 * 60 * 1000
36 | })
37 | // Returns data with access token
38 | resolve({
39 | accessToken: generateAccessToken(user._id),
40 | user: userInfo
41 | })
42 | } catch (error) {
43 | reject(error)
44 | }
45 | })
46 | })
47 | }
48 |
49 | module.exports = { saveUserAccessAndReturnToken }
50 |
--------------------------------------------------------------------------------
/config/passport.js:
--------------------------------------------------------------------------------
1 | const passport = require('passport')
2 | const User = require('../app/models/user')
3 | const auth = require('../app/middleware/auth')
4 | const JwtStrategy = require('passport-jwt').Strategy
5 | const CardanoWeb3Strategy = require('passport-cardano-web3').Strategy
6 |
7 | /**
8 | * Extracts token from: header, body or query
9 | * @param {Object} req - request object
10 | * @returns {string} token - decrypted token
11 | */
12 | const jwtExtractor = (req) => {
13 | let token = null
14 | if (req.headers.authorization) {
15 | token = req.headers.authorization.replace('Bearer ', '').trim()
16 | } else if (req.body.token) {
17 | token = req.body.token.trim()
18 | } else if (req.query.token) {
19 | token = req.query.token.trim()
20 | }
21 | if (token) {
22 | // Decrypts token
23 | token = auth.decrypt(token)
24 | }
25 | return token
26 | }
27 |
28 | /**
29 | * Options object for jwt middlware
30 | */
31 | const jwtOptions = {
32 | jwtFromRequest: jwtExtractor,
33 | secretOrKey: process.env.JWT_SECRET
34 | }
35 |
36 | /**
37 | * Login with JWT middleware
38 | */
39 | const jwtLogin = new JwtStrategy(jwtOptions, (payload, done) => {
40 | User.findById(payload.data._id, (err, user) => {
41 | if (err) {
42 | return done(err, false)
43 | }
44 | return !user ? done(null, false) : done(null, user)
45 | })
46 | })
47 |
48 | const cardanoWeb3Strategy = new CardanoWeb3Strategy({
49 | expirationTimeSpan:
50 | process.env.NODE_ENV === 'development'
51 | ? Number.MAX_SAFE_INTEGER
52 | : process.env.PAYLOAD_VALIDITY_IN_SECONDS,
53 | hostname: process.env.HOST
54 | })
55 |
56 | passport.use(jwtLogin)
57 | passport.use(cardanoWeb3Strategy)
58 |
--------------------------------------------------------------------------------
/app/models/user.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 | const validator = require('validator')
3 | const mongoosePaginate = require('mongoose-paginate-v2')
4 |
5 | const UserSchema = new mongoose.Schema(
6 | {
7 | name: {
8 | type: String,
9 | required: true
10 | },
11 | email: {
12 | type: String,
13 | validate: {
14 | validator: validator.isEmail,
15 | message: 'EMAIL_IS_NOT_VALID'
16 | },
17 | lowercase: true,
18 | unique: true,
19 | required: true
20 | },
21 | walletAddress: {
22 | type: String,
23 | required: true
24 | },
25 | role: {
26 | type: String,
27 | enum: ['user', 'admin'],
28 | default: 'user'
29 | },
30 | verification: {
31 | type: String
32 | },
33 | verified: {
34 | type: Boolean,
35 | default: false
36 | },
37 | phone: {
38 | type: String
39 | },
40 | city: {
41 | type: String
42 | },
43 | country: {
44 | type: String
45 | },
46 | urlTwitter: {
47 | type: String,
48 | validate: {
49 | validator(v) {
50 | return v === '' ? true : validator.isURL(v)
51 | },
52 | message: 'NOT_A_VALID_URL'
53 | },
54 | lowercase: true
55 | },
56 | urlGitHub: {
57 | type: String,
58 | validate: {
59 | validator(v) {
60 | return v === '' ? true : validator.isURL(v)
61 | },
62 | message: 'NOT_A_VALID_URL'
63 | },
64 | lowercase: true
65 | }
66 | },
67 | {
68 | versionKey: false,
69 | timestamps: true
70 | }
71 | )
72 |
73 | UserSchema.plugin(mongoosePaginate)
74 | module.exports = mongoose.model('User', UserSchema)
75 |
--------------------------------------------------------------------------------
/app/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | const trimRequest = require('trim-request')
4 | const passport = require('passport')
5 |
6 | const {
7 | register,
8 | verify,
9 | changeWallet,
10 | resetWallet,
11 | getRefreshToken,
12 | login,
13 | logout
14 | } = require('../controllers/auth')
15 |
16 | const {
17 | validateRegister,
18 | validateVerify,
19 | validateResetWallet,
20 | validateLogin,
21 | validateChangeWallet
22 | } = require('../controllers/auth/validators')
23 |
24 | /*
25 | * Auth routes
26 | */
27 |
28 | /*
29 | * Register route
30 | */
31 | router.post(
32 | '/register',
33 | trimRequest.all,
34 | validateRegister,
35 | passport.authenticate('cardano-web3', { action: 'Sign up', session: false }),
36 | register
37 | )
38 |
39 | /*
40 | * Verify route
41 | */
42 | router.post('/verify', trimRequest.all, validateVerify, verify)
43 |
44 | /*
45 | * Forgot password route
46 | */
47 | router.post('/change', trimRequest.all, validateChangeWallet, changeWallet)
48 |
49 | /*
50 | * Reset password route
51 | */
52 | router.post(
53 | '/reset',
54 | trimRequest.all,
55 | validateResetWallet,
56 | passport.authenticate('cardano-web3', { action: 'Reset', session: false }),
57 | resetWallet
58 | )
59 |
60 | /*
61 | * Get new refresh token
62 | */
63 | router.get('/token', trimRequest.all, getRefreshToken)
64 |
65 | /*
66 | * Login route
67 | */
68 | router.post(
69 | '/login',
70 | trimRequest.all,
71 | validateLogin,
72 | passport.authenticate('cardano-web3', { action: 'Login', session: false }),
73 | login
74 | )
75 |
76 | /*
77 | * Logout route
78 | */
79 | router.get('/logout', trimRequest.all, logout)
80 |
81 | module.exports = router
82 |
--------------------------------------------------------------------------------
/app/controllers/users/validators/validateUpdateUser.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const validator = require('validator')
3 | const { check } = require('express-validator')
4 |
5 | /**
6 | * Validates update item request
7 | */
8 | const validateUpdateUser = [
9 | check('name')
10 | .exists()
11 | .withMessage('MISSING')
12 | .not()
13 | .isEmpty()
14 | .withMessage('IS_EMPTY'),
15 | check('email')
16 | .exists()
17 | .withMessage('MISSING')
18 | .not()
19 | .isEmpty()
20 | .withMessage('IS_EMPTY'),
21 | check('role')
22 | .exists()
23 | .withMessage('MISSING')
24 | .not()
25 | .isEmpty()
26 | .withMessage('IS_EMPTY'),
27 | check('phone')
28 | .exists()
29 | .withMessage('MISSING')
30 | .not()
31 | .isEmpty()
32 | .withMessage('IS_EMPTY')
33 | .trim(),
34 | check('city')
35 | .exists()
36 | .withMessage('MISSING')
37 | .not()
38 | .isEmpty()
39 | .withMessage('IS_EMPTY')
40 | .trim(),
41 | check('country')
42 | .exists()
43 | .withMessage('MISSING')
44 | .not()
45 | .isEmpty()
46 | .withMessage('IS_EMPTY')
47 | .trim(),
48 | check('urlTwitter')
49 | .optional()
50 | .custom((v) => (v === '' ? true : validator.isURL(v)))
51 | .withMessage('NOT_A_VALID_URL'),
52 | check('urlGitHub')
53 | .optional()
54 | .custom((v) => (v === '' ? true : validator.isURL(v)))
55 | .withMessage('NOT_A_VALID_URL'),
56 | check('id')
57 | .exists()
58 | .withMessage('MISSING')
59 | .not()
60 | .isEmpty()
61 | .withMessage('IS_EMPTY'),
62 | (req, res, next) => {
63 | validateResult(req, res, next)
64 | }
65 | ]
66 |
67 | module.exports = { validateUpdateUser }
68 |
--------------------------------------------------------------------------------
/app/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | require('../../config/passport')
4 | const passport = require('passport')
5 | const requireAuth = passport.authenticate('jwt', {
6 | session: false
7 | })
8 | const trimRequest = require('trim-request')
9 |
10 | const { roleAuthorization } = require('../controllers/auth')
11 |
12 | const {
13 | getUsers,
14 | createUser,
15 | getUser,
16 | updateUser,
17 | deleteUser
18 | } = require('../controllers/users')
19 |
20 | const {
21 | validateCreateUser,
22 | validateGetUser,
23 | validateUpdateUser,
24 | validateDeleteUser
25 | } = require('../controllers/users/validators')
26 |
27 | /*
28 | * Users routes
29 | */
30 |
31 | /*
32 | * Get items route
33 | */
34 | router.get(
35 | '/',
36 | requireAuth,
37 | roleAuthorization(['admin']),
38 | trimRequest.all,
39 | getUsers
40 | )
41 |
42 | /*
43 | * Create new item route
44 | */
45 | router.post(
46 | '/',
47 | requireAuth,
48 | roleAuthorization(['admin']),
49 | trimRequest.all,
50 | validateCreateUser,
51 | createUser
52 | )
53 |
54 | /*
55 | * Get item route
56 | */
57 | router.get(
58 | '/:id',
59 | requireAuth,
60 | roleAuthorization(['admin']),
61 | trimRequest.all,
62 | validateGetUser,
63 | getUser
64 | )
65 |
66 | /*
67 | * Update item route
68 | */
69 | router.patch(
70 | '/:id',
71 | requireAuth,
72 | roleAuthorization(['admin']),
73 | trimRequest.all,
74 | validateUpdateUser,
75 | updateUser
76 | )
77 |
78 | /*
79 | * Delete item route
80 | */
81 | router.delete(
82 | '/:id',
83 | requireAuth,
84 | roleAuthorization(['admin']),
85 | trimRequest.all,
86 | validateDeleteUser,
87 | deleteUser
88 | )
89 |
90 | module.exports = router
91 |
--------------------------------------------------------------------------------
/app/controllers/auth/helpers/index.js:
--------------------------------------------------------------------------------
1 | const { blockIsExpired } = require('./blockIsExpired')
2 | const { blockUser } = require('./blockUser')
3 | const { checkPermissions } = require('./checkPermissions')
4 | const { findChangeWallet } = require('./findChangeWallet')
5 | const { findUser } = require('./findUser')
6 | const { findUserById } = require('./findUserById')
7 | const { findUserToResetWallet } = require('./findUserToResetWallet')
8 | const { changeWalletResponse } = require('./changeWalletResponse')
9 | const { generateAccessToken } = require('./generateAccessToken')
10 | const { getUserIdFromToken } = require('./getUserIdFromToken')
11 | const { markChangeWalletAsUsed } = require('./markChangeWalletAsUsed')
12 | const { registerUser } = require('./registerUser')
13 | const { returnRegisterToken } = require('./returnRegisterToken')
14 | const { saveChangeWallet } = require('./saveChangeWallet')
15 | const {
16 | saveUserAccessAndReturnToken
17 | } = require('./saveUserAccessAndReturnToken')
18 | const { setUserInfo } = require('./setUserInfo')
19 | const { updateWallet } = require('./updateWallet')
20 | const { verificationExists } = require('./verificationExists')
21 | const { verifyUser } = require('./verifyUser')
22 | const { generateRefreshToken } = require('./generateRefreshToken')
23 |
24 | module.exports = {
25 | blockIsExpired,
26 | blockUser,
27 | checkPermissions,
28 | findChangeWallet,
29 | findUser,
30 | findUserById,
31 | findUserToResetWallet,
32 | changeWalletResponse,
33 | generateAccessToken,
34 | getUserIdFromToken,
35 | markChangeWalletAsUsed,
36 | registerUser,
37 | returnRegisterToken,
38 | saveChangeWallet,
39 | saveUserAccessAndReturnToken,
40 | setUserInfo,
41 | updateWallet,
42 | verificationExists,
43 | verifyUser,
44 | generateRefreshToken
45 | }
46 |
--------------------------------------------------------------------------------
/app/routes/cities.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const router = express.Router()
3 | require('../../config/passport')
4 | const passport = require('passport')
5 | const requireAuth = passport.authenticate('jwt', {
6 | session: false
7 | })
8 | const trimRequest = require('trim-request')
9 |
10 | const { roleAuthorization } = require('../controllers/auth')
11 |
12 | const {
13 | getAllCities,
14 | getCities,
15 | createCity,
16 | getCity,
17 | updateCity,
18 | deleteCity
19 | } = require('../controllers/cities')
20 |
21 | const {
22 | validateCreateCity,
23 | validateGetCity,
24 | validateUpdateCity,
25 | validateDeleteCity
26 | } = require('../controllers/cities/validators')
27 |
28 | /*
29 | * Cities routes
30 | */
31 |
32 | /*
33 | * Get all items route
34 | */
35 | router.get('/all', getAllCities)
36 |
37 | /*
38 | * Get items route
39 | */
40 | router.get(
41 | '/',
42 | requireAuth,
43 | roleAuthorization(['admin']),
44 | trimRequest.all,
45 | getCities
46 | )
47 |
48 | /*
49 | * Create new item route
50 | */
51 | router.post(
52 | '/',
53 | requireAuth,
54 | roleAuthorization(['admin']),
55 | trimRequest.all,
56 | validateCreateCity,
57 | createCity
58 | )
59 |
60 | /*
61 | * Get item route
62 | */
63 | router.get(
64 | '/:id',
65 | requireAuth,
66 | roleAuthorization(['admin']),
67 | trimRequest.all,
68 | validateGetCity,
69 | getCity
70 | )
71 |
72 | /*
73 | * Update item route
74 | */
75 | router.patch(
76 | '/:id',
77 | requireAuth,
78 | roleAuthorization(['admin']),
79 | trimRequest.all,
80 | validateUpdateCity,
81 | updateCity
82 | )
83 |
84 | /*
85 | * Delete item route
86 | */
87 | router.delete(
88 | '/:id',
89 | requireAuth,
90 | roleAuthorization(['admin']),
91 | trimRequest.all,
92 | validateDeleteCity,
93 | deleteCity
94 | )
95 |
96 | module.exports = router
97 |
--------------------------------------------------------------------------------
/data/1.users/user.js:
--------------------------------------------------------------------------------
1 | const faker = require('faker')
2 | const ObjectID = require('mongodb').ObjectID
3 | const CSL = require('@emurgo/cardano-serialization-lib-nodejs')
4 |
5 | const adminPrivateKey = CSL.PrivateKey.from_normal_bytes(new Array(32).fill(0))
6 | const adminWalletAddress = CSL.RewardAddress.new(
7 | CSL.NetworkId.mainnet().kind(),
8 | CSL.StakeCredential.from_keyhash(adminPrivateKey.to_public().hash())
9 | )
10 | .to_address()
11 | .to_bech32()
12 |
13 | const simpleUserPrivateKey = CSL.PrivateKey.from_normal_bytes(
14 | new Array(32).fill(1)
15 | )
16 | const simpleUserWalletAddress = CSL.RewardAddress.new(
17 | CSL.NetworkId.mainnet().kind(),
18 | CSL.StakeCredential.from_keyhash(simpleUserPrivateKey.to_public().hash())
19 | )
20 | .to_address()
21 | .to_bech32()
22 |
23 | module.exports = [
24 | {
25 | _id: new ObjectID('5aa1c2c35ef7a4e97b5e995a'),
26 | name: 'Super Administrator',
27 | email: 'admin@admin.com',
28 | role: 'admin',
29 | walletAddress: adminWalletAddress,
30 | verified: true,
31 | verification: '3d6e072c-0eaf-4239-bb5e-495e6486148f',
32 | city: 'Bucaramanga',
33 | country: 'Colombia',
34 | phone: '123123',
35 | urlTwitter: faker.internet.url(),
36 | urlGitHub: faker.internet.url(),
37 | createdAt: faker.date.past(),
38 | updatedAt: faker.date.recent()
39 | },
40 | {
41 | _id: new ObjectID('5aa1c2c35ef7a4e97b5e995b'),
42 | name: 'Simple user',
43 | email: 'user@user.com',
44 | walletAddress: simpleUserWalletAddress,
45 | role: 'user',
46 | verified: true,
47 | verification: '3d6e072c-0eaf-4239-bb5e-495e6486148d',
48 | city: 'Bucaramanga',
49 | country: 'Colombia',
50 | phone: '123123',
51 | urlTwitter: faker.internet.url(),
52 | urlGitHub: faker.internet.url(),
53 | createdAt: faker.date.past(),
54 | updatedAt: faker.date.recent()
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/app/controllers/users/validators/validateCreateUser.js:
--------------------------------------------------------------------------------
1 | const { validateResult } = require('../../../middleware/utils')
2 | const validator = require('validator')
3 | const { check } = require('express-validator')
4 |
5 | /**
6 | * Validates create new item request
7 | */
8 | const validateCreateUser = [
9 | check('name')
10 | .exists()
11 | .withMessage('MISSING')
12 | .not()
13 | .isEmpty()
14 | .withMessage('IS_EMPTY'),
15 | check('email')
16 | .exists()
17 | .withMessage('MISSING')
18 | .not()
19 | .isEmpty()
20 | .withMessage('IS_EMPTY')
21 | .isEmail()
22 | .withMessage('EMAIL_IS_NOT_VALID'),
23 | check('walletAddress')
24 | .exists()
25 | .withMessage('MISSING')
26 | .not()
27 | .isEmpty()
28 | .withMessage('IS_EMPTY'),
29 | check('role')
30 | .exists()
31 | .withMessage('MISSING')
32 | .not()
33 | .isEmpty()
34 | .withMessage('IS_EMPTY')
35 | .isIn(['user', 'admin'])
36 | .withMessage('USER_NOT_IN_KNOWN_ROLE'),
37 | check('phone')
38 | .exists()
39 | .withMessage('MISSING')
40 | .not()
41 | .isEmpty()
42 | .withMessage('IS_EMPTY')
43 | .trim(),
44 | check('city')
45 | .exists()
46 | .withMessage('MISSING')
47 | .not()
48 | .isEmpty()
49 | .withMessage('IS_EMPTY')
50 | .trim(),
51 | check('country')
52 | .exists()
53 | .withMessage('MISSING')
54 | .not()
55 | .isEmpty()
56 | .withMessage('IS_EMPTY')
57 | .trim(),
58 | check('urlTwitter')
59 | .optional()
60 | .custom((v) => (v === '' ? true : validator.isURL(v)))
61 | .withMessage('NOT_A_VALID_URL'),
62 | check('urlGitHub')
63 | .optional()
64 | .custom((v) => (v === '' ? true : validator.isURL(v)))
65 | .withMessage('NOT_A_VALID_URL'),
66 | (req, res, next) => {
67 | validateResult(req, res, next)
68 | }
69 | ]
70 |
71 | module.exports = { validateCreateUser }
72 |
--------------------------------------------------------------------------------
/app/services/crypto/verifyCoseSign1Signature.test.js:
--------------------------------------------------------------------------------
1 | const { createCOSEKey } = require('../../../test/helpers/auth/createCOSEKey.js')
2 | const {
3 | createCOSESign1Signature
4 | } = require('../../../test/helpers/auth/createCOSESign1Signature.js')
5 | const {
6 | createFakePrivateKey
7 | } = require('../../../test/helpers/auth/createFakePrivateKey.js')
8 | const {
9 | createRewardAddress
10 | } = require('../../../test/helpers/auth/createRewardAddress.js')
11 | const { verifyCoseSign1Signature } = require('./verifyCoseSign1Signature.js')
12 |
13 | const stakePrivateKey1 = createFakePrivateKey(11)
14 |
15 | const stakeAddress1 = createRewardAddress(stakePrivateKey1)
16 |
17 | const stakePrivateKey2 = createFakePrivateKey(12)
18 |
19 | const stakeAddress2 = createRewardAddress(stakePrivateKey2)
20 |
21 | describe('verifyCoseSign1Signature()', () => {
22 | it('Should verify a correct signature', async () => {
23 | const result = await verifyCoseSign1Signature(
24 | Buffer.from(createCOSEKey(stakePrivateKey1).to_bytes()).toString('hex'),
25 | Buffer.from(
26 | createCOSESign1Signature({}, stakeAddress1, stakePrivateKey1).to_bytes()
27 | ).toString('hex')
28 | )
29 | expect(result).toBe(true)
30 | })
31 |
32 | it('Should not verify a incorrect key', async () => {
33 | const result = await verifyCoseSign1Signature(
34 | Buffer.from(createCOSEKey(stakePrivateKey2).to_bytes()).toString('hex'),
35 | Buffer.from(
36 | createCOSESign1Signature({}, stakeAddress1, stakePrivateKey1).to_bytes()
37 | ).toString('hex')
38 | )
39 | expect(result).toBe(false)
40 | })
41 |
42 | it('Should not verify a incorrect tampered signature', async () => {
43 | const result = await verifyCoseSign1Signature(
44 | Buffer.from(createCOSEKey(stakePrivateKey1).to_bytes()).toString('hex'),
45 | Buffer.from(
46 | createCOSESign1Signature({}, stakeAddress2, stakePrivateKey2).to_bytes()
47 | ).toString('hex')
48 | )
49 | expect(result).toBe(false)
50 | })
51 | })
52 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv-safe').config()
2 | const express = require('express')
3 | const bodyParser = require('body-parser')
4 | const morgan = require('morgan')
5 | const compression = require('compression')
6 | const helmet = require('helmet')
7 | const cors = require('cors')
8 | const passport = require('passport')
9 | const app = express()
10 | const i18n = require('i18n')
11 | const initMongo = require('./config/mongo')
12 | const path = require('path')
13 | const cookieParser = require('cookie-parser')
14 |
15 | // Setup express server port from ENV, default: 3000
16 | app.set('port', process.env.PORT || 3000)
17 |
18 | // Enable only in development HTTP request logger middleware
19 | if (process.env.NODE_ENV === 'development') {
20 | app.use(morgan('dev'))
21 | }
22 |
23 | // Redis cache enabled by env variable
24 | if (process.env.USE_REDIS === 'true') {
25 | const getExpeditiousCache = require('express-expeditious')
26 | const cache = getExpeditiousCache({
27 | namespace: 'expresscache',
28 | defaultTtl: '1 minute',
29 | engine: require('expeditious-engine-redis')({
30 | redis: {
31 | host: process.env.REDIS_HOST,
32 | port: process.env.REDIS_PORT
33 | }
34 | })
35 | })
36 | app.use(cache)
37 | }
38 |
39 | // for parsing json
40 | app.use(
41 | bodyParser.json({
42 | limit: '20mb'
43 | })
44 | )
45 | // for parsing application/x-www-form-urlencoded
46 | app.use(
47 | bodyParser.urlencoded({
48 | limit: '20mb',
49 | extended: true
50 | })
51 | )
52 |
53 | // i18n
54 | i18n.configure({
55 | locales: ['en', 'es'],
56 | directory: `${__dirname}/locales`,
57 | defaultLocale: 'en',
58 | objectNotation: true
59 | })
60 | app.use(i18n.init)
61 |
62 | // Init all other stuff
63 | app.use(
64 | cors({
65 | origin: 'http://localhost:3001',
66 | credentials: true
67 | })
68 | )
69 | app.use(passport.initialize())
70 | app.use(compression())
71 | app.use(
72 | helmet({
73 | contentSecurityPolicy: false
74 | })
75 | )
76 | app.use(express.static('public'))
77 | app.use(cookieParser())
78 | app.set('views', path.join(__dirname, 'views'))
79 | app.engine('html', require('ejs').renderFile)
80 | app.set('view engine', 'html')
81 | app.use(require('./app/routes'))
82 | app.listen(app.get('port'))
83 |
84 | // Init MongoDB
85 | initMongo()
86 |
87 | module.exports = app // for testing
88 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 |
4 | "env": {
5 | "node": true,
6 | "mocha": true,
7 | "es6": true
8 | },
9 |
10 | "globals": {
11 | "Promise": true,
12 | "_": true,
13 | "async": true,
14 | "expect": true,
15 | "jest": true
16 | },
17 |
18 | "rules": {
19 | "callback-return": [
20 | "error",
21 | ["done", "proceed", "next", "onwards", "callback", "cb"]
22 | ],
23 | "camelcase": [
24 | "warn",
25 | {
26 | "properties": "always"
27 | }
28 | ],
29 | "comma-style": ["warn", "last"],
30 | "curly": ["error"],
31 | "eqeqeq": ["error", "always"],
32 | "eol-last": ["warn"],
33 | "no-undef": 2,
34 | "handle-callback-err": ["error"],
35 | "arrow-body-style": ["off", 2],
36 | "indent": ["off", 2],
37 | "linebreak-style": ["error", "unix"],
38 | "no-dupe-keys": ["error"],
39 | "no-duplicate-case": ["error"],
40 | "no-extra-semi": ["warn"],
41 | "no-labels": ["error"],
42 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
43 | "no-redeclare": ["warn"],
44 | "no-return-assign": ["error", "always"],
45 | "no-sequences": ["error"],
46 | "no-trailing-spaces": ["warn"],
47 | "no-unexpected-multiline": ["warn"],
48 | "no-unreachable": ["warn"],
49 | "no-magic-numbers": ["off"],
50 | "max-params": ["off"],
51 | "max-len": ["off"],
52 | "max-nested-callbacks": ["off"],
53 | "new-cap": ["off"],
54 | "consistent-this": ["error", "that"],
55 | "no-unused-vars": [
56 | "error",
57 | {
58 | "caughtErrors": "all",
59 | "caughtErrorsIgnorePattern": "^unused($|[A-Z].*$)"
60 | }
61 | ],
62 | "no-use-before-define": [
63 | "error",
64 | {
65 | "functions": false
66 | }
67 | ],
68 | "no-var": 2,
69 | "one-var": ["warn", "never"],
70 | "prefer-arrow-callback": [
71 | "warn",
72 | {
73 | "allowNamedFunctions": true
74 | }
75 | ],
76 | "quotes": [
77 | "warn",
78 | "single",
79 | {
80 | "avoidEscape": false,
81 | "allowTemplateLiterals": true
82 | }
83 | ],
84 | "semi-spacing": [
85 | "warn",
86 | {
87 | "before": false,
88 | "after": true
89 | }
90 | ],
91 | "semi-style": ["warn", "last"],
92 | "space-before-function-paren": ["off", 2],
93 | "prettier/prettier": "error"
94 | },
95 | "extends": [
96 | "formidable/rules/eslint/best-practices/off",
97 | "formidable/rules/eslint/es6/off",
98 | "formidable/rules/eslint/errors/off",
99 | "formidable/rules/eslint/strict/on",
100 | "formidable/rules/eslint/node/off",
101 | "formidable/rules/eslint/style/on",
102 | "formidable/rules/eslint/variables/on",
103 | "prettier"
104 | ],
105 | "plugins": ["prettier"]
106 | }
107 |
--------------------------------------------------------------------------------
/app/services/crypto/verifyCoseSign1Address.test.js:
--------------------------------------------------------------------------------
1 | const { verifyCoseSign1Address } = require('./verifyCoseSign1Address.js')
2 | const {
3 | createFakePrivateKey
4 | } = require('../../../test/helpers/auth/createFakePrivateKey.js')
5 | const {
6 | createRewardAddress
7 | } = require('../../../test/helpers/auth/createRewardAddress.js')
8 | const {
9 | createCOSESign1Signature
10 | } = require('../../../test/helpers/auth/createCOSESign1Signature.js')
11 | const { createCOSEKey } = require('../../../test/helpers/auth/createCOSEKey.js')
12 |
13 | const stakePrivateKey1 = createFakePrivateKey(11)
14 |
15 | const stakeAddress1 = createRewardAddress(stakePrivateKey1)
16 |
17 | const stakePrivateKey2 = createFakePrivateKey(12)
18 |
19 | const stakeAddress2 = createRewardAddress(stakePrivateKey2)
20 |
21 | describe('verifyCoseSign1Address()', () => {
22 | it('Should verify a correct signature', async () => {
23 | const coseSign1 = createCOSESign1Signature(
24 | { host: 'host' },
25 | stakeAddress1,
26 | stakePrivateKey1
27 | )
28 | const coseKey = createCOSEKey(stakePrivateKey1)
29 |
30 | const result = await verifyCoseSign1Address(
31 | Buffer.from(coseKey.to_bytes()).toString('hex'),
32 | Buffer.from(coseSign1.to_bytes()).toString('hex'),
33 | stakeAddress1.to_address().to_bech32()
34 | )
35 | expect(result).toBe(true)
36 | })
37 |
38 | it('Should not verify a correct signature and incorrect address', async () => {
39 | const coseSign1 = createCOSESign1Signature(
40 | { host: 'host' },
41 | stakeAddress1,
42 | stakePrivateKey1
43 | )
44 | const coseKey = createCOSEKey(stakePrivateKey1)
45 |
46 | const result = await verifyCoseSign1Address(
47 | Buffer.from(coseKey.to_bytes()).toString('hex'),
48 | Buffer.from(coseSign1.to_bytes()).toString('hex'),
49 | stakeAddress2.to_address().to_bech32()
50 | )
51 | expect(result).toBe(false)
52 | })
53 |
54 | it('Should not verify a incorrect key', async () => {
55 | const coseSign1 = createCOSESign1Signature(
56 | { host: 'host' },
57 | stakeAddress1,
58 | stakePrivateKey1
59 | )
60 | const coseKey = createCOSEKey(stakePrivateKey2)
61 |
62 | const result = await verifyCoseSign1Address(
63 | Buffer.from(coseKey.to_bytes()).toString('hex'),
64 | Buffer.from(coseSign1.to_bytes()).toString('hex'),
65 | stakeAddress1.to_address().to_bech32()
66 | )
67 | expect(result).toBe(false)
68 | })
69 |
70 | it('Should not verify a incorrect tampered signature', async () => {
71 | const coseSign1 = createCOSESign1Signature(
72 | { host: 'host' },
73 | stakeAddress2,
74 | stakePrivateKey1
75 | )
76 | const coseKey = createCOSEKey(stakePrivateKey1)
77 |
78 | const result = await verifyCoseSign1Address(
79 | Buffer.from(coseKey.to_bytes()).toString('hex'),
80 | Buffer.from(coseSign1.to_bytes()).toString('hex'),
81 | stakeAddress1.to_address().to_bech32()
82 | )
83 | expect(result).toBe(false)
84 | })
85 | })
86 |
--------------------------------------------------------------------------------
/postmanCrypto.js:
--------------------------------------------------------------------------------
1 | const {
2 | createFakePrivateKey,
3 | createRewardAddress,
4 | createCOSESign1Signature,
5 | createCOSEKey
6 | } = require('./test/helpers/auth')
7 |
8 | const host = 'HOST'
9 |
10 | /**
11 | *
12 | * @param {String} name
13 | * @param {String} email
14 | * @param {CSL.RewardAddress} address
15 | * @param {CSL.PrivateKey} privateKey
16 | * @returns
17 | */
18 | const createRegisterUserSignature = (name, email, address, privateKey) => {
19 | const payload = {
20 | host,
21 | action: 'Sign up',
22 | name,
23 | email
24 | }
25 |
26 | return createCOSESign1Signature(payload, address, privateKey)
27 | }
28 |
29 | /**
30 | *
31 | * @param {String} name
32 | * @param {String} email
33 | * @param {CSL.RewardAddress} address
34 | * @param {CSL.PrivateKey} PrivateKey
35 | * @returns
36 | */
37 | const createLoginUserSignature = (email, address, privateKey) => {
38 | const payload = {
39 | host,
40 | action: 'Login',
41 | email
42 | }
43 | return createCOSESign1Signature(payload, address, privateKey)
44 | }
45 |
46 | const fakePrivateKey = createFakePrivateKey(20)
47 | const fakeStakeAddress = createRewardAddress(fakePrivateKey)
48 | const name = 'My Name'
49 | const email = 'my@email.com'
50 | const coseKey = createCOSEKey(fakePrivateKey)
51 | const coseSign1 = createRegisterUserSignature(
52 | name,
53 | email,
54 | fakeStakeAddress,
55 | fakePrivateKey
56 | )
57 |
58 | console.log(
59 | `Register data for address ${fakeStakeAddress.to_address().to_bech32()}`
60 | )
61 | console.log(`COSE signature for user ${email}:`)
62 | console.log(`COSE Key CBOR: ${Buffer.from(coseKey.to_bytes()).toString('hex')}`)
63 | console.log(
64 | `Full signature CBOR: ${Buffer.from(coseSign1.to_bytes()).toString('hex')}`
65 | )
66 |
67 | const adminPrivateKey = createFakePrivateKey(0)
68 | const adminStakeAddress = createRewardAddress(adminPrivateKey)
69 | const adminCoseKey = createCOSEKey(adminPrivateKey)
70 | const adminLoginCoseSign1 = createLoginUserSignature(
71 | 'admin@admin.com',
72 | adminStakeAddress,
73 | adminPrivateKey
74 | )
75 | const adminResetCoseSign1 = createCOSESign1Signature(
76 | { host, action: 'Reset' },
77 | adminStakeAddress,
78 | adminPrivateKey
79 | )
80 |
81 | console.log(
82 | `Register data for address ${adminStakeAddress.to_address().to_bech32()}`
83 | )
84 | console.log(`COSE signature to login as admin:`)
85 | console.log(
86 | `COSE Key CBOR: ${Buffer.from(adminCoseKey.to_bytes()).toString('hex')}`
87 | )
88 | console.log(
89 | `Full signature CBOR: ${Buffer.from(adminLoginCoseSign1.to_bytes()).toString(
90 | 'hex'
91 | )}`
92 | )
93 |
94 | console.log(`COSE signature for changing admin's wallet:`)
95 | console.log(
96 | `Full signature CBOR: ${Buffer.from(adminResetCoseSign1.to_bytes()).toString(
97 | 'hex'
98 | )}`
99 | )
100 |
101 | const fakePostUsetPrivateKey = createFakePrivateKey(13)
102 | const fakePostAddress = createRewardAddress(fakePostUsetPrivateKey)
103 |
104 | console.log(
105 | `Fake address for posting a new user ${fakePostAddress
106 | .to_address()
107 | .to_bech32()}`
108 | )
109 |
--------------------------------------------------------------------------------
/mockWalletSignatures.js:
--------------------------------------------------------------------------------
1 | const prompt = require('prompt')
2 | const {
3 | createCOSEKey,
4 | createRewardAddress,
5 | createFakePrivateKey,
6 | createCOSESign1Signature
7 | } = require('./test/helpers/auth')
8 | prompt.start()
9 |
10 | prompt.message = ''
11 | prompt.delimiter = ':'
12 |
13 | const host = 'HOST'
14 |
15 | const getPayload = async () => {
16 | const actionPrompt = await prompt.get({
17 | name: 'action',
18 | description: 'Action',
19 | message: 'Valid inputs: S, R or L',
20 | pattern: '[SRLsrl]'
21 | })
22 |
23 | switch (actionPrompt.action) {
24 | case 'S':
25 | case 's':
26 | console.log(`\u{1F916} Creating signup payload.`)
27 | const signupPrompt = await prompt.get([
28 | {
29 | name: 'name',
30 | description: 'User name'
31 | },
32 | {
33 | name: 'email',
34 | description: 'User email'
35 | }
36 | ])
37 |
38 | return {
39 | uri: host + '/register',
40 | action: 'Sign up',
41 | timestamp: Date.now(),
42 | name: signupPrompt.name,
43 | email: signupPrompt.email
44 | }
45 | case 'R':
46 | case 'r':
47 | console.log(`\u{1F916} Creating reset payload.`)
48 | return {
49 | uri: host + '/reset',
50 | action: 'Reset',
51 | timestamp: Date.now()
52 | }
53 | case 'L':
54 | case 'l':
55 | console.log(`\u{1F916} Creating login payload.`)
56 | return {
57 | uri: host + '/login',
58 | action: 'Login',
59 | timestamp: Date.now()
60 | }
61 | }
62 | }
63 |
64 | const main = async () => {
65 | console.log(
66 | `\u{1F916} Please, select the action for the payload (S: Signup, R: Reset, L: Login)`
67 | )
68 |
69 | const payload = await getPayload()
70 |
71 | console.log(
72 | `\u{1F916} Choose a number between 0 and 254. Each number represents a unique address and private key. For example in the sample data, the number 0 is the wallet for admin and the number 1 for the simple user. `
73 | )
74 | const walletNumberPrompt = await prompt.get({
75 | name: 'walletNumber',
76 | description: 'Wallet number',
77 | type: 'integer',
78 | minimum: 0,
79 | maximum: 254,
80 | message: 'Integer between 0 and 254'
81 | })
82 |
83 | const walletNumber = walletNumberPrompt.walletNumber
84 |
85 | const privateKey = createFakePrivateKey(walletNumber)
86 | const stakeAddress = createRewardAddress(privateKey)
87 | const coseKey = createCOSEKey(privateKey)
88 | const adminResetCoseSign1 = createCOSESign1Signature(
89 | payload,
90 | stakeAddress,
91 | privateKey
92 | )
93 |
94 | console.log(`\u{1F916} Generating wallet address, key and signature.\n`)
95 |
96 | console.log(
97 | `\u{1F4EA} Address: \x1b[34m${stakeAddress
98 | .to_address()
99 | .to_bech32()}\x1b[0m\n`
100 | )
101 |
102 | console.log(
103 | `\u{1F511} Key: \x1b[36m${Buffer.from(coseKey.to_bytes()).toString(
104 | 'hex'
105 | )} \x1b[0m\n`
106 | )
107 | console.log(
108 | `\u{1F4DD} Signature: \x1b[35m${Buffer.from(
109 | adminResetCoseSign1.to_bytes()
110 | ).toString('hex')} \x1b[0m`
111 | )
112 | }
113 |
114 | main()
115 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at \ The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 |
47 | [version]: http://contributor-covenant.org/version/1/4/
48 |
--------------------------------------------------------------------------------
/test/profile.js:
--------------------------------------------------------------------------------
1 | /* eslint handle-callback-err: "off"*/
2 |
3 | process.env.NODE_ENV = 'test'
4 |
5 | const chai = require('chai')
6 | const chaiHttp = require('chai-http')
7 | const server = require('../server')
8 | const { getAdminLoginDetails } = require('./helpers/auth')
9 | // eslint-disable-next-line no-unused-vars
10 | const should = chai.should()
11 | const host = 'HOST'
12 | const loginDetails = getAdminLoginDetails(host)
13 | let token = ''
14 |
15 | chai.use(chaiHttp)
16 |
17 | describe('*********** PROFILE ***********', () => {
18 | describe('/POST login', () => {
19 | it('it should GET token', (done) => {
20 | chai
21 | .request(server)
22 | .post('/login')
23 | .send(loginDetails)
24 | .end((err, res) => {
25 | res.should.have.status(200)
26 | res.body.should.be.an('object')
27 | res.body.should.have.property('accessToken')
28 | token = res.body.accessToken
29 | done()
30 | })
31 | })
32 | })
33 | describe('/GET profile', () => {
34 | it('it should NOT be able to consume the route since no token was sent', (done) => {
35 | chai
36 | .request(server)
37 | .get('/profile')
38 | .end((err, res) => {
39 | res.should.have.status(401)
40 | done()
41 | })
42 | })
43 | it('it should GET profile', (done) => {
44 | chai
45 | .request(server)
46 | .get('/profile')
47 | .set('Authorization', `Bearer ${token}`)
48 | .end((err, res) => {
49 | res.should.have.status(200)
50 | res.body.should.be.an('object')
51 | res.body.should.include.keys('name', 'email')
52 | done()
53 | })
54 | })
55 | })
56 | describe('/PATCH profile', () => {
57 | it('it should NOT UPDATE profile empty name/email', (done) => {
58 | const user = {}
59 | chai
60 | .request(server)
61 | .patch('/profile')
62 | .set('Authorization', `Bearer ${token}`)
63 | .send(user)
64 | .end((err, res) => {
65 | res.should.have.status(422)
66 | res.body.should.be.a('object')
67 | res.body.should.have.property('errors')
68 | done()
69 | })
70 | })
71 | it('it should UPDATE profile', (done) => {
72 | const user = {
73 | name: 'Test123456',
74 | urlTwitter: 'https://hello.com',
75 | urlGitHub: 'https://hello.io',
76 | phone: '123123123',
77 | city: 'Bucaramanga',
78 | country: 'Colombia'
79 | }
80 | chai
81 | .request(server)
82 | .patch('/profile')
83 | .set('Authorization', `Bearer ${token}`)
84 | .send(user)
85 | .end((err, res) => {
86 | res.should.have.status(200)
87 | res.body.should.be.a('object')
88 | res.body.should.have.property('name').eql('Test123456')
89 | res.body.should.have.property('urlTwitter').eql('https://hello.com')
90 | res.body.should.have.property('urlGitHub').eql('https://hello.io')
91 | res.body.should.have.property('phone').eql('123123123')
92 | res.body.should.have.property('city').eql('Bucaramanga')
93 | res.body.should.have.property('country').eql('Colombia')
94 | done()
95 | })
96 | })
97 | it('it should NOT UPDATE profile with email that already exists', (done) => {
98 | const user = {
99 | email: 'programmer@programmer.com'
100 | }
101 | chai
102 | .request(server)
103 | .patch('/profile')
104 | .set('Authorization', `Bearer ${token}`)
105 | .send(user)
106 | .end((err, res) => {
107 | res.should.have.status(422)
108 | res.body.should.be.a('object')
109 | res.body.should.have.property('errors')
110 | done()
111 | })
112 | })
113 | it('it should NOT UPDATE profile with not valid URL´s', (done) => {
114 | const user = {
115 | name: 'Test123456',
116 | urlTwitter: 'hello',
117 | urlGitHub: 'hello',
118 | phone: '123123123',
119 | city: 'Bucaramanga',
120 | country: 'Colombia'
121 | }
122 | chai
123 | .request(server)
124 | .patch('/profile')
125 | .set('Authorization', `Bearer ${token}`)
126 | .send(user)
127 | .end((err, res) => {
128 | res.should.have.status(422)
129 | res.body.should.be.a('object')
130 | res.body.should.have.property('errors').that.has.property('msg')
131 | res.body.errors.msg[0].should.have
132 | .property('msg')
133 | .eql('NOT_A_VALID_URL')
134 | done()
135 | })
136 | })
137 | })
138 | })
139 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cardano-express-web3-skeleton",
3 | "version": "1.1.1",
4 | "description": "Backend skeleton for Cardano Web3 dApps. This backend uses Express as server and MongoDB as user database. It provides a basic API Rest for authentication and authorization using CIP-0008 Signing spec and CIP-0030 Cardano dApp-Wallet Web Bridge.",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/jmagan/cardano-express-web3-skeleton.git"
9 | },
10 | "scripts": {
11 | "start": "cross-env NODE_ENV=production pm2 start server.js",
12 | "mocha": "nyc mocha --timeout=5000 --exit",
13 | "test": "npm run coverage:clean && npm run test:unit && npm run test:e2e && npm run coverage",
14 | "test:unit": "cross-env NODE_ENV=test jest --coverage",
15 | "test:e2e": "cross-env NODE_ENV=test npm run fresh && npm run mocha",
16 | "dev": "cross-env NODE_ENV=development nodemon --inspect=9230 server.js",
17 | "fresh": "npm run clean && npm run seed",
18 | "clean": "node clean.js",
19 | "seed": "node seed.js",
20 | "prettier": "prettier --write --config .prettierrc.json \"**/*.js\"",
21 | "lint": "eslint --fix --config .eslintrc.json \"**/*.js\"",
22 | "remark": "remark . -o",
23 | "coverage": "npm run coverage:merge && npm run coverage:merge-report",
24 | "coverage:clean": "rm -rf .nyc_output && rm -rf coverage",
25 | "coverage:merge": "istanbul-merge --out coverage/merged/coverage-final.json ./coverage/unit/coverage-final.json ./coverage/e2e/coverage-final.json",
26 | "coverage:merge-report": "nyc report --reporter=lcov --reporter=text --reporter=json --temp-dir=./coverage/merged --report-dir=./coverage/merged"
27 | },
28 | "nyc": {
29 | "reporter": [
30 | "json",
31 | "text",
32 | "lcov"
33 | ],
34 | "report-dir": "coverage/e2e",
35 | "include": [
36 | "**/*.js"
37 | ],
38 | "exclude": [
39 | "**/*.test.js",
40 | "jest.config.js",
41 | "**/data/**",
42 | "**/node_modules/**",
43 | "**/.history/**",
44 | "**/test/**",
45 | "**/coverage/**",
46 | "**/tmp/**"
47 | ],
48 | "all": true
49 | },
50 | "husky": {
51 | "hooks": {
52 | "pre-commit": "lint-staged && npm run remark",
53 | "pre-push": "npm run test || true"
54 | }
55 | },
56 | "lint-staged": {
57 | "*.js": [
58 | "prettier --write",
59 | "eslint --fix"
60 | ]
61 | },
62 | "dependencies": {
63 | "@emurgo/cardano-message-signing-nodejs": "^1.0.1",
64 | "@emurgo/cardano-serialization-lib-nodejs": "^11.1.0",
65 | "babel-eslint": "^10.1.0",
66 | "bcrypt": "^5.1.0",
67 | "body-parser": "^1.20.1",
68 | "compression": "^1.7.4",
69 | "cookie-parser": "^1.4.6",
70 | "cors": "^2.8.5",
71 | "date-fns": "^2.29.3",
72 | "dotenv-safe": "^8.2.0",
73 | "ejs": "^3.1.8",
74 | "expeditious-engine-redis": "^0.1.2",
75 | "express": "^4.18.2",
76 | "express-expeditious": "^5.1.1",
77 | "express-validator": "^6.14.2",
78 | "helmet": "^4.6.0",
79 | "i18n": "^0.13.4",
80 | "jsonwebtoken": "^8.5.1",
81 | "mongoose": "^5.13.15",
82 | "mongoose-paginate-v2": "^1.7.1",
83 | "morgan": "^1.10.0",
84 | "nodemailer": "^6.8.0",
85 | "nodemailer-mailgun-transport": "^2.1.5",
86 | "passport": "^0.6.0",
87 | "passport-cardano-web3": "^2.0.0",
88 | "passport-jwt": "^4.0.1",
89 | "request-ip": "^2.2.0",
90 | "trim-request": "^1.0.6",
91 | "uuid": "^8.3.2",
92 | "validator": "^13.7.0"
93 | },
94 | "devDependencies": {
95 | "@types/faker": "^5.5.3",
96 | "chai": "^4.3.7",
97 | "chai-http": "^4.3.0",
98 | "cross-env": "^7.0.3",
99 | "eslint": "^7.32.0",
100 | "eslint-config-formidable": "^4.0.0",
101 | "eslint-config-prettier": "^7.2.0",
102 | "eslint-plugin-prettier": "^3.4.1",
103 | "faker": "^5.5.3",
104 | "husky": "^4.3.8",
105 | "istanbul-merge": "^1.1.1",
106 | "jest": "^26.6.3",
107 | "lint-staged": "^10.5.4",
108 | "mocha": "^8.4.0",
109 | "mongo-seeding": "^3.7.2",
110 | "nodemon": "^2.0.20",
111 | "nyc": "^15.1.0",
112 | "prettier": "^2.7.1",
113 | "prettier-eslint": "^12.0.0",
114 | "prompt": "^1.3.0",
115 | "remark-cli": "^9.0.0"
116 | },
117 | "keywords": [
118 | "cardano",
119 | "web3",
120 | "dapp",
121 | "blockchain",
122 | "javascript",
123 | "api",
124 | "node",
125 | "express",
126 | "mongo",
127 | "mongodb",
128 | "jwt",
129 | "postman",
130 | "i18n",
131 | "jwt-authentication",
132 | "token",
133 | "eslint",
134 | "starter",
135 | "web",
136 | "app",
137 | "mongoose",
138 | "rest",
139 | "skeleton",
140 | "async",
141 | "await",
142 | "mvp",
143 | "front-end",
144 | "testing",
145 | "prettier",
146 | "mocha",
147 | "chai",
148 | "redis",
149 | "JSDoc",
150 | "CIP",
151 | "93",
152 | "CIP-0093"
153 | ]
154 | }
155 |
--------------------------------------------------------------------------------
/test/cities.js:
--------------------------------------------------------------------------------
1 | /* eslint handle-callback-err: "off"*/
2 |
3 | process.env.NODE_ENV = 'test'
4 |
5 | const City = require('../app/models/city')
6 | const faker = require('faker')
7 | const chai = require('chai')
8 | const chaiHttp = require('chai-http')
9 | const server = require('../server')
10 | const { getAdminLoginDetails } = require('./helpers/auth')
11 | // eslint-disable-next-line no-unused-vars
12 | const should = chai.should()
13 | const host = 'HOST'
14 | const loginDetails = getAdminLoginDetails(host)
15 | let token = ''
16 | const createdID = []
17 | const name = faker.random.words()
18 | const newName = faker.random.words()
19 | const repeatedName = faker.random.words()
20 |
21 | chai.use(chaiHttp)
22 |
23 | describe('*********** CITIES ***********', () => {
24 | describe('/POST login', () => {
25 | it('it should GET token', (done) => {
26 | chai
27 | .request(server)
28 | .post('/login')
29 | .send(loginDetails)
30 | .end((err, res) => {
31 | res.should.have.status(200)
32 | res.body.should.be.an('object')
33 | res.body.should.have.property('accessToken')
34 | token = res.body.accessToken
35 | done()
36 | })
37 | })
38 | })
39 |
40 | describe('/GET cities', () => {
41 | it('it should NOT be able to consume the route since no token was sent', (done) => {
42 | chai
43 | .request(server)
44 | .get('/cities')
45 | .end((err, res) => {
46 | res.should.have.status(401)
47 | done()
48 | })
49 | })
50 | it('it should GET all the cities', (done) => {
51 | chai
52 | .request(server)
53 | .get('/cities')
54 | .set('Authorization', `Bearer ${token}`)
55 | .end((err, res) => {
56 | res.should.have.status(200)
57 | res.body.should.be.an('object')
58 | res.body.docs.should.be.a('array')
59 | done()
60 | })
61 | })
62 | it('it should GET the cities with filters', (done) => {
63 | chai
64 | .request(server)
65 | .get('/cities?filter=Bucaramanga&fields=name')
66 | .set('Authorization', `Bearer ${token}`)
67 | .end((err, res) => {
68 | res.should.have.status(200)
69 | res.body.should.be.an('object')
70 | res.body.docs.should.be.a('array')
71 | res.body.docs.should.have.lengthOf(1)
72 | res.body.docs[0].should.have.property('name').eql('Bucaramanga')
73 | done()
74 | })
75 | })
76 | })
77 |
78 | describe('/POST city', () => {
79 | it('it should NOT POST a city without name', (done) => {
80 | const city = {}
81 | chai
82 | .request(server)
83 | .post('/cities')
84 | .set('Authorization', `Bearer ${token}`)
85 | .send(city)
86 | .end((err, res) => {
87 | res.should.have.status(422)
88 | res.body.should.be.a('object')
89 | res.body.should.have.property('errors')
90 | done()
91 | })
92 | })
93 | it('it should POST a city ', (done) => {
94 | const city = {
95 | name
96 | }
97 | chai
98 | .request(server)
99 | .post('/cities')
100 | .set('Authorization', `Bearer ${token}`)
101 | .send(city)
102 | .end((err, res) => {
103 | res.should.have.status(201)
104 | res.body.should.be.a('object')
105 | res.body.should.include.keys('_id', 'name')
106 | createdID.push(res.body._id)
107 | done()
108 | })
109 | })
110 | it('it should NOT POST a city that already exists', (done) => {
111 | const city = {
112 | name
113 | }
114 | chai
115 | .request(server)
116 | .post('/cities')
117 | .set('Authorization', `Bearer ${token}`)
118 | .send(city)
119 | .end((err, res) => {
120 | res.should.have.status(422)
121 | res.body.should.be.a('object')
122 | res.body.should.have.property('errors')
123 | done()
124 | })
125 | })
126 | })
127 |
128 | describe('/GET/:id city', () => {
129 | it('it should GET a city by the given id', (done) => {
130 | const id = createdID.slice(-1).pop()
131 | chai
132 | .request(server)
133 | .get(`/cities/${id}`)
134 | .set('Authorization', `Bearer ${token}`)
135 | .end((error, res) => {
136 | res.should.have.status(200)
137 | res.body.should.be.a('object')
138 | res.body.should.have.property('name')
139 | res.body.should.have.property('_id').eql(id)
140 | done()
141 | })
142 | })
143 | })
144 |
145 | describe('/PATCH/:id city', () => {
146 | it('it should UPDATE a city given the id', (done) => {
147 | const id = createdID.slice(-1).pop()
148 | chai
149 | .request(server)
150 | .patch(`/cities/${id}`)
151 | .set('Authorization', `Bearer ${token}`)
152 | .send({
153 | name: newName
154 | })
155 | .end((error, res) => {
156 | res.should.have.status(200)
157 | res.body.should.be.a('object')
158 | res.body.should.have.property('_id').eql(id)
159 | res.body.should.have.property('name').eql(newName)
160 | done()
161 | })
162 | })
163 | it('it should NOT UPDATE a city that already exists', (done) => {
164 | const city = {
165 | name: repeatedName
166 | }
167 | chai
168 | .request(server)
169 | .post('/cities')
170 | .set('Authorization', `Bearer ${token}`)
171 | .send(city)
172 | .end((err, res) => {
173 | res.should.have.status(201)
174 | res.body.should.be.a('object')
175 | res.body.should.include.keys('_id', 'name')
176 | res.body.should.have.property('name').eql(repeatedName)
177 | createdID.push(res.body._id)
178 | const anotherCity = {
179 | name: newName
180 | }
181 | chai
182 | .request(server)
183 | .patch(`/cities/${createdID.slice(-1).pop()}`)
184 | .set('Authorization', `Bearer ${token}`)
185 | .send(anotherCity)
186 | .end((error, result) => {
187 | result.should.have.status(422)
188 | result.body.should.be.a('object')
189 | result.body.should.have.property('errors')
190 | done()
191 | })
192 | })
193 | })
194 | })
195 |
196 | describe('/DELETE/:id city', () => {
197 | it('it should DELETE a city given the id', (done) => {
198 | const city = {
199 | name
200 | }
201 | chai
202 | .request(server)
203 | .post('/cities')
204 | .set('Authorization', `Bearer ${token}`)
205 | .send(city)
206 | .end((err, res) => {
207 | res.should.have.status(201)
208 | res.body.should.be.a('object')
209 | res.body.should.include.keys('_id', 'name')
210 | res.body.should.have.property('name').eql(name)
211 | chai
212 | .request(server)
213 | .delete(`/cities/${res.body._id}`)
214 | .set('Authorization', `Bearer ${token}`)
215 | .end((error, result) => {
216 | result.should.have.status(200)
217 | result.body.should.be.a('object')
218 | result.body.should.have.property('msg').eql('DELETED')
219 | done()
220 | })
221 | })
222 | })
223 | })
224 |
225 | after(() => {
226 | createdID.forEach((id) => {
227 | City.findByIdAndRemove(id, (err) => {
228 | if (err) {
229 | console.log(err)
230 | }
231 | })
232 | })
233 | })
234 | })
235 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Node.js express.js MongoDB JWT REST API - Basic Project Skeleton
2 |
3 | [](https://github.com/jmagan/cardano-express-web3-skeleton/blob/master/LICENSE)
4 |
5 | ## Getting started
6 |
7 | This is a basic API REST skeleton for Cardano dApp authentication and authorization written on JavaScript using async/await. This backend utilizes the standard [CIP-0008 signing spec](https://github.com/cardano-foundation/CIPs/blob/master/CIP-0008/README.md). The project has all necessary endpoints for athentication, authorization and user management. The authentication token is generated as a JWT web token, therefore it can be shared easily by other services.
8 |
9 | The authentication process is driven by signed payloads with the [CIP-0030 Cardano dApp-wallet web bridge](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0030). There are three actions that require the user's wallet signature, *Signup*, *Login* and *Reset*. Once the payload with the desired action is signed with the correct private key, a JWT web token is issued and takes control of the session.
10 |
11 | ## Frontend
12 |
13 | This repository is self-contained and you can use ir and test it through the test suite and Postman. You can read more about the Postman examples in the usage section below. This is a good method for developing and debugging the code. But a front end is required to better understand how it works.
14 |
15 | I've created another template for creating your web3 applications with ReactJS. You can clone it from the next repository [jmagan/cardano-react-web3-skeleton](https://github.com/jmagan/cardano-react-web3-skeleton).
16 |
17 | ## Features
18 |
19 | * Cardano [CIP-0008 signing spec](https://github.com/cardano-foundation/CIPs/blob/master/CIP-0008/README.md) for the login and registration process.
20 | * Multiple environment ready (development, production)
21 | * Custom email/cardano address user system with basic security and blocking for preventing brute force attacks.
22 | * Compressed responses.
23 | * Secured HTTP headers.
24 | * CORS ready.
25 | * Cache ready (Redis).
26 | * HTTP request logger in development mode.
27 | * i18n ready (for sending emails in multiple languages).
28 | * User roles.
29 | * Pagination ready.
30 | * User profile.
31 | * Users list for admin area.
32 | * Cities model and controller example.
33 | * Login access log with IP, browser and country location (for country it looks for the header `cf-ipcountry` that CloudFlare creates when protecting your website).
34 | * API autogenerated documentation by Postman.
35 | * API collection example for Postman.
36 | * Testing with mocha/chai for API endpoints.
37 | * NPM scripts for cleaning and seeding the MongoDB database.
38 | * NPM script for keeping good source code formatting using prettier and ESLint.
39 | * Use of ESLint for good coding practices.
40 | * Mailer example with Nodemailer and Mailgun.
41 | * HTTPOnly refresh token cookie.
42 | * Ability to refresh token.
43 | * JWT Tokens, make requests with a token after login with `Authorization` header with value `Bearer yourToken` where `yourToken` is the **signed and encrypted token** given in the response from the login process.
44 |
45 | ## Requirements
46 |
47 | * Node.js **10+**
48 | * MongoDB **3.6+**
49 | * Redis **5.0+**
50 |
51 | ## How to install
52 |
53 | ### Using Git (recommended)
54 |
55 | 1. Clone the project from github. Change "myproject" to your project name.
56 |
57 | ```bash
58 | git clone https://github.com/jmagan/cardano-express-web3-skeleton.git ./myproject
59 | ```
60 |
61 | ### Using manual download ZIP
62 |
63 | 1. Download repository
64 | 2. Uncompress to your desired directory
65 |
66 | ### Install npm dependencies after installing (Git or manual download)
67 |
68 | ```bash
69 | cd myproject
70 | npm install
71 | npm update
72 | ```
73 |
74 | ### Setting up environments (development or production)
75 |
76 | 1. In the root this repository you will find a file named `.env.example`
77 | 2. Create a new file by copying and pasting the file and then renaming it to just `.env`
78 | 3. The file `.env` is already ignored, so you never commit your credentials.
79 | 4. Change the values of the file to your environment (development or production)
80 | 5. Upload the `.env` to your environment server(development or production)
81 | 6. If you use the postman collection to try the endpoints, change value of the variable `server` on your environment to the url of your server, for development mode use
82 |
83 | **IMPORTANT:** By default token expires in 3 days (4320 minutes set in .env.example). You can refresh token at endpoint GET /token. If everything it´s ok you will get a new token.
84 |
85 | ### Mailer
86 |
87 | To ensure the deliverability of emails sent by this API, `Mailgun` is used for mailing users when they sign up, so if you want to use that feature go sign up at their website
88 |
89 | If you want to try a different method it´s ok, I used for this API and they have different transport methods like: smtp.
90 |
91 | ### i18n
92 |
93 | Language is automatically detected from `Accept-Language` header on the request. So either you send locale manually on the request or your browser will send its default, if `Accept-Language` header is not sent then it will use `en` locale as default.
94 |
95 | ## How to run
96 |
97 | ### Database cleaning and seeding samples
98 |
99 | There are 3 available commands for this: `fresh`, `clean` and `seed`.
100 |
101 | ```bash
102 | npm run command
103 | ```
104 |
105 | * `fresh` cleans and then seeds the database with dynamic data.
106 | * `clean` cleans the database.
107 | * `seed` seeds the database with dynamic data.
108 |
109 | ### Running in development mode (lifting API server)
110 |
111 | ```bash
112 | npm run dev
113 | ```
114 |
115 | You will know server is running by checking the output of the command `npm run dev`
116 |
117 | ```bash
118 | ****************************
119 | * Starting Server
120 | * Port: 3000
121 | * NODE_ENV: development
122 | * Database: MongoDB
123 | * DB Connection: OK
124 | ****************************
125 | ```
126 |
127 | ### Running tests
128 |
129 | It´s a good practice to do tests at your code, so a sample of how to do that in `mocha/chai` is also included in the `/test` directory
130 |
131 | ```bash
132 | npm run test
133 | ```
134 |
135 | ### Formatting code
136 |
137 | Format your code with prettier by typing:
138 |
139 | ```bash
140 | npm run format
141 | ```
142 |
143 | ### Formatting markdown files
144 |
145 | Format all your markdown files with remark by typing:
146 |
147 | ```bash
148 | npm run remark
149 | ```
150 |
151 | ### Linting code
152 |
153 | Lint your code with ESLint by typing:
154 |
155 | ```bash
156 | npm run lint
157 | ```
158 |
159 | ## Usage
160 |
161 | Once everything is set up to test API routes either use Postman or any other api testing application.
162 |
163 | ### Mocking Cardano signatures
164 |
165 | In order to use some endpoints, we need to sign payloads according to CIP-0008 signing spec. For this purpose, the project has a cli util for creating the keys and signatures. This can simulate 255 unique wallets selecting a number between 0 and 254. The cli util starts with the following command:
166 |
167 | ```bash
168 | $ node mockWalletSignatures.js
169 | ```
170 |
171 | In order to get the key and signature, we need to go through three steps.
172 |
173 | 1. First, select the payload for your endpoint. Each end point needs the corresponding action.
174 | 2. If the payload needs some additional info, the prompt will ask about them. In the case of the login payload, we will need to insert an email.
175 | 3. Select a number between 0 and 254 for selecting a unique wallet. Currently, the number 0 belongs to the admin's wallet and the number 1 to the simple user's wallet. These users are created in the database by the seed script. You can use them for testing purposes in Postman.
176 |
177 | In the next example, we can find how to create a login payload for the admin's account.
178 |
179 | 🤖 Please, select the action for the payload (S: Signup, R: Reset, L: Login)
180 | Action: L
181 | 🤖 Creating login payload.
182 | 🤖 Choose a number between 0 and 254. Each number represents a unique address and private key. For example in the sample data, the number 0 is the wallet for admin and the number 1 for the simple user.
183 | Wallet number: 0
184 | 🤖 Generating wallet address, key and signature.
185 |
186 | 📪 Address: stake1u89exkzjnh6898pjg632qv7tnqs6h073dhjg3qq9jp9tcsgq0wfzr
187 |
188 | 🔑 Key: a201012158203b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29
189 |
190 | 📝 Signature: 845828a16761646472657373581de1cb9358529df4729c3246a2a033cb9821abbfd16de4888005904abc41a166686173686564f4583a7b22686f7374223a22484f5354222c22616374696f6e223a224c6f67696e222c22656d61696c223a2261646d696e4061646d696e2e636f6d227d5840f93faf1473ad7ff9cdcbc4e2acbb4c24e90329c2ee77b04a8fcc8a716e04df9ae4094fb86f1ff3a88c85e892bf166d1b03bcf5c98cb821be40c285d9fea3e804
191 |
192 | The address, key and signature will be used calling the endpoints. This login endpoint call is currently implemented in the `postman-example.json`.
193 |
194 | ### Postman API example collection
195 |
196 | You can import the example collection to Postman. To import, click the import button located and select `postman-example.json` located within the root directory.
197 |
198 | Go to `manage environments` to create environments for development, production, etc. On each of the environments you create you will need to:
199 |
200 | 1. Create a new key `authToken` and within the `/login` request this value is automatically updated after a successfull login through a script located in the `tests` tab. Each time you make a request to the API it will send `Authorization` header with the `token` value in the request, you can check this on the headers of users or cities endpoints in the Postman example.
201 |
202 | 2. Create a second key `server` with the url of your server, for development mode use
203 |
204 | This is a REST API, so it works using the following HTTP methods:
205 |
206 | * GET (Read): Gets a list of items, or a single item
207 | * POST (Create): Creates an item
208 | * PATCH (Update): Updates an item
209 | * DELETE: Deletes an item
210 |
211 | ### Creating new models
212 |
213 | If you need to add more models to the project just create a new file in `/app/models/` and it will be loaded dynamically.
214 |
215 | ### Creating new routes
216 |
217 | If you need to add more routes to the project just create a new file in `/app/routes/` and it will be loaded dynamically.
218 |
219 | ### Creating new controllers
220 |
221 | When you create a new controller, try to also create another folder with validations and helpers. Ex. `/countries`, `/countries/validators` and `/countries/helpers`. An example of this is included in the repository.
222 |
223 | ## Support
224 |
225 | If you find it useful, please consider inviting me a coffee :)
226 |
227 | [](https://app.doitwithlovelace.io/users/DoItWithLovelace)
228 |
229 | ## Bugs or improvements
230 |
231 | You are welcome to report any bugs or improvements. Pull requests are always welcome.
232 |
233 | ## License
234 |
235 | This project is open-sourced software licensed under the MIT License. See the LICENSE file for more information.
236 |
--------------------------------------------------------------------------------
/test/users.js:
--------------------------------------------------------------------------------
1 | /* eslint handle-callback-err: "off"*/
2 |
3 | process.env.NODE_ENV = 'test'
4 |
5 | const User = require('../app/models/user')
6 | const faker = require('faker')
7 | const chai = require('chai')
8 | const chaiHttp = require('chai-http')
9 | const server = require('../server')
10 | const {
11 | getAdminLoginDetails,
12 | getUserLoginDetails,
13 | createFakePrivateKey,
14 | createRewardAddress
15 | } = require('./helpers/auth')
16 | // eslint-disable-next-line no-unused-vars
17 | const should = chai.should()
18 | const host = 'HOST'
19 | const loginDetails = {
20 | admin: getAdminLoginDetails(host),
21 | user: getUserLoginDetails(host)
22 | }
23 |
24 | const adminPrivateKey = createFakePrivateKey(0)
25 | const adminStakeAddress = createRewardAddress(adminPrivateKey)
26 |
27 | const tokens = {
28 | admin: '',
29 | user: ''
30 | }
31 |
32 | const email = faker.internet.email()
33 | const createdID = []
34 |
35 | chai.use(chaiHttp)
36 |
37 | describe('*********** USERS ***********', () => {
38 | describe('/POST login', () => {
39 | it('it should GET token as admin', (done) => {
40 | chai
41 | .request(server)
42 | .post('/login')
43 | .send(loginDetails.admin)
44 | .end((err, res) => {
45 | res.should.have.status(200)
46 | res.body.should.be.an('object')
47 | res.body.should.have.property('accessToken')
48 | tokens.admin = res.body.accessToken
49 | done()
50 | })
51 | })
52 | it('it should GET token as user', (done) => {
53 | chai
54 | .request(server)
55 | .post('/login')
56 | .send(loginDetails.user)
57 | .end((err, res) => {
58 | res.should.have.status(200)
59 | res.body.should.be.an('object')
60 | res.body.should.have.property('accessToken')
61 | tokens.user = res.body.accessToken
62 | done()
63 | })
64 | })
65 | })
66 | describe('/GET users', () => {
67 | it('it should NOT be able to consume the route since no token was sent', (done) => {
68 | chai
69 | .request(server)
70 | .get('/users')
71 | .end((err, res) => {
72 | res.should.have.status(401)
73 | done()
74 | })
75 | })
76 | it('it should GET all the users', (done) => {
77 | chai
78 | .request(server)
79 | .get('/users')
80 | .set('Authorization', `Bearer ${tokens.admin}`)
81 | .end((err, res) => {
82 | res.should.have.status(200)
83 | res.body.should.be.an('object')
84 | res.body.docs.should.be.a('array')
85 | done()
86 | })
87 | })
88 | it('it should GET the users with filters', (done) => {
89 | chai
90 | .request(server)
91 | .get('/users?filter=admin&fields=email')
92 | .set('Authorization', `Bearer ${tokens.admin}`)
93 | .end((err, res) => {
94 | res.should.have.status(200)
95 | res.body.should.be.an('object')
96 | res.body.docs.should.be.a('array')
97 | res.body.docs.should.have.lengthOf(1)
98 | res.body.docs[0].should.have.property('email').eql('admin@admin.com')
99 | done()
100 | })
101 | })
102 | })
103 | describe('/POST user', () => {
104 | it('it should NOT POST a user without name', (done) => {
105 | const user = {}
106 | chai
107 | .request(server)
108 | .post('/users')
109 | .set('Authorization', `Bearer ${tokens.admin}`)
110 | .send(user)
111 | .end((err, res) => {
112 | res.should.have.status(422)
113 | res.body.should.be.a('object')
114 | res.body.should.have.property('errors')
115 | done()
116 | })
117 | })
118 | it('it should POST a user ', (done) => {
119 | const user = {
120 | name: faker.random.words(),
121 | email,
122 | walletAddress: faker.random.words(),
123 | role: 'admin',
124 | urlTwitter: faker.internet.url(),
125 | urlGitHub: faker.internet.url(),
126 | phone: faker.phone.phoneNumber(),
127 | city: faker.random.words(),
128 | country: faker.random.words()
129 | }
130 | chai
131 | .request(server)
132 | .post('/users')
133 | .set('Authorization', `Bearer ${tokens.admin}`)
134 | .send(user)
135 | .end((err, res) => {
136 | res.should.have.status(201)
137 | res.body.should.be.a('object')
138 | res.body.should.include.keys('_id', 'name', 'email', 'verification')
139 | createdID.push(res.body._id)
140 | done()
141 | })
142 | })
143 | it('it should NOT POST a user with email that already exists', (done) => {
144 | const user = {
145 | name: faker.random.words(),
146 | email,
147 | password: faker.random.words(),
148 | role: 'admin',
149 | walletAddress: faker.random.alphaNumeric(23),
150 | phone: '1234',
151 | country: 'Country',
152 | city: 'City'
153 | }
154 | chai
155 | .request(server)
156 | .post('/users')
157 | .set('Authorization', `Bearer ${tokens.admin}`)
158 | .send(user)
159 | .end((err, res) => {
160 | res.should.have.status(422)
161 | res.body.should.be.a('object')
162 | res.body.should.have.property('errors')
163 | done()
164 | })
165 | })
166 | it('it should NOT POST a user with wallet address that already exists', (done) => {
167 | const user = {
168 | name: faker.random.words(),
169 | email: faker.internet.email(),
170 | password: faker.random.words(),
171 | walletAddress: adminStakeAddress.to_address().to_bech32(),
172 | role: 'admin',
173 | phone: '1234',
174 | country: 'Country',
175 | city: 'City'
176 | }
177 | chai
178 | .request(server)
179 | .post('/users')
180 | .set('Authorization', `Bearer ${tokens.admin}`)
181 | .send(user)
182 | .end((err, res) => {
183 | res.should.have.status(422)
184 | res.body.should.be.a('object')
185 | res.body.should.have.property('errors')
186 | done()
187 | })
188 | })
189 | it('it should NOT POST a user with not known role', (done) => {
190 | const user = {
191 | name: faker.random.words(),
192 | email,
193 | password: faker.random.words(),
194 | role: faker.random.words()
195 | }
196 | chai
197 | .request(server)
198 | .post('/users')
199 | .set('Authorization', `Bearer ${tokens.admin}`)
200 | .send(user)
201 | .end((err, res) => {
202 | res.should.have.status(422)
203 | res.body.should.be.a('object')
204 | res.body.should.have.property('errors')
205 | done()
206 | })
207 | })
208 | })
209 | describe('/GET/:id user', () => {
210 | it('it should GET a user by the given id', (done) => {
211 | const id = createdID.slice(-1).pop()
212 | chai
213 | .request(server)
214 | .get(`/users/${id}`)
215 | .set('Authorization', `Bearer ${tokens.admin}`)
216 | .end((error, res) => {
217 | res.should.have.status(200)
218 | res.body.should.be.a('object')
219 | res.body.should.have.property('name')
220 | res.body.should.have.property('_id').eql(id)
221 | done()
222 | })
223 | })
224 | })
225 | describe('/PATCH/:id user', () => {
226 | it('it should UPDATE a user given the id', (done) => {
227 | const id = createdID.slice(-1).pop()
228 | const user = {
229 | name: 'JS123456',
230 | email: 'emailthatalreadyexists@email.com',
231 | role: 'admin',
232 | urlTwitter: faker.internet.url(),
233 | urlGitHub: faker.internet.url(),
234 | phone: faker.phone.phoneNumber(),
235 | city: faker.random.words(),
236 | country: faker.random.words()
237 | }
238 | chai
239 | .request(server)
240 | .patch(`/users/${id}`)
241 | .set('Authorization', `Bearer ${tokens.admin}`)
242 | .send(user)
243 | .end((error, res) => {
244 | res.should.have.status(200)
245 | res.body.should.be.a('object')
246 | res.body.should.have.property('_id').eql(id)
247 | res.body.should.have.property('name').eql('JS123456')
248 | res.body.should.have
249 | .property('email')
250 | .eql('emailthatalreadyexists@email.com')
251 | createdID.push(res.body._id)
252 | done()
253 | })
254 | })
255 | it('it should NOT UPDATE a user with email that already exists', (done) => {
256 | const id = createdID.slice(-1).pop()
257 | const user = {
258 | name: faker.random.words(),
259 | email: 'admin@admin.com',
260 | role: 'admin',
261 | phone: '1234',
262 | country: 'Country',
263 | city: 'City'
264 | }
265 | chai
266 | .request(server)
267 | .patch(`/users/${id}`)
268 | .set('Authorization', `Bearer ${tokens.admin}`)
269 | .send(user)
270 | .end((err, res) => {
271 | res.should.have.status(422)
272 | res.body.should.be.a('object')
273 | res.body.should.have.property('errors')
274 | done()
275 | })
276 | })
277 | it('it should NOT UPDATE another user if not an admin', (done) => {
278 | const id = createdID.slice(-1).pop()
279 | const user = {
280 | name: faker.random.words(),
281 | email: 'toto@toto.com',
282 | role: 'user'
283 | }
284 | chai
285 | .request(server)
286 | .patch(`/users/${id}`)
287 | .set('Authorization', `Bearer ${tokens.user}`)
288 | .send(user)
289 | .end((err, res) => {
290 | res.should.have.status(401)
291 | res.body.should.be.a('object')
292 | res.body.should.have.property('errors')
293 | done()
294 | })
295 | })
296 | })
297 | describe('/DELETE/:id user', () => {
298 | it('it should DELETE a user given the id', (done) => {
299 | const user = {
300 | name: faker.random.words(),
301 | email,
302 | walletAddress: faker.random.words(),
303 | role: 'admin',
304 | urlTwitter: faker.internet.url(),
305 | urlGitHub: faker.internet.url(),
306 | phone: faker.phone.phoneNumber(),
307 | city: faker.random.words(),
308 | country: faker.random.words()
309 | }
310 | chai
311 | .request(server)
312 | .post('/users')
313 | .set('Authorization', `Bearer ${tokens.admin}`)
314 | .send(user)
315 | .end((err, res) => {
316 | res.should.have.status(201)
317 | res.body.should.be.a('object')
318 | res.body.should.include.keys('_id', 'name', 'email', 'verification')
319 | chai
320 | .request(server)
321 | .delete(`/users/${res.body._id}`)
322 | .set('Authorization', `Bearer ${tokens.admin}`)
323 | .end((error, result) => {
324 | result.should.have.status(200)
325 | result.body.should.be.a('object')
326 | result.body.should.have.property('msg').eql('DELETED')
327 | done()
328 | })
329 | })
330 | })
331 | })
332 |
333 | after(() => {
334 | createdID.forEach((id) => {
335 | User.findByIdAndRemove(id, (err) => {
336 | if (err) {
337 | console.log(err)
338 | }
339 | })
340 | })
341 | })
342 | })
343 |
--------------------------------------------------------------------------------
/test/auth.js:
--------------------------------------------------------------------------------
1 | /* eslint handle-callback-err: "off"*/
2 |
3 | process.env.NODE_ENV = 'test'
4 |
5 | const User = require('../app/models/user')
6 | const faker = require('faker')
7 | const chai = require('chai')
8 | const chaiHttp = require('chai-http')
9 | const server = require('../server')
10 | const {
11 | createFakePrivateKey,
12 | createRewardAddress,
13 | createCOSEKey,
14 | createCOSESign1Signature,
15 | getAdminLoginDetails
16 | } = require('./helpers/auth')
17 | // eslint-disable-next-line no-unused-vars
18 | const should = chai.should()
19 |
20 | const host = process.env.HOST
21 |
22 | const adminPrivateKey = createFakePrivateKey(0)
23 | const adminStakeAddress = createRewardAddress(adminPrivateKey)
24 |
25 | const stakePrivateKey1 = createFakePrivateKey(10)
26 | const stakeAddress1 = createRewardAddress(stakePrivateKey1)
27 |
28 | const stakePrivateKey2 = createFakePrivateKey(11)
29 | const stakeAddress2 = createRewardAddress(stakePrivateKey2)
30 |
31 | const stakePrivateKey3 = createFakePrivateKey(12)
32 | const stakeAddress3 = createRewardAddress(stakePrivateKey3)
33 |
34 | const testName = `${faker.name.firstName()} ${faker.name.lastName()}`
35 | const testEmail = faker.internet.email()
36 |
37 | const testEmail2 = faker.internet.email()
38 |
39 | /**
40 | *
41 | * @param {String} name
42 | * @param {String} email
43 | * @param {CSL.RewardAddress} address
44 | * @param {CSL.PrivateKey} privateKey
45 | * @returns
46 | */
47 | const createRegisterUserSignature = (name, email, address, privateKey) => {
48 | const payload = {
49 | host,
50 | action: 'Sign up',
51 | name,
52 | email,
53 | timestamp: Date.now(),
54 | uri: host + '/register'
55 | }
56 |
57 | return createCOSESign1Signature(payload, address, privateKey)
58 | }
59 |
60 | const loginDetails = getAdminLoginDetails(host)
61 | let accessToken = ''
62 | let refreshTokenCookie = ''
63 | const createdID = []
64 | let verification = ''
65 | let verificationChange = ''
66 | const anotherUser = {
67 | name: 'Another user',
68 | email: 'another@user.com'
69 | }
70 |
71 | chai.use(chaiHttp)
72 |
73 | describe('*********** AUTH ***********', () => {
74 | describe('/GET /', () => {
75 | it('it should GET home API url', (done) => {
76 | chai
77 | .request(server)
78 | .get('/')
79 | .end((err, res) => {
80 | res.should.have.status(200)
81 | done()
82 | })
83 | })
84 | })
85 |
86 | describe('/GET /404url', () => {
87 | it('it should GET 404 url', (done) => {
88 | chai
89 | .request(server)
90 | .get('/404url')
91 | .end((err, res) => {
92 | res.should.have.status(404)
93 | res.body.should.be.an('object')
94 | done()
95 | })
96 | })
97 | })
98 |
99 | describe('/POST login', () => {
100 | it('it should GET token', (done) => {
101 | chai
102 | .request(server)
103 | .post('/login')
104 | .send(loginDetails)
105 | .end((err, res) => {
106 | res.should.have.status(200)
107 | res.body.should.be.an('object')
108 | res.body.should.have.property('accessToken')
109 | res.should.has.cookie('jwt')
110 | accessToken = res.body.accessToken
111 | refreshTokenCookie = res.get('Set-Cookie')[0]
112 | done()
113 | })
114 | })
115 | it('it should NOT verify the payload if it is expired', (done) => {
116 | const payload = {
117 | host: process.env.HOST,
118 | action: 'Login',
119 | uri: '/login',
120 | timestamp:
121 | Date.now() - process.env.PAYLOAD_VALIDITY_IN_SECONDS * 1000 - 1
122 | }
123 |
124 | const coseSign1Signature = createCOSESign1Signature(
125 | payload,
126 | adminStakeAddress,
127 | adminPrivateKey
128 | )
129 | const coseKey = createCOSEKey(adminPrivateKey)
130 | const loginDetailsExpiredPayload = {
131 | key: Buffer.from(coseKey.to_bytes()).toString('hex'),
132 | signature: Buffer.from(coseSign1Signature.to_bytes()).toString('hex')
133 | }
134 |
135 | chai
136 | .request(server)
137 | .post('/login')
138 | .send(loginDetailsExpiredPayload)
139 | .end((err, res) => {
140 | res.should.have.status(401)
141 | done()
142 | })
143 | })
144 |
145 | it('it should NOT verify the payload if timestamp is missing', (done) => {
146 | const payload = {
147 | host: process.env.HOST,
148 | action: 'Login'
149 | }
150 |
151 | const coseSign1Signature = createCOSESign1Signature(
152 | payload,
153 | adminStakeAddress,
154 | adminPrivateKey
155 | )
156 | const coseKey = createCOSEKey(adminPrivateKey)
157 | const loginDetailsExpiredPayload = {
158 | key: Buffer.from(coseKey.to_bytes()).toString('hex'),
159 | signature: Buffer.from(coseSign1Signature.to_bytes()).toString('hex')
160 | }
161 |
162 | chai
163 | .request(server)
164 | .post('/login')
165 | .send(loginDetailsExpiredPayload)
166 | .end((err, res) => {
167 | res.should.have.status(500)
168 | done()
169 | })
170 | })
171 | })
172 |
173 | describe('/POST register', () => {
174 | it('it should POST register user 1', (done) => {
175 | const user = {
176 | name: testName,
177 | email: testEmail,
178 | walletAddress: stakeAddress1.to_address().to_bech32(),
179 | key: Buffer.from(createCOSEKey(stakePrivateKey1).to_bytes()).toString(
180 | 'hex'
181 | ),
182 | signature: Buffer.from(
183 | createRegisterUserSignature(
184 | testName,
185 | testEmail,
186 | stakeAddress1,
187 | stakePrivateKey1
188 | ).to_bytes()
189 | ).toString('hex')
190 | }
191 | chai
192 | .request(server)
193 | .post('/register')
194 | .send(user)
195 | .end((err, res) => {
196 | res.should.have.status(201)
197 | res.body.should.be.an('object')
198 | res.body.should.include.keys('accessToken', 'user')
199 | createdID.push(res.body.user._id)
200 | verification = res.body.user.verification
201 | done()
202 | })
203 | })
204 | it('it should NOT POST a register if payload data mismatch request data', (done) => {
205 | const user = {
206 | name: faker.name.firstName(),
207 | email: faker.internet.email(),
208 | walletAddress: stakeAddress3.to_address().to_bech32(),
209 | key: Buffer.from(createCOSEKey(stakePrivateKey3).to_bytes()).toString(
210 | 'hex'
211 | ),
212 | signature: Buffer.from(
213 | createRegisterUserSignature(
214 | faker.name.firstName(),
215 | faker.internet.email(),
216 | stakeAddress3,
217 | stakePrivateKey3
218 | ).to_bytes()
219 | ).toString('hex')
220 | }
221 | chai
222 | .request(server)
223 | .post('/register')
224 | .send(user)
225 | .end((err, res) => {
226 | res.should.have.status(422)
227 | res.body.should.be.a('object')
228 | res.body.should.have.property('errors').that.has.property('msg')
229 | res.body.errors.should.have.property('msg').eql('INVALID_PAYLOAD')
230 | done()
231 | })
232 | })
233 | it('it should NOT POST a register if wallet address already exists', (done) => {
234 | const user = {
235 | name: testName,
236 | email: testEmail2,
237 | walletAddress: stakeAddress1.to_address().to_bech32(),
238 | key: Buffer.from(createCOSEKey(stakePrivateKey1).to_bytes()).toString(
239 | 'hex'
240 | ),
241 | signature: Buffer.from(
242 | createRegisterUserSignature(
243 | testName,
244 | testEmail2,
245 | stakeAddress1,
246 | stakePrivateKey1
247 | ).to_bytes()
248 | ).toString('hex')
249 | }
250 | chai
251 | .request(server)
252 | .post('/register')
253 | .send(user)
254 | .end((err, res) => {
255 | res.should.have.status(422)
256 | res.body.should.be.a('object')
257 | res.body.should.have.property('errors').that.has.property('msg')
258 | res.body.errors.should.have
259 | .property('msg')
260 | .eql('WALLET_ADDRESS_ALREADY_EXISTS')
261 | done()
262 | })
263 | })
264 | it('it should NOT POST a register if email already exists', (done) => {
265 | const user = {
266 | name: testName,
267 | email: testEmail,
268 | walletAddress: stakeAddress1.to_address().to_bech32(),
269 | key: Buffer.from(createCOSEKey(stakePrivateKey1).to_bytes()).toString(
270 | 'hex'
271 | ),
272 | signature: Buffer.from(
273 | createRegisterUserSignature(
274 | testName,
275 | testEmail,
276 | stakeAddress1,
277 | stakePrivateKey1
278 | ).to_bytes()
279 | ).toString('hex')
280 | }
281 | chai
282 | .request(server)
283 | .post('/register')
284 | .send(user)
285 | .end((err, res) => {
286 | res.should.have.status(422)
287 | res.body.should.be.a('object')
288 | res.body.should.have.property('errors').that.has.property('msg')
289 | res.body.errors.should.have
290 | .property('msg')
291 | .eql('EMAIL_ALREADY_EXISTS')
292 | done()
293 | })
294 | })
295 | })
296 |
297 | describe('/POST verify', () => {
298 | it('it should POST verify', (done) => {
299 | chai
300 | .request(server)
301 | .post('/verify')
302 | .send({
303 | id: verification
304 | })
305 | .end((err, res) => {
306 | res.should.have.status(200)
307 | res.body.should.be.an('object')
308 | res.body.should.include.keys('email', 'verified')
309 | res.body.verified.should.equal(true)
310 | done()
311 | })
312 | })
313 | })
314 |
315 | describe('/POST change', () => {
316 | it('it should POST change', (done) => {
317 | chai
318 | .request(server)
319 | .post('/change')
320 | .send({
321 | email: testEmail
322 | })
323 | .end((err, res) => {
324 | res.should.have.status(200)
325 | res.body.should.be.an('object')
326 | res.body.should.include.keys('msg', 'verification')
327 | verificationChange = res.body.verification
328 | done()
329 | })
330 | })
331 | })
332 |
333 | describe('/POST reset', () => {
334 | it('it should POST reset', (done) => {
335 | const newPrivateKey = createFakePrivateKey(12)
336 | const newAddress = createRewardAddress(newPrivateKey)
337 | const newCoseKey = createCOSEKey(newPrivateKey)
338 | const newCoseSign1 = createCOSESign1Signature(
339 | { host, action: 'Reset', timestamp: Date.now(), uri: host + '/reset' },
340 | newAddress,
341 | newPrivateKey
342 | )
343 | chai
344 | .request(server)
345 | .post('/reset')
346 | .send({
347 | id: verificationChange,
348 | walletAddress: newAddress.to_address().to_bech32(),
349 | key: Buffer.from(newCoseKey.to_bytes()).toString('hex'),
350 | signature: Buffer.from(newCoseSign1.to_bytes()).toString('hex')
351 | })
352 | .end((err, res) => {
353 | res.should.have.status(200)
354 | res.body.should.be.a('object')
355 | res.body.should.have.property('msg').eql('WALLET_CHANGED')
356 | done()
357 | })
358 | })
359 | })
360 |
361 | describe('/GET token', () => {
362 | it('it should NOT be able to consume the route since no token was sent', (done) => {
363 | chai
364 | .request(server)
365 | .get('/token')
366 | .end((err, res) => {
367 | res.should.have.status(401)
368 | done()
369 | })
370 | })
371 | it('it should GET a fresh token', (done) => {
372 | chai
373 | .request(server)
374 | .get('/token')
375 | .set('Cookie', refreshTokenCookie)
376 | .set('Authorization', `Bearer ${accessToken}`)
377 | .end((err, res) => {
378 | res.should.have.status(200)
379 | res.body.should.be.an('object')
380 | res.body.should.have.property('accessToken')
381 | done()
382 | })
383 | })
384 | })
385 |
386 | describe('/POST register', () => {
387 | it('it should POST register user 2', (done) => {
388 | const user = {
389 | name: anotherUser.name,
390 | email: anotherUser.email,
391 | walletAddress: stakeAddress2.to_address().to_bech32(),
392 | key: Buffer.from(createCOSEKey(stakePrivateKey2).to_bytes()).toString(
393 | 'hex'
394 | ),
395 | signature: Buffer.from(
396 | createRegisterUserSignature(
397 | anotherUser.name,
398 | anotherUser.email,
399 | stakeAddress2,
400 | stakePrivateKey2
401 | ).to_bytes()
402 | ).toString('hex')
403 | }
404 | chai
405 | .request(server)
406 | .post('/register')
407 | .send(user)
408 | .end((err, res) => {
409 | res.should.have.status(201)
410 | res.body.should.be.an('object')
411 | res.body.should.include.keys('accessToken', 'user')
412 | createdID.push(res.body.user._id)
413 | done()
414 | })
415 | })
416 | })
417 |
418 | describe('/GET logout', () => {
419 | it('should logout', (done) => {
420 | chai
421 | .request(server)
422 | .get('/logout')
423 | .end((err, res) => {
424 | res.should.not.has.cookie('jwt')
425 | done()
426 | })
427 | })
428 | })
429 |
430 | after((done) => {
431 | createdID.forEach((id) => {
432 | User.findByIdAndRemove(id, (err) => {
433 | if (err) {
434 | console.log(err)
435 | }
436 | })
437 | })
438 | done()
439 | })
440 | })
441 |
--------------------------------------------------------------------------------