Repository: brainhubeu/hadron
Branch: master
Commit: 28cfe5143fad
Files: 315
Total size: 283.2 KB
Directory structure:
gitextract_tdgrkyma/
├── .circleci/
│ └── config.yml
├── .dockerignore
├── .gitignore
├── .gitlab-ci.yml
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .rebuild.ts
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── entrypoint.sh
├── features/
│ ├── step_definitions/
│ │ ├── steps.ts
│ │ └── theBestSteps.ts
│ ├── support/
│ │ ├── hooks/
│ │ │ └── mock.ts
│ │ ├── scripts/
│ │ │ ├── Client.ts
│ │ │ └── Response.ts
│ │ └── world.ts
│ └── theBest.feature
├── lerna.json
├── ormconfig.json
├── package-test.sh
├── package.json
├── packages/
│ ├── hadron-auth/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── HadronAuth.ts
│ │ │ ├── IRoute.ts
│ │ │ ├── ISecurityOptions.ts
│ │ │ ├── __tests__/
│ │ │ │ ├── HadronAuth.ts
│ │ │ │ ├── hierarchyProvider.ts
│ │ │ │ └── urlGlob.ts
│ │ │ ├── constants.ts
│ │ │ ├── declarations.d.ts
│ │ │ ├── helpers/
│ │ │ │ ├── flattenDeep.ts
│ │ │ │ └── urlGlob.ts
│ │ │ ├── hierarchyProvider.ts
│ │ │ ├── password/
│ │ │ │ ├── IHashMethod.ts
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── hashMethodProvider.ts
│ │ │ │ ├── bcrypt/
│ │ │ │ │ ├── IBcryptOptions.ts
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ └── bcrypt.ts
│ │ │ │ │ └── bcrypt.ts
│ │ │ │ └── hashMethodProvider.ts
│ │ │ └── providers/
│ │ │ └── expressMiddlewareAuthorization.ts
│ │ └── tsconfig.json
│ ├── hadron-core/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ └── hadronCore.test.ts
│ │ │ ├── constants/
│ │ │ │ └── eventNames.ts
│ │ │ ├── container/
│ │ │ │ ├── __tests__/
│ │ │ │ │ ├── container.ts
│ │ │ │ │ └── containerItem.ts
│ │ │ │ ├── container.ts
│ │ │ │ ├── containerItem.ts
│ │ │ │ ├── lifecycle.ts
│ │ │ │ └── types.ts
│ │ │ ├── declarations.d.ts
│ │ │ ├── errors/
│ │ │ │ ├── IncorrectContainerKeyNameError.ts
│ │ │ │ └── LoadingPackageError.ts
│ │ │ ├── hadronCore.ts
│ │ │ └── helpers/
│ │ │ ├── __tests__/
│ │ │ │ └── isVarName.ts
│ │ │ └── isVarName.ts
│ │ └── tsconfig.json
│ ├── hadron-demo/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── declarations.d.ts
│ │ ├── entity/
│ │ │ ├── Role.ts
│ │ │ ├── Team.ts
│ │ │ ├── User.ts
│ │ │ └── validation/
│ │ │ ├── schemas.ts
│ │ │ ├── team/
│ │ │ │ ├── insertTeam.json
│ │ │ │ └── updateTeam.json
│ │ │ ├── user/
│ │ │ │ ├── insertUser.json
│ │ │ │ └── updateUser.json
│ │ │ └── validate.ts
│ │ ├── event-emitter/
│ │ │ ├── case1/
│ │ │ │ └── index.ts
│ │ │ ├── case2/
│ │ │ │ └── index.ts
│ │ │ ├── case3/
│ │ │ │ └── index.ts
│ │ │ └── config.ts
│ │ ├── express-demo/
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ ├── logger/
│ │ │ ├── adapters/
│ │ │ │ └── winstonAdapter.ts
│ │ │ └── index.ts
│ │ ├── package.json
│ │ ├── performance-tests/
│ │ │ ├── README.md
│ │ │ ├── teamService/
│ │ │ │ ├── getTeams.yml
│ │ │ │ ├── insertTeam.yml
│ │ │ │ ├── team.yml
│ │ │ │ └── updateTeam.yml
│ │ │ └── userService/
│ │ │ ├── getUsers.yml
│ │ │ ├── insertUser.yml
│ │ │ ├── updateUser.yml
│ │ │ └── user.yml
│ │ ├── routing/
│ │ │ ├── home.config.js
│ │ │ ├── nested-routes.config.js
│ │ │ ├── team.config.js
│ │ │ └── user.config.js
│ │ ├── security/
│ │ │ ├── loginRoute.ts
│ │ │ └── securedRoutesConfig.ts
│ │ ├── serialization/
│ │ │ ├── routing/
│ │ │ │ ├── index.ts
│ │ │ │ ├── princesses.ts
│ │ │ │ └── unicorns.ts
│ │ │ ├── schemas/
│ │ │ │ ├── princess.json
│ │ │ │ └── unicorn.json
│ │ │ ├── serialization-demo.ts
│ │ │ └── unicorns-and-princesses.ts
│ │ ├── services/
│ │ │ ├── teamService.ts
│ │ │ └── userService.ts
│ │ ├── tsconfig.json
│ │ └── typeorm-demo/
│ │ └── index.ts
│ ├── hadron-error-handler/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── errorHandler.ts
│ │ └── tsconfig.json
│ ├── hadron-events/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ └── eventManagerProvider.ts
│ │ │ ├── constants.ts
│ │ │ ├── eventManagerProvider.ts
│ │ │ ├── helpers/
│ │ │ │ └── functionHelper.ts
│ │ │ ├── registerProcessEvents.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── hadron-express/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── __mocks__/
│ │ │ │ │ └── routes/
│ │ │ │ │ └── route.js
│ │ │ │ ├── createContainerProxy.ts
│ │ │ │ ├── generateMiddlewares.ts
│ │ │ │ └── hadronToExpress.ts
│ │ │ ├── constants/
│ │ │ │ ├── eventNames.ts
│ │ │ │ └── routing.ts
│ │ │ ├── createContainerProxy.ts
│ │ │ ├── declarations.d.ts
│ │ │ ├── errors/
│ │ │ │ ├── CreateRouteError.ts
│ │ │ │ ├── GenerateMiddlewareError.ts
│ │ │ │ ├── InvalidRouteMethodError.ts
│ │ │ │ └── NoRouterMethodSpecifiedError.ts
│ │ │ ├── generateMiddlewares.ts
│ │ │ ├── hadronToExpress.ts
│ │ │ ├── handleResponseSpec.ts
│ │ │ ├── prepareRequest.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── hadron-file-locator/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── file-locator.ts
│ │ │ │ └── mock/
│ │ │ │ ├── app/
│ │ │ │ │ ├── config/
│ │ │ │ │ │ ├── config.js
│ │ │ │ │ │ ├── config.json
│ │ │ │ │ │ ├── config.xml
│ │ │ │ │ │ ├── config_development.js
│ │ │ │ │ │ ├── config_development.json
│ │ │ │ │ │ └── config_test.js
│ │ │ │ │ ├── ext/
│ │ │ │ │ │ ├── config.js
│ │ │ │ │ │ ├── config.json
│ │ │ │ │ │ └── config.xml
│ │ │ │ │ └── universal/
│ │ │ │ │ ├── dog.config.js
│ │ │ │ │ ├── other.json
│ │ │ │ │ ├── team.config.js
│ │ │ │ │ ├── test.js
│ │ │ │ │ └── user.config.js
│ │ │ │ └── plugins/
│ │ │ │ ├── plugin1/
│ │ │ │ │ └── config/
│ │ │ │ │ ├── config.js
│ │ │ │ │ ├── config_development.js
│ │ │ │ │ ├── config_development.ts
│ │ │ │ │ └── config_test.js
│ │ │ │ ├── plugin2/
│ │ │ │ │ └── config/
│ │ │ │ │ ├── config.js
│ │ │ │ │ ├── config_development.js
│ │ │ │ │ └── config_test.js
│ │ │ │ └── plugin3/
│ │ │ │ └── config/
│ │ │ │ ├── config.js
│ │ │ │ ├── config_development.js
│ │ │ │ └── config_test.js
│ │ │ ├── declarations.d.ts
│ │ │ ├── file-locator.ts
│ │ │ └── glob-promise.ts
│ │ └── tsconfig.json
│ ├── hadron-json-provider/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── config-json-provider.ts
│ │ │ │ ├── js-loader.ts
│ │ │ │ ├── json-loader.ts
│ │ │ │ ├── json-provider.ts
│ │ │ │ ├── mock/
│ │ │ │ │ ├── app/
│ │ │ │ │ │ ├── config/
│ │ │ │ │ │ │ ├── config.js
│ │ │ │ │ │ │ ├── config.json
│ │ │ │ │ │ │ ├── config.xml
│ │ │ │ │ │ │ ├── config_development.js
│ │ │ │ │ │ │ ├── config_development.json
│ │ │ │ │ │ │ └── config_test.js
│ │ │ │ │ │ ├── ext/
│ │ │ │ │ │ │ ├── config.js
│ │ │ │ │ │ │ ├── config.json
│ │ │ │ │ │ │ └── config.xml
│ │ │ │ │ │ └── universal/
│ │ │ │ │ │ ├── dog.config.js
│ │ │ │ │ │ ├── exportDefaultConfig.js
│ │ │ │ │ │ ├── other.json
│ │ │ │ │ │ ├── team.config.js
│ │ │ │ │ │ ├── test.js
│ │ │ │ │ │ └── user.config.js
│ │ │ │ │ └── plugins/
│ │ │ │ │ ├── plugin1/
│ │ │ │ │ │ └── config/
│ │ │ │ │ │ ├── config.js
│ │ │ │ │ │ ├── config_development.js
│ │ │ │ │ │ ├── config_development.ts
│ │ │ │ │ │ └── config_test.js
│ │ │ │ │ ├── plugin2/
│ │ │ │ │ │ └── config/
│ │ │ │ │ │ ├── config.js
│ │ │ │ │ │ ├── config_development.js
│ │ │ │ │ │ └── config_test.js
│ │ │ │ │ └── plugin3/
│ │ │ │ │ └── config/
│ │ │ │ │ ├── config.js
│ │ │ │ │ ├── config_development.js
│ │ │ │ │ └── config_test.js
│ │ │ │ └── xml-loader.ts
│ │ │ ├── declarations.d.ts
│ │ │ └── json-provider.ts
│ │ └── tsconfig.json
│ ├── hadron-logger/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ └── logger.ts
│ │ │ ├── adapters/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── bunyan.ts
│ │ │ │ ├── bunyan.ts
│ │ │ │ └── index.ts
│ │ │ ├── errors/
│ │ │ │ ├── ConfigNotDefinedError.ts
│ │ │ │ ├── CouldNotRegisterLoggerInContainerError.ts
│ │ │ │ ├── LoggerAdapterNotDefinedError.ts
│ │ │ │ └── LoggerNameIsRequiredError.ts
│ │ │ ├── logger.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── hadron-oauth/
│ │ ├── .eslintrc
│ │ ├── .gitignore
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── facebook.ts
│ │ │ │ ├── formatQueryString.ts
│ │ │ │ ├── github.ts
│ │ │ │ └── google.ts
│ │ │ ├── facebook/
│ │ │ │ ├── redirect.ts
│ │ │ │ └── token.ts
│ │ │ ├── github/
│ │ │ │ ├── redirect.ts
│ │ │ │ └── token.ts
│ │ │ ├── google/
│ │ │ │ ├── redirect.ts
│ │ │ │ └── token.ts
│ │ │ ├── types.ts
│ │ │ └── util/
│ │ │ ├── constants.ts
│ │ │ └── formatQueryString.ts
│ │ └── tsconfig.json
│ ├── hadron-serialization/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── mocks.ts
│ │ │ │ ├── schema-provider.ts
│ │ │ │ └── serializer.ts
│ │ │ ├── constants.ts
│ │ │ ├── declarations.d.ts
│ │ │ ├── schema-provider.ts
│ │ │ ├── serializer.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── hadron-typeorm/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ ├── connectionHelper.ts
│ │ │ │ └── mocks/
│ │ │ │ ├── entity/
│ │ │ │ │ ├── Team.ts
│ │ │ │ │ ├── User.ts
│ │ │ │ │ └── UserStatus.ts
│ │ │ │ └── schema/
│ │ │ │ └── User.ts
│ │ │ ├── connectionHelper.ts
│ │ │ ├── constants.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── hadron-utils/
│ │ ├── LICENSE
│ │ ├── README.md
│ │ ├── index.ts
│ │ ├── package.json
│ │ ├── src/
│ │ │ ├── __tests__/
│ │ │ │ └── getArgs.ts
│ │ │ └── getArgs.ts
│ │ └── tsconfig.json
│ └── hadron-validation/
│ ├── LICENSE
│ ├── README.md
│ ├── index.ts
│ ├── package.json
│ ├── src/
│ │ ├── __tests__/
│ │ │ ├── __mocks__/
│ │ │ │ ├── Team.ts
│ │ │ │ ├── User.ts
│ │ │ │ ├── declarations.d.ts
│ │ │ │ ├── test-schemas/
│ │ │ │ │ ├── email.json
│ │ │ │ │ ├── person.json
│ │ │ │ │ ├── team.json
│ │ │ │ │ ├── user.json
│ │ │ │ │ └── users.json
│ │ │ │ ├── test-schemas.ts
│ │ │ │ └── test-validate.ts
│ │ │ └── validate.ts
│ │ └── validator-factory.ts
│ └── tsconfig.json
├── pm2.config.js
├── scripts/
│ ├── clean
│ └── copy-tsconfig
├── test.sh
├── tools/
│ └── testSetup.ts
├── tsconfig.json
└── tslint.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
# Javascript Node CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-javascript/ for more details
#
version: 2
jobs:
build_test:
docker:
- image: circleci/node:9.11.1
working_directory: ~/repo
steps:
- checkout
- run: npm install
- run: npm run test
workflows:
version: 2
test:
jobs:
- build_test
================================================
FILE: .dockerignore
================================================
node_modules
docs
features
.npmrc
.nvmrc
================================================
FILE: .gitignore
================================================
/dist
/logs
/npm-debug.log
/node_modules
.DS_Store
.idea/
.env
.vscode
node_modules
dist
package-lock.json
.nyc_output
/**/package-lock.json
/**/node_modules
/**/dist
package-lock.json
lerna-debug.log
*.swp
================================================
FILE: .gitlab-ci.yml
================================================
image: node:8.9.0
stages:
- build
- lint
- test
before_script:
- npm install
- apt-get update
- apt-get install -y netcat
# This folder is cached between builds
# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
cache:
paths:
- node_modules/
test:
script:
- npm run build
- npm run test
lint:
script:
- npm run lint
build:
script:
- npm run build
================================================
FILE: .npmrc
================================================
save-exact=true
package-lock=false
================================================
FILE: .nvmrc
================================================
9
================================================
FILE: .prettierignore
================================================
/.history
/node_modules
/.vscode
/.idea
/dist
*/**/*.temp
*/**/*.temp.*
*/**/package.json
*/**/package-lock.json
package.json
package-lock.json
================================================
FILE: .prettierrc
================================================
{
"trailingComma": "all",
"singleQuote": true,
"arrowParens" :"always"
}
================================================
FILE: .rebuild.ts
================================================
================================================
FILE: Dockerfile
================================================
FROM keymetrics/pm2:8-alpine
COPY .env /.env
COPY entrypoint.sh /
COPY package.json /usr/src/app/package.json
WORKDIR /tmp/app
RUN apk --no-cache add git openssh-client curl
COPY . /tmp/app
RUN export $(cat /.env | xargs) && NODE_ENV=development npm install --progress=false
RUN export $(cat /.env | xargs) && /tmp/app/node_modules/.bin/tsc --project /tmp/app --outDir /usr/src/app/
RUN export $(cat /.env | xargs) && npm install --progress=false --prefix /usr/src/app/
WORKDIR /usr/src/app
CMD ["npm", "run", "start:production"]
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 Brainhub
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
[](https://circleci.com/gh/brainhubeu/hadron)
## Why?
**Hadron's purpose is to facilitate the building of Node.js applications:**
### Low-level framework-agnostic
Your application is built independently from other frameworks (Express, Koa). Hadron creates a layer between HTTP requests and your app written in plain Javascript.
Hadron abstracts away underlying request and response objects, providing simple data structures as input/output of your routes' handlers, making them simple to test and easy to deal with.
### Dependency injection
The dependency injection pattern enables you to easily change interface implementation. Hadron gives us the power to create SOLID applications.
Containers as a dependency management solution provides a convenient way to access all dependencies in functions.
### Modular structure
The modular structure enables you to add/remove packages or create your own extensions. Hadron provides a complete solution for request processing using separate packages.
Current packages:
* security management
* input validation
* database integration (through TypeORM)
* data serialization
* logging
* events handling
* CLI tool
Built with TypeScript, but it's primary target is JavaScript apps. Hadron’s API embraces current ECMAScript standards, with the cherry of good IDE support via codebase types declarations on top.
> To read more about hadron check out our article: [How to use Hadron?](https://brainhub.eu/blog/building-api-expressjs-and-hadron/)
## Installation
* Install Node.js. We recommend using the latest version, installation details on [nodejs.org](https://nodejs.org)
* Install following modules from npm:
```bash
npm install @brainhubeu/hadron-core @brainhubeu/hadron-express express --save
```
## Hello World app
Let's start with a simple Hello World app. It will give you a quick grasp of the framework.
```javascript
const hadron = require('@brainhubeu/hadron-core').default;
const express = require('express');
const port = 8080;
const expressApp = express();
const config = {
routes: {
helloWorldRoute: {
path: '/',
callback: () => 'Hello world!',
methods: ['get'],
},
},
};
hadron(expressApp, [require('@brainhubeu/hadron-express')], config).then(() => {
expressApp.listen(port, () =>
console.log(`Listening on http://localhost:${port}`),
);
});
```
## Documentation
Hadron documentation can be found at [http://hadron.pro](http://hadron.pro)
## Getting Started
#### Requirements
* Installed GIT
* Installed node.js (we recommend using [nvm](https://github.com/creationix/nvm) to run multiple versions of node).
We recommend using latest version of node. If you want to use older versions you may need to add [babel-polyfill](https://babeljs.io/docs/usage/polyfill/) to use [some features](http://node.green/).
#### Clone it
```sh
git clone git@github.com:brainhubeu/hadron.git
cd brainhub-framework-app
```
#### Install dependencies
```sh
npm install
```
#### Run development server
```sh
npm run dev
```
#### Run production server
```sh
npm start
```
## Running tests
#### All tests
```sh
npm run test
# or
PORT=8181 npm run test
```
#### Unit tests
Run unit tests for each package:
```sh
npm run test:unit
```
Run unit tests for a single package:
```sh
npm run test:package
```
#### E2E tests
```sh
PORT=8181 npm run test:e2e
```
It will run `test.sh` script which in turn, will run app, wait for it to start listening and run `npm run test:cucumber` command.
You need to provide the script with valid PORT or default (8080) will be used.
#### Linter
```sh
npm run lint # to just show linter errors and warnings
npm run lint:fix # to fix the errors and show what's left
```
### Typescript types management
**Note!** Because we're using `"noImplicitAny": true`, we are required to have a `.d.ts` file for **every** library we use. While we could set `noImplicitAny` to `false` to silence errors about missing `.d.ts` files, it is a best practice to have a `.d.ts` file for every library.
1. After installing any npm package as a dependency or dev dependency, immediately try to install the `.d.ts` file via `@types`. If it succeeds, you are done. If not, continue to next step.
2. Try to generate a `.d.ts` file with dts-gen. If it succeeds, you are done. If not, continue to next step.
3. Create a file called `.d.ts` in `types` folder.
4. Add the following code:
```ts
declare module '';
```
5. At this point everything should compile with no errors and you can either improve the types in the `.d.ts` file by following this [guide on authoring `.d.ts` files](http://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) or continue with no types.
## Lerna
1. To run `npm i` on all packages, and compile them, just run
```bash
lerna bootstrap
```
2. To run any command on all packages, just can use `exec` command.
F.e. to compile all packages, you can run
```bash
lerna exec tsc
```
3. To clean all `node_modules` in packages, run
```bash
lerna clean
```
4. To clean all `node_modules` AND `dist` directories, run
```bash
npm run clean
```
5. To add dependency between packages, run
```bash
lerna add --scope=,
```
6. To publish to npm, run
```bash
lerna publish
```
To get more command, please visit this [link](https://github.com/lerna/lerna).
================================================
FILE: docker-compose.yml
================================================
version: '2'
networks:
brainhub: ~
services:
brainhub-framework-app:
image: brainhub-framework-app
build:
context: .
container_name: fs-brainhub-framework-app
volumes:
- ./:/usr/src/app
ports:
- 8080:8080
environment:
NODE_ENV: development
command: npm run start:development
networks:
brainhub: ~
================================================
FILE: entrypoint.sh
================================================
#!/usr/bin/env bash
set -e
if [ ! -f /usr/src/app/node_modules/.lock ] && [ -f /usr/src/app/package.json ]; then
npm install --progress=false
touch /usr/src/app/node_modules/.lock
fi
exec "$@"
================================================
FILE: features/step_definitions/steps.ts
================================================
import { defineSupportCode } from 'cucumber';
import stepsSupport from '@brainhubeu/cucumber-steps';
defineSupportCode(stepsSupport);
================================================
FILE: features/step_definitions/theBestSteps.ts
================================================
import { defineSupportCode } from 'cucumber';
/* tslint:disable:only-arrow-functions ter-prefer-arrow-callback */
defineSupportCode(function({ Given, When, Then }) {
Given('brainhub is the best', function(callback) {
callback();
});
When('add {string} to {string}', function(number1, number2) {
this.result = parseInt(number1, 10) + parseInt(number2, 10);
});
Then('the result is {string}', function(result) {
if (this.result !== parseInt(result, 10)) {
throw new Error(
`Expected result to be equal ${result} but it is ${this.result}`,
);
}
});
Then('the result is null', function() {
if (this.result !== null) {
throw new Error(`Expected result to be null but it is ${this.result}`);
}
});
});
================================================
FILE: features/support/hooks/mock.ts
================================================
import { defineSupportCode } from 'cucumber';
import * as superagent from 'superagent';
import Client from '../scripts/Client';
defineSupportCode(({ Before }) => {
Before(function(scenarioResult) {
this.client = new Client(superagent);
this.client.setHost('http://localhost:8080');
});
});
================================================
FILE: features/support/scripts/Client.ts
================================================
import Response from './Response';
const METHOD = {
DELETE: 'del',
GET: 'get',
PATCH: 'patch',
POST: 'post',
PUT: 'put',
};
export default class Client {
public superagent: any;
public host: string;
public headers: object;
constructor(superagent) {
this.superagent = superagent;
this.headers = {};
Object.keys(METHOD).forEach((methodKey) => {
const method = METHOD[methodKey.toUpperCase()];
this[methodKey.toLowerCase()] = (path, body) => {
return this.createRequest(method, path, body);
};
});
}
public setHost(host) {
this.host = host;
}
public createRequest(method, path, body) {
const request = this.superagent[method.toLowerCase()](this.host + path);
this.addRequestHeaders(request);
const createdRequest =
method.toLowerCase() !== 'get' ? request.send(body) : request;
//
return createdRequest.then(
// tslint:disable:no-shadowed-variable
({ body, status }) => new Response(body, status),
({ response: { body, status } }) => new Response(body, status),
);
}
public addRequestHeaders(request) {
Object.keys(this.headers).map((name) => {
request.set(name, this.headers[name]);
});
}
public setHeader(name, value) {
this.headers[name] = value;
}
}
================================================
FILE: features/support/scripts/Response.ts
================================================
export default class Response {
public status: any;
public body: any;
constructor(body, status) {
this.body = body;
this.status = status;
}
}
================================================
FILE: features/support/world.ts
================================================
import { defineSupportCode } from 'cucumber';
/* tslint:disable:only-arrow-functions ter-prefer-arrow-callback */
defineSupportCode(function({ setWorldConstructor }) {
const customWorld = function() {
this.result = null;
};
setWorldConstructor(customWorld);
});
================================================
FILE: features/theBest.feature
================================================
Feature: The Best
As a Brainhub
We want to become best in the world
Scenario: Adding two numbers
Given brainhub is the best
When add "2" to "3"
Then the result is "5"
Scenario: Not adding two numbers
Then the result is null
================================================
FILE: lerna.json
================================================
{
"lerna": "2.9.0",
"packages": [
"packages/hadron-logger",
"packages/hadron-utils",
"packages/hadron-error-handler",
"packages/hadron-core",
"packages/hadron-file-locator",
"packages/hadron-json-provider",
"packages/hadron-serialization",
"packages/hadron-validation",
"packages/hadron-typeorm",
"packages/hadron-events",
"packages/hadron-express",
"packages/hadron-oauth",
"packages/hadron-auth",
"packages/hadron-demo"
],
"version": "independent"
}
================================================
FILE: ormconfig.json
================================================
[
{
"name": "postgres",
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "postgres",
"password":"mysecretpassword",
"database": "_test",
"synchronize": true,
"logging": false,
"autoSchemaSync": true,
"entities": [
"../../src/entity/**/*.ts"
],
"migrations": [
"../../src/migration/**/*.ts"
],
"subscribers": [
"../../src/subscriber/**/*.ts"
]
},
{
"name": "mysql",
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password":"my-secret-pw",
"database": "_test",
"synchronize": true,
"logging": false,
"autoSchemaSync": true,
"entities": [
"../../src/entity/**/*.ts"
],
"migrations": [
"../../src/migration/**/*.ts"
],
"subscribers": [
"../../src/subscriber/**/*.ts"
]
}
]
================================================
FILE: package-test.sh
================================================
#!/usr/bin/env sh
# this runs unit tests on an individual package
# Usage example: ./package-test.sh oauth
./node_modules/mocha/bin/mocha -r ts-node/register packages/hadron-$1/src/__tests__/*
================================================
FILE: package.json
================================================
{
"name": "brainhub-framework-app",
"version": "1.0.0-alpha.1",
"description": "Brainhub framework app",
"main": "dist",
"scripts": {
"start": "NODE_ENV=production NODE_PATH=./packages/hadron-demo/dist npm start --prefix packages/hadron-demo",
"start:dev": "NODE_ENV=development NODE_PATH=./packages/hadron-demo nodemon --watch 'packages/hadron-demo/**/*.ts' --watch .rebuild.ts --ignore 'packages/hadron-demo/**/*.spec.ts' --exec 'ts-node' packages/hadron-demo/index.ts",
"start:test": "NODE_ENV=development NODE_PATH=./packages/hadron-demo ts-node packages/hadron-demo",
"start:lerna": "nodemon --watch 'packages/**/*.ts' --exec 'npm run build && touch .rebuild.ts' -e ts",
"build": "lerna bootstrap",
"prestart": "npm run build",
"precommit": "lint-staged",
"lint": "lerna exec --bail=false -- tslint -c \\$LERNA_ROOT_PATH/tslint.json -p ./tsconfig.json --format stylish",
"lint:fix": "lerna exec --bail=false -- tslint -c \\$LERNA_ROOT_PATH/tslint.json -p \\packages/$LERNA_PACKAGE_NAME/tsconfig.json --format stylish",
"test": "lerna bootstrap && npm run -s test:unit && npm run -s test:e2e",
"test:unit": "NODE_ENV=test NODE_PATH=src:lib mocha -r ts-node/register tools/testSetup.ts './**/__tests__/*.ts'",
"test:package": "./package-test.sh",
"test:unit:watch": "npm run test:unit -- --reporter min --watch-extensions ts --watch",
"test:unit:coverage": "NODE_ENV=test NODE_PATH=src:lib nyc mocha -r ts-node/register tools/testSetup.ts './**/__tests__/*.ts'",
"test:e2e": "./test.sh",
"test:cucumber": "NODE_ENV=test ./node_modules/.bin/cucumberjs --compiler=ts:ts-node/register",
"docker:build": "docker build -t brainhub-framework-app .",
"format": "prettier --write \"*/**/*.ts\"",
"tsc": "lerna exec tsc --parallel",
"clean": "bash ./scripts/clean",
"postinstall": "lerna bootstrap"
},
"author": "Brainhub",
"license": "MIT",
"dependencies": {
"@types/bunyan": "1.8.4",
"body-parser": "1.18.2",
"dotenv": "4.0.0",
"mysql": "2.15.0",
"pg": "7.4.1",
"pm2": "github:Unitech/pm2#development"
},
"devDependencies": {
"@types/chai": "4.1.2",
"@types/chai-as-promised": "7.1.0",
"@types/cucumber": "4.0.1",
"@types/dotenv": "4.0.2",
"@types/fs-extra": "5.0.1",
"@types/mocha": "2.2.48",
"@types/multer": "1.3.6",
"@types/node": "9.4.6",
"@types/sinon": "4.1.3",
"@types/sinon-chai": "2.7.29",
"chai": "4.1.2",
"chai-as-promised": "7.1.1",
"concurrently": "3.5.1",
"cucumber": "3.1.0",
"@brainhubeu/cucumber-steps": "git+https://github.com/brainhubeu/cucumber-steps.git",
"dirty-chai": "2.0.1",
"husky": "0.14.3",
"lerna": "^2.9.0",
"lint-staged": "7.0.0",
"mocha": "4.0.1",
"nodemon": "1.12.1",
"nyc": "11.6.0",
"prettier": "1.11.1",
"reflect-metadata": "0.1.12",
"sinon": "4.1.2",
"sinon-chai": "2.14.0",
"superagent": "3.8.2",
"ts-node": "5.0.0",
"tslint": "5.9.1",
"tslint-config-airbnb": "5.7.0",
"tslint-config-prettier": "1.10.0",
"tslint-eslint-rules": "5.1.0",
"typescript": "2.7.2"
},
"nyc": {
"extension": [
".ts"
]
},
"lint-staged": {
"*.{ts,tsx}": [
"npm run lint:fix",
"git add"
],
"*.{ts,js,json,css,md}": [
"prettier --write",
"git add"
]
},
"repository": {
"type": "git",
"url": "https://github.com/brainhubeu/hadron.git"
},
"keywords": [
"hadron"
]
}
================================================
FILE: packages/hadron-auth/LICENSE
================================================
MIT License
Copyright (c) 2018 Brainhub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/hadron-auth/README.md
================================================
## Installation
```bash
npm install @brainhubeu/hadron-auth --save
```
## Overview
**hadron-auth** provides back-end authorization layer for routes You will choose.
### Configuration with Hadron Core
If You want to use **hadron-auth** with **hadron-core** You should also use **hadron-typeorm** and **hadron-express**.
All You need to provide is two schemas for typeorm:
* `User` (id, username, and roles many-to-many relation required)
Here is the example schema:
```javascript
// schemas/User
const userSchema = {
name: 'User',
columns: {
id: {
primary: true,
type: 'int',
generated: true,
},
username: {
type: 'varchar',
unique: true,
},
passwordHash: {
type: 'varchar',
},
addedOn: {
type: 'timestamp',
},
},
relations: {
roles: {
target: 'Role',
type: 'many-to-many',
joinTable: {
name: 'user_role',
},
onDelete: 'CASCADE',
},
},
};
module.exports = userSchema;
```
* `Role` (id and name required)
Example schema:
```javascript
// schemas/Role
const roleSchema = {
name: 'Role',
columns: {
id: {
primary: true,
type: 'int',
generated: true,
},
name: {
type: 'varchar',
unique: true,
},
addedOn: {
type: 'timestamp',
},
},
};
module.exports = roleSchema;
```
Don't forget to add schemas to Your database config, example below:
```javascript
// config/db.js
const userSchema = require('../schemas/User');
const roleSchema = require('../schemas/Role');
const connection = {
name: 'mysql-connection',
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'my-secret-pw',
database: 'done-it',
entitySchemas: [roleSchema, userSchema],
synchronize: true,
};
module.exports = connection,
```
Now You need to prepare Your hadron configuration file, where You can add secured routes, for example:
```javascript
// index.js
const config = {
routes: {
helloWorldRoute: {
path: '/',
methods: ['GET'],
callback: () => 'Hello World',
},
adminRoute: {
path: '/admin',
methods: ['GET'],
callback: () => 'Hello Admin',
},
userRoute: {
path: '/user',
methods: ['GET'],
callback: () => 'Hello User',
},
},
securedRoutes: [
{
path: '/admin/*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
roles: 'Admin',
},
{
path: '/user/*',
roles: ['Admin', 'User'],
},
],
};
```
Finally You need to add **hadron-auth** to hadron initialization method:
```javascript
const hadron = require('@brainhubeu/hadron-core').default;
const hadronExpress = require('@brainhubeu/hadron-express');
const hadronTypeOrm = require('@brainhubeu/hadron-typeorm');
const hadronAuth = require('@brainhubeu/hadron-auth');
const express = require('express');
const expressApp = express();
const hadronInit = async () => {
const config = {
routes: {
helloWorldRoute: {
path: '/',
methods: ['GET'],
callback: () => 'Hello World',
},
adminRoute: {
path: '/admin',
methods: ['GET'],
callback: () => 'Hello Admin',
},
userRoute: {
path: '/user',
methods: ['GET'],
callback: () => 'Hello User',
},
},
securedRoutes: [
{
path: '/admin/*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
roles: 'Admin',
},
{
path: '/user/*',
roles: ['Admin', 'User'],
},
],
};
const container = await hadron(
expressApp,
[hadronAuth, hadronExpress, hadronTypeOrm],
config,
);
};
```
---
Warning, You should pass hadronAuth as first to hadron packages array.
---
Now Your routes are secured, by default, **hadron-auth** authorize user by **JWT Token**, passed as `Authorization` header.
### Creating custom auth middleware
You can pass Your own function in hadron configuration to check if a user is authorized to the secured route.
Here is the skeleton for the authorization middleware:
```javascript
const authorizationMiddleware = (container) => {
return (req, res, next) => {};
};
```
**hadron-auth** provides `isAllowed` function, to check if a user is allowed to specified route:
```javascript
isAllowed(path, method, user, allRoles);
```
Where:
* `path` - path to secured route, for example /api/admin/1
* `method` - HTTP method
* `user` - User object, which need to contain roles
* `allRoles` - All roles stored in database (only role names)
Here is an example authorization middleware:
```javascript
const jwt = require('jsonwebtoken');
const { isRouteSecure, isAllowed } = require('@brainhubeu/hadron-auth');
const errorResponse = {
message: 'Unauthorized',
};
const expressMiddlewareAuthorization = (container) => {
return async (req, res, next) => {
try {
if (!isRouteSecure(req.path)) {
return next();
}
const userRepository = container.take('userRepository');
const roleRepository = container.take('roleRepository');
const token = req.headers.authorization;
const decoded: any = jwt.decode(token);
const user = await userRepository.findOne({
where: { id: decoded.id },
relations: ['roles'],
});
if (!user) {
return res.status(403).json({ error: errorResponse });
}
const allRoles = await roleRepository.find();
if (
isAllowed(req.path, req.method, user, allRoles.map((role) => role.name))
) {
return next();
}
return res.status(403).json({ error: errorResponse });
} catch (error) {
return res.status(403).json({ error: errorResponse });
}
};
};
module.exports = expressMiddlewareAuthorization;
```
To use it, You need to pass an expressMiddlewareAuthorization function as `authorizationMiddleware` key in hadron config.
```javascript
const config = {
authorizationMiddleware: YourCustomFunction,
};
```
### Usage:
```javascript
const securedRoutes = [
{
path: '/api/**',
methods: ['GET'],
roles: ['Admin', 'User'],
},
{
path: '/api/**',
methods: ['POST', 'PUT', 'DELETE'],
roles: 'Admin',
},
{
path: '/admin/*',
roles: 'Admin',
},
{
path: 'product/info',
methods: ['GET'],
roles: [['Admin', 'User'], 'Manager'],
},
];
```
* `Path` - here we can specify the route path we want to secure, we can use a static path like `/api/admin/tasks` or by pattern:
* `/api/admin/*` - route after `/api/admin/` is secured, for example `/api/admin/tasks` - is secured, but `/api/admin/tasks/5` - will be not secured
* `/api/admin/**` - every route after `/api/admin` is secured
* `methods` - an array of strings, where You can pass role names, if You will not provide any role, then the route is secured and user with **ANY** role can access this if a user does not have any role he will be unauthorized.
* `roles` - here You can pass single role name, an array of role names or array of arrays of strings, which add some logic functionality, for example, if we declare:
```javascript
roles[(['Admin', 'User'], 'Manager')];
```
The user needs Admin **AND** User **OR** Manager role to access the route.
================================================
FILE: packages/hadron-auth/index.ts
================================================
import { IUser, IRole } from './src/hierarchyProvider';
import * as bcrypt from './src/password/bcrypt/bcrypt';
import * as HadronAuth from './src/HadronAuth';
export const register = (container: any, config: any) => {
HadronAuth.register(container, config);
};
export default HadronAuth;
export { IUser, IRole, bcrypt };
================================================
FILE: packages/hadron-auth/package.json
================================================
{
"name": "@brainhubeu/hadron-auth",
"version": "0.0.2",
"description": "Security package for hadron",
"main": "dist/index.js",
"files": [
"dist",
"LICENSE"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "tsc"
},
"keywords": [
"security",
"hadron"
],
"author": "Brainhub",
"license": "MIT",
"dependencies": {
"@types/bcrypt": "^2.0.0",
"@types/jsonwebtoken": "^7.2.7",
"bcrypt": "2.0.0",
"glob-to-regexp": "^0.4.0",
"jsonwebtoken": "8.3.0"
},
"publishConfig": {
"access": "public"
}
}
================================================
FILE: packages/hadron-auth/src/HadronAuth.ts
================================================
import { IRoute, IMethod } from './IRoute';
import urlGlob, { convertToPattern } from './helpers/urlGlob';
import { IUser } from '..';
import { isUserGranted } from './hierarchyProvider';
import expressMiddlewareAuthorization from './providers/expressMiddlewareAuthorization';
let routes: IRoute[] = [];
export interface ISecuredRoute {
path: string;
methods?: string[];
roles: string | Array;
}
export const getExistsingRoute = (path: string, routes: IRoute[]): IRoute => {
for (const route of routes) {
if (route.path === path) {
return route;
}
}
return null;
};
export const getRoleArray = (roles: string | Array) => {
const arr: any[] = [];
if (typeof roles === 'string') {
arr.push(roles);
} else {
roles.forEach((role) => arr.push(role));
}
return [...new Set(arr)];
};
export const getMethodsForExistsingRoute = (
existingRoute: IRoute,
methods: string[],
roles: string | Array,
): IMethod[] => {
const newMethods: IMethod[] = methods.map((method) => ({
allowedRoles: getRoleArray(roles),
name: method,
}));
const existingMethods = existingRoute.methods.filter(
(method) => newMethods.map((el) => el.name).indexOf(method.name) >= 0,
);
const nonExistingMethods = newMethods.filter(
(method) =>
existingRoute.methods.map((el) => el.name).indexOf(method.name) === -1,
);
existingMethods.forEach((method) => {
method.allowedRoles = [
...new Set(method.allowedRoles.concat(getRoleArray(roles))),
];
});
let methodsFromRoute = existingRoute.methods.filter(
(method) =>
existingMethods.map((el) => el.name).indexOf(method.name) === -1,
);
methodsFromRoute = methodsFromRoute.concat(existingMethods);
return [...methodsFromRoute, ...nonExistingMethods];
};
export const createNewRoute = (
path: string,
methods: string[] = [],
roles: string | Array,
) => {
const methodsForRoute: IMethod[] = [];
if (methods.length > 0) {
methods = [...new Set(methods)];
methods.forEach((methodName) => {
methodsForRoute.push({
name: methodName,
allowedRoles: getRoleArray(roles),
});
});
} else {
methodsForRoute.push({
name: '*',
allowedRoles: getRoleArray(roles),
});
}
const route: IRoute = {
path: convertToPattern(path),
methods: methodsForRoute,
};
return route;
};
export const initRoutes = (securedRoutes: ISecuredRoute[]): IRoute[] => {
const routes: IRoute[] = [];
securedRoutes.forEach((route) => {
const existingRoute = getExistsingRoute(
convertToPattern(route.path),
routes,
);
if (existingRoute) {
existingRoute.methods = getMethodsForExistsingRoute(
existingRoute,
route.methods,
route.roles,
);
} else {
routes.push(createNewRoute(route.path, route.methods, route.roles));
}
});
return routes;
};
export const register = (container: any, config: any) => {
if (config.authSecret) {
container.register('authSecret', config.authSecret);
}
routes = initRoutes(config.securedRoutes || []);
const server = container.take('server');
server.use(
config.authorizationMiddleware
? config.authorizationMiddleware(container)
: expressMiddlewareAuthorization(container),
);
};
export const getRouteFromPath = (path: string, routes: IRoute[]): IRoute => {
const route = routes.filter((r) => urlGlob(r.path, path));
if (route.length === 0) return null;
return route[0];
};
export const isRouteNotSecure = (path: string) => {
console.warn("HadronAuth: isRouteNotSecure is being deprecated. Use isRouteSecure instead.");
return getRouteFromPath(path, routes) === null;
}
export const isRouteSecure = (path: string) =>
getRouteFromPath(path, routes) !== null;
export const isAllowed = (
path: string,
allowedMethod: string,
user: IUser,
allRoles: string[],
): boolean => {
try {
const route = getRouteFromPath(path, routes);
let isGranted = false;
route.methods.forEach((method) => {
if (
method.name === '*' ||
method.name.toLowerCase() === allowedMethod.toLowerCase()
) {
if (method.allowedRoles.includes('*') && user.roles.length > 0) {
isGranted = true;
} else {
isGranted = isUserGranted(user, method.allowedRoles, {
ALL: allRoles,
});
}
}
});
return isGranted;
} catch (error) {
throw new Error('Unauthorized');
}
};
================================================
FILE: packages/hadron-auth/src/IRoute.ts
================================================
export interface IRoute {
path: string;
methods: IMethod[];
}
export interface IMethod {
name: string;
allowedRoles: Array;
}
================================================
FILE: packages/hadron-auth/src/ISecurityOptions.ts
================================================
import { IRolesMap } from './hierarchyProvider';
import IHashMethod from './password/IHashMethod';
export default interface ISecurityOptions {
roles: IRolesMap | string[];
hash?: {
method: IHashMethod | string;
options: any;
};
};
================================================
FILE: packages/hadron-auth/src/__tests__/HadronAuth.ts
================================================
import { expect } from 'chai';
import {
initRoutes,
ISecuredRoute,
getRouteFromPath,
getExistsingRoute,
createNewRoute,
getMethodsForExistsingRoute,
} from '../HadronAuth';
import { convertToPattern } from '../helpers/urlGlob';
describe('Hadron Authorization module', () => {
it('initRoutes should return array of prepared IRoute from ISecuredRoute array', () => {
const securedRoutes: ISecuredRoute[] = [
{
path: '/admin/**',
methods: ['POST', 'PUT'],
roles: ['Admin', 'User'],
},
];
const routes = initRoutes(securedRoutes);
expect(routes).to.be.instanceOf(Array);
expect(Object.keys(routes[0])).to.be.deep.equal(['path', 'methods']);
});
it('initRoutes should join two same paths with different methods/roles', () => {
const securedRoutes: ISecuredRoute[] = [
{
path: '/admin/**',
methods: ['POST', 'PUT'],
roles: 'Admin',
},
{
path: '/admin/**',
methods: ['GET'],
roles: 'User',
},
];
const routes = initRoutes(securedRoutes);
expect(routes.length).to.be.equal(1);
expect(Object.keys(routes[0])).to.be.deep.equal(['path', 'methods']);
});
it('initRoutes should convert path from securedRoute to regex pattern', () => {
const securedRoutes: ISecuredRoute[] = [
{
path: '/admin/*',
methods: ['POST'],
roles: 'Admin',
},
];
const routes = initRoutes(securedRoutes);
const matcher = new RegExp(routes[0].path);
const testPath = '/admin/1';
expect(matcher.test('admin/1')).to.be.equal(true);
});
describe('getRouteFromPath', () => {
const routes = initRoutes([
{
path: '/admin/*',
methods: ['POST'],
roles: 'Admin',
},
{
path: '/user/*',
methods: ['POST'],
roles: 'Admin',
},
]);
it('getRouteFromPath should return IRoute if path is already exists by regex in routes', () => {
expect(getRouteFromPath('/admin/1', routes)).to.be.an('object');
});
it('getRouteFromPath should return null if path does not exists by regex in routes', () => {
expect(getRouteFromPath('/qwe', routes)).to.be.equal(null);
});
});
describe('getExistingRoute', () => {
const routes = initRoutes([
{
path: '/admin/*',
methods: ['POST'],
roles: 'Admin',
},
{
path: '/user/*',
methods: ['POST'],
roles: 'Admin',
},
]);
it('getExistingRoute should return route if route exists in array', () => {
expect(
getExistsingRoute(convertToPattern('/admin/*'), routes),
).to.be.equal(routes[0]);
});
it('getExistingRoute should return null if route does not exists in array', () => {
expect(
getExistsingRoute(convertToPattern('/guest/*'), routes),
).to.be.equal(null);
});
});
describe('createNewRoute', () => {
const path = '/admin/**';
const methods: string[] = [];
const roles = 'Admin';
const route = createNewRoute(path, methods, roles);
it('createNewRoute should create IRoute from path, methods and roles', () => {
expect(Object.keys(route)).to.be.deep.equal(['path', 'methods']);
});
it('createNewRoute should create regex pattern from path string', () => {
expect(route.path).to.be.equal(convertToPattern('/admin/**'));
});
it('createNewRoute should push "*" to methods array if array is empty', () => {
expect(route.methods[0].name).to.be.equal('*');
});
it('createNewRoute should create IMethod with name and roles in route object', () => {
expect(Object.keys(route.methods[0])).to.be.deep.equal([
'name',
'allowedRoles',
]);
});
});
describe('getMethodsForExistingRoute', () => {
const path = '/admin/**';
const methods: string[] = ['GET'];
const roles = 'Admin';
const route = createNewRoute(path, methods, roles);
it('getMethodsForExistsinRoute should push new methods to existing route', () => {
const newMethods = getMethodsForExistsingRoute(
route,
['POST', 'PUT'],
['User', 'Admin', 'Guest'],
);
expect(newMethods.length).to.be.equal(3);
});
it('getMethodsForExistsinRoute should push new roles to existing method', () => {
const newMethods = getMethodsForExistsingRoute(
route,
['GET'],
['User', 'Guest'],
);
expect(newMethods.length).to.be.equal(1);
});
});
});
================================================
FILE: packages/hadron-auth/src/__tests__/hierarchyProvider.ts
================================================
import hierarchyProvider, {
fillMissingRoles,
checkRole,
checkRoles,
getDeeperRoles,
excludeRoles,
} from '../hierarchyProvider';
import { expect } from 'chai';
describe('hierarchyProvider', () => {
const basicHierarchy = {
ADMIN: ['USER', 'MANAGER'],
MANAGER: ['USER'],
GUEST: [],
};
describe('fillMissingRoles', () => {
it('should add missing roles from hierarchy', () => {
const roles = {
ROLE1: ['ROLE2', 'ROLE3'],
ROLE2: ['ROLE4'],
};
expect(fillMissingRoles(roles)).to.contain.keys([
'ROLE1',
'ROLE2',
'ROLE3',
'ROLE4',
]);
});
it('should keep hierarchy of previously mentioned role', () => {
const roles = {
ROLE1: ['ROLE2', 'ROLE3'],
ROLE2: ['ROLE4'],
};
expect(fillMissingRoles(roles).ROLE2).to.eql(['ROLE4']);
});
it('should handle array as roles', () => {
const roles = ['ROLE1', 'ROLE2'];
expect(fillMissingRoles(roles)).to.contain.keys(['ROLE1', 'ROLE2']);
});
});
describe('checkRole', () => {
it('should return true if role is exactly the same as requested', () =>
expect(checkRole(['ADMIN'], 'ADMIN', basicHierarchy)).to.be.eql(true));
it('should return false if role is not exactly the same and does not contain it in hierarchy', () =>
expect(checkRole(['USER'], 'MANAGER', basicHierarchy)).to.be.eql(false));
it('should return true if role is not exactly the same but does contain it in hierarchy', () =>
expect(checkRole(['MANAGER'], 'USER', basicHierarchy)).to.be.eql(true));
it('should return true if one of roles is matching', () =>
expect(
checkRole(['MANAGER', 'USER'], 'MANAGER', basicHierarchy),
).to.be.eql(true));
it('should return false if none of roles is matching', () =>
expect(checkRole(['MANAGER', 'USER'], 'ADMIN', basicHierarchy)).to.be.eql(
false,
));
it('should return true if role is in hierarchy of user roles', () =>
expect(checkRole(['ADMIN', 'USER'], 'MANAGER', basicHierarchy)).to.be.eql(
true,
));
it('should return true if role is in deeper hierarchy of user roles', () => {
const roles = {
...basicHierarchy,
MANAGER: ['TESTUSER', 'USER'],
};
return expect(checkRole(['ADMIN'], 'TESTUSER', roles)).to.be.eql(true);
});
it('should handle recurrent hierarchy', () => {
const roles = {
ADMIN: ['MANAGER'],
MANAGER: ['ADMIN'],
};
return expect(checkRole(['ADMIN'], 'TESTUSER', roles)).to.be.eql(false);
});
it("should return false, if role doesn't exists", () =>
expect(checkRole(['ADMIN'], 'UNEXISTING_ROLE', basicHierarchy)).to.be.eql(
false,
));
});
describe('checkRoles', () => {
it('should return true if role is one of given', () =>
expect(
checkRoles(['MANAGER'], ['MANAGER', 'ADMIN'], basicHierarchy),
).to.be.eql(true));
it('should return false, if none of roles is matching given one', () =>
expect(
checkRoles(['USER'], ['MANAGER', 'ADMIN'], basicHierarchy),
).to.be.eql(false));
it('should return true if user has both roles', () =>
expect(
checkRoles(
['MANAGER', 'ADMIN'],
['MANAGER', 'ADMIN'],
basicHierarchy,
true,
),
).to.be.eql(true));
it("should return false if user doesn't have one of roles", () =>
expect(
checkRoles(['MANAGER'], ['MANAGER', 'ADMIN'], basicHierarchy, true),
).to.be.eql(false));
it('should return true if role contains both required roles', () =>
expect(
checkRoles(['ADMIN'], ['MANAGER', 'USER'], basicHierarchy, true),
).to.be.eql(true));
});
describe('getDeeperRoles', () => {
it('should return roles that are related to given one', () =>
expect(getDeeperRoles(['ADMIN'], basicHierarchy)).to.has.members([
'MANAGER',
'USER',
]));
it('should return roles that are related to all given roles', () => {
const hierarchy = {
ROLE1: ['ROLE2', 'ROLE3'],
ROLE4: ['ROLE5'],
};
return expect(
getDeeperRoles(['ROLE1', 'ROLE4'], hierarchy),
).to.has.members(['ROLE2', 'ROLE3', 'ROLE5']);
});
it('should return roles that are related to all given roles distinctly', () => {
const hierarchy = {
ROLE1: ['ROLE2', 'ROLE3'],
ROLE4: ['ROLE5', 'ROLE3'],
};
return expect(
getDeeperRoles(['ROLE1', 'ROLE4'], hierarchy),
).to.has.members(['ROLE2', 'ROLE3', 'ROLE5']);
});
});
describe('excludeRoles', () => {
it('should remove given role from list', () =>
expect(excludeRoles(['ADMIN'], basicHierarchy)).to.contain.keys([
'MANAGER',
'GUEST',
]));
it('should remove given roles from list', () =>
expect(
excludeRoles(['ADMIN', 'MANAGER'], basicHierarchy),
).to.contain.keys(['GUEST']));
});
describe('isGranted', () => {
const { isGranted } = hierarchyProvider(basicHierarchy);
it('should pass if single matching role has been provided', () =>
expect(isGranted(['ADMIN'], 'ADMIN')).to.be.eql(true));
it('should fail if single not matching role has been provided', () =>
expect(isGranted(['ADMIN'], 'GUEST')).to.be.eql(false));
it('should pass if array of roles has been provided, with single matching one', () =>
expect(isGranted(['ADMIN'], ['ADMIN', 'GUEST'])).to.be.eql(true));
it('should fail if array of roles has been provided, with none that matches', () =>
expect(isGranted(['MANAGER'], ['ADMIN', 'GUEST'])).to.be.eql(false));
it('should pass if array of arrays of roles has been provided and all of them are matching', () =>
expect(isGranted(['ADMIN', 'GUEST'], [['ADMIN', 'GUEST']])).to.be.eql(
true,
));
it('should fail if array of arrays of roles has been provided and one of them is not matching', () =>
expect(isGranted(['ADMIN'], [['ADMIN', 'GUEST']])).to.be.eql(false));
it('should pass if array of arrays and single role has been provided and one of them are matching', () =>
expect(isGranted(['MANAGER'], [['ADMIN', 'GUEST'], 'MANAGER'])).to.be.eql(
true,
));
it('should fail if array of arrays of roles has been provided and none of them are matching', () =>
expect(
isGranted(['MANAGER'], [['ADMIN', 'GUEST'], ['MANAGER', 'GUEST']]),
).to.be.eql(false));
it('should pass if array of arrays of roles has been provided and one of them are matching', () =>
expect(
isGranted(
['MANAGER', 'GUEST'],
[['ADMIN', 'GUEST'], ['MANAGER', 'GUEST']],
),
).to.be.eql(true));
});
});
================================================
FILE: packages/hadron-auth/src/__tests__/urlGlob.ts
================================================
import { expect } from 'chai';
import urlGlob from '../helpers/urlGlob';
describe('Glob URL pattern', () => {
it('should return true if URL: /api/admin/qwe is valid for PATTERN: /api/admin/*', () => {
expect(urlGlob('/api/admin/*', '/api/admin/qwe')).to.be.equal(true);
});
it('should return false if URL: /api/adm/qwe is invalid for PATTERN: /api/admin/*', () => {
expect(urlGlob('/api/admin/*', '/api/adm/qwe')).to.be.equal(false);
});
it('should return false if URL: /api/admin/qwe/1 is invalid for PATTERN: /api/admin/*', () => {
expect(urlGlob('/api/admin/*', '/api/adm/qwe')).to.be.equal(false);
});
it('should return true if URL: /api/admin/tasks/1 is valid for PATTERN: /api/admin/**', () => {
expect(urlGlob('/api/admin/**', '/api/admin/tasks/1')).to.be.equal(true);
});
it('should return true if URL: /api/something/admin/more/user/in/manager/string for PATTERN: /api/**/admin/**/manager/**', () => {
expect(
urlGlob(
'/api/**/admin/**/user/**/manager/**',
'/api/something/admin/more/user/in/manager/string',
),
).to.be.equal(true);
});
it('should return false if URL: /api/something/admin/more/user/in/mage/string for PATTERN: /api/**/admin/**/manager/**', () => {
expect(
urlGlob(
'/api/**/admin/**/user/**/manager/**',
'/api/something/admin/more/user/in/mage/string',
),
).to.be.equal(false);
});
it('should return false if URL: /api/something/admin/more is invalid for PATTERNL /api/**/user/**', () => {
expect(urlGlob('/api/**/admin/**', '/api/something/user/more')).to.be.equal(
false,
);
});
it('should return true if URL: /api/admin is valid for strict PATTERN /api/admin', () => {
expect(urlGlob('/api/admin', '/api/admin')).to.be.equal(true);
});
it('should return false if URL: /api/adm or /api.admin/1 is invalid for strict PATTERN /api/admin', () => {
expect(urlGlob('/api/admin', '/api/adm')).to.be.equal(false);
expect(urlGlob('/api/admin', '/api/admin/1')).to.be.equal(false);
});
});
================================================
FILE: packages/hadron-auth/src/constants.ts
================================================
export const CONTAINER_NAME = 'isGranted';
================================================
FILE: packages/hadron-auth/src/declarations.d.ts
================================================
declare module 'glob-to-regexp';
================================================
FILE: packages/hadron-auth/src/helpers/flattenDeep.ts
================================================
const flattenDeep = (arr: any[]): any[] =>
Array.isArray(arr)
? arr.reduce((a, b) => [...flattenDeep(a), ...flattenDeep(b)], [])
: [arr];
export default flattenDeep;
================================================
FILE: packages/hadron-auth/src/helpers/urlGlob.ts
================================================
const countStars = (input: string) => {
try {
return input.match(/\*\*/g).length;
} catch (error) {
return 0;
}
};
export const convertToPattern = (url: string): string => {
const regexp = url[0] === '/' ? url.substring(1) : url;
if (url.endsWith('/*') && countStars(url) === 0) {
return `^/?${regexp.replace(/\/\*/g, '($|/$|/[^/]*$)')}$`;
}
if (url.endsWith('/**') && countStars(url) === 1) {
return `^/?${regexp.replace(/\/\*\*/g, '($|/.*$)')}$`;
}
if (countStars(url) === 0) {
return `^/?${url}$`;
}
return convertToPattern(regexp.replace('**', '[^/]*'));
};
const urlGlob = (pattern: string, input: string): boolean => {
const re = new RegExp(convertToPattern(pattern));
return re.test(input);
};
export default urlGlob;
================================================
FILE: packages/hadron-auth/src/hierarchyProvider.ts
================================================
export interface IRolesMap {
[s: string]: string[];
}
export interface IRole {
id: number | string;
name: string;
}
export interface IUser {
id: number | string;
username: string;
passwordHash: string;
roles: IRole[];
}
/**
* Function adds roles from dependency of other roles, to make sure that all roles are available
* @param roles available roles with all "dependent" ones
* @returns {IRolesMap}
*/
export function fillMissingRoles(roles: IRolesMap | string[]): IRolesMap {
if (roles instanceof Array) {
return roles.reduce(
(accumulator: IRolesMap, role: string) => ({
...accumulator,
[role]: [],
}),
{},
);
}
return Object.entries(roles).reduce(
(accumulator: IRolesMap, [key, value]: [string, string[]]) => {
accumulator[key] = value;
value.forEach((role: string) => {
if (!accumulator[role]) {
accumulator[role] = [];
}
});
return accumulator;
},
{} as IRolesMap,
);
}
/**
* Get array of all roles below in hierarchy distinctly
* @param userRoles
* @param availableRoles
* @returns {string[]}
*/
export function getDeeperRoles(userRoles: string[], availableRoles: IRolesMap) {
return userRoles
.filter((role: string) => !!availableRoles[role])
.reduce(
(accumulator: string[], role: string) => [
...accumulator,
...availableRoles[role].filter(
(roleToAdd) => !accumulator.includes(roleToAdd),
),
],
[],
);
}
/**
* Returns array of all given roles, without ones given in first parameter
* @param userRoles
* @param availableRoles
* @returns {string[]}
*/
export function excludeRoles(userRoles: string[], availableRoles: IRolesMap) {
return Object.entries(availableRoles)
.filter(([key, value]: [string, any]) => userRoles.indexOf(key) < 0)
.reduce(
(accumulator: object, [key, value]: [string, any]) => ({
...accumulator,
[key]: value,
}),
{},
);
}
/**
* Checks if user role contains required role
* @param userRoles
* @param requiredRole
* @param availableRoles
* @returns {boolean}
*/
export function checkRole(
userRoles: string[],
requiredRole: string,
availableRoles: IRolesMap,
): boolean {
if (userRoles.length <= 0) {
return false;
}
if (userRoles.indexOf(requiredRole) >= 0) {
return true;
}
return checkRole(
getDeeperRoles(userRoles, availableRoles),
requiredRole,
// excludes currently checked roles to avoid endless recurrency
excludeRoles(userRoles, availableRoles),
);
}
/**
* Checks list of roles
*
* @param userRoles
* @param requiredRoles
* @param availableRoles
* @param exact specifies if user needs all roles from requiredRoles (true), or only one of them (false)
* @return {boolean}
*/
export function checkRoles(
userRoles: string[],
requiredRoles: string[],
availableRoles: IRolesMap,
exact = false,
): boolean {
if (userRoles.length <= 0) {
return false;
}
return requiredRoles
.map(
(role) =>
typeof role === 'object'
? checkRoles(userRoles, role, availableRoles, true)
: checkRole(userRoles, role, availableRoles),
)
.reduce(
(accumulator, currentValue) =>
exact ? accumulator && currentValue : accumulator || currentValue,
);
}
/**
* Returns true if given roles are matching expected roles in hierarchy
* @param {IUser} user
* @param roles
* @param allRoles
* @returns {boolean}
*/
export function isGranted(
userRoles: string[],
roles: any,
allRoles: IRolesMap,
): boolean {
if (typeof roles === 'string') {
return checkRole(userRoles, roles, allRoles);
}
if (typeof roles === 'object') {
return checkRoles(userRoles, roles, allRoles);
}
throw new Error('Unknown role type');
}
/**
* Returns true if user has matching role in hierarchy
* @param {IUser} user
* @param roles
* @param allRoles
* @returns {boolean}
*/
export function isUserGranted(
user: IUser,
roles: any,
allRoles: IRolesMap,
): boolean {
return isGranted(user.roles.map((role) => role.name), roles, allRoles);
}
/**
* Provider for hierarchy manager
* @param rolesHierarchy
* @returns {function}
*/
export default function hierarchyProvider(
rolesHierarchy: IRolesMap | string[],
) {
const fullRoles: IRolesMap = fillMissingRoles(rolesHierarchy);
return {
isGranted: (userRoles: string[], roles: any) =>
isGranted(userRoles, roles, fullRoles),
isUserGranted: (user: IUser, roles: any) =>
isUserGranted(user, roles, fullRoles),
};
}
================================================
FILE: packages/hadron-auth/src/password/IHashMethod.ts
================================================
export default interface IHashMethod {
hash(password: string, salt?: string, options?: object): Promise;
compare(
userPassword: string,
hashedPassword: string,
salt?: string,
options?: object,
): Promise;
};
================================================
FILE: packages/hadron-auth/src/password/__tests__/hashMethodProvider.ts
================================================
import hashMethodProvider from '../hashMethodProvider';
import { expect } from 'chai';
import * as sinon from 'sinon';
import ISecurityOptions from '../../ISecurityOptions';
import IHashMethod from '../IHashMethod';
describe('hashMethodProvider', () => {
const defaultOptions = {
roles: [],
} as ISecurityOptions;
it('should return bcrypt on default', () => {
const bcryptSpy = sinon.spy();
hashMethodProvider(defaultOptions, { bcrypt: bcryptSpy });
return expect(bcryptSpy.calledOnce).to.be.equal(true);
});
it('should return method, which name was defined in config', () => {
const testSpy = sinon.spy();
const options = {
...defaultOptions,
hash: {
method: 'testMethod',
},
} as ISecurityOptions;
hashMethodProvider(options, { testMethod: testSpy });
return expect(testSpy.calledOnce).to.be.equal(true);
});
it("should return default method, if method name from config doesn't exists", () => {
const bcryptSpy = sinon.spy();
const options = {
...defaultOptions,
hash: {
method: 'testMethod',
},
} as ISecurityOptions;
hashMethodProvider(options, { bcrypt: bcryptSpy });
return expect(bcryptSpy.calledOnce).to.be.equal(true);
});
it('should return method defined in config', () => {
const hashStub = sinon.spy();
const method = {
hash: hashStub,
compare: (userPassword: string, hashedPassword: string) =>
Promise.resolve(true),
} as IHashMethod;
const options = {
...defaultOptions,
hash: {
method,
},
} as ISecurityOptions;
hashMethodProvider(options).hash('smth');
return expect(hashStub.calledOnce).to.be.equal(true);
});
it('should return default method, if hash method defined in config is incorrect', () => {
const bcryptSpy = sinon.spy();
const method = {
hassh: (userPassword: string, hashedPassword: string) =>
Promise.resolve(true),
compaare: (userPassword: string, hashedPassword: string) =>
Promise.resolve(true),
};
const options = {
...defaultOptions,
hash: {
method,
},
};
hashMethodProvider(options as any, { bcrypt: bcryptSpy });
return expect(bcryptSpy.calledOnce).to.be.equal(true);
});
it('should pass options to hash method call of hashMethod', () => {
const hashStub = sinon.spy();
const method = {
hash: hashStub,
compare: (userPassword: string, hashedPassword: string) =>
Promise.resolve(true),
} as IHashMethod;
const options = {
...defaultOptions,
hash: {
method,
options: { lorem: 'ipsum' },
},
} as ISecurityOptions;
hashMethodProvider(options).hash('smth');
return expect(
hashStub.calledWith('smth', undefined, { lorem: 'ipsum' }),
).to.be.equal(true);
});
it('should pass options to compare method call of hashMethod', () => {
const compareStub = sinon.spy();
const method = {
hash: (userPassword: string) => Promise.resolve('password'),
compare: compareStub,
} as IHashMethod;
const options = {
...defaultOptions,
hash: {
method,
options: { lorem: 'ipsum' },
},
} as ISecurityOptions;
hashMethodProvider(options).compare('simple', 'hashed');
return expect(
compareStub.calledWith('simple', 'hashed', undefined, { lorem: 'ipsum' }),
).to.be.equal(true);
});
});
================================================
FILE: packages/hadron-auth/src/password/bcrypt/IBcryptOptions.ts
================================================
export default interface IBcryptOptions {
saltRounds?: number;
};
================================================
FILE: packages/hadron-auth/src/password/bcrypt/__tests__/bcrypt.ts
================================================
import { hash, compare } from '../bcrypt';
import * as bcrypt from 'bcrypt';
import { expect } from 'chai';
import * as sinon from 'sinon';
describe('bcrypt', () => {
describe('hash', () => {
let hashStub: any = null;
let genSaltStub: any = null;
before(() => {
hashStub = sinon.stub(bcrypt, 'hash');
genSaltStub = sinon.stub(bcrypt, 'genSalt');
});
beforeEach(() => {
hashStub.reset();
genSaltStub.reset();
hashStub.returns(Promise.resolve('h45h'));
genSaltStub.returns(Promise.resolve('54lt'));
});
after(() => {
hashStub.restore();
genSaltStub.restore();
});
it('should run bcrypt hash method, if salt given', () => {
const password = 'loremIpsum';
const salt = 'd0n7-b3-s0-s4l7y';
return hash(password, salt).then(() =>
expect(hashStub.calledWithExactly(password, salt)).to.be.equal(true),
);
});
it('should generate salt if none given', () => {
const password = 'loremIpsum';
return hash(password, null).then(() =>
expect(hashStub.calledWithExactly(password, '54lt')).to.be.equal(true),
);
});
it('should run genSalt method of bcrypt, if no salt was passed', () => {
const password = 'loremIpsum';
return hash(password, null).then(() =>
expect(genSaltStub.calledWith()).to.be.equal(true),
);
});
it('should run genSalt method of bcrypt, if no salt was passed, with saltRound property from options', () => {
const password = 'loremIpsum';
const options = {
saltRounds: 12,
};
return hash(password, null, options).then(() =>
expect(genSaltStub.calledWithExactly(12)).to.be.equal(true),
);
});
});
describe('compare', () => {
let compareStub: any = null;
before(() => {
compareStub = sinon.stub(bcrypt, 'compare');
});
beforeEach(() => {
compareStub.reset();
compareStub.returns(Promise.resolve(true));
});
after(() => {
compareStub.restore();
});
it('should run compare method', () =>
compare('loremIpsum', 'loremIpsum1').then(() =>
expect(
compareStub.calledWithExactly('loremIpsum', 'loremIpsum1'),
).to.be.equal(true),
));
it('should resolves with true if passwords are matching', () =>
expect(compare('loremIpsum', 'loremIpsum1')).to.be.eventually.equal(
true,
));
it('should resolves with false if passwords are not matching', () => {
compareStub.returns(Promise.resolve(false));
return expect(
compare('loremIpsum', 'loremIpsum1'),
).to.be.eventually.equal(false);
});
});
});
================================================
FILE: packages/hadron-auth/src/password/bcrypt/bcrypt.ts
================================================
import * as bcrypt from 'bcrypt';
import IHashMethod from '../IHashMethod';
import IBcryptOptions from './IBcryptOptions';
export function hash(
password: string,
salt?: string,
options: IBcryptOptions = {},
): Promise {
if (!salt) {
return bcrypt
.genSalt(options.saltRounds)
.then((salt) => bcrypt.hash(password, salt));
}
return bcrypt.hash(password, salt);
}
export function compare(
userPassword: string,
hashedPassword: string,
options?: IBcryptOptions,
): Promise {
return bcrypt.compare(userPassword, hashedPassword);
}
export default function bcryptProvider(options: IBcryptOptions): IHashMethod {
return {
hash: (password: string, salt?: string) => hash(password, salt, options),
compare: (userPassword: string, hashedPassword: string) =>
compare(userPassword, hashedPassword, options),
} as IHashMethod;
}
================================================
FILE: packages/hadron-auth/src/password/hashMethodProvider.ts
================================================
import bcrypt from './bcrypt/bcrypt';
import ISecurityOptions from '../ISecurityOptions';
import IHashMethod from './IHashMethod';
export interface IHashProviderMap {
[s: string]: (options?: object) => IHashMethod;
}
const availableMethod: IHashProviderMap = {
bcrypt,
};
/**
* Function checks if given object is implementing IHashMethod interface
* @param {IHashMethod | string} hashMethod
* @returns {boolean}
*/
function isHashMethod(
hashMethod: IHashMethod | string,
): hashMethod is IHashMethod {
return (
(hashMethod as IHashMethod).compare !== undefined &&
(hashMethod as IHashMethod).hash !== undefined
);
}
/**
* Funtion exctracts hashing method from config. Bcrypt on default.
* @param config
* @param methods
*/
export default function hashMethodProvider(
config: ISecurityOptions,
methods?: IHashProviderMap,
) {
const allMethods: IHashProviderMap = { ...availableMethod, ...methods };
if (config.hash) {
const { method, options } = config.hash;
if (typeof method === 'string' && allMethods[method]) {
return allMethods[method](options);
}
if (isHashMethod(method)) {
return {
hash: (password: string, salt?: string) =>
method.hash(password, salt, options),
compare: (
userPassword: string,
hashedPassword: string,
salt?: string,
) => method.compare(userPassword, hashedPassword, salt, options),
};
}
}
return allMethods.bcrypt();
}
================================================
FILE: packages/hadron-auth/src/providers/expressMiddlewareAuthorization.ts
================================================
import * as jwt from 'jsonwebtoken';
import { isRouteSecure, isAllowed } from '../HadronAuth';
const errorResponse = {
message: 'Unauthorized',
};
const expressMiddlewareAuthorization = (container: any) => {
return async (req: any, res: any, next: any) => {
try {
if (!isRouteSecure(req.path)) {
return next();
}
const userRepository = container.take('userRepository');
const roleRepository = container.take('roleRepository');
const token = req.headers.authorization.split(' ')[1];
const secret = container.take('authSecret');
const id: any = jwt.verify(token, secret);
const user = await userRepository.findOne({
where: { id },
relations: ['roles'],
});
if (!user) {
return res.status(401).json({ error: errorResponse });
}
const allRoles = await roleRepository.find();
if (
// @ts-ignore
isAllowed(req.path, req.method, user, allRoles.map((role) => role.name))
) {
return next();
}
return res.status(401).json({ error: errorResponse });
} catch (error) {
return res.status(401).json({ error: errorResponse });
}
};
};
export default expressMiddlewareAuthorization;
================================================
FILE: packages/hadron-auth/tsconfig.json
================================================
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"noEmitOnError": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"allowJs": true,
"paths": {
"*": ["./*"]
},
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"inlineSources": true,
"lib": ["es2017", "dom"]
},
"include": ["src/**/*.*", "index.ts", "LICENSE"],
"exclude": ["node_modules", "src/__tests__/**"]
}
================================================
FILE: packages/hadron-core/LICENSE
================================================
MIT License
Copyright (c) 2018 Brainhub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/hadron-core/README.md
================================================
## Installation
* Install Node.js. We recommend using the latest version, installation details on [nodejs.org](https://nodejs.org)
* Install following modules from npm:
```bash
npm install @brainhubeu/hadron-core @brainhubeu/hadron-express express --save
```
## Hello world app
Let's start with traditional Hello World app. It will give you a quick grasp of the framework.
```javascript
const hadron = require('@brainhubeu/hadron-core').default;
const express = require('express');
const port = 8080;
const expressApp = express();
const config = {
routes: {
helloWorldRoute: {
path: '/',
callback: () => 'Hello world!',
methods: ['get'],
},
},
};
hadron(expressApp, [require('@brainhubeu/hadron-express')], config).then(() => {
expressApp.listen(port, () =>
console.log(`Listening on http://localhost:${port}`),
);
});
```
In the sections below, we will describe step by step what just happened.
## Bootstrapping an app
The main hadron-core function is responsible for bootstrapping the app. It registers packages based on passed config and server instance:
```javascript
const hadron = require('hadron-core').default;
hadron(serverInstance, [...packages], config);
```
The purpose of the main function is to initialize DI container and register package dependencies according to correspondent sections in config object (described in details in next chapters).
Main function returns a promise that resolves to created DI container instance. In the promise `.then()` method, besides performing operations on the container instance, we can actually start our server, by calling Express `listen` method:
```javascript
hadron(serverInstance, ...rest).then((container) => {
// do some things on container...
serverInstance.listen(PORT, callback);
});
```
Now, let's move to DI container itself.
## Dependency Injection
The whole framework is built around DI Container concept. Its purpose is to automatically supply proper arguments for routes callbacks and other framework's building blocks.
DI container instance is created and used internally by bootstrapping function, it is also returned (as a promise) from bootstrapping function, as mentioned in the previous section.
### Container methods
#### Registering items
```javascript
container.register(key, item, lifetime);
```
* `key` - item name on which it will be registered inside the container
* `item` - any value (primitive, data structure, function, class, etc.)
* `lifetime` - the type of item's life-span
Lifetime options:
* `'value'` - container returns registered item as is [default]
* `'singleton'` - returns always the same instance of registered class / constructor function
* `'transient'` - returns always a new instance of registered class / constructor function
#### Retrieving items
```javascript
container.take(key);
```
* `key` - item name (same as provided during registration)
The method returns item or item instance according to item type and lifetime option.
#### Example usage in bootstrapping function
```javascript
const { default: hadron, Lifetime } = require('hadron-core');
hadron(...args).then((container) => {
container.register('foo', 123);
container.register('bar', class Bar {}, Lifetime.Singleton);
container.register('baz', class Baz {}, Lifetime.Transient);
// other stuff...
});
```
### Accessing container items from routes' callbacks
To access container items from callbacks, you can just set arguments' names to match container keys, and required dependency will be provided.
See an example [here](../routing/#retrieving-items-from-container-in-callback)
================================================
FILE: packages/hadron-core/index.ts
================================================
import hadronCore from './src/hadronCore';
export { default as Container } from './src/container/container';
export * from './src/container/lifecycle';
export * from './src/container/types';
export default hadronCore;
================================================
FILE: packages/hadron-core/package.json
================================================
{
"name": "@brainhubeu/hadron-core",
"version": "1.0.0",
"description": "Hadron core module",
"main": "dist/index.js",
"files": [
"dist",
"./LICENSE"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "tsc"
},
"keywords": [
"hadron",
"brainhub",
"hadron-core"
],
"author": "Brainhub",
"license": "MIT",
"dependencies": {
"@brainhubeu/hadron-error-handler": "^1.0.0",
"@brainhubeu/hadron-utils": "^1.0.0",
"bunyan": "^1.8.12"
},
"publishConfig": {
"access": "public"
}
}
================================================
FILE: packages/hadron-core/src/__tests__/hadronCore.test.ts
================================================
import { expect } from 'chai';
import * as sinon from 'sinon';
import container from '../container/container';
import hadronCore, { prepareConfig } from '../hadronCore';
describe('prepareConfig()', () => {
const logger = { error: sinon.spy() };
const defaultConfig = { prop1: 1, prop2: 2 };
it('should accept object', () => {
const config = { prop2: 4, prop3: 5 };
const expectedResult = { prop1: 1, prop2: 4, prop3: 5 };
return prepareConfig(defaultConfig, config, logger).then((resolvedConfig) =>
expect(resolvedConfig).to.deep.equal(expectedResult),
);
});
it('should accept promise, which returns object', () => {
const config = new Promise((res, rej) => res({ prop2: 4, prop3: 5 }));
const expectedResult = { prop1: 1, prop2: 4, prop3: 5 };
return prepareConfig(defaultConfig, config, logger).then((resolvedConfig) =>
expect(resolvedConfig).to.deep.equal(expectedResult),
);
});
it('should log error when config promise is rejected', () => {
const config = new Promise((res, rej) => rej());
const loggerMock = { error: sinon.spy() };
return prepareConfig(defaultConfig, config, loggerMock).catch((error) =>
expect(loggerMock.error.called).to.be.eql(true),
);
});
it('should return default config when config promise is rejected', () => {
const config = new Promise((res, rej) => rej());
return prepareConfig(defaultConfig, config, logger).then((resolvedConfig) =>
expect(resolvedConfig).to.deep.equal(defaultConfig),
);
});
});
describe('hadronCore()', () => {
const mockRegister = sinon.stub(container, 'register');
beforeEach(() => {
return mockRegister.reset();
});
after(() => {
return mockRegister.restore();
});
it('should return promise with Container instance', () => {
return hadronCore({}).then((returnedContainer) =>
expect(returnedContainer).to.equal(container),
);
});
it('should register server in container', () => {
const server = { call: 'I am server!' };
return hadronCore(server).then((returnedContainer) =>
expect(mockRegister.calledWith('server', server)).to.be.eql(true),
);
});
it('should run register function from given package', () => {
const server = { call: 'I am server!' };
const mockPackage = { register: sinon.spy() };
return hadronCore(server, [mockPackage]).then((returnedContainer) =>
expect(mockPackage.register.calledOnce).to.be.eql(true),
);
});
it('should include given config to hadron configuration and pass it to register function of package', () => {
const testConfig = {
testField: 'I am test!',
};
const server = { call: 'I am server!' };
const mockPackage = { register: sinon.spy() };
return hadronCore(server, [mockPackage], testConfig).then(
(returnedContainer) =>
// second argument, which should be config
expect(mockPackage.register.args[0][1]).to.contain(testConfig),
);
});
});
================================================
FILE: packages/hadron-core/src/constants/eventNames.ts
================================================
export enum eventNames {
HANDLE_INITIALIZE_APPLICATION_EVENT = 'handleInitializeApplicationEvent',
HANDLE_TERMINATE_APPLICATION_EVENT = 'handleTerminateApplicationEvent',
}
================================================
FILE: packages/hadron-core/src/container/__tests__/container.ts
================================================
/* tslint:disable:max-classes-per-file */
import { expect } from 'chai';
import container from '../container';
import { Lifecycle } from '../lifecycle';
describe('container register', () => {
it('should overrive value for the the same key', () => {
const itemName = 'test';
container.register(itemName, 'given');
container.register(itemName, 'given2');
expect('given2').to.equal(container.take(itemName));
});
it('should always return the same object - Singleton', () => {
const itemName = 'test';
class Foo {
public value: string;
constructor() {
this.value = new Date().getTime().toString();
}
}
container.register(itemName, Foo, Lifecycle.Singleton);
const item1 = container.take(itemName);
const item2 = container.take(itemName);
expect(item2).to.deep.equal(item1);
});
it('should always return a new same object - Transient', () => {
class Foo {
public value: string;
constructor() {
this.value = 'xxxx';
}
}
container.register('test', Foo, Lifecycle.Transient);
const item1 = container.take('test');
const item2 = container.take('test');
expect(item1).to.deep.equal(item2);
});
});
describe('container items with parameters in constructor', () => {
it('second level of injection', () => {
class Foo {
public value: number;
constructor() {
this.value = 4;
}
}
class Foo2 {
public value: number;
constructor(parameterName: Foo) {
this.value = parameterName.value;
}
}
container.register('parameterName', Foo, Lifecycle.Transient);
container.register('foo2', Foo2, Lifecycle.Transient);
const item = container.take('parameterName') as Foo;
expect(4).to.be.equal(item.value);
});
});
describe("list of container's items keys ", () => {
it('should return array of keys of earlier registered items', () => {
container.register('key1', 'item1');
container.register('key2', 'item2');
const keys = container.keys();
expect(keys).to.include('key1');
expect(keys).to.include('key2');
});
});
================================================
FILE: packages/hadron-core/src/container/__tests__/containerItem.ts
================================================
/* tslint:disable:max-classes-per-file */
import { expect } from 'chai';
import containerItem from '../containerItem';
import { Lifecycle } from '../lifecycle';
describe('containerItem set lifecycle', () => {
it('should be default(value)', () => {
const item = containerItem.containerItemFactory('object', Object);
expect(item.constructor.name).to.equal('ContainerItem');
});
it('should be transient', () => {
const item = containerItem.containerItemFactory(
'object1',
Object,
'transient',
);
expect(item.constructor.name).to.equal('ContainerItemTransient');
});
it('should be singleton', () => {
const item = containerItem.containerItemFactory(
'object2',
Object,
Lifecycle.Singleton,
);
expect(item.constructor.name).to.equal('ContainerItemSingleton');
});
});
describe('containerItem set value', () => {
it('should return 1', () => {
const item = containerItem.containerItemFactory('number', 5);
expect(item.Item).to.equal(5);
});
it("should return 'oko'", () => {
const item = containerItem.containerItemFactory('string', 'oko');
expect(item.Item).to.equal('oko');
});
it("should return '{}'", () => {
const item = containerItem.containerItemFactory('object', {});
expect(item.Item).to.deep.equal({});
});
it('should return the same object twice', () => {
class Foo {
public value: string;
constructor() {
this.value = new Date().getTime().toString();
}
}
const item = containerItem.containerItemFactory(
'Fooooo',
Foo,
Lifecycle.Singleton,
);
const item1 = item.Item;
const item2 = item.Item;
expect(item1).to.equal(item2);
});
it('should always return new instance of given type', (done) => {
class Foo {
public value: string;
constructor() {
this.value = new Date().getTime().toString();
}
}
const item = containerItem.containerItemFactory(
'Foo',
Foo,
Lifecycle.Transient,
);
const item1 = item.Item;
setTimeout(() => {
const item2 = item.Item;
expect(item1.value).to.not.equal(item2.value);
done();
}, 10);
});
it('should always return new instance of given type 2', () => {
class Foo {
public value: string;
constructor() {
this.value = new Date().getTime().toString();
}
}
const item = containerItem.containerItemFactory(
'Foo',
Foo,
Lifecycle.Transient,
);
const item1 = item.Item;
const item2 = item.Item;
expect(item1).to.not.equal(item2);
});
});
================================================
FILE: packages/hadron-core/src/container/container.ts
================================================
import containerItem from './containerItem';
import { IContainerItem, IContainer } from './types';
import isVarName from '../helpers/isVarName';
import IncorrectContainerKeyNameError from '../errors/IncorrectContainerKeyNameError';
const containerRegister = new Array();
const takeContainerByKey = (key: string): IContainerItem[] =>
containerRegister.filter((x) => x.getKey() === key);
/* method for registering items in container */
/**
* Method for registering items in container, optionally setting a lifespan for items
* @param key for representing item in register, second use of the same key override previous item
* @param item stored item, can be any type: value, type, function....
* @param lifecycle setting type of life-span [value, singleton, transient] - value is default
*/
const register = (key: string, item: any, lifecycle?: string): void => {
if (!isVarName(key)) {
throw new IncorrectContainerKeyNameError(key);
}
const containerItems = takeContainerByKey(key);
if (containerItems.length === 0) {
const ci = containerItem.containerItemFactory(key, item, lifecycle);
containerRegister.push(ci);
} else {
containerItems[0].Item = item;
}
};
/** method for getting item from register
* Method which returns item previously registered for passed key
* @param key for representing item in register
* @return { any } return stored item
*/
const take = (key: string): any => {
const containerItems = takeContainerByKey(key);
return containerItems.length === 0 ? null : containerItems[0].Item;
};
/** method for getting all the keys in container
* @return array of keys
*/
const keys = (): string[] => containerRegister.map((x) => x.getKey());
const container = {
register,
take,
keys,
};
export default container as IContainer;
================================================
FILE: packages/hadron-core/src/container/containerItem.ts
================================================
import container from './container';
import { IContainerItem } from './types';
import { Lifecycle } from './lifecycle';
import { getArgs } from '@brainhubeu/hadron-utils';
export class ContainerItem implements IContainerItem {
// tslint:disable-next-line:variable-name
constructor(protected key: string, protected item: any) {}
get Item(): any {
return this.item;
}
set Item(item: any) {
this.item = item;
}
public getKey() {
return this.key;
}
public getArgs(): string[] {
return getArgs(this.item);
}
}
// tslint:disable-next-line:max-classes-per-file
class ContainerItemSingleton extends ContainerItem {
// tslint:disable-next-line:variable-name
private _itemInstanse: any;
constructor(key: string, item: any) {
super(key, item);
this._itemInstanse = null;
}
set Item(item: any) {
this.item = item;
}
get Item(): any {
const parameters = this.getArgs();
if (parameters.length > 0) {
const parameterInstances = parameters.map((paramName) =>
container.take(paramName),
);
if (this._itemInstanse === null) {
try {
this._itemInstanse = new this.item(...parameterInstances);
} catch (error) {
throw new Error(`can not create an instance of ${this.key}`);
}
}
return this._itemInstanse;
}
if (this._itemInstanse === null) {
try {
this._itemInstanse = new this.item();
} catch (error) {
throw new Error(`can not create an instance of ${this.key}`);
}
}
return this._itemInstanse;
}
}
// tslint:disable-next-line:max-classes-per-file
class ContainerItemTransient extends ContainerItem {
constructor(key: string, item: any) {
super(key, item);
}
set Item(item: any) {
this.item = item;
}
get Item(): any {
const parameters = this.getArgs();
if (parameters.length > 0) {
const parameterInstances = parameters.map((paramName) =>
container.take(paramName),
);
try {
return new this.item(...parameterInstances);
} catch (error) {
throw new Error(`can not create a new instance of ${this.key}`);
}
} else {
try {
return new this.item();
} catch (error) {
throw new Error(`can not create a new instance of ${this.key}`);
}
}
}
}
const containerItemFactory = (
key: string,
item: any,
lifecycle?: string,
): ContainerItem => {
switch (lifecycle) {
case Lifecycle.Singleton:
return new ContainerItemSingleton(key, item);
case Lifecycle.Transient:
return new ContainerItemTransient(key, item);
default:
return new ContainerItem(key, item);
}
};
export default { containerItemFactory };
================================================
FILE: packages/hadron-core/src/container/lifecycle.ts
================================================
enum Lifecycle {
Transient = 'transient',
Singleton = 'singleton',
Value = 'value',
}
export { Lifecycle };
================================================
FILE: packages/hadron-core/src/container/types.ts
================================================
interface IContainerItem {
Item(): any;
Item(key: string): void;
getKey(): string;
getArgs(): string[];
}
interface IContainer {
take: (key: string) => any;
register: (key: string, value: any, lifecycle?: string) => any;
keys: () => string[];
}
export { IContainerItem, IContainer };
================================================
FILE: packages/hadron-core/src/declarations.d.ts
================================================
declare module '@hadron/utils';
================================================
FILE: packages/hadron-core/src/errors/IncorrectContainerKeyNameError.ts
================================================
import HadronErrorHandler from '@brainhubeu/hadron-error-handler';
export default class IncorrectContainerKeyNameError extends HadronErrorHandler {
constructor(key: string, err: Error = new Error()) {
super();
this.message = `The key name '${key}' is incorrect to register in container, should be a valid variable name.`;
this.name = 'IncorrectContainerKeyNameError';
this.stack = null;
this.error = err;
}
}
================================================
FILE: packages/hadron-core/src/errors/LoadingPackageError.ts
================================================
import HadronErrorHandler from '@brainhubeu/hadron-error-handler';
export default class LoadingPackageError extends HadronErrorHandler {
constructor(err: Error) {
super();
this.message = `Problem with loading package`;
this.name = 'LoadingPackageError';
this.stack = null;
this.error = err;
}
}
================================================
FILE: packages/hadron-core/src/hadronCore.ts
================================================
import container from './container/container';
import { IContainer } from './container/types';
import { createLogger } from 'bunyan';
const hadronDefaultConfig = {};
export const prepareConfig = (
defaultConfig: object | Promise