├── 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 |

For more info visit: cardano-react-web3-skeleton

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 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](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 | [![DoItWithLovelace](https://app.doitwithlovelace.io/api/og/assets/donationButton)](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 | --------------------------------------------------------------------------------