├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── node.js.yml │ └── release.yml ├── .gitignore ├── .npmignore ├── .releaserc ├── .travis.yml ├── README.md ├── SECURITY.md ├── _config.yml ├── generators ├── api │ ├── index.js │ └── templates │ │ ├── controller.js │ │ ├── index.js │ │ ├── model.js │ │ ├── service.js │ │ ├── test.js │ │ └── validation.js └── app │ ├── index.js │ └── templates │ ├── .dockerignore │ ├── .editorconfig │ ├── .env.example │ ├── .gitignore │ ├── README.md │ ├── backend │ ├── .babelrc │ ├── .eslintrc │ ├── .prettierrc │ ├── .vscode │ │ └── launch.json │ ├── certs │ │ └── .gitkeep │ ├── keys │ │ └── .gitkeep │ ├── logs │ │ └── .gitkeep │ ├── nodemon.json │ ├── package-lock.json │ ├── package.json │ ├── processes.json │ ├── scripts │ │ ├── backup.sh │ │ ├── backups.sh │ │ └── restore.sh │ ├── src │ │ ├── api │ │ │ ├── auth │ │ │ │ ├── auth.controller.js │ │ │ │ ├── auth.service.js │ │ │ │ ├── auth.test.js │ │ │ │ ├── auth.validation.js │ │ │ │ └── index.js │ │ │ ├── configs │ │ │ │ ├── config.controller.js │ │ │ │ ├── config.model.js │ │ │ │ ├── config.service.js │ │ │ │ └── index.js │ │ │ ├── deviceTokens │ │ │ │ ├── deviceToken.controller.js │ │ │ │ ├── deviceToken.model.js │ │ │ │ ├── deviceToken.service.js │ │ │ │ ├── deviceToken.test.js │ │ │ │ ├── deviceToken.validation.js │ │ │ │ └── index.js │ │ │ ├── iaps │ │ │ │ ├── iap.controller.js │ │ │ │ ├── iap.model.js │ │ │ │ ├── iap.service.js │ │ │ │ ├── iap.test.js │ │ │ │ ├── iap.validation.js │ │ │ │ └── index.js │ │ │ ├── inAppPurchaseNotifications │ │ │ │ ├── inAppPurchaseNotification.controller.js │ │ │ │ ├── inAppPurchaseNotification.model.js │ │ │ │ ├── inAppPurchaseNotification.service.js │ │ │ │ ├── inAppPurchaseNotification.test.js │ │ │ │ ├── inAppPurchaseNotification.validation.js │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ ├── packages │ │ │ │ ├── index.js │ │ │ │ ├── packages.controller.js │ │ │ │ ├── packages.model.js │ │ │ │ ├── packages.service.js │ │ │ │ ├── packages.test.js │ │ │ │ └── packages.validation.js │ │ │ ├── refreshTokens │ │ │ │ ├── index.js │ │ │ │ ├── refreshToken.controller.js │ │ │ │ ├── refreshToken.model.js │ │ │ │ ├── refreshToken.service.js │ │ │ │ ├── refreshToken.test.js │ │ │ │ └── refreshToken.validation.js │ │ │ ├── uploads │ │ │ │ ├── index.js │ │ │ │ ├── upload.controller.js │ │ │ │ └── upload.service.js │ │ │ └── users │ │ │ │ ├── index.js │ │ │ │ ├── user.validation.js │ │ │ │ ├── users.controller.js │ │ │ │ ├── users.model.js │ │ │ │ └── users.service.js │ │ ├── app.js │ │ ├── config │ │ │ └── index.js │ │ ├── core │ │ │ ├── controller.js │ │ │ ├── error.response.js │ │ │ ├── service.js │ │ │ └── success.response.js │ │ ├── db_seed │ │ │ ├── index.js │ │ │ └── user_seeder.js │ │ ├── helpers │ │ │ ├── common │ │ │ │ ├── Controller.js │ │ │ │ ├── Service.js │ │ │ │ └── index.js │ │ │ ├── error.js │ │ │ ├── handle-errors.js │ │ │ ├── index.js │ │ │ ├── pushToken.js │ │ │ ├── receipts.js │ │ │ ├── response.js │ │ │ ├── schemas.js │ │ │ └── utils.js │ │ ├── middlewares │ │ │ ├── auth.js │ │ │ └── rate-limit.js │ │ ├── server.js │ │ └── services │ │ │ ├── index.js │ │ │ ├── jwt.js │ │ │ ├── logger.js │ │ │ ├── mailgun.js │ │ │ ├── mongoose.js │ │ │ ├── passport.js │ │ │ ├── redis.js │ │ │ ├── response.js │ │ │ ├── storage.js │ │ │ └── swagger.js │ └── uploads │ │ └── .gitkeep │ ├── bitbucket-pipelines.yml │ ├── compose │ ├── client │ │ └── Dockerfile │ ├── nginx │ │ ├── Dockerfile │ │ └── conf.d │ │ │ └── <%=project_slug%>.conf │ └── node │ │ ├── Dockerfile.local │ │ └── Dockerfile.production │ ├── docker-compose.prod.yml │ ├── docker-compose.staging.yml │ ├── docker-compose.test.yml │ ├── docker-compose.yml │ ├── frontend │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── NotFound.js │ │ ├── addUploadFeature.js │ │ ├── authProvider.js │ │ ├── configs │ │ │ ├── ConfigCreate.js │ │ │ ├── ConfigEdit.js │ │ │ ├── ConfigList.js │ │ │ └── index.js │ │ ├── deviceTokens │ │ │ ├── DeviceTokenCreate.js │ │ │ ├── DeviceTokenEdit.js │ │ │ ├── DeviceTokenList.js │ │ │ └── index.js │ │ ├── iaps │ │ │ ├── IapCreate.js │ │ │ ├── IapEdit.js │ │ │ ├── IapList.js │ │ │ └── index.js │ │ ├── index.css │ │ ├── index.js │ │ ├── logo.svg │ │ ├── restProvider.js │ │ ├── serviceWorker.js │ │ ├── users │ │ │ ├── MyUrlField.js │ │ │ ├── UserEdit.js │ │ │ ├── UserList.js │ │ │ └── index.js │ │ ├── utils │ │ │ ├── fetch.js │ │ │ └── tokenProvider.js │ │ └── validates │ │ │ └── index.js │ └── yarn.lock │ └── scripts │ ├── deploy.production.sh │ └── deploy.staging.sh ├── package-lock.json ├── package.json └── tests ├── index.js └── test_docker.sh /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | 25 | [nginx.conf] 26 | indent_style = space 27 | indent_size = 2 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for backend 9 | - package-ecosystem: "npm" 10 | directory: "/generators/app/templates/backend" 11 | schedule: 12 | interval: "weekly" 13 | # Maintain dependencies for frontend 14 | - package-ecosystem: "npm" 15 | directory: "/generators/app/templates/frontend" 16 | schedule: 17 | interval: "weekly" 18 | # Maintain dependencies for generators 19 | - package-ecosystem: "npm" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: ["master"] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: ["master"] 20 | schedule: 21 | - cron: '38 8 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: ['javascript'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v3 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v3 73 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | # push: 8 | # branches: [ master ] 9 | pull_request: 10 | branches: [master] 11 | 12 | permissions: 13 | contents: read # for checkout 14 | 15 | jobs: 16 | test-gen-project: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node-version: [lts/*] 21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | test-api: 33 | runs-on: ubuntu-latest 34 | env: 35 | DOCKER_BUILDKIT: 1 36 | COMPOSE_DOCKER_CLI_BUILD: 1 37 | strategy: 38 | matrix: 39 | node-version: [lts/*] 40 | steps: 41 | - uses: actions/checkout@v4 42 | - name: Use Node.js ${{ matrix.node-version }} 43 | uses: actions/setup-node@v2 44 | with: 45 | node-version: ${{ matrix.node-version }} 46 | cache: 'npm' 47 | - name: Install dependencies 48 | run: npm install -g yo@4.3.1 49 | - name: Install package 50 | run: npm install 51 | - name: Test docker 52 | run: sh tests/test_docker.sh 53 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master # or main 6 | 7 | permissions: 8 | contents: read # for checkout 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write # to be able to publish a GitHub release 16 | issues: write # to be able to comment on released issues 17 | pull-requests: write # to be able to comment on released pull requests 18 | id-token: write # to enable use of OIDC for npm provenance 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - name: Setup Node.js 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: "lts/*" 28 | - name: Install dependencies 29 | run: npm clean-install 30 | - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies 31 | run: npm audit signatures 32 | - name: Release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | run: npx semantic-release 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs/* 3 | logs/.gitkeep 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | .env.test 69 | 70 | # parcel-bundler cache (https://parceljs.org/) 71 | .cache 72 | 73 | # next.js build output 74 | .next 75 | 76 | # nuxt.js build output 77 | .nuxt 78 | 79 | # vuepress build output 80 | .vuepress/dist 81 | 82 | # Serverless directories 83 | .serverless/ 84 | 85 | # FuseBox cache 86 | .fusebox/ 87 | 88 | # DynamoDB Local files 89 | .dynamodb/ 90 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "release": { 3 | "branches": [ 4 | "master" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | 5 | services: 6 | - docker 7 | 8 | node_js: 9 | - "10.16.0" 10 | 11 | env: 12 | - TOX_ENV=py36 13 | 14 | before_install: 15 | - docker-compose -v 16 | - docker -v 17 | 18 | script: 19 | # - sh tests/test_docker.sh 20 | - npm run test 21 | 22 | install: 23 | - npm install -g yo 24 | - npm install 25 | - npm link 26 | 27 | notifications: 28 | email: 29 | on_success: change 30 | on_failure: always 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generator Expressjs Rest 2 | 3 | [![Build Status](https://travis-ci.org/minhuyen/generator-expressjs-rest.svg?branch=master)](https://travis-ci.org/minhuyen/generator-expressjs-rest) 4 | 5 | ## Features 6 | 7 | - User Registration 8 | - Basic Authentication with username and password 9 | - Admin use [react-admin](https://github.com/marmelab/react-admin) 10 | - Oauth 2.0 Authentication 11 | - Facebook 12 | - Google 13 | - Upload Photo to S3 amazon 14 | - Docker 15 | 16 | ### Prerequisites 17 | 18 | - [Docker (at least 1.10)](https://www.docker.com/) 19 | - [Docker-compose (at least 1.6)](https://docs.docker.com/compose/install/) 20 | 21 | ## Installation 22 | 23 | Fist, install [Yeoman](http://yeoman.io) and generator-expressjs-rest using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)). 24 | 25 | ```bash 26 | npm install -g yo 27 | npm install -g generator-expressjs-rest 28 | ``` 29 | 30 | ## Generators 31 | 32 | Then, you can use `yo` to generate your project. 33 | 34 | ```bash 35 | yo expressjs-rest # generate a new project 36 | yo expressjs-rest:api # generate a new api endpoint inside your project 37 | ``` 38 | 39 | ## Commands 40 | 41 | After you generate your project, these commands. 42 | 43 | ```bash 44 | cd your-project-name 45 | mv .env.example .env 46 | 47 | docker-compose build 48 | docker-compose run --rm client yarn build 49 | docker-compose up 50 | ``` 51 | 52 | ## Playing locally 53 | 54 | ## Deploy 55 | 56 | ## Directory structure 57 | 58 | ### Overview 59 | 60 | ```bash 61 | src/ 62 | ├─ api/ 63 | │ ├─ auth/ 64 | │ │ ├─ index.js 65 | │ │ ├─ auth.service.js 66 | │ │ ├─ auth.validation.js 67 | │ │ ├─ auth.controller.js 68 | │ │ └─ auth.test.js 69 | │ ├─ uploads/ 70 | │ │ ├─ index.js 71 | │ │ ├─ upload.controller.js 72 | │ ├─ users/ 73 | │ │ ├─ index.js 74 | │ │ ├─ user.controller.js 75 | │ │ ├─ user.validation.js 76 | │ │ ├─ user.model.js 77 | │ │ ├─ user.service.js 78 | │ │ └─ user.test.js 79 | │ └─ index.js 80 | ├─ services/ 81 | │ ├─ index.js 82 | │ ├─ jwt.js 83 | │ ├─ logger.js 84 | │ ├─ mailgun.js 85 | │ ├─ mongoose.js 86 | │ ├─ passport.js 87 | │ ├─ response.js 88 | │ ├─ s3.js 89 | │ ├─ swagger.js 90 | │ └─ your-service.js 91 | ├─ app.js 92 | ├─ config.js 93 | └─ index.js 94 | ``` 95 | 96 | ### src/api/ 97 | 98 | Here is where the API endpoints are defined. Each API has its own folder. 99 | 100 | #### src/api/some-endpoint/model.js 101 | 102 | It defines the Mongoose schema and model for the API endpoint. Any changes to the data model should be done here. 103 | 104 | #### src/api/some-endpoint/controller.js 105 | 106 | This is the API controller file. It defines the main router middlewares which use the API model. 107 | 108 | #### src/api/some-endpoint/index.js 109 | 110 | This is the entry file of the API. It defines the routes using, along other middlewares (like session, validation etc.), the middlewares defined in the `some-endpoint.controller.js` file. 111 | 112 | ### services/ 113 | 114 | Here you can put `helpers`, `libraries` and other types of modules which you want to use in your APIs. 115 | 116 | ## TODO 117 | 118 | - Support optional phone authentication 119 | - Support optional email confirmation process 120 | - Support Twitter and other social login methods 121 | - Socket.io support 122 | 123 | PRs are welcome. 124 | 125 | ## Credits 126 | 127 | [@minhuyen](https://github.com/minhuyen) and all [contributors](https://github.com/minhuyen/generator-express-api/graphs/contributors) 128 | 129 | ## License 130 | 131 | MIT © [minhuyen](https://github.com/minhuyen) 132 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /generators/api/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Generator = require("yeoman-generator"); 4 | const pluralize = require("pluralize"); 5 | const _ = require("lodash"); 6 | const path = require("path"); 7 | 8 | module.exports = class extends Generator { 9 | // The name `constructor` is important here 10 | constructor(args, opts) { 11 | // Calling the super constructor is important so our generator is correctly set up 12 | super(args, opts); 13 | 14 | // Next, add your custom code 15 | this.option("babel"); // This method adds support for a `--babel` flag 16 | } 17 | 18 | async prompting() { 19 | this.answers = await this.prompt([ 20 | { 21 | type: "input", 22 | name: "name", 23 | message: "Your model name", 24 | default: this.appname, // Default to current folder name 25 | }, 26 | ]); 27 | this.config.save(); 28 | } 29 | 30 | writing() { 31 | const srcDir = this.config.get("srcDir") || "src"; 32 | const apiDir = this.config.get("apiDir") || "api"; 33 | const modelName = _.startCase(this.answers.name).replace(" ", ""); 34 | const camel = _.camelCase(modelName); 35 | const camels = pluralize(camel); 36 | const modelNames = pluralize(modelName); 37 | const filepath = function (filename) { 38 | return path.join("backend", srcDir, apiDir, camels, filename); 39 | }; 40 | 41 | this.fs.copyTpl( 42 | this.templatePath("controller.js"), 43 | this.destinationPath(filepath(`${camel}.controller.js`)), 44 | { name: modelName, camelName: camel } 45 | ); 46 | 47 | this.fs.copyTpl( 48 | this.templatePath("index.js"), 49 | this.destinationPath(filepath("index.js")), 50 | { 51 | name: modelName, 52 | camelName: camel, 53 | camelNames: camels, 54 | names: modelNames, 55 | } 56 | ); 57 | 58 | this.fs.copyTpl( 59 | this.templatePath("model.js"), 60 | this.destinationPath(filepath(`${camel}.model.js`)), 61 | { name: modelName, camelName: camel } 62 | ); 63 | 64 | this.fs.copyTpl( 65 | this.templatePath("service.js"), 66 | this.destinationPath(filepath(`${camel}.service.js`)), 67 | { name: modelName, camelName: camel } 68 | ); 69 | 70 | this.fs.copyTpl( 71 | this.templatePath("test.js"), 72 | this.destinationPath(filepath(`${camel}.test.js`)), 73 | { name: modelName, camelName: camel } 74 | ); 75 | 76 | this.fs.copyTpl( 77 | this.templatePath("validation.js"), 78 | this.destinationPath(filepath(`${camel}.validation.js`)), 79 | { name: modelName, camelName: camel } 80 | ); 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /generators/api/templates/controller.js: -------------------------------------------------------------------------------- 1 | import Controller from "../../core/controller"; 2 | import <%=camelName%>Service from "./<%=camelName%>.service"; 3 | 4 | class <%=name%>Controller extends Controller { 5 | constructor(service, name) { 6 | super(service, name); 7 | } 8 | } 9 | 10 | export default new <%=name%>Controller(<%=camelName%>Service, "<%=name%>"); 11 | -------------------------------------------------------------------------------- /generators/api/templates/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { celebrate } from "celebrate"; 3 | import <%=camelName%>Controller from "./<%=camelName%>.controller"; 4 | import AuthService from "../../middlewares/auth"; 5 | import { 6 | createValidationSchema, 7 | updateValidationSchema, 8 | customPaginateValidateSchema, 9 | } from "./<%=camelName%>.validation"; 10 | 11 | const router = express.Router(); 12 | /** 13 | * @swagger 14 | * 15 | * definitions: 16 | * <%=name%>: 17 | * type: object 18 | * required: 19 | * - field1 20 | * - field2 21 | * properties: 22 | * field1: 23 | * type: string 24 | * field2: 25 | * type: string 26 | * 27 | * ArrayOf<%=names%>: 28 | * type: array 29 | * items: 30 | * $ref: "#/definitions/<%=name%>" 31 | */ 32 | 33 | /** 34 | * @swagger 35 | * 36 | * /<%=camelNames%>: 37 | * post: 38 | * tags: [<%=camelNames%>] 39 | * description: create a <%=camelName%> 40 | * security: 41 | * - BearerAuth: [] 42 | * produces: 43 | * - application/json 44 | * parameters: 45 | * - name: data 46 | * in: body 47 | * required: true 48 | * schema: 49 | * $ref: "#/definitions/<%=name%>" 50 | * 51 | * responses: 52 | * 200: 53 | * description: OK 54 | * schema: 55 | * $ref: "#/definitions/<%=name%>" 56 | * 400: 57 | * $ref: "#/responses/Error" 58 | * 401: 59 | * $ref: "#/responses/Unauthorized" 60 | */ 61 | 62 | router.post( 63 | "/", 64 | [AuthService.required, celebrate({ body: createValidationSchema })], 65 | <%=camelName%>Controller.create 66 | ); 67 | 68 | /** 69 | * @swagger 70 | * 71 | * /<%=camelNames%>: 72 | * put: 73 | * tags: [<%=camelNames%>] 74 | * description: create a <%=camelName%> 75 | * security: 76 | * - BearerAuth: [] 77 | * produces: 78 | * - application/json 79 | * parameters: 80 | * - name: data 81 | * in: body 82 | * required: true 83 | * schema: 84 | * $ref: "#/definitions/<%=name%>" 85 | * 86 | * responses: 87 | * 200: 88 | * description: OK 89 | * schema: 90 | * $ref: "#/definitions/<%=name%>" 91 | * 400: 92 | * $ref: "#/responses/Error" 93 | * 401: 94 | * $ref: "#/responses/Unauthorized" 95 | */ 96 | 97 | router.put( 98 | "/:id", 99 | [AuthService.required], 100 | celebrate({ body: updateValidationSchema }), 101 | <%=camelName%>Controller.update 102 | ); 103 | 104 | /** 105 | * @swagger 106 | * 107 | * /<%=camelNames%>: 108 | * get: 109 | * tags: [<%=camelNames%>] 110 | * description: get all <%=camelNames%> 111 | * produces: 112 | * - application/json 113 | * parameters: 114 | * - $ref: "#/parameters/pageParam" 115 | * - $ref: "#/parameters/limitParam" 116 | * responses: 117 | * 200: 118 | * description: OK 119 | * schema: 120 | * type: object 121 | * properties: 122 | * page: 123 | * type: integer 124 | * format: int32 125 | * pages: 126 | * type: integer 127 | * format: int32 128 | * limit: 129 | * type: integer 130 | * format: int32 131 | * total: 132 | * type: integer 133 | * format: int32 134 | * data: 135 | * $ref: "#/definitions/ArrayOf<%=names%>" 136 | * 401: 137 | * $ref: "#/responses/Unauthorized" 138 | */ 139 | router.get( 140 | "/", 141 | AuthService.optional, 142 | celebrate({ query: customPaginateValidateSchema }), 143 | <%=camelName%>Controller.findAll 144 | ); 145 | 146 | /** 147 | * @swagger 148 | * 149 | * /<%=camelNames%>/{id}: 150 | * get: 151 | * tags: [<%=camelNames%>] 152 | * description: get detail <%=camelName%> 153 | * produces: 154 | * - application/json 155 | * parameters: 156 | * - name: id 157 | * in: path 158 | * description: <%=camelName%> id 159 | * required: true 160 | * type: string 161 | * responses: 162 | * 200: 163 | * description: OK 164 | * schema: 165 | * $ref: "#/definitions/<%=name%>" 166 | * 400: 167 | * $ref: "#/responses/Error" 168 | * 401: 169 | * $ref: "#/responses/Unauthorized" 170 | */ 171 | router.get("/:id", <%=camelName%>Controller.findOne); 172 | 173 | /** 174 | * @swagger 175 | * 176 | * /<%=camelNames%>/{id}: 177 | * delete: 178 | * tags: [<%=camelNames%>] 179 | * description: delete a <%=camelName%> 180 | * security: 181 | * - BearerAuth: [] 182 | * produces: 183 | * - application/json 184 | * parameters: 185 | * - name: id 186 | * in: path 187 | * description: <%=camelNames%> id 188 | * required: true 189 | * type: string 190 | * responses: 191 | * 200: 192 | * description: OK 193 | * schema: 194 | * $ref: "#/definitions/<%=name%>" 195 | * 400: 196 | * $ref: "#/responses/Error" 197 | * 401: 198 | * $ref: "#/responses/Unauthorized" 199 | */ 200 | router.delete("/:id", AuthService.required, <%=camelName%>Controller.remove); 201 | 202 | 203 | export default router; 204 | -------------------------------------------------------------------------------- /generators/api/templates/model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import mongooseUniqueValidator from "mongoose-unique-validator"; 4 | 5 | const <%=name%>Schema = new Schema( 6 | { 7 | field: { 8 | type: String, 9 | required: true 10 | }, 11 | field2: { 12 | type: String, 13 | required: true 14 | } 15 | }, 16 | { timestamps: true } 17 | ); 18 | 19 | <%=name%>Schema.plugin(mongoosePaginate); 20 | <%=name%>Schema.plugin(mongooseUniqueValidator); 21 | 22 | const <%=name%> = mongoose.model("<%=name%>", <%=name%>Schema); 23 | export default <%=name%>; 24 | -------------------------------------------------------------------------------- /generators/api/templates/service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import <%=name%> from "./<%=camelName%>.model"; 3 | 4 | 5 | class <%=name%>Service extends Service { 6 | constructor() { 7 | super(<%=name%>); 8 | } 9 | } 10 | 11 | export default new <%=name%>Service(); 12 | -------------------------------------------------------------------------------- /generators/api/templates/test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | 5 | const api = supertest(app); 6 | 7 | describe("Basic Mocha String Test", function() { 8 | it("should return number of characters in a string", function() { 9 | assert.equal("Hello".length, 5); 10 | }); 11 | it("should return first character of the string", function() { 12 | assert.equal("Hello".charAt(0), "H"); 13 | }); 14 | }); -------------------------------------------------------------------------------- /generators/api/templates/validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema, ObjectId } = schemas; 5 | 6 | 7 | export const customPaginateValidateSchema = paginateValidationSchema.keys(); 8 | 9 | export const createValidationSchema = Joi.object({ 10 | field: Joi.string().required(), 11 | field2: Joi.string().required() 12 | }); 13 | 14 | export const updateValidationSchema = Joi.object({ 15 | field: Joi.string().optional(), 16 | field2: Joi.string().optional() 17 | }).unknown(true); -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Generator = require("yeoman-generator"); 4 | 5 | module.exports = class extends Generator { 6 | // The name `constructor` is important here 7 | constructor(args, opts) { 8 | // Calling the super constructor is important so our generator is correctly set up 9 | super(args, opts); 10 | 11 | // Next, add your custom code 12 | this.option("input", { type: Boolean, default: true }); // This method adds support for a `--no-input` flag 13 | this.input = this.options["input"]; 14 | } 15 | 16 | async prompting() { 17 | if (this.input) { 18 | this.answers = await this.prompt([ 19 | { 20 | type: "input", 21 | name: "name", 22 | message: "Your project name", 23 | default: "awesome-express-project", // Default to current folder name 24 | }, 25 | { 26 | type: "confirm", 27 | name: "cool", 28 | message: "Would you like to enable the Cool feature?", 29 | default: true, 30 | }, 31 | ]); 32 | // this.config.save(); 33 | } 34 | } 35 | 36 | writing() { 37 | const coolFeature = this.answers?.cool || true; 38 | this.log("cool feature", coolFeature); // user answer `cool` used 39 | const projectName = this.answers?.name || "awesome-express-project"; 40 | this.fs.copyTpl( 41 | this.templatePath(), 42 | this.destinationPath(projectName), 43 | { project_slug: projectName }, 44 | {}, 45 | { globOptions: { dot: true } } //https://github.com/SBoudrias/mem-fs-editor/issues/86 46 | ); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /generators/app/templates/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/.git 3 | **/README.md 4 | **/LICENSE 5 | **/.vscode 6 | **/npm-debug.log 7 | **/coverage 8 | **/.env 9 | **/.editorconfig 10 | **/.aws 11 | **/dist 12 | **/logs 13 | -------------------------------------------------------------------------------- /generators/app/templates/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /generators/app/templates/.env.example: -------------------------------------------------------------------------------- 1 | # database 2 | DATABASE_URL=mongodb://mongo:27017/<%=project_slug%>?authSource=admin 3 | MONGO_INITDB_ROOT_USERNAME=admin 4 | MONGO_INITDB_ROOT_PASSWORD=pwd123 5 | MONGO_BACKUP_FOLDER=./backups 6 | MONGO_DATABASE=<%=project_slug%> 7 | 8 | # Session 9 | SESSION_SECRET=your-session-secret 10 | 11 | # Default admin account 12 | DEFAULT_ADMIN_EMAIL=admin@gmail.com 13 | DEFAULT_ADMIN_PASSWORD=admin@123 14 | 15 | # Redis 16 | REDIS_URL=redis://redis:6379/0 17 | 18 | # Social Login 19 | FACEBOOK_CLIENT_ID=your-client-id 20 | FACEBOOK_CLIENT_SECRET=your-client-secret 21 | 22 | GOOGLE_CLIENT_ID=your-client-id 23 | GOOGLE_CLIENT_SECRET=your-client-secret 24 | 25 | # Jwt config 26 | JWT_SECRET=jwt_secret 27 | JWT_REFRESH_SECRET=jwt_refresh_secret 28 | JWT_EXPIRES=15m 29 | JWT_REFRESH_EXPIRES=60d 30 | 31 | # Upload 32 | AWS_S3_ACCESS_KEY_ID= 33 | AWS_S3_SECRET_ACCESS_KEY= 34 | AWS_STORAGE_BUCKET_NAME= 35 | AWS_REGION_NAME=us-east-1 36 | 37 | # Node environment 38 | NODE_ENV=development 39 | BABEL_ENV=debug 40 | 41 | # Push notification 42 | APN_KEY_ID=your-key-id 43 | APN_TEAM_ID=developer-team-id 44 | APN_TOPIC=your-app-bundle-id 45 | APN_PRODUCTION=false 46 | 47 | # Send email 48 | MAILGUN_API_KEY= 49 | MAILGUN_DOMAIN= 50 | 51 | # React 52 | WDS_SOCKET_PORT=0 53 | 54 | # Social login 55 | GOOGLE_USER_PROFILE_URL_ACCESS_TOKEN=https://www.googleapis.com/oauth2/v1/userinfo 56 | GOOGLE_USER_PROFILE_URL_ID_TOKEN=https://oauth2.googleapis.com/tokeninfo 57 | FACEBOOK_USER_PROFILE_URL=https://graph.facebook.com/me 58 | APPLE_USER_PROFILE_URL=https://appleid.apple.com/auth/keys 59 | 60 | # IAP iOS 61 | IOS_VERIFY_RECEIPT_URL=https://buy.itunes.apple.com/verifyReceipt 62 | IOS_SANDBOX_VERIFY_RECEIPT_URL=https://sandbox.itunes.apple.com/verifyReceipt 63 | IOS_VERIFY_RECEIPT_PASSWORD= 64 | 65 | # IAP Android 66 | GOOGLE_APPLICATION_CREDENTIALS=./key/serviceAccountKey.json 67 | 68 | # Apple In-app Purchase 69 | APPLE_ISSUER_ID= 70 | APPLE_KEY_ID= 71 | APPLE_BUNDLE_ID= 72 | APP_APPLE_ID= 73 | APPLE_PRIVATE_KEY_FILE_PATH= 74 | -------------------------------------------------------------------------------- /generators/app/templates/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs/* 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | backend/uploads/* 91 | !backend/uploads/.gitkeep 92 | 93 | backend/logs/* 94 | !backend/logs/.gitkeep 95 | -------------------------------------------------------------------------------- /generators/app/templates/README.md: -------------------------------------------------------------------------------- 1 | # <%=project_slug%> 2 | 3 | <%=project_slug%> 4 | 5 | ### Prerequisites 6 | 7 | - [Docker (at least 1.10)](https://www.docker.com/) 8 | - [Docker-compose (at least 1.6)](https://docs.docker.com/compose/install/) 9 | 10 | ## Getting Started 11 | 12 | To get up and running on local, simply do the following: 13 | 14 | $ cd <%=project_slug%> 15 | # build docker images 16 | $ docker-compose build 17 | $ docker-compose up 18 | 19 | ## Deployment 20 | 21 | ssh to server 22 | 23 | $ cd ~/<%=project_slug%> 24 | $ git pull origin develop 25 | $ docker-compose -f docker-compose.dev.yml build 26 | $ docker-compose -f docker-compose.dev.yml up -d 27 | -------------------------------------------------------------------------------- /generators/app/templates/backend/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | [ 14 | "@babel/plugin-proposal-object-rest-spread", 15 | { 16 | "useBuiltIns": true 17 | } 18 | ] 19 | ], 20 | "ignore": ["./src/client/*"], 21 | "env": { 22 | "debug": { 23 | "sourceMaps": "inline", 24 | "retainLines": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /generators/app/templates/backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "extends": [ 10 | "plugin:prettier/recommended", 11 | "eslint:recommended", 12 | "prettier" 13 | ], 14 | "plugins": [ 15 | "prettier" 16 | ], 17 | "parserOptions": { 18 | "ecmaVersion": 2020, 19 | "sourceType": "module" 20 | }, 21 | "rules": { 22 | "indent": [ 23 | 2, 24 | 2, 25 | { 26 | "SwitchCase": 1 27 | } 28 | ], 29 | "linebreak-style": [ 30 | "warn", 31 | "unix" 32 | ], 33 | "quotes": [ 34 | "error", 35 | "double" 36 | ], 37 | "semi": [ 38 | "warn", 39 | "always" 40 | ], 41 | "no-unused-vars": "warn", 42 | "no-console": "off" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /generators/app/templates/backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "useTabs": false, 7 | "printWidth": 80 8 | } 9 | -------------------------------------------------------------------------------- /generators/app/templates/backend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "address": "localhost", 9 | "localRoot": "${workspaceFolder}", 10 | "name": "Attach to Remote", 11 | "port": 9229, 12 | "remoteRoot": "/home/app/<%=project_slug%>", 13 | "request": "attach", 14 | "skipFiles": ["/**"], 15 | "type": "node" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /generators/app/templates/backend/certs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minhuyen/generator-expressjs-rest/9ec1b6dbb455631b02c46e29f0081b367a5dddfc/generators/app/templates/backend/certs/.gitkeep -------------------------------------------------------------------------------- /generators/app/templates/backend/keys/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minhuyen/generator-expressjs-rest/9ec1b6dbb455631b02c46e29f0081b367a5dddfc/generators/app/templates/backend/keys/.gitkeep -------------------------------------------------------------------------------- /generators/app/templates/backend/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minhuyen/generator-expressjs-rest/9ec1b6dbb455631b02c46e29f0081b367a5dddfc/generators/app/templates/backend/logs/.gitkeep -------------------------------------------------------------------------------- /generators/app/templates/backend/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "verbose": false, 3 | "ignore": [ 4 | "test/*", 5 | "fixtures/*", 6 | "node_modules/*", 7 | "package*.json", 8 | "logs/*", 9 | "dist/*", 10 | "src/client/*" 11 | ], 12 | "ext": "js css html" 13 | } 14 | -------------------------------------------------------------------------------- /generators/app/templates/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "<%=project_slug%>", 3 | "version": "0.1.0", 4 | "description": "<%=project_slug%>", 5 | "main": "server.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "NODE_ENV=test mocha --require @babel/register --exit ./src/api/**/*.test.js", 9 | "start": "NODE_ENV=development DEBUG=app:* nodemon --exec babel-node src/server.js", 10 | "debug": "nodemon --inspect-brk=0.0.0.0:9229 --nolazy --exec babel-node src/server.js", 11 | "build": "npm run clean && babel src --copy-files --out-dir dist --ignore src/client", 12 | "heroku-postbuild": "cd client && npm install && npm run build", 13 | "serve": "NODE_ENV=production node dist/server.js", 14 | "clean": "rimraf dist", 15 | "lint": "eslint './src/**/*.{js,jsx}'", 16 | "lint:fix": "eslint './src/**/*.{js,jsx}' --fix", 17 | "db": "babel-node src/db_seed" 18 | }, 19 | "author": "Uyen Do", 20 | "license": "ISC", 21 | "homepage": "https://bitbucket.org/minhuyen/<%=project_slug%>#readme", 22 | "dependencies": { 23 | "@apple/app-store-server-library": "^1.4.0", 24 | "@godaddy/terminus": "^4.12.0", 25 | "api-query-params": "^5.4.0", 26 | "aws-sdk": "^2.1383.0", 27 | "axios": "^1.4.0", 28 | "bcryptjs": "^2.4.3", 29 | "body-parser": "^1.20.2", 30 | "celebrate": "^15.0.1", 31 | "compression": "^1.7.3", 32 | "cookie-parser": "^1.4.5", 33 | "cors": "^2.8.5", 34 | "debug": "^4.3.4", 35 | "dotenv": "^6.2.0", 36 | "ejs": "^3.1.7", 37 | "express": "^4.16.4", 38 | "express-rate-limit": "^6.7.0", 39 | "express-session": "^1.17.3", 40 | "express-validator": "^6.15.0", 41 | "firebase-admin": "^11.8.0", 42 | "form-data": "^4.0.0", 43 | "googleapis": "^109.0.1", 44 | "helmet": "^7.0.0", 45 | "http-status": "^1.6.2", 46 | "http-status-codes": "^2.3.0", 47 | "jimp": "^0.22.8", 48 | "jsonwebtoken": "^9.0.0", 49 | "jwks-rsa": "^3.0.1", 50 | "lodash": "^4.17.11", 51 | "mailgun.js": "^9.3.0", 52 | "mime": "^3.0.0", 53 | "moment": "^2.29.4", 54 | "mongoose": "^6.12.0", 55 | "mongoose-delete": "^0.5.0", 56 | "mongoose-paginate-v2": "^1.3.18", 57 | "mongoose-unique-validator": "^3.1.0", 58 | "morgan": "^1.9.1", 59 | "multer": "^1.4.5-lts.1", 60 | "multer-s3": "^3.0.1", 61 | "node-schedule": "^1.3.2", 62 | "nodemon": "^2.0.17", 63 | "passport": "^0.6.0", 64 | "passport-custom": "^1.0.5", 65 | "passport-facebook-token": "^3.3.0", 66 | "passport-jwt": "^4.0.0", 67 | "passport-local": "^1.0.0", 68 | "passport-token-google": "^0.1.5", 69 | "randomstring": "^1.1.5", 70 | "rate-limit-redis": "^3.1.0", 71 | "redis": "^4.6.6", 72 | "sharp": "^0.32.1", 73 | "swagger-jsdoc": "^6.2.8", 74 | "swagger-ui-express": "^4.0.2", 75 | "util": "^0.12.5", 76 | "winston": "^3.8.2", 77 | "winston-daily-rotate-file": "^4.5.5" 78 | }, 79 | "devDependencies": { 80 | "@babel/cli": "^7.4.4", 81 | "@babel/core": "^7.22.1", 82 | "@babel/node": "^7.2.2", 83 | "@babel/plugin-proposal-object-rest-spread": "^7.4.4", 84 | "@babel/preset-env": "^7.22.4", 85 | "concurrently": "^5.3.0", 86 | "eslint": "^6.6.0", 87 | "eslint-config-prettier": "^6.5.0", 88 | "eslint-plugin-prettier": "^3.1.1", 89 | "jest": "^29.5.0", 90 | "mocha": "^10.2.0", 91 | "prettier": "^1.19.1", 92 | "rimraf": "^2.6.3", 93 | "supertest": "^4.0.2" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /generators/app/templates/backend/processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "merge_logs": true, 5 | "name": "<%=project_slug%>", 6 | "script": "dist/server.js", 7 | "exec_mode": "cluster", 8 | "instances": "max", 9 | "out_file": "out.log", 10 | "error_file": "error.log", 11 | "log_date_format": "MM/DD/YYYY HH:mm:ss" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/scripts/backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### Create a database backup. 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f .yml (exec |run --rm) mongo backup 7 | 8 | set -e 9 | 10 | BACKUP_NAME="backup_$(date +'%Y_%m_%dT%H_%M_%S').gz" 11 | DB="${MONGO_DATABASE}" 12 | PASSWORD="${MONGO_INITDB_ROOT_PASSWORD}" 13 | USERNAME="${MONGO_INITDB_ROOT_USERNAME}" 14 | BACKUP_FOLDER="${MONGO_BACKUP_FOLDER}" 15 | 16 | echo "Backing up MongoDB database" 17 | 18 | echo "Dumping MongoDB $DB database to compressed archive" 19 | mongodump -h mongo -u $USERNAME -p $PASSWORD --authenticationDatabase admin --db $DB --gzip --archive="$BACKUP_FOLDER/$BACKUP_NAME" 20 | 21 | # Delete backups created 5 or more days ago 22 | find $BACKUP_FOLDER -mindepth 1 -mtime +3 -delete 23 | 24 | echo 'Backup complete!' 25 | -------------------------------------------------------------------------------- /generators/app/templates/backend/scripts/backups.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### View backups. 4 | ### 5 | ### Usage: 6 | ### $ docker-compose -f .yml (exec |run --rm) mongo backups 7 | 8 | BACKUP_FOLDER="${MONGO_BACKUP_FOLDER}" 9 | 10 | echo "These are the backups you have got:" 11 | 12 | ls -lht "${BACKUP_FOLDER}" 13 | -------------------------------------------------------------------------------- /generators/app/templates/backend/scripts/restore.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### Restore database from a backup. 4 | ### 5 | ### Parameters: 6 | ### <1> filename of an existing backup. 7 | ### 8 | ### Usage: 9 | ### $ docker-compose -f .yml (exec |run --rm) mongo restore <1> 10 | 11 | set -e 12 | 13 | DB="${MONGO_DATABASE}" 14 | PASSWORD="${MONGO_INITDB_ROOT_PASSWORD}" 15 | USERNAME="${MONGO_INITDB_ROOT_USERNAME}" 16 | BACKUP_FOLDER="${MONGO_BACKUP_FOLDER}" 17 | 18 | if [[ -z ${1+x} ]]; then 19 | echo "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." 20 | exit 1 21 | fi 22 | 23 | backup_filename="${BACKUP_FOLDER}/${1}" 24 | 25 | if [[ ! -f "${backup_filename}" ]]; then 26 | echo "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." 27 | exit 1 28 | fi 29 | 30 | echo "Restoring up MongoDB database" 31 | 32 | echo "Restoring MongoDB $DB database to compressed archive" 33 | 34 | mongorestore -h mongo -u $USERNAME -p $PASSWORD --authenticationDatabase admin -d $DB --gzip --drop --archive=$backup_filename 35 | 36 | echo 'Restore complete!' 37 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/auth/auth.controller.js: -------------------------------------------------------------------------------- 1 | import httpStatus from "http-status"; 2 | import { logger } from "../../services"; 3 | import Response from "../../helpers/response"; 4 | import authService from "./auth.service"; 5 | import { OK, CREATED } from "../../core/success.response"; 6 | 7 | export const signup = async (req, res, next) => { 8 | try { 9 | const data = req.body; 10 | const ipAddress = req.ip; 11 | const result = await authService.signup(data, ipAddress); 12 | return new CREATED({ data: result }).send(res); 13 | // return Response.success(res, result, httpStatus.CREATED); 14 | } catch (exception) { 15 | next(exception); 16 | } 17 | }; 18 | 19 | export const login = async (req, res) => { 20 | const user = req.user; 21 | const ipAddress = req.ip; 22 | const result = await authService.login(user, ipAddress); 23 | setTokenCookie(res, result.refreshToken); 24 | // return the information including token as JSON 25 | // return Response.success(res, result, httpStatus.OK); 26 | return new OK({ data: result }).send(res); 27 | }; 28 | 29 | export const requestOtpLogin = async (req, res) => { 30 | const { email } = req.body; 31 | await authService.requestOtpLogin(email); 32 | // return the information including token as JSON 33 | return Response.success(res, { message: "Opt has been sent" }); 34 | }; 35 | 36 | export const handleCompareOtp = async (req, res, next) => { 37 | try { 38 | const { email, otpRequest } = req.body; 39 | const result = await authService.handleCompareOtp(email, otpRequest); 40 | return Response.success(res, result); 41 | } catch (exception) { 42 | next(exception); 43 | } 44 | }; 45 | 46 | export const logout = async (req, res, next) => { 47 | try { 48 | const { token } = req.body; 49 | const result = await authService.logout(token); 50 | return Response.success(res, result, httpStatus.OK); 51 | } catch (exception) { 52 | next(exception); 53 | } 54 | }; 55 | 56 | export const checkEmail = async (req, res, next) => { 57 | try { 58 | const { email } = req.body; 59 | const result = await authService.checkEmailIsValid(email); 60 | return Response.success(res, result, httpStatus.OK); 61 | } catch (exception) { 62 | next(exception); 63 | } 64 | }; 65 | 66 | export const checkUsername = async (req, res, next) => { 67 | try { 68 | const { username } = req.body; 69 | const result = await authService.checkUsernameIsValid(username); 70 | return Response.success(res, result, httpStatus.OK); 71 | } catch (exception) { 72 | next(exception); 73 | } 74 | }; 75 | 76 | export const forgotPassword = async (req, res, next) => { 77 | const { email } = req.body; 78 | try { 79 | await authService.forgotPassword(email); 80 | return Response.success(res); 81 | } catch (exception) { 82 | next(exception); 83 | } 84 | }; 85 | 86 | export const verifyCode = async (req, res, next) => { 87 | const data = req.body; 88 | try { 89 | const result = await authService.verifyCode(data); 90 | return Response.success(res, result); 91 | } catch (exception) { 92 | next(exception); 93 | } 94 | }; 95 | 96 | export const resetPassword = async (req, res, next) => { 97 | const { newPassword } = req.body; 98 | const user = req.user; 99 | try { 100 | const result = await authService.resetPassword(user, newPassword); 101 | return Response.success(res, result); 102 | } catch (exception) { 103 | next(exception); 104 | } 105 | }; 106 | 107 | export const loginWithGoogle = async (req, res, next) => { 108 | try { 109 | const { id_token, access_token } = req.body; 110 | const ipAddress = req.ip; 111 | const result = await authService.loginWithGoogle( 112 | id_token, 113 | access_token, 114 | ipAddress 115 | ); 116 | return Response.success(res, result); 117 | } catch (exception) { 118 | next(exception); 119 | } 120 | }; 121 | 122 | export const loginWithFacebook = async (req, res, next) => { 123 | try { 124 | const { access_token } = req.body; 125 | const ipAddress = req.ip; 126 | const result = await authService.loginWithFacebook(access_token, ipAddress); 127 | return Response.success(res, result); 128 | } catch (exception) { 129 | next(exception); 130 | } 131 | }; 132 | 133 | export const loginWithApple = async (req, res, next) => { 134 | try { 135 | const { access_token } = req.body; 136 | const ipAddress = req.ip; 137 | let result = await authService.loginWithApple(access_token, ipAddress); 138 | 139 | return Response.success(res, result); 140 | } catch (e) { 141 | next(e); 142 | } 143 | }; 144 | 145 | export const refreshToken = async (req, res, next) => { 146 | try { 147 | const token = req.body.refreshToken || req.cookies.refreshToken; 148 | const ipAddress = req.ip; 149 | const result = await authService.refreshToken(token, ipAddress); 150 | return Response.success(res, result); 151 | } catch (exception) { 152 | next(exception); 153 | } 154 | }; 155 | 156 | const setTokenCookie = (res, token) => { 157 | const cookieOptions = { 158 | httpOnly: true 159 | // expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) 160 | }; 161 | res.cookie("refreshToken", token, cookieOptions); 162 | }; 163 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/auth/auth.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | import User from "../users/users.model"; 5 | import authService from "../auth/auth.service"; 6 | 7 | const api = supertest(app); 8 | 9 | beforeEach(async () => { 10 | await User.deleteOne({ email: "test@test.com" }); 11 | await User.deleteOne({ email: "testLogin@test.com" }); 12 | await authService.signup({ 13 | email: "testLogin@test.com", 14 | password: "123456", 15 | fullName: "TestLogin" 16 | }); 17 | }); 18 | 19 | describe("GET /login", function() { 20 | it("should return 200", async function() { 21 | await api 22 | .post("/api/v1/auth/login") 23 | .send({ email: "testLogin@test.com", password: "123456" }) 24 | .set("Accept", "application/json") 25 | .expect("Content-Type", /json/) 26 | .expect(200); 27 | }); 28 | }); 29 | 30 | describe("GET /signup", function() { 31 | it("should return 201", async function() { 32 | await api 33 | .post("/api/v1/auth/signup") 34 | .send({ 35 | email: "test@test.com", 36 | password: "123456", 37 | fullName: "Test" 38 | }) 39 | .set("Accept", "application/json") 40 | .expect("Content-Type", /json/) 41 | .expect(201); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/auth/auth.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | // password and confirmPassword must contain the same value 4 | export const signupValidationSchema = Joi.object({ 5 | fullName: Joi.string().optional(), 6 | email: Joi.string() 7 | .email() 8 | .lowercase() 9 | .required(), 10 | password: Joi.string() 11 | .min(4) 12 | .required() 13 | .strict() 14 | }); 15 | 16 | export const loginValidationSchema = Joi.object({ 17 | email: Joi.string() 18 | .email() 19 | .required(), 20 | password: Joi.string() 21 | .min(4) 22 | .max(255) 23 | .required() 24 | }); 25 | 26 | export const compareOtpValidationSchema = Joi.object({ 27 | email: Joi.string() 28 | .email() 29 | .required(), 30 | otpRequest: Joi.string() 31 | .length(6) 32 | .regex(/\d/) 33 | .required() 34 | }); 35 | 36 | export const requestOtpLoginValidationSchema = Joi.object({ 37 | email: Joi.string() 38 | .email() 39 | .required() 40 | }); 41 | 42 | export const logoutValidationSchema = Joi.object({ 43 | token: Joi.string().optional() 44 | }); 45 | 46 | export const forgotPasswordSchema = Joi.object({ 47 | email: Joi.string() 48 | .email() 49 | .required() 50 | }); 51 | 52 | export const verifyCodeValidationSchema = Joi.object({ 53 | email: Joi.string() 54 | .email() 55 | .required(), 56 | code: Joi.string() 57 | .length(6) 58 | .regex(/\d/) 59 | .required() 60 | }); 61 | 62 | export const resetPasswordSchema = Joi.object({ 63 | newPassword: Joi.string() 64 | .min(4) 65 | .max(255) 66 | .required(), 67 | confirmNewPassword: Joi.string() 68 | .required() 69 | .valid(Joi.ref("newPassword")) 70 | }); 71 | 72 | export const refreshTokenSchema = Joi.object({ 73 | refreshToken: Joi.string().optional() 74 | }); 75 | 76 | export const getProfileSchema = Joi.object({ 77 | access_token: Joi.string().optional(), 78 | id_token: Joi.when("access_token", { 79 | is: null, 80 | then: Joi.string().required(), 81 | otherwise: Joi.string() 82 | }) 83 | }).or("access_token", "id_token"); 84 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/configs/config.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import configService from "./config.service"; 3 | import { handleResponse } from "../../helpers"; 4 | 5 | class ConfigController extends Controller { 6 | constructor(service, name) { 7 | super(service, name); 8 | 9 | this.listConfigsForApp = this.listConfigsForApp.bind(this); 10 | } 11 | 12 | async listConfigsForApp(req, res) { 13 | let data = await this.service.listForApp(); 14 | 15 | return handleResponse.success(res, data); 16 | } 17 | } 18 | 19 | export default new ConfigController(configService, "Configs"); 20 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/configs/config.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | 4 | const ConfigSchema = new Schema( 5 | { 6 | name: { 7 | type: String, 8 | required: true 9 | }, 10 | value: { 11 | type: String 12 | } 13 | }, 14 | { timestamps: true } 15 | ); 16 | 17 | ConfigSchema.plugin(mongoosePaginate); 18 | 19 | export default mongoose.model("Config", ConfigSchema); 20 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/configs/config.service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import Configs from "./config.model"; 3 | import { toNumber } from "../../helpers/utils"; 4 | 5 | class ConfigService extends Service { 6 | constructor(model) { 7 | super(model); 8 | } 9 | 10 | async listForApp() { 11 | let data = {}; 12 | let listConfigs = await Configs.find(); 13 | for (let i = 0; i < listConfigs.length; i++) { 14 | if ( 15 | listConfigs[i].name === "managesubscriptionlink" && 16 | (listConfigs[i].value == 0 || listConfigs[i].value == "") 17 | ) { 18 | listConfigs[i].value = null; 19 | } 20 | data = { ...data, [listConfigs[i].name]: toNumber(listConfigs[i].value) }; 21 | } 22 | 23 | return data; 24 | } 25 | } 26 | export default new ConfigService(Configs); 27 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/configs/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import configController from "./config.controller"; 3 | import AuthService from "../../middlewares/auth"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post( 8 | "/", 9 | AuthService.required, 10 | AuthService.isAdmin(), 11 | configController.create 12 | ); 13 | router.put( 14 | "/:id", 15 | AuthService.required, 16 | AuthService.isAdmin(), 17 | configController.update 18 | ); 19 | router.get( 20 | "/", 21 | AuthService.required, 22 | AuthService.isAdmin(), 23 | configController.findAll 24 | ); 25 | router.get("/all", configController.listConfigsForApp); 26 | router.get( 27 | "/:id", 28 | AuthService.required, 29 | AuthService.isAdmin(), 30 | configController.findOne 31 | ); 32 | router.delete( 33 | "/:id", 34 | AuthService.required, 35 | AuthService.isAdmin(), 36 | configController.remove 37 | ); 38 | 39 | export default router; 40 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/deviceTokens/deviceToken.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import deviceTokenService from "./deviceToken.service"; 3 | import { handleResponse as Response, utils, pushToken } from "../../helpers"; 4 | 5 | class DeviceTokenController extends Controller { 6 | constructor(service, name) { 7 | super(service, name); 8 | } 9 | 10 | async create(req, res, next) { 11 | try { 12 | const data = req.body; 13 | const deviceId = utils.getDeviceId(req); 14 | const user = req.user; 15 | const result = await this.service.createOrUpdate(deviceId, { 16 | ...data, 17 | deviceId, 18 | user 19 | }); 20 | return Response.success(res, result); 21 | } catch (exception) { 22 | next(exception); 23 | } 24 | } 25 | 26 | async sendNotification(req, res, next) { 27 | try { 28 | const data = req.body; 29 | const result = await pushToken.sendNotificationToDeviceToken(data); 30 | return Response.success(res, result); 31 | } catch (exception) { 32 | next(exception); 33 | } 34 | } 35 | async sendNotificationByDeviceId(req, res, next) { 36 | try { 37 | const data = req.body; 38 | const result = await pushToken.sendNotificationToDeviceId(data); 39 | return Response.success(res, result); 40 | } catch (exception) { 41 | next(exception); 42 | } 43 | } 44 | } 45 | 46 | export default new DeviceTokenController(deviceTokenService, "DeviceToken"); 47 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/deviceTokens/deviceToken.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import mongooseUniqueValidator from "mongoose-unique-validator"; 4 | 5 | const PLATFORM = { 6 | IOS: "iOS", 7 | ANDROID: "android" 8 | }; 9 | 10 | const DeviceTokenSchema = new Schema( 11 | { 12 | name: { 13 | type: String, 14 | required: false 15 | }, 16 | user: { 17 | type: Schema.Types.ObjectId, 18 | ref: "User", 19 | required: false 20 | }, 21 | deviceId: { 22 | type: String, 23 | required: false 24 | }, 25 | platform: { 26 | type: String, 27 | enum: Object.values(PLATFORM), 28 | default: PLATFORM.IOS, 29 | required: true 30 | }, 31 | token: { 32 | type: String, 33 | required: true 34 | } 35 | }, 36 | { timestamps: true } 37 | ); 38 | 39 | DeviceTokenSchema.plugin(mongoosePaginate); 40 | DeviceTokenSchema.plugin(mongooseUniqueValidator); 41 | 42 | const DeviceToken = mongoose.model("DeviceToken", DeviceTokenSchema); 43 | export default DeviceToken; 44 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/deviceTokens/deviceToken.service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import DeviceToken from "./deviceToken.model"; 3 | 4 | class DeviceTokenService extends Service { 5 | constructor() { 6 | super(DeviceToken); 7 | this.createOrUpdate = this.createOrUpdate.bind(this); 8 | } 9 | 10 | async createOrUpdate(deviceId, update) { 11 | const result = await this._model.findOneAndUpdate({ deviceId }, update, { 12 | new: true, 13 | upsert: true 14 | }); 15 | return result; 16 | } 17 | } 18 | 19 | export default new DeviceTokenService(); 20 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/deviceTokens/deviceToken.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | 5 | const api = supertest(app); 6 | 7 | describe("Basic Mocha String Test", function() { 8 | it("should return number of characters in a string", function() { 9 | assert.equal("Hello".length, 5); 10 | }); 11 | it("should return first character of the string", function() { 12 | assert.equal("Hello".charAt(0), "H"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/deviceTokens/deviceToken.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema, ObjectId } = schemas; 5 | 6 | export const headerValidationSchema = schemas.headerValidationSchema; 7 | 8 | export const customPaginateValidateSchema = paginateValidationSchema.keys(); 9 | 10 | export const createValidationSchema = Joi.object({ 11 | platform: Joi.string().valid("iOS", "android"), 12 | token: Joi.string().required() 13 | }); 14 | 15 | export const updateValidationSchema = Joi.object({ 16 | platform: Joi.string().optional(), 17 | token: Joi.string().optional() 18 | }).unknown(true); 19 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/iaps/iap.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import iapService from "./iap.service"; 3 | import { PURCHASE_TYPE } from "./iap.model"; 4 | import { handleResponse as Response, utils } from "../../helpers"; 5 | 6 | class IapController extends Controller { 7 | constructor(service, name) { 8 | super(service, name); 9 | 10 | this.verifyIosInAppReceipt = this.verifyIosInAppReceipt.bind(this); 11 | this.verifyIosSubscriptionReceipt = this.verifyIosSubscriptionReceipt.bind( 12 | this 13 | ); 14 | this.verifyAndroidInAppReceipt = this.verifyAndroidInAppReceipt.bind(this); 15 | this.verifyAndroidSubReceipt = this.verifyAndroidSubReceipt.bind(this); 16 | this.checkIapModel = this.checkIapModel.bind(this); 17 | this.handleIOSWebhook = this.handleIOSWebhook.bind(this); 18 | this.handleAndroidWebhook = this.handleAndroidWebhook.bind(this); 19 | } 20 | 21 | async verifyIosInAppReceipt(req, res, next) { 22 | try { 23 | const data = req.body; 24 | let userId = null; 25 | const user = req.user; 26 | if (user) { 27 | userId = user._id; 28 | } 29 | const deviceId = utils.getDeviceId(req); 30 | const result = await this.service.verifyIOSReceipt( 31 | data, 32 | userId, 33 | deviceId, 34 | PURCHASE_TYPE.LIFETIME 35 | ); 36 | return Response.success(res, result); 37 | } catch (exception) { 38 | next(exception); 39 | } 40 | } 41 | 42 | async verifyIosSubscriptionReceipt(req, res, next) { 43 | try { 44 | const data = req.body; 45 | let userId = null; 46 | const user = req.user; 47 | if (user) { 48 | userId = user._id; 49 | } 50 | const deviceId = utils.getDeviceId(req); 51 | const result = await this.service.verifyIOSReceipt( 52 | data, 53 | userId, 54 | deviceId, 55 | PURCHASE_TYPE.SUBSCRIPTION 56 | ); 57 | return Response.success(res, result); 58 | } catch (exception) { 59 | next(exception); 60 | } 61 | } 62 | 63 | async verifyAndroidInAppReceipt(req, res, next) { 64 | try { 65 | const data = req.body; 66 | let userId = null; 67 | const user = req.user; 68 | if (user) { 69 | userId = user._id; 70 | } 71 | const deviceId = utils.getDeviceId(req); 72 | const result = await this.service.verifyAndroidInAppReceipt( 73 | data, 74 | userId, 75 | deviceId 76 | ); 77 | return Response.success(res, result); 78 | } catch (exception) { 79 | next(exception); 80 | } 81 | } 82 | 83 | async verifyAndroidSubReceipt(req, res, next) { 84 | try { 85 | const data = req.body; 86 | let userId = null; 87 | const user = req.user; 88 | if (user) { 89 | userId = user._id; 90 | } 91 | const deviceId = utils.getDeviceId(req); 92 | const result = await this.service.verifyAndroidSubReceipt( 93 | data, 94 | userId, 95 | deviceId 96 | ); 97 | return Response.success(res, result); 98 | } catch (exception) { 99 | next(exception); 100 | } 101 | } 102 | 103 | async checkIapModel(req, res, next) { 104 | try { 105 | // const user = req.user; 106 | const deviceId = utils.getDeviceId(req); 107 | const result = await this.service.checkIapModel(deviceId); 108 | return Response.success(res, { iap: result }); 109 | } catch (exception) { 110 | next(exception); 111 | } 112 | } 113 | 114 | async handleIOSWebhook(req, res, next) { 115 | try { 116 | const data = req.body; 117 | const result = await this.service.handleIOSWebhook(data); 118 | return Response.success(res, result); 119 | } catch (exception) { 120 | next(exception); 121 | } 122 | } 123 | 124 | async handleAndroidWebhook(req, res, next) { 125 | try { 126 | const data = req.body; 127 | const result = await this.service.handleAndroidWebhook(data); 128 | return Response.success(res, result); 129 | } catch (exception) { 130 | next(exception); 131 | } 132 | } 133 | } 134 | 135 | export default new IapController(iapService, "Iap"); 136 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/iaps/iap.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import Packages, { PACKAGE_TYPE } from "../packages/packages.model"; 4 | import Users from "../users/users.model"; 5 | 6 | export const PLATFORM_TYPE = { 7 | ANDROID: "ANDROID", 8 | IOS: "IOS" 9 | }; 10 | 11 | export const PURCHASE_TYPE = { 12 | LIFETIME: "LIFETIME", 13 | SUBSCRIPTION: "SUBSCRIPTION", 14 | PREPAID: "PREPAID" 15 | }; 16 | 17 | export const STATUS_TYPE = { 18 | NEW: "new", 19 | USED: "used" 20 | }; 21 | 22 | const IAPSchema = new Schema( 23 | { 24 | user: { 25 | type: Schema.Types.ObjectId, 26 | ref: "User", 27 | required: false 28 | }, 29 | deviceId: { 30 | type: String, 31 | required: false 32 | }, 33 | productId: { 34 | type: String, 35 | required: true 36 | }, 37 | bundleId: { 38 | type: String 39 | }, 40 | subscriptionGroupIdentifier: { 41 | type: String, 42 | required: true 43 | }, 44 | environment: { 45 | type: String 46 | }, 47 | originalTransactionId: { 48 | type: String 49 | }, 50 | webOrderLineItemId: { 51 | type: String 52 | }, 53 | transactionIds: { 54 | type: [String] 55 | }, 56 | purchaseDate: { 57 | type: Date 58 | }, 59 | originalPurchaseDate: { 60 | type: Date 61 | }, 62 | expiresDate: { 63 | type: Date 64 | }, 65 | quantity: { 66 | type: Number 67 | }, 68 | type: { 69 | type: String 70 | }, 71 | inAppOwnershipType: { 72 | type: String 73 | }, 74 | signedDate: { 75 | type: Date 76 | }, 77 | transactionReason: { 78 | type: String 79 | }, 80 | storefront: { 81 | type: String 82 | }, 83 | storefrontId: { 84 | type: String 85 | }, 86 | price: { 87 | type: Number 88 | }, 89 | currency: { 90 | type: String 91 | }, 92 | latestReceipt: { 93 | type: String 94 | }, 95 | purchaseType: { 96 | type: String, 97 | enum: Object.values(PURCHASE_TYPE), 98 | default: PURCHASE_TYPE.PREPAID 99 | }, 100 | platform: { 101 | type: String, 102 | enum: Object.values(PLATFORM_TYPE), 103 | default: PLATFORM_TYPE.IOS 104 | }, 105 | retryNumber: { 106 | type: Number, 107 | default: 0 108 | }, 109 | status: { 110 | type: String, 111 | enum: Object.values(STATUS_TYPE), 112 | default: STATUS_TYPE.NEW 113 | }, 114 | reason: { 115 | type: String 116 | } 117 | }, 118 | { 119 | timestamps: true 120 | } 121 | ); 122 | 123 | IAPSchema.plugin(mongoosePaginate); 124 | 125 | export default mongoose.model("IAP", IAPSchema); 126 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/iaps/iap.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | 5 | const api = supertest(app); 6 | 7 | describe("Basic Mocha String Test", function() { 8 | it("should return number of characters in a string", function() { 9 | assert.equal("Hello".length, 5); 10 | }); 11 | it("should return first character of the string", function() { 12 | assert.equal("Hello".charAt(0), "H"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/iaps/iap.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema } = schemas; 5 | 6 | export const headerValidationSchema = schemas.headerValidationSchema; 7 | 8 | export const customPaginateValidateSchema = paginateValidationSchema.keys(); 9 | 10 | export const createValidationSchema = Joi.object({ 11 | field: Joi.string().optional(), 12 | field2: Joi.string().required() 13 | }); 14 | 15 | export const iosIapReceiptValidationSchema = Joi.object({ 16 | receiptData: Joi.string().required(), 17 | password: Joi.string().optional() 18 | }); 19 | 20 | export const androidIapReceiptValidationSchema = Joi.object({ 21 | packageName: Joi.string().required(), 22 | productId: Joi.string().required(), 23 | purchaseToken: Joi.string().required() 24 | }); 25 | 26 | export const updateValidationSchema = Joi.object({ 27 | field: Joi.string().optional(), 28 | field2: Joi.string().required() 29 | }).unknown(true); 30 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/iaps/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { celebrate } from "celebrate"; 3 | import AuthService from "../../middlewares/auth"; 4 | import iapController from "./iap.controller"; 5 | import * as iapValidation from "./iap.validation"; 6 | 7 | const router = express.Router(); 8 | 9 | router.post( 10 | "/in-app/ios", 11 | [ 12 | AuthService.optional, 13 | celebrate({ 14 | headers: iapValidation.headerValidationSchema, 15 | body: iapValidation.iosIapReceiptValidationSchema 16 | }) 17 | ], 18 | iapController.verifyIosInAppReceipt 19 | ); 20 | 21 | router.post( 22 | "/in-app/android", 23 | [ 24 | AuthService.optional, 25 | celebrate({ 26 | headers: iapValidation.headerValidationSchema, 27 | body: iapValidation.androidIapReceiptValidationSchema 28 | }) 29 | ], 30 | iapController.verifyAndroidInAppReceipt 31 | ); 32 | 33 | router.post( 34 | "/in-app/check", 35 | [ 36 | AuthService.optional, 37 | celebrate({ 38 | headers: iapValidation.headerValidationSchema 39 | }) 40 | ], 41 | iapController.checkIapModel 42 | ); 43 | 44 | router.post( 45 | "/subs/ios", 46 | AuthService.optional, 47 | iapController.verifyIosSubscriptionReceipt 48 | ); 49 | 50 | router.post( 51 | "/subs/android", 52 | AuthService.optional, 53 | iapController.verifyAndroidSubReceipt 54 | ); 55 | 56 | router.post( 57 | "/webhook/ios", 58 | AuthService.optional, 59 | iapController.handleIOSWebhook 60 | ); 61 | 62 | router.post( 63 | "/webhook/android", 64 | AuthService.optional, 65 | iapController.handleAndroidWebhook 66 | ); 67 | 68 | router.post( 69 | "/", 70 | [AuthService.required, AuthService.isAdmin()], 71 | iapController.create 72 | ); 73 | 74 | router.get( 75 | "/", 76 | [AuthService.required, AuthService.isAdmin()], 77 | iapController.findAll 78 | ); 79 | 80 | router.get("/:id", iapController.findOne); 81 | 82 | router.put( 83 | "/:id", 84 | [AuthService.required, AuthService.isAdmin()], 85 | iapController.update 86 | ); 87 | 88 | router.delete( 89 | "/:id", 90 | AuthService.required, 91 | AuthService.isAdmin(), 92 | iapController.remove 93 | ); 94 | 95 | export default router; 96 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/inAppPurchaseNotifications/inAppPurchaseNotification.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import inAppPurchaseNotificationService from "./inAppPurchaseNotification.service"; 3 | import { handleResponse as Response } from "../../helpers"; 4 | 5 | class InAppPurchaseNotificationController extends Controller { 6 | constructor(service, name) { 7 | super(service, name); 8 | } 9 | } 10 | 11 | export default new InAppPurchaseNotificationController( 12 | inAppPurchaseNotificationService, 13 | "InAppPurchaseNotification" 14 | ); 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/inAppPurchaseNotifications/inAppPurchaseNotification.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import mongooseUniqueValidator from "mongoose-unique-validator"; 4 | 5 | const InAppPurchaseNotificationSchema = new Schema( 6 | { 7 | notificationType: { 8 | type: String, 9 | required: false 10 | }, 11 | subtype: { 12 | type: String, 13 | required: false 14 | }, 15 | data: { 16 | type: Object, 17 | required: false 18 | }, 19 | summary: { 20 | type: Object, 21 | required: false 22 | }, 23 | externalPurchaseToken: { 24 | type: Object, 25 | required: false 26 | }, 27 | version: { 28 | type: String, 29 | required: false 30 | }, 31 | signedDate: { 32 | type: Date, 33 | required: false 34 | }, 35 | notificationUUID: { 36 | type: String, 37 | required: false 38 | }, 39 | platform: { 40 | type: String, 41 | required: false 42 | } 43 | }, 44 | { timestamps: true } 45 | ); 46 | 47 | InAppPurchaseNotificationSchema.plugin(mongoosePaginate); 48 | InAppPurchaseNotificationSchema.plugin(mongooseUniqueValidator); 49 | 50 | const InAppPurchaseNotification = mongoose.model( 51 | "InAppPurchaseNotification", 52 | InAppPurchaseNotificationSchema 53 | ); 54 | export default InAppPurchaseNotification; 55 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/inAppPurchaseNotifications/inAppPurchaseNotification.service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import InAppPurchaseNotification from "./inAppPurchaseNotification.model"; 3 | 4 | class InAppPurchaseNotificationService extends Service { 5 | constructor() { 6 | super(InAppPurchaseNotification); 7 | } 8 | } 9 | 10 | export default new InAppPurchaseNotificationService(); 11 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/inAppPurchaseNotifications/inAppPurchaseNotification.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | 5 | const api = supertest(app); 6 | 7 | describe("Basic Mocha String Test", function() { 8 | it("should return number of characters in a string", function() { 9 | assert.equal("Hello".length, 5); 10 | }); 11 | it("should return first character of the string", function() { 12 | assert.equal("Hello".charAt(0), "H"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/inAppPurchaseNotifications/inAppPurchaseNotification.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema, ObjectId } = schemas; 5 | 6 | export const customPaginateValidateSchema = paginateValidationSchema.keys(); 7 | 8 | export const createValidationSchema = Joi.object({ 9 | field: Joi.string().required(), 10 | field2: Joi.string().required() 11 | }); 12 | 13 | export const updateValidationSchema = Joi.object({ 14 | field: Joi.string().optional(), 15 | field2: Joi.string().optional() 16 | }).unknown(true); 17 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import auth from "./auth"; 3 | import users from "./users"; 4 | import uploads from "./uploads"; 5 | import configs from "./configs"; 6 | import iaps from "./iaps"; 7 | import deviceTokens from "./deviceTokens"; 8 | import inAppPurchaseNotifications from "./inAppPurchaseNotifications"; 9 | 10 | const router = new Router(); 11 | 12 | router.use("/auth", auth); 13 | router.use("/users", users); 14 | router.use("/uploads", uploads); 15 | router.use("/configs", configs); 16 | router.use("/iaps", iaps); 17 | router.use("/device-tokens", deviceTokens); 18 | router.use("/inAppPurchaseNotifications", inAppPurchaseNotifications); 19 | 20 | export default router; 21 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/packages/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import AuthService from "../../middlewares/auth"; 3 | import PackageCtrl from "./packages.controller"; 4 | 5 | const router = express.Router(); 6 | 7 | router.post( 8 | "/", 9 | AuthService.required, 10 | AuthService.isAdmin(), 11 | PackageCtrl.create 12 | ); 13 | 14 | router.get("/", PackageCtrl.findAll); 15 | router.get("/:id", PackageCtrl.findOne); 16 | 17 | router.put( 18 | "/:id", 19 | AuthService.required, 20 | AuthService.isAdmin(), 21 | PackageCtrl.update 22 | ); 23 | 24 | router.delete( 25 | "/:id", 26 | AuthService.required, 27 | AuthService.isAdmin(), 28 | PackageCtrl.remove 29 | ); 30 | 31 | export default router; 32 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/packages/packages.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import packagesService from "./packages.service"; 3 | import { handleResponse as Response } from "../../helpers"; 4 | 5 | class PackagesController extends Controller { 6 | constructor(service, name) { 7 | super(service, name); 8 | } 9 | } 10 | 11 | export default new PackagesController(packagesService, "Packages"); 12 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/packages/packages.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import mongooseUniqueValidator from "mongoose-unique-validator"; 4 | 5 | export const PACKAGE_TYPE = { 6 | LIFETIME: "LIFETIME", 7 | SUBSCRIPTION: "SUBSCRIPTION", 8 | PREPAID: "PREPAID" 9 | }; 10 | 11 | const UNIT_TYPE = { 12 | DAY: "day", 13 | YEAR: "month" 14 | }; 15 | 16 | const PackagesSchema = new Schema( 17 | { 18 | name: { 19 | type: String, 20 | required: false 21 | }, 22 | product_id: { 23 | type: String, 24 | unique: true, 25 | required: true 26 | }, 27 | credit: { 28 | type: Number, 29 | default: 0 30 | }, 31 | package_type: { 32 | type: String, 33 | enum: Object.values(PACKAGE_TYPE), 34 | default: PACKAGE_TYPE.PREPAID 35 | }, 36 | duration: { 37 | type: Number 38 | }, 39 | unit: { 40 | type: String, 41 | enum: Object.values(UNIT_TYPE) 42 | } 43 | }, 44 | { 45 | timestamps: true 46 | } 47 | ); 48 | 49 | PackagesSchema.plugin(mongoosePaginate); 50 | PackagesSchema.plugin(mongooseUniqueValidator); 51 | 52 | const Packages = mongoose.model("Packages", PackagesSchema); 53 | export default Packages; 54 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/packages/packages.service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import Packages from "./packages.model"; 3 | 4 | class PackagesService extends Service { 5 | constructor() { 6 | super(Packages); 7 | } 8 | } 9 | 10 | export default new PackagesService(); 11 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/packages/packages.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | 5 | const api = supertest(app); 6 | 7 | describe("Basic Mocha String Test", function() { 8 | it("should return number of charachters in a string", function() { 9 | assert.equal("Hello".length, 5); 10 | }); 11 | it("should return first charachter of the string", function() { 12 | assert.equal("Hello".charAt(0), "H"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/packages/packages.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema, ObjectId } = schemas; 5 | 6 | export const customPaginateValidateSchema = paginateValidationSchema.keys(); 7 | 8 | export const createValidationSchema = Joi.object({ 9 | field: Joi.string().optional(), 10 | field2: Joi.string().required() 11 | }); 12 | 13 | export const updateValidationSchema = Joi.object({ 14 | field: Joi.string().optional(), 15 | field2: Joi.string().required() 16 | }).unknown(true); 17 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/refreshTokens/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { celebrate } from "celebrate"; 3 | import refreshTokenController from "./refreshToken.controller"; 4 | import AuthService from "../../middlewares/auth"; 5 | import { 6 | createValidationSchema, 7 | updateValidationSchema, 8 | customPaginateValidateSchema 9 | } from "./refreshToken.validation"; 10 | 11 | const router = express.Router(); 12 | /** 13 | * @swagger 14 | * 15 | * definitions: 16 | * RefreshToken: 17 | * type: object 18 | * required: 19 | * - field1 20 | * - field2 21 | * properties: 22 | * field1: 23 | * type: string 24 | * field2: 25 | * type: string 26 | * 27 | * ArrayOfRefreshTokens: 28 | * type: array 29 | * items: 30 | * $ref: '#/definitions/RefreshToken' 31 | */ 32 | 33 | /** 34 | * @swagger 35 | * 36 | * /refreshTokens: 37 | * post: 38 | * tags: [refreshTokens] 39 | * description: create a refreshToken 40 | * security: 41 | * - BearerAuth: [] 42 | * produces: 43 | * - application/json 44 | * parameters: 45 | * - name: data 46 | * in: body 47 | * required: true 48 | * schema: 49 | * $ref: '#/definitions/RefreshToken' 50 | * 51 | * responses: 52 | * 200: 53 | * description: OK 54 | * schema: 55 | * $ref: '#/definitions/RefreshToken' 56 | * 400: 57 | * $ref: '#/responses/Error' 58 | * 401: 59 | * $ref: '#/responses/Unauthorized' 60 | */ 61 | 62 | router.post( 63 | "/", 64 | [AuthService.required, celebrate({ body: createValidationSchema })], 65 | refreshTokenController.create 66 | ); 67 | 68 | /** 69 | * @swagger 70 | * 71 | * /refreshTokens: 72 | * put: 73 | * tags: [refreshTokens] 74 | * description: create a refreshToken 75 | * security: 76 | * - BearerAuth: [] 77 | * produces: 78 | * - application/json 79 | * parameters: 80 | * - name: data 81 | * in: body 82 | * required: true 83 | * schema: 84 | * $ref: '#/definitions/RefreshToken' 85 | * 86 | * responses: 87 | * 200: 88 | * description: OK 89 | * schema: 90 | * $ref: '#/definitions/RefreshToken' 91 | * 400: 92 | * $ref: '#/responses/Error' 93 | * 401: 94 | * $ref: '#/responses/Unauthorized' 95 | */ 96 | 97 | router.put( 98 | "/:id", 99 | [AuthService.required], 100 | celebrate({ body: updateValidationSchema }), 101 | refreshTokenController.update 102 | ); 103 | 104 | /** 105 | * @swagger 106 | * 107 | * /refreshTokens: 108 | * get: 109 | * tags: [refreshTokens] 110 | * description: get all refreshTokens 111 | * produces: 112 | * - application/json 113 | * parameters: 114 | * - $ref: '#/parameters/pageParam' 115 | * - $ref: '#/parameters/limitParam' 116 | * responses: 117 | * 200: 118 | * description: OK 119 | * schema: 120 | * type: object 121 | * properties: 122 | * page: 123 | * type: integer 124 | * format: int32 125 | * pages: 126 | * type: integer 127 | * format: int32 128 | * limit: 129 | * type: integer 130 | * format: int32 131 | * total: 132 | * type: integer 133 | * format: int32 134 | * data: 135 | * $ref: '#/definitions/ArrayOfRefreshTokens' 136 | * 401: 137 | * $ref: '#/responses/Unauthorized' 138 | */ 139 | router.get( 140 | "/", 141 | AuthService.optional, 142 | celebrate({ query: customPaginateValidateSchema }), 143 | refreshTokenController.findAll 144 | ); 145 | 146 | /** 147 | * @swagger 148 | * 149 | * /refreshTokens/{id}: 150 | * get: 151 | * tags: [refreshTokens] 152 | * description: get detail refreshToken 153 | * produces: 154 | * - application/json 155 | * parameters: 156 | * - name: id 157 | * in: path 158 | * description: refreshToken id 159 | * required: true 160 | * type: string 161 | * responses: 162 | * 200: 163 | * description: OK 164 | * schema: 165 | * $ref: '#/definitions/RefreshToken' 166 | * 400: 167 | * $ref: '#/responses/Error' 168 | * 401: 169 | * $ref: '#/responses/Unauthorized' 170 | */ 171 | router.get("/:id", refreshTokenController.findOne); 172 | 173 | /** 174 | * @swagger 175 | * 176 | * /refreshTokens/{id}: 177 | * delete: 178 | * tags: [refreshTokens] 179 | * description: delete a refreshToken 180 | * security: 181 | * - BearerAuth: [] 182 | * produces: 183 | * - application/json 184 | * parameters: 185 | * - name: id 186 | * in: path 187 | * description: refreshTokens id 188 | * required: true 189 | * type: string 190 | * responses: 191 | * 200: 192 | * description: OK 193 | * schema: 194 | * $ref: '#/definitions/RefreshToken' 195 | * 400: 196 | * $ref: '#/responses/Error' 197 | * 401: 198 | * $ref: '#/responses/Unauthorized' 199 | */ 200 | router.delete("/:id", AuthService.required, refreshTokenController.remove); 201 | 202 | export default router; 203 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/refreshTokens/refreshToken.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import refreshTokenService from "./refreshToken.service"; 3 | import { handleResponse as Response } from "../../helpers"; 4 | 5 | class RefreshTokenController extends Controller { 6 | constructor(service, name) { 7 | super(service, name); 8 | } 9 | } 10 | 11 | export default new RefreshTokenController(refreshTokenService, "RefreshToken"); 12 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/refreshTokens/refreshToken.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import mongooseUniqueValidator from "mongoose-unique-validator"; 4 | 5 | const RefreshTokenSchema = new Schema( 6 | { 7 | user: { 8 | type: Schema.Types.ObjectId, 9 | ref: "User", 10 | required: true 11 | }, 12 | token: { 13 | type: String, 14 | required: true 15 | }, 16 | expires: { 17 | type: Date 18 | }, 19 | createdByIp: { 20 | type: String 21 | }, 22 | revoked: { 23 | type: Date 24 | }, 25 | revokedByIp: { 26 | type: String 27 | }, 28 | replacedByToken: { 29 | type: String 30 | } 31 | }, 32 | { timestamps: true } 33 | ); 34 | 35 | RefreshTokenSchema.virtual("isExpired").get(function() { 36 | return Date.now() >= this.expires; 37 | }); 38 | 39 | RefreshTokenSchema.virtual("isActive").get(function() { 40 | return !this.revoked && !this.isExpired; 41 | }); 42 | 43 | RefreshTokenSchema.plugin(mongoosePaginate); 44 | RefreshTokenSchema.plugin(mongooseUniqueValidator); 45 | 46 | const RefreshToken = mongoose.model("RefreshToken", RefreshTokenSchema); 47 | export default RefreshToken; 48 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/refreshTokens/refreshToken.service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import RefreshToken from "./refreshToken.model"; 3 | 4 | class RefreshTokenService extends Service { 5 | constructor() { 6 | super(RefreshToken); 7 | } 8 | } 9 | 10 | export default new RefreshTokenService(); 11 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/refreshTokens/refreshToken.test.js: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import supertest from "supertest"; 3 | import app from "../../app"; 4 | 5 | const api = supertest(app); 6 | 7 | describe("Basic Mocha String Test", function() { 8 | it("should return number of characters in a string", function() { 9 | assert.equal("Hello".length, 5); 10 | }); 11 | it("should return first charachter of the string", function() { 12 | assert.equal("Hello".charAt(0), "H"); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/refreshTokens/refreshToken.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema, ObjectId } = schemas; 5 | 6 | export const customPaginateValidateSchema = paginateValidationSchema.keys(); 7 | 8 | export const createValidationSchema = Joi.object({ 9 | field: Joi.string().optional(), 10 | field2: Joi.string().required() 11 | }); 12 | 13 | export const updateValidationSchema = Joi.object({ 14 | field: Joi.string().optional(), 15 | field2: Joi.string().required() 16 | }).unknown(true); 17 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/uploads/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import AuthService from "../../middlewares/auth"; 3 | import { imageUpload, imagesUpload } from "../../services/storage"; 4 | import { upload, deleteFile, multiUpload } from "./upload.controller"; 5 | 6 | const router = express.Router(); 7 | 8 | /** 9 | * @swagger 10 | * 11 | * /uploads: 12 | * post: 13 | * tags: [uploads] 14 | * description: upload a file. Fill url will be http://server/media/filename 15 | * consumes: 16 | * - multipart/form-data 17 | * parameters: 18 | * - in: formData 19 | * name: image 20 | * type: file 21 | * description: The file to upload. 22 | * responses: 23 | * 200: 24 | * description: OK 25 | * schema: 26 | * type: object 27 | * properties: 28 | * data: 29 | * type: object 30 | * properties: 31 | * url: 32 | * type: string 33 | * thumbnail: 34 | * type: string 35 | * 36 | * 400: 37 | * $ref: '#/responses/Error' 38 | */ 39 | router.post("/", imageUpload, upload); 40 | /** 41 | * @swagger 42 | * 43 | * /uploads/multi: 44 | * post: 45 | * tags: [uploads] 46 | * description: upload multi file 47 | * consumes: 48 | * - multipart/form-data 49 | * parameters: 50 | * - in: formData 51 | * name: photos 52 | * type: file 53 | * description: The file to upload. 54 | * responses: 55 | * 200: 56 | * description: OK 57 | * schema: 58 | * type: object 59 | * properties: 60 | * data: 61 | * type: array 62 | * items: 63 | * type: object 64 | * properties: 65 | * url: 66 | * type: string 67 | * thumbnail: 68 | * type: string 69 | * 400: 70 | * $ref: '#/responses/Error' 71 | */ 72 | router.post("/multi", imagesUpload, multiUpload); 73 | /** 74 | * @swagger 75 | * 76 | * /uploads/{filename}: 77 | * delete: 78 | * tags: [uploads] 79 | * description: upload a file 80 | * parameters: 81 | * - name: filename 82 | * in: path 83 | * description: file name 84 | * required: true 85 | * type: string 86 | * responses: 87 | * 200: 88 | * description: OK 89 | * schema: 90 | * type: object 91 | * properties: 92 | * message: 93 | * type: string 94 | * 400: 95 | * $ref: '#/responses/Error' 96 | */ 97 | router.delete("/:filename", AuthService.optional, deleteFile); 98 | 99 | export default router; 100 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/uploads/upload.controller.js: -------------------------------------------------------------------------------- 1 | import httpStatus from "http-status"; 2 | import axios from "axios"; 3 | import Response from "../../helpers/response"; 4 | import { 5 | removeFile, 6 | deleteS3Object, 7 | resizeImageS3, 8 | resize 9 | } from "./upload.service"; 10 | 11 | export const upload = (req, res) => { 12 | let image = ""; 13 | let originalname = ""; 14 | 15 | if (req.file) { 16 | image = req.file.filename; 17 | originalname = req.file.originalname; 18 | } 19 | 20 | Response.success( 21 | res, 22 | { 23 | url: `/media/${image}`, 24 | title: originalname 25 | }, 26 | httpStatus.CREATED 27 | ); 28 | }; 29 | 30 | export const multiUpload = (req, res) => { 31 | const images = req.files.map(file => { 32 | return { 33 | url: `/media/${file.filename}`, 34 | title: file.originalname 35 | }; 36 | }); 37 | Response.success(res, images, httpStatus.CREATED); 38 | }; 39 | 40 | export const uploadS3 = (req, res, next) => { 41 | try { 42 | let image = ""; 43 | let originalname = ""; 44 | let key = ""; 45 | 46 | if (req.file) { 47 | console.log(req.file); 48 | image = req.file.location; 49 | originalname = req.file.originalname; 50 | key = req.file.key; 51 | } 52 | 53 | return Response.success( 54 | res, 55 | { 56 | url: image, 57 | title: originalname, 58 | key 59 | }, 60 | httpStatus.CREATED 61 | ); 62 | } catch (exception) { 63 | next(exception); 64 | } 65 | }; 66 | 67 | export const multiUploadS3 = (req, res, next) => { 68 | try { 69 | const images = req.files.map(file => { 70 | return { 71 | url: file.location, 72 | title: file.originalname 73 | }; 74 | }); 75 | Response.success(res, images, httpStatus.CREATED); 76 | } catch (exception) { 77 | next(exception); 78 | } 79 | }; 80 | 81 | export const resizeImage = async (req, res) => { 82 | const { imageUrl, width, height } = req.body; 83 | try { 84 | const result = await resizeImageS3(imageUrl, width, height); 85 | return Response.success(res, result); 86 | } catch (e) { 87 | return Response.error(res, e); 88 | } 89 | }; 90 | 91 | export const deleteFile = async (req, res) => { 92 | const fileName = req.params.filename; 93 | try { 94 | await removeFile(fileName); 95 | return Response.success(res, { message: "File was deleted successfully!" }); 96 | } catch (e) { 97 | return Response.error(res, e); 98 | } 99 | }; 100 | 101 | export const deleteS3File = async (req, res) => { 102 | const fileName = req.params.filename; 103 | try { 104 | await deleteS3Object(fileName); 105 | return Response.success(res, { message: "File was deleted successfully!" }); 106 | } catch (e) { 107 | return Response.error(res, e); 108 | } 109 | }; 110 | 111 | export const resizeImageStream = async (req, res, next) => { 112 | try { 113 | // Extract the query-parameter 114 | const widthString = req.query.width; 115 | const heightString = req.query.height; 116 | const format = req.query.format; 117 | const url = req.query.url; 118 | 119 | // Parse to integer if possible 120 | let width, height; 121 | if (widthString) { 122 | width = parseInt(widthString); 123 | } 124 | if (heightString) { 125 | height = parseInt(heightString); 126 | } 127 | // Set the content-type of the response 128 | res.type(`image/${format || "png"}`); 129 | const readStream = await axios({ url: url, responseType: "stream" }); 130 | 131 | // Get the resized image 132 | resize(readStream, format, width, height).pipe(res); 133 | } catch (exception) { 134 | next(exception); 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/uploads/upload.service.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import { promisify } from "util"; 4 | import sharp from "sharp"; 5 | 6 | import { 7 | deleteObject, 8 | resizeImage, 9 | uploadObject 10 | } from "../../services/storage"; 11 | 12 | export const removeFile = async fileName => { 13 | const unlink = promisify(fs.unlink); 14 | const result = await unlink( 15 | path.join(__dirname, "../../../uploads", fileName) 16 | ); 17 | return result; 18 | }; 19 | 20 | export const deleteS3Object = async key => { 21 | await deleteObject(key); 22 | }; 23 | 24 | export const resizeImageS3 = async (imageUrl, width, height) => { 25 | const buffer = await resizeImage(imageUrl, width, height); 26 | const result = await uploadObject(buffer); 27 | return { 28 | url: result.Location, 29 | key: result.Key, 30 | title: result.Key 31 | }; 32 | }; 33 | 34 | export const resize = (readStream, format, width, height) => { 35 | let transform = sharp(); 36 | 37 | if (format) { 38 | transform = transform.toFormat(format); 39 | } 40 | 41 | if (width || height) { 42 | transform = transform.resize(width, height); 43 | } 44 | 45 | return readStream.data.pipe(transform); 46 | }; 47 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/users/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { celebrate } from "celebrate"; 3 | import AuthService from "../../middlewares/auth"; 4 | import userController from "./users.controller"; 5 | import { schemas } from "../../helpers"; 6 | import { 7 | paginateUserValidateSchema, 8 | changePasswordSchema, 9 | updateMeSchema 10 | } from "./user.validation"; 11 | 12 | const { objectIdSchema, paginateValidationSchema } = schemas; 13 | 14 | const router = express.Router(); 15 | 16 | /** 17 | * @swagger 18 | * 19 | * /users: 20 | * get: 21 | * tags: [users] 22 | * description: get all users 23 | * produces: 24 | * - application/json 25 | * security: 26 | * - BearerAuth: [] 27 | * parameters: 28 | * - $ref: '#/parameters/pageParam' 29 | * - $ref: '#/parameters/limitParam' 30 | * responses: 31 | * 200: 32 | * description: OK 33 | * schema: 34 | * type: object 35 | * properties: 36 | * page: 37 | * type: integer 38 | * format: int32 39 | * pages: 40 | * type: integer 41 | * format: int32 42 | * limit: 43 | * type: integer 44 | * format: int32 45 | * total: 46 | * type: integer 47 | * format: int32 48 | * data: 49 | * $ref: '#/definitions/ArrayOfUsers' 50 | * 401: 51 | * $ref: '#/responses/Unauthorized' 52 | */ 53 | router.get( 54 | "/", 55 | AuthService.required, 56 | celebrate({ query: paginateUserValidateSchema }), 57 | userController.findAll 58 | ); 59 | 60 | router.get("/me", AuthService.required, userController.getMe); 61 | 62 | /** 63 | * @swagger 64 | * 65 | * /users/{id}: 66 | * get: 67 | * tags: [users] 68 | * description: Get User by ID 69 | * security: 70 | * - BearerAuth: [] 71 | * produces: 72 | * - application/json 73 | * parameters: 74 | * - name: id 75 | * description: ID of user 76 | * in: path 77 | * required: true 78 | * type: string 79 | * responses: 80 | * 200: 81 | * description: OK 82 | * schema: 83 | * $ref: '#/definitions/User' 84 | * 400: 85 | * $ref: '#/responses/Error' 86 | * 401: 87 | * $ref: '#/responses/Unauthorized' 88 | */ 89 | router.get( 90 | "/:id", 91 | AuthService.required, 92 | celebrate({ params: objectIdSchema }), 93 | userController.findOne 94 | ); 95 | 96 | router.post( 97 | "/me/change-password", 98 | AuthService.required, 99 | celebrate({ body: changePasswordSchema }), 100 | userController.changePassword 101 | ); 102 | 103 | /** 104 | * @swagger 105 | * 106 | * /users/{id}: 107 | * delete: 108 | * tags: [users] 109 | * description: delete a user 110 | * security: 111 | * - BearerAuth: [] 112 | * produces: 113 | * - application/json 114 | * parameters: 115 | * - name: id 116 | * in: path 117 | * description: users id 118 | * required: true 119 | * type: string 120 | * responses: 121 | * 200: 122 | * description: OK 123 | * schema: 124 | * $ref: '#/definitions/User' 125 | * 400: 126 | * $ref: '#/responses/Error' 127 | * 401: 128 | * $ref: '#/responses/Unauthorized' 129 | */ 130 | router.delete( 131 | "/:id", 132 | AuthService.required, 133 | AuthService.isAdmin(), 134 | celebrate({ params: objectIdSchema }), 135 | userController.remove 136 | ); 137 | 138 | router.put( 139 | "/me", 140 | celebrate({ body: updateMeSchema }), 141 | AuthService.required, 142 | userController.updateMe 143 | ); 144 | 145 | /** 146 | * @swagger 147 | * 148 | * /users/{id}: 149 | * put: 150 | * tags: [users] 151 | * description: update a user 152 | * security: 153 | * - BearerAuth: [] 154 | * produces: 155 | * - application/json 156 | * parameters: 157 | * - name: id 158 | * in: path 159 | * description: user id 160 | * required: true 161 | * type: string 162 | * - name: data 163 | * in: body 164 | * required: true 165 | * schema: 166 | * $ref: '#/definitions/User' 167 | * responses: 168 | * 200: 169 | * description: OK 170 | * schema: 171 | * $ref: '#/definitions/User' 172 | * 400: 173 | * $ref: '#/responses/Error' 174 | * 401: 175 | * $ref: '#/responses/Unauthorized' 176 | */ 177 | router.put( 178 | "/:id", 179 | AuthService.required, 180 | AuthService.isAdmin(), 181 | celebrate({ params: objectIdSchema }), 182 | userController.update 183 | ); 184 | 185 | router.delete( 186 | "/me/delete-account", 187 | AuthService.required, 188 | userController.deleteAccount 189 | ); 190 | 191 | export default router; 192 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/users/user.validation.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | import { schemas } from "../../helpers"; 3 | 4 | const { paginateValidationSchema } = schemas; 5 | 6 | export const paginateUserValidateSchema = paginateValidationSchema.keys({ 7 | email: Joi.string().optional() 8 | }); // add more key 9 | 10 | export const changePasswordSchema = Joi.object({ 11 | currentPassword: Joi.string() 12 | .min(4) 13 | .max(255) 14 | .default(" ") 15 | .optional(), 16 | newPassword: Joi.string() 17 | .required() 18 | .invalid(Joi.ref("password")), 19 | confirmNewPassword: Joi.string() 20 | .required() 21 | .valid(Joi.ref("newPassword")) 22 | }); 23 | 24 | export const updateMeSchema = Joi.object({ 25 | email: Joi.any().forbidden(), 26 | isPremium: Joi.any().forbidden(), 27 | role: Joi.any().forbidden() 28 | }).unknown(true); 29 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/users/users.controller.js: -------------------------------------------------------------------------------- 1 | import { Controller } from "../../helpers/common"; 2 | import userService from "./users.service"; 3 | import { handleResponse } from "../../helpers"; 4 | 5 | class UserController extends Controller { 6 | constructor(service, name) { 7 | super(service, name); 8 | this.updateMe = this.updateMe.bind(this); 9 | this.getMe = this.getMe.bind(this); 10 | this.changePassword = this.changePassword.bind(this); 11 | this.deleteAccount = this.deleteAccount.bind(this); 12 | } 13 | 14 | async changePassword(req, res, next) { 15 | try { 16 | const user = req.user; 17 | const { currentPassword, newPassword } = req.body; 18 | const result = await this.service.handleChangePassword( 19 | user, 20 | currentPassword, 21 | newPassword 22 | ); 23 | return handleResponse.success(res, result); 24 | } catch (error) { 25 | next(error); 26 | } 27 | } 28 | 29 | async updateMe(req, res, next) { 30 | try { 31 | let result = await this.service.handleUpdateMe(req.user._id, req.body); 32 | 33 | return handleResponse.success(res, result); 34 | } catch (e) { 35 | next(e); 36 | } 37 | } 38 | 39 | async getMe(req, res, next) { 40 | try { 41 | let result = await this.service.handleGetMe(req.user); 42 | 43 | return handleResponse.success(res, result); 44 | } catch (e) { 45 | next(e); 46 | } 47 | } 48 | 49 | async deleteAccount(req, res, next) { 50 | try { 51 | const user = req.user; 52 | const result = await this.service.deleteAccount(user); 53 | return handleResponse.success(res, result); 54 | } catch (e) { 55 | next(e); 56 | } 57 | } 58 | } 59 | 60 | export default new UserController(userService, "User"); 61 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/users/users.model.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | import mongoosePaginate from "mongoose-paginate-v2"; 3 | import mongooseUniqueValidator from "mongoose-unique-validator"; 4 | import bcrypt from "bcryptjs"; 5 | 6 | const ROLES = { 7 | USER: "user", 8 | ADMIN: "admin" 9 | }; 10 | 11 | const UserSchema = new Schema( 12 | { 13 | fullName: { 14 | type: String, 15 | required: false 16 | }, 17 | avatar: { 18 | src: { 19 | type: String, 20 | required: false 21 | }, 22 | title: { 23 | type: String, 24 | required: false 25 | } 26 | }, 27 | email: { 28 | type: String, 29 | required: true, 30 | unique: true, 31 | trim: true, 32 | lowercase: true 33 | }, 34 | password: { 35 | type: String, 36 | required: false, 37 | minlength: 4 38 | }, 39 | role: { 40 | type: String, 41 | enum: Object.values(ROLES), 42 | default: ROLES.USER 43 | }, 44 | services: { 45 | facebook: { 46 | id: String, 47 | token: String 48 | }, 49 | google: { 50 | id: String, 51 | token: String 52 | }, 53 | apple: { 54 | id: String, 55 | token: String 56 | } 57 | }, 58 | resetPasswordToken: { 59 | type: String 60 | }, 61 | resetPasswordExpires: { 62 | type: Date 63 | }, 64 | isPremium: { 65 | type: Boolean, 66 | default: false 67 | }, 68 | refreshToken: { 69 | type: String 70 | } 71 | }, 72 | { 73 | timestamps: true 74 | } 75 | ); 76 | 77 | UserSchema.pre("save", async function() { 78 | const password = this.password; 79 | if (this.isModified("password")) { 80 | const saltRounds = 10; 81 | const passwordHash = await bcrypt.hash(password, saltRounds); 82 | this.password = passwordHash; 83 | } 84 | }); 85 | 86 | UserSchema.methods = { 87 | verifyPassword: function(password) { 88 | return bcrypt.compareSync(password, this.password); 89 | }, 90 | toJSON: function() { 91 | const obj = this.toObject({ virtuals: true }); 92 | obj.hasPassword = !!obj.password; 93 | delete obj.password; 94 | delete obj.resetPasswordToken; 95 | delete obj.resetPasswordExpires; 96 | return obj; 97 | }, 98 | isAdmin: function() { 99 | return this.role === ROLES.ADMIN; 100 | } 101 | }; 102 | 103 | UserSchema.plugin(mongoosePaginate); 104 | UserSchema.plugin(mongooseUniqueValidator); 105 | 106 | export default mongoose.model("User", UserSchema); 107 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/api/users/users.service.js: -------------------------------------------------------------------------------- 1 | import { Service } from "../../helpers/common"; 2 | import User from "./users.model"; 3 | import { logger } from "../../services"; 4 | 5 | class UserService extends Service { 6 | constructor(model) { 7 | super(model); 8 | } 9 | 10 | async handleChangePassword(user, currentPassword, newPassword) { 11 | const verifyPassword = user.toJSON().hasPassword 12 | ? user.verifyPassword(currentPassword) 13 | : "social"; 14 | 15 | if (!verifyPassword) { 16 | throw new Error("Incorrect Password"); 17 | } else { 18 | user.password = newPassword; 19 | return user.save(); 20 | } 21 | } 22 | 23 | async handleUpdateMe(userId, data) { 24 | return await User.findByIdAndUpdate(userId, data, { new: true }); 25 | } 26 | 27 | async handleGetMe(user) { 28 | return { ...user.toJSON() }; 29 | } 30 | 31 | async deleteAccount(user) { 32 | const result = await User.findByIdAndDelete(user._id); 33 | if (result) { 34 | return result; 35 | } else throw new Error("User not found!"); 36 | } 37 | } 38 | 39 | export default new UserService(User); 40 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/app.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import path from "path"; 3 | import helmet from "helmet"; 4 | import cors from "cors"; 5 | import compression from "compression"; 6 | import morgan from "morgan"; 7 | import bodyParser from "body-parser"; 8 | import cookieParser from "cookie-parser"; 9 | import passport from "passport"; 10 | import session from "express-session"; 11 | import rateLimit from "express-rate-limit"; 12 | import { 13 | errorHandle, 14 | notFoundHandle, 15 | logErrors 16 | } from "./helpers/handle-errors"; 17 | import { logger, swagger } from "./services"; 18 | import api from "./api"; 19 | import config from "./config"; 20 | 21 | require("./services/passport"); 22 | 23 | const rootApi = "/api/v1"; 24 | const ROOT_FOLDER = path.join(__dirname, ".."); 25 | const SRC_FOLDER = path.join(ROOT_FOLDER, "src"); 26 | 27 | const app = express(); 28 | 29 | app.set("trust proxy", 1); // trust first proxy 30 | 31 | // init middlewares 32 | 33 | // Security 34 | app.use(helmet()); 35 | app.use(cors()); 36 | app.disable("x-powered-by"); 37 | 38 | // compression 39 | app.use(compression()); 40 | 41 | app.use(cookieParser()); 42 | // logs http request 43 | app.use(morgan(process.env.LOG_FORMAT || "dev", { stream: logger.stream })); 44 | 45 | // parse requests of content-type - application/x-www-form-urlencoded 46 | app.use(bodyParser.urlencoded({ extended: true })); 47 | 48 | // parse requests of content-type - application/json 49 | app.use(bodyParser.json()); 50 | 51 | // session 52 | app.use( 53 | session({ 54 | secret: config.session.secret, 55 | resave: true, 56 | saveUninitialized: true 57 | }) 58 | ); 59 | 60 | // passport 61 | app.use(passport.initialize()); 62 | 63 | // rate limit 64 | const limiter = rateLimit({ 65 | windowMs: 60 * 1000, // 1 minute 66 | max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) 67 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 68 | legacyHeaders: false // Disable the `X-RateLimit-*` headers 69 | }); 70 | app.use(limiter); 71 | 72 | // init database 73 | require("./services/mongoose"); 74 | 75 | app.use(express.static(path.join(ROOT_FOLDER, "build"), { index: false })); 76 | app.use("/static", express.static(path.join(SRC_FOLDER, "public"))); 77 | app.use("/media", express.static(path.join(ROOT_FOLDER, "uploads"))); 78 | app.get("/", (req, res) => 79 | res.json({ message: "Welcome to <%=project_slug%> API!" }) 80 | ); 81 | 82 | app.use("/api-docs", swagger()); 83 | 84 | app.use(rootApi, api); 85 | 86 | // The "catchall" handler: for any request that doesn't 87 | // match one above, send back React's index.html file. 88 | app.get("/admin", (req, res) => { 89 | res.sendFile(path.join(ROOT_FOLDER, "build", "index.html")); 90 | }); 91 | 92 | app.use(notFoundHandle); 93 | app.use(logErrors); 94 | app.use(errorHandle); 95 | 96 | export default app; 97 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/config/index.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | 3 | dotenv.config(); 4 | 5 | export default { 6 | mongodb: { 7 | url: process.env.DATABASE_URL, 8 | secret: "!&!&OJpWXnDtB0eju7OE!zDp20G1JC%6bpq2", 9 | options: { 10 | user: process.env.MONGO_INITDB_ROOT_USERNAME, 11 | pass: process.env.MONGO_INITDB_ROOT_PASSWORD 12 | } 13 | }, 14 | 15 | session: { 16 | secret: process.env.SESSION_SECRET 17 | }, 18 | 19 | redis: { 20 | url: process.env.REDIS_URL 21 | }, 22 | 23 | facebook: { 24 | clientID: process.env.FACEBOOK_CLIENT_ID, 25 | clientSecret: process.env.FACEBOOK_CLIENT_SECRET, 26 | callbackURL: process.env.FACEBOOK_CALLBACK_URL 27 | }, 28 | 29 | google: { 30 | clientID: process.env.GOOGLE_CLIENT_ID, 31 | clientSecret: process.env.GOOGLE_CLIENT_SECRET, 32 | callbackURL: process.env.GOOGLE_CALLBACK_URL 33 | }, 34 | 35 | firebase: { 36 | private_key: process.env.private_key, 37 | client_email: process.env.client_email, 38 | client_x509_cert_url: process.env.client_x509_cert_url 39 | }, 40 | 41 | apple: { 42 | issuerId: process.env.APPLE_ISSUER_ID, 43 | keyId: process.env.APPLE_KEY_ID, 44 | bundleId: process.env.APPLE_BUNDLE_ID, 45 | appAppleId: process.env.APP_APPLE_ID, 46 | privateKeyFilePath: process.env.APPLE_PRIVATE_KEY_FILE_PATH 47 | }, 48 | 49 | aws: { 50 | accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID, 51 | secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY, 52 | bucketName: process.env.AWS_STORAGE_BUCKET_NAME, 53 | region: process.env.AWS_REGION_NAME 54 | }, 55 | 56 | jwt: { 57 | secret: process.env.JWT_SECRET 58 | }, 59 | 60 | app: { 61 | ROLE: { 62 | ADMIN: "ADMIN" 63 | } 64 | }, 65 | 66 | admin: { 67 | email: process.env.DEFAULT_ADMIN_EMAIL, 68 | password: process.env.DEFAULT_ADMIN_PASSWORD 69 | }, 70 | 71 | apn: { 72 | keyId: process.env.APN_KEY_ID, 73 | teamId: process.env.APN_TEAM_ID, 74 | topic: process.env.APN_TOPIC, 75 | production: process.env.APN_PRODUCTION 76 | }, 77 | iap: { 78 | IOS: { 79 | VERIFY_RECEIPT_URL: "https://buy.itunes.apple.com/verifyReceipt", 80 | SANDBOX_VERIFY_RECEIPT_URL: 81 | "https://sandbox.itunes.apple.com/verifyReceipt" 82 | } 83 | }, 84 | mailgun: { 85 | apiKey: process.env.MAILGUN_API_KEY, 86 | domain: process.env.MAILGUN_DOMAIN 87 | }, 88 | profileUrl: { 89 | google_access_token: process.env.GOOGLE_USER_PROFILE_URL_ACCESS_TOKEN, 90 | google_id_token: process.env.GOOGLE_USER_PROFILE_URL_ID_TOKEN, 91 | facebook: process.env.FACEBOOK_USER_PROFILE_URL, 92 | apple: process.env.APPLE_USER_PROFILE_URL 93 | } 94 | }; 95 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/core/controller.js: -------------------------------------------------------------------------------- 1 | import aqp from "api-query-params"; 2 | import { CREATED, LIST, OK } from "./success.response"; 3 | import { BadRequestError } from "./error.response"; 4 | 5 | class Controller { 6 | constructor(service, name) { 7 | this.service = service; 8 | this._name = name; 9 | this.findAll = this.findAll.bind(this); 10 | this.create = this.create.bind(this); 11 | this.findOne = this.findOne.bind(this); 12 | this.update = this.update.bind(this); 13 | this.remove = this.remove.bind(this); 14 | this.get_queryset = this.get_queryset.bind(this); 15 | } 16 | 17 | async create(req, res, next) { 18 | try { 19 | const data = req.body; 20 | const result = await this.service.create(data); 21 | return new CREATED({ data: result }).send(res); 22 | } catch (exception) { 23 | next(exception); 24 | } 25 | } 26 | 27 | get_queryset(req) { 28 | let params = aqp(req.query, { 29 | skipKey: "page" 30 | }); 31 | return params; 32 | } 33 | 34 | async findAll(req, res, next) { 35 | try { 36 | const query = this.get_queryset(req); 37 | const { 38 | docs, 39 | totalDocs, 40 | page, 41 | totalPages, 42 | limit 43 | } = await this.service.findAll(query); 44 | return new LIST({ 45 | data: docs, 46 | total: totalDocs, 47 | page: page, 48 | pages: totalPages, 49 | limit: limit 50 | }).send(res); 51 | } catch (exception) { 52 | next(exception); 53 | } 54 | } 55 | 56 | async findOne(req, res, next) { 57 | try { 58 | const result = await this.service.findById(req.params.id); 59 | if (!result) { 60 | throw new BadRequestError( 61 | `${this._name} does not found with id ${req.params.id}` 62 | ); 63 | } 64 | return new OK({ data: result }).send(res); 65 | } catch (exception) { 66 | next(exception); 67 | } 68 | } 69 | 70 | async update(req, res, next) { 71 | try { 72 | const result = await this.service.update(req.params.id, req.body, { 73 | new: true 74 | }); 75 | if (!result) { 76 | throw new BadRequestError( 77 | `${this._name} does not found with id ${req.params.id}` 78 | ); 79 | } 80 | return new OK({ data: result }).send(res); 81 | } catch (exception) { 82 | next(exception); 83 | } 84 | } 85 | 86 | async remove(req, res, next) { 87 | try { 88 | const result = await this.service.remove(req.params.id); 89 | if (!result) { 90 | throw new BadRequestError( 91 | `${this._name} does not found with id ${req.params.id}` 92 | ); 93 | } 94 | return new OK({ data: result }).send(res); 95 | } catch (exception) { 96 | next(exception); 97 | } 98 | } 99 | } 100 | 101 | export default Controller; 102 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/core/error.response.js: -------------------------------------------------------------------------------- 1 | import { ReasonPhrases, StatusCodes } from "http-status-codes"; 2 | 3 | export class AppError extends Error { 4 | constructor(message, status, code) { 5 | // Calling parent constructor of base Error class. 6 | super(message); 7 | 8 | // Saving class name in the property of our custom error as a shortcut. 9 | this.name = this.constructor.name; 10 | 11 | // Capturing stack trace, excluding constructor call from it. 12 | Error.captureStackTrace(this, this.constructor); 13 | 14 | // You can use any additional properties you want. 15 | // I'm going to use preferred HTTP status for this error types. 16 | // `500` is the default value if not specified. 17 | this.status = status; 18 | this.code = code; 19 | } 20 | } 21 | 22 | export class NotFoundError extends AppError { 23 | constructor( 24 | message = ReasonPhrases.NOT_FOUND, 25 | code = "NOT_FOUND", 26 | status = StatusCodes.NOT_FOUND 27 | ) { 28 | super(message, status, code); 29 | } 30 | } 31 | 32 | export class BadRequestError extends AppError { 33 | constructor( 34 | message = ReasonPhrases.BAD_REQUEST, 35 | code = "BAD_REQUEST", 36 | status = StatusCodes.BAD_REQUEST 37 | ) { 38 | super(message, status, code); 39 | } 40 | } 41 | 42 | export class InternalServerError extends AppError { 43 | constructor( 44 | message = ReasonPhrases.INTERNAL_SERVER_ERROR, 45 | code = "INTERNAL_SERVER_ERROR", 46 | status = StatusCodes.INTERNAL_SERVER_ERROR 47 | ) { 48 | super(message, status, code); 49 | } 50 | } 51 | 52 | export class ForbiddenError extends AppError { 53 | constructor( 54 | message = ReasonPhrases.FORBIDDEN, 55 | code = "FORBIDDEN", 56 | status = StatusCodes.FORBIDDEN 57 | ) { 58 | super(message, status, code); 59 | } 60 | } 61 | 62 | export class UnAuthorizedError extends AppError { 63 | constructor( 64 | message = ReasonPhrases.UNAUTHORIZED, 65 | code = "UNAUTHORIZED", 66 | status = StatusCodes.UNAUTHORIZED 67 | ) { 68 | super(message, status, code); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/core/service.js: -------------------------------------------------------------------------------- 1 | class Service { 2 | constructor(model) { 3 | this._model = model; 4 | this.findAll = this.findAll.bind(this); 5 | this.findById = this.findById.bind(this); 6 | this.findOne = this.findOne.bind(this); 7 | this.create = this.create.bind(this); 8 | this.update = this.update.bind(this); 9 | this.remove = this.remove.bind(this); 10 | } 11 | 12 | async findAll(query) { 13 | const { filter, skip, limit, sort, population, projection } = query; 14 | const result = await this._model.paginate(filter, { 15 | page: skip || 1, 16 | limit: limit || 25, 17 | sort: sort || "-createdAt", 18 | populate: population, 19 | select: projection 20 | }); 21 | return result; 22 | } 23 | 24 | async findById(id) { 25 | const result = await this._model.findById(id); 26 | return result; 27 | } 28 | 29 | async findOne(data) { 30 | const result = await this._model.findOne(data); 31 | return result; 32 | } 33 | 34 | async find(data) { 35 | const result = await this._model.find(data); 36 | return result; 37 | } 38 | 39 | async create(data) { 40 | const result = await this._model.create(data); 41 | return result; 42 | } 43 | 44 | async update(id, data) { 45 | const result = await this._model.findOneAndUpdate({ _id: id }, data, { 46 | new: true 47 | // runValidators: true, 48 | // context: 'query' 49 | }); 50 | return result; 51 | } 52 | 53 | async remove(id) { 54 | const result = await this._model.findByIdAndRemove(id); 55 | return result; 56 | } 57 | } 58 | 59 | export default Service; 60 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/core/success.response.js: -------------------------------------------------------------------------------- 1 | import { ReasonPhrases, StatusCodes } from "http-status-codes"; 2 | 3 | export class SuccessResponse { 4 | constructor({ 5 | message, 6 | statusCode = StatusCodes.OK, 7 | reasonStatusCode = ReasonPhrases.OK, 8 | data, 9 | total, 10 | limit, 11 | page, 12 | pages 13 | }) { 14 | this.message = !message ? reasonStatusCode : message; 15 | this.status = "success"; 16 | this.statusCode = statusCode; 17 | this.data = data; 18 | this.total = total; 19 | this.limit = limit; 20 | this.page = page; 21 | this.pages = pages; 22 | } 23 | 24 | send(res, headers = {}) { 25 | const { statusCode, ...rest } = this; 26 | return res.status(statusCode).json(rest); 27 | } 28 | } 29 | 30 | export class OK extends SuccessResponse { 31 | constructor({ message, data }) { 32 | super({ message, data }); 33 | } 34 | } 35 | 36 | export class CREATED extends SuccessResponse { 37 | constructor({ 38 | message, 39 | statusCode = StatusCodes.CREATED, 40 | reasonStatusCode = ReasonPhrases.CREATED, 41 | data 42 | }) { 43 | super({ message, statusCode, reasonStatusCode, data }); 44 | } 45 | } 46 | 47 | export class LIST extends SuccessResponse { 48 | constructor({ 49 | message, 50 | statusCode = StatusCodes.OK, 51 | reasonStatusCode = ReasonPhrases.OK, 52 | data, 53 | total, 54 | limit, 55 | page, 56 | pages 57 | }) { 58 | super({ 59 | message, 60 | statusCode, 61 | reasonStatusCode, 62 | data, 63 | total, 64 | limit, 65 | page, 66 | pages 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/db_seed/index.js: -------------------------------------------------------------------------------- 1 | import { logger } from "../services"; 2 | import { createAdminAccount } from "./user_seeder"; 3 | import User from "../api/users/users.model"; 4 | 5 | require("../services/mongoose"); 6 | 7 | (async () => { 8 | try { 9 | logger.info("=======seeding data==========="); 10 | await createAdminAccount(); 11 | await User.syncIndexes(); 12 | logger.info("=======seeded data was successfully==========="); 13 | } catch (error) { 14 | logger.error("==============error==========%j", error); 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/db_seed/user_seeder.js: -------------------------------------------------------------------------------- 1 | import config from "../config"; 2 | import User from "../api/users/users.model"; 3 | 4 | export const createAdminAccount = async () => { 5 | const defaultEmail = config.admin.email; 6 | const defaultPassword = config.admin.password; 7 | const admin = await User.findOne({ email: defaultEmail }); 8 | // console.log('======admin======', admin); 9 | if (!admin) { 10 | await User.create({ 11 | email: defaultEmail, 12 | fullName: "admin", 13 | password: defaultPassword, 14 | role: "admin" 15 | }); 16 | } else { 17 | admin.password = defaultPassword; 18 | await admin.save(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/common/Controller.js: -------------------------------------------------------------------------------- 1 | import aqp from "api-query-params"; 2 | import { CREATED, LIST, OK } from "../../core/success.response"; 3 | import { BadRequestError } from "../../core/error.response"; 4 | 5 | class Controller { 6 | constructor(service, name) { 7 | this.service = service; 8 | this._name = name; 9 | this.findAll = this.findAll.bind(this); 10 | this.create = this.create.bind(this); 11 | this.findOne = this.findOne.bind(this); 12 | this.update = this.update.bind(this); 13 | this.remove = this.remove.bind(this); 14 | this.get_queryset = this.get_queryset.bind(this); 15 | } 16 | 17 | async create(req, res, next) { 18 | try { 19 | const data = req.body; 20 | const result = await this.service.create(data); 21 | return new CREATED({ data: result }).send(res); 22 | } catch (exception) { 23 | next(exception); 24 | } 25 | } 26 | 27 | get_queryset(req) { 28 | let params = aqp(req.query, { 29 | skipKey: "page" 30 | }); 31 | return params; 32 | } 33 | 34 | async findAll(req, res, next) { 35 | try { 36 | const query = this.get_queryset(req); 37 | const { 38 | docs, 39 | totalDocs, 40 | page, 41 | totalPages, 42 | limit 43 | } = await this.service.findAll(query); 44 | return new LIST({ 45 | data: docs, 46 | total: totalDocs, 47 | page: page, 48 | pages: totalPages, 49 | limit: limit 50 | }).send(res); 51 | } catch (exception) { 52 | next(exception); 53 | } 54 | } 55 | 56 | async findOne(req, res, next) { 57 | try { 58 | const result = await this.service.findById(req.params.id); 59 | if (!result) { 60 | throw new BadRequestError( 61 | `${this._name} does not found with id ${req.params.id}` 62 | ); 63 | } 64 | return new OK({ data: result }).send(res); 65 | } catch (exception) { 66 | next(exception); 67 | } 68 | } 69 | 70 | async update(req, res, next) { 71 | try { 72 | const result = await this.service.update(req.params.id, req.body, { 73 | new: true 74 | }); 75 | if (!result) { 76 | throw new BadRequestError( 77 | `${this._name} does not found with id ${req.params.id}` 78 | ); 79 | } 80 | return new OK({ data: result }).send(res); 81 | } catch (exception) { 82 | next(exception); 83 | } 84 | } 85 | 86 | async remove(req, res, next) { 87 | try { 88 | const result = await this.service.remove(req.params.id); 89 | if (!result) { 90 | throw new BadRequestError( 91 | `${this._name} does not found with id ${req.params.id}` 92 | ); 93 | } 94 | return new OK({ data: result }).send(res); 95 | } catch (exception) { 96 | next(exception); 97 | } 98 | } 99 | } 100 | 101 | export default Controller; 102 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/common/Service.js: -------------------------------------------------------------------------------- 1 | class Service { 2 | constructor(model) { 3 | this._model = model; 4 | this.findAll = this.findAll.bind(this); 5 | this.findById = this.findById.bind(this); 6 | this.findOne = this.findOne.bind(this); 7 | this.create = this.create.bind(this); 8 | this.update = this.update.bind(this); 9 | this.remove = this.remove.bind(this); 10 | } 11 | 12 | async findAll(query) { 13 | const { filter, skip, limit, sort, population, projection } = query; 14 | const result = await this._model.paginate(filter, { 15 | page: skip || 1, 16 | limit: limit || 25, 17 | sort: sort || "-createdAt", 18 | populate: population, 19 | select: projection 20 | }); 21 | return result; 22 | } 23 | 24 | async findById(id) { 25 | const result = await this._model.findById(id); 26 | return result; 27 | } 28 | 29 | async findOne(data) { 30 | const result = await this._model.findOne(data); 31 | return result; 32 | } 33 | 34 | async find(data) { 35 | const result = await this._model.find(data); 36 | return result; 37 | } 38 | 39 | async create(data) { 40 | const result = await this._model.create(data); 41 | return result; 42 | } 43 | 44 | async update(id, data) { 45 | const result = await this._model.findOneAndUpdate({ _id: id }, data, { 46 | new: true 47 | // runValidators: true, 48 | // context: 'query' 49 | }); 50 | return result; 51 | } 52 | 53 | async remove(id) { 54 | const result = await this._model.findByIdAndRemove(id); 55 | return result; 56 | } 57 | } 58 | 59 | export default Service; 60 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/common/index.js: -------------------------------------------------------------------------------- 1 | import Service from "./Service"; 2 | import Controller from "./Controller"; 3 | 4 | export { Service, Controller }; 5 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/error.js: -------------------------------------------------------------------------------- 1 | export class AppError extends Error { 2 | constructor(message, code) { 3 | // Calling parent constructor of base Error class. 4 | super(message); 5 | 6 | // Saving class name in the property of our custom error as a shortcut. 7 | this.name = this.constructor.name; 8 | 9 | // Capturing stack trace, excluding constructor call from it. 10 | Error.captureStackTrace(this, this.constructor); 11 | 12 | // You can use any additional properties you want. 13 | // I'm going to use preferred HTTP status for this error types. 14 | // `500` is the default value if not specified. 15 | this.code = code || "500"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/handle-errors.js: -------------------------------------------------------------------------------- 1 | import { isCelebrateError, Segments } from "celebrate"; 2 | import { logger } from "../services"; 3 | import Response from "./response"; 4 | import { NotFoundError } from "../core/error.response"; 5 | 6 | // eslint-disable-next-line no-unused-vars 7 | export const errorHandle = (error, req, res, next) => { 8 | if (typeof error === "string") { 9 | // custom application error 10 | return Response.error(res, { message: error }); 11 | } else if (isCelebrateError(error)) { 12 | const bodyCelebrateError = error.details.get(Segments.BODY); 13 | const headerCelebrateError = error.details.get(Segments.HEADERS); 14 | 15 | const response = { 16 | message: "Invalid request data. Please review the request and try again.", 17 | code: [] 18 | }; 19 | 20 | if (bodyCelebrateError) { 21 | response.code = response.code.concat( 22 | bodyCelebrateError.details.map(({ message, type }) => ({ 23 | message: message.replace(/['"]/g, ""), 24 | code: type 25 | })) 26 | ); 27 | } 28 | 29 | if (headerCelebrateError) { 30 | response.code = response.code.concat( 31 | headerCelebrateError.details.map(({ message, type }) => ({ 32 | message: message.replace(/['"]/g, ""), 33 | code: type 34 | })) 35 | ); 36 | } 37 | return Response.error(res, response); 38 | } else if (error.name === "CastError" && error.kind === "ObjectId") { 39 | return Response.error(res, { 40 | // code: error.name, 41 | message: "malformatted id" 42 | }); 43 | } else { 44 | // default to 500 server error 45 | // logger.error("%o", error); 46 | return Response.error( 47 | res, 48 | { 49 | message: error.message, 50 | stack: process.env.NODE_ENV === "development" ? error.stack : null, 51 | code: error.code 52 | }, 53 | error.status 54 | ); 55 | } 56 | }; 57 | 58 | export const logErrors = (err, req, res, next) => { 59 | console.error(err.stack); 60 | next(err); 61 | }; 62 | 63 | export const notFoundHandle = (req, res, next) => { 64 | const error = new NotFoundError(); 65 | next(error); 66 | }; 67 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/index.js: -------------------------------------------------------------------------------- 1 | import handleResponse from "./response"; 2 | import * as pushToken from "./pushToken"; 3 | import * as schemas from "./schemas"; 4 | import * as utils from "./utils"; 5 | import * as verifyReceiptHelper from "./receipts"; 6 | 7 | export { handleResponse, pushToken, schemas, utils, verifyReceiptHelper }; 8 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/pushToken.js: -------------------------------------------------------------------------------- 1 | import admin from "firebase-admin"; 2 | 3 | export const sendNotificationToIosDevice = async ({ token, title, body }) => { 4 | admin.initializeApp({ 5 | credential: admin.credential.applicationDefault() 6 | }); 7 | const ios = { 8 | headers: { 9 | "apns-priority": 10, 10 | "apns-expiration": 360000 11 | }, 12 | payload: { 13 | aps: { 14 | alert: { 15 | title: title, 16 | body: body 17 | }, 18 | badge: 1, 19 | sound: "default" 20 | } 21 | } 22 | }; 23 | let message = { 24 | apns: ios, 25 | token: token 26 | }; 27 | const response = await admin.messaging().send(message); 28 | return response; 29 | }; 30 | 31 | export const sendNotificationToDevice = async ({ 32 | token, 33 | title, 34 | body, 35 | data = {} 36 | }) => { 37 | admin.initializeApp({ 38 | credential: admin.credential.applicationDefault() 39 | }); 40 | const android = { 41 | notification: { 42 | title: title, 43 | body: body 44 | } 45 | }; 46 | 47 | const apns = { 48 | payload: { 49 | aps: { 50 | alert: { 51 | title: title, 52 | body: body 53 | }, 54 | badge: 1, 55 | sound: "default" 56 | } 57 | } 58 | }; 59 | 60 | const message = { 61 | notification: { 62 | title: title, 63 | body: body 64 | }, 65 | data: data, 66 | android: android, 67 | apns: apns, 68 | token: token 69 | }; 70 | try { 71 | const response = await admin.messaging().send(message); 72 | return response; 73 | } catch (error) { 74 | console.error(error); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/response.js: -------------------------------------------------------------------------------- 1 | // https://github.com/cryptlex/rest-api-response-format 2 | export default class Response { 3 | static success(res, data, status = 200) { 4 | res.status(status); 5 | if (data && data.docs) { 6 | return res.json({ 7 | status: "success", 8 | data: data.docs, 9 | total: data.totalDocs, 10 | limit: data.limit, 11 | page: data.page, 12 | pages: data.totalPages 13 | }); 14 | } 15 | return res.json({ 16 | status: "success", 17 | data: data 18 | }); 19 | } 20 | 21 | static error(res, error, status = 400) { 22 | res.status(status); 23 | return res.json({ 24 | status: "error", 25 | error: { 26 | message: error.message, 27 | code: error.code, 28 | stack: process.env.NODE_ENV === "development" ? error.stack : {} 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/schemas.js: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | // accepts a valid UUID v4 string as id 4 | export const ObjectId = Joi.string().regex(/^[0-9a-fA-F]{24}$/); 5 | 6 | export const objectIdSchema = Joi.object({ 7 | id: ObjectId.required() 8 | }); 9 | 10 | export const paginateValidationSchema = Joi.object({ 11 | sort: Joi.string() 12 | .default("-createdAt") 13 | .optional(), 14 | page: Joi.number() 15 | .greater(0) 16 | .default(1) 17 | .positive() 18 | .optional(), 19 | limit: Joi.number() 20 | .greater(0) 21 | .default(25) 22 | .positive() 23 | .optional(), 24 | filter: Joi.string().optional() 25 | }); 26 | 27 | export const headerValidationSchema = Joi.object({ 28 | "device-id": Joi.string(), 29 | authorization: Joi.string() 30 | }) 31 | .or("authorization", "device-id") 32 | .unknown(true); 33 | 34 | export const imageSchema = Joi.object({ 35 | _id: ObjectId.optional(), 36 | src: Joi.string().required(), 37 | title: Joi.string().optional(), 38 | metadata: Joi.object().optional() 39 | }); 40 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/helpers/utils.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | import jwt from "jsonwebtoken"; 3 | import jwksClient from "jwks-rsa"; 4 | import client from "../services/redis"; 5 | 6 | export const randomInt = (low, high) => { 7 | return Math.floor(Math.random() * (high - low) + low); 8 | }; 9 | 10 | export const randomVerifiedCode = () => { 11 | return randomInt(100000, 999999); 12 | }; 13 | 14 | export const toNumber = string => { 15 | return Number(string) || string === "0" ? Number(string) : string; 16 | }; 17 | 18 | export const asyncForEach = async (array, callback) => { 19 | for (let index = 0; index < array.length; index++) { 20 | await callback(array[index], index, array); 21 | } 22 | }; 23 | 24 | export const parseMillisecond = ms => moment(parseInt(ms)); 25 | 26 | export const decodeToken = async token => { 27 | const decoded = jwt.decode(token, { complete: true }); 28 | const { kid, alg } = decoded.header; 29 | const client = jwksClient({ 30 | jwksUri: "https://appleid.apple.com/auth/keys", 31 | requestHeaders: {}, // Optional 32 | timeout: 30000 // Defaults to 30s 33 | }); 34 | const key = await client.getSigningKey(kid); 35 | const signingKey = key.getPublicKey() || key.rsaPublicKey(); 36 | return jwt.verify(token, signingKey, { algorithms: alg }); 37 | }; 38 | 39 | export const getDeviceId = req => { 40 | const deviceId = req.headers["device-id"] || req.params.deviceId || "NONE"; 41 | return deviceId; 42 | }; 43 | 44 | export const generateOtp = async email => { 45 | const otp = randomVerifiedCode(); 46 | 47 | await Promise.all([ 48 | client.set(`${email}_otp`, otp, { 49 | EX: 180, 50 | NX: false 51 | }), 52 | client.set(`${email}_attempts`, 0, { 53 | EX: 180, 54 | NX: false 55 | }) 56 | ]); 57 | 58 | return otp; 59 | }; 60 | 61 | export const compareOtp = async (email, otpRequest) => { 62 | const [numAttempts, otpStored] = await Promise.all([ 63 | client.incr(`${email}_attempts`), 64 | client.get(`${email}_otp`) 65 | ]); 66 | 67 | if (!numAttempts || !otpStored) return false; 68 | 69 | if (parseInt(numAttempts) > 3) { 70 | console.log("Deleting OTP due to 3 failed attempts"); 71 | await Promise.all([ 72 | client.del(`${email}_otp`), 73 | client.del(`${email}_attempts`) 74 | ]); 75 | 76 | return false; 77 | } 78 | 79 | if (parseInt(otpStored) !== parseInt(otpRequest)) { 80 | return false; 81 | } 82 | 83 | await Promise.all([ 84 | client.del(`${email}_otp`), 85 | client.del(`${email}_attempts`) 86 | ]); 87 | 88 | return true; 89 | }; 90 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import passport from "passport"; 2 | import Response from "../helpers/response"; 3 | import { UnAuthorizedError } from "../core/error.response"; 4 | 5 | export default class AuthService { 6 | static getTokenFromHeaderOrQuerystring(req) { 7 | const re = /(\S+)\s+(\S+)/; 8 | if (req.headers.authorization) { 9 | const matches = req.headers.authorization.match(re); 10 | return matches && { scheme: matches[1], value: matches[2] }; 11 | } else if (req.query && req.query.token) { 12 | const matches = req.query.token.match(re); 13 | return matches && { scheme: matches[1], value: matches[2] }; 14 | } else { 15 | return null; 16 | } 17 | } 18 | 19 | static required(req, res, next) { 20 | return passport.authenticate( 21 | "jwt", 22 | { session: false }, 23 | (err, user, info) => { 24 | // console.log('=======info==========', info); 25 | if (err) { 26 | return next(err); 27 | } 28 | if (!user) { 29 | throw new UnAuthorizedError("Invalid Token"); 30 | } else { 31 | req.logIn(user, function(err) { 32 | if (err) { 33 | return next(err); 34 | } 35 | return next(); 36 | }); 37 | } 38 | } 39 | )(req, res, next); 40 | } 41 | 42 | static roles(roles = []) { 43 | return (req, res, next) => { 44 | if (typeof roles === "string") { 45 | roles = [roles]; 46 | } 47 | if (roles.length && !roles.includes(req.user.role)) { 48 | throw new UnAuthorizedError( 49 | "You are not authorized to access this page!" 50 | ); 51 | } 52 | 53 | // authentication and authorization successful 54 | next(); 55 | }; 56 | } 57 | 58 | static optional(req, res, next) { 59 | const token = AuthService.getTokenFromHeaderOrQuerystring(req); 60 | if (token) { 61 | return AuthService.required(req, res, next); 62 | } else { 63 | next(); 64 | } 65 | } 66 | 67 | static isAdmin() { 68 | return AuthService.roles("admin"); 69 | } 70 | } 71 | 72 | // export const authLocal = passport.authenticate('local', { session: false }); 73 | export const authLocal = (req, res, next) => { 74 | passport.authenticate("local", { session: false }, (err, user, info) => { 75 | if (err) { 76 | return next(err); 77 | } 78 | if (!user) { 79 | throw new UnAuthorizedError("Your email or password is incorrect"); 80 | } 81 | req.logIn(user, function(err) { 82 | if (err) { 83 | return next(err); 84 | } 85 | return next(); 86 | }); 87 | })(req, res, next); 88 | }; 89 | export const authJwt = passport.authenticate("jwt", { session: false }); 90 | export const authFacebookToken = passport.authenticate("facebook-token"); 91 | export const authGoogleToken = passport.authenticate("google-token"); 92 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/middlewares/rate-limit.js: -------------------------------------------------------------------------------- 1 | import rateLimit from "express-rate-limit"; 2 | import config from "../config"; 3 | import { utils } from "../helpers"; 4 | import iapService from "../api/iaps/iap.service"; 5 | import RedisStore from "rate-limit-redis"; 6 | import client from "../services/redis"; 7 | 8 | /** 9 | * Limit each user to requests per `window` 10 | * @param {number} limit: number of allowed requests 11 | * @param {number} duration (in seconds) 12 | * @returns middleware 13 | */ 14 | export const rateLimitByUser = (limit, duration) => { 15 | return rateLimit({ 16 | windowMs: duration * 1000, 17 | max: limit, 18 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 19 | message: { 20 | message: 21 | "Too many request from this IP, please try again after an minutes", 22 | code: "429" 23 | }, 24 | legacyHeaders: false // Disable the `X-RateLimit-*` headers 25 | }); 26 | }; 27 | 28 | export const rateLimitByUserPurchase = () => { 29 | return rateLimit({ 30 | windowMs: 24 * 60 * 60 * 1000, 31 | // eslint-disable-next-line no-unused-vars 32 | keyGenerator: (request, response) => { 33 | const mode = request.body.mode; 34 | const deviceId = utils.getDeviceId(request); 35 | return `${mode}:${deviceId}`; 36 | }, 37 | max: async (req, res) => { 38 | const deviceId = utils.getDeviceId(req); 39 | const checkPremium = await iapService.checkIapModel(deviceId); 40 | if (!checkPremium) return config.maxRequestGpt; 41 | return 0; 42 | }, 43 | standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 44 | message: { 45 | message: "You've reached daily Limit", 46 | code: "429" 47 | }, 48 | legacyHeaders: false, // Disable the `X-RateLimit-*` headers 49 | store: new RedisStore({ 50 | sendCommand: (...args) => client.sendCommand(args) 51 | }) 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/server.js: -------------------------------------------------------------------------------- 1 | const debug = require("debug")("app:http"); 2 | import http from "http"; 3 | const { createTerminus } = require("@godaddy/terminus"); 4 | import app from "./app"; // the actual Express app 5 | 6 | const PORT = process.env.PORT || 3000; 7 | const server = http.createServer(app); 8 | 9 | process.on("SIGTERM", () => { 10 | debug("SIGTERM signal received: closing HTTP server"); 11 | server.close(() => { 12 | debug("HTTP server closed"); 13 | }); 14 | }); 15 | 16 | function onSignal() { 17 | console.log("server is starting cleanup"); 18 | // start cleanup of resource, like databases or file descriptors 19 | } 20 | 21 | async function onHealthCheck() { 22 | // checks if the system is healthy, like the db connection is live 23 | // resolves, if health, rejects if not 24 | } 25 | 26 | createTerminus(server, { 27 | signal: "SIGINT", 28 | healthChecks: { "/healthcheck": onHealthCheck }, 29 | onSignal 30 | }); 31 | 32 | server.listen(PORT, () => { 33 | debug(`Server running on port ${PORT}`); 34 | }); 35 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/index.js: -------------------------------------------------------------------------------- 1 | import logger from "./logger"; 2 | import mongoose from "./mongoose"; 3 | import Response from "./response"; 4 | import swagger from "./swagger"; 5 | import jwt from "./jwt"; 6 | import * as mailService from "./mailgun"; 7 | import redis from "./redis"; 8 | 9 | export { logger, mongoose, Response, swagger, jwt, mailService, redis }; 10 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/jwt.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | 3 | export const sign = data => { 4 | const token = jwt.sign(data, process.env.JWT_SECRET, { 5 | expiresIn: process.env.JWT_EXPIRES 6 | }); 7 | return token; 8 | }; 9 | 10 | export const verify = token => { 11 | return jwt.verify(token, process.env.JWT_SECRET, { ignoreExpiration: false }); 12 | }; 13 | 14 | export const refreshSign = uid => { 15 | const token = jwt.sign({ uid: uid }, process.env.JWT_REFRESH_SECRET, { 16 | expiresIn: process.env.JWT_REFRESH_EXPIRES 17 | }); 18 | return token; 19 | }; 20 | 21 | export const refreshVerify = token => { 22 | return jwt.verify(token, process.env.JWT_REFRESH_SECRET, { 23 | ignoreExpiration: false 24 | }); 25 | }; 26 | 27 | export const getToken = req => { 28 | let authorization = null; 29 | let token = null; 30 | if (req.query && req.query.token) { 31 | return req.query.token; 32 | } else if (req.authorization) { 33 | authorization = req.authorization; 34 | } else if (req.headers) { 35 | authorization = req.headers.authorization; 36 | } else if (req.socket) { 37 | if (req.socket.handshake.query && req.socket.handshake.query.token) { 38 | return req.socket.handshake.query.token; 39 | } 40 | authorization = req.socket.handshake.headers.authorization; 41 | } 42 | if (authorization) { 43 | const tokens = authorization.split("Bearer "); 44 | if (Array.isArray(tokens) || tokens.length === 2) { 45 | token = tokens[1]; 46 | } 47 | } 48 | return token; 49 | }; 50 | 51 | export default { 52 | sign, 53 | verify, 54 | getToken, 55 | refreshSign, 56 | refreshVerify 57 | }; 58 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/logger.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import { createLogger, format, transports } from "winston"; 4 | require("winston-daily-rotate-file"); 5 | 6 | const fileEnvs = ["production", "development"]; 7 | const env = process.env.NODE_ENV || "development"; 8 | const logDir = "logs"; 9 | 10 | // Create the log directory if it does not exist. 11 | if (!fs.existsSync(logDir)) { 12 | fs.mkdirSync(logDir); 13 | } 14 | 15 | const logger = createLogger({ 16 | level: "info", 17 | format: format.combine( 18 | format.label({ label: path.basename(module.parent.filename) }), 19 | format.colorize(), 20 | format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), 21 | format.splat(), // https://nodejs.org/dist/latest/docs/api/util.html#util_util_format_format_args 22 | format.printf( 23 | // We display the label text between square brackets using ${info.label} on the next line 24 | info => `${info.timestamp} ${info.level} [${info.label}]: ${info.message}` 25 | ) 26 | ), 27 | transports: [ 28 | new transports.Console({ 29 | level: env === "development" ? "info" : "error" 30 | // format: format.simple(), 31 | }) 32 | ] 33 | }); 34 | 35 | if (fileEnvs.includes(env)) { 36 | logger.add( 37 | new transports.DailyRotateFile({ 38 | filename: `${logDir}/%DATE%-combined.log`, 39 | datePattern: "YYYY-MM-DD", 40 | zippedArchive: true, 41 | maxSize: "20m", 42 | maxFiles: "14d" 43 | }) 44 | ); 45 | logger.add( 46 | new transports.DailyRotateFile({ 47 | filename: `${logDir}/%DATE%-error.log`, 48 | datePattern: "YYYY-MM-DD", 49 | zippedArchive: true, 50 | maxSize: "20m", 51 | maxFiles: "14d", 52 | level: "error" 53 | }) 54 | ); 55 | } 56 | 57 | // create a stream object with a 'write' function that will be used by `morgan` 58 | logger.stream = { 59 | write: function(message, encoding) { 60 | // use the 'info' log level so the output will be picked up by both transports (file and console) 61 | logger.info(message); 62 | } 63 | }; 64 | 65 | export default logger; 66 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/mailgun.js: -------------------------------------------------------------------------------- 1 | import Mailgun from "mailgun.js"; 2 | import formData from "form-data"; 3 | import config from "../config"; 4 | // import { logger } from "../services"; 5 | 6 | class MailgunConnect { 7 | constructor() {} 8 | static getInstance() { 9 | if (!this.instance) { 10 | const mailgun = new Mailgun(formData); 11 | const mg = mailgun.client({ 12 | username: "api", 13 | key: config.mailgun.apiKey, 14 | timeout: 60000 15 | }); 16 | this.instance = mg; 17 | } 18 | 19 | return this.instance; 20 | } 21 | } 22 | 23 | const domain = config.mailgun.domain; 24 | const fromEmail = "Support"; 25 | 26 | export const sendPasswordResetEmail = async (to, passcode) => { 27 | try { 28 | const data = { 29 | from: fromEmail, 30 | to: to, 31 | subject: "Reset Password App", 32 | // text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' + 33 | // 'Please use the passcode below:\n\n' + 34 | // '--' + passcode + '-- \n\n' + 35 | // 'If you did not request this, please ignore this email and your password will remain unchanged.\n', 36 | html: 37 | "

You are receiving this because you (or someone else) have requested the reset of the password for your account.

" + 38 | "

Please use the passcode below:

" + 39 | "" + 40 | passcode + 41 | "" + 42 | "

If you did not request this, please ignore this email and your password will remain unchanged.

" 43 | }; 44 | const mg = MailgunConnect.getInstance(); 45 | const msg = await mg.messages.create(domain, data); 46 | return msg; 47 | } catch (error) { 48 | console.log(error); 49 | } 50 | }; 51 | 52 | export const sendEmail = async options => { 53 | try { 54 | const mailOptions = { 55 | from: fromEmail, 56 | to: options.to, 57 | subject: options.subject, 58 | html: options.html 59 | }; 60 | const mg = MailgunConnect.getInstance(); 61 | return mg.messages.create(domain, mailOptions); 62 | } catch (error) { 63 | console.log(error); 64 | } 65 | }; 66 | 67 | export const sendOtpByEmail = async (to, optCode) => { 68 | try { 69 | const mailOptions = { 70 | from: fromEmail, 71 | to: to, 72 | subject: "Your log in code for SnapCards", 73 | html: ` 74 | 75 |

Hello ${to}

76 |

Please log in your account with the below code:

77 |

${optCode}

78 |

The code is valid for 5 minutes.

79 |

SnapCards Team

80 | 81 | ` 82 | }; 83 | const mg = MailgunConnect.getInstance(); 84 | return mg.messages.create(domain, mailOptions); 85 | } catch (error) { 86 | console.log(error); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/mongoose.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { logger } from "../services"; 3 | import config from "../config"; 4 | 5 | mongoose 6 | .connect(config.mongodb.url, config.mongodb.options) 7 | .then(_ => logger.info("Successfully connected to the mongoDB database")) 8 | .catch(error => logger.error("MongoDB connection error: ", error)); 9 | 10 | export default mongoose; 11 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/redis.js: -------------------------------------------------------------------------------- 1 | import { createClient } from "redis"; 2 | import config from "../config"; 3 | import logger from "./logger"; 4 | 5 | const client = createClient({ url: config.redis.url }); 6 | 7 | client.on("connect", () => { 8 | logger.info("Connected to Redis"); 9 | }); 10 | 11 | client.on("error", error => { 12 | logger.error("Redis Client Error %o", error); 13 | }); 14 | 15 | client.connect(); 16 | 17 | export default client; 18 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/response.js: -------------------------------------------------------------------------------- 1 | // https://github.com/cryptlex/rest-api-response-format 2 | export default class Response { 3 | static success(res, data, status = 200) { 4 | res.status(status); 5 | if (data && data.docs) { 6 | return res.json({ 7 | status: "success", 8 | data: data.docs, 9 | total: data.total, 10 | limit: data.limit, 11 | page: data.page, 12 | pages: data.pages 13 | }); 14 | } 15 | return res.json({ 16 | status: "success", 17 | data: data 18 | }); 19 | } 20 | 21 | static error(res, error, status = 400) { 22 | res.status(status); 23 | return res.json({ 24 | success: "failed", 25 | error: { 26 | message: error.message, 27 | code: error.code || "Invalid", 28 | errors: error.errors 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/storage.js: -------------------------------------------------------------------------------- 1 | import mime from "mime"; 2 | import crypto from "crypto"; 3 | import path from "path"; 4 | import fs from "fs"; 5 | import { promisify } from "util"; 6 | import { DeleteObjectCommand, S3Client } from "@aws-sdk/client-s3"; 7 | import { Upload } from "@aws-sdk/lib-storage"; 8 | import multer from "multer"; 9 | import multerS3 from "multer-s3"; 10 | import axios from "axios"; 11 | import sharp from "sharp"; 12 | 13 | import config from "../config"; 14 | 15 | const pseudoRandomBytesAsync = promisify(crypto.pseudoRandomBytes); 16 | const S3_ACL = "public-read"; 17 | 18 | const s3 = new S3Client({ 19 | region: config.aws.region, 20 | credentials: { 21 | accessKeyId: config.aws.accessKeyId, 22 | secretAccessKey: config.aws.secretAccessKey 23 | } 24 | }); 25 | 26 | const s3Storage = multerS3({ 27 | s3: s3, 28 | bucket: config.aws.bucketName, 29 | acl: S3_ACL, 30 | contentType: multerS3.AUTO_CONTENT_TYPE, 31 | metadata: function(req, file, cb) { 32 | cb(null, { fieldName: file.fieldname }); 33 | }, 34 | key: function(req, file, cb) { 35 | crypto.pseudoRandomBytes(16, function(err, raw) { 36 | cb( 37 | null, 38 | raw.toString("hex") + 39 | Date.now() + 40 | "." + 41 | mime.getExtension(file.mimetype) 42 | ); 43 | }); 44 | } 45 | }); 46 | 47 | const localStorage = multer.diskStorage({ 48 | destination: function(req, file, callback) { 49 | const uploadFolder = path.join(__dirname, "..", "..", "uploads"); 50 | if (!fs.existsSync(uploadFolder)) { 51 | fs.mkdirSync(uploadFolder); 52 | } 53 | callback(null, uploadFolder); 54 | }, 55 | filename: function(req, file, cb) { 56 | crypto.pseudoRandomBytes(16, function(err, raw) { 57 | cb( 58 | null, 59 | raw.toString("hex") + 60 | Date.now() + 61 | "." + 62 | mime.getExtension(file.mimetype) 63 | ); 64 | }); 65 | } 66 | }); 67 | 68 | const generateFileName = async fileExt => { 69 | const raw = await pseudoRandomBytesAsync(16); 70 | const fileName = raw.toString("hex") + Date.now() + "." + fileExt; 71 | return fileName; 72 | }; 73 | 74 | const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB 75 | 76 | const fileFilter = (req, file, cb) => { 77 | if ( 78 | file.mimetype == "image/png" || 79 | file.mimetype == "image/jpg" || 80 | file.mimetype == "image/jpeg" || 81 | file.mimetype == "image/gif" || 82 | file.mimetype == "video/mp4" 83 | ) { 84 | cb(null, true); 85 | } else { 86 | cb(null, false); 87 | return cb(new Error("Only .png, .jpg and .jpeg .gif .mp4 format allowed!")); 88 | } 89 | }; 90 | 91 | const fileFilterAllImageVideo = (req, file, cb) => { 92 | const acceptedTypes = file.mimetype.split("/"); 93 | 94 | if (acceptedTypes[0] === "image" || acceptedTypes[0] === "video") { 95 | cb(null, true); 96 | } else { 97 | cb(null, false); 98 | cb(new Error("Only images and videos formats allowed!")); 99 | } 100 | }; 101 | 102 | const upload = multer({ 103 | storage: localStorage, 104 | fileFilter: fileFilter, 105 | limits: { 106 | fileSize: MAX_FILE_SIZE 107 | } 108 | }); 109 | 110 | const uploadS3 = multer({ 111 | storage: s3Storage, 112 | fileFilter: fileFilterAllImageVideo, 113 | limits: { 114 | fileSize: MAX_FILE_SIZE 115 | } 116 | }); 117 | 118 | export const deleteObject = async key => { 119 | try { 120 | const command = new DeleteObjectCommand({ 121 | Bucket: config.aws.bucketName, 122 | Key: key 123 | }); 124 | const response = await s3.send(command); 125 | // console.log(response); 126 | return response; 127 | } catch (err) { 128 | console.error(err); 129 | } 130 | }; 131 | 132 | export const uploadObject = async buffer => { 133 | const key = await generateFileName("png"); 134 | const params = { 135 | Bucket: config.aws.bucketName, 136 | Key: key, 137 | Body: Buffer.from(buffer, "base64"), 138 | ACL: S3_ACL, 139 | ContentType: "image/png" 140 | }; 141 | 142 | try { 143 | const uploadS3 = new Upload({ 144 | client: s3, 145 | params: params 146 | }); 147 | 148 | uploadS3.on("httpUploadProgress", progress => { 149 | console.log(progress); 150 | }); 151 | 152 | return await uploadS3.done(); 153 | } catch (err) { 154 | console.error(err); 155 | } 156 | }; 157 | 158 | export const resizeImage = async (imageUrl, width, height) => { 159 | // let cachedCompositeInput = null; 160 | const input = (await axios({ url: imageUrl, responseType: "arraybuffer" })) 161 | .data; 162 | return await sharp(input) 163 | // .composite({ input: cachedCompositeInput }) 164 | .resize({ width: width, height: height }) 165 | .webp({ lossless: true }) 166 | .toBuffer(); 167 | }; 168 | 169 | export const avatarUpload = upload.single("avatar"); 170 | export const imageUpload = upload.single("image"); 171 | export const imagesUpload = upload.array("images", 12); 172 | export const fileUpload = upload.single("file"); 173 | 174 | export const avatarUploadS3 = uploadS3.single("avatar"); 175 | export const imageUploadS3 = uploadS3.single("image"); 176 | export const imagesUploadS3 = uploadS3.array("images", 12); 177 | export const fileUploadS3 = uploadS3.single("file"); 178 | -------------------------------------------------------------------------------- /generators/app/templates/backend/src/services/swagger.js: -------------------------------------------------------------------------------- 1 | import swaggerUi from "swagger-ui-express"; 2 | import swaggerJSDoc from "swagger-jsdoc"; 3 | 4 | const swagger = () => { 5 | const swaggerDefinition = { 6 | // openapi: '3.0.0', // Specification (optional, defaults to swagger: '2.0') 7 | info: { 8 | title: "private-phone-number", // Title (required) 9 | version: "1.0.0" // Version (required) 10 | }, 11 | basePath: "/api/v1", // Base path (optional) 12 | schemes: 13 | process.env.SWAGGER_SCHEMA_HTTPS === "true" 14 | ? ["https"] 15 | : ["http", "https"], 16 | securityDefinitions: { 17 | BearerAuth: { 18 | type: "apiKey", 19 | name: "Authorization", 20 | in: "header" 21 | } 22 | } 23 | }; 24 | 25 | const options = { 26 | swaggerDefinition, 27 | apis: ["src/api/**/*.js"] // <-- not in the definition, but in the options 28 | }; 29 | 30 | const swaggerSpec = swaggerJSDoc(options); 31 | 32 | const swOptions = { 33 | explorer: true, 34 | customCss: 35 | ".swagger-ui .opblock-body pre span {color: #DCD427 !important} .swagger-ui .opblock-body pre {color: #DCD427} .swagger-ui textarea.curl {color: #DCD427} .swagger-ui .response-col_description__inner div.markdown, .swagger-ui .response-col_description__inner div.renderedMarkdown {color: #DCD427}" 36 | }; 37 | 38 | return [swaggerUi.serve, swaggerUi.setup(swaggerSpec, swOptions)]; 39 | }; 40 | 41 | export default swagger; 42 | -------------------------------------------------------------------------------- /generators/app/templates/backend/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minhuyen/generator-expressjs-rest/9ec1b6dbb455631b02c46e29f0081b367a5dddfc/generators/app/templates/backend/uploads/.gitkeep -------------------------------------------------------------------------------- /generators/app/templates/compose/client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.20.0-alpine 2 | 3 | RUN addgroup -S app && adduser -S app -G app 4 | 5 | ENV HOME=/home/app 6 | 7 | COPY frontend/package.json frontend/yarn.lock $HOME/client/ 8 | 9 | RUN mkdir $HOME/client/build 10 | 11 | RUN chown -R app:app $HOME/* 12 | 13 | USER app 14 | 15 | WORKDIR $HOME/client 16 | 17 | RUN yarn install 18 | 19 | CMD ["yarn", "start"] 20 | -------------------------------------------------------------------------------- /generators/app/templates/compose/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.25.2 2 | RUN apt-get update && apt-get -y install nano 3 | RUN mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.backup 4 | 5 | ADD conf.d/ /etc/nginx/conf.d/ -------------------------------------------------------------------------------- /generators/app/templates/compose/nginx/conf.d/<%=project_slug%>.conf: -------------------------------------------------------------------------------- 1 | upstream app { 2 | server backend:3000; 3 | } 4 | 5 | server { 6 | 7 | listen 80; 8 | underscores_in_headers on; 9 | charset utf-8; 10 | 11 | location / { 12 | proxy_pass http://app; 13 | proxy_set_header Host $http_host; 14 | proxy_set_header X-Real-IP $remote_addr; 15 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 16 | proxy_redirect off; 17 | proxy_http_version 1.1; 18 | proxy_set_header Upgrade $http_upgrade; 19 | proxy_set_header Connection 'upgrade'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /generators/app/templates/compose/node/Dockerfile.local: -------------------------------------------------------------------------------- 1 | FROM node:18.16-alpine 2 | 3 | RUN addgroup -S app && adduser -S app -G app 4 | 5 | RUN apk update \ 6 | && apk add mongodb-tools 7 | 8 | ENV HOME=/home/app 9 | 10 | COPY backend/package*.json $HOME/<%=project_slug%>/ 11 | RUN chown -R app:app $HOME/* 12 | 13 | USER app 14 | WORKDIR $HOME/<%=project_slug%> 15 | RUN npm install 16 | 17 | CMD ["npm", "start"] 18 | -------------------------------------------------------------------------------- /generators/app/templates/compose/node/Dockerfile.production: -------------------------------------------------------------------------------- 1 | ########### 2 | # BUILDER # 3 | ########### 4 | 5 | # Setup and build the client 6 | 7 | FROM node:18.16-alpine as builder 8 | 9 | # set working directory 10 | WORKDIR /home/node/app 11 | 12 | COPY --chown=node:node frontend/package.json frontend/yarn.lock ./ 13 | RUN chown -R node:node /home/node/* 14 | 15 | USER node 16 | 17 | # RUN yarn upgrade caniuse-lite browserslist 18 | 19 | RUN yarn install 20 | 21 | COPY --chown=node:node frontend . 22 | 23 | RUN yarn build 24 | 25 | # Setup the server 26 | 27 | FROM node:18.16-alpine 28 | 29 | ENV NPM_CONFIG_PRODUCTION false 30 | 31 | RUN apk update \ 32 | # curl depenencies 33 | && apk add curl \ 34 | # Git depenencies 35 | && apk add git \ 36 | # mongo tools 37 | && apk add mongodb-tools 38 | 39 | RUN addgroup -S app && adduser -S app -G app 40 | 41 | # RUN npm config set unsafe-perm true 42 | 43 | RUN npm install -g pm2 44 | 45 | ENV HOME=/home/app 46 | 47 | COPY backend/package*.json $HOME/<%=project_slug%>/ 48 | RUN chown -R app:app $HOME/* 49 | 50 | USER app 51 | 52 | WORKDIR $HOME/<%=project_slug%> 53 | 54 | COPY --from=builder /home/node/app/build/ ./build/ 55 | 56 | # Safe install 57 | RUN npm ci && npm cache clean --force 58 | 59 | COPY --chown=app:app backend . 60 | 61 | RUN npm run build 62 | 63 | # Clean dev packages 64 | RUN npm prune --production 65 | 66 | EXPOSE 3000 67 | 68 | CMD ["pm2-runtime", "processes.json"] 69 | -------------------------------------------------------------------------------- /generators/app/templates/docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | volumes: 4 | mongo_data: {} 5 | mongo_backup: {} 6 | node_logs: {} 7 | media_data: {} 8 | backend_node_module: {} 9 | 10 | networks: 11 | app-network: 12 | driver: bridge 13 | 14 | services: 15 | backend: 16 | restart: always 17 | image: registry.astraler.com/<%=project_slug%>:latest 18 | container_name: <%=project_slug%>_node_production 19 | # build: 20 | # context: . 21 | # dockerfile: compose/node/Dockerfile.production 22 | env_file: 23 | - .env 24 | volumes: 25 | - backend_node_module:/home/app/<%=project_slug%>/node_modules 26 | - node_logs:/home/app/<%=project_slug%>/logs 27 | - media_data:/home/app/<%=project_slug%>/uploads 28 | - ./backend/keys:/home/app/human-design-backend/keys 29 | - ./backend/certs:/home/app/human-design-backend/certs 30 | depends_on: 31 | - mongo 32 | - redis 33 | networks: 34 | - app-network 35 | 36 | # tty: true 37 | mongo: 38 | restart: always 39 | image: mongo:4.2 40 | container_name: <%=project_slug%>_mongo_production 41 | env_file: 42 | - .env 43 | command: mongod --storageEngine wiredTiger 44 | volumes: 45 | - mongo_data:/data/db 46 | - mongo_backup:/data/backup 47 | networks: 48 | - app-network 49 | 50 | redis: 51 | image: redis:6.2.13-alpine 52 | container_name: <%=project_slug%>_redis_production 53 | networks: 54 | - app-network 55 | 56 | nginx: 57 | restart: always 58 | build: ./compose/nginx 59 | container_name: <%=project_slug%>_nginx_production 60 | depends_on: 61 | - backend 62 | ports: 63 | - "80:80" 64 | - "443:443" 65 | networks: 66 | - app-network 67 | 68 | -------------------------------------------------------------------------------- /generators/app/templates/docker-compose.staging.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | volumes: 4 | mongo_data: {} 5 | mongo_backup: {} 6 | node_logs: {} 7 | media_data: {} 8 | backend_node_module: {} 9 | 10 | networks: 11 | app-network: 12 | driver: bridge 13 | 14 | services: 15 | backend: 16 | restart: always 17 | image: registry.astraler.com/<%=project_slug%>:staging 18 | container_name: <%=project_slug%>_node_staging 19 | # build: 20 | # context: . 21 | # dockerfile: compose/node/Dockerfile.staging 22 | env_file: 23 | - .env 24 | volumes: 25 | - backend_node_module:/home/app/<%=project_slug%>/node_modules 26 | - node_logs:/home/app/<%=project_slug%>/logs 27 | - media_data:/home/app/<%=project_slug%>/uploads 28 | - ./backend/keys:/home/app/human-design-backend/keys 29 | - ./backend/certs:/home/app/human-design-backend/certs 30 | depends_on: 31 | - mongo 32 | - redis 33 | networks: 34 | - app-network 35 | # tty: true 36 | 37 | mongo: 38 | restart: always 39 | image: mongo:4.2 40 | container_name: <%=project_slug%>_mongo_staging 41 | env_file: 42 | - .env 43 | command: mongod --storageEngine wiredTiger 44 | volumes: 45 | - mongo_data:/data/db 46 | - mongo_backup:/data/backup 47 | networks: 48 | - app-network 49 | 50 | redis: 51 | image: redis:6.2.13-alpine 52 | container_name: <%=project_slug%>_redis_staging 53 | networks: 54 | - app-network 55 | 56 | nginx: 57 | restart: always 58 | build: ./compose/nginx 59 | container_name: <%=project_slug%>_nginx_staging 60 | depends_on: 61 | - backend 62 | ports: 63 | - "80:80" 64 | - "443:443" 65 | networks: 66 | - app-network 67 | 68 | -------------------------------------------------------------------------------- /generators/app/templates/docker-compose.test.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | volumes: 4 | mongo_data: {} 5 | mongo_backup: {} 6 | node_logs: {} 7 | backend_node_module: {} 8 | 9 | networks: 10 | app-network: 11 | driver: bridge 12 | 13 | services: 14 | backend: 15 | restart: always 16 | # image: registry.astraler.com/<%=project_slug%>:staging 17 | container_name: <%=project_slug%>_node_test 18 | build: 19 | context: . 20 | dockerfile: compose/node/Dockerfile.local 21 | env_file: 22 | - .env 23 | volumes: 24 | - ./backend:/home/app/<%=project_slug%> 25 | - backend_node_module:/home/app/<%=project_slug%>/node_modules 26 | - node_logs:/home/app/<%=project_slug%>/logs 27 | ports: 28 | - "3000:3000" 29 | depends_on: 30 | - mongo 31 | - redis 32 | networks: 33 | - app-network 34 | # tty: true 35 | mongo: 36 | restart: always 37 | container_name: <%=project_slug%>_mongo_test 38 | image: mongo:4.2 39 | command: mongod --storageEngine wiredTiger 40 | env_file: 41 | - .env 42 | volumes: 43 | - mongo_data:/data/db 44 | - mongo_backup:/data/backup 45 | # ports: 46 | # - '27017:27017' 47 | networks: 48 | - app-network 49 | 50 | redis: 51 | image: redis:6.2.13-alpine 52 | container_name: <%=project_slug%>_redis_local 53 | networks: 54 | - app-network 55 | 56 | -------------------------------------------------------------------------------- /generators/app/templates/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | volumes: 4 | mongo_data: {} 5 | frontend: {} 6 | backend_node_module: {} 7 | frontend_node_module: {} 8 | 9 | networks: 10 | app-network: 11 | driver: bridge 12 | 13 | services: 14 | backend: 15 | restart: always 16 | container_name: <%=project_slug%>_node_local 17 | build: 18 | context: . 19 | dockerfile: compose/node/Dockerfile.local 20 | # command: nodemon --inspect=0.0.0.0:9229 --nolazy app.js 21 | ports: 22 | - "3000:3000" 23 | - "9229:9229" 24 | env_file: 25 | - .env 26 | volumes: 27 | - ./backend:/home/app/<%=project_slug%> 28 | - backend_node_module:/home/app/<%=project_slug%>/node_modules 29 | - frontend:/home/app/<%=project_slug%>/src/client/build 30 | depends_on: 31 | - mongo 32 | - redis 33 | networks: 34 | - app-network 35 | 36 | frontend: 37 | restart: always 38 | container_name: <%=project_slug%>_client_local 39 | build: 40 | context: . 41 | dockerfile: compose/client/Dockerfile 42 | ports: 43 | - "3001:3000" 44 | env_file: 45 | - .env 46 | volumes: 47 | - ./frontend:/home/app/client 48 | - frontend_node_module:/home/app/client/node_modules 49 | - frontend:/home/app/client/build 50 | depends_on: 51 | - backend 52 | networks: 53 | - app-network 54 | stdin_open: true 55 | 56 | mongo: 57 | image: mongo:4.2 58 | container_name: <%=project_slug%>_mongo_local 59 | env_file: 60 | - .env 61 | command: mongod --port 27017 --storageEngine wiredTiger 62 | volumes: 63 | - mongo_data:/data/db 64 | ports: 65 | - "27017:27017" 66 | networks: 67 | - app-network 68 | 69 | redis: 70 | image: redis:6.2.13-alpine 71 | container_name: <%=project_slug%>_redis_local 72 | networks: 73 | - app-network 74 | 75 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | "eslint:recommended", 10 | "plugin:react/recommended", 11 | "plugin:prettier/recommended", 12 | "prettier", 13 | ], 14 | plugins: ["react", "prettier"], 15 | parserOptions: { 16 | ecmaFeatures: { 17 | jsx: true, 18 | }, 19 | ecmaVersion: 2018, 20 | sourceType: "module", 21 | requireConfigFile: false, 22 | babelOptions: { 23 | presets: ["@babel/preset-react"], 24 | }, 25 | }, 26 | parser: "@babel/eslint-parser", 27 | rules: { 28 | indent: [ 29 | 2, 30 | 2, 31 | { 32 | SwitchCase: 1, 33 | }, 34 | ], 35 | "linebreak-style": ["error", "unix"], 36 | quotes: ["error", "double"], 37 | semi: ["error", "always"], 38 | eqeqeq: "error", 39 | "no-trailing-spaces": "error", 40 | "object-curly-spacing": ["error", "always"], 41 | "arrow-spacing": ["error", { before: true, after: true }], 42 | "no-console": 0, 43 | "react/prop-types": 0, 44 | "react/react-in-jsx-scope": "off", 45 | "prettier/prettier": "error", 46 | }, 47 | settings: { 48 | react: { 49 | version: "detect", 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "jwt-decode": "^3.1.2", 7 | "ra-input-rich-text": "^4.11.1", 8 | "react": "^18.2.0", 9 | "react-admin": "^4.16.15", 10 | "react-dom": "^18.2.0", 11 | "react-scripts": "^5.0.1" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "lint-staged" 16 | } 17 | }, 18 | "lint-staged": { 19 | "src/**/*.{js,jsx,ts,tsx,json,css,scss,md}": [ 20 | "prettier --write" 21 | ] 22 | }, 23 | "scripts": { 24 | "start": "react-scripts start", 25 | "build": "react-scripts build", 26 | "test": "react-scripts test", 27 | "eject": "react-scripts eject", 28 | "lint": "eslint './src/**/*.{js,jsx}'", 29 | "lint:fix": "eslint './src/**/*.{js,jsx}' --fix" 30 | }, 31 | "proxy": "http://backend:3000", 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ], 38 | "devDependencies": { 39 | "eslint-config-prettier": "^8.8.0", 40 | "eslint-plugin-prettier": "^4.2.1", 41 | "eslint-plugin-react": "^7.32.2", 42 | "husky": "^8.0.3", 43 | "lint-staged": "^13.2.2", 44 | "prettier": "^2.8.8" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/minhuyen/generator-expressjs-rest/9ec1b6dbb455631b02c46e29f0081b367a5dddfc/generators/app/templates/frontend/public/favicon.ico -------------------------------------------------------------------------------- /generators/app/templates/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | } 9 | 10 | .App-header { 11 | background-color: #282c34; 12 | min-height: 100vh; 13 | display: flex; 14 | flex-direction: column; 15 | align-items: center; 16 | justify-content: center; 17 | font-size: calc(10px + 2vmin); 18 | color: white; 19 | } 20 | 21 | .App-link { 22 | color: #61dafb; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { 27 | transform: rotate(0deg); 28 | } 29 | to { 30 | transform: rotate(360deg); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Admin, Resource } from "react-admin"; 3 | import * as fetchUtils from "./utils/fetch"; 4 | import decodeJwt from "jwt-decode"; 5 | import addUploadFeature from "./addUploadFeature"; 6 | import NotFound from "./NotFound"; 7 | import authProvider from "./authProvider"; 8 | import restProvider from "./restProvider"; 9 | import users from "./users"; 10 | import configs from "./configs"; 11 | import tokenProvider from "./utils/tokenProvider"; 12 | import iaps from "./iaps"; 13 | import deviceTokens from "./deviceTokens"; 14 | 15 | const httpClient = (url, options = {}) => { 16 | if (!options.headers) { 17 | options.headers = new Headers({ Accept: "application/json" }); 18 | } 19 | const token = tokenProvider.getToken(); 20 | if (token) { 21 | const decodedToken = decodeJwt(token); 22 | const { exp } = decodedToken; 23 | const now = new Date(); 24 | if (now > (exp + 5) * 1000) { 25 | return tokenProvider.getRefreshedToken().then((gotFreshToken) => { 26 | if (gotFreshToken) { 27 | options.headers.set( 28 | "Authorization", 29 | `Bearer ${tokenProvider.getToken()}` 30 | ); 31 | } 32 | return fetchUtils.fetchJson(url, options); 33 | }); 34 | } else { 35 | options.headers.set("Authorization", `Bearer ${token}`); 36 | return fetchUtils.fetchJson(url, options); 37 | } 38 | } 39 | return fetchUtils.fetchJson(url, options); 40 | }; 41 | 42 | const API_URL = process.env.API_URL || ""; 43 | const dataProvider = restProvider(`${API_URL}/api/v1`, httpClient); 44 | const uploadCapableDataProvider = addUploadFeature(dataProvider); 45 | 46 | const App = () => ( 47 | 53 | 54 | 59 | 60 | 61 | 62 | ); 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "@mui/material/Card"; 3 | import CardContent from "@mui/material/CardContent"; 4 | import { Title } from "react-admin"; 5 | 6 | const NotFound = () => ( 7 | 8 | 9 | <CardContent> 10 | <h1>404: Page not found</h1> 11 | </CardContent> 12 | </Card> 13 | ); 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/addUploadFeature.js: -------------------------------------------------------------------------------- 1 | const addUploadFeature = (dataProvider) => ({ 2 | ...dataProvider, 3 | update: (resource, params) => { 4 | // console.log("resource=====", resource); 5 | if (resource !== "discoveries" && resource !== "tools") { 6 | // fallback to the default implementation 7 | return dataProvider.update(resource, params); 8 | } 9 | 10 | return uploadFile(params, "image", "avatar") 11 | .then((params) => dataProvider.update(resource, params)) 12 | .catch((error) => { 13 | console.error(error); 14 | throw new Error(error.message); 15 | }); 16 | }, 17 | create: (resource, params) => { 18 | // console.log("resource=====", resource); 19 | if (resource !== "discoveries" && resource !== "tools") { 20 | // fallback to the default implementation 21 | return dataProvider.create(resource, params); 22 | } 23 | console.log("resource=====", resource); 24 | return uploadFile(params, "image", "avatar") 25 | .then((params) => dataProvider.create(resource, params)) 26 | .catch((error) => { 27 | console.error(error); 28 | throw new Error(error.message); 29 | }); 30 | }, 31 | }); 32 | 33 | export const uploadFile = (params, field, name) => { 34 | return new Promise((resolve, reject) => { 35 | if (params.data[name] && params.data[name].rawFile instanceof File) { 36 | const formData = new FormData(); 37 | formData.append(field, params.data[name].rawFile); 38 | const token = localStorage.getItem("token"); 39 | 40 | fetch("/api/v1/uploads", { 41 | method: "post", 42 | headers: { 43 | Authorization: `Bearer ${token}`, 44 | }, 45 | body: formData, 46 | }) 47 | .then((response) => response.json()) 48 | .then((image) => { 49 | const tmp = { 50 | ...params, 51 | data: { 52 | ...params.data, 53 | [name]: { 54 | url: image.data.url, 55 | title: image.data.title, 56 | }, 57 | }, 58 | }; 59 | resolve(tmp); 60 | }) 61 | .catch((err) => reject(err)); 62 | } else { 63 | resolve(params); 64 | } 65 | }); 66 | }; 67 | 68 | export const uploadFiles = (params, field, name) => { 69 | return new Promise((resolve, reject) => { 70 | const newPictures = params.data[name]?.filter( 71 | (p) => p.rawFile instanceof File 72 | ); 73 | const formerPictures = params.data[name]?.filter( 74 | (p) => !(p.rawFile instanceof File) 75 | ); 76 | 77 | if (newPictures) { 78 | const formData = new FormData(); 79 | const token = localStorage.getItem("token"); 80 | newPictures?.map((p) => { 81 | formData.append(field, p.rawFile); 82 | }); 83 | fetch("/api/v1/uploads/multi", { 84 | method: "post", 85 | headers: { 86 | Authorization: `Bearer ${token}`, 87 | }, 88 | body: formData, 89 | }) 90 | .then((response) => response.json()) 91 | .then((images) => { 92 | const newUploadPictures = images.data.map((image) => { 93 | return { 94 | src: image.src, 95 | title: image.title, 96 | }; 97 | }); 98 | 99 | const tmp = { 100 | ...params, 101 | data: { 102 | ...params.data, 103 | [name]: [...formerPictures, ...newUploadPictures], 104 | }, 105 | }; 106 | resolve(tmp); 107 | }) 108 | .catch((err) => reject(err)); 109 | } else { 110 | resolve(params); 111 | } 112 | }); 113 | }; 114 | 115 | export default addUploadFeature; 116 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/authProvider.js: -------------------------------------------------------------------------------- 1 | import decodeJwt from "jwt-decode"; 2 | import tokenProvider from "./utils/tokenProvider"; 3 | 4 | const API_URL = process.env.API_URL || ""; 5 | 6 | const authProvider = { 7 | // authentication 8 | login: ({ username, password }) => { 9 | const request = new Request(`${API_URL}/api/v1/auth/login`, { 10 | method: "POST", 11 | body: JSON.stringify({ email: username, password }), 12 | headers: new Headers({ "Content-Type": "application/json" }), 13 | }); 14 | return fetch(request) 15 | .then((response) => { 16 | if (response.status < 200 || response.status >= 300) { 17 | throw new Error(response.message); 18 | } 19 | return response.json(); 20 | }) 21 | .then(({ data }) => { 22 | const { token } = data; 23 | tokenProvider.setToken(token); 24 | }) 25 | .catch(() => { 26 | throw new Error("Network error"); 27 | }); 28 | }, 29 | checkError: (error) => { 30 | const status = error.status; 31 | if (status === 401 || status === 403) { 32 | tokenProvider.removeToken(); 33 | return Promise.reject(); 34 | } 35 | // other error code (404, 500, etc): no need to log out 36 | return Promise.resolve(); 37 | }, 38 | checkAuth: () => { 39 | return tokenProvider.getToken() ? Promise.resolve() : Promise.reject(); 40 | }, 41 | logout: () => { 42 | tokenProvider.removeToken(); 43 | return Promise.resolve(); 44 | }, 45 | getIdentity: () => Promise.resolve(), 46 | // authorization 47 | getPermissions: () => { 48 | const token = tokenProvider.getToken(); 49 | if (token) { 50 | const decodedToken = decodeJwt(token); 51 | const role = decodedToken.role; 52 | return role ? Promise.resolve(role) : Promise.reject(); 53 | } 54 | return Promise.reject(); 55 | }, 56 | }; 57 | 58 | export default authProvider; 59 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/configs/ConfigCreate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Create, SimpleForm, TextInput } from "react-admin"; 3 | 4 | const ConfigCreate = (props) => ( 5 | <Create {...props}> 6 | <SimpleForm redirect="list"> 7 | <TextInput source="name" multiline fullWidth={true} /> 8 | <TextInput source="value" multiline fullWidth={true} /> 9 | </SimpleForm> 10 | </Create> 11 | ); 12 | 13 | export default ConfigCreate; 14 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/configs/ConfigEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Create, SimpleForm, TextInput } from "react-admin"; 3 | 4 | const ConfigCreate = () => ( 5 | <Create> 6 | <SimpleForm redirect="list"> 7 | <TextInput source="name" multiline fullWidth={true} /> 8 | <TextInput source="value" multiline fullWidth={true} /> 9 | </SimpleForm> 10 | </Create> 11 | ); 12 | 13 | export default ConfigCreate; 14 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/configs/ConfigList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { List, Datagrid, TextField, TextInput, EditButton } from "react-admin"; 3 | 4 | const configFilters = [ 5 | <TextInput key="1" label="Name" source="name" alwaysOn />, 6 | ]; 7 | 8 | const ConfigList = () => ( 9 | <List filters={configFilters}> 10 | <Datagrid rowClick="edit"> 11 | <TextField source="id" /> 12 | <TextField source="name" /> 13 | <TextField source="value" /> 14 | <EditButton /> 15 | </Datagrid> 16 | </List> 17 | ); 18 | 19 | export default ConfigList; 20 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/configs/index.js: -------------------------------------------------------------------------------- 1 | import ConfigList from "./ConfigList"; 2 | import ConfigEdit from "./ConfigEdit"; 3 | import ConfigCreate from "./ConfigCreate"; 4 | import SettingsIcon from "@mui/icons-material/Settings"; 5 | 6 | export default { 7 | list: ConfigList, 8 | edit: ConfigEdit, 9 | create: ConfigCreate, 10 | icon: SettingsIcon, 11 | }; 12 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/deviceTokens/DeviceTokenCreate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Create, SimpleForm, TextInput, required } from "react-admin"; 3 | 4 | const DeviceTokenCreate = () => ( 5 | <Create> 6 | <SimpleForm redirect="list"> 7 | <TextInput 8 | source="deviceId" 9 | multiline 10 | fullWidth={true} 11 | validate={required()} 12 | /> 13 | <TextInput source="platform" multiline fullWidth={true} /> 14 | <TextInput source="token" multiline fullWidth={true} /> 15 | </SimpleForm> 16 | </Create> 17 | ); 18 | 19 | export default DeviceTokenCreate; 20 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/deviceTokens/DeviceTokenEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Edit, SimpleForm, TextInput, required } from "react-admin"; 3 | 4 | const DeviceTokenEdit = () => ( 5 | <Edit> 6 | <SimpleForm redirect="list"> 7 | <TextInput 8 | source="deviceId" 9 | multiline 10 | fullWidth={true} 11 | validate={required()} 12 | /> 13 | <TextInput source="platform" multiline fullWidth={true} /> 14 | <TextInput source="token" multiline fullWidth={true} /> 15 | </SimpleForm> 16 | </Edit> 17 | ); 18 | 19 | export default DeviceTokenEdit; 20 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/deviceTokens/DeviceTokenList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { List, Datagrid, TextField, TextInput, EditButton } from "react-admin"; 3 | 4 | // eslint-disable-next-line react/jsx-key 5 | const deviceTokenFilters = [<TextInput label="Name" source="name" alwaysOn />]; 6 | 7 | const DeviceTokenList = () => ( 8 | <List filters={deviceTokenFilters} sort={{ field: "position", order: "ASC" }}> 9 | <Datagrid rowClick="edit"> 10 | <TextField source="id" /> 11 | <TextField source="deviceId" /> 12 | <TextField source="platform" /> 13 | <EditButton /> 14 | </Datagrid> 15 | </List> 16 | ); 17 | 18 | export default DeviceTokenList; 19 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/deviceTokens/index.js: -------------------------------------------------------------------------------- 1 | import DeviceTokenList from "./DeviceTokenList"; 2 | import DeviceTokenEdit from "./DeviceTokenEdit"; 3 | import DeviceTokenCreate from "./DeviceTokenCreate"; 4 | import CategoryIcon from "@mui/icons-material/Category"; 5 | 6 | export default { 7 | list: DeviceTokenList, 8 | edit: DeviceTokenEdit, 9 | create: DeviceTokenCreate, 10 | icon: CategoryIcon, 11 | }; 12 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/iaps/IapCreate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Create, SimpleForm, TextInput, required } from "react-admin"; 3 | 4 | const IapCreate = () => ( 5 | <Create> 6 | <SimpleForm redirect="list"> 7 | <TextInput 8 | source="deviceId" 9 | multiline 10 | fullWidth={true} 11 | validate={required()} 12 | /> 13 | <TextInput source="productId" multiline fullWidth={true} /> 14 | <TextInput source="environment" multiline fullWidth={true} /> 15 | <TextInput source="originalTransactionId" multiline fullWidth={true} /> 16 | <TextInput source="transactionId" multiline fullWidth={true} /> 17 | </SimpleForm> 18 | </Create> 19 | ); 20 | 21 | export default IapCreate; 22 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/iaps/IapEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Edit, SimpleForm, TextInput, required } from "react-admin"; 3 | 4 | const IapEdit = () => ( 5 | <Edit> 6 | <SimpleForm redirect="list"> 7 | <TextInput 8 | source="deviceId" 9 | multiline 10 | fullWidth={true} 11 | validate={required()} 12 | /> 13 | <TextInput source="productId" multiline fullWidth={true} /> 14 | <TextInput source="environment" multiline fullWidth={true} /> 15 | <TextInput source="originalTransactionId" multiline fullWidth={true} /> 16 | <TextInput source="transactionId" multiline fullWidth={true} /> 17 | </SimpleForm> 18 | </Edit> 19 | ); 20 | 21 | export default IapEdit; 22 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/iaps/IapList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { List, Datagrid, TextField, TextInput, EditButton } from "react-admin"; 3 | 4 | // eslint-disable-next-line react/jsx-key 5 | const iapFilters = [<TextInput label="Name" source="name" alwaysOn />]; 6 | 7 | const IapList = () => ( 8 | <List filters={iapFilters} sort={{ field: "position", order: "ASC" }}> 9 | <Datagrid rowClick="edit"> 10 | <TextField source="id" /> 11 | <TextField source="deviceId" /> 12 | <TextField source="productId" /> 13 | <TextField source="environment" /> 14 | <TextField source="originalTransactionId" /> 15 | <TextField source="transactionId" /> 16 | <EditButton /> 17 | </Datagrid> 18 | </List> 19 | ); 20 | 21 | export default IapList; 22 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/iaps/index.js: -------------------------------------------------------------------------------- 1 | import IapList from "./IapList"; 2 | import IapEdit from "./IapEdit"; 3 | import IapCreate from "./IapCreate"; 4 | import CategoryIcon from "@mui/icons-material/Category"; 5 | 6 | export default { 7 | list: IapList, 8 | edit: IapEdit, 9 | create: IapCreate, 10 | icon: CategoryIcon, 11 | }; 12 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 5 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import * as serviceWorker from "./serviceWorker"; 6 | 7 | const container = document.getElementById("root"); 8 | const root = createRoot(container); 9 | root.render(<App />); 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: http://bit.ly/CRA-PWA 14 | serviceWorker.unregister(); 15 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/logo.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"> 2 | <g fill="#61DAFB"> 3 | <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/> 4 | <circle cx="420.9" cy="296.5" r="45.7"/> 5 | <path d="M520.5 78.1z"/> 6 | </g> 7 | </svg> 8 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/restProvider.js: -------------------------------------------------------------------------------- 1 | import { fetchUtils } from "react-admin"; 2 | import { stringify } from "query-string"; 3 | 4 | export default (apiUrl, httpClient = fetchUtils.fetchJson) => ({ 5 | getList: (resource, params) => { 6 | const { page, perPage } = params.pagination; 7 | const { field, order } = params.sort; 8 | const _field = field === "id" ? "_id" : field; 9 | const query = { 10 | ...fetchUtils.flattenObject(params.filter), 11 | sort: order === "DESC" ? `-${_field}` : _field, 12 | page: page, 13 | limit: perPage, 14 | }; 15 | const url = `${apiUrl}/${resource}?${stringify(query)}`; 16 | 17 | return httpClient(url).then(({ json }) => ({ 18 | data: json.data.map((resource) => ({ ...resource, id: resource._id })), 19 | total: json.total, 20 | })); 21 | }, 22 | 23 | getOne: (resource, params) => 24 | httpClient(`${apiUrl}/${resource}/${params.id}`).then(({ json }) => ({ 25 | data: { ...json.data, id: json.data._id }, 26 | })), 27 | 28 | getMany: (resource, params) => { 29 | const query = { 30 | filter: JSON.stringify({ _id: { $in: params.ids } }), 31 | }; 32 | const url = `${apiUrl}/${resource}?${stringify(query)}`; 33 | return httpClient(url).then(({ json }) => ({ 34 | data: json.data.map((resource) => ({ ...resource, id: resource._id })), 35 | total: json.total, 36 | })); 37 | }, 38 | 39 | getManyReference: (resource, params) => { 40 | const { page, perPage } = params.pagination; 41 | const { field, order } = params.sort; 42 | const _field = field === "id" ? "_id" : field; 43 | const query = { 44 | sort: order === "DESC" ? `-${_field}` : _field, 45 | page: page, 46 | limit: perPage, 47 | filter: JSON.stringify({ 48 | ...params.filter, 49 | [params.target]: params.id, 50 | }), 51 | }; 52 | const url = `${apiUrl}/${resource}?${stringify(query)}`; 53 | 54 | return httpClient(url).then(({ json }) => ({ 55 | data: json.data.map((resource) => ({ ...resource, id: resource._id })), 56 | total: json.total, 57 | })); 58 | }, 59 | 60 | create: (resource, params) => 61 | httpClient(`${apiUrl}/${resource}`, { 62 | method: "POST", 63 | body: JSON.stringify(params.data), 64 | }).then(({ json }) => ({ 65 | data: { ...params.data, id: json._id }, 66 | })), 67 | 68 | update: (resource, params) => 69 | httpClient(`${apiUrl}/${resource}/${params.id}`, { 70 | method: "PUT", 71 | body: JSON.stringify(params.data), 72 | }).then(({ json }) => ({ data: { ...json.data, id: json.data._id } })), 73 | 74 | updateMany: (resource, params) => { 75 | return Promise.all( 76 | params.ids.map((id) => 77 | httpClient(`${apiUrl}/${resource}/${id}`, { 78 | method: "PUT", 79 | body: JSON.stringify(params.data), 80 | }) 81 | ) 82 | ).then((responses) => ({ 83 | data: responses.map((response) => response.json), 84 | })); 85 | }, 86 | 87 | delete: (resource, params) => 88 | httpClient(`${apiUrl}/${resource}/${params.id}`, { 89 | method: "DELETE", 90 | }).then(({ json }) => ({ data: { ...params.data, id: json.data._id } })), 91 | 92 | deleteMany: (resource, params) => { 93 | return Promise.all( 94 | params.ids.map((id) => 95 | httpClient(`${apiUrl}/${resource}/${id}`, { 96 | method: "DELETE", 97 | }) 98 | ) 99 | ).then((responses) => ({ 100 | data: responses.map((response) => response.json), 101 | })); 102 | }, 103 | }); 104 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/users/MyUrlField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { withStyles } from "@material-ui/core/styles"; 3 | import LaunchIcon from "@material-ui/icons/Launch"; 4 | 5 | const styles = { 6 | link: { 7 | textDecoration: "none", 8 | }, 9 | icon: { 10 | width: "0.5em", 11 | paddingLeft: 2, 12 | }, 13 | }; 14 | 15 | const MyUrlField = ({ record = {}, source, classes }) => ( 16 | <a href={record[source]} className={classes.link}> 17 | {record[source]} 18 | <LaunchIcon className={classes.icon} /> 19 | </a> 20 | ); 21 | 22 | export default withStyles(styles)(MyUrlField); 23 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/users/UserEdit.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Edit, SimpleForm, TextInput, required } from "react-admin"; 3 | 4 | import { validateEmail } from "../validates"; 5 | 6 | const UserTitle = ({ record }) => { 7 | return <span>User {record ? record.username : ""}</span>; 8 | }; 9 | 10 | const UserEdit = () => ( 11 | <Edit title={<UserTitle />}> 12 | <SimpleForm> 13 | <TextInput disabled source="id" /> 14 | <TextInput source="username" /> 15 | <TextInput source="fullName" /> 16 | <TextInput source="email" validate={[validateEmail, required()]} /> 17 | <TextInput source="role" validate={required()} /> 18 | </SimpleForm> 19 | </Edit> 20 | ); 21 | 22 | export default UserEdit; 23 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/users/UserList.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { List, Datagrid, TextField, EmailField, TextInput } from "react-admin"; 3 | 4 | const userFilters = [ 5 | <TextInput key="1" label="Search" source="email" alwaysOn />, 6 | ]; 7 | 8 | const UserList = () => ( 9 | <List filters={userFilters}> 10 | <Datagrid rowClick="edit"> 11 | <TextField source="id" /> 12 | <TextField source="fullName" /> 13 | <EmailField source="email" /> 14 | <TextField source="role" /> 15 | </Datagrid> 16 | </List> 17 | ); 18 | 19 | export default UserList; 20 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/users/index.js: -------------------------------------------------------------------------------- 1 | import UserIcon from "@mui/icons-material/Group"; 2 | import UserList from "./UserList"; 3 | import UserEdit from "./UserEdit"; 4 | 5 | export default { 6 | list: UserList, 7 | edit: UserEdit, 8 | icon: UserIcon, 9 | }; 10 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/utils/fetch.js: -------------------------------------------------------------------------------- 1 | import { HttpError } from "react-admin"; 2 | 3 | export const createHeadersFromOptions = (options) => { 4 | const requestHeaders = options.headers || { 5 | Accept: "application/json", 6 | }; 7 | 8 | if ( 9 | !requestHeaders.has("Content-Type") && 10 | !(options && (!options.method || options.method === "GET")) && 11 | !(options && options.body && options.body instanceof FormData) 12 | ) { 13 | requestHeaders.set("Content-Type", "application/json"); 14 | } 15 | if (options.user && options.user.authenticated && options.user.token) { 16 | requestHeaders.set("Authorization", options.user.token); 17 | } 18 | 19 | return requestHeaders; 20 | }; 21 | 22 | export const fetchJson = (url, options = {}) => { 23 | const requestHeaders = createHeadersFromOptions(options); 24 | return fetch(url, { ...options, headers: requestHeaders }) 25 | .then((response) => 26 | response.text().then((text) => ({ 27 | status: response.status, 28 | statusText: response.statusText, 29 | headers: response.headers, 30 | body: text, 31 | })) 32 | ) 33 | .then(({ status, statusText, headers, body }) => { 34 | let json; 35 | try { 36 | json = JSON.parse(body); 37 | } catch (e) { 38 | // console.log("=====fetchJson=error===", e); 39 | // not json, no big deal 40 | } 41 | if (status < 200 || status >= 300) { 42 | // console.log("=====fetchJson=error===dddd"); 43 | return Promise.reject( 44 | new HttpError( 45 | (json && json?.error?.message) || statusText, 46 | status, 47 | json 48 | ) 49 | ); 50 | } 51 | return Promise.resolve({ status, headers, body, json }); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/utils/tokenProvider.js: -------------------------------------------------------------------------------- 1 | const tokenProvider = () => { 2 | const getRefreshedToken = () => { 3 | const refreshEndpoint = "/api/v1/auth/refresh-token"; 4 | const request = new Request(refreshEndpoint, { 5 | method: "POST", 6 | headers: new Headers({ "Content-Type": "application/json" }), 7 | credentials: "include", 8 | }); 9 | return fetch(request) 10 | .then((response) => { 11 | if (response.status !== 200) { 12 | removeToken(); 13 | return false; 14 | } 15 | return response.json(); 16 | }) 17 | .then(({ data }) => { 18 | if (data && data.token) { 19 | setToken(data.token); 20 | return true; 21 | } 22 | 23 | return false; 24 | }); 25 | }; 26 | 27 | const setToken = (token) => { 28 | // const decodedToken = decodeJwt(token); 29 | localStorage.setItem("token", token); 30 | return true; 31 | }; 32 | 33 | const removeToken = () => { 34 | localStorage.removeItem("token"); 35 | return true; 36 | }; 37 | 38 | const getToken = () => { 39 | const token = localStorage.getItem("token"); 40 | return token; 41 | }; 42 | 43 | return { 44 | getRefreshedToken, 45 | getToken, 46 | setToken, 47 | removeToken, 48 | }; 49 | }; 50 | 51 | export default tokenProvider(); 52 | -------------------------------------------------------------------------------- /generators/app/templates/frontend/src/validates/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | required, 3 | minLength, 4 | maxLength, 5 | minValue, 6 | maxValue, 7 | number, 8 | regex, 9 | email, 10 | choices, 11 | } from "react-admin"; 12 | 13 | const validateFirstName = [required(), minLength(2), maxLength(15)]; 14 | const validateEmail = email(); 15 | const validateAge = [number(), minValue(18), maxValue(100)]; 16 | const validateZipCode = regex(/^\d{5}$/, "Must be a valid Zip Code"); 17 | const validateSex = choices(["M", "F"], "Must be Male or Female"); 18 | 19 | export { 20 | validateAge, 21 | validateEmail, 22 | validateFirstName, 23 | validateZipCode, 24 | validateSex, 25 | }; 26 | -------------------------------------------------------------------------------- /generators/app/templates/scripts/deploy.production.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd <%=project_slug%> 3 | git pull 4 | echo ${CI_REGISTRY_PASSWORD} | docker login $CI_REGISTRY --username $CI_REGISTRY_USERNAME --password-stdin 5 | docker pull ${CI_REGISTRY_IMAGE}:latest 6 | docker compose -f docker-compose.prod.yml up -d 7 | docker image prune -f -------------------------------------------------------------------------------- /generators/app/templates/scripts/deploy.staging.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd <%=project_slug%> 3 | git pull 4 | # docker compose -f docker-compose.prod.yml build 5 | echo ${CI_REGISTRY_PASSWORD} | docker login $CI_REGISTRY --username $CI_REGISTRY_USERNAME --password-stdin 6 | docker pull ${CI_REGISTRY_IMAGE}:staging 7 | docker compose -f docker-compose.staging.yml up -d 8 | docker image prune -f -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-expressjs-rest", 3 | "version": "1.7.5", 4 | "description": "yo template for an ExpressJS application", 5 | "main": "generators/index.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "files": [ 10 | "generators" 11 | ], 12 | "keywords": [ 13 | "yeoman-generator", 14 | "nodejs", 15 | "expressjs", 16 | "babel", 17 | "docker", 18 | "swagger", 19 | "admin-dashboard", 20 | "api", 21 | "passport" 22 | ], 23 | "scripts": { 24 | "test": "NODE_ENV=test mocha --exit ./tests", 25 | "patch": "npm version patch && npm publish --access public", 26 | "minor": "npm version minor && npm publish --access public" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://minhuyen@github.com/minhuyen/generate-expressjs-api.git" 31 | }, 32 | "author": "Uyen Do", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/minhuyen/generate-expressjs-api/issues" 36 | }, 37 | "homepage": "https://github.com/minhuyen/generate-expressjs-api#readme", 38 | "dependencies": { 39 | "lodash": "^4.17.15", 40 | "pluralize": "^8.0.0", 41 | "yeoman-generator": "^5.8.0" 42 | }, 43 | "devDependencies": { 44 | "mocha": "^9.1.4", 45 | "semantic-release": "^24.0.0", 46 | "yeoman-assert": "^3.1.1", 47 | "yeoman-environment": "^3.5.1", 48 | "yeoman-test": "^6.2.0" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | var helplers = require("yeoman-test"); 2 | var assert = require("yeoman-assert"); 3 | var path = require("path"); 4 | 5 | describe("expressjs-rest:app", function() { 6 | it("generate a project", function() { 7 | return helplers 8 | .run(path.join(__dirname, "../generators/app")) 9 | .withOptions({ foo: "bar" }) 10 | .withArguments(["name-x"]) 11 | .withPrompts({ name: "test" }) 12 | .withLocalConfig({ lang: "en" }) 13 | .then(function() { 14 | assert.file("test/README.md"); 15 | }); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/test_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # this is a very simple script that tests the docker configuration for cookiecutter-django 3 | # it is meant to be run from the root directory of the repository, eg: 4 | # sh tests/test_docker.sh 5 | 6 | set -o errexit 7 | set -x 8 | 9 | 10 | # create a cache directory 11 | mkdir -p .cache/docker 12 | cd .cache/docker 13 | 14 | 15 | # create the project using the default settings in cookiecutter.json 16 | yo ../../generators/app --no-input 17 | 18 | cd awesome-express-project 19 | 20 | cp .env.example .env 21 | 22 | # run the project's tests 23 | docker compose -f docker-compose.test.yml build 24 | docker compose -f docker-compose.test.yml run --rm backend npm run test 25 | docker compose -f docker-compose.test.yml down 26 | --------------------------------------------------------------------------------