├── .dockerignore ├── .editorconfig ├── .env ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js ├── tsconfig.json └── webpack.config.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── apps ├── .gitkeep ├── admin-e2e │ ├── .eslintrc.json │ ├── cypress.json │ ├── src │ │ ├── db │ │ │ └── index.ts │ │ ├── fixtures │ │ │ ├── example.json │ │ │ └── users.json │ │ ├── integration │ │ │ ├── auth │ │ │ │ ├── login.feature │ │ │ │ └── login │ │ │ │ │ └── index.ts │ │ │ ├── common │ │ │ │ ├── auth.steps.js │ │ │ │ ├── db.hook.ts │ │ │ │ └── users.steps.js │ │ │ └── users │ │ │ │ ├── create.feature │ │ │ │ ├── create.spec.ts │ │ │ │ ├── edit.spec.ts │ │ │ │ └── list.feature │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── CreatePage.js │ │ │ ├── CustomFormPage.js │ │ │ ├── CustomPage.js │ │ │ ├── EditPage.js │ │ │ ├── ListPage.js │ │ │ ├── LoginPage.js │ │ │ ├── ShowPage.js │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ ├── index.js │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ └── tsconfig.json ├── admin │ ├── .babelrc │ ├── .browserslistrc │ ├── .env │ ├── .eslintrc.json │ ├── babel-jest.config.json │ ├── jest.config.js │ ├── proxy-test.conf.json │ ├── proxy.conf.json │ ├── src │ │ ├── app │ │ │ └── app.tsx │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── components │ │ │ ├── dashboard.tsx │ │ │ ├── index.ts │ │ │ └── simple-chip-field.tsx │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── lib │ │ │ ├── auth.provider.ts │ │ │ ├── data.provider.ts │ │ │ └── index.ts │ │ ├── main.tsx │ │ ├── polyfills.ts │ │ └── resources │ │ │ ├── index.ts │ │ │ └── user │ │ │ ├── index.ts │ │ │ ├── user-create.resource.tsx │ │ │ ├── user-edit.resource.tsx │ │ │ ├── user-list.resource.tsx │ │ │ └── user.form.tsx │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── webpack.config.js ├── api │ ├── .env │ ├── .eslintrc.json │ ├── .test.env │ ├── jest.config.js │ ├── src │ │ ├── app.middleware.ts │ │ ├── app.module.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── auth │ │ │ ├── auth.module.ts │ │ │ ├── controller │ │ │ │ ├── auth.controller.spec.ts │ │ │ │ └── auth.controller.ts │ │ │ ├── dto │ │ │ │ └── login.dto.ts │ │ │ ├── security │ │ │ │ ├── role.enum.ts │ │ │ │ ├── roles.decorator.ts │ │ │ │ └── roles.guard.ts │ │ │ └── services │ │ │ │ ├── auth.service.spec.ts │ │ │ │ ├── auth.service.ts │ │ │ │ └── jwt.strategy.ts │ │ ├── bootstrap.module.ts │ │ ├── config │ │ │ └── config.service.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── main.ts │ │ ├── migrations │ │ │ └── 1609184562041-add-user.ts │ │ └── user │ │ │ ├── application │ │ │ ├── command │ │ │ │ ├── create-user.command.ts │ │ │ │ ├── create-user.handler.ts │ │ │ │ ├── delete-user.command.ts │ │ │ │ ├── delete-user.handler.ts │ │ │ │ ├── index.ts │ │ │ │ ├── update-user.command.ts │ │ │ │ └── update-user.handler.ts │ │ │ ├── index.ts │ │ │ └── query │ │ │ │ ├── get-user-by-username.handler.ts │ │ │ │ ├── get-user-by-username.query.ts │ │ │ │ ├── get-user.handler.ts │ │ │ │ ├── get-user.query.ts │ │ │ │ ├── get-users.handler.ts │ │ │ │ ├── get-users.query.ts │ │ │ │ └── index.ts │ │ │ ├── domain │ │ │ ├── event │ │ │ │ ├── index.ts │ │ │ │ ├── user-password-was-updated.event.ts │ │ │ │ ├── user-role-was-added.event.ts │ │ │ │ ├── user-role-was-removed.event.ts │ │ │ │ ├── user-was-created.event.ts │ │ │ │ └── user-was-deleted.event.ts │ │ │ ├── exception │ │ │ │ ├── index.ts │ │ │ │ ├── user-id-already-taken.error.ts │ │ │ │ ├── user-id-not-found.error.ts │ │ │ │ ├── username-already-taken.error.ts │ │ │ │ └── username-not-found.error.ts │ │ │ ├── index.ts │ │ │ ├── model │ │ │ │ ├── index.ts │ │ │ │ ├── password.spec.ts │ │ │ │ ├── password.ts │ │ │ │ ├── role.ts │ │ │ │ ├── user-id.ts │ │ │ │ ├── user.ts │ │ │ │ ├── username.spec.ts │ │ │ │ └── username.ts │ │ │ └── repository │ │ │ │ ├── index.ts │ │ │ │ └── users.ts │ │ │ └── infrastructure │ │ │ ├── controller │ │ │ └── user.controller.ts │ │ │ ├── entity │ │ │ └── user.entity.ts │ │ │ ├── index.ts │ │ │ ├── repository │ │ │ ├── user.mapper.spec.ts │ │ │ ├── user.mapper.ts │ │ │ └── user.repository.ts │ │ │ ├── saga │ │ │ └── user-was-deleted.saga.ts │ │ │ ├── services │ │ │ └── user.service.ts │ │ │ ├── user.module.ts │ │ │ └── user.providers.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ ├── tsconfig.typeorm.json │ └── webpack.config.js ├── web-e2e │ ├── .eslintrc.json │ ├── cypress.json │ ├── src │ │ ├── db │ │ │ └── index.ts │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ ├── common │ │ │ │ ├── common.steps.ts │ │ │ │ └── landing │ │ │ │ │ └── index.steps.ts │ │ │ └── landing.feature │ │ ├── plugins │ │ │ └── index.js │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ ├── tsconfig.e2e.json │ └── tsconfig.json └── web │ ├── .babelrc │ ├── .env │ ├── .eslintrc.json │ ├── babel-jest.config.json │ ├── index.d.ts │ ├── jest.config.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth].ts │ └── index.tsx │ ├── proxy.conf.json │ ├── public │ ├── nx-logo-white.svg │ └── star.svg │ ├── specs │ └── index.spec.tsx │ ├── tsconfig.app.json │ ├── tsconfig.json │ └── tsconfig.spec.json ├── babel.config.json ├── commitlint.config.js ├── cypress-cucumber-preprocessor.config.js ├── jest.config.js ├── jest.preset.js ├── libs ├── .gitkeep ├── contracts │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── auth │ │ │ ├── access-token.interface.ts │ │ │ ├── credentials.interface.ts │ │ │ ├── index.ts │ │ │ ├── jwt-payload.interface.ts │ │ │ └── role.enum.ts │ │ │ └── user │ │ │ ├── create-user.dto.ts │ │ │ ├── edit-user.dto.ts │ │ │ ├── index.ts │ │ │ └── user.dto.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── domain │ ├── .eslintrc.json │ ├── README.md │ ├── jest.config.js │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── event.ts │ │ │ ├── id.spec.ts │ │ │ ├── id.ts │ │ │ ├── invalid-id-error.ts │ │ │ ├── value-object.spec.ts │ │ │ └── value-object.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── ui │ ├── .babelrc │ ├── .eslintrc.json │ ├── .storybook │ ├── main.js │ ├── preview.js │ ├── tsconfig.json │ └── webpack.config.js │ ├── README.md │ ├── babel-jest.config.json │ ├── jest.config.js │ ├── src │ ├── index.ts │ └── lib │ │ ├── layout │ │ ├── layout.spec.tsx │ │ ├── layout.stories.tsx │ │ └── layout.tsx │ │ ├── navbar │ │ ├── navbar.spec.tsx │ │ ├── navbar.stories.tsx │ │ └── navbar.tsx │ │ ├── sidebar │ │ ├── sidebar.spec.tsx │ │ ├── sidebar.stories.tsx │ │ └── sidebar.tsx │ │ └── theme.tsx │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── nx.json ├── package.json ├── tools ├── generate-cucumber-report.js ├── generators │ └── .gitkeep ├── scripts │ └── write-type-orm-config.ts └── tsconfig.tools.json ├── tsconfig.base.json ├── workspace.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/.dockerignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET=changeme 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx", "simple-import-sort", "import"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } 16 | ] 17 | } 18 | ], 19 | "simple-import-sort/imports": "error", 20 | "simple-import-sort/exports": "error", 21 | "sort-imports": "off", 22 | "import/first": "error", 23 | "import/newline-after-import": "error", 24 | "import/no-duplicates": "error" 25 | } 26 | }, 27 | { 28 | "files": ["*.ts", "*.tsx"], 29 | "extends": ["plugin:@nrwl/nx/typescript"], 30 | "parserOptions": { "project": "./tsconfig.*?.json" }, 31 | "rules": {} 32 | }, 33 | { 34 | "files": ["*.js", "*.jsx"], 35 | "extends": ["plugin:@nrwl/nx/javascript"], 36 | "rules": {} 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Project Files 42 | ormconfig.json 43 | .env.local 44 | cyreport 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: [], 3 | addons: ['@storybook/addon-essentials'], 4 | }; 5 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "exclude": [ 4 | "../**/*.spec.js", 5 | "../**/*.spec.ts", 6 | "../**/*.spec.tsx", 7 | "../**/*.spec.jsx" 8 | ], 9 | "include": ["../**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Export a function. Accept the base config as the only param. 3 | * @param {Object} options 4 | * @param {Required} options.config 5 | * @param {'DEVELOPMENT' | 'PRODUCTION'} options.mode - change the build configuration. 'PRODUCTION' is used when building the static version of storybook. 6 | */ 7 | module.exports = async ({ config, mode }) => { 8 | // Make whatever fine-grained changes you need 9 | 10 | // Return the altered config 11 | return config; 12 | }; 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "project-relative", 3 | "cucumberautocomplete.formatConfOverride": { 4 | "Característica:": 0, 5 | "Escenario:": 1, 6 | "Antecedentes:": 1, 7 | "Esquema del escenario:": 1, 8 | "Ejemplos:": 2, 9 | "Dado": 2, 10 | "Dada": 2, 11 | "Dados": 2, 12 | "Dadas": 2, 13 | "Cuando": 2, 14 | "Entonces": 2, 15 | "Y": 2, 16 | "Pero": 2, 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### 0.0.2 (2021-01-31) 6 | 7 | 8 | ### Features 9 | 10 | * add husky and lint commit ([1fd5091](https://github.com/sgomez/nx-boilerplate/commit/1fd50913d58c589ae031fa07e13d3880261c24b2)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * husky precommit tasks ([d2a6496](https://github.com/sgomez/nx-boilerplate/commit/d2a649618e98bac51760587c2b1ced896fb39b16)) 16 | * next-auth mock ([967bfc8](https://github.com/sgomez/nx-boilerplate/commit/967bfc8a213cb546121e505ade84ee2ddd5f9864)) 17 | * package yarn run script ([3a6a1e9](https://github.com/sgomez/nx-boilerplate/commit/3a6a1e922acdc8dbf7d57a81be268bf1cc1de3ee)) 18 | 19 | ### 0.0.1 (2021-01-05) 20 | 21 | 22 | ### Features 23 | 24 | * add husky and lint commit ([1fd5091](https://github.com/sgomez/nx-boilerplate/commit/1fd50913d58c589ae031fa07e13d3880261c24b2)) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * husky precommit tasks ([d2a6496](https://github.com/sgomez/nx-boilerplate/commit/d2a649618e98bac51760587c2b1ced896fb39b16)) 30 | * package yarn run script ([3a6a1e9](https://github.com/sgomez/nx-boilerplate/commit/3a6a1e922acdc8dbf7d57a81be268bf1cc1de3ee)) 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Boilerplate 2 | 3 | This project was generated using [Nx](https://nx.dev). 4 | 5 |

6 | 7 | 🔎 **Nx is a set of Extensible Dev Tools for Monorepos.** 8 | 9 | ## Adding capabilities to your workspace 10 | 11 | Nx supports many plugins which add capabilities for developing different types of applications and different tools. 12 | 13 | These capabilities include generating applications, libraries, etc as well as the devtools to test, and build projects as well. 14 | 15 | Below are our core plugins: 16 | 17 | - [React](https://reactjs.org) 18 | - `npm install --save-dev @nrwl/react` 19 | - Web (no framework frontends) 20 | - `npm install --save-dev @nrwl/web` 21 | - [Angular](https://angular.io) 22 | - `npm install --save-dev @nrwl/angular` 23 | - [Nest](https://nestjs.com) 24 | - `npm install --save-dev @nrwl/nest` 25 | - [Express](https://expressjs.com) 26 | - `npm install --save-dev @nrwl/express` 27 | - [Node](https://nodejs.org) 28 | - `npm install --save-dev @nrwl/node` 29 | 30 | There are also many [community plugins](https://nx.dev/nx-community) you could add. 31 | 32 | ## Generate an application 33 | 34 | Run `nx g @nrwl/react:app my-app` to generate an application. 35 | 36 | > You can use any of the plugins above to generate applications as well. 37 | 38 | When using Nx, you can create multiple applications and libraries in the same workspace. 39 | 40 | ## Generate a library 41 | 42 | Run `nx g @nrwl/react:lib my-lib` to generate a library. 43 | 44 | > You can also use any of the plugins above to generate libraries as well. 45 | 46 | Libraries are sharable across libraries and applications. They can be imported from `@boilerplate/mylib`. 47 | 48 | ## Development server 49 | 50 | Run `nx serve my-app` for a dev server. Navigate to http://localhost:4200/. The app will automatically reload if you change any of the source files. 51 | 52 | ## Code scaffolding 53 | 54 | Run `nx g @nrwl/react:component my-component --project=my-app` to generate a new component. 55 | 56 | ## Build 57 | 58 | Run `nx build my-app` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 59 | 60 | ## Running unit tests 61 | 62 | Run `nx test my-app` to execute the unit tests via [Jest](https://jestjs.io). 63 | 64 | Run `nx affected:test` to execute the unit tests affected by a change. 65 | 66 | ## Running end-to-end tests 67 | 68 | Run `ng e2e my-app` to execute the end-to-end tests via [Cypress](https://www.cypress.io). 69 | 70 | Run `nx affected:e2e` to execute the end-to-end tests affected by a change. 71 | 72 | ## Understand your workspace 73 | 74 | Run `nx dep-graph` to see a diagram of the dependencies of your projects. 75 | 76 | ## Further help 77 | 78 | Visit the [Nx Documentation](https://nx.dev) to learn more. 79 | 80 | ## ☁ Nx Cloud 81 | 82 | ### Computation Memoization in the Cloud 83 | 84 |

85 | 86 | Nx Cloud pairs with Nx in order to enable you to build and test code more rapidly, by up to 10 times. Even teams that are new to Nx can connect to Nx Cloud and start saving time instantly. 87 | 88 | Teams using Nx gain the advantage of building full-stack applications with their preferred framework alongside Nx’s advanced code generation and project dependency graph, plus a unified experience for both frontend and backend developers. 89 | 90 | Visit [Nx Cloud](https://nx.app/) to learn more. 91 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/apps/.gitkeep -------------------------------------------------------------------------------- /apps/admin-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["src/plugins/index.js"], 7 | "rules": { 8 | "@typescript-eslint/no-var-requires": "off", 9 | "no-undef": "off" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/admin-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/admin-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/admin-e2e/screenshots", 11 | "chromeWebSecurity": false, 12 | "testFiles": "**/*.{feature,features}" 13 | } 14 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Database } from 'sqlite3'; 3 | 4 | export const teardown = () => { 5 | const db = new Database(path.join(__dirname, '../../../../tmp/test.sqlite3')); 6 | 7 | db.each( 8 | "select 'delete from ' || name as query from sqlite_master where type = 'table'", 9 | (err, row) => db.run(row.query) 10 | ); 11 | 12 | return true; 13 | }; 14 | 15 | export const seed = () => { 16 | const db = new Database(path.join(__dirname, '../../../../tmp/test.sqlite3')); 17 | 18 | db.run( 19 | "INSERT INTO `users` (`id`, `username`, `password`, `roles`) VALUES ('f60d593d-9ea9-4add-8f6c-5d86dd8c9f87', 'admin', '$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy', 'ROLE_ADMIN')" 20 | ); 21 | db.run( 22 | "INSERT INTO `users` (`id`, `username`, `password`, `roles`) VALUES ('f60d593d-9ea9-4add-8f6c-5d86dd8c9f88', 'user', '$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy', 'ROLE_USER')" 23 | ); 24 | 25 | return true; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/fixtures/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "f60d593d-9ea9-4add-8f6c-5d86dd8c9f87", 4 | "username": "admin", 5 | "password": "$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy", 6 | "roles": ["ROLE_ADMIN"] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/auth/login.feature: -------------------------------------------------------------------------------- 1 | #language: es 2 | Característica: Iniciar sesion 3 | 4 | Escenario: Iniciar sesión como administrador con la contraseña correcta 5 | Dado que estoy en la página de inicio de sesión 6 | Cuando introduzco el usuario y la contraseña de administrador 7 | Entonces debo estar en la página principal 8 | 9 | Escenario: Iniciar sesión como administrador con la contraseña incorrecta 10 | Dado que estoy en la página de inicio de sesión 11 | Cuando introduzco mal la contraseña de administrador 12 | Entonces debo recibir un mensaje de permiso denegado 13 | 14 | Escenario: Iniciar sesión como usuario normal 15 | Dado que estoy en la página de inicio de sesión 16 | Cuando introduzco el usuario y la contraseña de un usuario normal 17 | Entonces debo recibir un mensaje de que mi usuario no es administrador 18 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/auth/login/index.ts: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | Given('que estoy en la página de inicio de sesión', () => { 4 | cy.visit('/#/login'); 5 | }); 6 | 7 | When('introduzco el usuario y la contraseña de administrador', () => { 8 | const username = 'admin'; 9 | const password = 'password'; 10 | 11 | cy.get('input[name=username]').type(username); 12 | cy.get('input[name=password]').type(`${password}{enter}`); 13 | }); 14 | 15 | When('introduzco mal la contraseña de administrador', () => { 16 | const username = 'admin'; 17 | const password = 'password2'; 18 | 19 | cy.get('input[name=username]').type(username); 20 | cy.get('input[name=password]').type(`${password}{enter}`); 21 | }); 22 | 23 | When('introduzco el usuario y la contraseña de un usuario normal', () => { 24 | const username = 'user'; 25 | const password = 'password'; 26 | 27 | cy.get('input[name=username]').type(username); 28 | cy.get('input[name=password]').type(`${password}{enter}`); 29 | }); 30 | 31 | Then('debo estar en la página principal', () => { 32 | cy.url().should('include', '/'); 33 | 34 | cy.get('span').should('contain', 'admin'); 35 | }); 36 | 37 | Then('debo recibir un mensaje de permiso denegado', () => { 38 | cy.contains('Request failed with status code 401'); 39 | }); 40 | 41 | Then('debo recibir un mensaje de que mi usuario no es administrador', () => { 42 | cy.contains('Acceso exclusivo para administradores'); 43 | }); 44 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/common/auth.steps.js: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | import loginPageFactory from '../../support/LoginPage'; 4 | 5 | const LoginPage = loginPageFactory('/#/login'); 6 | 7 | Given('que estoy logueado como administrador', () => { 8 | LoginPage.navigate(); 9 | LoginPage.login('admin', 'password'); 10 | }); 11 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/common/db.hook.ts: -------------------------------------------------------------------------------- 1 | import { Before } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | Before(() => { 4 | cy.task('db:teardown'); 5 | cy.task('db:seed'); 6 | console.info('RUN'); 7 | }); 8 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/common/users.steps.js: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | import createPageFactory from '../../support/CreatePage'; 4 | import editPageFactory from '../../support/EditPage'; 5 | import listPageFactory from '../../support/ListPage'; 6 | 7 | const CreatePage = createPageFactory('/#/users/create'); 8 | const EditPage = editPageFactory('/#/users/4'); 9 | const ListPage = listPageFactory('/#/users'); 10 | 11 | Given('que estoy en la página de usuarios', () => { 12 | ListPage.navigate(); 13 | ListPage.waitUntilDataLoaded(); 14 | }); 15 | 16 | Given('que estoy en la página de creación de usuarios', () => { 17 | CreatePage.navigate(); 18 | CreatePage.waitUntilVisible(); 19 | }); 20 | 21 | When('relleno el formulario con los siguientes datos:', (table) => { 22 | console.info(table); 23 | CreatePage.setValues( 24 | table.rawTable.map((el) => ({ name: el[0], type: el[1], value: el[2] })) 25 | ); 26 | }); 27 | 28 | When('pulso guardar', () => { 29 | CreatePage.submit(); 30 | }); 31 | 32 | Then('tengo que ver {int} elementos', (count) => { 33 | cy.contains(`1-${count} of ${count}`); 34 | }); 35 | 36 | Then('estoy en la página de edición del usuario {string}', (username) => { 37 | EditPage.waitUntilVisible(); 38 | 39 | cy.get(EditPage.elements.input('username')).should((el) => 40 | expect(el).to.have.value(username) 41 | ); 42 | }); 43 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/users/create.feature: -------------------------------------------------------------------------------- 1 | #language: es 2 | Característica: Crear usuarios 3 | 4 | Antecedentes: 5 | Dado que estoy logueado como administrador 6 | 7 | Escenario: Crear usuarios 8 | Dado que estoy en la página de creación de usuarios 9 | Cuando relleno el formulario con los siguientes datos: 10 | | username | input | johndoe | 11 | | roles | select | Administrador | 12 | | plainPassword | input | password | 13 | | plainPasswordRepeat | input | password | 14 | Y pulso guardar 15 | Entonces estoy en la página de edición del usuario "johndoe" 16 | 17 | 18 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/users/create.spec.ts: -------------------------------------------------------------------------------- 1 | import createPageFactory from '../../support/CreatePage'; 2 | import editPageFactory from '../../support/EditPage'; 3 | import listPageFactory from '../../support/ListPage'; 4 | import loginPageFactory from '../../support/LoginPage'; 5 | 6 | describe('Create users', () => { 7 | const CreatePage = createPageFactory('/#/users/create'); 8 | const LoginPage = loginPageFactory('/#/login'); 9 | const ListPage = listPageFactory('/#/users'); 10 | const EditPage = editPageFactory('/#/users/4'); 11 | 12 | before(() => { 13 | cy.task('db:teardown'); 14 | cy.task('db:seed'); 15 | }); 16 | 17 | beforeEach(() => { 18 | LoginPage.navigate(); 19 | LoginPage.login('admin', 'password'); 20 | }); 21 | 22 | it('should create a regular user', () => { 23 | const values = [ 24 | { 25 | type: 'input', 26 | name: 'username', 27 | value: 'johndoe', 28 | }, 29 | { 30 | type: 'input', 31 | name: 'plainPassword', 32 | value: 'password', 33 | }, 34 | { 35 | type: 'input', 36 | name: 'plainPasswordRepeat', 37 | value: 'password', 38 | }, 39 | ]; 40 | 41 | CreatePage.navigate(); 42 | CreatePage.waitUntilVisible(); 43 | CreatePage.setValues(values); 44 | CreatePage.submit(); 45 | EditPage.waitUntilVisible(); 46 | 47 | cy.get(EditPage.elements.input('username')).should((el) => 48 | expect(el).to.have.value('johndoe') 49 | ); 50 | cy.get(EditPage.elements.input('plainPassword')).should( 51 | (el) => expect(el).to.be.empty 52 | ); 53 | }); 54 | 55 | it('should appears in list page', () => { 56 | ListPage.navigate(); 57 | ListPage.waitUntilDataLoaded(); 58 | 59 | cy.contains('1-2 of 2'); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/users/edit.spec.ts: -------------------------------------------------------------------------------- 1 | import editPageFactory from '../../support/EditPage'; 2 | import loginPageFactory from '../../support/LoginPage'; 3 | 4 | describe('Edit users', () => { 5 | const EditPage = editPageFactory( 6 | '/#/users/f60d593d-9ea9-4add-8f6c-5d86dd8c9f87' 7 | ); 8 | const LoginPage = loginPageFactory('/#/login'); 9 | 10 | beforeEach(() => { 11 | EditPage.navigate(); 12 | EditPage.waitUntilVisible(); 13 | }); 14 | 15 | before(() => { 16 | cy.task('db:teardown'); 17 | cy.task('db:seed'); 18 | 19 | LoginPage.navigate(); 20 | LoginPage.login('admin', 'password'); 21 | }); 22 | 23 | it('should update a user', () => { 24 | EditPage.setInputValue('input', 'plainPassword', 'password'); 25 | EditPage.setInputValue('input', 'plainPasswordRepeat', 'password'); 26 | EditPage.submit(); 27 | EditPage.navigate(); 28 | 29 | cy.get(EditPage.elements.input('username')).should((el) => 30 | expect(el).to.have.value('admin') 31 | ); 32 | cy.get(EditPage.elements.input('plainPassword')).should( 33 | (el) => expect(el).to.be.empty 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/integration/users/list.feature: -------------------------------------------------------------------------------- 1 | #language: es 2 | Característica: Administrar usuarios 3 | 4 | Antecedentes: 5 | Dado que estoy logueado como administrador 6 | 7 | Escenario: Listar usuarios 8 | Dado que estoy en la página de usuarios 9 | Entonces tengo que ver 2 elementos 10 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { getWebpackConfig } = require('@nrwl/cypress/plugins/preprocessor'); 15 | const webpack = require('@cypress/webpack-preprocessor'); 16 | const { teardown, seed } = require('../db'); 17 | 18 | const featureConfig = { 19 | test: /\.feature$/, 20 | use: [ 21 | { 22 | loader: 'cypress-cucumber-preprocessor/loader', 23 | }, 24 | ], 25 | }; 26 | 27 | const featuresConfig = { 28 | test: /\.features$/, 29 | use: [ 30 | { 31 | loader: 'cypress-cucumber-preprocessor/lib/featuresLoader', 32 | }, 33 | ], 34 | }; 35 | 36 | module.exports = (on, config) => { 37 | const webpackConfig = getWebpackConfig(config); 38 | webpackConfig.node = { 39 | fs: 'empty', 40 | child_process: 'empty', 41 | readline: 'empty', 42 | }; 43 | webpackConfig.module.rules.push(featureConfig); 44 | webpackConfig.module.rules.push(featuresConfig); 45 | 46 | const options = { 47 | webpackOptions: webpackConfig, 48 | }; 49 | on('file:preprocessor', webpack(options)); 50 | 51 | on('task', { 52 | 'db:teardown': () => { 53 | return teardown(); 54 | }, 55 | 'db:seed': () => { 56 | return seed(); 57 | }, 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/CreatePage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable cypress/no-unnecessary-waiting */ 2 | export default (url) => ({ 3 | elements: { 4 | addAuthor: '.button-add-authors', 5 | body: 'body', 6 | input: (name, type = 'input') => { 7 | if (type === 'rich-text-input') { 8 | return `.ra-input-${name} .ql-editor`; 9 | } 10 | return `.create-page ${type}[name='${name}']`; 11 | }, 12 | inputs: `.ra-input`, 13 | richTextInputError: '.create-page .ra-rich-text-input-error', 14 | select: '', 15 | snackbar: 'div[role="alert"]', 16 | submitButton: ".create-page div[role='toolbar'] button[type='submit']", 17 | submitAndShowButton: 18 | ".create-page form div[role='toolbar'] button[type='button']:nth-child(2)", 19 | submitAndAddButton: 20 | ".create-page form div[role='toolbar'] button[type='button']:nth-child(3)", 21 | submitCommentable: 22 | ".create-page form div[role='toolbar'] button[type='button']:last-child", 23 | descInput: '.ql-editor', 24 | tab: (index) => `.form-tab:nth-of-type(${index})`, 25 | title: '#react-admin-title', 26 | userMenu: 'button[title="Profile"]', 27 | logout: '.logout', 28 | }, 29 | 30 | navigate() { 31 | cy.visit(url); 32 | }, 33 | 34 | waitUntilVisible() { 35 | cy.get(this.elements.submitButton).should('be.visible'); 36 | }, 37 | 38 | setInputValue(type, name, value, clearPreviousValue = true) { 39 | if (type === 'checkbox') { 40 | if (value === true) { 41 | return cy.get(this.elements.input(name, 'input')).check(); 42 | } 43 | return cy.get(this.elements.input(name, 'input')).check(); 44 | } 45 | if (type === 'select') { 46 | return cy.get(`#${name}`).click().get('div').contains(value).click(); 47 | } 48 | if (clearPreviousValue) { 49 | cy.get(this.elements.input(name, type)).clear(); 50 | } 51 | cy.get(this.elements.input(name, type)).type(value); 52 | if (type === 'rich-text-input') { 53 | cy.wait(500); 54 | } 55 | }, 56 | 57 | setValues(values, clearPreviousValue = true) { 58 | values.forEach((val) => { 59 | this.setInputValue(val.type, val.name, val.value, clearPreviousValue); 60 | }); 61 | }, 62 | 63 | submit() { 64 | cy.get(this.elements.submitButton).click(); 65 | cy.get(this.elements.snackbar); 66 | cy.get(this.elements.body).click(); // dismiss notification 67 | cy.wait(200); // let the notification disappear (could block further submits) 68 | }, 69 | 70 | submitWithKeyboard() { 71 | cy.get("input[type='text']:first").type('{enter}'); 72 | cy.get(this.elements.snackbar); 73 | cy.get(this.elements.body).click(); // dismiss notification 74 | cy.wait(200); // let the notification disappear (could block further submits) 75 | }, 76 | 77 | submitAndShow() { 78 | cy.get(this.elements.submitAndShowButton).click(); 79 | cy.get(this.elements.snackbar); 80 | cy.get(this.elements.body).click(); // dismiss notification 81 | cy.wait(200); // let the notification disappear (could block further submits) 82 | }, 83 | 84 | submitAndAdd() { 85 | cy.get(this.elements.submitAndAddButton).click(); 86 | cy.get(this.elements.snackbar); 87 | cy.get(this.elements.body).click(); // dismiss notification 88 | cy.wait(200); // let the notification disappear (could block further submits) 89 | }, 90 | 91 | submitWithAverageNote() { 92 | cy.get(this.elements.submitCommentable).click(); 93 | cy.get(this.elements.snackbar); 94 | cy.get(this.elements.body).click(); // dismiss notification 95 | cy.wait(200); // let the notification disappear (could block further submits) 96 | }, 97 | 98 | gotoTab(index) { 99 | cy.get(this.elements.tab(index)).click({ force: true }); 100 | }, 101 | 102 | logout() { 103 | cy.get(this.elements.userMenu).click(); 104 | cy.get(this.elements.logout).click(); 105 | }, 106 | }); 107 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/CustomFormPage.js: -------------------------------------------------------------------------------- 1 | export default url => ({ 2 | elements: { 3 | appLoader: '.app-loader', 4 | body: 'body', 5 | input: (name, type = 'input') => `${type}[name='${name}']`, 6 | modalCloseButton: "[data-testid='button-close-modal']", 7 | modalSubmitButton: 8 | "[data-testid='dialog-add-post'] button[type='submit']", 9 | submitAndAddButton: 10 | ".create-page form>div:last-child button[type='button']", 11 | postSelect: '.ra-input-post_id [role="button"]', 12 | postItem: id => `li[data-value="${id}"]`, 13 | showPostCreateModalButton: '[data-testid="button-add-post"]', 14 | showPostPreviewModalButton: '[data-testid="button-show-post"]', 15 | postCreateModal: '[data-testid="dialog-add-post"]', 16 | postPreviewModal: '[data-testid="dialog-show-post"]', 17 | }, 18 | 19 | navigate() { 20 | cy.visit(url); 21 | }, 22 | 23 | setInputValue(type, name, value, clearPreviousValue = true) { 24 | if (clearPreviousValue) { 25 | cy.get(this.elements.input(name, type)).clear(); 26 | } 27 | cy.get(this.elements.input(name, type)).type(value); 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/CustomPage.js: -------------------------------------------------------------------------------- 1 | export default url => ({ 2 | elements: { 3 | appLoader: '.app-loader', 4 | total: '.total', 5 | layout: '.layout', 6 | }, 7 | 8 | navigate() { 9 | cy.visit(url); 10 | }, 11 | 12 | getTotal() { 13 | return cy.get(this.elements.total); 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/EditPage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable cypress/no-unnecessary-waiting */ 2 | export default (url) => ({ 3 | elements: { 4 | body: 'body', 5 | deleteButton: '.ra-delete-button', 6 | input: (name, type = 'input') => { 7 | if (type === 'rich-text-input') { 8 | return `.ra-input-${name} .ql-editor`; 9 | } 10 | if (type === 'reference-array-input') { 11 | return `.ra-input div[role=combobox]`; 12 | } 13 | return `.edit-page [name='${name}']`; 14 | }, 15 | inputs: `.ra-input`, 16 | tabs: `.form-tab`, 17 | snackbar: 'div[role="alert"]', 18 | submitButton: ".edit-page div[role='toolbar'] button[type='submit']", 19 | cloneButton: '.button-clone', 20 | tab: (index) => `.form-tab:nth-of-type(${index})`, 21 | title: '#react-admin-title', 22 | userMenu: 'button[title="Profile"]', 23 | logout: '.logout', 24 | }, 25 | 26 | navigate() { 27 | cy.visit(url); 28 | }, 29 | 30 | waitUntilVisible() { 31 | return cy.get(this.elements.title); 32 | }, 33 | 34 | setInputValue(type, name, value, clearPreviousValue = true) { 35 | if (clearPreviousValue) { 36 | cy.get(this.elements.input(name)).clear(); 37 | } 38 | cy.get(this.elements.input(name)).type(value); 39 | if (type === 'rich-text-input') { 40 | cy.wait(500); 41 | } 42 | }, 43 | 44 | clickInput(name, type = 'input') { 45 | cy.get(this.elements.input(name, type)).click(); 46 | }, 47 | 48 | gotoTab(index) { 49 | cy.get(this.elements.tab(index)).click({ force: true }); 50 | }, 51 | 52 | submit() { 53 | cy.get(this.elements.submitButton).click(); 54 | }, 55 | 56 | delete() { 57 | cy.get(this.elements.deleteButton).click(); 58 | cy.get(this.elements.snackbar); 59 | cy.get(this.elements.body).click(); // dismiss notification 60 | cy.wait(200); // let the notification disappear (could block further submits) 61 | }, 62 | 63 | clone() { 64 | cy.get(this.elements.cloneButton).click(); 65 | }, 66 | 67 | logout() { 68 | cy.get(this.elements.userMenu).click(); 69 | cy.get(this.elements.logout).click(); 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/ListPage.js: -------------------------------------------------------------------------------- 1 | export default url => ({ 2 | elements: { 3 | addFilterButton: '.add-filter', 4 | appLoader: '.app-loader', 5 | displayedRecords: '.displayed-records', 6 | filter: name => `.filter-field[data-source='${name}'] input`, 7 | filterMenuItems: `.new-filter-item`, 8 | menuItems: `[role=menuitem`, 9 | filterMenuItem: source => `.new-filter-item[data-key="${source}"]`, 10 | hideFilterButton: source => 11 | `.filter-field[data-source="${source}"] .hide-filter`, 12 | nextPage: '.next-page', 13 | pageNumber: n => `.page-number[data-page='${n - 1}']`, 14 | previousPage: '.previous-page', 15 | recordRows: '.datagrid-body tr', 16 | viewsColumn: '.datagrid-body tr td:nth-child(7)', 17 | datagridHeaders: 'th', 18 | sortBy: name => `th span[data-field="${name}"]`, 19 | svg: (name, criteria = '') => 20 | `th span[data-field="${name}"] svg${criteria}`, 21 | logout: '.logout', 22 | bulkActionsToolbar: '[data-test=bulk-actions-toolbar]', 23 | customBulkActionsButton: 24 | '[data-test=bulk-actions-toolbar] button[aria-label="Reset views"]', 25 | deleteBulkActionsButton: 26 | '[data-test=bulk-actions-toolbar] button[aria-label="Delete"]', 27 | selectAll: '.select-all', 28 | selectedItem: '.select-item input:checked', 29 | selectItem: '.select-item input', 30 | userMenu: 'button[title="Profile"]', 31 | title: '#react-admin-title', 32 | headroomUnfixed: '.headroom--unfixed', 33 | headroomUnpinned: '.headroom--unpinned', 34 | }, 35 | 36 | navigate() { 37 | cy.visit(url); 38 | }, 39 | 40 | waitUntilVisible() { 41 | return cy.get(this.elements.title); 42 | }, 43 | 44 | waitUntilDataLoaded() { 45 | return cy.get(this.elements.appLoader); 46 | }, 47 | 48 | openFilters() { 49 | cy.get(this.elements.addFilterButton).click(); 50 | }, 51 | 52 | nextPage() { 53 | cy.get(this.elements.nextPage).click({ force: true }); 54 | }, 55 | 56 | previousPage() { 57 | cy.get(this.elements.previousPage).click({ force: true }); 58 | }, 59 | 60 | goToPage(n) { 61 | return cy.get(this.elements.pageNumber(n)).click({ force: true }); 62 | }, 63 | 64 | setFilterValue(name, value, clearPreviousValue = true) { 65 | cy.get(this.elements.filter(name)); 66 | if (clearPreviousValue) { 67 | cy.get(this.elements.filter(name)).clear(); 68 | } 69 | if (value) { 70 | cy.get(this.elements.filter(name)).type(value); 71 | } 72 | }, 73 | 74 | showFilter(name) { 75 | cy.get(this.elements.addFilterButton).click(); 76 | 77 | cy.get(this.elements.filterMenuItem(name)).click(); 78 | }, 79 | 80 | hideFilter(name) { 81 | cy.get(this.elements.hideFilterButton(name)).click(); 82 | }, 83 | 84 | logout() { 85 | cy.get(this.elements.userMenu).click(); 86 | cy.get(this.elements.logout).click(); 87 | }, 88 | 89 | toggleSelectAll() { 90 | cy.get(this.elements.selectAll).click(); 91 | }, 92 | 93 | toggleSelectSomeItems(count) { 94 | cy.get(this.elements.selectItem).then(els => { 95 | for (let i = 0; i < count; i++) { 96 | els[i].click(); 97 | } 98 | }); 99 | }, 100 | 101 | applyUpdateBulkAction() { 102 | cy.get(this.elements.customBulkActionsButton).click(); 103 | }, 104 | 105 | applyDeleteBulkAction() { 106 | cy.get(this.elements.deleteBulkActionsButton).click(); 107 | }, 108 | 109 | toggleColumnSort(name) { 110 | cy.get(this.elements.sortBy(name)).click(); 111 | }, 112 | }); 113 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/LoginPage.js: -------------------------------------------------------------------------------- 1 | export default url => ({ 2 | elements: { 3 | username: "input[name='username']", 4 | password: "input[name='password']", 5 | submitButton: 'button', 6 | }, 7 | 8 | navigate() { 9 | cy.visit(url); 10 | this.waitUntilVisible(); 11 | }, 12 | 13 | waitUntilVisible() { 14 | cy.get(this.elements.username); 15 | }, 16 | 17 | login(username = 'login', password = 'password') { 18 | cy.get(this.elements.username).type(username); 19 | cy.get(this.elements.password).type(password); 20 | cy.get(this.elements.submitButton).click(); 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/ShowPage.js: -------------------------------------------------------------------------------- 1 | export default (url, initialField = 'title') => ({ 2 | elements: { 3 | body: 'body', 4 | field: name => `.ra-field-${name} > div > div > span`, 5 | fields: `.ra-field`, 6 | snackbar: 'div[role="alertdialog"]', 7 | tabs: `.show-tab`, 8 | tab: index => `.show-tab:nth-of-type(${index})`, 9 | userMenu: 'button[title="Profile"]', 10 | logout: '.logout', 11 | }, 12 | 13 | navigate() { 14 | cy.visit(url); 15 | }, 16 | 17 | waitUntilVisible() { 18 | cy.get(this.elements.field(initialField)).should('be.visible'); 19 | }, 20 | 21 | gotoTab(index) { 22 | cy.get(this.elements.tab(index)).click(); 23 | }, 24 | 25 | logout() { 26 | cy.get(this.elements.userMenu).click(); 27 | cy.get(this.elements.logout).click(); 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(username: string, password: string): void; 16 | } 17 | } 18 | 19 | // 20 | // -- This is a parent command -- 21 | Cypress.Commands.add('login', (username, password) => { 22 | cy.clearLocalStorage(); 23 | cy.visit('/#/login'); 24 | cy.get('input[name=username]').type(username); 25 | cy.get('input[name=password]').type(`${password}{enter}`); 26 | }); 27 | // 28 | // -- This is a child command -- 29 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This is a dual command -- 33 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 34 | // 35 | // 36 | // -- This will overwrite an existing command -- 37 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 38 | -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/apps/admin-e2e/src/support/index.js -------------------------------------------------------------------------------- /apps/admin-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/admin-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/admin-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/admin/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nrwl/react/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /apps/admin/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by: 2 | # 1. autoprefixer to adjust CSS to support the below specified browsers 3 | # 2. babel preset-env to adjust included polyfills 4 | # 5 | # For additional information regarding the format and rule options, please see: 6 | # https://github.com/browserslist/browserslist#queries 7 | # 8 | # If you need to support different browsers in production, you may tweak the list below. 9 | 10 | last 1 Chrome version 11 | last 1 Firefox version 12 | last 2 Edge major versions 13 | last 2 Safari major version 14 | last 2 iOS major versions 15 | Firefox ESR 16 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /apps/admin/.env: -------------------------------------------------------------------------------- 1 | API_URL=http://localhost:4200 2 | -------------------------------------------------------------------------------- /apps/admin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/admin/babel-jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript", 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/admin/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'admin', 3 | preset: '../../jest.preset.js', 4 | transform: { 5 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', 6 | '^.+\\.[tj]sx?$': [ 7 | 'babel-jest', 8 | { cwd: __dirname, configFile: './babel-jest.config.json' }, 9 | ], 10 | }, 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 12 | coverageDirectory: '../../coverage/apps/admin', 13 | }; 14 | -------------------------------------------------------------------------------- /apps/admin/proxy-test.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3334", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/admin/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3333", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/admin/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Admin, Resource } from 'react-admin'; 3 | 4 | import { Dashboard } from '../components'; 5 | import { authProvider, dataProvider } from '../lib'; 6 | import { UserCreate, UserEdit, UserList } from '../resources'; 7 | 8 | const App = () => ( 9 | 14 | 20 | 21 | ); 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /apps/admin/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/apps/admin/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/admin/src/components/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader } from '@material-ui/core'; 2 | import React from 'react'; 3 | 4 | export const Dashboard: React.FunctionComponent = () => ( 5 | 6 | 7 | Boilerplate de un admin de react 8 | 9 | ); 10 | -------------------------------------------------------------------------------- /apps/admin/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dashboard'; 2 | export * from './simple-chip-field'; 3 | -------------------------------------------------------------------------------- /apps/admin/src/components/simple-chip-field.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core'; 2 | import Chip from '@material-ui/core/Chip'; 3 | import React from 'react'; 4 | 5 | const useStyles = makeStyles({ 6 | main: { 7 | display: 'flex', 8 | flexWrap: 'wrap', 9 | marginTop: 5, 10 | marginBottom: 5, 11 | }, 12 | chip: { margin: 4 }, 13 | }); 14 | 15 | const SimpleChipField = ({ record = '' }) => { 16 | const classes = useStyles(); 17 | 18 | return record ? ( 19 | 20 | 21 | 22 | ) : null; 23 | }; 24 | 25 | SimpleChipField.defaultProps = { 26 | addLabel: true, 27 | }; 28 | 29 | export default SimpleChipField; 30 | -------------------------------------------------------------------------------- /apps/admin/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/admin/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // When building for production, this file is replaced with `environment.prod.ts`. 3 | 4 | export const environment = { 5 | production: false, 6 | }; 7 | -------------------------------------------------------------------------------- /apps/admin/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/apps/admin/src/favicon.ico -------------------------------------------------------------------------------- /apps/admin/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Admin 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/admin/src/lib/auth.provider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAccessToken, 3 | isCredentials, 4 | isJwtPayload, 5 | JwtPayloadInterface, 6 | } from '@boilerplate/contracts'; 7 | import axios from 'axios'; 8 | import jwt from 'jwt-decode'; 9 | import { AuthProvider } from 'react-admin'; 10 | 11 | const authProvider: AuthProvider = { 12 | login: async ({ ...credentials }) => { 13 | if (!isCredentials(credentials)) { 14 | throw new Error('authProvider - missing attributes in credentials'); 15 | } 16 | const res = await axios.post(`/api/login`, credentials); 17 | 18 | saveToken(res.data); 19 | }, 20 | logout: () => { 21 | removeToken(); 22 | return Promise.resolve(); 23 | }, 24 | checkError: ({ status }) => { 25 | if (status === 401 || status === 403) { 26 | removeToken(); 27 | return Promise.reject(); 28 | } 29 | return Promise.resolve(); 30 | }, 31 | checkAuth: () => { 32 | if (!isLogged()) { 33 | return Promise.reject(); 34 | } 35 | 36 | if (!getDecodedToken().roles.includes('ROLE_ADMIN')) { 37 | return Promise.reject({ 38 | message: 'Acceso exclusivo para administradores', 39 | }); 40 | } 41 | 42 | return Promise.resolve(); 43 | }, 44 | getPermissions: () => { 45 | const { roles } = getDecodedToken(); 46 | return getDecodedToken().roles ? Promise.resolve(roles) : Promise.reject(); 47 | }, 48 | getIdentity: () => { 49 | const { username: id } = getDecodedToken(); 50 | 51 | return Promise.resolve({ id, fullName: id }); 52 | }, 53 | }; 54 | 55 | const saveToken = (token) => { 56 | if (!isAccessToken(token)) { 57 | throw new Error( 58 | 'authProvider - missing attributes in response access token' 59 | ); 60 | } 61 | 62 | const decodedToken = jwt(token.access_token); 63 | 64 | if (!isJwtPayload(decodedToken)) { 65 | throw new Error('authProvider - missing attributes in response payload'); 66 | } 67 | 68 | localStorage.setItem('auth', token.access_token); 69 | }; 70 | 71 | const removeToken = () => localStorage.removeItem('auth'); 72 | 73 | const isLogged = (): boolean => !!getToken(); 74 | 75 | const getToken = (): string | null => localStorage.getItem('auth'); 76 | 77 | const getDecodedToken = (): JwtPayloadInterface => jwt(getToken()); 78 | 79 | export { authProvider, getToken }; 80 | -------------------------------------------------------------------------------- /apps/admin/src/lib/data.provider.ts: -------------------------------------------------------------------------------- 1 | import jsonServerProvider from 'ra-data-json-server'; 2 | import { fetchUtils } from 'react-admin'; 3 | 4 | import { getToken } from './auth.provider'; 5 | 6 | const httpClient = (url, options = { headers: undefined }) => { 7 | if (!options.headers) { 8 | options.headers = new Headers({ Accept: 'application/json' }); 9 | } 10 | 11 | const access_token = getToken(); 12 | options.headers.set('Authorization', `Bearer ${access_token}`); 13 | return fetchUtils.fetchJson(url, options); 14 | }; 15 | 16 | export const dataProvider = jsonServerProvider(`/api`, httpClient); 17 | -------------------------------------------------------------------------------- /apps/admin/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.provider'; 2 | export * from './data.provider'; 3 | -------------------------------------------------------------------------------- /apps/admin/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | import App from './app/app'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | -------------------------------------------------------------------------------- /apps/admin/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polyfill stable language features. These imports will be optimized by `@babel/preset-env`. 3 | * 4 | * See: https://github.com/zloirock/core-js#babel 5 | */ 6 | import 'core-js/stable'; 7 | import 'regenerator-runtime/runtime'; 8 | -------------------------------------------------------------------------------- /apps/admin/src/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; 2 | -------------------------------------------------------------------------------- /apps/admin/src/resources/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-create.resource'; 2 | export * from './user-edit.resource'; 3 | export * from './user-list.resource'; 4 | -------------------------------------------------------------------------------- /apps/admin/src/resources/user/user-create.resource.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Create, 4 | PasswordInput, 5 | required, 6 | SelectInput, 7 | SimpleForm, 8 | TextInput, 9 | } from 'react-admin'; 10 | import * as uuid from 'uuid'; 11 | 12 | import { transformUserForm, validateUserForm } from './user.form'; 13 | 14 | const postDefaultValue = () => ({ id: uuid.v4(), roles: ['ROLE_USER'] }); 15 | 16 | export const UserCreate = (props) => ( 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /apps/admin/src/resources/user/user-edit.resource.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Edit, 4 | PasswordInput, 5 | required, 6 | SelectInput, 7 | SimpleForm, 8 | TextInput, 9 | } from 'react-admin'; 10 | 11 | import { transformUserForm, validateUserForm } from './user.form'; 12 | 13 | export const UserEdit = ({ permissions, ...props }) => ( 14 | 15 | 16 | 17 | 18 | 25 | 26 | 27 | 28 | 29 | ); 30 | -------------------------------------------------------------------------------- /apps/admin/src/resources/user/user-list.resource.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | ArrayField, 4 | Datagrid, 5 | List, 6 | SingleFieldList, 7 | TextField, 8 | } from 'react-admin'; 9 | 10 | import SimpleChipField from '../../components/simple-chip-field'; 11 | 12 | export const UserList = (props) => ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /apps/admin/src/resources/user/user.form.tsx: -------------------------------------------------------------------------------- 1 | export interface UserForm { 2 | id: string; 3 | username: string; 4 | roles: string; 5 | plainPassword: string; 6 | plainPasswordRepeat: string; 7 | } 8 | 9 | export const validateUserForm = (values: UserForm) => { 10 | const errors: Partial = {}; 11 | if (values.plainPassword !== values.plainPasswordRepeat) { 12 | errors.plainPassword = 'Las contraseñas no coinciden'; 13 | } 14 | 15 | return errors; 16 | }; 17 | 18 | export const transformUserForm = (values: UserForm) => ({ 19 | ...values, 20 | roles: Array.isArray(values.roles) ? values.roles : [values.roles], 21 | }); 22 | -------------------------------------------------------------------------------- /apps/admin/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 9 | "../../node_modules/@nrwl/react/typings/image.d.ts" 10 | ], 11 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], 12 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 13 | } 14 | -------------------------------------------------------------------------------- /apps/admin/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "files": [], 10 | "include": [], 11 | "references": [ 12 | { 13 | "path": "./tsconfig.app.json" 14 | }, 15 | { 16 | "path": "./tsconfig.spec.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /apps/admin/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ], 15 | "files": [ 16 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 17 | "../../node_modules/@nrwl/react/typings/image.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /apps/admin/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const nrwlConfig = require('@nrwl/react/plugins/webpack.js'); 3 | 4 | module.exports = (config, context) => { 5 | nrwlConfig(config); 6 | return { 7 | ...config, 8 | node: { global: true, fs: 'empty' }, 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /apps/api/.env: -------------------------------------------------------------------------------- 1 | DB_HOST=localhost 2 | DB_PORT=3306 3 | DB_USER=root 4 | DB_PASSWORD=mysql 5 | DB_DATABASE=boilerplate 6 | -------------------------------------------------------------------------------- /apps/api/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /apps/api/.test.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | -------------------------------------------------------------------------------- /apps/api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'api', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]s$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'js', 'html'], 13 | coverageDirectory: '../../coverage/apps/api', 14 | }; 15 | -------------------------------------------------------------------------------- /apps/api/src/app.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger, NestMiddleware } from '@nestjs/common'; 2 | import { NextFunction, Request, Response } from 'express'; 3 | 4 | @Injectable() 5 | export class AppLoggerMiddleware implements NestMiddleware { 6 | private logger = new Logger('HTTP'); 7 | 8 | use(request: Request, response: Response, next: NextFunction): void { 9 | const { ip, method, originalUrl: url } = request; 10 | const userAgent = request.get('user-agent') || ''; 11 | 12 | response.on('close', () => { 13 | const { statusCode } = response; 14 | 15 | this.logger.log(`${method} ${url} ${statusCode} - ${userAgent} ${ip}`); 16 | }); 17 | 18 | next(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/api/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { DynamicModule, MiddlewareConsumer, NestModule } from '@nestjs/common'; 2 | 3 | import { AppLoggerMiddleware } from './app.middleware'; 4 | import { AuthModule } from './auth/auth.module'; 5 | import { BootstrapModule } from './bootstrap.module'; 6 | import { UserModule } from './user/infrastructure'; 7 | 8 | export class AppModule implements NestModule { 9 | static forRoot(): DynamicModule { 10 | return { 11 | module: this, 12 | imports: [BootstrapModule, AuthModule, UserModule], 13 | }; 14 | } 15 | 16 | configure(consumer: MiddlewareConsumer) { 17 | consumer.apply(AppLoggerMiddleware).forRoutes('*'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/api/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/apps/api/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/api/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | import { JwtModule } from '@nestjs/jwt'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | 6 | import { AuthController } from './controller/auth.controller'; 7 | import { AuthService } from './services/auth.service'; 8 | import { JwtStrategy } from './services/jwt.strategy'; 9 | 10 | @Module({ 11 | imports: [ 12 | CqrsModule, 13 | JwtModule.register({ 14 | secret: process.env.JWT_SECRET, 15 | signOptions: { expiresIn: process.env.JWT_EXPIRES || '60d' }, 16 | }), 17 | PassportModule, 18 | ], 19 | controllers: [AuthController], 20 | providers: [AuthService, JwtStrategy], 21 | exports: [AuthService], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /apps/api/src/auth/controller/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from '@boilerplate/contracts'; 2 | import { UnauthorizedException } from '@nestjs/common'; 3 | import { QueryBus } from '@nestjs/cqrs'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { Test, TestingModule } from '@nestjs/testing'; 6 | 7 | import { AuthService } from '../services/auth.service'; 8 | import { AuthController } from './auth.controller'; 9 | 10 | const ID = '78dbd5bd-86c1-4925-a08c-1d0170e4851d'; 11 | const USERNAME = 'username'; 12 | const PASSWORD = 'password'; 13 | const CRYPT_PASSWORD = 14 | '$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy'; 15 | 16 | describe('AuthController', () => { 17 | let controller: AuthController; 18 | let user: UserDTO; 19 | const queryBus: Partial = {}; 20 | 21 | beforeEach(async () => { 22 | const app: TestingModule = await Test.createTestingModule({ 23 | controllers: [AuthController], 24 | imports: [ 25 | JwtModule.register({ 26 | secret: 'secret', 27 | }), 28 | ], 29 | providers: [ 30 | AuthService, 31 | { 32 | provide: QueryBus, 33 | useValue: queryBus, 34 | }, 35 | ], 36 | }).compile(); 37 | app.useLogger(false); 38 | 39 | user = { 40 | id: ID, 41 | username: USERNAME, 42 | password: CRYPT_PASSWORD, 43 | roles: [], 44 | }; 45 | controller = app.get(AuthController); 46 | queryBus.execute = jest.fn().mockResolvedValue(user); 47 | }); 48 | 49 | it('should be defined', () => { 50 | expect(controller).toBeDefined(); 51 | }); 52 | 53 | it('should login valid user', async () => { 54 | expect( 55 | await controller.login({ username: USERNAME, password: PASSWORD }) 56 | ).toHaveProperty('access_token'); 57 | }); 58 | 59 | it('should not login invalid password', () => { 60 | expect( 61 | controller.login({ username: USERNAME, password: 'wrong password' }) 62 | ).rejects.toThrow(UnauthorizedException); 63 | }); 64 | 65 | it('should not login invalid user', () => { 66 | queryBus.execute = jest.fn().mockResolvedValue(null); 67 | 68 | expect( 69 | controller.login({ username: USERNAME, password: PASSWORD }) 70 | ).rejects.toThrow(UnauthorizedException); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /apps/api/src/auth/controller/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { AccessTokenInterface } from '@boilerplate/contracts'; 2 | import { 3 | Body, 4 | Controller, 5 | Logger, 6 | Post, 7 | UnauthorizedException, 8 | } from '@nestjs/common'; 9 | import { ApiTags } from '@nestjs/swagger'; 10 | 11 | import { LoginDTO } from '../dto/login.dto'; 12 | import { AuthService } from '../services/auth.service'; 13 | 14 | @ApiTags('authorization') 15 | @Controller('login') 16 | export class AuthController { 17 | private readonly logger = new Logger(AuthController.name); 18 | 19 | constructor(private authService: AuthService) {} 20 | 21 | @Post() 22 | async login(@Body() loginDTO: LoginDTO): Promise { 23 | this.logger.debug(`login: ${JSON.stringify(loginDTO)}`); 24 | const { username, password } = loginDTO; 25 | const isValid = await this.authService.validateUser(username, password); 26 | 27 | if (!isValid) { 28 | throw new UnauthorizedException(); 29 | } 30 | 31 | return this.authService.generateAccessToken(username); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /apps/api/src/auth/dto/login.dto.ts: -------------------------------------------------------------------------------- 1 | import { CredentialsInterface } from '@boilerplate/contracts'; 2 | import { ApiProperty } from '@nestjs/swagger'; 3 | 4 | export class LoginDTO implements CredentialsInterface { 5 | @ApiProperty() 6 | username: string; 7 | @ApiProperty() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/src/auth/security/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | Admin = 'ROLE_ADMIN', 3 | User = 'ROLE_USER', 4 | } 5 | -------------------------------------------------------------------------------- /apps/api/src/auth/security/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '@boilerplate/contracts'; 2 | import { SetMetadata } from '@nestjs/common'; 3 | 4 | export const ROLES_KEY = 'roles'; 5 | export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles); 6 | -------------------------------------------------------------------------------- /apps/api/src/auth/security/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { Role, UserDTO } from '@boilerplate/contracts'; 2 | import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; 3 | import { Reflector } from '@nestjs/core'; 4 | import { AuthGuard } from '@nestjs/passport'; 5 | 6 | import { ROLES_KEY } from './roles.decorator'; 7 | 8 | @Injectable() 9 | export class RolesGuard extends AuthGuard('jwt') { 10 | private readonly logger = new Logger(RolesGuard.name); 11 | 12 | constructor(private reflector: Reflector) { 13 | super(); 14 | } 15 | 16 | handleRequest(err, user, info, context) { 17 | const roles = this.reflector.get(ROLES_KEY, context.getHandler()); 18 | 19 | if (!roles) { 20 | return user; 21 | } 22 | 23 | this.logger.debug('roles: ' + JSON.stringify(roles)); 24 | 25 | if (err || !user || !this.userHasRequiredRoles(user, roles)) { 26 | throw new UnauthorizedException(); 27 | } 28 | 29 | return user; 30 | } 31 | 32 | private userHasRequiredRoles(user: UserDTO, roles: string[]) { 33 | return user.roles.some((role) => roles.includes(role)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/api/src/auth/services/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { QueryBus } from '@nestjs/cqrs'; 2 | import { JwtModule } from '@nestjs/jwt'; 3 | import { Test } from '@nestjs/testing'; 4 | import * as bcrypt from 'bcrypt'; 5 | 6 | import { AuthService } from './auth.service'; 7 | 8 | const PASSWORD = 'password'; 9 | 10 | describe('AuthService', () => { 11 | let authService: AuthService; 12 | const queryBus: Partial = {}; 13 | 14 | beforeEach(async () => { 15 | const app = await Test.createTestingModule({ 16 | imports: [ 17 | JwtModule.register({ 18 | secret: 'secret', 19 | }), 20 | ], 21 | providers: [ 22 | AuthService, 23 | { 24 | provide: QueryBus, 25 | useValue: queryBus, 26 | }, 27 | ], 28 | }).compile(); 29 | 30 | authService = app.get(AuthService); 31 | }); 32 | 33 | it('encodes password', async () => { 34 | const encoded = await authService.encodePassword(PASSWORD); 35 | 36 | expect(bcrypt.compareSync(PASSWORD, encoded)).toBeTruthy(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /apps/api/src/auth/services/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccessTokenInterface, 3 | JwtPayloadInterface, 4 | UserDTO, 5 | } from '@boilerplate/contracts'; 6 | import { Injectable } from '@nestjs/common'; 7 | import { QueryBus } from '@nestjs/cqrs'; 8 | import { JwtService } from '@nestjs/jwt'; 9 | import * as bcrypt from 'bcrypt'; 10 | 11 | import { GetUserByUsernameQuery } from '../../user/application'; 12 | 13 | @Injectable() 14 | export class AuthService { 15 | constructor(private queryBus: QueryBus, private jwtService: JwtService) {} 16 | 17 | async encodePassword(password: string): Promise { 18 | const salt = await bcrypt.genSalt(); 19 | 20 | return await bcrypt.hashSync(password, salt); 21 | } 22 | 23 | async validateUser(username: string, password: string): Promise { 24 | const user = await this.queryBus.execute( 25 | new GetUserByUsernameQuery(username) 26 | ); 27 | 28 | return user && (await bcrypt.compareSync(password, user.password)); 29 | } 30 | 31 | async generateAccessToken(username: string): Promise { 32 | const user = await this.queryBus.execute( 33 | new GetUserByUsernameQuery(username) 34 | ); 35 | 36 | const payload: JwtPayloadInterface = { 37 | username: user.username, 38 | roles: user.roles, 39 | }; 40 | 41 | return { 42 | access_token: this.jwtService.sign(payload, { 43 | algorithm: 'HS512', 44 | }), 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/api/src/auth/services/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { JwtPayloadInterface, UserDTO } from '@boilerplate/contracts'; 2 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 3 | import { QueryBus } from '@nestjs/cqrs'; 4 | import { PassportStrategy } from '@nestjs/passport'; 5 | import { ExtractJwt, Strategy } from 'passport-jwt'; 6 | 7 | import { GetUserByUsernameQuery } from '../../user/application'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor(private queryBus: QueryBus) { 12 | super({ 13 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 14 | ignoreExpiration: false, 15 | secretOrKey: process.env.JWT_SECRET, 16 | }); 17 | } 18 | 19 | async validate(payload: JwtPayloadInterface): Promise { 20 | const user = await this.queryBus.execute( 21 | new GetUserByUsernameQuery(payload.username) 22 | ); 23 | 24 | if (!user) { 25 | throw new UnauthorizedException(); 26 | } 27 | 28 | return user; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /apps/api/src/bootstrap.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { APP_GUARD } from '@nestjs/core'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | 5 | import { RolesGuard } from './auth/security/roles.guard'; 6 | import { configService } from './config/config.service'; 7 | 8 | @Module({ 9 | imports: [TypeOrmModule.forRoot(configService.getTypeOrmConfig())], 10 | providers: [ 11 | { 12 | provide: APP_GUARD, 13 | useClass: RolesGuard, 14 | }, 15 | ], 16 | }) 17 | export class BootstrapModule {} 18 | -------------------------------------------------------------------------------- /apps/api/src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmModuleOptions } from '@nestjs/typeorm'; 2 | import path = require('path'); 3 | 4 | class ConfigService { 5 | constructor(private env: { [k: string]: string | undefined }) {} 6 | 7 | private getValue(key: string, throwOnMissing = true): string { 8 | const value = process.env[key]; 9 | if (!value && throwOnMissing) { 10 | throw new Error(`config error - missing env.${key}`); 11 | } 12 | 13 | return value || ''; 14 | } 15 | 16 | public ensureValues(keys: string[]) { 17 | keys.forEach((k) => this.getValue(k, true)); 18 | return this; 19 | } 20 | 21 | public getPort() { 22 | return this.getValue('PORT', true); 23 | } 24 | 25 | public isTest() { 26 | const mode = this.getValue('NODE_ENV', false); 27 | return mode === 'test'; 28 | } 29 | 30 | public getTypeOrmConfig(): TypeOrmModuleOptions { 31 | if (this.isTest()) { 32 | return { 33 | type: 'sqlite', 34 | database: path.join(__dirname, '../../../tmp/test.sqlite3'), 35 | dropSchema: true, 36 | entities: [ 37 | path.join(__dirname, '../../../../libs/**/*.entity{.ts,.js}'), 38 | ], 39 | synchronize: true, 40 | autoLoadEntities: true, 41 | logging: false, 42 | }; 43 | } 44 | 45 | return { 46 | type: 'mariadb', 47 | 48 | host: this.getValue('DB_HOST'), 49 | port: parseInt(this.getValue('DB_PORT')), 50 | username: this.getValue('DB_USER'), 51 | password: this.getValue('DB_PASSWORD'), 52 | database: this.getValue('DB_DATABASE'), 53 | 54 | migrationsTableName: 'migrations', 55 | 56 | entities: [path.join(__dirname, '../../../../libs/**/*.entity{.ts,.js}')], 57 | migrations: [path.join(__dirname, '../migrations/*{.ts,.js}')], 58 | 59 | cli: { 60 | migrationsDir: 'apps/api/src/migrations', 61 | }, 62 | 63 | keepConnectionAlive: true, 64 | autoLoadEntities: true, 65 | synchronize: this.isTest(), 66 | }; 67 | } 68 | } 69 | 70 | const configService = new ConfigService(process.env).ensureValues([ 71 | 'DB_HOST', 72 | 'DB_PORT', 73 | 'DB_USER', 74 | 'DB_PASSWORD', 75 | 'DB_DATABASE', 76 | ]); 77 | 78 | export { configService }; 79 | -------------------------------------------------------------------------------- /apps/api/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: false, 3 | }; 4 | -------------------------------------------------------------------------------- /apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 4 | 5 | import { AppModule } from './app.module'; 6 | 7 | const GLOBAL_PREFIX = 'api'; 8 | 9 | async function bootstrap() { 10 | const app = await NestFactory.create(AppModule.forRoot(), { 11 | logger: 12 | process.env.NODE_ENV == 'development' 13 | ? ['debug', 'error', 'log', 'verbose', 'warn'] 14 | : ['error', 'warn'], 15 | }); 16 | app.setGlobalPrefix(GLOBAL_PREFIX); 17 | 18 | const options = new DocumentBuilder() 19 | .addBearerAuth() 20 | .setTitle('Boilerplate API') 21 | .setVersion('1.0') 22 | .build(); 23 | 24 | const document = SwaggerModule.createDocument(app, options, {}); 25 | SwaggerModule.setup(GLOBAL_PREFIX, app, document); 26 | 27 | const port = process.env.PORT || 3333; 28 | await app.listen(port, () => { 29 | Logger.log('Listening at http://localhost:' + port + '/' + GLOBAL_PREFIX); 30 | }); 31 | } 32 | 33 | bootstrap(); 34 | -------------------------------------------------------------------------------- /apps/api/src/migrations/1609184562041-add-user.ts: -------------------------------------------------------------------------------- 1 | import { MigrationInterface, QueryRunner } from 'typeorm'; 2 | 3 | export class addUser1609184562041 implements MigrationInterface { 4 | name = 'addUser1609184562041'; 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | await queryRunner.query( 8 | 'CREATE TABLE `users` (`id` varchar(36) NOT NULL, `username` varchar(255) NOT NULL, `password` varchar(70) NOT NULL, `roles` text NOT NULL, UNIQUE INDEX `IDX_fe0bb3f6520ee0469504521e71` (`username`), PRIMARY KEY (`id`)) ENGINE=InnoDB' 9 | ); 10 | await queryRunner.query( 11 | "INSERT INTO `users` (`id`, `username`, `password`, `roles`) VALUES ('f60d593d-9ea9-4add-8f6c-5d86dd8c9f87','admin', '$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy', 'ROLE_ADMIN') " 12 | ); 13 | } 14 | 15 | public async down(queryRunner: QueryRunner): Promise { 16 | await queryRunner.query( 17 | 'DROP INDEX `IDX_fe0bb3f6520ee0469504521e71` ON `users`' 18 | ); 19 | await queryRunner.query('DROP TABLE `users`'); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/create-user.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs'; 2 | 3 | export class CreateUserCommand implements ICommand { 4 | constructor( 5 | public readonly userId: string, 6 | public readonly username: string, 7 | public readonly password: string, 8 | public readonly roles: string[] 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/create-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; 3 | 4 | import { 5 | Password, 6 | Role, 7 | User, 8 | UserId, 9 | Username, 10 | USERS, 11 | Users, 12 | } from '../../domain'; 13 | import { 14 | UserIdAlreadyTakenError, 15 | UsernameAlreadyTakenError, 16 | } from '../../domain/exception/'; 17 | import { UserMapper } from '../../infrastructure/repository/user.mapper'; 18 | import { CreateUserCommand } from './create-user.command'; 19 | 20 | @CommandHandler(CreateUserCommand) 21 | export class CreateUserHandler implements ICommandHandler { 22 | constructor( 23 | @Inject(USERS) private users: Users, 24 | private userMapper: UserMapper 25 | ) {} 26 | 27 | async execute(command: CreateUserCommand) { 28 | const userId = UserId.fromString(command.userId); 29 | const username = Username.fromString(command.username); 30 | const password = Password.fromString(command.password); 31 | 32 | if (await this.users.find(userId)) { 33 | throw UserIdAlreadyTakenError.with(userId); 34 | } 35 | 36 | if (await this.users.findOneByUsername(username)) { 37 | throw UsernameAlreadyTakenError.with(username); 38 | } 39 | 40 | const user = User.add(userId, username, password); 41 | command.roles.map((role: string) => user.addRole(Role.fromString(role))); 42 | 43 | this.users.save(user); 44 | 45 | return this.userMapper.aggregateToEntity(user); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/delete-user.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs'; 2 | 3 | export class DeleteUserCommand implements ICommand { 4 | constructor(public readonly userId: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/delete-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; 3 | 4 | import { UserId, UserIdNotFoundError, USERS, Users } from '../../domain'; 5 | import { UserMapper } from '../../infrastructure/repository/user.mapper'; 6 | import { DeleteUserCommand } from './delete-user.command'; 7 | 8 | @CommandHandler(DeleteUserCommand) 9 | export class DeleteUserHandler implements ICommandHandler { 10 | constructor( 11 | @Inject(USERS) private users: Users, 12 | private userMapper: UserMapper 13 | ) {} 14 | 15 | async execute(command: DeleteUserCommand) { 16 | const userId = UserId.fromString(command.userId); 17 | 18 | const user = await this.users.find(userId); 19 | if (!user) { 20 | throw UserIdNotFoundError.with(userId); 21 | } 22 | 23 | user.delete(); 24 | 25 | this.users.save(user); 26 | 27 | return this.userMapper.aggregateToEntity(user); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-user.command'; 2 | export * from './delete-user.command'; 3 | export * from './update-user.command'; 4 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/update-user.command.ts: -------------------------------------------------------------------------------- 1 | import { ICommand } from '@nestjs/cqrs'; 2 | 3 | export class UpdateUserCommand implements ICommand { 4 | constructor( 5 | public readonly userId: string, 6 | public readonly username: string, 7 | public readonly password: string | null, 8 | public readonly roles: string[] 9 | ) {} 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/user/application/command/update-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; 3 | 4 | import { 5 | Password, 6 | Role, 7 | User, 8 | UserId, 9 | UserIdNotFoundError, 10 | USERS, 11 | Users, 12 | } from '../../domain'; 13 | import { UserMapper } from '../../infrastructure/repository/user.mapper'; 14 | import { UpdateUserCommand } from './update-user.command'; 15 | 16 | @CommandHandler(UpdateUserCommand) 17 | export class UpdateUserHandler implements ICommandHandler { 18 | constructor( 19 | @Inject(USERS) private users: Users, 20 | private userMapper: UserMapper 21 | ) {} 22 | 23 | async execute(command: UpdateUserCommand) { 24 | const userId = UserId.fromString(command.userId); 25 | 26 | const user = await this.users.find(userId); 27 | if (!user) { 28 | throw UserIdNotFoundError.with(userId); 29 | } 30 | 31 | // TODO: this.updateUsername(user, command); 32 | this.updatePassword(user, command); 33 | this.updateRoles(user, command); 34 | 35 | this.users.save(user); 36 | 37 | return this.userMapper.aggregateToEntity(user); 38 | } 39 | 40 | private updatePassword(user: User, command: UpdateUserCommand) { 41 | command.password && 42 | user.updatePassword(Password.fromString(command.password)); 43 | } 44 | 45 | private updateRoles(user: User, command: UpdateUserCommand) { 46 | user.roles.map( 47 | (role) => !command.roles.includes(role.value) && user.removeRole(role) 48 | ); 49 | command.roles.map((role) => user.addRole(Role.fromString(role))); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/api/src/user/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './command'; 2 | export * from './query'; 3 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/get-user-by-username.handler.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from '@boilerplate/contracts'; 2 | import { Inject } from '@nestjs/common'; 3 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; 4 | 5 | import { Username } from '../../domain/model/username'; 6 | import { USERS, Users } from '../../domain/repository/users'; 7 | import { UserMapper } from '../../infrastructure/repository/user.mapper'; 8 | import { GetUserByUsernameQuery } from './get-user-by-username.query'; 9 | 10 | @QueryHandler(GetUserByUsernameQuery) 11 | export class GetUserByUsernameHandler 12 | implements IQueryHandler { 13 | constructor( 14 | @Inject(USERS) private users: Users, 15 | private userMapper: UserMapper 16 | ) {} 17 | 18 | async execute(query: GetUserByUsernameQuery): Promise { 19 | const user = await this.users.findOneByUsername( 20 | Username.fromString(query.username) 21 | ); 22 | 23 | if (!user) { 24 | return null; 25 | } 26 | 27 | return this.userMapper.aggregateToEntity(user); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/get-user-by-username.query.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from '@nestjs/cqrs'; 2 | 3 | export class GetUserByUsernameQuery implements IQuery { 4 | constructor(public readonly username: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/get-user.handler.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from '@boilerplate/contracts'; 2 | import { Inject } from '@nestjs/common'; 3 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; 4 | 5 | import { UserId } from '../../domain'; 6 | import { USERS, Users } from '../../domain/repository/users'; 7 | import { UserMapper } from '../../infrastructure/repository/user.mapper'; 8 | import { GetUserQuery } from './get-user.query'; 9 | 10 | @QueryHandler(GetUserQuery) 11 | export class GetUserHandler implements IQueryHandler { 12 | constructor( 13 | @Inject(USERS) private users: Users, 14 | private userMapper: UserMapper 15 | ) {} 16 | 17 | async execute(query: GetUserQuery): Promise { 18 | const user = await this.users.find(UserId.fromString(query.id)); 19 | 20 | if (!user) { 21 | return null; 22 | } 23 | 24 | return this.userMapper.aggregateToEntity(user); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/get-user.query.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from '@nestjs/cqrs'; 2 | 3 | export class GetUserQuery implements IQuery { 4 | constructor(public readonly id: string) {} 5 | } 6 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/get-users.handler.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from '@boilerplate/contracts'; 2 | import { Inject } from '@nestjs/common'; 3 | import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; 4 | 5 | import { USERS, Users } from '../../domain'; 6 | import { UserMapper } from '../../infrastructure/repository/user.mapper'; 7 | import { GetUsersQuery } from './get-users.query'; 8 | 9 | @QueryHandler(GetUsersQuery) 10 | export class GetUsersHandler implements IQueryHandler { 11 | constructor( 12 | @Inject(USERS) private users: Users, 13 | private userMapper: UserMapper 14 | ) {} 15 | 16 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 17 | async execute(query: GetUsersQuery): Promise { 18 | const users = await this.users.findAll(); 19 | 20 | return users.map(this.userMapper.aggregateToEntity); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/get-users.query.ts: -------------------------------------------------------------------------------- 1 | import { IQuery } from '@nestjs/cqrs'; 2 | 3 | export class GetUsersQuery implements IQuery {} 4 | -------------------------------------------------------------------------------- /apps/api/src/user/application/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-user.query'; 2 | export * from './get-user-by-username.query'; 3 | export * from './get-users.query'; 4 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/event/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-password-was-updated.event'; 2 | export * from './user-role-was-added.event'; 3 | export * from './user-role-was-removed.event'; 4 | export * from './user-was-created.event'; 5 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/event/user-password-was-updated.event.ts: -------------------------------------------------------------------------------- 1 | import { StorableEvent } from 'event-sourcing-nestjs'; 2 | 3 | export class UserPasswordWasUpdated extends StorableEvent { 4 | eventAggregate = 'user'; 5 | eventVersion = 1; 6 | 7 | constructor(public readonly id: string, public readonly password: string) { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/event/user-role-was-added.event.ts: -------------------------------------------------------------------------------- 1 | import { StorableEvent } from 'event-sourcing-nestjs'; 2 | 3 | export class UserRoleWasAdded extends StorableEvent { 4 | eventAggregate = 'user'; 5 | eventVersion = 1; 6 | 7 | constructor(public readonly id: string, public readonly role: string) { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/event/user-role-was-removed.event.ts: -------------------------------------------------------------------------------- 1 | import { StorableEvent } from 'event-sourcing-nestjs'; 2 | 3 | export class UserRoleWasRemoved extends StorableEvent { 4 | eventAggregate = 'user'; 5 | eventVersion = 1; 6 | 7 | constructor(public readonly id: string, public readonly role: string) { 8 | super(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/event/user-was-created.event.ts: -------------------------------------------------------------------------------- 1 | import { StorableEvent } from 'event-sourcing-nestjs'; 2 | 3 | export class UserWasCreated extends StorableEvent { 4 | eventAggregate = 'user'; 5 | eventVersion = 1; 6 | 7 | constructor( 8 | public readonly id: string, 9 | public readonly username: string, 10 | public readonly password: string 11 | ) { 12 | super(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/event/user-was-deleted.event.ts: -------------------------------------------------------------------------------- 1 | import { StorableEvent } from 'event-sourcing-nestjs'; 2 | 3 | export class UserWasDeleted extends StorableEvent { 4 | eventAggregate = 'user'; 5 | eventVersion = 1; 6 | public readonly createdOn = new Date(); 7 | 8 | constructor(public readonly id: string) { 9 | super(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/exception/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-id-already-taken.error'; 2 | export * from './user-id-not-found.error'; 3 | export * from './username-already-taken.error'; 4 | export * from './username-not-found.error'; 5 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/exception/user-id-already-taken.error.ts: -------------------------------------------------------------------------------- 1 | import { UserId } from '../model'; 2 | 3 | export class UserIdAlreadyTakenError extends Error { 4 | public static with(userId: UserId): UserIdAlreadyTakenError { 5 | return new UserIdAlreadyTakenError(`User id ${userId.value} already taken`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/exception/user-id-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { UserId } from '../model'; 2 | 3 | export class UserIdNotFoundError extends Error { 4 | public static with(userId: UserId): UserIdNotFoundError { 5 | return new UserIdNotFoundError(`User id ${userId.value} not found`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/exception/username-already-taken.error.ts: -------------------------------------------------------------------------------- 1 | import { Username } from '../model'; 2 | 3 | export class UsernameAlreadyTakenError extends Error { 4 | public static with(username: Username): UsernameAlreadyTakenError { 5 | return new UsernameAlreadyTakenError( 6 | `Username ${username.value} already taken` 7 | ); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/exception/username-not-found.error.ts: -------------------------------------------------------------------------------- 1 | import { Username } from '../model'; 2 | 3 | export class UsernameNotFoundError extends Error { 4 | public static with(username: Username): UsernameNotFoundError { 5 | return new UsernameNotFoundError(`Username ${username.value} not found`); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event'; 2 | export * from './exception'; 3 | export * from './model'; 4 | export * from './repository'; 5 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './password'; 2 | export * from './role'; 3 | export * from './user'; 4 | export * from './user-id'; 5 | export * from './username'; 6 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/password.spec.ts: -------------------------------------------------------------------------------- 1 | import { Password } from './password'; 2 | 3 | describe('Password', () => { 4 | it('should be a string', () => { 5 | expect(Password.fromString('password').value).toBe('password'); 6 | }); 7 | 8 | it('should not be empty', () => { 9 | expect(() => { 10 | Password.fromString(''); 11 | }).toThrow(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/password.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@boilerplate/domain'; 2 | 3 | interface Props { 4 | value: string; 5 | } 6 | 7 | export class Password extends ValueObject { 8 | public static fromString(name: string): Password { 9 | if (name.length < 8) { 10 | throw new Error('Password too short'); 11 | } 12 | 13 | return new Password({ value: name }); 14 | } 15 | 16 | get value(): string { 17 | return this.props.value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/role.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@boilerplate/domain'; 2 | 3 | interface Props { 4 | value: string; 5 | } 6 | 7 | export class Role extends ValueObject { 8 | public static fromString(name: string): Role { 9 | if (name.length === 0) { 10 | throw new Error('Role cannot be empty'); 11 | } 12 | 13 | name = name.toUpperCase(); 14 | 15 | if (!/^[A-Z_]+$/.test(name)) { 16 | throw new Error('Invalid role characters'); 17 | } 18 | 19 | return new Role({ value: name }); 20 | } 21 | 22 | get value(): string { 23 | return this.props.value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/user-id.ts: -------------------------------------------------------------------------------- 1 | import { Id } from '@boilerplate/domain'; 2 | import * as uuid from 'uuid'; 3 | 4 | export class UserId extends Id { 5 | static generate(): UserId { 6 | return new UserId(uuid.v4()); 7 | } 8 | 9 | public static fromString(id: string): UserId { 10 | return new UserId(id); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/user.ts: -------------------------------------------------------------------------------- 1 | import { AggregateRoot } from '@nestjs/cqrs'; 2 | 3 | import { 4 | UserPasswordWasUpdated, 5 | UserRoleWasAdded, 6 | UserRoleWasRemoved, 7 | UserWasCreated, 8 | } from '../event'; 9 | import { UserWasDeleted } from '../event/user-was-deleted.event'; 10 | import { Password } from './password'; 11 | import { Role } from './role'; 12 | import { UserId } from './user-id'; 13 | import { Username } from './username'; 14 | 15 | export class User extends AggregateRoot { 16 | private _userId: UserId; 17 | private _username: Username; 18 | private _password: Password; 19 | private _roles: Role[]; 20 | private _deleted?: Date; 21 | 22 | private constructor() { 23 | super(); 24 | } 25 | 26 | public static add( 27 | userId: UserId, 28 | username: Username, 29 | password: Password 30 | ): User { 31 | const user = new User(); 32 | 33 | user.apply( 34 | new UserWasCreated(userId.value, username.value, password.value) 35 | ); 36 | 37 | return user; 38 | } 39 | 40 | get id(): UserId { 41 | return this._userId; 42 | } 43 | 44 | get username(): Username { 45 | return this._username; 46 | } 47 | 48 | get password(): Password { 49 | return this._password; 50 | } 51 | 52 | get roles(): Role[] { 53 | return Array.from(this._roles); 54 | } 55 | 56 | hasRole(role: Role): boolean { 57 | return this._roles.some((item: Role) => item.equals(role)); 58 | } 59 | 60 | addRole(role: Role): void { 61 | if (this.hasRole(role)) { 62 | return; 63 | } 64 | 65 | this.apply(new UserRoleWasAdded(this.id.value, role.value)); 66 | } 67 | 68 | removeRole(role: Role): void { 69 | if (!this.hasRole(role)) { 70 | return; 71 | } 72 | 73 | this.apply(new UserRoleWasRemoved(this.id.value, role.value)); 74 | } 75 | 76 | updatePassword(password: Password): void { 77 | if (this._password.equals(password)) { 78 | return; 79 | } 80 | 81 | this.apply(new UserPasswordWasUpdated(this.id.value, password.value)); 82 | } 83 | 84 | delete(): void { 85 | if (this._deleted) { 86 | return; 87 | } 88 | 89 | this.apply(new UserWasDeleted(this.id.value)); 90 | } 91 | 92 | private onUserWasCreated(event: UserWasCreated) { 93 | this._userId = UserId.fromString(event.id); 94 | this._username = Username.fromString(event.username); 95 | this._password = Password.fromString(event.password); 96 | this._roles = []; 97 | this._deleted = undefined; 98 | } 99 | 100 | private onUserRoleWasAdded(event: UserRoleWasAdded) { 101 | this._roles.push(Role.fromString(event.role)); 102 | } 103 | 104 | private onUserRoleWasRemoved(event: UserRoleWasRemoved) { 105 | this._roles = this._roles.filter( 106 | (item: Role) => !item.equals(Role.fromString(event.role)) 107 | ); 108 | } 109 | 110 | private onUserPasswordWasUpdated(event: UserPasswordWasUpdated) { 111 | this._password = Password.fromString(event.password); 112 | } 113 | 114 | private onUserWasDeleted(event: UserWasDeleted) { 115 | this._deleted = event.createdOn; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/username.spec.ts: -------------------------------------------------------------------------------- 1 | import { Username } from './username'; 2 | 3 | describe('Username', () => { 4 | it('should be a lowercase string without spaces', () => { 5 | expect(Username.fromString('username').value).toBe('username'); 6 | }); 7 | it('should not have spaces', () => { 8 | expect(() => { 9 | Username.fromString('user name'); 10 | }).toThrow(); 11 | }); 12 | 13 | it('should not be empty', () => { 14 | expect(() => { 15 | Username.fromString(''); 16 | }).toThrow(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/model/username.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from '@boilerplate/domain'; 2 | 3 | interface Props { 4 | value: string; 5 | } 6 | 7 | export class Username extends ValueObject { 8 | public static fromString(name: string): Username { 9 | if (name.length === 0) { 10 | throw new Error('Username cannot be empty'); 11 | } 12 | 13 | if (!/^[a-zA-Z0-9ñÑ]+$/.test(name)) { 14 | throw new Error('Invalid username characters'); 15 | } 16 | 17 | return new Username({ value: name }); 18 | } 19 | 20 | get value(): string { 21 | return this.props.value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from './users'; 2 | -------------------------------------------------------------------------------- /apps/api/src/user/domain/repository/users.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../model/user'; 2 | import { UserId } from '../model/user-id'; 3 | import { Username } from '../model/username'; 4 | 5 | export interface Users { 6 | find(userId: UserId): Promise; 7 | findAll(): Promise; 8 | findOneByUsername(username: Username): Promise; 9 | save(user: User): void; 10 | } 11 | 12 | export const USERS = 'USERS'; 13 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/controller/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CreateUserDTO, 3 | EditUserDTO, 4 | Role, 5 | UserDTO, 6 | } from '@boilerplate/contracts'; 7 | import { 8 | BadRequestException, 9 | Body, 10 | ClassSerializerInterceptor, 11 | Controller, 12 | Delete, 13 | Get, 14 | HttpCode, 15 | NotFoundException, 16 | Param, 17 | Post, 18 | Put, 19 | Res, 20 | UseInterceptors, 21 | } from '@nestjs/common'; 22 | import { CommandBus, QueryBus } from '@nestjs/cqrs'; 23 | import { 24 | ApiBearerAuth, 25 | ApiOperation, 26 | ApiResponse, 27 | ApiTags, 28 | } from '@nestjs/swagger'; 29 | import { Response } from 'express'; 30 | 31 | import { Roles } from '../../../auth/security/roles.decorator'; 32 | import { AuthService } from '../../../auth/services/auth.service'; 33 | import { 34 | CreateUserCommand, 35 | DeleteUserCommand, 36 | GetUserQuery, 37 | GetUsersQuery, 38 | UpdateUserCommand, 39 | } from '../../application'; 40 | import { UserIdNotFoundError } from '../../domain'; 41 | 42 | @ApiBearerAuth() 43 | @ApiTags('users') 44 | @Controller('users') 45 | @UseInterceptors(ClassSerializerInterceptor) 46 | export class UserController { 47 | constructor( 48 | private authService: AuthService, 49 | private queryBus: QueryBus, 50 | private commandBus: CommandBus 51 | ) {} 52 | 53 | @Post() 54 | @Roles(Role.Admin) 55 | @ApiResponse({ status: 200, description: 'User created' }) 56 | async create(@Body() createUserDto: CreateUserDTO): Promise { 57 | try { 58 | const password = await this.authService.encodePassword( 59 | createUserDto.plainPassword 60 | ); 61 | 62 | return await this.commandBus.execute( 63 | new CreateUserCommand( 64 | createUserDto.id, 65 | createUserDto.username, 66 | password, 67 | createUserDto.roles 68 | ) 69 | ); 70 | } catch (e) { 71 | if (e instanceof Error) { 72 | throw new BadRequestException(e.message); 73 | } else { 74 | throw new BadRequestException('Server error'); 75 | } 76 | } 77 | } 78 | 79 | @Get() 80 | @Roles(Role.Admin) 81 | @ApiResponse({ status: 200, description: 'Users found' }) 82 | async findAll(@Res({ passthrough: true }) res: Response) { 83 | try { 84 | const users = await this.queryBus.execute( 85 | new GetUsersQuery() 86 | ); 87 | 88 | res.setHeader('X-Total-Count', users.length); 89 | 90 | return users; 91 | } catch (e) { 92 | if (e instanceof Error) { 93 | throw new BadRequestException(e.message); 94 | } else { 95 | throw new BadRequestException('Server error'); 96 | } 97 | } 98 | } 99 | 100 | @Get(':id') 101 | @Roles(Role.Admin) 102 | @ApiResponse({ status: 200, description: 'User found' }) 103 | @ApiResponse({ status: 404, description: 'Not found' }) 104 | async findOne(@Param('id') id: string): Promise { 105 | try { 106 | const user = await this.queryBus.execute( 107 | new GetUserQuery(id) 108 | ); 109 | 110 | if (!user) throw new NotFoundException(); 111 | 112 | return user; 113 | } catch (e) { 114 | if (e instanceof UserIdNotFoundError) { 115 | throw new NotFoundException('User not found'); 116 | } else if (e instanceof Error) { 117 | throw new BadRequestException(e.message); 118 | } else { 119 | throw new BadRequestException('Server error'); 120 | } 121 | } 122 | } 123 | 124 | @Put(':id') 125 | @Roles(Role.Admin) 126 | @ApiOperation({ summary: 'Updated user' }) 127 | @ApiResponse({ status: 200, description: 'User updated' }) 128 | @ApiResponse({ status: 404, description: 'Not found' }) 129 | async update( 130 | @Param('id') id: string, 131 | @Body() editUserDTO: EditUserDTO 132 | ): Promise { 133 | try { 134 | const user = await this.queryBus.execute( 135 | new GetUserQuery(id) 136 | ); 137 | 138 | if (!user) throw new NotFoundException(); 139 | 140 | return this.commandBus.execute( 141 | new UpdateUserCommand( 142 | id, 143 | editUserDTO.username, 144 | editUserDTO.plainPassword, 145 | editUserDTO.roles 146 | ) 147 | ); 148 | } catch (e) { 149 | if (e instanceof UserIdNotFoundError) { 150 | throw new NotFoundException('User not found'); 151 | } else if (e instanceof Error) { 152 | throw new BadRequestException(e.message); 153 | } else { 154 | throw new BadRequestException('Server error'); 155 | } 156 | } 157 | } 158 | 159 | @ApiOperation({ summary: 'Delete user' }) 160 | @ApiResponse({ status: 200, description: 'Delete user' }) 161 | @ApiResponse({ status: 404, description: 'Not found' }) 162 | @HttpCode(200) 163 | @Delete(':id') 164 | @Roles(Role.Admin) 165 | async remove(@Param('id') id: string): Promise { 166 | try { 167 | return this.commandBus.execute(new DeleteUserCommand(id)); 168 | } catch (e) { 169 | if (e instanceof UserIdNotFoundError) { 170 | throw new NotFoundException('User not found'); 171 | } else if (e instanceof Error) { 172 | throw new BadRequestException(e.message); 173 | } else { 174 | throw new BadRequestException('Server error'); 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/entity/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { UserDTO } from '@boilerplate/contracts'; 2 | import { Exclude } from 'class-transformer'; 3 | import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; 4 | 5 | @Entity('users') 6 | export class UserEntity implements UserDTO { 7 | @PrimaryGeneratedColumn('uuid') 8 | id: string; 9 | 10 | @Column({ 11 | unique: true, 12 | }) 13 | username: string; 14 | 15 | @Column({ 16 | type: 'varchar', 17 | length: 70, 18 | nullable: false, 19 | }) 20 | @Exclude() 21 | password: string; 22 | 23 | @Column({ 24 | type: 'simple-array', 25 | }) 26 | roles: string[]; 27 | 28 | constructor(id: string, username: string, password: string, roles: string[]) { 29 | this.id = id; 30 | this.username = username; 31 | this.password = password; 32 | this.roles = roles; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.module'; 2 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/repository/user.mapper.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | import * as uuid from 'uuid'; 3 | 4 | import { Password, Role, User, UserId, Username } from '../../domain'; 5 | import { UserEntity } from '../entity/user.entity'; 6 | import { UserMapper } from './user.mapper'; 7 | 8 | describe('User mapper', () => { 9 | let userMapper: UserMapper; 10 | const userId = UserId.fromString(uuid.v4()); 11 | const username = Username.fromString('username'); 12 | const password = Password.fromString('password'); 13 | const roleUser = Role.fromString('ROLE_USER'); 14 | const roleAdmin = Role.fromString('ROLE_ADMIN'); 15 | const roleAnonymous = Role.fromString('ROLE_ANONYMOUS'); 16 | 17 | beforeAll(async () => { 18 | const app = await Test.createTestingModule({ 19 | providers: [UserMapper], 20 | }).compile(); 21 | 22 | userMapper = app.get(UserMapper); 23 | }); 24 | 25 | it('converts from entity to aggregate', () => { 26 | const aggregate = userMapper.entityToAggregate( 27 | new UserEntity(userId.value, username.value, password.value, [ 28 | roleUser.value, 29 | roleAdmin.value, 30 | ]) 31 | ); 32 | 33 | expect(aggregate.id.equals(userId)).toBeTruthy(); 34 | expect(aggregate.username.equals(username)).toBeTruthy(); 35 | expect(aggregate.password.equals(password)).toBeTruthy(); 36 | expect(aggregate.hasRole(roleUser)).toBeTruthy(); 37 | expect(aggregate.hasRole(roleAdmin)).toBeTruthy(); 38 | expect(aggregate.hasRole(roleAnonymous)).toBeFalsy(); 39 | }); 40 | 41 | it('converts from aggregate to entity', () => { 42 | const user = User.add(userId, username, password); 43 | user.addRole(roleUser); 44 | user.addRole(roleAdmin); 45 | 46 | const entity = userMapper.aggregateToEntity(user); 47 | 48 | expect(entity.id).toEqual(userId.value); 49 | expect(entity.username).toEqual(username.value); 50 | expect(entity.password).toEqual(password.value); 51 | expect(entity.roles).toEqual( 52 | expect.arrayContaining([roleUser.value, roleAdmin.value]) 53 | ); 54 | expect(entity.roles).not.toEqual( 55 | expect.arrayContaining([roleAnonymous.value]) 56 | ); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/repository/user.mapper.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | import { Password, Role, User, UserId, Username } from '../../domain'; 4 | import { UserEntity } from '../entity/user.entity'; 5 | 6 | @Injectable() 7 | export class UserMapper { 8 | entityToAggregate(userEntity: UserEntity): User { 9 | const { id, username, password, roles } = userEntity; 10 | 11 | const user: User = Reflect.construct(User, []); 12 | Reflect.set(user, '_userId', UserId.fromString(id)); 13 | Reflect.set(user, '_username', Username.fromString(username)); 14 | Reflect.set(user, '_password', Password.fromString(password)); 15 | Reflect.set( 16 | user, 17 | '_roles', 18 | roles.map((role: string) => Role.fromString(role)) 19 | ); 20 | 21 | return user; 22 | } 23 | 24 | aggregateToEntity(user: User): UserEntity { 25 | return new UserEntity( 26 | user.id.value, 27 | user.username.value, 28 | user.password.value, 29 | user.roles.map((role: Role) => role.value) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/repository/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { EventPublisher } from '@nestjs/cqrs'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | 6 | import { User, UserId, Username, Users } from '../../domain'; 7 | import { UserEntity } from '../entity/user.entity'; 8 | import { UserMapper } from './user.mapper'; 9 | 10 | @Injectable() 11 | export class UserRepository implements Users { 12 | constructor( 13 | @InjectRepository(UserEntity) 14 | private userRepository: Repository, 15 | private userMapper: UserMapper, 16 | private publisher: EventPublisher 17 | ) {} 18 | 19 | async find(userId: UserId): Promise { 20 | const user = await this.userRepository.findOne(userId.value); 21 | 22 | if (!user) { 23 | return null; 24 | } 25 | 26 | return this.userMapper.entityToAggregate(user); 27 | } 28 | 29 | async findAll(): Promise { 30 | const users = await this.userRepository.find(); 31 | 32 | return users.map(this.userMapper.entityToAggregate); 33 | } 34 | 35 | async findOneByUsername(username: Username): Promise { 36 | const user = await this.userRepository.findOne({ 37 | username: username.value, 38 | }); 39 | 40 | if (!user) { 41 | return null; 42 | } 43 | 44 | return this.userMapper.entityToAggregate(user); 45 | } 46 | 47 | save(user: User): void { 48 | this.userRepository.save(this.userMapper.aggregateToEntity(user)); 49 | 50 | user = this.publisher.mergeObjectContext(user); 51 | user.commit(); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/saga/user-was-deleted.saga.ts: -------------------------------------------------------------------------------- 1 | import { EventsHandler, IEventHandler } from '@nestjs/cqrs'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository } from 'typeorm'; 4 | 5 | import { UserWasDeleted } from '../../domain/event/user-was-deleted.event'; 6 | import { UserEntity } from '../entity/user.entity'; 7 | 8 | @EventsHandler(UserWasDeleted) 9 | export class UserWasDeletedSaga implements IEventHandler { 10 | constructor( 11 | @InjectRepository(UserEntity) private userRepository: Repository 12 | ) {} 13 | 14 | async handle(event: UserWasDeleted) { 15 | const user = await this.userRepository.findOne(event.id); 16 | 17 | if (!user) { 18 | return; 19 | } 20 | 21 | this.userRepository.remove(user); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Inject, Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import * as bcrypt from 'bcrypt'; 4 | import { Repository } from 'typeorm'; 5 | 6 | import { USERS, Users } from '../../domain'; 7 | import { UserEntity } from '../entity/user.entity'; 8 | 9 | @Injectable() 10 | export class UserService { 11 | constructor( 12 | @Inject(USERS) private users: Users, 13 | @InjectRepository(UserEntity) 14 | private userRepository: Repository 15 | ) {} 16 | 17 | async validatePassword(username: string, password: string): Promise { 18 | const user = await this.userRepository.findOne({ username }); 19 | 20 | if (!user) { 21 | return false; 22 | } 23 | 24 | return bcrypt.compare(password, user.password); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { CqrsModule } from '@nestjs/cqrs'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | 5 | import { AuthModule } from '../../auth/auth.module'; 6 | import { CreateUserHandler } from '../application/command/create-user.handler'; 7 | import { DeleteUserHandler } from '../application/command/delete-user.handler'; 8 | import { UpdateUserHandler } from '../application/command/update-user.handler'; 9 | import { GetUserHandler } from '../application/query/get-user.handler'; 10 | import { GetUserByUsernameHandler } from '../application/query/get-user-by-username.handler'; 11 | import { GetUsersHandler } from '../application/query/get-users.handler'; 12 | import { UserController } from './controller/user.controller'; 13 | import { UserEntity } from './entity/user.entity'; 14 | import { UserMapper } from './repository/user.mapper'; 15 | import { UserWasDeletedSaga } from './saga/user-was-deleted.saga'; 16 | import { userProviders } from './user.providers'; 17 | 18 | const CommandHandlers = [CreateUserHandler, DeleteUserHandler]; 19 | const QueryHandlers = [ 20 | GetUserByUsernameHandler, 21 | GetUserHandler, 22 | GetUsersHandler, 23 | UpdateUserHandler, 24 | ]; 25 | const Sagas = [UserWasDeletedSaga]; 26 | 27 | @Module({ 28 | controllers: [UserController], 29 | imports: [AuthModule, CqrsModule, TypeOrmModule.forFeature([UserEntity])], 30 | providers: [ 31 | ...userProviders, 32 | ...CommandHandlers, 33 | ...QueryHandlers, 34 | ...Sagas, 35 | UserMapper, 36 | ], 37 | }) 38 | export class UserModule {} 39 | -------------------------------------------------------------------------------- /apps/api/src/user/infrastructure/user.providers.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | 3 | import { USERS } from '../domain/repository/users'; 4 | import { UserRepository } from './repository/user.repository'; 5 | 6 | export const userProviders: Provider[] = [ 7 | { 8 | provide: USERS, 9 | useClass: UserRepository, 10 | }, 11 | ]; 12 | -------------------------------------------------------------------------------- /apps/api/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"], 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "target": "es2015" 9 | }, 10 | "exclude": ["**/*.spec.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "compilerOptions": { 6 | "module": "commonjs" 7 | }, 8 | "references": [ 9 | { 10 | "path": "./tsconfig.app.json" 11 | }, 12 | { 13 | "path": "./tsconfig.spec.json" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /apps/api/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/api/tsconfig.typeorm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"], 6 | "experimentalDecorators": true, 7 | "emitDecoratorMetadata": true, 8 | "target": "es2015" 9 | }, 10 | "exclude": ["**/*.spec.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/api/webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (config, _context) => { 2 | const tsLoader = config.module.rules.find((r) => 3 | r.loader.includes('ts-loader') 4 | ); 5 | 6 | if (tsLoader) { 7 | tsLoader.options.transpileOnly = false; 8 | tsLoader.options.getCustomTransformers = (program) => { 9 | return { 10 | before: [require('@nestjs/swagger/plugin').before({}, program)], 11 | }; 12 | }; 13 | } 14 | 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /apps/web-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["src/plugins/index.js"], 7 | "rules": { 8 | "@typescript-eslint/no-var-requires": "off", 9 | "no-undef": "off" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/web-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "pluginsFile": "./src/plugins/index", 7 | "supportFile": "./src/support/index.ts", 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/admin-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/admin-e2e/screenshots", 11 | "chromeWebSecurity": false, 12 | "testFiles": "**/*.{feature,features}" 13 | } 14 | -------------------------------------------------------------------------------- /apps/web-e2e/src/db/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { Database } from 'sqlite3'; 3 | 4 | export const teardown = () => { 5 | const db = new Database(path.join(__dirname, '../../../../tmp/test.sqlite3')); 6 | 7 | db.each( 8 | "select 'delete from ' || name as query from sqlite_master where type = 'table'", 9 | (err, row) => db.run(row.query) 10 | ); 11 | 12 | return true; 13 | }; 14 | 15 | export const seed = () => { 16 | const db = new Database(path.join(__dirname, '../../../../tmp/test.sqlite3')); 17 | 18 | db.run( 19 | "INSERT INTO `users` (`id`, `username`, `password`, `roles`) VALUES ('f60d593d-9ea9-4add-8f6c-5d86dd8c9f87', 'admin', '$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy', 'ROLE_ADMIN')" 20 | ); 21 | db.run( 22 | "INSERT INTO `users` (`id`, `username`, `password`, `roles`) VALUES ('f60d593d-9ea9-4add-8f6c-5d86dd8c9f88', 'user', '$2a$04$J.qvJcqZRPBlGFKWIxPOYOsPRXpkZmTyTHScEF3Kq5/QXV.8oMcfy', 'ROLE_USER')" 23 | ); 24 | 25 | return true; 26 | }; 27 | -------------------------------------------------------------------------------- /apps/web-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/web-e2e/src/integration/common/common.steps.ts: -------------------------------------------------------------------------------- 1 | import { Before } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | Before(() => { 4 | cy.server(); 5 | }); 6 | -------------------------------------------------------------------------------- /apps/web-e2e/src/integration/common/landing/index.steps.ts: -------------------------------------------------------------------------------- 1 | import { Before, Given, Then } from 'cypress-cucumber-preprocessor/steps'; 2 | 3 | Before(() => { 4 | cy.visit('/'); 5 | }); 6 | 7 | Given('the user is logged in', () => { 8 | cy.login('my-email@something.com', 'myPassword'); 9 | }); 10 | 11 | Then('the {string} message is displayed', (message: string) => { 12 | cy.get('h1').contains(message); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/web-e2e/src/integration/landing.feature: -------------------------------------------------------------------------------- 1 | Feature: Landing page 2 | 3 | Scenario: The application displays the welcome message. 4 | Given the user is logged in 5 | Then the "Next.js example" message is displayed 6 | -------------------------------------------------------------------------------- /apps/web-e2e/src/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | const { getWebpackConfig } = require('@nrwl/cypress/plugins/preprocessor'); 15 | const webpack = require('@cypress/webpack-preprocessor'); 16 | const { teardown, seed } = require('../db'); 17 | 18 | const featureConfig = { 19 | test: /\.feature$/, 20 | use: [ 21 | { 22 | loader: 'cypress-cucumber-preprocessor/loader', 23 | }, 24 | ], 25 | }; 26 | 27 | const featuresConfig = { 28 | test: /\.features$/, 29 | use: [ 30 | { 31 | loader: 'cypress-cucumber-preprocessor/lib/featuresLoader', 32 | }, 33 | ], 34 | }; 35 | 36 | module.exports = (on, config) => { 37 | const webpackConfig = getWebpackConfig(config); 38 | webpackConfig.node = { 39 | fs: 'empty', 40 | child_process: 'empty', 41 | readline: 'empty', 42 | }; 43 | webpackConfig.module.rules.push(featureConfig); 44 | webpackConfig.module.rules.push(featuresConfig); 45 | 46 | const options = { 47 | webpackOptions: webpackConfig, 48 | }; 49 | on('file:preprocessor', webpack(options)); 50 | 51 | on('task', { 52 | 'db:teardown': () => { 53 | return teardown(); 54 | }, 55 | 'db:seed': () => { 56 | return seed(); 57 | }, 58 | }); 59 | }; 60 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /apps/web-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/web-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"] 8 | }, 9 | "include": ["src/**/*.ts", "src/**/*.js"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/.env: -------------------------------------------------------------------------------- 1 | NEXTAUTH_URL=http://localhost:4200 2 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /apps/web/babel-jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript", 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /apps/web/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'web', 3 | preset: '../../jest.preset.js', 4 | transform: { 5 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', 6 | '^.+\\.[tj]sx?$': [ 7 | 'babel-jest', 8 | { cwd: __dirname, configFile: './babel-jest.config.json' }, 9 | ], 10 | }, 11 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 12 | coverageDirectory: '../../coverage/apps/web', 13 | }; 14 | -------------------------------------------------------------------------------- /apps/web/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const withNx = require('@nrwl/next/plugins/with-nx'); 3 | 4 | module.exports = withNx({}); 5 | -------------------------------------------------------------------------------- /apps/web/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { theme } from '@boilerplate/ui'; 2 | import { CssBaseline, ThemeProvider } from '@material-ui/core'; 3 | import { AppProps } from 'next/app'; 4 | import Head from 'next/head'; 5 | import { Provider as NextAuthProvider } from 'next-auth/client'; 6 | import PropTypes from 'prop-types'; 7 | import React from 'react'; 8 | 9 | export default function MyApp({ Component, pageProps }: AppProps) { 10 | React.useEffect(() => { 11 | // Remove the server-side injected CSS. 12 | const jssStyles = document.querySelector('#jss-server-side'); 13 | if (jssStyles) { 14 | jssStyles.parentElement.removeChild(jssStyles); 15 | } 16 | }, []); 17 | 18 | return ( 19 | 20 | 21 | 22 | NX Boilerplate 23 | 27 | 28 | 29 |
30 | {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} 31 | 32 | 33 |
34 |
35 |
36 |
37 | ); 38 | } 39 | 40 | MyApp.propTypes = { 41 | Component: PropTypes.elementType.isRequired, 42 | pageProps: PropTypes.object.isRequired, 43 | }; 44 | -------------------------------------------------------------------------------- /apps/web/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { theme } from '@boilerplate/ui' 2 | import { ServerStyleSheets } from '@material-ui/core' 3 | import Document, { Head, Html, Main, NextScript } from 'next/document' 4 | import React from 'react' 5 | 6 | 7 | export default class MyDocument extends Document { 8 | render() { 9 | return ( 10 | 11 | 12 | 13 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | ) 24 | } 25 | } 26 | 27 | MyDocument.getInitialProps = async (ctx) => { 28 | const sheets = new ServerStyleSheets(); 29 | const originalRenderPage = ctx.renderPage; 30 | 31 | ctx.renderPage = () => originalRenderPage({ 32 | enhanceApp: (App) => (props) => sheets.collect() 33 | }) 34 | 35 | const initialProps = await Document.getInitialProps(ctx); 36 | 37 | return { 38 | ...initialProps, 39 | styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /apps/web/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAccessToken, 3 | isCredentials, 4 | isJwtPayload, 5 | } from '@boilerplate/contracts'; 6 | import axios from 'axios'; 7 | import jose from 'jose'; 8 | import jwt from 'jsonwebtoken'; 9 | import { NextApiRequest, NextApiResponse } from 'next'; 10 | import NextAuth, { InitOptions, User } from 'next-auth'; 11 | import Providers from 'next-auth/providers'; 12 | 13 | const options: InitOptions = { 14 | session: { 15 | jwt: true, 16 | }, 17 | callbacks: { 18 | session: async (session, user) => { 19 | // @ts-expect-error: Custom session attributes 20 | session.roles = user.roles; 21 | return Promise.resolve(session); 22 | }, 23 | jwt: async (token, user, account, profile, isNewUser) => { 24 | const isSignIn = user ? true : false; 25 | if (isSignIn) { 26 | token.roles = profile.roles; 27 | } 28 | return Promise.resolve(token); 29 | }, 30 | }, 31 | secret: process.env.JWT_SECRET, 32 | jwt: { 33 | secret: process.env.JWT_SECRET, 34 | encode: async ({ secret, token, maxAge }) => { 35 | const signingOptions: jose.JWT.SignOptions = { 36 | expiresIn: `${maxAge}s`, 37 | algorithm: 'HS512', 38 | }; 39 | 40 | return jose.JWT.sign(token, secret, signingOptions); 41 | }, 42 | // @ts-expect-error: Error in InitOptions declaration 43 | decode: async ({ secret, token, maxAge }) => { 44 | if (!token) return null; 45 | 46 | const verificationOptions = { 47 | maxTokenAge: `${maxAge}s`, 48 | algorithms: ['RS256', 'HS256', 'RS512', 'HS512'], 49 | }; 50 | 51 | return jose.JWT.verify(token, secret, verificationOptions); 52 | }, 53 | }, 54 | providers: [ 55 | Providers.Credentials({ 56 | name: 'Credentials', 57 | credentials: { 58 | username: { label: 'Usuario', type: 'text' }, 59 | password: { label: 'Contraseña', type: 'password' }, 60 | }, 61 | authorize: async (credentials): Promise => { 62 | try { 63 | if (!isCredentials(credentials)) { 64 | console.error('next-auth - missing attributes in credentials'); 65 | 66 | return Promise.resolve(null); 67 | } 68 | const res = await axios.post( 69 | `${process.env.NEXTAUTH_URL}/api/login`, 70 | credentials 71 | ); 72 | 73 | if (!isAccessToken(res.data)) { 74 | console.error( 75 | 'next-auth - missing attributes in response access token', 76 | JSON.stringify(res.data) 77 | ); 78 | 79 | return Promise.resolve(null); 80 | } 81 | 82 | const verify = jwt.verify( 83 | res.data.access_token, 84 | process.env.JWT_SECRET 85 | ); 86 | 87 | if (!isJwtPayload(verify)) { 88 | console.error( 89 | 'next-auth - missing attributes in response payload', 90 | JSON.stringify(verify) 91 | ); 92 | 93 | return Promise.resolve(null); 94 | } 95 | 96 | return Promise.resolve({ 97 | name: verify.username, 98 | email: verify.username, 99 | roles: verify.roles, 100 | }); 101 | } catch (e) { 102 | console.error('next-auth - error in credentials'); 103 | } 104 | 105 | return Promise.resolve(null); 106 | }, 107 | }), 108 | ], 109 | }; 110 | 111 | export default (req: NextApiRequest, res: NextApiResponse): Promise => 112 | NextAuth(req, res, options); 113 | -------------------------------------------------------------------------------- /apps/web/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from '@boilerplate/ui'; 2 | import Box from '@material-ui/core/Box'; 3 | import Container from '@material-ui/core/Container'; 4 | import Typography from '@material-ui/core/Typography'; 5 | import { useSession } from 'next-auth/client'; 6 | import React from 'react'; 7 | 8 | export default function Index() { 9 | const [ session, loading ] = useSession() 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | Next.js example 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/web/proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/(users|login)": { 3 | "target": "http://localhost:3333", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/public/nx-logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/web/public/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /apps/web/specs/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import client, { Session } from "next-auth/client"; 3 | import React from 'react'; 4 | 5 | import Index from '../pages/index'; 6 | 7 | jest.mock("next-auth/client"); 8 | 9 | describe('Index', () => { 10 | it('should render successfully', () => { 11 | const mockSession: Session = { 12 | expires: "1", 13 | user: { email: "john@doe.com", name: "John Doe" }, 14 | }; 15 | 16 | (client.useSession as jest.Mock).mockReturnValueOnce([mockSession, false]); 17 | 18 | const { baseElement } = render(); 19 | expect(baseElement).toBeTruthy(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /apps/web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node", "jest"] 6 | }, 7 | "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], 8 | "include": ["**/*.ts", "**/*.tsx", "next-env.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": false, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true 13 | }, 14 | "files": [], 15 | "include": [], 16 | "references": [ 17 | { 18 | "path": "./tsconfig.app.json" 19 | }, 20 | { 21 | "path": "./tsconfig.spec.json" 22 | } 23 | ], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "**/*.spec.ts", 11 | "**/*.spec.tsx", 12 | "**/*.spec.js", 13 | "**/*.spec.jsx", 14 | "**/*.d.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nrwl/web/babel"], 3 | "babelrcRoots": ["*"] 4 | } 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /cypress-cucumber-preprocessor.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const stepDefinitionsPath = path.resolve(process.cwd(), './src/integration'); 4 | const outputFolder = path.resolve( 5 | process.cwd(), 6 | '../../cyreport/cucumber-json' 7 | ); 8 | 9 | module.exports = { 10 | nonGlobalStepDefinitions: true, 11 | stepDefinitions: stepDefinitionsPath, 12 | cucumberJson: { 13 | generate: true, 14 | outputFolder: outputFolder, 15 | filePrefix: '', 16 | fileSuffix: '.cucumber', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | '/apps/web', 4 | '/apps/api', 5 | '/libs/domain', 6 | '/libs/ui', 7 | '/libs/contracts', 8 | '/apps/admin', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset'); 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/libs/.gitkeep -------------------------------------------------------------------------------- /libs/contracts/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /libs/contracts/README.md: -------------------------------------------------------------------------------- 1 | # contracts 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test contracts` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/contracts/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'contracts', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsConfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 13 | coverageDirectory: '../../coverage/libs/contracts', 14 | }; 15 | -------------------------------------------------------------------------------- /libs/contracts/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/auth'; 2 | export * from './lib/user'; 3 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/auth/access-token.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AccessTokenInterface { 2 | access_token: string; 3 | } 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | export function isAccessToken(arg: any): arg is AccessTokenInterface { 7 | return arg && arg.access_token; 8 | } 9 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/auth/credentials.interface.ts: -------------------------------------------------------------------------------- 1 | export interface CredentialsInterface { 2 | username: string; 3 | password: string; 4 | } 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export function isCredentials(arg: any): arg is CredentialsInterface { 8 | return arg && arg.username && arg.password; 9 | } 10 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access-token.interface'; 2 | export * from './credentials.interface'; 3 | export * from './jwt-payload.interface'; 4 | export * from './role.enum'; 5 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/auth/jwt-payload.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayloadInterface { 2 | username: string; 3 | roles: string[]; 4 | } 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export function isJwtPayload(arg: any): arg is JwtPayloadInterface { 8 | return arg && arg.username && arg.roles; 9 | } 10 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/auth/role.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | Admin = 'ROLE_ADMIN', 3 | User = 'ROLE_USER', 4 | } 5 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/user/create-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '../auth'; 2 | 3 | export class CreateUserDTO { 4 | id: string; 5 | username: string; 6 | plainPassword: string; 7 | roles: Role[]; 8 | } 9 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/user/edit-user.dto.ts: -------------------------------------------------------------------------------- 1 | import { Role } from '../auth'; 2 | 3 | export class EditUserDTO { 4 | username: string; 5 | plainPassword: string; 6 | roles: Role[]; 7 | } 8 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './create-user.dto'; 2 | export * from './edit-user.dto'; 3 | export * from './user.dto'; 4 | -------------------------------------------------------------------------------- /libs/contracts/src/lib/user/user.dto.ts: -------------------------------------------------------------------------------- 1 | export class UserDTO { 2 | id: string; 3 | username: string; 4 | password: string; 5 | roles: string[]; 6 | } 7 | -------------------------------------------------------------------------------- /libs/contracts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/contracts/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/contracts/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/domain/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc.json", "ignorePatterns": ["!**/*"], "rules": {} } 2 | -------------------------------------------------------------------------------- /libs/domain/README.md: -------------------------------------------------------------------------------- 1 | # domain 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test domain` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/domain/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'domain', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 13 | coverageDirectory: '../../coverage/libs/domain', 14 | }; 15 | -------------------------------------------------------------------------------- /libs/domain/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/event'; 2 | export * from './lib/id'; 3 | export * from './lib/invalid-id-error'; 4 | export * from './lib/value-object'; 5 | -------------------------------------------------------------------------------- /libs/domain/src/lib/event.ts: -------------------------------------------------------------------------------- 1 | import { IEvent } from '@nestjs/cqrs'; 2 | 3 | export abstract class Event implements IEvent { 4 | abstract id: string; 5 | eventName: string; 6 | 7 | constructor() { 8 | this.eventName = this.constructor.name; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/domain/src/lib/id.spec.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid'; 2 | 3 | import { Id } from './id'; 4 | 5 | describe('Id', () => { 6 | it('creates a id value object', () => { 7 | const id = uuid.v4(); 8 | const myId = MyId.fromString(id); 9 | 10 | expect(myId.value).toBe(id); 11 | }); 12 | }); 13 | 14 | class MyId extends Id { 15 | private constructor(id: string) { 16 | super(id); 17 | } 18 | 19 | static fromString(id: string): MyId { 20 | return new this(id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/domain/src/lib/id.ts: -------------------------------------------------------------------------------- 1 | import * as uuid from 'uuid'; 2 | 3 | import { InvalidIdError } from './invalid-id-error'; 4 | import { ValueObject } from './value-object'; 5 | 6 | interface Props { 7 | value: string; 8 | } 9 | 10 | export abstract class Id extends ValueObject { 11 | protected constructor(id: string) { 12 | if (uuid.version(id) !== 4) { 13 | throw InvalidIdError.withString(id); 14 | } 15 | 16 | super({ value: id }); 17 | } 18 | 19 | get value(): string { 20 | return this.props.value; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/domain/src/lib/invalid-id-error.ts: -------------------------------------------------------------------------------- 1 | export class InvalidIdError extends Error { 2 | private constructor(stack?: string) { 3 | super(stack); 4 | } 5 | 6 | public static withString(value: string): InvalidIdError { 7 | return new InvalidIdError(`${value} is not a valid uuid v4.`); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/domain/src/lib/value-object.spec.ts: -------------------------------------------------------------------------------- 1 | import { ValueObject } from './value-object'; 2 | 3 | describe('ValueObject', () => { 4 | it('creates a value object with one attribute', () => { 5 | const foo = FooValueObject.fromString('foo'); 6 | 7 | expect(foo.value).toBe('foo'); 8 | }); 9 | 10 | it('creates a value object with many attributes', () => { 11 | const person = FullName.from('John', 'Doe'); 12 | 13 | expect(person.first).toBe('John'); 14 | expect(person.last).toBe('Doe'); 15 | }); 16 | 17 | it('a value objects is equal with itself', () => { 18 | const foo = FooValueObject.fromString('foo'); 19 | 20 | expect(foo.equals(foo)).toBeTruthy(); 21 | }); 22 | 23 | it('two value objects with same values are equals', () => { 24 | const foo = FooValueObject.fromString('foo'); 25 | const foo2 = FooValueObject.fromString('foo'); 26 | 27 | expect(foo.equals(foo2)).toBeTruthy(); 28 | }); 29 | 30 | it('two value objects with different values are not equals', () => { 31 | const foo = FooValueObject.fromString('foo'); 32 | const bar = FooValueObject.fromString('bar'); 33 | 34 | expect(foo.equals(bar)).toBeFalsy(); 35 | }); 36 | 37 | it('two different value objects with same values are not equals', () => { 38 | const foo = FooValueObject.fromString('foo'); 39 | const bar = BarValueObject.fromString('foo'); 40 | 41 | expect(foo.equals(bar)).toBeFalsy(); 42 | }); 43 | }); 44 | 45 | class FooValueObject extends ValueObject<{ value: string }> { 46 | public static fromString(value: string): FooValueObject { 47 | return new FooValueObject({ value }); 48 | } 49 | 50 | get value(): string { 51 | return this.props.value; 52 | } 53 | } 54 | 55 | class BarValueObject extends ValueObject<{ value: string }> { 56 | public static fromString(value: string): BarValueObject { 57 | return new BarValueObject({ value }); 58 | } 59 | 60 | get value(): string { 61 | return this.props.value; 62 | } 63 | } 64 | 65 | class FullName extends ValueObject<{ first: string; last: string }> { 66 | public static from(first: string, last: string): FullName { 67 | return new FullName({ first, last }); 68 | } 69 | 70 | get first(): string { 71 | return this.props.first; 72 | } 73 | 74 | get last(): string { 75 | return this.props.last; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /libs/domain/src/lib/value-object.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { shallowEqual } from 'shallow-equal-object'; 3 | 4 | interface ValueObjectProps { 5 | [index: string]: any; 6 | } 7 | 8 | export abstract class ValueObject { 9 | public readonly props: T; 10 | 11 | protected constructor(props: T) { 12 | this.props = Object.freeze(props); 13 | } 14 | 15 | public equals(other: ValueObject): boolean { 16 | if (this.constructor !== other.constructor) { 17 | return false; 18 | } 19 | 20 | return shallowEqual(this.props, other.props); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/domain/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/domain/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "exclude": ["**/*.spec.ts"], 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/domain/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /libs/ui/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nrwl/react/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /libs/ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"] 4 | } 5 | -------------------------------------------------------------------------------- /libs/ui/.storybook/main.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const rootMain = require('../../../.storybook/main'); 3 | 4 | // Use the following syntax to add addons! 5 | // rootMain.addons.push(''); 6 | rootMain.stories.push( 7 | ...['../src/lib/**/*.stories.mdx', '../src/lib/**/*.stories.@(js|jsx|ts|tsx)'] 8 | ); 9 | 10 | module.exports = rootMain; 11 | -------------------------------------------------------------------------------- /libs/ui/.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import { withKnobs } from '@storybook/addon-knobs'; 2 | import { addDecorator } from '@storybook/react'; 3 | 4 | addDecorator(withKnobs); 5 | 6 | export const parameters = { layout: 'fullscreen' }; 7 | -------------------------------------------------------------------------------- /libs/ui/.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDecoratorMetadata": true 5 | }, 6 | "exclude": ["../**/*.spec.ts" , "../**/*.spec.js", "../**/*.spec.tsx", "../**/*.spec.jsx"], 7 | "include": ["../src/**/*"] 8 | } 9 | -------------------------------------------------------------------------------- /libs/ui/.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 3 | const rootWebpackConfig = require('../../../.storybook/webpack.config'); 4 | /** 5 | * Export a function. Accept the base config as the only param. 6 | * 7 | * @param {Parameters[0]} options 8 | */ 9 | module.exports = async ({ config, mode }) => { 10 | config = await rootWebpackConfig({ config, mode }); 11 | 12 | const tsPaths = new TsconfigPathsPlugin({ 13 | configFile: './tsconfig.base.json', 14 | }); 15 | 16 | config.resolve.plugins 17 | ? config.resolve.plugins.push(tsPaths) 18 | : (config.resolve.plugins = [tsPaths]); 19 | 20 | // Found this here: https://github.com/nrwl/nx/issues/2859 21 | // And copied the part of the solution that made it work 22 | 23 | const svgRuleIndex = config.module.rules.findIndex((rule) => { 24 | const { test } = rule; 25 | 26 | return test.toString().startsWith('/\\.(svg|ico'); 27 | }); 28 | config.module.rules[ 29 | svgRuleIndex 30 | ].test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/; 31 | 32 | config.module.rules.push( 33 | { 34 | test: /\.(png|jpe?g|gif|webp)$/, 35 | loader: require.resolve('url-loader'), 36 | options: { 37 | limit: 10000, // 10kB 38 | name: '[name].[hash:7].[ext]', 39 | }, 40 | }, 41 | { 42 | test: /\.svg$/, 43 | oneOf: [ 44 | // If coming from JS/TS file, then transform into React component using SVGR. 45 | { 46 | issuer: { 47 | test: /\.[jt]sx?$/, 48 | }, 49 | use: [ 50 | '@svgr/webpack?-svgo,+titleProp,+ref![path]', 51 | { 52 | loader: require.resolve('url-loader'), 53 | options: { 54 | limit: 10000, // 10kB 55 | name: '[name].[hash:7].[ext]', 56 | esModule: false, 57 | }, 58 | }, 59 | ], 60 | }, 61 | // Fallback to plain URL loader. 62 | { 63 | use: [ 64 | { 65 | loader: require.resolve('url-loader'), 66 | options: { 67 | limit: 10000, // 10kB 68 | name: '[name].[hash:7].[ext]', 69 | }, 70 | }, 71 | ], 72 | }, 73 | ], 74 | } 75 | ); 76 | 77 | return config; 78 | }; 79 | -------------------------------------------------------------------------------- /libs/ui/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test ui` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/ui/babel-jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript", 12 | "@babel/preset-react" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /libs/ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'ui', 3 | preset: '../../jest.preset.js', 4 | transform: { 5 | '^.+\\.[tj]sx?$': [ 6 | 'babel-jest', 7 | { cwd: __dirname, configFile: './babel-jest.config.json' }, 8 | ], 9 | }, 10 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 11 | coverageDirectory: '../../coverage/libs/ui', 12 | }; 13 | -------------------------------------------------------------------------------- /libs/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/layout/layout'; 2 | export * from './lib/navbar/navbar'; 3 | export * from './lib/sidebar/sidebar'; 4 | export * from './lib/theme'; 5 | -------------------------------------------------------------------------------- /libs/ui/src/lib/layout/layout.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Layout from './layout'; 5 | 6 | describe('Layout', () => { 7 | it('should render successfully', () => { 8 | const { baseElement } = render(Hello World); 9 | expect(baseElement).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/ui/src/lib/layout/layout.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from '@material-ui/core'; 2 | import { Meta, Story } from '@storybook/react'; 3 | import React from 'react'; 4 | import { withNextRouter } from 'storybook-addon-next-router'; 5 | 6 | import { Layout, LayoutProps } from './layout'; 7 | 8 | export default { 9 | component: Layout, 10 | title: 'Widgets/Layout', 11 | decorators: [withNextRouter], 12 | } as Meta; 13 | 14 | const Template: Story = (args) => ( 15 | 16 | 17 | Hello World 18 | 19 | 20 | ); 21 | 22 | export const Default = Template.bind({}); 23 | Default.args = { 24 | session: {} 25 | }; 26 | -------------------------------------------------------------------------------- /libs/ui/src/lib/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Container, 4 | CssBaseline, 5 | Drawer, 6 | Grid, 7 | Paper, 8 | Typography, 9 | } from '@material-ui/core'; 10 | import { Session } from 'next-auth'; 11 | import React from 'react'; 12 | 13 | import Navbar from '../navbar/navbar'; 14 | import Sidebar from '../sidebar/sidebar'; 15 | import { useStyles } from '../theme'; 16 | 17 | export interface LayoutProps { 18 | session?: Session 19 | } 20 | 21 | export const Layout: React.FunctionComponent = ({session, children}) => { 22 | const classes = useStyles(); 23 | const [open, setOpen] = React.useState(false); 24 | 25 | return ( 26 |
27 | 28 | setOpen(true)} 32 | /> 33 | setOpen(false)} /> 34 |
35 |
36 | 37 | {children} 38 | 39 |
40 |
41 | ); 42 | } 43 | 44 | export default Layout; 45 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navbar.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Navbar from './navbar'; 5 | 6 | describe('Navbar', () => { 7 | it('should render successfully', () => { 8 | const { baseElement } = render(); 9 | expect(baseElement).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navbar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react'; 2 | import React from 'react'; 3 | import { withNextRouter } from 'storybook-addon-next-router'; 4 | 5 | import { Navbar, NavbarProps } from './navbar'; 6 | 7 | export default { 8 | component: Navbar, 9 | title: 'Widgets/Navbar', 10 | decorators: [withNextRouter], 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const Default = Template.bind({}); 16 | Default.args = { 17 | open: false, 18 | session: undefined, 19 | }; 20 | 21 | export const Open = Template.bind({}); 22 | Open.args = { 23 | open: true, 24 | session: undefined, 25 | }; 26 | 27 | export const Logged = Template.bind({}); 28 | Logged.args = { 29 | open: false, 30 | session: {}, 31 | }; 32 | -------------------------------------------------------------------------------- /libs/ui/src/lib/navbar/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AppBar, 3 | Button, 4 | IconButton, 5 | Toolbar, 6 | Typography, 7 | } from '@material-ui/core'; 8 | import MenuIcon from '@material-ui/icons/Menu'; 9 | import clsx from 'clsx'; 10 | import Link from 'next/link'; 11 | import { Session } from 'next-auth'; 12 | import { useSession } from 'next-auth/client'; 13 | import React from 'react'; 14 | 15 | import { useStyles } from '../theme'; 16 | 17 | /* eslint-disable-next-line */ 18 | export interface NavbarProps { 19 | open: boolean; 20 | onOpenSidebar: (event: React.MouseEvent) => void; 21 | session: Session; 22 | } 23 | 24 | export function Navbar({ 25 | open, 26 | onOpenSidebar: onOpenDrawer, 27 | session, 28 | }: NavbarProps) { 29 | const classes = useStyles(); 30 | 31 | return ( 32 | 36 | 37 | 44 | 45 | 46 | 53 | Boilerplate 54 | 55 | {session && ( 56 | 57 | 58 | 59 | )} 60 | {!session && ( 61 | 62 | 63 | 64 | )} 65 | 66 | 67 | ); 68 | } 69 | 70 | export default Navbar; 71 | -------------------------------------------------------------------------------- /libs/ui/src/lib/sidebar/sidebar.spec.tsx: -------------------------------------------------------------------------------- 1 | import { render } from '@testing-library/react'; 2 | import React from 'react'; 3 | 4 | import Sidebar from './sidebar'; 5 | 6 | describe('Sidebar', () => { 7 | it('should render successfully', () => { 8 | const { baseElement } = render(); 9 | expect(baseElement).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/ui/src/lib/sidebar/sidebar.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, Story } from '@storybook/react'; 2 | import React from 'react'; 3 | import { withNextRouter } from 'storybook-addon-next-router'; 4 | 5 | import { Sidebar, SidebarProps } from './sidebar'; 6 | 7 | export default { 8 | component: Sidebar, 9 | title: 'Widgets/Sidebar', 10 | decorators: [withNextRouter], 11 | } as Meta; 12 | 13 | const Template: Story = (args) => ; 14 | 15 | export const Default = Template.bind({}); 16 | Default.args = { 17 | open: false, 18 | }; 19 | 20 | export const Open = Template.bind({}); 21 | Open.args = { 22 | open: true, 23 | }; 24 | -------------------------------------------------------------------------------- /libs/ui/src/lib/sidebar/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Divider, 3 | Drawer, 4 | List, 5 | ListItem, 6 | ListItemIcon, 7 | ListItemText, 8 | ListSubheader, 9 | } from '@material-ui/core'; 10 | import IconButton from '@material-ui/core/IconButton'; 11 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 12 | import DashboardIcon from '@material-ui/icons/Dashboard'; 13 | import ShoppingCartIcon from '@material-ui/icons/ShoppingCart'; 14 | import clsx from 'clsx'; 15 | import React from 'react'; 16 | 17 | import { useStyles } from '../theme'; 18 | 19 | export interface SidebarProps { 20 | open: boolean; 21 | onCloseSidebar: (event: React.MouseEvent) => void; 22 | } 23 | 24 | export function Sidebar({ open, onCloseSidebar }: SidebarProps) { 25 | const classes = useStyles(); 26 | 27 | return ( 28 | 35 |
36 | 37 | 38 | 39 |
40 | 41 | 42 | Saved reports 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | 60 | export default Sidebar; 61 | -------------------------------------------------------------------------------- /libs/ui/src/lib/theme.tsx: -------------------------------------------------------------------------------- 1 | import red from '@material-ui/core/colors/red'; 2 | import { createMuiTheme, makeStyles } from '@material-ui/core/styles'; 3 | 4 | const drawerWidth = 240; 5 | 6 | export const useStyles = makeStyles((theme) => ({ 7 | root: { 8 | display: 'flex', 9 | }, 10 | toolbar: { 11 | paddingRight: 24, // keep right padding when drawer closed 12 | }, 13 | toolbarIcon: { 14 | display: 'flex', 15 | alignItems: 'center', 16 | justifyContent: 'flex-end', 17 | padding: '0 8px', 18 | ...theme.mixins.toolbar, 19 | }, 20 | appBar: { 21 | zIndex: theme.zIndex.drawer + 1, 22 | transition: theme.transitions.create(['width', 'margin'], { 23 | easing: theme.transitions.easing.sharp, 24 | duration: theme.transitions.duration.leavingScreen, 25 | }), 26 | }, 27 | appBarShift: { 28 | marginLeft: drawerWidth, 29 | width: `calc(100% - ${drawerWidth}px)`, 30 | transition: theme.transitions.create(['width', 'margin'], { 31 | easing: theme.transitions.easing.sharp, 32 | duration: theme.transitions.duration.enteringScreen, 33 | }), 34 | }, 35 | menuButton: { 36 | marginRight: 36, 37 | }, 38 | menuButtonHidden: { 39 | display: 'none', 40 | }, 41 | title: { 42 | flexGrow: 1, 43 | }, 44 | drawerPaper: { 45 | position: 'relative', 46 | whiteSpace: 'nowrap', 47 | width: drawerWidth, 48 | transition: theme.transitions.create('width', { 49 | easing: theme.transitions.easing.sharp, 50 | duration: theme.transitions.duration.enteringScreen, 51 | }), 52 | }, 53 | drawerPaperClose: { 54 | overflowX: 'hidden', 55 | transition: theme.transitions.create('width', { 56 | easing: theme.transitions.easing.sharp, 57 | duration: theme.transitions.duration.leavingScreen, 58 | }), 59 | width: theme.spacing(7), 60 | [theme.breakpoints.up('sm')]: { 61 | width: theme.spacing(9), 62 | }, 63 | }, 64 | appBarSpacer: theme.mixins.toolbar, 65 | content: { 66 | flexGrow: 1, 67 | height: '100vh', 68 | overflow: 'auto', 69 | }, 70 | container: { 71 | paddingTop: theme.spacing(4), 72 | paddingBottom: theme.spacing(4), 73 | }, 74 | paper: { 75 | padding: theme.spacing(2), 76 | display: 'flex', 77 | overflow: 'auto', 78 | flexDirection: 'column', 79 | }, 80 | fixedHeight: { 81 | height: 240, 82 | }, 83 | })); 84 | 85 | export const theme = createMuiTheme({ 86 | palette: { 87 | primary: { 88 | main: '#556cd6', 89 | }, 90 | secondary: { 91 | main: '#19857b', 92 | }, 93 | error: { 94 | main: red.A400, 95 | }, 96 | background: { 97 | default: '#fff', 98 | }, 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "files": [], 10 | "include": [], 11 | "references": [ 12 | { 13 | "path": "./tsconfig.lib.json" 14 | }, 15 | { 16 | "path": "./tsconfig.spec.json" 17 | }, 18 | { 19 | "path": "./.storybook/tsconfig.json" 20 | }, 21 | { 22 | "path": "./.storybook/tsconfig.json" 23 | }, 24 | { 25 | "path": "./.storybook/tsconfig.json" 26 | }, 27 | { 28 | "path": "./.storybook/tsconfig.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "../../node_modules/@nrwl/react/typings/cssmodule.d.ts", 9 | "../../node_modules/@nrwl/react/typings/image.d.ts" 10 | ], 11 | "exclude": [ 12 | "**/*.spec.ts", 13 | "**/*.spec.tsx", 14 | "**/*.stories.ts", 15 | "**/*.stories.js", 16 | "**/*.stories.jsx", 17 | "**/*.stories.tsx", 18 | "**/*.stories.ts", 19 | "**/*.stories.js", 20 | "**/*.stories.jsx", 21 | "**/*.stories.tsx", 22 | "**/*.stories.ts", 23 | "**/*.stories.js", 24 | "**/*.stories.jsx", 25 | "**/*.stories.tsx", 26 | "**/*.stories.ts", 27 | "**/*.stories.js", 28 | "**/*.stories.jsx", 29 | "**/*.stories.tsx" 30 | ], 31 | "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] 32 | } 33 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "boilerplate", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "implicitDependencies": { 7 | "workspace.json": "*", 8 | "package.json": { 9 | "dependencies": "*", 10 | "devDependencies": "*" 11 | }, 12 | "tsconfig.base.json": "*", 13 | "tslint.json": "*", 14 | ".eslintrc.json": "*", 15 | "nx.json": "*" 16 | }, 17 | "tasksRunnerOptions": { 18 | "default": { 19 | "runner": "@nrwl/workspace/tasks-runners/default", 20 | "options": { 21 | "cacheableOperations": [ 22 | "build", 23 | "lint", 24 | "test", 25 | "e2e", 26 | "build-storybook" 27 | ] 28 | } 29 | } 30 | }, 31 | "projects": { 32 | "web": { 33 | "tags": [] 34 | }, 35 | "web-e2e": { 36 | "tags": [], 37 | "implicitDependencies": ["web"] 38 | }, 39 | "api": { 40 | "tags": [] 41 | }, 42 | "domain": { 43 | "tags": [] 44 | }, 45 | "ui": { 46 | "tags": [] 47 | }, 48 | "contracts": { 49 | "tags": [] 50 | }, 51 | "admin": { 52 | "tags": [] 53 | }, 54 | "admin-e2e": { 55 | "tags": [], 56 | "implicitDependencies": ["admin"] 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "boilerplate", 3 | "version": "0.0.2", 4 | "license": "MIT", 5 | "scripts": { 6 | "nx": "nx", 7 | "start": "nx serve", 8 | "build": "nx build", 9 | "test": "nx test", 10 | "lint": "nx workspace-lint && nx lint", 11 | "e2e": "nx e2e", 12 | "affected:apps": "nx affected:apps", 13 | "affected:libs": "nx affected:libs", 14 | "affected:build": "nx affected:build", 15 | "affected:e2e": "nx affected:e2e", 16 | "affected:test": "nx affected:test", 17 | "affected:lint": "nx affected:lint", 18 | "affected:dep-graph": "nx affected:dep-graph", 19 | "affected": "nx affected", 20 | "format": "nx format:write", 21 | "format:write": "nx format:write", 22 | "format:check": "nx format:check", 23 | "update": "nx migrate latest", 24 | "workspace-generator": "nx workspace-generator", 25 | "dep-graph": "nx dep-graph", 26 | "help": "nx help", 27 | "devtools:cucumber:report": "node ./tools/generate-cucumber-report.js", 28 | "release": "standard-version" 29 | }, 30 | "private": true, 31 | "dependencies": { 32 | "@material-ui/core": "^4.11.2", 33 | "@material-ui/icons": "^4.11.2", 34 | "@nestjs/common": "^7.0.0", 35 | "@nestjs/core": "^7.0.0", 36 | "@nestjs/cqrs": "^7.0.1", 37 | "@nestjs/jwt": "^7.2.0", 38 | "@nestjs/passport": "^7.1.5", 39 | "@nestjs/platform-express": "^7.0.0", 40 | "@nestjs/swagger": "^4.7.8", 41 | "@nestjs/typeorm": "^7.1.5", 42 | "bcrypt": "^5.0.0", 43 | "class-transformer": "^0.3.2", 44 | "core-js": "^3.6.5", 45 | "document-register-element": "1.13.1", 46 | "event-sourcing-nestjs": "^1.1.4", 47 | "jwt-decode": "^3.1.2", 48 | "mysql": "^2.18.1", 49 | "next": "^10.0.1", 50 | "next-auth": "^3.1.0", 51 | "passport": "^0.4.1", 52 | "passport-jwt": "^4.0.0", 53 | "ra-data-json-server": "^3.11.1", 54 | "react": "^17.0.1", 55 | "react-admin": "^3.11.1", 56 | "react-dom": "^17.0.1", 57 | "reflect-metadata": "^0.1.13", 58 | "rxjs": "^6.5.5", 59 | "shallow-equal-object": "^1.1.1", 60 | "swagger-ui-express": "^4.1.6", 61 | "tslib": "^2.0.0", 62 | "typeorm": "^0.2.29", 63 | "uuid": "^8.3.2" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "7.9.6", 67 | "@babel/preset-env": "7.9.6", 68 | "@babel/preset-react": "7.9.4", 69 | "@babel/preset-typescript": "7.9.0", 70 | "@commitlint/cli": "^11.0.0", 71 | "@commitlint/config-conventional": "^11.0.0", 72 | "@nestjs/schematics": "^7.0.0", 73 | "@nestjs/testing": "^7.0.0", 74 | "@nrwl/cli": "^11.0.16", 75 | "@nrwl/cypress": "^11.0.16", 76 | "@nrwl/eslint-plugin-nx": "^11.0.16", 77 | "@nrwl/jest": "^11.0.16", 78 | "@nrwl/nest": "^11.0.16", 79 | "@nrwl/next": "^11.0.16", 80 | "@nrwl/node": "^11.0.16", 81 | "@nrwl/react": "^11.0.16", 82 | "@nrwl/storybook": "^11.0.16", 83 | "@nrwl/tao": "^11.0.16", 84 | "@nrwl/web": "^11.0.16", 85 | "@nrwl/workspace": "^11.0.16", 86 | "@nx-tools/nx-docker": "^0.4.1", 87 | "@storybook/addon-essentials": "^6.1.11", 88 | "@storybook/addon-knobs": "^6.1.11", 89 | "@storybook/react": "^6.1.11", 90 | "@svgr/webpack": "^5.4.0", 91 | "@testing-library/react": "^11.1.2", 92 | "@types/bcrypt": "^3.0.0", 93 | "@types/express": "^4.17.9", 94 | "@types/jest": "^26.0.8", 95 | "@types/jwt-decode": "^3.1.0", 96 | "@types/next-auth": "^3.1.19", 97 | "@types/node": "^14.14.14", 98 | "@types/passport-jwt": "^3.0.3", 99 | "@types/react": "^17.0.0", 100 | "@types/react-dom": "^17.0.0", 101 | "@types/sqlite3": "^3.1.6", 102 | "@types/uuid": "^8.3.0", 103 | "@types/webpack": "^4.41.21", 104 | "@typescript-eslint/eslint-plugin": "4.3.0", 105 | "@typescript-eslint/parser": "4.3.0", 106 | "babel-jest": "^26.2.2", 107 | "babel-loader": "8.1.0", 108 | "cucumber-html-reporter": "^5.3.0", 109 | "cypress": "^6.2.0", 110 | "cypress-cucumber-preprocessor": "^4.0.0", 111 | "dotenv": "^6.2.0", 112 | "eslint": "^7.10.0", 113 | "eslint-config-prettier": "^6.0.0", 114 | "eslint-plugin-cypress": "^2.10.3", 115 | "eslint-plugin-import": "^2.22.1", 116 | "eslint-plugin-jsx-a11y": "^6.4.1", 117 | "eslint-plugin-react": "^7.21.5", 118 | "eslint-plugin-react-hooks": "^4.2.0", 119 | "eslint-plugin-simple-import-sort": "^7.0.0", 120 | "husky": "^4.3.6", 121 | "jest": "^26.2.2", 122 | "prettier": "^2.1.2", 123 | "sqlite3": "^5.0.0", 124 | "standard-version": "^9.1.0", 125 | "storybook-addon-next-router": "^2.0.3", 126 | "ts-jest": "^26.4.0", 127 | "ts-node": "^9.1.1", 128 | "tslint": "^6.1.0", 129 | "typescript": "4.0.*", 130 | "url-loader": "^3.0.0" 131 | }, 132 | "husky": { 133 | "hooks": { 134 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 135 | "pre-commit": "yarn run affected:lint --uncommitted && yarn run affected:test --uncommitted" 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tools/generate-cucumber-report.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const reporter = require('cucumber-html-reporter'); 3 | const chalk = require('chalk'); 4 | 5 | const options = { 6 | theme: 'bootstrap', 7 | jsonDir: path.join(process.cwd(), 'cyreport/cucumber-json'), 8 | output: path.join(process.cwd(), 'cyreport/cucumber_report.html'), 9 | reportSuiteAsScenarios: true, 10 | scenarioTimestamp: true, 11 | launchReport: false, 12 | storeScreenshots: true, 13 | noInlineScreenshots: false, 14 | }; 15 | 16 | try { 17 | reporter.generate(options); 18 | } catch (e) { 19 | console.log(chalk.red(`Could not generate cypress reports`)); 20 | console.log(chalk.red(`${e}`)); 21 | } 22 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aulasoftwarelibre/nx-boilerplate/f819c0ff56b118cea008b615897c0790693d480e/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/scripts/write-type-orm-config.ts: -------------------------------------------------------------------------------- 1 | import { configService } from '../../apps/api/src/config/config.service'; 2 | import * as fs from 'fs'; 3 | 4 | fs.writeFileSync( 5 | 'ormconfig.json', 6 | JSON.stringify(configService.getTypeOrmConfig(), null, 2) 7 | ); 8 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "strictNullChecks": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@boilerplate/domain": ["libs/domain/src/index.ts"], 20 | "@boilerplate/ui": ["libs/ui/src/index.ts"], 21 | "@boilerplate/contracts": ["libs/contracts/src/index.ts"] 22 | } 23 | }, 24 | "exclude": ["node_modules", "tmp"] 25 | } 26 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "web": { 5 | "root": "apps/web", 6 | "sourceRoot": "apps/web", 7 | "projectType": "application", 8 | "targets": { 9 | "build": { 10 | "executor": "@nrwl/next:build", 11 | "outputs": ["{options.outputPath}"], 12 | "options": { 13 | "root": "apps/web", 14 | "outputPath": "dist/apps/web" 15 | }, 16 | "configurations": { 17 | "production": {} 18 | } 19 | }, 20 | "serve": { 21 | "executor": "@nrwl/next:server", 22 | "options": { 23 | "buildTarget": "web:build", 24 | "dev": true, 25 | "proxyConfig": "apps/web/proxy.conf.json" 26 | }, 27 | "configurations": { 28 | "production": { 29 | "buildTarget": "web:build:production", 30 | "dev": false 31 | } 32 | } 33 | }, 34 | "export": { 35 | "executor": "@nrwl/next:export", 36 | "options": { 37 | "buildTarget": "web:build:production" 38 | } 39 | }, 40 | "lint": { 41 | "executor": "@nrwl/linter:eslint", 42 | "options": { 43 | "lintFilePatterns": ["apps/web/**/*.{ts,tsx,js,jsx}"] 44 | } 45 | }, 46 | "test": { 47 | "executor": "@nrwl/jest:jest", 48 | "outputs": ["coverage/apps/web"], 49 | "options": { 50 | "jestConfig": "apps/web/jest.config.js", 51 | "passWithNoTests": true 52 | } 53 | } 54 | } 55 | }, 56 | "web-e2e": { 57 | "root": "apps/web-e2e", 58 | "sourceRoot": "apps/web-e2e/src", 59 | "projectType": "application", 60 | "targets": { 61 | "e2e": { 62 | "executor": "@nrwl/cypress:cypress", 63 | "options": { 64 | "cypressConfig": "apps/web-e2e/cypress.json", 65 | "tsConfig": "apps/web-e2e/tsconfig.e2e.json", 66 | "devServerTarget": "web:serve" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "devServerTarget": "web:serve:production" 71 | } 72 | } 73 | }, 74 | "lint": { 75 | "executor": "@nrwl/linter:eslint", 76 | "options": { 77 | "lintFilePatterns": ["apps/web-e2e/**/*.{js,ts}"] 78 | } 79 | } 80 | } 81 | }, 82 | "api": { 83 | "root": "apps/api", 84 | "sourceRoot": "apps/api/src", 85 | "projectType": "application", 86 | "prefix": "api", 87 | "targets": { 88 | "build": { 89 | "executor": "@nrwl/node:build", 90 | "outputs": ["{options.outputPath}"], 91 | "options": { 92 | "outputPath": "dist/apps/api", 93 | "main": "apps/api/src/main.ts", 94 | "tsConfig": "apps/api/tsconfig.app.json", 95 | "assets": ["apps/api/src/assets"], 96 | "webpackConfig": "apps/api/webpack.config.js" 97 | }, 98 | "configurations": { 99 | "production": { 100 | "optimization": true, 101 | "extractLicenses": true, 102 | "inspect": false, 103 | "fileReplacements": [ 104 | { 105 | "replace": "apps/api/src/environments/environment.ts", 106 | "with": "apps/api/src/environments/environment.prod.ts" 107 | } 108 | ] 109 | } 110 | } 111 | }, 112 | "serve": { 113 | "executor": "@nrwl/node:execute", 114 | "options": { 115 | "buildTarget": "api:build" 116 | } 117 | }, 118 | "serve-test": { 119 | "builder": "@nrwl/workspace:run-commands", 120 | "options": { 121 | "commands": ["yarn run nx serve api"], 122 | "parallel": false, 123 | "envFile": "apps/api/.test.env" 124 | } 125 | }, 126 | "lint": { 127 | "executor": "@nrwl/linter:eslint", 128 | "options": { 129 | "lintFilePatterns": ["apps/api/**/*.ts"] 130 | } 131 | }, 132 | "test": { 133 | "executor": "@nrwl/jest:jest", 134 | "outputs": ["coverage/apps/api"], 135 | "options": { 136 | "jestConfig": "apps/api/jest.config.js", 137 | "passWithNoTests": true 138 | } 139 | }, 140 | "migration": { 141 | "executor": "@nrwl/workspace:run-commands", 142 | "outputs": [], 143 | "options": { 144 | "commands": [ 145 | "(rm ormconfig.json || :) && yarn run ts-node tools/scripts/write-type-orm-config.ts", 146 | "yarn run ts-node -P apps/api/tsconfig.typeorm.json node_modules/.bin/typeorm {args.run}" 147 | ], 148 | "parallel": false 149 | } 150 | }, 151 | "migration-create": { 152 | "executor": "@nrwl/workspace:run-commands", 153 | "outputs": [], 154 | "options": { 155 | "commands": [ 156 | "(rm ormconfig.json || :) && yarn run ts-node tools/scripts/write-type-orm-config.ts", 157 | "yarn run ts-node -P apps/api/tsconfig.typeorm.json node_modules/.bin/typeorm migration:create -n {args.name}" 158 | ], 159 | "parallel": false 160 | } 161 | }, 162 | "migration-generate": { 163 | "executor": "@nrwl/workspace:run-commands", 164 | "outputs": [], 165 | "options": { 166 | "commands": [ 167 | "(rm ormconfig.json || :) && yarn run ts-node tools/scripts/write-type-orm-config.ts", 168 | "yarn run ts-node -P apps/api/tsconfig.typeorm.json node_modules/.bin/typeorm migration:generate -n {args.name}" 169 | ], 170 | "parallel": false 171 | } 172 | }, 173 | "migration-run": { 174 | "executor": "@nrwl/workspace:run-commands", 175 | "outputs": [], 176 | "options": { 177 | "commands": [ 178 | "(rm ormconfig.json || :) && yarn run ts-node tools/scripts/write-type-orm-config.ts", 179 | "yarn run ts-node -P apps/api/tsconfig.typeorm.json node_modules/.bin/typeorm migration:run" 180 | ], 181 | "parallel": false 182 | } 183 | } 184 | } 185 | }, 186 | "domain": { 187 | "root": "libs/domain", 188 | "sourceRoot": "libs/domain/src", 189 | "projectType": "library", 190 | "targets": { 191 | "lint": { 192 | "executor": "@nrwl/linter:eslint", 193 | "options": { 194 | "lintFilePatterns": ["libs/domain/**/*.ts"] 195 | } 196 | }, 197 | "test": { 198 | "executor": "@nrwl/jest:jest", 199 | "outputs": ["coverage/libs/domain"], 200 | "options": { 201 | "jestConfig": "libs/domain/jest.config.js", 202 | "passWithNoTests": true 203 | } 204 | } 205 | } 206 | }, 207 | "ui": { 208 | "root": "libs/ui", 209 | "sourceRoot": "libs/ui/src", 210 | "projectType": "library", 211 | "targets": { 212 | "lint": { 213 | "executor": "@nrwl/linter:eslint", 214 | "options": { 215 | "lintFilePatterns": ["libs/ui/**/*.{ts,tsx,js,jsx}"] 216 | } 217 | }, 218 | "test": { 219 | "executor": "@nrwl/jest:jest", 220 | "outputs": ["coverage/libs/ui"], 221 | "options": { 222 | "jestConfig": "libs/ui/jest.config.js", 223 | "passWithNoTests": true 224 | } 225 | }, 226 | "storybook": { 227 | "executor": "@nrwl/storybook:storybook", 228 | "options": { 229 | "uiFramework": "@storybook/react", 230 | "port": 4400, 231 | "config": { 232 | "configFolder": "libs/ui/.storybook" 233 | } 234 | }, 235 | "configurations": { 236 | "ci": { 237 | "quiet": true 238 | } 239 | } 240 | }, 241 | "build-storybook": { 242 | "executor": "@nrwl/storybook:build", 243 | "options": { 244 | "uiFramework": "@storybook/react", 245 | "outputPath": "dist/storybook/ui", 246 | "config": { 247 | "configFolder": "libs/ui/.storybook" 248 | } 249 | }, 250 | "configurations": { 251 | "ci": { 252 | "quiet": true 253 | } 254 | } 255 | } 256 | } 257 | }, 258 | "contracts": { 259 | "root": "libs/contracts", 260 | "sourceRoot": "libs/contracts/src", 261 | "projectType": "library", 262 | "targets": { 263 | "lint": { 264 | "executor": "@nrwl/linter:eslint", 265 | "options": { 266 | "lintFilePatterns": ["libs/contracts/**/*.ts"] 267 | } 268 | }, 269 | "test": { 270 | "executor": "@nrwl/jest:jest", 271 | "outputs": ["coverage/libs/contracts"], 272 | "options": { 273 | "jestConfig": "libs/contracts/jest.config.js", 274 | "passWithNoTests": true 275 | } 276 | } 277 | } 278 | }, 279 | "admin": { 280 | "root": "apps/admin", 281 | "sourceRoot": "apps/admin/src", 282 | "projectType": "application", 283 | "targets": { 284 | "build": { 285 | "executor": "@nrwl/web:build", 286 | "outputs": ["{options.outputPath}"], 287 | "options": { 288 | "outputPath": "dist/apps/admin", 289 | "index": "apps/admin/src/index.html", 290 | "main": "apps/admin/src/main.tsx", 291 | "polyfills": "apps/admin/src/polyfills.ts", 292 | "tsConfig": "apps/admin/tsconfig.app.json", 293 | "assets": ["apps/admin/src/favicon.ico", "apps/admin/src/assets"], 294 | "styles": [], 295 | "scripts": [], 296 | "webpackConfig": "apps/admin/webpack.config.js" 297 | }, 298 | "configurations": { 299 | "production": { 300 | "fileReplacements": [ 301 | { 302 | "replace": "apps/admin/src/environments/environment.ts", 303 | "with": "apps/admin/src/environments/environment.prod.ts" 304 | } 305 | ], 306 | "optimization": true, 307 | "outputHashing": "all", 308 | "sourceMap": false, 309 | "extractCss": true, 310 | "namedChunks": false, 311 | "extractLicenses": true, 312 | "vendorChunk": false, 313 | "budgets": [ 314 | { 315 | "type": "initial", 316 | "maximumWarning": "2mb", 317 | "maximumError": "5mb" 318 | } 319 | ] 320 | } 321 | } 322 | }, 323 | "serve": { 324 | "executor": "@nrwl/web:dev-server", 325 | "options": { 326 | "buildTarget": "admin:build", 327 | "dev": true, 328 | "proxyConfig": "apps/admin/proxy.conf.json" 329 | }, 330 | "configurations": { 331 | "production": { 332 | "buildTarget": "admin:build:production" 333 | } 334 | } 335 | }, 336 | "lint": { 337 | "executor": "@nrwl/linter:eslint", 338 | "options": { 339 | "lintFilePatterns": ["apps/admin/**/*.{ts,tsx,js,jsx}"] 340 | } 341 | }, 342 | "test": { 343 | "executor": "@nrwl/jest:jest", 344 | "outputs": ["coverage/apps/admin"], 345 | "options": { 346 | "jestConfig": "apps/admin/jest.config.js", 347 | "passWithNoTests": true 348 | } 349 | } 350 | } 351 | }, 352 | "admin-e2e": { 353 | "root": "apps/admin-e2e", 354 | "sourceRoot": "apps/admin-e2e/src", 355 | "projectType": "application", 356 | "targets": { 357 | "e2e": { 358 | "executor": "@nrwl/cypress:cypress", 359 | "options": { 360 | "cypressConfig": "apps/admin-e2e/cypress.json", 361 | "tsConfig": "apps/admin-e2e/tsconfig.e2e.json", 362 | "devServerTarget": "admin:serve" 363 | }, 364 | "configurations": { 365 | "production": { 366 | "devServerTarget": "admin:serve:production" 367 | } 368 | } 369 | }, 370 | "lint": { 371 | "executor": "@nrwl/linter:eslint", 372 | "options": { 373 | "lintFilePatterns": ["apps/admin-e2e/**/*.{js,ts}"] 374 | } 375 | } 376 | } 377 | } 378 | }, 379 | "cli": { 380 | "defaultCollection": "@nrwl/next" 381 | }, 382 | "generators": { 383 | "@nrwl/react": { 384 | "application": { 385 | "style": "none", 386 | "linter": "eslint", 387 | "babel": true 388 | }, 389 | "component": { 390 | "style": "none" 391 | }, 392 | "library": { 393 | "style": "none", 394 | "linter": "eslint" 395 | } 396 | }, 397 | "@nrwl/next": { 398 | "application": { 399 | "style": "css", 400 | "linter": "eslint" 401 | } 402 | } 403 | }, 404 | "defaultProject": "web" 405 | } 406 | --------------------------------------------------------------------------------