├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitignore ├── .gitlab-ci.yml ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── docker-compose.dev.yml ├── docker-compose.stag.yml ├── docker-compose.yml ├── ecs ├── deploy.sh ├── ecs-params.dev.yml ├── ecs-params.stag.yml ├── envs.js ├── rds-combined-ca-bundle.pem ├── secret-access.json └── task-execution-assume-role.json ├── metrics ├── grafana-dashboard.json └── prometheus │ ├── alert.yml │ └── prometheus.yml ├── nodemon.json ├── package-lock.json ├── package.json ├── run.sh ├── server ├── index.html ├── server.pem └── simple-https-server.py ├── src ├── app │ ├── _helpers │ │ ├── database │ │ │ ├── deep-partial.ts │ │ │ ├── index.ts │ │ │ ├── repository.interface.ts │ │ │ └── typeorm-filter.mapper.ts │ │ ├── decorators │ │ │ ├── index.ts │ │ │ ├── profile.decorator.ts │ │ │ └── user.decorator.ts │ │ ├── entity │ │ │ ├── extended-entity.ts │ │ │ └── index.ts │ │ ├── facebook │ │ │ └── facebook-token.strategy.js │ │ ├── filters │ │ │ ├── http-exception.filter.ts │ │ │ ├── index.ts │ │ │ └── twig-exception.filter.ts │ │ ├── graphql │ │ │ ├── gql-config.service.ts │ │ │ ├── graphql.guard.ts │ │ │ ├── index.ts │ │ │ └── user.decorator.ts │ │ ├── index.ts │ │ ├── mail │ │ │ └── index.ts │ │ ├── middleware │ │ │ └── request-context.middleware.ts │ │ ├── push │ │ │ └── index.ts │ │ ├── request-context.ts │ │ ├── rest.exception.ts │ │ └── sms │ │ │ ├── SMSType.enum.ts │ │ │ ├── index.ts │ │ │ └── sms-options.interface.ts │ ├── app.dispatcher.ts │ ├── app.logger.ts │ ├── app.module.ts │ ├── auth │ │ ├── auth-error.enum.ts │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── dto │ │ │ ├── credentials.dto.ts │ │ │ ├── facebook-token.dto.ts │ │ │ ├── jwt.dto.ts │ │ │ ├── password-reset.dto.ts │ │ │ ├── password-token.dto.ts │ │ │ ├── refresh-token.dto.ts │ │ │ ├── token.dto.ts │ │ │ ├── user-entity.dto.ts │ │ │ ├── verify-resend.dto.ts │ │ │ └── verify-token.dto.ts │ │ ├── interfaces │ │ │ ├── facebook-profile.interface.ts │ │ │ └── jwt-payload.inteface.ts │ │ ├── jwt │ │ │ └── index.ts │ │ └── stategies │ │ │ ├── facebook-token.strategy.ts │ │ │ ├── index.ts │ │ │ └── jwt.strategy.ts │ ├── contract │ │ ├── contract.constants.ts │ │ ├── contract.graphql │ │ ├── contract.module.ts │ │ ├── contract.providers.ts │ │ ├── contract.resolver.ts │ │ ├── contract.service.ts │ │ ├── entity │ │ │ ├── contract.entity.ts │ │ │ └── index.ts │ │ └── security │ │ │ └── contract.voter.ts │ ├── database │ │ ├── database.constants.ts │ │ ├── database.module.ts │ │ └── database.providers.ts │ ├── graphql.schema.ts │ ├── healtcheck │ │ ├── healthcheck.controller.ts │ │ └── healthcheck.module.ts │ ├── home-favorite │ │ ├── dto │ │ │ ├── create-home-favorites.dto.ts │ │ │ ├── delete-home-favorites.dto.ts │ │ │ └── index.ts │ │ ├── entity │ │ │ ├── home-favorite.entity.ts │ │ │ └── index.ts │ │ ├── home-favorite.constants.ts │ │ ├── home-favorite.controller.ts │ │ ├── home-favorite.graphql │ │ ├── home-favorite.guard.ts │ │ ├── home-favorite.module.ts │ │ ├── home-favorite.providers.ts │ │ ├── home-favorite.resolver.ts │ │ ├── home-favorite.service.ts │ │ ├── home-favorite.validator.ts │ │ └── security │ │ │ └── home-favorite.voter.ts │ ├── home-media │ │ ├── entity │ │ │ ├── home-media.entity.ts │ │ │ └── index.ts │ │ ├── home-media.constants.ts │ │ ├── home-media.controller.ts │ │ ├── home-media.graphql │ │ ├── home-media.module.ts │ │ ├── home-media.providers.ts │ │ ├── home-media.resolver.ts │ │ ├── home-media.service.ts │ │ └── security │ │ │ └── home-media.voter.ts │ ├── home │ │ ├── attom-data-api.service.ts │ │ ├── attom │ │ │ └── property-api.ts │ │ ├── dto │ │ │ ├── condition-score.dto.ts │ │ │ ├── convert-template.dto.ts │ │ │ ├── create-home.dto.ts │ │ │ ├── delete-home.dto.ts │ │ │ ├── index.ts │ │ │ └── update-home.dto.ts │ │ ├── entity │ │ │ ├── home-pdf.entity.ts │ │ │ ├── home.entity.ts │ │ │ └── index.ts │ │ ├── foxyai.graphql │ │ ├── foxyai.service.ts │ │ ├── home-error.enum.ts │ │ ├── home-pdf.service.ts │ │ ├── home.command.ts │ │ ├── home.constants.ts │ │ ├── home.controller.ts │ │ ├── home.graphql │ │ ├── home.guard.ts │ │ ├── home.module.ts │ │ ├── home.providers.ts │ │ ├── home.resolver.ts │ │ ├── home.service.ts │ │ ├── home.validator.ts │ │ ├── pipe │ │ │ └── home.pipe.ts │ │ └── security │ │ │ └── home.voter.ts │ ├── index.ts │ ├── media │ │ ├── dto │ │ │ └── media-upload.dto.ts │ │ ├── media.constants.ts │ │ ├── media.controller.ts │ │ ├── media.module.ts │ │ └── multer-config.service.ts │ ├── message │ │ ├── date.scalar.ts │ │ ├── entity │ │ │ ├── conversation.entity.ts │ │ │ ├── index.ts │ │ │ ├── message.entity.ts │ │ │ └── user-conversation.entity.ts │ │ ├── message.buffer.ts │ │ ├── message.constants.ts │ │ ├── message.controller.ts │ │ ├── message.cron.ts │ │ ├── message.graphql │ │ ├── message.module.ts │ │ ├── message.providers.ts │ │ ├── resolvers │ │ │ ├── conversation.resolver.ts │ │ │ ├── message.resolver.ts │ │ │ └── user-conversation.resolver.ts │ │ ├── security │ │ │ ├── message.voter.ts │ │ │ └── user-conversation.voter.ts │ │ └── services │ │ │ ├── conversation.service.ts │ │ │ ├── message.service.ts │ │ │ ├── subscriptions.service.ts │ │ │ └── user-conversation.service.ts │ ├── security │ │ ├── access-decision │ │ │ ├── access-decision-manager.interface.ts │ │ │ ├── access-decision-manager.ts │ │ │ ├── access-decision-strategy.enum.ts │ │ │ └── index.ts │ │ ├── authorization-checker │ │ │ ├── authorization-checker.interface.ts │ │ │ ├── authorization-checker.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── security.module.ts │ │ ├── security.service.ts │ │ └── voter │ │ │ ├── access.enum.ts │ │ │ ├── index.ts │ │ │ ├── rest-voter-action.enum.ts │ │ │ ├── voter-registry.ts │ │ │ ├── voter.interface.ts │ │ │ └── voter.ts │ └── user │ │ ├── dto │ │ ├── delete-user.dto.ts │ │ ├── index.ts │ │ ├── subscription.dto.ts │ │ └── update-user.dto.ts │ │ ├── entity │ │ ├── index.ts │ │ ├── social.entity.ts │ │ ├── user-email.entity.ts │ │ ├── user-subscription.entity.ts │ │ └── user.entity.ts │ │ ├── index.ts │ │ ├── online.service.ts │ │ ├── user-error.enum.ts │ │ ├── user-subscription.service.ts │ │ ├── user.command.ts │ │ ├── user.constants.ts │ │ ├── user.controller.ts │ │ ├── user.graphql │ │ ├── user.module.ts │ │ ├── user.providers.ts │ │ ├── user.resolver.ts │ │ ├── user.service.ts │ │ └── user.validator.ts ├── assets │ ├── mail │ │ ├── message_new.twig │ │ ├── password_new.twig │ │ ├── password_reset.twig │ │ ├── registration.twig │ │ └── welcome.twig │ └── media │ │ └── templates │ │ └── brochure_01.html ├── base │ ├── crud.service.ts │ ├── index.ts │ └── rest.controller.ts ├── cli.ts ├── config │ └── index.ts ├── index.ts └── migrations │ ├── 1554826342372-UserConversation.ts │ ├── 1555006181991-Home.ts │ ├── 1555006376049-HomeFavorite.ts │ └── 1555332883287-Conversation.ts ├── tsconfig.json └── tslint.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff 7 | .idea/ 8 | ### VisualStudioCode template 9 | .vscode/ 10 | 11 | ### Node template 12 | # Logs 13 | logs 14 | *.log 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | # Runtime data 20 | pids 21 | *.pid 22 | *.seed 23 | *.pid.lock 24 | 25 | # Directory for instrumented libs generated by jscoverage/JSCover 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | coverage 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # TypeScript v1 declaration files 51 | typings/ 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | 71 | # next.js build output 72 | .next 73 | 74 | dist/ 75 | ecs/ 76 | metrics/ 77 | server/ 78 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | 12 | [*.{ts,html,css}] 13 | indent_style = tab 14 | indent_size = 2 15 | 16 | [*.json] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.twig] 21 | indent_style = tab 22 | indent_size = 2 23 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | APP_UUID= 3 | APP_SALT= 4 | APP_MAIL_FROM= 5 | APP_SESSION_DOMAIN= 6 | APP_SESSION_SECRET= 7 | APP_SESSION_TIMEOUT= 8 | APP_SESSION_REFRESH_SECRET= 9 | APP_SESSION_REFRESH_TIMEOUT= 10 | APP_SESSION_PASSWORD_RESET_SECRET= 11 | APP_SESSION_PASSWORD_RESET_TIMEOUT= 12 | APP_SESSION_VERIFY_SECRET= 13 | APP_SESSION_VERIFY_TIMEOUT= 14 | APP_FACEBOOK_APP_ID= 15 | APP_FACEBOOK_APP_SECRET= 16 | APP_AWS_API_KEY= 17 | APP_AWS_SECRET_KEY= 18 | APP_AWS_REGION= 19 | APP_AWS_S3_BUCKET_NAME= 20 | APP_AWS_PINPOINT_SMTP_HOST= 21 | APP_AWS_PINPOINT_SMTP_PORT= 22 | APP_AWS_PINPOINT_SMTP_USER= 23 | APP_AWS_PINPOINT_SMTP_SECRET= 24 | APP_PORT= 25 | APP_HOST= 26 | APP_LOGGER_LEVEL= 27 | APP_HOME_API_ATTOM_DATA_API_KEY= 28 | APP_HOME_API_FOXYAI_API_KEY= 29 | APP_GOOGLE_API_SECRET= 30 | APP_API2PDF_SECRET= 31 | APP_DATABASE_SECRET_URL= 32 | APP_FIREBASE_SECRET= 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | .env 4 | .env.stag 5 | .env.dev 6 | dist/ 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ## The builder 2 | 3 | FROM node:12-alpine as builder 4 | 5 | WORKDIR /usr/src/app 6 | 7 | ENV OPENCOLLECTIVE_HIDE=1 8 | ENV SUPPRESS_SUPPORT=1 9 | 10 | COPY package.json package-lock.json ./ 11 | 12 | RUN npm ci --loglevel error 13 | 14 | COPY . . 15 | 16 | RUN npm run build 17 | 18 | 19 | ## The cleaner 20 | 21 | FROM node:12-alpine as cleaner 22 | 23 | WORKDIR /usr/src/app 24 | 25 | COPY --from=builder /usr/src/app . 26 | 27 | RUN npm prune --production 28 | 29 | 30 | ## Output image 31 | 32 | FROM node:12-alpine 33 | 34 | LABEL maintainer="Przemysław Czekaj " 35 | 36 | HEALTHCHECK CMD curl -f http://localhost/healthcheck || exit 1 37 | 38 | RUN apk add --update curl 39 | 40 | WORKDIR /usr/src/app 41 | 42 | COPY --from=cleaner /usr/src/app . 43 | 44 | EXPOSE 80 45 | 46 | CMD [ "npm", "run", "prod" ] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 NEOTERIC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd $(dirname $0) 3 | docker build -t threeleaf-backend . 4 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | threeleaf-backend: 4 | image: 180122916419.dkr.ecr.eu-west-1.amazonaws.com/threeleaf:develop 5 | ports: 6 | - "80:80" 7 | logging: 8 | driver: awslogs 9 | options: 10 | awslogs-group: threeleaf 11 | awslogs-region: us-east-2 12 | awslogs-stream-prefix: threeleaf 13 | -------------------------------------------------------------------------------- /docker-compose.stag.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | threeleaf-stag: 4 | image: 391232177924.dkr.ecr.us-east-2.amazonaws.com/threeleaf-stag:master 5 | ports: 6 | - "80:80" 7 | logging: 8 | driver: awslogs 9 | options: 10 | awslogs-group: threeleaf 11 | awslogs-region: us-east-2 12 | awslogs-stream-prefix: threeleaf 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.0" 2 | services: 3 | mongo: 4 | image: mongo 5 | ports: 6 | - "127.0.0.1:27017:27017" 7 | -------------------------------------------------------------------------------- /ecs/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | -------------------------------------------------------------------------------- /ecs/ecs-params.dev.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | task_definition: 3 | task_execution_role: ecsTaskExecutionRole 4 | ecs_network_mode: awsvpc 5 | task_size: 6 | mem_limit: 0.5GB 7 | cpu_limit: 256 8 | services: 9 | threeleaf-backend: 10 | secrets: 11 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/node_env 12 | name: NODE_ENV 13 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_uuid 14 | name: APP_UUID 15 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_salt 16 | name: APP_SALT 17 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_mail_from 18 | name: APP_MAIL_FROM 19 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_domain 20 | name: APP_SESSION_DOMAIN 21 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_secret 22 | name: APP_SESSION_SECRET 23 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_timeout 24 | name: APP_SESSION_TIMEOUT 25 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_refresh_secret 26 | name: APP_SESSION_REFRESH_SECRET 27 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_refresh_timeout 28 | name: APP_SESSION_REFRESH_TIMEOUT 29 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_password_reset_secret 30 | name: APP_SESSION_PASSWORD_RESET_SECRET 31 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_password_reset_timeout 32 | name: APP_SESSION_PASSWORD_RESET_TIMEOUT 33 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_verify_secret 34 | name: APP_SESSION_VERIFY_SECRET 35 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_session_verify_timeout 36 | name: APP_SESSION_VERIFY_TIMEOUT 37 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_facebook_app_id 38 | name: APP_FACEBOOK_APP_ID 39 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_facebook_app_secret 40 | name: APP_FACEBOOK_APP_SECRET 41 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_api_key 42 | name: APP_AWS_API_KEY 43 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_secret_key 44 | name: APP_AWS_SECRET_KEY 45 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_region 46 | name: APP_AWS_REGION 47 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_s3_bucket_name 48 | name: APP_AWS_S3_BUCKET_NAME 49 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_pinpoint_smtp_host 50 | name: APP_AWS_PINPOINT_SMTP_HOST 51 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_pinpoint_smtp_port 52 | name: APP_AWS_PINPOINT_SMTP_PORT 53 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_pinpoint_smtp_user 54 | name: APP_AWS_PINPOINT_SMTP_USER 55 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_aws_pinpoint_smtp_secret 56 | name: APP_AWS_PINPOINT_SMTP_SECRET 57 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_port 58 | name: APP_PORT 59 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_host 60 | name: APP_HOST 61 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_logger_level 62 | name: APP_LOGGER_LEVEL 63 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_home_api_attom_data_api_key 64 | name: APP_HOME_API_ATTOM_DATA_API_KEY 65 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_google_api_secret 66 | name: APP_GOOGLE_API_SECRET 67 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_api2pdf_secret 68 | name: APP_API2PDF_SECRET 69 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_database_secret_url 70 | name: APP_DATABASE_SECRET_URL 71 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_home_api_foxyai_api_key 72 | name: APP_HOME_API_FOXYAI_API_KEY 73 | - value_from: arn:aws:ssm:us-east-2:180122916419:parameter/dev/threeleaf/app_firebase_secret 74 | name: APP_FIREBASE_SECRET 75 | run_params: 76 | network_configuration: 77 | awsvpc_configuration: 78 | subnets: 79 | - "subnet-003ca58b53fce6ff1" 80 | - "subnet-0b33d7fcc07d0e794" 81 | security_groups: 82 | - "sg-03b4344e485235887" 83 | assign_public_ip: ENABLED 84 | -------------------------------------------------------------------------------- /ecs/envs.js: -------------------------------------------------------------------------------- 1 | const args = process.argv.slice(2); 2 | const allowed_envs = ['dev', 'stag', 'prod']; 3 | 4 | let env = args[0]; 5 | const keyId = args[1]; 6 | 7 | if (!env) { 8 | console.error(`Please specify environment: ${allowed_envs}`); 9 | process.exit(1); 10 | } else { 11 | if (allowed_envs.indexOf(env) === -1) { 12 | console.error(`Only allowed envs are: ${allowed_envs}`); 13 | process.exit(1); 14 | } 15 | } 16 | 17 | const fs = require('fs'); 18 | const AWS = require('aws-sdk'); 19 | 20 | const envs = fs.readFileSync(`${__dirname}/../.env.${env}`, 'utf8'); 21 | const credentials = new AWS.SharedIniFileCredentials({profile: `threeleaf_${env}`}); 22 | 23 | AWS.config.update({ 24 | credentials, 25 | region: 'us-east-2' 26 | }); 27 | const ssm = new AWS.SSM(); 28 | 29 | if (!keyId) { 30 | // https://aws.amazon.com/blogs/compute/managing-secrets-for-amazon-ecs-applications-using-parameter-store-and-iam-roles-for-tasks/ 31 | console.error('Missing KeyId as argument for decryption'); 32 | process.exit(1); 33 | } 34 | 35 | for (const line of envs.split('\n')) { 36 | if (!line) continue; 37 | const idx = line.indexOf('='); 38 | let key = line.substr(0, idx); 39 | let val = line.substr(idx+1, line.length).trim(); 40 | const isSecret = key.indexOf('SECRET') !== -1; 41 | if (key === 'APP_PORT') { 42 | val = ''+80; 43 | } else if (key === 'APP_HOST') { 44 | val = '0.0.0.0'; 45 | } else if (key === 'APP_AWS_REGION') { 46 | val = 'us-east-2'; 47 | } 48 | const params = { 49 | Name: `/${env}/threeleaf/${key.toLocaleLowerCase()}`, 50 | Type: isSecret ? 'SecureString' : 'String', 51 | Value: val, 52 | Overwrite: true 53 | }; 54 | if (isSecret) { 55 | params.KeyId = keyId; 56 | } 57 | ssm.putParameter(params, (err, data) => { 58 | if (err) { 59 | console.error(err); 60 | } else { 61 | console.log(`${key} updated wit value`, params.Value); 62 | } 63 | }); 64 | } 65 | 66 | -------------------------------------------------------------------------------- /ecs/secret-access.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Action": [ 7 | "ssm:DescribeParameters" 8 | ], 9 | "Resource": "*" 10 | }, 11 | { 12 | "Sid": "Stmt1482841904000", 13 | "Effect": "Allow", 14 | "Action": [ 15 | "ssm:GetParameters" 16 | ], 17 | "Resource": [ 18 | "arn:aws:ssm:${REGION}:${USER_ID}:parameter/${ENV}/threeleaf/*" 19 | ] 20 | }, 21 | { 22 | "Sid": "Stmt1482841948000", 23 | "Effect": "Allow", 24 | "Action": [ 25 | "kms:Decrypt" 26 | ], 27 | "Resource": [ 28 | "${AWS_KMS_ARN}" 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /ecs/task-execution-assume-role.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Sid": "", 6 | "Effect": "Allow", 7 | "Principal": { 8 | "Service": [ 9 | "ec2.amazonaws.com", 10 | "ecs-tasks.amazonaws.com", 11 | "ecs.amazonaws.com" 12 | ] 13 | }, 14 | "Action": "sts:AssumeRole" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /metrics/prometheus/alert.yml: -------------------------------------------------------------------------------- 1 | groups: 2 | - name: response 3 | rules: 4 | - alert: APIHighMedianResponseTime 5 | expr: histogram_quantile(0.5, sum(rate(http_request_duration_ms_bucket[1m])) by (le, service, route, method)) > 100 6 | for: 60s 7 | labels: 8 | severity: page 9 | annotations: 10 | summary: "High median response time on {{ $labels.service }} and {{ $labels.method }} {{ $labels.route }}" 11 | description: "{{ $labels.service }}, {{ $labels.method }} {{ $labels.route }} has a median response time above 100ms (current value: {{ $value }}ms)" 12 | -------------------------------------------------------------------------------- /metrics/prometheus/prometheus.yml: -------------------------------------------------------------------------------- 1 | # my global config 2 | global: 3 | scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. 4 | evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. 5 | # scrape_timeout is set to the global default (10s). 6 | 7 | # Alertmanager configuration 8 | alerting: 9 | alertmanagers: 10 | - static_configs: 11 | - targets: 12 | # - alertmanager:9093 13 | 14 | # Load rules once and periodically evaluate them according to the global 'evaluation_interval'. 15 | rule_files: 16 | - "/etc/prometheus/alert.yml" 17 | # - "first_rules.yml" 18 | # - "second_rules.yml" 19 | 20 | # A scrape configuration containing exactly one endpoint to scrape: 21 | # Here it's Prometheus itself. 22 | scrape_configs: 23 | # The job name is added as a label `job=` to any timeseries scraped from this config. 24 | - job_name: 'prometheus' 25 | 26 | # metrics_path defaults to '/metrics' 27 | # scheme defaults to 'http'. 28 | 29 | static_configs: 30 | - targets: ['172.23.0.1:1337'] 31 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node -r dotenv-safe/config src/index.ts" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ava", 3 | "version": "0.10.2", 4 | "description": "AVA - The new agent in town", 5 | "main": "dist/index.js", 6 | "repository": "git@github.com:neoteric-eu/nestjs-auth.git", 7 | "scripts": { 8 | "start": "node -r ts-node/register -r dotenv-safe/config src/index.ts", 9 | "cli": "node -r dotenv-safe/config -r ts-node/register src/cli.ts", 10 | "watch": "nodemon", 11 | "prebuild": "rm -rf ./dist", 12 | "build": "tsc", 13 | "postbuild": "cp -R ./src/assets ./dist", 14 | "prod": "node -r dotenv-safe/config dist/index.js", 15 | "lint:check": "tslint --project ./", 16 | "lint:fix": "tslint --project ./ --fix", 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "Przemysław Czekaj ", 20 | "license": "MIT", 21 | "engines": { 22 | "node": ">= 11" 23 | }, 24 | "precommit": [ 25 | "lint:check" 26 | ], 27 | "dependencies": { 28 | "@nestjs/common": "^5.7.4", 29 | "@nestjs/core": "^5.7.4", 30 | "@nestjs/graphql": "^5.5.7", 31 | "@nestjs/microservices": "^5.7.4", 32 | "@nestjs/passport": "^5.1.0", 33 | "@nestjs/swagger": "^2.5.1", 34 | "@nestjs/testing": "^5.7.4", 35 | "@nestjs/websockets": "^5.7.4", 36 | "apollo-server-express": "^2.4.8", 37 | "async": "^2.6.2", 38 | "async-exit-hook": "^2.0.1", 39 | "aws-sdk": "^2.426.0", 40 | "class-transformer": "^0.2.0", 41 | "class-validator": "^0.9.1", 42 | "cls-hooked": "^4.2.2", 43 | "cors": "^2.8.5", 44 | "dotenv-safe": "^5.0.1", 45 | "faker": "^4.1.0", 46 | "firebase-admin": "^8.3.0", 47 | "googleapis": "^37.2.0", 48 | "graphql": "^14.1.1", 49 | "graphql-subscriptions": "^1.0.0", 50 | "graphql-tools": "^4.0.3", 51 | "helmet": "^3.16.0", 52 | "jsonwebtoken": "^8.5.1", 53 | "lodash": "^4.17.11", 54 | "luxon": "^1.12.0", 55 | "mongodb": "^3.2.1", 56 | "multer-s3": "^2.9.0", 57 | "nestjs-command": "0.0.4", 58 | "nodemailer": "^5.1.1", 59 | "passport": "^0.4.0", 60 | "passport-facebook-token": "^3.3.0", 61 | "passport-http-bearer": "^1.0.1", 62 | "passport-jwt": "^4.0.0", 63 | "qs-middleware": "^1.0.3", 64 | "reflect-metadata": "^0.1.12", 65 | "rxjs": "^6.3.3", 66 | "rxjs-compat": "^6.3.3", 67 | "tslib": "^1.9.3", 68 | "twing": "^2.2.3", 69 | "typeorm": "^0.2.15", 70 | "voucher-code-generator": "^1.1.1", 71 | "winston": "^2.4.4" 72 | }, 73 | "devDependencies": { 74 | "@types/express": "^4.16.0", 75 | "@types/faker": "^4.1.5", 76 | "@types/graphql": "^14.0.7", 77 | "@types/jsonwebtoken": "^8.3.2", 78 | "@types/node": "^10.14.2", 79 | "@types/nodemailer": "^4.6.6", 80 | "@types/redis": "^2.8.11", 81 | "@types/socket.io": "^2.1.2", 82 | "@types/async": "^2.4.1", 83 | "@types/lodash": "^4.14.123", 84 | "@types/luxon": "^1.11.1", 85 | "@types/mongodb": "^3.1.23", 86 | "nodemon": "^1.18.10", 87 | "pre-commit": "^1.2.2", 88 | "ts-node": "^8.0.3", 89 | "tsconfig-paths": "^3.8.0", 90 | "tslint": "^5.14.0", 91 | "typescript": "^3.2.4" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd $(dirname $0) 3 | docker run --rm --env-file ./.env threeleaf-backend:latest 4 | -------------------------------------------------------------------------------- /server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Facebook Login JavaScript Example 5 | 6 | 7 | 8 | 115 | 116 | 121 | 122 | 123 | 124 | 125 |
126 |
127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /server/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/bo9jQzn0fTzD 3 | uDg7JZZwKA/GzBhjaE3QBQlSzXW3uV3BeyiROqgP0mB81bL3iN0xtXCCwjNkqMfB 4 | SgrN5U03IYduS0rdGaFzeJsE6Xp5mJR28SBI5/PJZlKgFqEqQH2YbMTYTAAVmf50 5 | AO/4pL1TmukvAfOZNTt1v1CGhKNvQidq47WQSGbO7TcLvtQOVjLJqb0jrwtwaQED 6 | jwIl5WBqUhLKGVo6gWZ6pOyeKoHvQBWvWi/6rCYu8QKA0Y46hJGyNZ5fzaSvUisr 7 | ZbQaQ/0arm8M8nrDnGaNRdQSyGdlSJ658zsSdgBoaZEXebjq3BRx47RPSaiN9iA2 8 | Ww0tU7oPAgMBAAECggEAfS2n18nzENwAKx/ArXSjzg10W5PEcuSW1WslsJ6n1o8B 9 | Lu8BFQ6dLVNtQtRe5hz6P3Ic83RYNP9lOBDZxc+m0WE8dTQI7VHAUIjSHcErFG5g 10 | zvJYITSEJvOhHgwOMVnghxbBFYuFFpsPV/3w7c+T+iK3TsTg7RNxIUsMNhpv4OTo 11 | wYfNuSMZ8n3yCto66GgMxURO8wZq34pmViKbdFODxLsNH0NJ6cv0OwcxDS3FTcUe 12 | MtzPg94vz/u/JzAKuGOUa2//A3uwOoCS59yLsbcHL79SeK9d0SAXVhPV8RYIsDU8 13 | vSDvB8p4BwDbJMuBMWILcgibqsjmJEGbl2fB3ysOiQKBgQDk3K/ACNBj/uxNKSIn 14 | GnGIigeBC0uFj70H3CzXMR0yP+OxW60IsyH0aB/xmrVdIxX4Kl7uAHHPsGgfKNR1 15 | 7V6qmtukbLn7wKEm2rbzx+TBZ3zNoUkLMxUc2BJltVzkxC9qPT1YWrhvev99IIbz 16 | Sy/8/kFQFNl8t/WV2uLqimhdKwKBgQDWIaWTeOyOWNeaTkFXFwMwcb79ksAdZ6+o 17 | 45CuUp2u8BgAtFSAI/p2EBXuI1ucC8BVnKq4Dd73lAZGFpaR7YtUhB9zMw5hhDf4 18 | gLCkPn/YBnmnQDLct8IDDoFz3fqIRd6dr8Tia7L7X+xyFleEcKuXQdBdytXcXieV 19 | xFD+kWxMrQKBgGnlAx0j9uiN5+C8C6V0QtweoeI/y639GEYuxkC75Pp/PWevN50y 20 | 5Z8lHsK4BvsVZPDzCgGuCvtCcKsaRVRbhNYD2WV3OYcXVnmX8ddSwrIKpGCxJA8e 21 | j8JM6BZPmK/xZs8Njvj24SgUDWtkRY0FWSsCqk3Xl9pxdBzmruA/MpMJAoGANVD/ 22 | Zy7Ox8q7NEKT5llmg+NuiLjHeQreJBE1bxJHDM3fTB2ahKVwsYDj1P2cy8fpRefS 23 | Yi2h/McwoLbzGrao4IxdQFPQGtvPC+MPGHYvYtyJ4ekMQQTIG/a6qNz2ioPLIDwp 24 | q67hS9Hgj6+cbu+W6fyLIy9j/JC9Dn9pI1mWO6kCgYEA2ZDWeMpGeExKrILjfCD0 25 | XDVMki97HFBSRatGgyI6EjHb0nG1LJidW2PfijygtuSegrmzayKej1ryECYA3EUW 26 | Zyd6HOs3y9r+hB8EiyTivf/UPzTqUU5NKwz7J/7RPPtYNEQodubrSoJiE7Ssw+5Q 27 | NOhpIMRfBqtO73GXdf+jP/w= 28 | -----END PRIVATE KEY----- 29 | -----BEGIN CERTIFICATE----- 30 | MIIDazCCAlOgAwIBAgIUOD4W3T61pWKnL+nvJuNJfnw3upwwDQYJKoZIhvcNAQEL 31 | BQAwRTELMAkGA1UEBhMCUEwxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 32 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xOTAxMTUwOTA2MTdaFw0yMDAx 33 | MTUwOTA2MTdaMEUxCzAJBgNVBAYTAlBMMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 34 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 35 | AQUAA4IBDwAwggEKAoIBAQC/bo9jQzn0fTzDuDg7JZZwKA/GzBhjaE3QBQlSzXW3 36 | uV3BeyiROqgP0mB81bL3iN0xtXCCwjNkqMfBSgrN5U03IYduS0rdGaFzeJsE6Xp5 37 | mJR28SBI5/PJZlKgFqEqQH2YbMTYTAAVmf50AO/4pL1TmukvAfOZNTt1v1CGhKNv 38 | Qidq47WQSGbO7TcLvtQOVjLJqb0jrwtwaQEDjwIl5WBqUhLKGVo6gWZ6pOyeKoHv 39 | QBWvWi/6rCYu8QKA0Y46hJGyNZ5fzaSvUisrZbQaQ/0arm8M8nrDnGaNRdQSyGdl 40 | SJ658zsSdgBoaZEXebjq3BRx47RPSaiN9iA2Ww0tU7oPAgMBAAGjUzBRMB0GA1Ud 41 | DgQWBBTV0r4dbEaVtrED1DrV87akf6O85DAfBgNVHSMEGDAWgBTV0r4dbEaVtrED 42 | 1DrV87akf6O85DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBu 43 | C+mMkHdo+72D8jFg2q9e0HcvX3hBWNpolxBDfB4W8Q4qwOR8xE37A/Jq93EnwX3Q 44 | H6Z4XVNF7+UJ5ON0FmopzX7pDqtcO3tIWy/7rFYvoTQT2hIG7RV4KgY9sosFKi0p 45 | dnQDHt9XPdIjAGLxzzJTSQa8coIU5TwR8sXpuuS7g2MqTk8lKd4vxYKTQXdSlmCu 46 | /TRA6PXHQWQ9+btjXPpqSFhmpq6qfJhDrKjdaOkaP8o3/2sA1wgBM6scXV5JjO16 47 | 2ArU1xMVAYUypxHARocVKV6sorndEWIFgGfBa5qk5MBD+JE4+207gHafSJoMWRWx 48 | P3BoAfHqjhwQcSkV8Xva 49 | -----END CERTIFICATE----- 50 | -------------------------------------------------------------------------------- /server/simple-https-server.py: -------------------------------------------------------------------------------- 1 | import BaseHTTPServer, SimpleHTTPServer 2 | import ssl 3 | 4 | httpd = BaseHTTPServer.HTTPServer(('localhost', 4443), SimpleHTTPServer.SimpleHTTPRequestHandler) 5 | httpd.socket = ssl.wrap_socket (httpd.socket, certfile='./server.pem', server_side=True) 6 | httpd.serve_forever() 7 | -------------------------------------------------------------------------------- /src/app/_helpers/database/deep-partial.ts: -------------------------------------------------------------------------------- 1 | export type DeepPartial = { 2 | [P in keyof T]?: DeepPartial; 3 | }; 4 | -------------------------------------------------------------------------------- /src/app/_helpers/database/index.ts: -------------------------------------------------------------------------------- 1 | export * from './deep-partial'; 2 | export * from './repository.interface'; 3 | export * from './typeorm-filter.mapper'; 4 | -------------------------------------------------------------------------------- /src/app/_helpers/database/repository.interface.ts: -------------------------------------------------------------------------------- 1 | import {DeepPartial} from './deep-partial'; 2 | 3 | export interface Repository { 4 | find(options): Promise; 5 | findOneOrFail(id: string|number): Promise; 6 | findOne(cond): Promise; 7 | create(model: DeepPartial): T; 8 | save(model: T): Promise; 9 | bulkSave(models: DeepPartial): Promise; 10 | delete(id: string): Promise; 11 | deleteAll(options): Promise; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/_helpers/database/typeorm-filter.mapper.ts: -------------------------------------------------------------------------------- 1 | import {FindManyOptions} from 'typeorm'; 2 | 3 | const arrayToObject = (arr) => Object.assign({}, ...arr.map(item => ({...item}))); 4 | 5 | export function typeormFilterMapper(options: FindManyOptions) { 6 | const filters = options.where; 7 | const where = {}; 8 | Object.keys(filters).forEach(filterName => { 9 | const operators = filters[filterName]; 10 | const filter = Object.keys(operators).map(key => { 11 | return mapToTypeOrm(key, operators); 12 | }); 13 | where[filterName] = arrayToObject(filter); 14 | }); 15 | 16 | return where; 17 | } 18 | 19 | function mapToTypeOrm(key, filter) { 20 | switch (key) { 21 | case 'eq': 22 | return {'$eq': filter[key]}; 23 | case 'ne': 24 | return {'$ne': filter[key]}; 25 | case 'le': 26 | return {'$lte': filter[key]}; 27 | case 'lt': 28 | return {'$lt': filter[key]}; 29 | case 'ge': 30 | return {'$gte': filter[key]}; 31 | case 'gt': 32 | return {'$gt': filter[key]}; 33 | case 'in': 34 | return {'$in': filter[key]}; 35 | case 'nin': 36 | return {'$nin': filter[key]}; 37 | case 'contains': 38 | return {'$regex': new RegExp(filter[key], 'gi')}; 39 | case 'beginsWith': 40 | return {'$regex': new RegExp(`^${filter[key]}`, 'gi')}; 41 | case 'notContains': 42 | return {'$not': new RegExp(filter[key])}; 43 | case 'between': 44 | return { 45 | '$gte': filter[key][0], 46 | '$lte': filter[key][1] 47 | }; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/_helpers/decorators/index.ts: -------------------------------------------------------------------------------- 1 | export * from './profile.decorator'; 2 | export * from './user.decorator'; 3 | -------------------------------------------------------------------------------- /src/app/_helpers/decorators/profile.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const Profile = createParamDecorator((data, req) => { 4 | return req.user; 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/_helpers/decorators/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const User = createParamDecorator((data, req) => { 4 | return req.user; 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/_helpers/entity/extended-entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {BaseEntity, Column} from 'typeorm'; 3 | import {DateTime} from 'luxon'; 4 | 5 | export class ExtendedEntity extends BaseEntity { 6 | public id?: string; 7 | 8 | @Column() 9 | public isDeleted = false; 10 | 11 | @ApiModelProperty() 12 | @Column() 13 | public createdAt: DateTime; 14 | 15 | @ApiModelProperty() 16 | @Column() 17 | public updatedAt: DateTime; 18 | } 19 | -------------------------------------------------------------------------------- /src/app/_helpers/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './extended-entity'; 2 | -------------------------------------------------------------------------------- /src/app/_helpers/filters/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus} from '@nestjs/common'; 2 | import {AppLogger} from '../../app.logger'; 3 | 4 | @Catch(HttpException) 5 | export class HttpExceptionFilter implements ExceptionFilter { 6 | private logger = new AppLogger(HttpExceptionFilter.name); 7 | 8 | catch(exception: HttpException, host: ArgumentsHost) { 9 | const ctx = host.switchToHttp(); 10 | const response = ctx.getResponse(); 11 | let status = HttpStatus.BAD_REQUEST; 12 | 13 | if (typeof exception === 'string') { 14 | exception = new HttpException({error: 'Undefined', message: exception}, status); 15 | } 16 | 17 | if (typeof exception.message === 'string') { 18 | exception = new HttpException({error: 'Undefined', message: exception.message}, status); 19 | } 20 | 21 | if (exception.getStatus) { 22 | status = exception.getStatus(); 23 | } 24 | 25 | this.logger.error(`[${exception.message.error}] ${exception.message.message}`, exception.stack); 26 | 27 | response 28 | .status(status) 29 | .json({ 30 | statusCode: status, 31 | ...exception.getResponse() as object, 32 | timestamp: new Date().toISOString() 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/_helpers/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-exception.filter'; 2 | export * from './twig-exception.filter'; 3 | -------------------------------------------------------------------------------- /src/app/_helpers/filters/twig-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus} from '@nestjs/common'; 2 | import {AppLogger} from '../../app.logger'; 3 | import {TwingError, TwingErrorLoader} from 'twing'; 4 | 5 | @Catch(TwingErrorLoader) 6 | export class TwigExceptionFilter implements ExceptionFilter { 7 | private logger = new AppLogger(TwingErrorLoader.name); 8 | 9 | catch(exception: TwingError, host: ArgumentsHost) { 10 | const ctx = host.switchToHttp(); 11 | const response = ctx.getResponse(); 12 | 13 | this.logger.error(`[${exception.name}] ${exception.message}`, exception.stack); 14 | 15 | response 16 | .status(status) 17 | .json({ 18 | statusCode: HttpStatus.NOT_FOUND, 19 | error: 'Template', 20 | message: 'Template not found', 21 | timestamp: new Date().toISOString() 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/_helpers/graphql/gql-config.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {GqlModuleOptions, GqlOptionsFactory} from '@nestjs/graphql'; 3 | import auth_hdr from 'passport-jwt/lib/auth_header'; 4 | import {join} from 'path'; 5 | import {has} from 'lodash'; 6 | import {config} from '../../../config'; 7 | import {AppLogger} from '../../app.logger'; 8 | import {TokenDto} from '../../auth/dto/token.dto'; 9 | import {verifyToken} from '../../auth/jwt'; 10 | import {OnlineService} from '../../user/online.service'; 11 | import {UserService} from '../../user/user.service'; 12 | 13 | @Injectable() 14 | export class GqlConfigService implements GqlOptionsFactory { 15 | private logger = new AppLogger(GqlConfigService.name); 16 | 17 | constructor( 18 | private readonly userService: UserService, 19 | private readonly onlineService: OnlineService 20 | ) { 21 | 22 | } 23 | 24 | createGqlOptions(): GqlModuleOptions { 25 | return { 26 | typePaths: [join(process.cwd(), '**/*.graphql')], 27 | introspection: true, 28 | playground: true, 29 | installSubscriptionHandlers: true, 30 | tracing: !config.isProduction, 31 | debug: !config.isProduction, 32 | definitions: { 33 | path: join(process.cwd(), 'src/app/graphql.schema.ts'), 34 | outputAs: 'class' 35 | }, 36 | subscriptions: { 37 | onConnect: (connectionParams, websocket, context) => { 38 | return new Promise(async (resolve, reject) => { 39 | try { 40 | const authToken = connectionParams['Authorization']; 41 | const token = await this.validateToken(authToken); 42 | const user = await this.userService.findOneById(token.id); 43 | await this.onlineService.addUser(user); 44 | resolve({req: {...context.request, user}}); 45 | } catch (e) { 46 | this.logger.error(e.message, e.stack); 47 | reject({message: 'Unauthorized'}); 48 | } 49 | }); 50 | }, 51 | onDisconnect: (websocket, context: any) => { 52 | return new Promise(async resolve => { 53 | const initialContext = await context.initPromise; 54 | if (has(initialContext, 'req.user')) { 55 | await this.onlineService.removeUser(initialContext.req.user); 56 | } 57 | resolve(); 58 | }); 59 | } 60 | }, 61 | formatError: error => { 62 | if (config.isProduction) { 63 | const err: any = {}; 64 | Object.assign(err, error); 65 | delete err.extensions; 66 | return err; 67 | } 68 | return error; 69 | }, 70 | context: (context) => { 71 | let req = context.req; 72 | if (context.connection) { 73 | req = context.connection.context.req; 74 | } 75 | return {req}; 76 | } 77 | }; 78 | } 79 | 80 | private async validateToken(authToken: string): Promise { 81 | const jwtToken = auth_hdr.parse(authToken).value; 82 | this.logger.debug(`[validateToken] token ${jwtToken}`); 83 | return verifyToken(jwtToken, config.session.secret); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/_helpers/graphql/graphql.guard.ts: -------------------------------------------------------------------------------- 1 | import {ExecutionContext, Injectable} from '@nestjs/common'; 2 | import {GqlExecutionContext} from '@nestjs/graphql'; 3 | import {AuthGuard} from '@nestjs/passport'; 4 | import {ExecutionContextHost} from '@nestjs/core/helpers/execution-context.host'; 5 | import {Observable} from 'rxjs'; 6 | 7 | @Injectable() 8 | export class GraphqlGuard extends AuthGuard('jwt') { 9 | canActivate(context: ExecutionContext): boolean | Promise | Observable { 10 | const ctx = GqlExecutionContext.create(context); 11 | const {req} = ctx.getContext(); 12 | return super.canActivate(new ExecutionContextHost([req])); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/_helpers/graphql/index.ts: -------------------------------------------------------------------------------- 1 | export * from './graphql.guard'; 2 | export * from './user.decorator'; 3 | export * from './gql-config.service'; 4 | -------------------------------------------------------------------------------- /src/app/_helpers/graphql/user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator } from '@nestjs/common'; 2 | 3 | export const User = createParamDecorator((data, [root, args, ctx, info]) => { 4 | return ctx.req.user; 5 | }); 6 | -------------------------------------------------------------------------------- /src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | import {config} from '../../config'; 3 | 4 | export * from './mail'; 5 | export * from './sms'; 6 | export * from './sms/sms-options.interface'; 7 | export * from './entity'; 8 | export * from './filters'; 9 | export * from './database'; 10 | export * from './graphql'; 11 | export * from './rest.exception'; 12 | export * from './request-context'; 13 | export * from './middleware/request-context.middleware'; 14 | 15 | export function ucfirst(string) { 16 | return string[0].toUpperCase() + string.slice(1); 17 | } 18 | 19 | export function passwordHash(password: string) { 20 | return crypto.createHmac('sha256', config.salt) 21 | .update(password, 'utf8') 22 | .digest('hex'); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/_helpers/mail/index.ts: -------------------------------------------------------------------------------- 1 | import {createTransport, SendMailOptions, SentMessageInfo} from 'nodemailer'; 2 | import {TwingEnvironment, TwingLoaderFilesystem} from 'twing'; 3 | import {config} from '../../../config'; 4 | 5 | export async function mail(options: SendMailOptions): Promise { 6 | const transporter = createTransport({ 7 | host: config.aws.pinpoint.smtp.host, 8 | port: config.aws.pinpoint.smtp.port, 9 | secure: true, 10 | auth: { 11 | user: config.aws.pinpoint.smtp.user, 12 | pass: config.aws.pinpoint.smtp.secret 13 | } 14 | }); 15 | 16 | return transporter.sendMail({...options, from: config.mail.from}); 17 | } 18 | 19 | export function renderTemplate(path: string, data: any): string { 20 | const loader = new TwingLoaderFilesystem(config.assetsPath); 21 | const twing = new TwingEnvironment(loader); 22 | return twing.render(path, data); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/_helpers/middleware/request-context.middleware.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, MiddlewareFunction, NestMiddleware} from '@nestjs/common'; 2 | import cls from 'cls-hooked'; 3 | import {RequestContext} from '../request-context'; 4 | 5 | @Injectable() 6 | export class RequestContextMiddleware implements NestMiddleware { 7 | resolve(): MiddlewareFunction { 8 | return(req, res, next) => { 9 | const requestContext = new RequestContext(req, res); 10 | const session = cls.getNamespace(RequestContext.nsid) || cls.createNamespace(RequestContext.nsid); 11 | 12 | session.run(async () => { 13 | session.set(RequestContext.name, requestContext); 14 | next(); 15 | }); 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/_helpers/push/index.ts: -------------------------------------------------------------------------------- 1 | import admin from 'firebase-admin'; 2 | import {config} from '../../../config'; 3 | import {AppLogger} from '../../app.logger'; 4 | import MessagingPayload = admin.messaging.MessagingPayload; 5 | 6 | const logger = new AppLogger(`FirebasePush`); 7 | 8 | admin.initializeApp({ 9 | credential: admin.credential.cert(config.firebase as any) 10 | }); 11 | 12 | export async function push(token: string, message: MessagingPayload) { 13 | return admin.messaging().sendToDevice(token, message) 14 | .then((response) => { 15 | // Response is a message ID string. 16 | logger.log(`Successfully sent message: ${response}`); 17 | }) 18 | .catch(error => { 19 | logger.error('Error sending message:', error); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /src/app/_helpers/request-context.ts: -------------------------------------------------------------------------------- 1 | import {Response, Request} from 'express'; 2 | import uuid from 'uuid'; 3 | import cls from 'cls-hooked'; 4 | import {UserEntity} from '../user/entity'; 5 | 6 | export class RequestContext { 7 | 8 | public static nsid = uuid.v4(); 9 | public readonly id: number; 10 | public request: Request; 11 | public response: Response; 12 | 13 | constructor(request: Request, response: Response) { 14 | this.id = Math.random(); 15 | this.request = request; 16 | this.response = response; 17 | } 18 | 19 | public static currentRequestContext(): RequestContext { 20 | const session = cls.getNamespace(RequestContext.nsid); 21 | if (session && session.active) { 22 | return session.get(RequestContext.name); 23 | } 24 | 25 | return null; 26 | } 27 | 28 | public static currentRequest(): Request { 29 | const requestContext = RequestContext.currentRequestContext(); 30 | 31 | if (requestContext) { 32 | return requestContext.request; 33 | } 34 | 35 | return null; 36 | } 37 | 38 | public static currentUser(throwError?: boolean): UserEntity { 39 | const requestContext = RequestContext.currentRequestContext(); 40 | 41 | if (requestContext) { 42 | const user: UserEntity = requestContext.request['user']; 43 | if (user) { 44 | return user; 45 | } 46 | } 47 | 48 | return null; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/_helpers/rest.exception.ts: -------------------------------------------------------------------------------- 1 | import {HttpException, HttpStatus} from '@nestjs/common'; 2 | 3 | export interface RestExceptionResponse { 4 | error: string; 5 | message: string; 6 | condition: number; 7 | } 8 | 9 | export class RestException extends HttpException { 10 | constructor(response: RestExceptionResponse, status: number = HttpStatus.BAD_REQUEST) { 11 | super(response, status); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/_helpers/sms/SMSType.enum.ts: -------------------------------------------------------------------------------- 1 | export enum SMSTypeEnum { 2 | PROMOTIONAL = 'Promotional', 3 | TRANSACTIONAL = 'Transactional' 4 | } 5 | -------------------------------------------------------------------------------- /src/app/_helpers/sms/index.ts: -------------------------------------------------------------------------------- 1 | import SNS from 'aws-sdk/clients/sns'; 2 | import {SmsOptionsInterface} from './sms-options.interface'; 3 | 4 | export async function sms(options: SmsOptionsInterface) { 5 | return new Promise((resolve, reject) => { 6 | const sns = new SNS(); 7 | sns.setSMSAttributes({ 8 | attributes: { 9 | DefaultSenderID: options.sender, 10 | DefaultSMSType: options.smsType 11 | } 12 | }); 13 | const response = sns.publish({ 14 | Message: options.message, 15 | PhoneNumber: options.phoneNumber 16 | }, (err, data) => { 17 | if (err) { 18 | return reject(err); 19 | } 20 | resolve(data); 21 | }); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /src/app/_helpers/sms/sms-options.interface.ts: -------------------------------------------------------------------------------- 1 | import {SMSTypeEnum} from './SMSType.enum'; 2 | 3 | export interface SmsOptionsInterface { 4 | sender: string; 5 | message: string; 6 | phoneNumber: string; 7 | smsType: SMSTypeEnum; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/app.dispatcher.ts: -------------------------------------------------------------------------------- 1 | import {INestApplication, INestApplicationContext, INestMicroservice} from '@nestjs/common'; 2 | import {NestFactory} from '@nestjs/core'; 3 | import {DocumentBuilder, SwaggerModule} from '@nestjs/swagger'; 4 | import {useContainer} from 'class-validator'; 5 | import cors from 'cors'; 6 | import helmet from 'helmet'; 7 | import query from 'qs-middleware'; 8 | import {config} from '../config'; 9 | import {AppLogger} from './app.logger'; 10 | import {AppModule} from './app.module'; 11 | import {HttpExceptionFilter, TwigExceptionFilter} from './_helpers/filters'; 12 | 13 | export class AppDispatcher { 14 | private app: INestApplication; 15 | private microservice: INestMicroservice; 16 | private logger = new AppLogger(AppDispatcher.name); 17 | 18 | async dispatch(): Promise { 19 | await this.createServer(); 20 | this.createMicroservices(); 21 | await this.startMicroservices(); 22 | return this.startServer(); 23 | } 24 | 25 | async shutdown(): Promise { 26 | await this.app.close(); 27 | } 28 | 29 | public getContext(): Promise { 30 | return NestFactory.createApplicationContext(AppModule); 31 | } 32 | 33 | private async createServer(): Promise { 34 | this.app = await NestFactory.create(AppModule, { 35 | logger: new AppLogger('Nest') 36 | }); 37 | useContainer(this.app.select(AppModule), {fallbackOnErrors: true}); 38 | this.app.use(cors()); 39 | this.app.use(query()); 40 | this.app.useGlobalFilters(new HttpExceptionFilter()); 41 | this.app.useGlobalFilters(new TwigExceptionFilter()); 42 | if (config.isProduction) { 43 | this.app.use(helmet()); 44 | } 45 | const options = new DocumentBuilder() 46 | .setTitle(config.name) 47 | .setDescription(config.description) 48 | .setVersion(config.version) 49 | .addBearerAuth() 50 | .build(); 51 | 52 | const document = SwaggerModule.createDocument(this.app, options); 53 | document.paths['/graphql'] = {get: { tags: ['graphql']}, post: { tags: ['graphql']}}; 54 | SwaggerModule.setup('/swagger', this.app, document); 55 | } 56 | 57 | private createMicroservices(): void { 58 | this.microservice = this.app.connectMicroservice(config.microservice); 59 | } 60 | 61 | private startMicroservices(): Promise { 62 | return this.app.startAllMicroservicesAsync(); 63 | } 64 | 65 | private async startServer(): Promise { 66 | await this.app.listen(config.port, config.host); 67 | this.logger.log(`Swagger is exposed at http://${config.host}:${config.port}/swagger`); 68 | this.logger.log(`Graphql is exposed at http://${config.host}:${config.port}/graphql`); 69 | this.logger.log(`Server is listening http://${config.host}:${config.port}`); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/app.logger.ts: -------------------------------------------------------------------------------- 1 | import { LoggerService } from '@nestjs/common'; 2 | import { DateTime } from 'luxon'; 3 | import { LoggerInstance, transports, Logger as WsLogger } from 'winston'; 4 | import { config } from '../config'; 5 | 6 | export class AppLogger implements LoggerService { 7 | private logger: LoggerInstance; 8 | 9 | constructor(label?: string) { 10 | this.logger = new WsLogger({ 11 | level: config.logger.level, 12 | transports: [ 13 | new transports.Console({ 14 | label, 15 | timestamp: () => DateTime.local().toString(), 16 | formatter: options => `${options.timestamp()} [${options.level.toUpperCase()}] ${options.label} - ${options.message}` 17 | }) 18 | ] 19 | }); 20 | } 21 | 22 | error(message: string, trace: string) { 23 | this.logger.error(message, trace); 24 | } 25 | 26 | warn(message: string) { 27 | this.logger.warn(message); 28 | } 29 | 30 | log(message: string) { 31 | this.logger.info(message); 32 | } 33 | 34 | verbose(message: string) { 35 | this.logger.verbose(message); 36 | } 37 | 38 | debug(message: string) { 39 | this.logger.debug(message); 40 | } 41 | 42 | silly(message: string) { 43 | this.logger.silly(message); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {MiddlewareConsumer, Module, RequestMethod} from '@nestjs/common'; 2 | import {CommandModule} from 'nestjs-command'; 3 | import {AuthModule} from './auth/auth.module'; 4 | import {AppLogger} from './app.logger'; 5 | import {ContractModule} from './contract/contract.module'; 6 | import {DatabaseModule} from './database/database.module'; 7 | import {GraphQLModule} from '@nestjs/graphql'; 8 | import {HealthCheckModule} from './healtcheck/healthcheck.module'; 9 | import {HomeModule} from './home/home.module'; 10 | import {UserModule} from './user/user.module'; 11 | import {MessageModule} from './message/message.module'; 12 | import {HomeFavoriteModule} from './home-favorite/home-favorite.module'; 13 | import {MediaModule} from './media/media.module'; 14 | import {HomeMediaModule} from './home-media/home-media.module'; 15 | import {GqlConfigService, RequestContextMiddleware} from './_helpers'; 16 | import {SecurityModule} from './security'; 17 | 18 | @Module({ 19 | imports: [ 20 | CommandModule, 21 | HealthCheckModule, 22 | SecurityModule, 23 | DatabaseModule, 24 | AuthModule, 25 | UserModule, 26 | MediaModule, 27 | HomeModule, 28 | HomeFavoriteModule, 29 | HomeMediaModule, 30 | MessageModule, 31 | ContractModule, 32 | GraphQLModule.forRootAsync({ 33 | imports: [UserModule], 34 | useClass: GqlConfigService 35 | }) 36 | ] 37 | }) 38 | export class AppModule { 39 | private logger = new AppLogger(AppModule.name); 40 | 41 | constructor() { 42 | this.logger.log('Initialize constructor'); 43 | } 44 | 45 | configure(consumer: MiddlewareConsumer) { 46 | consumer 47 | .apply(RequestContextMiddleware) 48 | .forRoutes({path: '*', method: RequestMethod.ALL}); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/auth/auth-error.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AuthErrorEnum { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/app/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {AuthService} from './auth.service'; 3 | import {JwtStrategy, FacebookTokenStrategy} from './stategies'; 4 | import {AuthController} from './auth.controller'; 5 | import {UserModule} from '../user/user.module'; 6 | import {DatabaseModule} from '../database/database.module'; 7 | 8 | @Module({ 9 | imports: [UserModule, DatabaseModule], 10 | providers: [AuthService, JwtStrategy, FacebookTokenStrategy], 11 | controllers: [AuthController] 12 | }) 13 | export class AuthModule { 14 | } 15 | -------------------------------------------------------------------------------- /src/app/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {UserService} from '../user/user.service'; 3 | import {JwtPayload} from './interfaces/jwt-payload.inteface'; 4 | 5 | @Injectable() 6 | export class AuthService { 7 | constructor(private readonly userService: UserService) {} 8 | 9 | async validateUser(payload: JwtPayload): Promise { 10 | return await this.userService.findOneById(payload.id); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/app/auth/dto/credentials.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | import { ApiModelProperty } from '@nestjs/swagger'; 3 | 4 | export class CredentialsDto { 5 | 6 | @ApiModelProperty() 7 | @IsString() 8 | readonly email: string; 9 | 10 | @ApiModelProperty() 11 | @IsString() 12 | readonly password: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/auth/dto/facebook-token.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class FacebookTokenDto { 4 | @ApiModelProperty() 5 | access_token: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/dto/jwt.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class JwtDto { 4 | @ApiModelProperty() 5 | expiresIn: number; 6 | 7 | @ApiModelProperty() 8 | accessToken: string; 9 | 10 | @ApiModelProperty() 11 | refreshToken: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/auth/dto/password-reset.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class PasswordResetDto { 4 | @ApiModelProperty() 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/dto/password-token.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class PasswordTokenDto { 4 | @ApiModelProperty() 5 | resetToken: string; 6 | 7 | @ApiModelProperty() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/auth/dto/refresh-token.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class RefreshTokenDto { 4 | @ApiModelProperty() 5 | refreshToken: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/dto/token.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class TokenDto { 4 | @ApiModelProperty() 5 | id: string; 6 | 7 | @ApiModelProperty() 8 | expiresIn: number; 9 | 10 | @ApiModelProperty() 11 | audience: string; 12 | 13 | @ApiModelProperty() 14 | issuer: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/auth/dto/user-entity.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {config} from '../../../config'; 3 | 4 | export class UserEntityDto { 5 | @ApiModelProperty() 6 | public first_name: string; 7 | 8 | @ApiModelProperty() 9 | public last_name: string; 10 | 11 | @ApiModelProperty() 12 | public email: string; 13 | 14 | @ApiModelProperty({ 15 | minLength: config.passwordMinLength 16 | }) 17 | public password: string; 18 | 19 | @ApiModelProperty() 20 | public phone_num: string; 21 | 22 | @ApiModelProperty({ 23 | required: false 24 | }) 25 | public profile_img?: string; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/auth/dto/verify-resend.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class VerifyResendDto { 4 | @ApiModelProperty() 5 | email: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/app/auth/dto/verify-token.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class VerifyTokenDto { 4 | @ApiModelProperty() 5 | verifyToken: string; 6 | 7 | @ApiModelProperty() 8 | email: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/facebook-profile.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FacebookObject { 2 | value: string; 3 | } 4 | 5 | export type FacebookProvider = 'facebook'; 6 | 7 | export interface FacebookProfile { 8 | displayName: string; 9 | email: FacebookObject[]; 10 | gender: string; 11 | id: string; 12 | name: { 13 | familyName: string; 14 | givenName: string; 15 | middleName: string; 16 | }; 17 | photos: FacebookObject[]; 18 | provider: FacebookProvider; 19 | _json: { 20 | email: string; 21 | first_name: string; 22 | id: string; 23 | last_name: string; 24 | middle_name: string; 25 | name: string; 26 | }; 27 | _raw: string; 28 | } 29 | -------------------------------------------------------------------------------- /src/app/auth/interfaces/jwt-payload.inteface.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayload { 2 | id: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/auth/jwt/index.ts: -------------------------------------------------------------------------------- 1 | import {JsonWebTokenError, sign, verify} from 'jsonwebtoken'; 2 | import {DeepPartial} from '../../_helpers/database'; 3 | import {UserEntity} from '../../user/entity'; 4 | import {config} from '../../../config'; 5 | import {TokenDto} from '../dto/token.dto'; 6 | 7 | export async function createAuthToken({id}: DeepPartial) { 8 | const expiresIn = config.session.timeout; 9 | const accessToken = createToken(id, expiresIn, config.session.secret); 10 | const refreshToken = createToken(id, config.session.refresh.timeout, config.session.refresh.secret); 11 | return { 12 | expiresIn, 13 | accessToken, 14 | refreshToken 15 | }; 16 | } 17 | 18 | export function createToken(id, expiresIn, secret) { 19 | return sign({id}, secret, { 20 | expiresIn, 21 | audience: config.session.domain, 22 | issuer: config.uuid 23 | }); 24 | } 25 | 26 | export async function verifyToken(token: string, secret: string): Promise { 27 | return new Promise((resolve, reject) => { 28 | verify(token, secret, (err, decoded) => { 29 | if (err) { 30 | return reject(err); 31 | } 32 | resolve(decoded as TokenDto); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/auth/stategies/facebook-token.strategy.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {PassportStrategy} from '@nestjs/passport'; 3 | import Strategy from '../../_helpers/facebook/facebook-token.strategy'; 4 | import {config} from '../../../config'; 5 | import {FacebookProfile} from '../interfaces/facebook-profile.interface'; 6 | 7 | @Injectable() 8 | export class FacebookTokenStrategy extends PassportStrategy(Strategy) { 9 | constructor() { 10 | super({ 11 | clientID: config.facebook.app_id, 12 | clientSecret: config.facebook.app_secret 13 | }); 14 | } 15 | 16 | async validate(accessToken: string, refreshToken: string, profile: FacebookProfile) { 17 | return profile; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/auth/stategies/index.ts: -------------------------------------------------------------------------------- 1 | export * from './facebook-token.strategy'; 2 | export * from './jwt.strategy'; 3 | -------------------------------------------------------------------------------- /src/app/auth/stategies/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import {ExtractJwt, Strategy} from 'passport-jwt'; 2 | import {PassportStrategy} from '@nestjs/passport'; 3 | import {Injectable, UnauthorizedException} from '@nestjs/common'; 4 | import {config} from '../../../config'; 5 | import {AuthService} from '../auth.service'; 6 | import {JwtPayload} from '../interfaces/jwt-payload.inteface'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor(private readonly authService: AuthService) { 11 | super({ 12 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 13 | secretOrKey: config.session.secret, 14 | issuer: config.uuid, 15 | audience: config.session.domain 16 | }); 17 | } 18 | 19 | async validate(payload: JwtPayload) { 20 | const user = await this.authService.validateUser(payload); 21 | if (!user) { 22 | throw new UnauthorizedException(); 23 | } 24 | return user; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/contract/contract.constants.ts: -------------------------------------------------------------------------------- 1 | export const CONTRACT_TOKEN = 'ContractToken'; 2 | -------------------------------------------------------------------------------- /src/app/contract/contract.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | allContractsAsBuyer(filter: ContractFilterInput, limit: Int): [Contract] 3 | allContractsAsSeller(filter: ContractFilterInput, limit: Int): [Contract] 4 | getContract(id: ID!): Contract 5 | } 6 | 7 | type Mutation { 8 | createContract(input: ContractInput): Contract 9 | updateContract(id: ID!, input: ContractInput): Contract 10 | deleteContract(id: ID!): Boolean 11 | } 12 | 13 | type Subscription { 14 | newContract: Contract 15 | updatedContract: Contract 16 | deletedContract: Contract 17 | } 18 | 19 | type Contract { 20 | id: ID! 21 | home: Home 22 | user: User 23 | sales_price: Float 24 | buyer_cash: Float 25 | loan_amount: Float 26 | earnest_money: Float 27 | earnest_money_days: Int 28 | earnest_money_to: String 29 | property_condition: String 30 | repairs: String 31 | home_warranty: Boolean 32 | home_warranty_amount: Float 33 | closing_date: Date 34 | special_provisions: String 35 | non_realty_items: String 36 | seller_expenses: String 37 | option_period_fee: Float 38 | option_period_days: Int 39 | option_period_credit: Boolean 40 | contract_execution_date: Date 41 | digital_signatures: [String] 42 | title_policy_expense: String 43 | title_policy_issuer: String 44 | title_policy_discrepancies_amendments_payment: Float 45 | survey: String 46 | new_survey_days: Int 47 | new_survey_payment: String 48 | amendments: String 49 | createdAt: Date 50 | updatedAt: Date 51 | } 52 | 53 | input ContractInput { 54 | home_id: String 55 | sales_price: Float 56 | buyer_cash: Float 57 | loan_amount: Float 58 | earnest_money: Float 59 | earnest_money_days: Int 60 | earnest_money_to: String 61 | property_condition: String 62 | repairs: String 63 | home_warranty: Boolean 64 | home_warranty_amount: Float 65 | closing_date: Date 66 | special_provisions: String 67 | non_realty_items: String 68 | seller_expenses: String 69 | option_period_fee: Float 70 | option_period_days: Int 71 | option_period_credit: Boolean 72 | contract_execution_date: Date 73 | digital_signatures: [String] 74 | title_policy_expense: String 75 | title_policy_issuer: String 76 | title_policy_discrepancies_amendments_payment: Float 77 | survey: String 78 | new_survey_days: Int 79 | new_survey_payment: String 80 | amendments: String 81 | } 82 | 83 | input ContractFilterInput { 84 | home: ModelIDFilterInput 85 | sales_price: ModelFloatFilterInput 86 | buyer_cash: ModelFloatFilterInput 87 | loan_amount: ModelFloatFilterInput 88 | earnest_money: ModelFloatFilterInput 89 | earnest_money_days: ModelIntFilterInput 90 | earnest_money_to: ModelStringFilterInput 91 | property_condition: ModelStringFilterInput 92 | repairs: ModelStringFilterInput 93 | home_warranty: ModelBooleanFilterInput 94 | home_warranty_amount: ModelFloatFilterInput 95 | closing_date: ModelDateFilterInput 96 | special_provisions: ModelStringFilterInput 97 | non_realty_items: ModelStringFilterInput 98 | seller_expenses: ModelStringFilterInput 99 | option_period_fee: ModelFloatFilterInput 100 | option_period_days: ModelIntFilterInput 101 | option_period_credit: ModelBooleanFilterInput 102 | contract_execution_date: ModelDateFilterInput 103 | title_policy_expense: ModelStringFilterInput 104 | title_policy_issuer: ModelStringFilterInput 105 | title_policy_discrepancies_amendments_payment: ModelFloatFilterInput 106 | survey: ModelStringFilterInput 107 | new_survey_days: ModelIntFilterInput 108 | new_survey_payment: ModelStringFilterInput 109 | amendments: ModelStringFilterInput 110 | } 111 | -------------------------------------------------------------------------------- /src/app/contract/contract.module.ts: -------------------------------------------------------------------------------- 1 | import {forwardRef, Module} from '@nestjs/common'; 2 | import {DatabaseModule} from '../database/database.module'; 3 | import {HomeModule} from '../home/home.module'; 4 | import {UserModule} from '../user/user.module'; 5 | import {contractProviders} from './contract.providers'; 6 | import {ContractResolver} from './contract.resolver'; 7 | import {ContractService} from './contract.service'; 8 | import {ContractVoter} from './security/contract.voter'; 9 | 10 | const PROVIDERS = [ 11 | ...contractProviders, 12 | ContractVoter, 13 | ContractResolver, 14 | ContractService 15 | ]; 16 | 17 | @Module({ 18 | providers: [ 19 | ...PROVIDERS 20 | ], 21 | imports: [ 22 | DatabaseModule, 23 | forwardRef(() => HomeModule), 24 | UserModule 25 | ], 26 | exports: [ 27 | ContractService 28 | ] 29 | }) 30 | export class ContractModule { 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/app/contract/contract.providers.ts: -------------------------------------------------------------------------------- 1 | import {DB_CON_TOKEN} from '../database/database.constants'; 2 | import {CONTRACT_TOKEN} from './contract.constants'; 3 | import {ContractEntity} from './entity'; 4 | import {Connection} from 'typeorm'; 5 | 6 | export const contractProviders = [ 7 | { 8 | provide: CONTRACT_TOKEN, 9 | useFactory: (connection: Connection) => connection.getRepository(ContractEntity), 10 | inject: [DB_CON_TOKEN] 11 | } 12 | ]; 13 | -------------------------------------------------------------------------------- /src/app/contract/contract.resolver.ts: -------------------------------------------------------------------------------- 1 | import {UseGuards} from '@nestjs/common'; 2 | import {Args, Mutation, Parent, Query, ResolveProperty, Resolver, Subscription} from '@nestjs/graphql'; 3 | import {PubSub} from 'graphql-subscriptions'; 4 | import {DeepPartial} from 'typeorm'; 5 | import {GraphqlGuard} from '../_helpers'; 6 | import {User} from '../_helpers/graphql'; 7 | import {AppLogger} from '../app.logger'; 8 | import {ContractFilterInput} from '../graphql.schema'; 9 | import {HomeEntity} from '../home/entity'; 10 | import {HomeService} from '../home/home.service'; 11 | import {UserEntity} from '../user/entity'; 12 | import {UserService} from '../user/user.service'; 13 | import {ContractService} from './contract.service'; 14 | import {ContractEntity} from './entity'; 15 | 16 | @Resolver('Contract') 17 | @UseGuards(GraphqlGuard) 18 | export class ContractResolver { 19 | 20 | private pubSub = new PubSub(); 21 | private logger = new AppLogger(ContractResolver.name); 22 | 23 | constructor( 24 | private readonly contractService: ContractService, 25 | private readonly homeService: HomeService, 26 | private readonly userService: UserService 27 | ) { 28 | } 29 | 30 | @Query('allContractsAsBuyer') 31 | async allContractsAsBuyer( 32 | @User() user: UserEntity, 33 | @Args('filter') filter?: ContractFilterInput, 34 | @Args('limit') limit?: number 35 | ): Promise { 36 | this.logger.log(`[allContractsAsBuyer] ${JSON.stringify(filter)}`); 37 | return this.contractService.findAll({ 38 | where: { 39 | ...filter, 40 | userId: { 41 | eq: user.id.toString() 42 | }, 43 | isDeleted: { 44 | eq: false 45 | } 46 | }, 47 | take: limit 48 | }); 49 | } 50 | 51 | @Query('allContractsAsSeller') 52 | async allContractsAsSeller( 53 | @User() user: UserEntity, 54 | @Args('filter') filter: ContractFilterInput = {}, 55 | @Args('limit') limit?: number 56 | ) { 57 | this.logger.log(`[allContractsAsSeller] ${JSON.stringify(filter)}`); 58 | const homes = await this.homeService.findAll({ 59 | where: { 60 | owner: { 61 | eq: user.id.toString() 62 | } 63 | } 64 | }); 65 | // @ts-ignore 66 | filter.home = {in: homes.map(home => home.id.toString())}; 67 | 68 | return this.contractService.findAll({ 69 | where: { 70 | ...filter 71 | }, 72 | take: limit 73 | }); 74 | } 75 | 76 | @Query('getContract') 77 | async getContract(@Args('id') id: string): Promise { 78 | this.logger.log(`[getContract] ${id}`); 79 | return this.contractService.findOneById(id); 80 | } 81 | 82 | @Mutation('createContract') 83 | async createContract(@User() user: UserEntity, @Args('input') data: DeepPartial): Promise { 84 | this.logger.log(`[createContract] ${JSON.stringify(data)}`); 85 | const contract = this.contractService.create({ 86 | ...data, 87 | userId: user.id.toString() 88 | }); 89 | await this.pubSub.publish('newContract', {newContract: contract}); 90 | return contract; 91 | } 92 | 93 | @Mutation('updateContract') 94 | async updateContract(@Args('id') id: string, @Args('input') data: DeepPartial) { 95 | this.logger.log(`[updateContract] ${JSON.stringify(data)}`); 96 | const contract = this.contractService.patch(id, data); 97 | await this.pubSub.publish('updatedContract', {updatedContract: contract}); 98 | return contract; 99 | } 100 | 101 | @Mutation('deleteContract') 102 | async deleteContract(@Args('id') id: string) { 103 | this.logger.log(`[deleteContract] ${id}`); 104 | const contract = await this.contractService.findOneById(id); 105 | await this.contractService.softDelete(contract); 106 | await this.pubSub.publish('deletedContract', {deletedContract: contract}); 107 | return contract; 108 | } 109 | 110 | @Subscription('newContract') 111 | onNewContract() { 112 | return { 113 | subscribe: () => this.pubSub.asyncIterator('newContract') 114 | }; 115 | } 116 | 117 | @Subscription('updatedContract') 118 | onUpdatedContract() { 119 | return { 120 | subscribe: () => this.pubSub.asyncIterator('updatedContract') 121 | }; 122 | } 123 | 124 | @Subscription('deletedContract') 125 | onDeletedContract() { 126 | return { 127 | subscribe: () => this.pubSub.asyncIterator('deletedContract') 128 | }; 129 | } 130 | 131 | @ResolveProperty('user') 132 | async getUser(@Parent() contract: ContractEntity): Promise { 133 | return this.userService.findOneById(contract.userId); 134 | } 135 | 136 | @ResolveProperty('home') 137 | async getHome(@Parent() contract: ContractEntity): Promise { 138 | return this.homeService.findOneById(contract.home_id); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/app/contract/contract.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@nestjs/common'; 2 | import {MongoRepository} from 'typeorm'; 3 | import {CrudService} from '../../base'; 4 | import {CONTRACT_TOKEN} from './contract.constants'; 5 | import {ContractEntity} from './entity'; 6 | 7 | @Injectable() 8 | export class ContractService extends CrudService { 9 | 10 | constructor(@Inject(CONTRACT_TOKEN) protected readonly repository: MongoRepository) { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/contract/entity/contract.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsBoolean, IsNumber, IsOptional, IsString} from 'class-validator'; 3 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 4 | import {ExtendedEntity} from '../../_helpers/entity'; 5 | 6 | @Entity() 7 | export class ContractEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public userId: string; 17 | 18 | @ApiModelProperty() 19 | @IsString() 20 | @Column() 21 | public home_id: string; 22 | 23 | @ApiModelProperty() 24 | @IsNumber() 25 | @IsOptional() 26 | @Column() 27 | public sales_price: number; 28 | 29 | @ApiModelProperty() 30 | @IsNumber() 31 | @IsOptional() 32 | @Column() 33 | public buyer_cash: number; 34 | 35 | @ApiModelProperty() 36 | @IsNumber() 37 | @IsOptional() 38 | @Column() 39 | public loan_amount: number; 40 | 41 | @ApiModelProperty() 42 | @IsNumber() 43 | @IsOptional() 44 | @Column() 45 | public earnest_money: number; 46 | 47 | @ApiModelProperty() 48 | @IsNumber() 49 | @IsOptional() 50 | @Column() 51 | public earnest_money_days: number; 52 | 53 | @ApiModelProperty() 54 | @IsString() 55 | @IsOptional() 56 | @Column() 57 | public earnest_money_to: string; 58 | 59 | @ApiModelProperty() 60 | @IsString() 61 | @IsOptional() 62 | @Column() 63 | public property_condition: string; 64 | 65 | @ApiModelProperty() 66 | @IsString() 67 | @IsOptional() 68 | @Column() 69 | public repairs: string; 70 | 71 | @ApiModelProperty() 72 | @IsBoolean() 73 | @IsOptional() 74 | @Column() 75 | public home_warranty: boolean; 76 | 77 | @ApiModelProperty() 78 | @IsNumber() 79 | @IsOptional() 80 | @Column() 81 | public home_warranty_amount: number; 82 | 83 | @ApiModelProperty() 84 | @IsOptional() 85 | @Column() 86 | public closing_date: Date; 87 | 88 | @ApiModelProperty() 89 | @IsString() 90 | @IsOptional() 91 | @Column() 92 | public special_provisions: string; 93 | 94 | @ApiModelProperty() 95 | @IsString() 96 | @IsOptional() 97 | @Column() 98 | public non_realty_items: string; 99 | 100 | @ApiModelProperty() 101 | @IsString() 102 | @IsOptional() 103 | @Column() 104 | public seller_expenses: string; 105 | 106 | @ApiModelProperty() 107 | @IsNumber() 108 | @IsOptional() 109 | @Column() 110 | public option_period_fee: number; 111 | 112 | @ApiModelProperty() 113 | @IsNumber() 114 | @IsOptional() 115 | @Column() 116 | public option_period_days: number; 117 | 118 | @ApiModelProperty() 119 | @IsBoolean() 120 | @IsOptional() 121 | @Column() 122 | public option_period_credit: boolean; 123 | 124 | @ApiModelProperty() 125 | @IsOptional() 126 | @Column() 127 | public contract_execution_date: Date; 128 | 129 | @ApiModelProperty() 130 | @IsOptional() 131 | @Column({ 132 | array: true 133 | }) 134 | public digital_signatures: string[]; 135 | 136 | @ApiModelProperty() 137 | @IsString() 138 | @IsOptional() 139 | @Column() 140 | public title_policy_expense: string; 141 | 142 | @ApiModelProperty() 143 | @IsString() 144 | @IsOptional() 145 | @Column() 146 | public title_policy_issuer: string; 147 | 148 | @ApiModelProperty() 149 | @IsNumber() 150 | @IsOptional() 151 | @Column() 152 | public title_policy_discrepancies_amendments_payment: number; 153 | 154 | @ApiModelProperty() 155 | @IsString() 156 | @IsOptional() 157 | @Column() 158 | public survey: string; 159 | 160 | @ApiModelProperty() 161 | @IsNumber() 162 | @IsOptional() 163 | @Column() 164 | public new_survey_days: number; 165 | 166 | @ApiModelProperty() 167 | @IsString() 168 | @IsOptional() 169 | @Column() 170 | public new_survey_payment: string; 171 | 172 | @ApiModelProperty() 173 | @IsString() 174 | @IsOptional() 175 | @Column() 176 | public amendments: string; 177 | 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/app/contract/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contract.entity'; 2 | -------------------------------------------------------------------------------- /src/app/contract/security/contract.voter.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {AppLogger} from '../../app.logger'; 3 | import {HomeService} from '../../home/home.service'; 4 | import {RestVoterActionEnum, Voter} from '../../security'; 5 | import {UserEntity} from '../../user/entity'; 6 | import {ContractEntity} from '../entity'; 7 | 8 | @Injectable() 9 | export class ContractVoter extends Voter { 10 | 11 | private logger = new AppLogger(ContractVoter.name); 12 | 13 | private readonly attributes = [ 14 | RestVoterActionEnum.READ_ALL, 15 | RestVoterActionEnum.READ, 16 | RestVoterActionEnum.CREATE, 17 | RestVoterActionEnum.DELETE 18 | ]; 19 | 20 | constructor( 21 | private readonly homeService: HomeService 22 | ) { 23 | super(); 24 | } 25 | 26 | protected supports(attribute: any, subject: any): boolean { 27 | if (!this.attributes.includes(attribute)) { 28 | return false; 29 | } 30 | 31 | if (Array.isArray(subject)) { 32 | return subject.every(element => element instanceof ContractEntity); 33 | } 34 | 35 | return subject instanceof ContractEntity; 36 | } 37 | 38 | protected async voteOnAttribute(attribute: string, subject: ContractEntity | ContractEntity[], context): Promise { 39 | const user = context.getUser(); 40 | 41 | switch (attribute) { 42 | case RestVoterActionEnum.READ_ALL: 43 | return this.canReadAll(subject as ContractEntity[], user); 44 | case RestVoterActionEnum.READ: 45 | return this.canRead(subject as ContractEntity, user); 46 | case RestVoterActionEnum.CREATE: 47 | return this.canCreate(subject as ContractEntity, user); 48 | case RestVoterActionEnum.UPDATE: 49 | return this.canUpdate(subject as ContractEntity, user); 50 | case RestVoterActionEnum.DELETE: 51 | return this.canDelete(subject as ContractEntity, user); 52 | } 53 | 54 | return Promise.resolve(false); 55 | } 56 | 57 | private async canReadAll(contracts: ContractEntity[], user: UserEntity): Promise { 58 | this.logger.debug('[canReadAll] everybody can read thier contracts'); 59 | return true; 60 | } 61 | 62 | private async canRead(contract: ContractEntity, user: UserEntity): Promise { 63 | this.logger.debug(`[canRead] any user can read contract ${contract.id}`); 64 | return true; 65 | } 66 | 67 | private canCreate(contract: ContractEntity, user: UserEntity) { 68 | this.logger.debug('[canCreate] everybody can create contracts'); 69 | return true; 70 | } 71 | 72 | private async canUpdate(contract: ContractEntity, user: UserEntity) { 73 | this.logger.debug('[canUpdate] only owner of the contract can update his contract'); 74 | return contract.userId === user.id.toString(); 75 | } 76 | 77 | private async canDelete(contract: ContractEntity, user: UserEntity) { 78 | this.logger.debug('[canDelete] only owner of the contract or house can delete his contract'); 79 | const userId = user.id.toString(); 80 | const home = await this.homeService.findOneById(contract.home_id); 81 | return contract.userId === userId || home.owner === userId; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/app/database/database.constants.ts: -------------------------------------------------------------------------------- 1 | export const DB_CON_TOKEN = 'DbConnectionToken'; 2 | export const AWS_CON_TOKEN = 'AwsConnectionToken'; 3 | -------------------------------------------------------------------------------- /src/app/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { databaseProviders } from './database.providers'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [...databaseProviders], 7 | exports: [...databaseProviders] 8 | }) 9 | export class DatabaseModule { 10 | } 11 | -------------------------------------------------------------------------------- /src/app/database/database.providers.ts: -------------------------------------------------------------------------------- 1 | import {config} from '../../config'; 2 | import {AWS_CON_TOKEN, DB_CON_TOKEN} from './database.constants'; 3 | import AWS from 'aws-sdk'; 4 | import {createConnection} from 'typeorm'; 5 | 6 | export const databaseProviders = [ 7 | { 8 | provide: AWS_CON_TOKEN, 9 | useFactory: async () => { 10 | AWS.config.update({ 11 | accessKeyId: config.aws.api_key, 12 | secretAccessKey: config.aws.secret_key, 13 | region: config.aws.region 14 | }); 15 | return new AWS.DynamoDB(); 16 | } 17 | }, 18 | { 19 | provide: DB_CON_TOKEN, 20 | useFactory: async () => createConnection(config.database) 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/healtcheck/healthcheck.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, Get} from '@nestjs/common'; 2 | 3 | @Controller() 4 | export class HealthCheckController { 5 | private start: number; 6 | 7 | constructor() { 8 | this.start = Date.now(); 9 | } 10 | 11 | @Get('healthcheck') 12 | async get() { 13 | const now = Date.now(); 14 | return { 15 | status: 'API Online', 16 | uptime: Number((now - this.start) / 1000).toFixed(0) 17 | }; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/healtcheck/healthcheck.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {HealthCheckController} from './healthcheck.controller'; 3 | 4 | @Module({ 5 | controllers: [HealthCheckController] 6 | }) 7 | export class HealthCheckModule {} 8 | -------------------------------------------------------------------------------- /src/app/home-favorite/dto/create-home-favorites.dto.ts: -------------------------------------------------------------------------------- 1 | import {CreateHomeFavoriteInput} from '../../graphql.schema'; 2 | 3 | export class CreateHomeFavoriteDto extends CreateHomeFavoriteInput {} 4 | -------------------------------------------------------------------------------- /src/app/home-favorite/dto/delete-home-favorites.dto.ts: -------------------------------------------------------------------------------- 1 | import {DeleteHomeFavoriteInput} from '../../graphql.schema'; 2 | 3 | export class DeleteHomeFavoriteDto extends DeleteHomeFavoriteInput {} 4 | -------------------------------------------------------------------------------- /src/app/home-favorite/dto/index.ts: -------------------------------------------------------------------------------- 1 | import {CreateHomeFavoriteDto} from './create-home-favorites.dto'; 2 | import {DeleteHomeFavoriteDto} from './delete-home-favorites.dto'; 3 | 4 | export { 5 | CreateHomeFavoriteDto, 6 | DeleteHomeFavoriteDto 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/home-favorite/entity/home-favorite.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsString} from 'class-validator'; 3 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 4 | import {ExtendedEntity} from '../../_helpers'; 5 | 6 | @Entity() 7 | export class HomeFavoriteEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | 14 | @ApiModelProperty() 15 | @IsString() 16 | @Column() 17 | public homeFavoriteUserId: string; 18 | 19 | @ApiModelProperty() 20 | @IsString() 21 | @Column() 22 | public homeFavoriteHomeId: string; 23 | 24 | public fake = false; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/home-favorite/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home-favorite.entity'; 2 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.constants.ts: -------------------------------------------------------------------------------- 1 | export const HOME_FAVORITE_TOKEN = 'HomeFavoriteRepositoryToken'; 2 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Client, ClientProxy, MessagePattern, Transport} from '@nestjs/microservices'; 3 | import {AppLogger} from '../app.logger'; 4 | import {HOME_CMD_DELETE} from '../home/home.constants'; 5 | import {HomeFavoriteService} from './home-favorite.service'; 6 | import {HomeEntity} from '../home/entity'; 7 | 8 | @Controller('home-favorite') 9 | export class HomeFavoriteController { 10 | 11 | @Client({ transport: Transport.TCP }) 12 | private client: ClientProxy; 13 | 14 | private logger = new AppLogger(HomeFavoriteController.name); 15 | 16 | constructor(private readonly homeFavoriteService: HomeFavoriteService) { 17 | 18 | } 19 | 20 | @MessagePattern({ cmd: HOME_CMD_DELETE }) 21 | public async onHomeDelete(home: HomeEntity): Promise { 22 | this.logger.debug(`[onHomeDelete] soft delete all favorites for home ${home.id}`); 23 | await this.homeFavoriteService.updateAll({homeFavoriteHomeId: {eq: home.id.toString()}}, {'$set': {isDeleted: true}}); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getHomeFavorites: [HomeFavorite] 3 | getHomeFavorite(id: ID!): HomeFavorite 4 | } 5 | 6 | type Mutation { 7 | createHomeFavorite(createHomeFavoriteInput: CreateHomeFavoriteInput): HomeFavorite 8 | deleteHomeFavorite(deleteHomeFavoriteInput: DeleteHomeFavoriteInput): HomeFavorite 9 | } 10 | 11 | type Subscription { 12 | homeFavoriteCreated: HomeFavorite 13 | } 14 | 15 | type HomeFavorite { 16 | id: ID! 17 | homeFavoriteUserId: String 18 | homeFavoriteHomeId: String 19 | home: Home 20 | } 21 | 22 | input CreateHomeFavoriteInput { 23 | homeFavoriteUserId: String 24 | homeFavoriteHomeId: String 25 | } 26 | 27 | input DeleteHomeFavoriteInput { 28 | id: ID! 29 | } 30 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.guard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ExecutionContext, Injectable} from '@nestjs/common'; 2 | import {GqlExecutionContext} from '@nestjs/graphql'; 3 | 4 | @Injectable() 5 | export class HomeFavoriteGuard implements CanActivate { 6 | canActivate(context: ExecutionContext): boolean { 7 | const ctx = GqlExecutionContext.create(context); 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.module.ts: -------------------------------------------------------------------------------- 1 | import {forwardRef, Module} from '@nestjs/common'; 2 | import {HomeModule} from '../home/home.module'; 3 | import {HomeFavoriteService} from './home-favorite.service'; 4 | import {HomeFavoriteResolver} from './home-favorite.resolver'; 5 | import {homeFavoriteProviders} from './home-favorite.providers'; 6 | import {DatabaseModule} from '../database/database.module'; 7 | import {HomeFavoriteController} from './home-favorite.controller'; 8 | 9 | @Module({ 10 | controllers: [HomeFavoriteController], 11 | providers: [...homeFavoriteProviders, HomeFavoriteService, HomeFavoriteResolver], 12 | imports: [DatabaseModule, forwardRef(() => HomeModule)], 13 | exports: [HomeFavoriteService] 14 | }) 15 | export class HomeFavoriteModule { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.providers.ts: -------------------------------------------------------------------------------- 1 | import {DB_CON_TOKEN} from '../database/database.constants'; 2 | import {HomeFavoriteEntity} from './entity'; 3 | import {HOME_FAVORITE_TOKEN} from './home-favorite.constants'; 4 | import {Connection} from 'typeorm'; 5 | 6 | export const homeFavoriteProviders = [ 7 | { 8 | provide: HOME_FAVORITE_TOKEN, 9 | useFactory: (connection: Connection) => connection.getRepository(HomeFavoriteEntity), 10 | inject: [DB_CON_TOKEN] 11 | } 12 | ]; 13 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.resolver.ts: -------------------------------------------------------------------------------- 1 | import {UseGuards} from '@nestjs/common'; 2 | import {Args, Mutation, Query, Resolver, Subscription, ResolveProperty, Parent} from '@nestjs/graphql'; 3 | import {PubSub} from 'graphql-subscriptions'; 4 | import {GraphqlGuard} from '../_helpers'; 5 | import {User as CurrentUser} from '../_helpers/graphql/user.decorator'; 6 | import {Home, HomeFavorite} from '../graphql.schema'; 7 | import {HomeEntity} from '../home/entity'; 8 | import {HomeService} from '../home/home.service'; 9 | import {UserEntity as User} from '../user/entity'; 10 | import {CreateHomeFavoriteDto, DeleteHomeFavoriteDto} from './dto'; 11 | import {HomeFavoriteService} from './home-favorite.service'; 12 | 13 | @Resolver('HomeFavorite') 14 | @UseGuards(GraphqlGuard) 15 | export class HomeFavoriteResolver { 16 | private pubSub = new PubSub(); 17 | 18 | constructor( 19 | private readonly homeFavoriteService: HomeFavoriteService, 20 | private readonly homeService: HomeService 21 | ) { 22 | } 23 | 24 | @Query('getHomeFavorites') 25 | async findAll(@CurrentUser() user: User): Promise { 26 | return this.homeFavoriteService.findAll({ 27 | where : { 28 | homeFavoriteUserId: { 29 | eq: user.id.toString() 30 | }, 31 | isDeleted: { 32 | eq: false 33 | } 34 | } 35 | }); 36 | } 37 | 38 | @Query('getHomeFavorite') 39 | async findOneById(@Args('id') id: string): Promise { 40 | return this.homeFavoriteService.findOneById(id); 41 | } 42 | 43 | @Mutation('createHomeFavorite') 44 | async create(@CurrentUser() user: User, @Args('createHomeFavoriteInput') args: CreateHomeFavoriteDto): Promise { 45 | args.homeFavoriteUserId = user.id.toString(); 46 | const createdHomeFavorite = await this.homeFavoriteService.create(args); 47 | await this.pubSub.publish('homeFavoriteCreated', {homeCreatedFavorite: createdHomeFavorite}); 48 | return createdHomeFavorite; 49 | } 50 | 51 | @Mutation('deleteHomeFavorite') 52 | async delete(@CurrentUser() user: User, @Args('deleteHomeFavoriteInput') args: DeleteHomeFavoriteDto): Promise { 53 | const deletedHomeFavorite = await this.homeFavoriteService.delete(args.id); 54 | await this.pubSub.publish('homeFavoriteDeleted', {homeFavoriteDeleted: deletedHomeFavorite}); 55 | return deletedHomeFavorite; 56 | } 57 | 58 | @Subscription('homeFavoriteCreated') 59 | homeFavoriteCreated() { 60 | return { 61 | subscribe: () => this.pubSub.asyncIterator('homeFavoriteCreated') 62 | }; 63 | } 64 | 65 | @ResolveProperty('home') 66 | getHome(@Parent() homeFavorite: HomeFavorite): Promise { 67 | return this.homeService.findOneById(homeFavorite.homeFavoriteHomeId); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@nestjs/common'; 2 | import {MongoRepository} from 'typeorm'; 3 | import {CrudService} from '../../base'; 4 | import {HomeFavoriteEntity} from './entity'; 5 | import {HOME_FAVORITE_TOKEN} from './home-favorite.constants'; 6 | 7 | @Injectable() 8 | export class HomeFavoriteService extends CrudService { 9 | 10 | constructor(@Inject(HOME_FAVORITE_TOKEN) protected readonly repository: MongoRepository) { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/home-favorite/home-favorite.validator.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neoteric-eu/nestjs-auth/dc294e4ce2436579dea9958e24b90c0cc0ef2569/src/app/home-favorite/home-favorite.validator.ts -------------------------------------------------------------------------------- /src/app/home-favorite/security/home-favorite.voter.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {RestVoterActionEnum, Voter} from '../../security'; 3 | import {UserEntity} from '../../user/entity'; 4 | import {AppLogger} from '../../app.logger'; 5 | import {HomeFavoriteEntity} from '../entity'; 6 | 7 | @Injectable() 8 | export class HomeFavoriteVoter extends Voter { 9 | 10 | private logger = new AppLogger(HomeFavoriteVoter.name); 11 | 12 | private readonly attributes = [ 13 | RestVoterActionEnum.READ_ALL, 14 | RestVoterActionEnum.CREATE, 15 | RestVoterActionEnum.DELETE 16 | ]; 17 | 18 | constructor() { 19 | super(); 20 | } 21 | 22 | protected supports(attribute: any, subject: any): boolean { 23 | if (!this.attributes.includes(attribute)) { 24 | return false; 25 | } 26 | 27 | if (Array.isArray(subject)) { 28 | return subject.every(element => element instanceof HomeFavoriteEntity); 29 | } 30 | 31 | return subject instanceof HomeFavoriteEntity; 32 | } 33 | 34 | protected async voteOnAttribute(attribute: string, subject: HomeFavoriteEntity | HomeFavoriteEntity[], context): Promise { 35 | const user = context.getUser(); 36 | 37 | if (!(user instanceof UserEntity)) { 38 | return false; 39 | } 40 | 41 | switch (attribute) { 42 | case RestVoterActionEnum.READ_ALL: 43 | return this.canReadAll(subject as HomeFavoriteEntity[], user); 44 | case RestVoterActionEnum.CREATE: 45 | return this.canCreate(subject as HomeFavoriteEntity, user); 46 | case RestVoterActionEnum.DELETE: 47 | return this.canDelete(subject as HomeFavoriteEntity, user); 48 | } 49 | 50 | return Promise.resolve(false); 51 | } 52 | 53 | private async canReadAll(homeFavorites: HomeFavoriteEntity[], user: UserEntity): Promise { 54 | this.logger.debug('[canReadAll] everybody can read their home favorites'); 55 | return true; 56 | } 57 | 58 | private async canCreate(homeFavorite: HomeFavoriteEntity, user: UserEntity): Promise { 59 | this.logger.debug('[canReadAll] everybody can add home to their home favorites'); 60 | return true; 61 | } 62 | 63 | private async canDelete(homeFavorite: HomeFavoriteEntity, user: UserEntity): Promise { 64 | this.logger.debug('[canDelete] only owner of the house can delete their favorites'); 65 | return homeFavorite.homeFavoriteUserId === user.id.toString(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/home-media/entity/home-media.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsNumber, IsString} from 'class-validator'; 3 | import {ExtendedEntity} from '../../_helpers/entity'; 4 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 5 | 6 | @Entity() 7 | export class HomeMediaEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | homeId: string; 17 | 18 | @ApiModelProperty() 19 | @IsString() 20 | @Column() 21 | originalname: string; 22 | 23 | @ApiModelProperty() 24 | @IsString() 25 | @Column() 26 | mimetype: string; 27 | 28 | @ApiModelProperty() 29 | @IsNumber() 30 | @Column() 31 | size: number; 32 | 33 | @ApiModelProperty() 34 | @IsString() 35 | @Column() 36 | url: string; 37 | 38 | @ApiModelProperty() 39 | @IsString() 40 | @Column() 41 | type: string; 42 | 43 | @ApiModelProperty() 44 | @IsNumber() 45 | @Column() 46 | order: number; 47 | 48 | @ApiModelProperty() 49 | @IsString() 50 | @Column() 51 | caption: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/app/home-media/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home-media.entity'; 2 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.constants.ts: -------------------------------------------------------------------------------- 1 | export const HOME_MEDIA_TOKEN = 'HomeMediaToken'; 2 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {Client, ClientProxy, MessagePattern, Transport} from '@nestjs/microservices'; 3 | import {AppLogger} from '../app.logger'; 4 | import {HomeEntity} from '../home/entity'; 5 | import {HOME_CMD_DELETE} from '../home/home.constants'; 6 | import {MEDIA_CMD_DELETE} from '../media/media.constants'; 7 | import {HomeMediaService} from './home-media.service'; 8 | 9 | @Controller('home-favorite') 10 | export class HomeMediaController { 11 | 12 | @Client({ transport: Transport.TCP }) 13 | private client: ClientProxy; 14 | 15 | private logger = new AppLogger(HomeMediaController.name); 16 | 17 | constructor(private readonly homeMediaService: HomeMediaService) { 18 | 19 | } 20 | 21 | @MessagePattern({ cmd: HOME_CMD_DELETE }) 22 | public async onMediaDelete(home: HomeEntity): Promise { 23 | this.logger.debug(`[onMediaDelete] soft delete all medias for home ${home.id}`); 24 | const homeMedias = await this.homeMediaService.findAll({where: {homeId: home.id.toString()}}); 25 | await this.homeMediaService.updateAll({homeId: {eq: home.id.toString()}}, {'$set': {isDeleted: true}}); 26 | for (const homeMedia of homeMedias) { 27 | this.client.send({cmd: MEDIA_CMD_DELETE}, homeMedia).subscribe(() => {}, error => { 28 | this.logger.error(error, ''); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getHomeMedia(homeId: ID!): [HomeMedia] 3 | } 4 | 5 | type Mutation { 6 | createHomeMedia(createHomeMediaInput: CreateHomeMediaInput): HomeMedia 7 | updateHomeMedia(updateHomeMediaInput: UpdateHomeMediaInput): HomeMedia 8 | deleteHomeMedia(deleteHomeMediaInput: DeleteHomeMediaInput): HomeMedia 9 | } 10 | 11 | type Subscription { 12 | homeMediaCreated: HomeMedia 13 | homeMediaUpdated: HomeMedia 14 | homeMediaDeleted: HomeMedia 15 | } 16 | 17 | type HomeMedia { 18 | id: ID! 19 | homeId: String 20 | originalname: String 21 | mimetype: String 22 | size: Int 23 | url: String 24 | type: String! 25 | order: Int! 26 | caption: String! 27 | } 28 | 29 | input CreateHomeMediaInput { 30 | homeId: String 31 | originalname: String 32 | mimetype: String 33 | size: Int 34 | url: String 35 | type: String! 36 | order: Int! 37 | caption: String! 38 | } 39 | 40 | input UpdateHomeMediaInput { 41 | id: ID! 42 | order: Int! 43 | caption: String! 44 | } 45 | 46 | input DeleteHomeMediaInput { 47 | id: ID! 48 | } 49 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {DatabaseModule} from '../database/database.module'; 3 | import {homeMediaProviders} from './home-media.providers'; 4 | import {HomeMediaService} from './home-media.service'; 5 | import {HomeMediaResolver} from './home-media.resolver'; 6 | import {HomeMediaController} from './home-media.controller'; 7 | 8 | @Module({ 9 | controllers: [HomeMediaController], 10 | providers: [...homeMediaProviders, HomeMediaService, HomeMediaResolver], 11 | imports: [DatabaseModule], 12 | exports: [HomeMediaService] 13 | }) 14 | export class HomeMediaModule { 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.providers.ts: -------------------------------------------------------------------------------- 1 | import {DB_CON_TOKEN} from '../database/database.constants'; 2 | import {HomeMediaEntity} from './entity'; 3 | import {HOME_MEDIA_TOKEN} from './home-media.constants'; 4 | import {Connection} from 'typeorm'; 5 | 6 | export const homeMediaProviders = [ 7 | { 8 | provide: HOME_MEDIA_TOKEN, 9 | useFactory: (connection: Connection) => connection.getRepository(HomeMediaEntity), 10 | inject: [DB_CON_TOKEN] 11 | } 12 | ]; 13 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.resolver.ts: -------------------------------------------------------------------------------- 1 | import {UseGuards} from '@nestjs/common'; 2 | import {Args, Mutation, Query, Resolver, Subscription} from '@nestjs/graphql'; 3 | import {Client, ClientProxy, Transport} from '@nestjs/microservices'; 4 | import {PubSub} from 'graphql-subscriptions'; 5 | import {MEDIA_CMD_DELETE} from '../media/media.constants'; 6 | import {HomeMediaService} from './home-media.service'; 7 | import {CreateHomeMediaInput, UpdateHomeMediaInput, DeleteHomeMediaInput, HomeMedia} from '../graphql.schema'; 8 | import {GraphqlGuard} from '../_helpers'; 9 | import {User as CurrentUser} from '../_helpers/graphql/user.decorator'; 10 | import {UserEntity as User} from '../user/entity'; 11 | import {AppLogger} from '../app.logger'; 12 | 13 | @Resolver('HomeMedia') 14 | @UseGuards(GraphqlGuard) 15 | export class HomeMediaResolver { 16 | 17 | private pubSub = new PubSub(); 18 | private logger = new AppLogger(HomeMediaResolver.name); 19 | 20 | @Client({ transport: Transport.TCP }) 21 | private client: ClientProxy; 22 | 23 | 24 | constructor(private readonly homeMediaService: HomeMediaService) { 25 | } 26 | 27 | @Query('getHomeMedia') 28 | async findAll(@Args('homeId') homeId: string): Promise { 29 | return this.homeMediaService.findAll({where: {homeId: {eq: homeId}}}); 30 | } 31 | 32 | @Mutation('createHomeMedia') 33 | async create(@CurrentUser() user: User, @Args('createHomeMediaInput') args: CreateHomeMediaInput): Promise { 34 | const createdHomeMedia = await this.homeMediaService.create(args); 35 | await this.pubSub.publish('homeMediaCreated', {homeMediaCreated: createdHomeMedia}); 36 | return createdHomeMedia; 37 | } 38 | 39 | @Mutation('updateHomeMedia') 40 | async update(@CurrentUser() user: User, @Args('updateHomeMediaInput') args: UpdateHomeMediaInput): Promise { 41 | const updatedHomeMedia = await this.homeMediaService.update(args); 42 | await this.pubSub.publish('homeMediaUpdated', {homeMediaCreated: updatedHomeMedia}); 43 | return updatedHomeMedia; 44 | } 45 | 46 | @Mutation('deleteHomeMedia') 47 | async delete(@CurrentUser() user: User, @Args('deleteHomeMediaInput') args: DeleteHomeMediaInput): Promise { 48 | const deletedHomeMedia = await this.homeMediaService.delete(args.id); 49 | await this.pubSub.publish('homeMediaDeleted', {homeMediaDeleted: deletedHomeMedia}); 50 | this.client.send({cmd: MEDIA_CMD_DELETE}, deletedHomeMedia).subscribe(() => {}, error => { 51 | this.logger.error(error, ''); 52 | }); 53 | return deletedHomeMedia; 54 | } 55 | 56 | @Subscription('homeMediaCreated') 57 | homeMediaCreated() { 58 | return { 59 | subscribe: () => this.pubSub.asyncIterator('homeMediaCreated') 60 | }; 61 | } 62 | 63 | @Subscription('homeMediaUpdated') 64 | homeMediaUpdated() { 65 | return { 66 | subscribe: () => this.pubSub.asyncIterator('homeMediaUpdated') 67 | }; 68 | } 69 | 70 | @Subscription('homeMediaDeleted') 71 | homeMediaDeleted() { 72 | return { 73 | subscribe: () => this.pubSub.asyncIterator('homeMediaDeleted') 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/home-media/home-media.service.ts: -------------------------------------------------------------------------------- 1 | import {CrudService} from '../../base'; 2 | import {Inject, Injectable} from '@nestjs/common'; 3 | import {HomeMediaEntity} from './entity'; 4 | import {HOME_MEDIA_TOKEN} from './home-media.constants'; 5 | import {MongoRepository, Repository} from 'typeorm'; 6 | 7 | @Injectable() 8 | export class HomeMediaService extends CrudService { 9 | 10 | constructor(@Inject(HOME_MEDIA_TOKEN) protected readonly repository: MongoRepository) { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/home-media/security/home-media.voter.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {RestVoterActionEnum, Voter} from '../../security'; 3 | import {UserEntity} from '../../user/entity'; 4 | import {AppLogger} from '../../app.logger'; 5 | import {HomeMediaService} from '../home-media.service'; 6 | import {HomeMediaEntity} from '../entity'; 7 | import {HomeService} from '../../home/home.service'; 8 | 9 | @Injectable() 10 | export class HomeMediaVoter extends Voter { 11 | 12 | private logger = new AppLogger(HomeMediaVoter.name); 13 | 14 | private readonly attributes = [ 15 | RestVoterActionEnum.READ_ALL, 16 | RestVoterActionEnum.CREATE, 17 | RestVoterActionEnum.DELETE 18 | ]; 19 | 20 | constructor( 21 | private readonly homeMediaService: HomeMediaService, 22 | private readonly homeService: HomeService 23 | ) { 24 | super(); 25 | } 26 | 27 | protected supports(attribute: any, subject: any): boolean { 28 | if (!this.attributes.includes(attribute)) { 29 | return false; 30 | } 31 | 32 | if (Array.isArray(subject)) { 33 | return subject.every(element => element instanceof HomeMediaEntity); 34 | } 35 | 36 | return subject instanceof HomeMediaEntity; 37 | } 38 | 39 | protected async voteOnAttribute(attribute: string, subject: HomeMediaEntity | HomeMediaEntity[], context): Promise { 40 | const user = context.getUser(); 41 | 42 | if (!(user instanceof UserEntity) && attribute !== RestVoterActionEnum.READ_ALL) { 43 | return false; 44 | } 45 | 46 | switch (attribute) { 47 | case RestVoterActionEnum.READ_ALL: 48 | return this.canReadAll(subject as HomeMediaEntity[], user); 49 | case RestVoterActionEnum.CREATE: 50 | return this.canCreate(subject as HomeMediaEntity, user); 51 | case RestVoterActionEnum.DELETE: 52 | return this.canDelete(subject as HomeMediaEntity, user); 53 | } 54 | 55 | return Promise.resolve(false); 56 | } 57 | 58 | private async canReadAll(homeMedias: HomeMediaEntity[], user: UserEntity): Promise { 59 | this.logger.debug('[canReadAll] everybody can read home medias'); 60 | return true; 61 | } 62 | 63 | private async canCreate(homeMedia: HomeMediaEntity, user: UserEntity): Promise { 64 | this.logger.debug('[canCreate] only owner of the house can create home media'); 65 | const home = await this.homeService.findOneById(homeMedia.homeId); 66 | return home.owner === user.id.toString(); 67 | } 68 | 69 | private async canDelete(homeMedia: HomeMediaEntity, user: UserEntity): Promise { 70 | this.logger.debug('[canDelete] only owner of the house can delete home media'); 71 | const home = await this.homeService.findOneById(homeMedia.homeId); 72 | return home.owner === user.id.toString(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/app/home/attom-data-api.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpException, HttpService, HttpStatus, Injectable} from '@nestjs/common'; 2 | import {config} from '../../config'; 3 | import {AppLogger} from '../app.logger'; 4 | import {PropertyApi} from './attom/property-api'; 5 | import {HomeErrorEnum} from './home-error.enum'; 6 | 7 | @Injectable() 8 | export class AttomDataApiService { 9 | 10 | private logger = new AppLogger(AttomDataApiService.name); 11 | 12 | constructor(private httpService: HttpService) { 13 | } 14 | 15 | public async getAVMDetail({address1, address2}) { 16 | try { 17 | return await this.httpService.get(`${config.homeApi.attomData.apiUrl}/attomavm/detail`, { 18 | params: { 19 | address1, 20 | address2 21 | }, 22 | headers: { 23 | apiKey: config.homeApi.attomData.apiKey 24 | } 25 | }).toPromise(); 26 | } catch (e) { 27 | if (e.response.status === 403) { 28 | this.handleAttomError(e.response.data.Response.status); 29 | } else { 30 | this.handleAttomError(e.response.data.status); 31 | } 32 | } 33 | } 34 | 35 | public async getDetailWithSchools({id}) { 36 | try { 37 | return await this.httpService.get(`${config.homeApi.attomData.apiUrl}/property/detailwithschools`, { 38 | params: { 39 | id 40 | }, 41 | headers: { 42 | apiKey: config.homeApi.attomData.apiKey 43 | } 44 | }).toPromise(); 45 | } catch (e) { 46 | this.handleAttomError(e.response); 47 | } 48 | } 49 | 50 | public async getSchoolDetail({id}) { 51 | try { 52 | return await this.httpService.get(`${config.homeApi.attomData.apiUrl}/school/detail`, { 53 | params: { 54 | id 55 | }, 56 | headers: { 57 | apiKey: config.homeApi.attomData.apiKey 58 | } 59 | }).toPromise(); 60 | } catch (e) { 61 | this.handleAttomError(e.response); 62 | } 63 | } 64 | 65 | public async getLocation({address}) { 66 | try { 67 | return await this.httpService.get(config.googleApi.apiUrl, { 68 | params: { 69 | address, 70 | key: config.googleApi.apiKey 71 | } 72 | }).toPromise(); 73 | } catch (e) { 74 | this.logger.error(e.message, e.stack); 75 | } 76 | } 77 | 78 | private handleAttomError(status) { 79 | let statuscode = PropertyApi.STATUS_CODES['' + status.code]; 80 | 81 | if (!statuscode) { 82 | statuscode = { 83 | message: 'Undefined error' 84 | }; 85 | } 86 | if (!statuscode.condition) { 87 | statuscode.condition = HomeErrorEnum.ATTOM_DEFAULT_API_ERROR; 88 | this.logger.warn(`Missing condition for status ${status.code}, use default as ATTOM_API_ERROR ${statuscode.condition}`); 89 | } 90 | 91 | this.logger.warn(`[handleAttomError] ${statuscode.name}`); 92 | 93 | throw new HttpException({ 94 | error: 'Home', 95 | condition: statuscode.condition, 96 | message: statuscode.message 97 | }, HttpStatus.BAD_REQUEST); 98 | } 99 | } 100 | 101 | -------------------------------------------------------------------------------- /src/app/home/dto/condition-score.dto.ts: -------------------------------------------------------------------------------- 1 | import {Expose, Type} from 'class-transformer'; 2 | import {ScoreOutput} from '../../graphql.schema'; 3 | 4 | export class Condition { 5 | 6 | @Expose() 7 | avg_condition: number; 8 | 9 | @Expose() 10 | count: number; 11 | } 12 | 13 | export class ConditionScoreDto extends ScoreOutput { 14 | 15 | @Expose() 16 | @Type(() => Condition) 17 | public bathrooms: Condition; 18 | 19 | @Expose() 20 | @Type(() => Condition) 21 | public exterior: Condition; 22 | 23 | @Expose() 24 | @Type(() => Condition) 25 | public interior: Condition; 26 | 27 | @Expose() 28 | @Type(() => Condition) 29 | public kitchen: Condition; 30 | 31 | @Expose() 32 | @Type(() => Condition) 33 | public overall: Condition; 34 | } 35 | -------------------------------------------------------------------------------- /src/app/home/dto/convert-template.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | 3 | export class ConvertTemplateDto { 4 | 5 | @ApiModelProperty() 6 | public template: string; 7 | 8 | @ApiModelProperty() 9 | public mainMedia: string; 10 | 11 | @ApiModelProperty() 12 | public firstMedia: string; 13 | 14 | @ApiModelProperty() 15 | public secondMedia: string; 16 | 17 | @ApiModelProperty() 18 | public thirdMedia: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/app/home/dto/create-home.dto.ts: -------------------------------------------------------------------------------- 1 | import {CreateHomeInput} from '../../graphql.schema'; 2 | 3 | export class CreateHomeDto extends CreateHomeInput {} 4 | -------------------------------------------------------------------------------- /src/app/home/dto/delete-home.dto.ts: -------------------------------------------------------------------------------- 1 | import {DeleteHomeInput} from '../../graphql.schema'; 2 | 3 | export class DeleteHomeDto extends DeleteHomeInput {} 4 | -------------------------------------------------------------------------------- /src/app/home/dto/index.ts: -------------------------------------------------------------------------------- 1 | import { DeleteHomeDto } from './delete-home.dto'; 2 | import { UpdateHomeDto } from './update-home.dto'; 3 | import { CreateHomeDto } from './create-home.dto'; 4 | 5 | export { 6 | DeleteHomeDto, 7 | UpdateHomeDto, 8 | CreateHomeDto 9 | }; 10 | -------------------------------------------------------------------------------- /src/app/home/dto/update-home.dto.ts: -------------------------------------------------------------------------------- 1 | import {UpdateHomeInput} from '../../graphql.schema'; 2 | 3 | export class UpdateHomeDto extends UpdateHomeInput {} 4 | -------------------------------------------------------------------------------- /src/app/home/entity/home-pdf.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsString} from 'class-validator'; 3 | import {ExtendedEntity} from '../../_helpers'; 4 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 5 | 6 | @Entity() 7 | export class HomePdfEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public homeId: string; 17 | 18 | @ApiModelProperty() 19 | @IsString() 20 | @Column() 21 | public sha1: string; 22 | 23 | @ApiModelProperty() 24 | @IsString() 25 | @Column() 26 | public bucket: string; 27 | 28 | @ApiModelProperty() 29 | @IsString() 30 | @Column() 31 | public key: string; 32 | 33 | public getUrl(): string { 34 | return `https://${this.bucket}.s3.amazonaws.com/${this.key}`; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/home/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.entity'; 2 | export * from './home-pdf.entity'; 3 | -------------------------------------------------------------------------------- /src/app/home/foxyai.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getScore(scoreInput: ScoreInput): ScoreOutput 3 | } 4 | 5 | input ScoreInput { 6 | urls: [String] 7 | } 8 | 9 | type ScoreOutput { 10 | bathrooms: ScoreCondition 11 | exterior: ScoreCondition 12 | interior: ScoreCondition 13 | kitchen: ScoreCondition 14 | overall: ScoreCondition 15 | } 16 | 17 | type ScoreCondition { 18 | avg_condition: Float 19 | count: Int 20 | } 21 | -------------------------------------------------------------------------------- /src/app/home/foxyai.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpService, Injectable} from '@nestjs/common'; 2 | import {AxiosResponse} from 'axios'; 3 | import {plainToClass} from 'class-transformer'; 4 | import {get} from 'lodash'; 5 | import {map} from 'rxjs/operators'; 6 | import {config} from '../../config'; 7 | import {AppLogger} from '../app.logger'; 8 | import {ConditionScoreDto} from './dto/condition-score.dto'; 9 | 10 | @Injectable() 11 | export class FoxyaiService { 12 | 13 | private logger = new AppLogger(FoxyaiService.name); 14 | 15 | constructor(private httpService: HttpService) { 16 | } 17 | 18 | public async propertyConditionScore(urls: Array): Promise { 19 | try { 20 | return await this.httpService.post(`${config.homeApi.foxyai.apiUrl}/property_condition_score`, { 21 | key: config.homeApi.foxyai.apiKey, 22 | urls 23 | }).pipe(map((response: AxiosResponse) => plainToClass(ConditionScoreDto, { 24 | bathrooms: get(response, 'data.property_condition_score.Bathrooms'), 25 | exterior: get(response, 'data.property_condition_score.Exterior'), 26 | interior: get(response, 'data.property_condition_score.Interior'), 27 | kitchen: get(response, 'data.property_condition_score[Kitchen/Dining]'), 28 | overall: get(response, 'data.property_condition_score.Overall') 29 | }, {strategy: 'excludeAll'}) 30 | )).toPromise(); 31 | } catch (e) { 32 | this.logger.error(e.message, e.trace); 33 | } 34 | } 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/app/home/home-error.enum.ts: -------------------------------------------------------------------------------- 1 | export enum HomeErrorEnum { 2 | ATTOM_DEFAULT_API_ERROR = 211111, 3 | ATTOM_USAGE_EXCEED_LIMIT = 222222, 4 | GEOCODE_RESULTS_NOT_FOUND = 200001, 5 | GEOCODE_ZIP_CODE_POSITION = 200002, 6 | GEOCODE_ZIP_CODE_REQUIRED = 200003, 7 | GEOCODE_INVALID_SERVICE = 200100, 8 | GEOCODE_SERVICE_TEMPORARY_UNAVAILABLE = 200101, 9 | GEOCODE_UNEXPECTED_ERROR = 200102, 10 | GEOCODE_FORMAT_ERROR = 200103, 11 | GEOCODE_ADDRESS_NOT_IDENTIFIED = 200104 12 | } 13 | -------------------------------------------------------------------------------- /src/app/home/home-pdf.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@nestjs/common'; 2 | import {MongoRepository, Repository} from 'typeorm'; 3 | import {CrudService} from '../../base'; 4 | import {HomePdfEntity} from './entity'; 5 | import {HOME_PDF_TOKEN} from './home.constants'; 6 | import {AppLogger} from '../app.logger'; 7 | 8 | @Injectable() 9 | export class HomePdfService extends CrudService { 10 | 11 | private logger = new AppLogger(HomePdfService.name); 12 | 13 | constructor( 14 | @Inject(HOME_PDF_TOKEN) protected readonly repository: MongoRepository 15 | ) { 16 | super(); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/app/home/home.command.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {Command, Positional} from 'nestjs-command'; 3 | import {DateTime} from 'luxon'; 4 | import {HomeService} from './home.service'; 5 | import faker from 'faker'; 6 | import {AppLogger} from '../app.logger'; 7 | import {HomeEntity} from './entity'; 8 | import {UserService} from '../user/user.service'; 9 | import {HomeMediaService} from '../home-media/home-media.service'; 10 | import {DeepPartial} from '../_helpers/database'; 11 | import {HomeMediaEntity} from '../home-media/entity'; 12 | import {HomeFavoriteService} from '../home-favorite/home-favorite.service'; 13 | 14 | @Injectable() 15 | export class HomeCommand { 16 | 17 | private logger = new AppLogger(HomeCommand.name); 18 | 19 | constructor( 20 | private readonly homeService: HomeService, 21 | private readonly homeMediaService: HomeMediaService, 22 | private readonly homeFavoriteService: HomeFavoriteService, 23 | private readonly userService: UserService 24 | ) { 25 | faker.locale = 'en_US'; 26 | } 27 | 28 | @Command({ command: 'create:home [amount]', describe: 'create a random fake homes' }) 29 | public async create(@Positional({name: 'amount'}) amount) { 30 | amount = parseInt(amount || 50, 10); 31 | this.logger.debug(`[create] execute for amount ${amount}!`); 32 | 33 | this.logger.debug(`[create] delete from home everything with json "faker"`); 34 | await this.homeService.deleteAll({json: {eq: '{"faker": true}'}}); 35 | this.logger.debug(`[create] delete from home_media everything with mimetype "image/fake"`); 36 | await this.homeMediaService.deleteAll({mimetype: {eq: 'image/fake'}}); 37 | this.logger.debug(`[create] delete from home_favorites everything with fake`); 38 | await this.homeFavoriteService.deleteAll({fake: {eq: true}}); 39 | 40 | this.logger.debug(`[create] fetch faked users`); 41 | const users = await this.userService.findAll({where: {provider: {eq: 'faker'}}}); 42 | const usersIds = users.map(user => user.id.toString()); 43 | 44 | const homes: HomeEntity[] = []; 45 | for (let i = 0; i < amount; i++) { 46 | const home: HomeEntity = { 47 | owner: faker.random.arrayElement(usersIds), 48 | json: '{"faker": true}', 49 | price: Number(faker.finance.amount(100000, 1000000)), 50 | price_adjustment: faker.random.number(100), 51 | descr: faker.lorem.paragraphs(faker.random.number({max: 5, min: 1})), 52 | address_1: faker.address.streetAddress(), 53 | address_2: faker.address.secondaryAddress(), 54 | city: faker.address.city(), 55 | state: faker.address.state(), 56 | zip: faker.address.zipCode(), 57 | country: faker.address.country(), 58 | beds: faker.random.number({max: 10, min: 1}), 59 | baths: faker.random.number({max: 10, min: 1}), 60 | lot_size: faker.random.number(20), 61 | sqft: faker.random.number({max: 350, min: 50}), 62 | lat: Number(faker.address.latitude()), 63 | lng: Number(faker.address.longitude()), 64 | pool: faker.random.boolean(), 65 | fav_count: faker.random.number(10), 66 | showing_count: faker.random.number({max: 100000, min: 100}), 67 | buyers_agent: faker.random.boolean() 68 | } as any; 69 | if (home.buyers_agent) { 70 | home.buyers_agent_amt = faker.random.number(1000); 71 | home.buyers_agent_type = faker.random.number(1000); 72 | } 73 | homes.push(home); 74 | } 75 | 76 | this.logger.debug(`[create] create ${amount} random homes with json {"faker": true}`); 77 | 78 | const savedHomes = await this.homeService.saveAll(homes); 79 | 80 | this.logger.debug(`[create] create home media for homes`); 81 | 82 | const homeFavs = []; 83 | const homeMedias = []; 84 | 85 | for (const home of savedHomes) { 86 | const homeMedia = this.generateHomeMedias(home.id.toString()); 87 | homeMedias.push(homeMedia); 88 | if (faker.random.boolean()) { 89 | homeFavs.push({ 90 | homeFavoriteHomeId: home.id.toString(), 91 | homeFavoriteUserId: faker.random.arrayElement(usersIds), 92 | fake: true 93 | }); 94 | } 95 | } 96 | 97 | this.logger.debug(`[create] saving home medias`); 98 | 99 | await this.homeMediaService.saveAll(homeMedias); 100 | 101 | this.logger.debug(`[create] saving home favorites`); 102 | 103 | await this.homeFavoriteService.saveAll(homeFavs); 104 | 105 | this.logger.debug(`[create] done!`); 106 | } 107 | 108 | private generateHomeMedias(homeId: string): DeepPartial { 109 | return Array(faker.random.number({min: 2, max: 5, precision: 1})).fill({}).map(() => ({ 110 | homeId, 111 | originalname: faker.system.commonFileName('jpeg'), 112 | mimetype: 'image/fake', 113 | size: faker.random.number({min: 86400, max: 259200}), 114 | url: 'http://lorempixel.com/640/480/city' 115 | })); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/app/home/home.constants.ts: -------------------------------------------------------------------------------- 1 | export const HOME_TOKEN = 'HomeRepositoryToken'; 2 | export const HOME_PDF_TOKEN = 'HomePdfRepositoryToken'; 3 | export const HOME_CMD_DELETE = 'onHomeDeleteCmd'; 4 | -------------------------------------------------------------------------------- /src/app/home/home.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, HttpCode, Param, Post, UseGuards} from '@nestjs/common'; 2 | import {Client, ClientProxy, Transport} from '@nestjs/microservices'; 3 | import {ApiImplicitParam, ApiResponse, ApiUseTags, ApiBearerAuth} from '@nestjs/swagger'; 4 | import crypto from 'crypto'; 5 | import {AppLogger} from '../app.logger'; 6 | import {HomeService} from './home.service'; 7 | import {HomePipe} from './pipe/home.pipe'; 8 | import {HomeEntity} from './entity'; 9 | import {ConvertTemplateDto} from './dto/convert-template.dto'; 10 | import {HomePdfService} from './home-pdf.service'; 11 | import {AuthGuard} from '@nestjs/passport'; 12 | 13 | @ApiUseTags('home') 14 | @Controller('home') 15 | @ApiBearerAuth() 16 | @UseGuards(AuthGuard('jwt')) 17 | export class HomeController { 18 | 19 | @Client({transport: Transport.TCP}) 20 | private client: ClientProxy; 21 | 22 | private logger = new AppLogger(HomeController.name); 23 | 24 | constructor( 25 | private readonly homeService: HomeService, 26 | private readonly homePdfService: HomePdfService 27 | ) { 28 | 29 | } 30 | 31 | @Post('import/addresses') 32 | @HttpCode(200) 33 | @ApiResponse({status: 204, description: 'NO CONTENT'}) 34 | public async importAddresses(): Promise { 35 | this.logger.silly(`[importAddresses] execute `); 36 | return this.homeService.importAddresses(); 37 | } 38 | 39 | @Post('convert/:homeId') 40 | @ApiImplicitParam({name: 'homeId', description: 'Home identity', type: 'string', required: true}) 41 | public async convertTemplate( 42 | @Param('homeId', HomePipe) home: HomeEntity, 43 | @Body() convert: ConvertTemplateDto 44 | ) { 45 | const hash = JSON.stringify(Object.assign({}, { 46 | updatedAt: home.updatedAt, 47 | ...convert 48 | })); 49 | const shasum = crypto.createHash('sha1'); 50 | shasum.update(hash); 51 | const sha1 = shasum.digest('hex'); 52 | let homePdf = await this.homePdfService.findOne({where: {homeId: home.id.toString(), sha1: sha1}}); 53 | if (!homePdf) { 54 | const pdfParams = await this.homeService.callApi2pdf({home, media: convert}); 55 | homePdf = await this.homePdfService.create({ 56 | homeId: home.id.toString(), 57 | sha1: sha1, 58 | bucket: pdfParams.bucket, 59 | key: pdfParams.key 60 | }); 61 | } 62 | return { 63 | url: homePdf.getUrl() 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/home/home.guard.ts: -------------------------------------------------------------------------------- 1 | import {CanActivate, ExecutionContext, Injectable} from '@nestjs/common'; 2 | import {GqlExecutionContext} from '@nestjs/graphql'; 3 | 4 | @Injectable() 5 | export class HomeGuard implements CanActivate { 6 | canActivate(context: ExecutionContext): boolean { 7 | const ctx = GqlExecutionContext.create(context); 8 | return true; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import {forwardRef, HttpModule, Module} from '@nestjs/common'; 2 | import {ContractModule} from '../contract/contract.module'; 3 | import {DatabaseModule} from '../database/database.module'; 4 | import {HomeFavoriteModule} from '../home-favorite/home-favorite.module'; 5 | import {HomeMediaModule} from '../home-media/home-media.module'; 6 | import {UserModule} from '../user/user.module'; 7 | import {AttomDataApiService} from './attom-data-api.service'; 8 | import {FoxyaiService} from './foxyai.service'; 9 | import {HomePdfService} from './home-pdf.service'; 10 | import {HomeCommand} from './home.command'; 11 | import {HomeController} from './home.controller'; 12 | import {homeProviders} from './home.providers'; 13 | import {HomeResolver} from './home.resolver'; 14 | import {HomeService} from './home.service'; 15 | import {HomePipe} from './pipe/home.pipe'; 16 | import {HomeVoter} from './security/home.voter'; 17 | 18 | const PROVIDERS = [ 19 | ...homeProviders, 20 | HomeService, 21 | HomePdfService, 22 | HomeResolver, 23 | AttomDataApiService, 24 | FoxyaiService, 25 | HomeVoter, 26 | HomeCommand, 27 | HomePipe 28 | ]; 29 | 30 | const MODULES = [ 31 | HttpModule, 32 | DatabaseModule, 33 | UserModule, 34 | HomeFavoriteModule, 35 | HomeMediaModule, 36 | forwardRef(() => ContractModule) 37 | ]; 38 | 39 | @Module({ 40 | controllers: [HomeController], 41 | providers: [...PROVIDERS], 42 | imports: [...MODULES], 43 | exports: [HomeService] 44 | }) 45 | export class HomeModule { 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/home/home.providers.ts: -------------------------------------------------------------------------------- 1 | import {DB_CON_TOKEN} from '../database/database.constants'; 2 | import {HomeEntity, HomePdfEntity} from './entity'; 3 | import {HOME_PDF_TOKEN, HOME_TOKEN} from './home.constants'; 4 | import {Connection} from 'typeorm'; 5 | 6 | export const homeProviders = [ 7 | { 8 | provide: HOME_TOKEN, 9 | useFactory: (connection: Connection) => connection.getMongoRepository(HomeEntity), 10 | inject: [DB_CON_TOKEN] 11 | }, 12 | { 13 | provide: HOME_PDF_TOKEN, 14 | useFactory: (connection: Connection) => connection.getMongoRepository(HomePdfEntity), 15 | inject: [DB_CON_TOKEN] 16 | } 17 | ]; 18 | -------------------------------------------------------------------------------- /src/app/home/home.validator.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neoteric-eu/nestjs-auth/dc294e4ce2436579dea9958e24b90c0cc0ef2569/src/app/home/home.validator.ts -------------------------------------------------------------------------------- /src/app/home/pipe/home.pipe.ts: -------------------------------------------------------------------------------- 1 | import {ArgumentMetadata, Injectable, PipeTransform} from '@nestjs/common'; 2 | import {HomeService} from '../home.service'; 3 | import {HomeEntity} from '../entity'; 4 | 5 | @Injectable() 6 | export class HomePipe implements PipeTransform { 7 | constructor(private readonly homeService: HomeService) { 8 | 9 | } 10 | async transform(homeId: string, metadata: ArgumentMetadata): Promise { 11 | return this.homeService.findOneById(homeId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/home/security/home.voter.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {RestVoterActionEnum, Voter} from '../../security'; 3 | import {UserEntity} from '../../user/entity'; 4 | import {AppLogger} from '../../app.logger'; 5 | import {HomeService} from '../home.service'; 6 | import {HomeEntity} from '../entity'; 7 | 8 | @Injectable() 9 | export class HomeVoter extends Voter { 10 | 11 | private logger = new AppLogger(HomeVoter.name); 12 | 13 | private readonly attributes = [ 14 | RestVoterActionEnum.READ_ALL, 15 | RestVoterActionEnum.READ, 16 | RestVoterActionEnum.CREATE, 17 | RestVoterActionEnum.DELETE 18 | ]; 19 | 20 | constructor(private readonly homeService: HomeService) { 21 | super(); 22 | } 23 | 24 | protected supports(attribute: any, subject: any): boolean { 25 | if (!this.attributes.includes(attribute)) { 26 | return false; 27 | } 28 | 29 | if (Array.isArray(subject)) { 30 | return subject.every(element => element instanceof HomeEntity); 31 | } 32 | 33 | return subject instanceof HomeEntity; 34 | } 35 | 36 | protected async voteOnAttribute(attribute: string, subject: HomeEntity | HomeEntity[], context): Promise { 37 | const user = context.getUser(); 38 | 39 | switch (attribute) { 40 | case RestVoterActionEnum.READ_ALL: 41 | return this.canReadAll(subject as HomeEntity[], user); 42 | case RestVoterActionEnum.READ: 43 | return this.canRead(subject as HomeEntity, user); 44 | case RestVoterActionEnum.CREATE: 45 | return this.canCreate(subject as HomeEntity, user); 46 | case RestVoterActionEnum.UPDATE: 47 | return this.canUpdate(subject as HomeEntity, user); 48 | case RestVoterActionEnum.DELETE: 49 | return this.canDelete(subject as HomeEntity, user); 50 | } 51 | 52 | return Promise.resolve(false); 53 | } 54 | 55 | private async canReadAll(homes: HomeEntity[], user: UserEntity): Promise { 56 | this.logger.debug('[canReadAll] everybody can read homes'); 57 | return true; 58 | } 59 | 60 | private async canRead(home: HomeEntity, user: UserEntity): Promise { 61 | this.logger.debug(`[canRead] any user can read home ${home.id}`); 62 | return true; 63 | } 64 | 65 | private canCreate(home: HomeEntity, user: UserEntity) { 66 | this.logger.debug('[canCreate] everybody can create homes'); 67 | return true; 68 | } 69 | 70 | private canUpdate(home: HomeEntity, user: UserEntity) { 71 | this.logger.debug('[canUpdate] only owner of the house can update his home'); 72 | return home.owner === user.id.toString(); 73 | } 74 | 75 | private canDelete(home: HomeEntity, user: UserEntity) { 76 | this.logger.debug('[canDelete] only owner of the house can delete his home'); 77 | return home.owner === user.id.toString(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './_helpers'; 2 | export * from './app.dispatcher'; 3 | export * from './app.logger'; 4 | export * from './app.module'; 5 | -------------------------------------------------------------------------------- /src/app/media/dto/media-upload.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiModelProperty } from '@nestjs/swagger'; 2 | 3 | export class MediaMetadataDto { 4 | @ApiModelProperty() 5 | fieldname: string; 6 | 7 | @ApiModelProperty() 8 | originalname: string; 9 | 10 | @ApiModelProperty() 11 | encoding: string; 12 | 13 | @ApiModelProperty() 14 | mimetype: string; 15 | } 16 | 17 | export class MediaUploadDto { 18 | 19 | @ApiModelProperty() 20 | public fieldname: string; 21 | 22 | @ApiModelProperty() 23 | public originalname: string; 24 | 25 | @ApiModelProperty() 26 | public encoding: string; 27 | 28 | @ApiModelProperty() 29 | public mimetype: string; 30 | 31 | @ApiModelProperty() 32 | public size: number; 33 | 34 | @ApiModelProperty() 35 | public bucket: string; 36 | 37 | @ApiModelProperty() 38 | public key: string; 39 | 40 | @ApiModelProperty() 41 | public acl: string; 42 | 43 | @ApiModelProperty() 44 | public contentType: string; 45 | 46 | @ApiModelProperty() 47 | public contentDisposition: string; 48 | 49 | @ApiModelProperty() 50 | public storageClass: string; 51 | 52 | @ApiModelProperty() 53 | public serverSideEncryption: string; 54 | 55 | @ApiModelProperty() 56 | public metadata: MediaMetadataDto; 57 | 58 | @ApiModelProperty() 59 | location: string; 60 | 61 | @ApiModelProperty() 62 | etag: string; 63 | } 64 | -------------------------------------------------------------------------------- /src/app/media/media.constants.ts: -------------------------------------------------------------------------------- 1 | export const MEDIA_CMD_DELETE = 'MediaCmdOnDelete'; 2 | -------------------------------------------------------------------------------- /src/app/media/media.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller, FileInterceptor, Inject, Post, UploadedFile, UseGuards, UseInterceptors} from '@nestjs/common'; 2 | import {MessagePattern} from '@nestjs/microservices'; 3 | import {AuthGuard} from '@nestjs/passport'; 4 | import {ApiBearerAuth, ApiConsumes, ApiImplicitFile, ApiResponse, ApiUseTags} from '@nestjs/swagger'; 5 | import S3 from 'aws-sdk/clients/s3'; 6 | import {config} from '../../config'; 7 | import {AppLogger} from '../app.logger'; 8 | import {HomeMediaEntity} from '../home-media/entity'; 9 | import {MEDIA_CMD_DELETE} from './media.constants'; 10 | import {AWS_CON_TOKEN} from '../database/database.constants'; 11 | import {JwtDto} from '../auth/dto/jwt.dto'; 12 | import {MediaUploadDto} from './dto/media-upload.dto'; 13 | 14 | @ApiUseTags('media') 15 | @ApiBearerAuth() 16 | @UseGuards(AuthGuard('jwt')) 17 | @Controller('media') 18 | export class MediaController { 19 | 20 | private logger = new AppLogger(MediaController.name); 21 | 22 | constructor(@Inject(AWS_CON_TOKEN) private readonly awsConnection) {} 23 | 24 | @Post('upload') 25 | @UseInterceptors(FileInterceptor('media')) 26 | @ApiConsumes('multipart/form-data') 27 | @ApiImplicitFile({ name: 'media', required: true, description: 'Any media file' }) 28 | @ApiResponse({ status: 200, description: 'OK', type: MediaUploadDto }) 29 | public uploadFile(@UploadedFile() file) { 30 | return file; 31 | } 32 | 33 | @MessagePattern({ cmd: MEDIA_CMD_DELETE }) 34 | public async onMediaDelete(homeMedia: HomeMediaEntity): Promise { 35 | const key = new URL(homeMedia.url).pathname.substring(1); 36 | this.logger.debug(`[onMediaDelete] Going to remove key ${key} from bucket ${config.aws.s3.bucket_name}`); 37 | const s3 = new S3(); 38 | s3.deleteObject({ 39 | Bucket: config.aws.s3.bucket_name, 40 | Key: key 41 | }).promise().then(() => { 42 | this.logger.debug(`[onMediaDelete] item with key: ${key} removed from bucket`); 43 | }).catch(() => { 44 | this.logger.warn(`[onMediaDelete] looks like this key ${key} doesn't exists on bucket`); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app/media/media.module.ts: -------------------------------------------------------------------------------- 1 | import {Module, MulterModule} from '@nestjs/common'; 2 | import {MulterConfigService} from './multer-config.service'; 3 | import {DatabaseModule} from '../database/database.module'; 4 | import {MediaController} from './media.controller'; 5 | 6 | @Module({ 7 | controllers: [MediaController], 8 | imports: [ 9 | DatabaseModule, 10 | MulterModule.registerAsync({ 11 | useClass: MulterConfigService 12 | }) 13 | ] 14 | }) 15 | export class MediaModule { 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/app/media/multer-config.service.ts: -------------------------------------------------------------------------------- 1 | import S3 from 'aws-sdk/clients/s3'; 2 | import {Injectable, MulterModuleOptions, MulterOptionsFactory} from '@nestjs/common'; 3 | import {v4} from 'uuid'; 4 | import s3Storage, {AUTO_CONTENT_TYPE} from 'multer-s3'; 5 | import {config} from '../../config'; 6 | 7 | @Injectable() 8 | export class MulterConfigService implements MulterOptionsFactory { 9 | createMulterOptions(): MulterModuleOptions { 10 | const s3 = new S3(); 11 | const bucket = config.aws.s3.bucket_name; 12 | return { 13 | storage: s3Storage({ 14 | s3, 15 | bucket, 16 | acl: 'public-read', 17 | contentType: AUTO_CONTENT_TYPE, 18 | metadata: (req, file, cb) => cb(null, Object.assign({}, file)), 19 | key: (req, media, cb) => cb(null, v4()) 20 | }) 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/message/date.scalar.ts: -------------------------------------------------------------------------------- 1 | import {Scalar} from '@nestjs/graphql'; 2 | import {Kind} from 'graphql'; 3 | import {DateTime} from 'luxon'; 4 | 5 | @Scalar('Date') 6 | export class DateScalar { 7 | description = 'Date custom scalar type'; 8 | 9 | parseValue(value) { 10 | return new Date(value); // value from the client 11 | } 12 | 13 | serialize(value: Date|DateTime) { 14 | if (value instanceof DateTime) { 15 | return value.toJSON(); 16 | } 17 | return value.toISOString(); // value sent to the client 18 | } 19 | 20 | parseLiteral(ast) { 21 | if (ast.kind === Kind.STRING) { 22 | return new Date(ast.value); // ast value is always in string format 23 | } 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/app/message/entity/conversation.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsString} from 'class-validator'; 3 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 4 | import {ExtendedEntity} from '../../_helpers'; 5 | 6 | @Entity() 7 | export class ConversationEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public homeId: string; 17 | 18 | @ApiModelProperty() 19 | @IsString() 20 | @Column() 21 | public authorId: string; 22 | 23 | @ApiModelProperty() 24 | @IsString() 25 | @Column() 26 | public type: string; 27 | } 28 | -------------------------------------------------------------------------------- /src/app/message/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './conversation.entity'; 2 | export * from './message.entity'; 3 | export * from './user-conversation.entity'; 4 | -------------------------------------------------------------------------------- /src/app/message/entity/message.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsBoolean, IsString} from 'class-validator'; 3 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 4 | import {ExtendedEntity} from '../../_helpers'; 5 | 6 | @Entity() 7 | export class MessageEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public authorId: string; 17 | 18 | @ApiModelProperty() 19 | @IsString() 20 | @Column() 21 | public content: string; 22 | 23 | @ApiModelProperty() 24 | @IsString() 25 | @Column() 26 | public type: string; 27 | 28 | @ApiModelProperty() 29 | @IsString() 30 | @Column() 31 | public conversationId: string; 32 | 33 | @ApiModelProperty() 34 | @IsBoolean() 35 | @Column() 36 | public isSent = false; 37 | 38 | @ApiModelProperty() 39 | @IsBoolean() 40 | @Column() 41 | public isRead = false; 42 | 43 | @Column({ 44 | array: true 45 | }) 46 | public deletedFor: string[]; 47 | } 48 | -------------------------------------------------------------------------------- /src/app/message/entity/user-conversation.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsString} from 'class-validator'; 3 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 4 | import {ExtendedEntity} from '../../_helpers'; 5 | 6 | @Entity() 7 | export class UserConversationEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public conversationId: string; 17 | 18 | @ApiModelProperty() 19 | @IsString() 20 | @Column() 21 | public userId: string; 22 | 23 | @Column() 24 | public isDeleted = false; 25 | } 26 | -------------------------------------------------------------------------------- /src/app/message/message.buffer.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, OnModuleInit} from '@nestjs/common'; 2 | import {UserEntity} from '../user/entity'; 3 | import {MessageEntity} from './entity'; 4 | 5 | @Injectable() 6 | export class MessageBuffer { 7 | private messages = new Map>(); 8 | 9 | constructor() { 10 | 11 | } 12 | 13 | public addMessage(userId: string, message: MessageEntity) { 14 | if (!this.messages.has(userId)) { 15 | this.messages.set(userId, new Set()); 16 | } 17 | this.messages.get(userId).add(message); 18 | } 19 | 20 | public getEntries(): IterableIterator<[string, Set]> { 21 | return this.messages.entries(); 22 | } 23 | 24 | public flush(userId) { 25 | this.messages.get(userId).clear(); 26 | } 27 | 28 | public flushAll() { 29 | for (const [id, set] of this.messages.entries()) { 30 | set.clear(); 31 | } 32 | this.messages.clear(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/message/message.constants.ts: -------------------------------------------------------------------------------- 1 | export const MESSAGE_TOKEN = 'MessageRepositoryToken'; 2 | export const CONVERSATION_TOKEN = 'ConversationRepositoryToken'; 3 | export const USER_CONVERSATION_TOKEN = 'UserConversationRepositoryToken'; 4 | export const MESSAGE_CMD_NEW = 'onMessageNewCmd'; 5 | export const OFFLINE_MESSAGE_CMD_NEW = 'whenOfflineMessageNewCmd'; 6 | -------------------------------------------------------------------------------- /src/app/message/message.controller.ts: -------------------------------------------------------------------------------- 1 | import {Controller} from '@nestjs/common'; 2 | import {MessagePattern} from '@nestjs/microservices'; 3 | import {DateTime} from 'luxon'; 4 | import {AppLogger} from '../app.logger'; 5 | import {UserEntity} from '../user/entity'; 6 | import {OnlineService} from '../user/online.service'; 7 | import {MessageEntity} from './entity'; 8 | import {MessageBuffer} from './message.buffer'; 9 | import {MESSAGE_CMD_NEW, OFFLINE_MESSAGE_CMD_NEW} from './message.constants'; 10 | import {ConversationService} from './services/conversation.service'; 11 | import {MessageService} from './services/message.service'; 12 | import {UserConversationService} from './services/user-conversation.service'; 13 | 14 | @Controller() 15 | export class MessageController { 16 | private logger = new AppLogger(MessageController.name); 17 | 18 | constructor( 19 | private readonly onlineService: OnlineService, 20 | private readonly messageBuffer: MessageBuffer, 21 | private readonly messageService: MessageService, 22 | private readonly conversationService: ConversationService, 23 | private readonly userConversationService: UserConversationService 24 | ) { 25 | 26 | } 27 | 28 | @MessagePattern({cmd: MESSAGE_CMD_NEW}) 29 | public async onMessageNew(message: MessageEntity): Promise { 30 | this.logger.debug(`[onMessageNew] new message ${message.id}`); 31 | const userConversations = await this.userConversationService.findAll({ 32 | where: { 33 | conversationId: { 34 | eq: message.conversationId 35 | }, 36 | userId: { 37 | ne: message.authorId 38 | } 39 | } 40 | }); 41 | this.logger.debug(`[onMessageNew] how manny conversation ${userConversations.length} to update`); 42 | for (const userConversation of userConversations) { 43 | this.logger.debug(`[onMessageNew] if user ${userConversation.userId} if offline, send to him notification`); 44 | if (this.onlineService.isOffline({id: userConversation.userId} as UserEntity)) { 45 | this.logger.debug(`[onMessageNew] user ${userConversation.userId} is offline`); 46 | this.messageBuffer.addMessage(userConversation.userId, message); 47 | } 48 | userConversation.updatedAt = DateTime.utc(); 49 | await userConversation.save(); 50 | await this.messageService.pubSub.publish('userConversationUpdated', {userConversationUpdated: userConversation}); 51 | } 52 | } 53 | 54 | @MessagePattern({cmd: OFFLINE_MESSAGE_CMD_NEW}) 55 | public whenOfflineMessageNew(message: MessageEntity) { 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/app/message/message.cron.ts: -------------------------------------------------------------------------------- 1 | import {Injectable, OnModuleInit} from '@nestjs/common'; 2 | import {config} from '../../config'; 3 | import {mail, renderTemplate} from '../_helpers/mail'; 4 | import {push} from '../_helpers/push'; 5 | import {AppLogger} from '../app.logger'; 6 | import {UserService} from '../user/user.service'; 7 | import {MessageEntity} from './entity'; 8 | import {MessageBuffer} from './message.buffer'; 9 | 10 | @Injectable() 11 | export class MessageCron implements OnModuleInit { 12 | private logger = new AppLogger(MessageCron.name); 13 | 14 | constructor( 15 | private readonly messageBuffer: MessageBuffer, 16 | private readonly userService: UserService 17 | ) { 18 | 19 | } 20 | 21 | public onModuleInit(): void { 22 | setInterval(() => this.collectMessages(), 60000); 23 | } 24 | 25 | private async collectMessages() { 26 | this.logger.silly(`[collectMessages] start`); 27 | for (const [userId, messages] of this.messageBuffer.getEntries()) { 28 | this.logger.silly(`[collectMessages] for ${userId} we have ${messages.size} to send`); 29 | if (messages.size) { 30 | await this.sendNotification(userId, messages); 31 | this.messageBuffer.flush(userId); 32 | } 33 | } 34 | this.logger.silly(`[collectMessages] end`); 35 | } 36 | 37 | private async sendNotification(userId: string, messages: Set) { 38 | const subscription = await this.userService.subscription.findOne({where: {user: {eq: userId}}}); 39 | if (!subscription || !subscription.push || !subscription.email) { 40 | return; 41 | } 42 | const user = await this.userService.findOneById(userId); 43 | if (subscription.email) { 44 | await mail({ 45 | subject: `You've got new ${messages.size > 1 ? 'messages' : 'message'} when you was offline`, 46 | to: user.email, 47 | html: renderTemplate(`/mail/message_new.twig`, {user, config, messages: Array.from(messages)}) 48 | }); 49 | } 50 | 51 | if (subscription.push) { 52 | await push(user.phone_token, { 53 | data: { messages } as any, 54 | notification: { 55 | title: `You've got new ${messages.size > 1 ? 'messages' : 'message'}`, 56 | badge: '1' 57 | } 58 | }); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/message/message.graphql: -------------------------------------------------------------------------------- 1 | scalar Date 2 | 3 | type Query { 4 | allConversations: [UserConversation] 5 | allMessages(filter: AllMessagesFilterInput, after: Int, limit: Int): [Message] 6 | } 7 | 8 | type Mutation { 9 | createConversation(conversationInput: CreateConversationInput): UserConversation 10 | deleteConversation(conversationId: ID!): Boolean 11 | createMessage(conversationId: ID!, content: String, type: String): Message 12 | deleteMessage(messageId: ID!, forEveryone: Boolean): Boolean 13 | markAsRead(messageId: ID!): Boolean 14 | startTyping(conversationId: ID!): Boolean 15 | stopTyping(conversationId: ID!): Boolean 16 | } 17 | 18 | type Subscription { 19 | newMessage: Message 20 | newUserConversation: UserConversation 21 | messageUpdated: Message 22 | messageDeleted: Message 23 | userConversationUpdated: UserConversation 24 | userConversationDeleted: UserConversation 25 | startTyping: UserConversation 26 | stopTyping: UserConversation 27 | 28 | } 29 | 30 | type Conversation { 31 | id: ID! 32 | home: Home 33 | author: User 34 | type: String 35 | messages: [Message] 36 | createdAt: Date 37 | updatedAt: Date 38 | } 39 | 40 | input CreateConversationInput { 41 | homeId: ID!, 42 | type: String!, 43 | recipientId: String! 44 | } 45 | 46 | type UserConversation { 47 | conversation: Conversation 48 | user: User 49 | message: Message 50 | createdAt: Date 51 | updatedAt: Date 52 | } 53 | 54 | type Message { 55 | id: ID! 56 | author: User 57 | content: String! 58 | conversationId: ID! 59 | type: String 60 | isSent: Boolean 61 | isRead: Boolean 62 | createdAt: Date 63 | updatedAt: Date 64 | } 65 | 66 | input ModelDateFilterInput { 67 | ne: Date 68 | eq: Date 69 | le: Date 70 | lt: Date 71 | ge: Date 72 | gt: Date 73 | between: [Date] 74 | } 75 | 76 | input AllMessagesFilterInput { 77 | conversationId: ModelIDFilterInput 78 | content: ModelStringFilterInput 79 | type: ModelStringFilterInput 80 | isSent: ModelBooleanFilterInput 81 | isRead: ModelBooleanFilterInput 82 | createdAt: ModelDateFilterInput 83 | updatedAt: ModelDateFilterInput 84 | } 85 | -------------------------------------------------------------------------------- /src/app/message/message.module.ts: -------------------------------------------------------------------------------- 1 | import {HttpModule, Module} from '@nestjs/common'; 2 | import {DatabaseModule} from '../database/database.module'; 3 | import {HomeModule} from '../home/home.module'; 4 | import {UserModule} from '../user/user.module'; 5 | import {DateScalar} from './date.scalar'; 6 | import {MessageBuffer} from './message.buffer'; 7 | import {MessageCron} from './message.cron'; 8 | import {messageProviders} from './message.providers'; 9 | import {ConversationResolver} from './resolvers/conversation.resolver'; 10 | import {MessageResolver} from './resolvers/message.resolver'; 11 | import {UserConversationResolver} from './resolvers/user-conversation.resolver'; 12 | import {ConversationService} from './services/conversation.service'; 13 | import {MessageService} from './services/message.service'; 14 | import {SubscriptionsService} from './services/subscriptions.service'; 15 | import {UserConversationService} from './services/user-conversation.service'; 16 | import {MessageVoter} from './security/message.voter'; 17 | import {MessageController} from './message.controller'; 18 | import {UserConversationVoter} from './security/user-conversation.voter'; 19 | 20 | 21 | @Module({ 22 | controllers: [ 23 | MessageController 24 | ], 25 | providers: [ 26 | ...messageProviders, 27 | DateScalar, 28 | MessageService, 29 | ConversationService, 30 | UserConversationService, 31 | MessageResolver, 32 | MessageBuffer, 33 | MessageCron, 34 | UserConversationResolver, 35 | ConversationResolver, 36 | SubscriptionsService, 37 | MessageVoter, 38 | UserConversationVoter 39 | ], 40 | imports: [HttpModule, DatabaseModule, UserModule, HomeModule], 41 | exports: [MessageService, ConversationService, UserConversationService, SubscriptionsService] 42 | }) 43 | export class MessageModule { 44 | } 45 | -------------------------------------------------------------------------------- /src/app/message/message.providers.ts: -------------------------------------------------------------------------------- 1 | import {DB_CON_TOKEN} from '../database/database.constants'; 2 | import {CONVERSATION_TOKEN, MESSAGE_TOKEN, USER_CONVERSATION_TOKEN} from './message.constants'; 3 | import {MessageEntity, ConversationEntity, UserConversationEntity} from './entity'; 4 | import {Connection} from 'typeorm'; 5 | 6 | export const messageProviders = [ 7 | { 8 | provide: MESSAGE_TOKEN, 9 | useFactory: (connection: Connection) => connection.getRepository(MessageEntity), 10 | inject: [DB_CON_TOKEN] 11 | }, 12 | { 13 | provide: CONVERSATION_TOKEN, 14 | useFactory: (connection: Connection) => connection.getRepository(ConversationEntity), 15 | inject: [DB_CON_TOKEN] 16 | }, 17 | { 18 | provide: USER_CONVERSATION_TOKEN, 19 | useFactory: (connection: Connection) => connection.getRepository(UserConversationEntity), 20 | inject: [DB_CON_TOKEN] 21 | } 22 | ]; 23 | -------------------------------------------------------------------------------- /src/app/message/resolvers/conversation.resolver.ts: -------------------------------------------------------------------------------- 1 | import {Parent, ResolveProperty, Resolver} from '@nestjs/graphql'; 2 | import {HomeEntity} from '../../home/entity'; 3 | import {HomeService} from '../../home/home.service'; 4 | import {ConversationEntity} from '../entity'; 5 | import {Conversation} from '../../graphql.schema'; 6 | import {MessageService} from '../services/message.service'; 7 | import {User as CurrentUser} from '../../_helpers/graphql'; 8 | import {UserEntity as User} from '../../user/entity'; 9 | import {UserService} from '../../user/user.service'; 10 | 11 | @Resolver('Conversation') 12 | export class ConversationResolver { 13 | constructor( 14 | private readonly messageService: MessageService, 15 | private readonly homeService: HomeService, 16 | private readonly userService: UserService 17 | ) { 18 | } 19 | 20 | @ResolveProperty('messages') 21 | async getMessage(@CurrentUser() user: User, @Parent() conversation: ConversationEntity): Promise { 22 | return this.messageService.findAll({ 23 | where: { 24 | conversationId: { 25 | eq: conversation.id.toString() 26 | }, 27 | deletedFor: { 28 | nin: [user.id.toString()] 29 | } 30 | }, 31 | order: { 32 | createdAt: 'DESC' 33 | } 34 | }); 35 | } 36 | 37 | @ResolveProperty('home') 38 | async getOwner(@Parent() conversation: ConversationEntity): Promise { 39 | try { 40 | return this.homeService.findOneById(conversation.homeId.toString()); 41 | } catch (e) { 42 | return {} as any; 43 | } 44 | } 45 | 46 | @ResolveProperty('author') 47 | async getAuthor(@Parent() conversation: ConversationEntity): Promise { 48 | try { 49 | return this.userService.findOneById(conversation.authorId); 50 | } catch (e) { 51 | return {} as User; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/message/security/message.voter.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {AppLogger} from '../../app.logger'; 3 | import {RestVoterActionEnum, Voter} from '../../security'; 4 | import {UserEntity} from '../../user/entity'; 5 | import {MessageEntity} from '../entity'; 6 | import {UserConversationService} from '../services/user-conversation.service'; 7 | 8 | @Injectable() 9 | export class MessageVoter extends Voter { 10 | 11 | private logger = new AppLogger(MessageVoter.name); 12 | 13 | private readonly attributes = [ 14 | RestVoterActionEnum.READ_ALL, 15 | RestVoterActionEnum.CREATE, 16 | RestVoterActionEnum.UPDATE 17 | ]; 18 | 19 | constructor(private readonly userConversationService: UserConversationService) { 20 | super(); 21 | } 22 | 23 | protected supports(attribute: any, subject: any): boolean { 24 | if (!this.attributes.includes(attribute)) { 25 | return false; 26 | } 27 | 28 | if (Array.isArray(subject)) { 29 | return subject.every(element => element instanceof MessageEntity); 30 | } 31 | 32 | return subject instanceof MessageEntity; 33 | } 34 | 35 | protected async voteOnAttribute(attribute: string, subject: MessageEntity | MessageEntity[], token): Promise { 36 | const user = token.getUser(); 37 | 38 | if (!(user instanceof UserEntity)) { 39 | return false; 40 | } 41 | 42 | switch (attribute) { 43 | case RestVoterActionEnum.READ_ALL: 44 | return this.canReadAll(subject as MessageEntity[], user); 45 | case RestVoterActionEnum.CREATE: 46 | return this.canCreate(subject as MessageEntity, user); 47 | case RestVoterActionEnum.UPDATE: 48 | return this.canUpdate(subject as MessageEntity, user); 49 | case RestVoterActionEnum.SOFT_DELETE: 50 | return this.canSoftDelete(subject as MessageEntity, user); 51 | } 52 | 53 | return Promise.resolve(false); 54 | } 55 | 56 | private async canReadAll(messages: MessageEntity[], user: UserEntity): Promise { 57 | const message = messages[0]; 58 | const userConversations = await this.userConversationService.findAll({where: {userId: {eq: user.id.toString()}}}); 59 | return userConversations.some(userConversation => message.conversationId === userConversation.conversationId); 60 | } 61 | 62 | private async canCreate(message: MessageEntity, user: UserEntity): Promise { 63 | this.logger.debug(`[canCreate[ everyone can create message but only in their conversations`); 64 | const userConversation = await this.userConversationService.findOne({ 65 | where: { 66 | userId: {eq: user.id.toString()}, 67 | conversationId: {eq: message.conversationId} 68 | } 69 | }); 70 | return !!userConversation; 71 | } 72 | 73 | private async canUpdate(message: MessageEntity, user: UserEntity): Promise { 74 | this.logger.debug(`[canUpdate[ only owner of message can update it`); 75 | return message.authorId === user.id.toString(); 76 | } 77 | 78 | private async canSoftDelete(message: MessageEntity, user: UserEntity): Promise { 79 | this.logger.debug(`[canSoftDelete[ only conversation collaborators can soft delete it`); 80 | const userId = user.id.toString(); 81 | const userConversations = await this.userConversationService.findAll({where: {conversationId: {eq: message.conversationId}}}); 82 | return userConversations.some(userConversation => userConversation.userId === userId); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/app/message/security/user-conversation.voter.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {AppLogger} from '../../app.logger'; 3 | import {RestVoterActionEnum, Voter} from '../../security'; 4 | import {UserEntity} from '../../user/entity'; 5 | import {UserConversationEntity} from '../entity'; 6 | 7 | @Injectable() 8 | export class UserConversationVoter extends Voter { 9 | 10 | private logger = new AppLogger(UserConversationVoter.name); 11 | 12 | private readonly attributes = [ 13 | RestVoterActionEnum.SOFT_DELETE 14 | ]; 15 | 16 | protected supports(attribute: any, subject: any): boolean { 17 | if (!this.attributes.includes(attribute)) { 18 | return false; 19 | } 20 | 21 | if (Array.isArray(subject)) { 22 | return subject.every(element => element instanceof UserConversationEntity); 23 | } 24 | 25 | return subject instanceof UserConversationEntity; 26 | } 27 | 28 | protected async voteOnAttribute(attribute: string, subject: UserConversationEntity | UserConversationEntity[], token): Promise { 29 | const user = token.getUser(); 30 | 31 | if (!(user instanceof UserEntity)) { 32 | return false; 33 | } 34 | 35 | switch (attribute) { 36 | case RestVoterActionEnum.SOFT_DELETE: 37 | return this.canSoftDelete(subject as UserConversationEntity, user); 38 | default: 39 | return false; 40 | } 41 | } 42 | 43 | private async canSoftDelete(userConversation: UserConversationEntity, user: UserEntity): Promise { 44 | this.logger.debug(`[canSoftDelete[ only conversation owner can soft delete it`); 45 | const userId = user.id.toString(); 46 | return userConversation.userId === userId; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/message/services/conversation.service.ts: -------------------------------------------------------------------------------- 1 | import {CrudService} from '../../../base'; 2 | import {Inject, Injectable} from '@nestjs/common'; 3 | import {MongoRepository} from 'typeorm'; 4 | import {CONVERSATION_TOKEN} from '../message.constants'; 5 | import {ConversationEntity} from '../entity'; 6 | 7 | @Injectable() 8 | export class ConversationService extends CrudService { 9 | 10 | constructor(@Inject(CONVERSATION_TOKEN) protected readonly repository: MongoRepository) { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/message/services/message.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@nestjs/common'; 2 | import {MongoRepository} from 'typeorm'; 3 | import {CrudService} from '../../../base'; 4 | import {RestVoterActionEnum} from '../../security/voter'; 5 | import {MessageEntity} from '../entity'; 6 | import {MESSAGE_TOKEN} from '../message.constants'; 7 | 8 | @Injectable() 9 | export class MessageService extends CrudService { 10 | 11 | public pubSub; 12 | 13 | constructor(@Inject(MESSAGE_TOKEN) protected readonly repository: MongoRepository) { 14 | super(); 15 | } 16 | 17 | public async softDelete(message: MessageEntity): Promise { 18 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.SOFT_DELETE, message); 19 | return message.save(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/app/message/services/subscriptions.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {AppLogger} from '../../app.logger'; 3 | import {UserConversationService} from './user-conversation.service'; 4 | 5 | @Injectable() 6 | export class SubscriptionsService { 7 | 8 | private logger = new AppLogger(SubscriptionsService.name); 9 | 10 | constructor(private readonly userConversationService: UserConversationService) { 11 | } 12 | 13 | public async newUserConversation(payload, variables, context) { 14 | const found = await this.haveConversationForUser(payload.newUserConversation.conversationId, context.req.user); 15 | this.logger.debug(`[newUserConversation] Do we found this conversation for this user ${context.req.user.id}? ${found}`); 16 | return found; 17 | } 18 | 19 | public async newMessage(payload, variables, context) { 20 | const found = await this.haveConversationForUser(payload.newMessage.conversationId, context.req.user); 21 | this.logger.debug(`[newMessage] Do we found this conversation for this user ${context.req.user.id}? ${found}`); 22 | return found; 23 | } 24 | 25 | public async messageUpdated(payload, variables, context) { 26 | const found = await this.haveConversationForUser(payload.messageUpdated.conversationId, context.req.user); 27 | this.logger.debug(`[messageUpdated] Do we found this conversation for this user ${context.req.user.id}? ${found}`); 28 | return found; 29 | } 30 | 31 | public async messageDeleted(payload, variables, context) { 32 | const found = await this.haveConversationForUser(payload.messageDeleted.conversationId, context.req.user); 33 | this.logger.debug(`[messageDeleted] Do we found this conversation for this user ${context.req.user.id}? ${found}`); 34 | return found; 35 | } 36 | 37 | public async userConversationUpdated(payload, variables, context) { 38 | const found = await this.haveConversationForUser(payload.userConversationUpdated.conversationId, context.req.user); 39 | this.logger.debug(`[userConversationUpdated] Do we found this conversation for this user ${context.req.user.id}? ${found}`); 40 | return found; 41 | } 42 | 43 | public async userConversationDeleted(payload, variables, context) { 44 | const found = await this.haveConversationForUser(payload.userConversationDeleted.conversationId, context.req.user); 45 | this.logger.debug(`[userConversationDeleted] Do we found this conversation for this user ${context.req.user.id}? ${found}`); 46 | return found; 47 | } 48 | 49 | public async startTyping(payload, varibles, context) { 50 | this.logger.debug(`[startTyping]`); 51 | try { 52 | const userId = context.req.user.id.toString(); 53 | const conversationId = payload.startTyping.conversationId; 54 | return this.typingLifecycle(userId, conversationId); 55 | } catch (e) { 56 | this.logger.error(e.message, e.stack); 57 | return false; 58 | } 59 | } 60 | 61 | public async stopTyping(payload, variables, context) { 62 | this.logger.debug(`[stopTyping]`); 63 | try { 64 | const userId = context.req.user.id.toString(); 65 | const conversationId = payload.stopTyping.conversationId; 66 | return this.typingLifecycle(userId, conversationId); 67 | } catch (e) { 68 | this.logger.error(e.message, e.stack); 69 | return false; 70 | } 71 | } 72 | 73 | private async haveConversationForUser(conversationId, user): Promise { 74 | const userId = user.id.toString(); 75 | const conversations = await this.userConversationService.findAll({ 76 | where: { 77 | userId: { 78 | eq: userId 79 | } 80 | } 81 | }); 82 | return conversations.some(conversation => conversation.conversationId === conversationId); 83 | } 84 | 85 | private async typingLifecycle(userId, conversationId): Promise { 86 | this.logger.debug(`[typingLifecycle] for user ${userId}`); 87 | this.logger.debug(`[typingLifecycle] for conversationId ${conversationId}`); 88 | const conversations = await this.userConversationService.findAll({where: {userId: {eq: userId}}}); 89 | const found = conversations.some(conversation => conversation.conversationId === conversationId); 90 | this.logger.debug(`[typingLifecycle] Do we found conversation for this user ${userId}? ${found}`); 91 | return found; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/message/services/user-conversation.service.ts: -------------------------------------------------------------------------------- 1 | import {CrudService} from '../../../base'; 2 | import {Inject, Injectable} from '@nestjs/common'; 3 | import {MongoRepository} from 'typeorm'; 4 | import {USER_CONVERSATION_TOKEN} from '../message.constants'; 5 | import {UserConversationEntity} from '../entity'; 6 | import {ConversationService} from './conversation.service'; 7 | import {UserEntity} from '../../user/entity'; 8 | import {CreateConversationInput} from '../../graphql.schema'; 9 | 10 | @Injectable() 11 | export class UserConversationService extends CrudService { 12 | 13 | constructor( 14 | @Inject(USER_CONVERSATION_TOKEN) protected readonly repository: MongoRepository, 15 | private readonly conversationService: ConversationService 16 | ) { 17 | super(); 18 | } 19 | 20 | public async createConversationIfMissing(user: UserEntity, input: CreateConversationInput): Promise<[UserConversationEntity, boolean]> { 21 | const allConversations = await this.findAll({ 22 | where: { 23 | userId: {eq: user.id.toString()} 24 | } 25 | }); 26 | 27 | const existingConversation = await this.findOne({ 28 | where: { 29 | userId: { 30 | eq: input.recipientId 31 | }, 32 | conversationId: { 33 | in: allConversations.map(con => con.conversationId) 34 | } 35 | } 36 | }); 37 | 38 | if (!existingConversation) { 39 | const createdConversation = await this.createConversation(user, input); 40 | return [createdConversation, true]; 41 | } 42 | 43 | const userConversation = allConversations.find(conversation => conversation.conversationId === existingConversation.conversationId); 44 | 45 | if (userConversation.isDeleted) { 46 | userConversation.isDeleted = false; 47 | await this.update(userConversation); 48 | } 49 | 50 | if (existingConversation.isDeleted) { 51 | existingConversation.isDeleted = false; 52 | await this.update(existingConversation); 53 | } 54 | 55 | return [userConversation, false]; 56 | } 57 | 58 | public async restoreConversations(conversationId: string): Promise { 59 | await this.updateAll({ 60 | conversationId: { 61 | eq: conversationId 62 | } 63 | }, { 64 | $set: { 65 | isDeleted: false 66 | } 67 | }); 68 | } 69 | 70 | private async createConversation(user: UserEntity, input: CreateConversationInput): Promise { 71 | const createdConversation = await this.conversationService.create({ 72 | ...input, authorId: user.id.toString() 73 | }); 74 | 75 | const createdAuthorConversation = await this.create({ 76 | userId: user.id.toString(), 77 | conversationId: createdConversation.id.toString() 78 | }); 79 | await this.create({ 80 | userId: input.recipientId.toString(), 81 | conversationId: createdConversation.id.toString() 82 | }); 83 | return createdAuthorConversation; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/app/security/access-decision/access-decision-manager.interface.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AccessDecisionManagerInterface { 3 | decide(token, attributes: any[], object: any): Promise; 4 | } 5 | -------------------------------------------------------------------------------- /src/app/security/access-decision/access-decision-manager.ts: -------------------------------------------------------------------------------- 1 | import {AccessDecisionManagerInterface} from './access-decision-manager.interface'; 2 | import {AccessDecisionStrategyEnum} from './access-decision-strategy.enum'; 3 | import {AccessEnum, Voter} from '../voter'; 4 | import {ucfirst} from '../../_helpers'; 5 | 6 | export class AccessDecisionManager implements AccessDecisionManagerInterface { 7 | 8 | private strategyMethod: string; 9 | 10 | constructor( 11 | private voters: IterableIterator, 12 | private strategy: AccessDecisionStrategyEnum = AccessDecisionStrategyEnum.STRATEGY_AFFIRMATIVE, 13 | private allowIfAllAbstainDecisions: boolean = false, 14 | private allowIfEqualGrantedDeniedDecisions: boolean = true 15 | ) { 16 | const strategyMethod = 'decide' + ucfirst(strategy); 17 | if (typeof this[strategyMethod] !== 'function') { 18 | throw new Error(`'The strategy "${strategyMethod}" is not supported.'`); 19 | } 20 | this.strategyMethod = strategyMethod; 21 | } 22 | 23 | public async decide(token, attributes: any[], object: any): Promise { 24 | return this[this.strategyMethod].call(this, token, attributes, object); 25 | } 26 | 27 | private async decideAffirmative(token, attributes, object): Promise { 28 | let deny = 0; 29 | for (const voter of this.voters) { 30 | const result = await voter.vote(token, object, attributes); 31 | switch (result) { 32 | case AccessEnum.ACCESS_GRANTED: 33 | return true; 34 | case AccessEnum.ACCESS_DENIED: 35 | ++deny; 36 | break; 37 | default: 38 | break; 39 | } 40 | } 41 | if (deny > 0) { 42 | return false; 43 | } 44 | return this.allowIfAllAbstainDecisions; 45 | } 46 | 47 | private async decideConsensus(token, attributes, object = null): Promise { 48 | let grant = 0; 49 | let deny = 0; 50 | for (const voter of this.voters) { 51 | const result = await voter.vote(token, object, attributes); 52 | switch (result) { 53 | case AccessEnum.ACCESS_GRANTED: 54 | ++grant; 55 | break; 56 | case AccessEnum.ACCESS_DENIED: 57 | ++deny; 58 | break; 59 | } 60 | } 61 | if (grant > deny) { 62 | return true; 63 | } 64 | if (deny > grant) { 65 | return false; 66 | } 67 | if (grant > 0) { 68 | return this.allowIfEqualGrantedDeniedDecisions; 69 | } 70 | return this.allowIfAllAbstainDecisions; 71 | } 72 | 73 | private async decideUnanimous(token, attributes, object = null): Promise { 74 | let grant = 0; 75 | for (const voter of this.voters) { 76 | for (const attribute of attributes) { 77 | const result = await voter.vote(token, object, [attribute]); 78 | switch (result) { 79 | case AccessEnum.ACCESS_GRANTED: 80 | ++grant; 81 | break; 82 | case AccessEnum.ACCESS_DENIED: 83 | return false; 84 | default: 85 | break; 86 | } 87 | } 88 | } 89 | if (grant > 0) { 90 | return true; 91 | } 92 | return this.allowIfAllAbstainDecisions; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/app/security/access-decision/access-decision-strategy.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AccessDecisionStrategyEnum { 2 | STRATEGY_AFFIRMATIVE = 'affirmative', 3 | STRATEGY_CONSENSUS = 'consensus', 4 | STRATEGY_UNANIMOUS = 'unanimous' 5 | } 6 | -------------------------------------------------------------------------------- /src/app/security/access-decision/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access-decision-manager.interface'; 2 | export * from './access-decision-strategy.enum'; 3 | export * from './access-decision-manager'; 4 | -------------------------------------------------------------------------------- /src/app/security/authorization-checker/authorization-checker.interface.ts: -------------------------------------------------------------------------------- 1 | export interface AuthorizationCheckerInterface { 2 | isGranted(attributes: any[], subject?: any): Promise; 3 | } 4 | -------------------------------------------------------------------------------- /src/app/security/authorization-checker/authorization-checker.ts: -------------------------------------------------------------------------------- 1 | import {AuthorizationCheckerInterface} from './authorization-checker.interface'; 2 | import {AccessDecisionManager, AccessDecisionStrategyEnum} from '../access-decision'; 3 | import {Injectable} from '@nestjs/common'; 4 | import {VoterRegistry} from '../voter'; 5 | import {RequestContext} from '../../_helpers/request-context'; 6 | 7 | @Injectable() 8 | export class AuthorizationChecker implements AuthorizationCheckerInterface { 9 | private readonly adm: AccessDecisionManager; 10 | private readonly tokenStorage; 11 | 12 | constructor(voterRegistry: VoterRegistry) { 13 | this.adm = new AccessDecisionManager(voterRegistry.getVoters(), AccessDecisionStrategyEnum.STRATEGY_AFFIRMATIVE, true); 14 | this.tokenStorage = function () { 15 | return { 16 | getUser: () => RequestContext.currentUser() 17 | }; 18 | }; 19 | } 20 | 21 | public async isGranted(attributes, subject = null) { 22 | const token = this.tokenStorage(); 23 | 24 | if (!Array.isArray(attributes)) { 25 | attributes = [attributes]; 26 | } 27 | 28 | return this.adm.decide(token, attributes, subject); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/app/security/authorization-checker/index.ts: -------------------------------------------------------------------------------- 1 | export * from './authorization-checker.interface'; 2 | export * from './authorization-checker'; 3 | -------------------------------------------------------------------------------- /src/app/security/index.ts: -------------------------------------------------------------------------------- 1 | export * from './voter'; 2 | export * from './access-decision'; 3 | export * from './authorization-checker'; 4 | export * from './security.module'; 5 | -------------------------------------------------------------------------------- /src/app/security/security.module.ts: -------------------------------------------------------------------------------- 1 | import {Global, Module} from '@nestjs/common'; 2 | import {VoterRegistry} from './voter'; 3 | import {AuthorizationChecker} from './authorization-checker'; 4 | import {SecurityService} from './security.service'; 5 | 6 | const PROVIDERS = [ 7 | VoterRegistry, 8 | SecurityService, 9 | AuthorizationChecker 10 | ]; 11 | 12 | @Global() 13 | @Module({ 14 | providers: [...PROVIDERS], 15 | exports: [...PROVIDERS] 16 | }) 17 | export class SecurityModule { 18 | } 19 | -------------------------------------------------------------------------------- /src/app/security/security.service.ts: -------------------------------------------------------------------------------- 1 | import {ForbiddenException, Injectable} from '@nestjs/common'; 2 | import {AuthorizationChecker} from './authorization-checker'; 3 | 4 | @Injectable() 5 | export class SecurityService { 6 | constructor(private readonly authorizationChecker: AuthorizationChecker) { 7 | 8 | } 9 | 10 | public async denyAccessUnlessGranted(attributes, subject): Promise { 11 | const isGranted = await this.authorizationChecker.isGranted(attributes, subject); 12 | if (!isGranted) { 13 | throw new ForbiddenException(`You don't have permission to access this resource`); 14 | } 15 | return isGranted; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/security/voter/access.enum.ts: -------------------------------------------------------------------------------- 1 | export enum AccessEnum { 2 | ACCESS_GRANTED = 1, 3 | ACCESS_ABSTAIN = 0, 4 | ACCESS_DENIED = -1 5 | } 6 | -------------------------------------------------------------------------------- /src/app/security/voter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './access.enum'; 2 | export * from './rest-voter-action.enum'; 3 | export * from './voter.interface'; 4 | export * from './voter-registry'; 5 | export * from './voter'; 6 | -------------------------------------------------------------------------------- /src/app/security/voter/rest-voter-action.enum.ts: -------------------------------------------------------------------------------- 1 | export enum RestVoterActionEnum { 2 | READ_ALL = 'read_all', 3 | READ = 'read', 4 | CREATE = 'create', 5 | UPDATE = 'update', 6 | DELETE = 'delete', 7 | SOFT_DELETE = 'soft_delete' 8 | } 9 | -------------------------------------------------------------------------------- /src/app/security/voter/voter-registry.ts: -------------------------------------------------------------------------------- 1 | import {Voter} from './voter'; 2 | import {Injectable} from '@nestjs/common'; 3 | 4 | @Injectable() 5 | export class VoterRegistry { 6 | private voters = new Set(); 7 | 8 | public register(voter: Voter) { 9 | this.voters.add(voter); 10 | } 11 | 12 | public getVoters(): IterableIterator { 13 | return this.voters.values(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/security/voter/voter.interface.ts: -------------------------------------------------------------------------------- 1 | import {AccessEnum} from './access.enum'; 2 | 3 | export interface VoterInterface { 4 | vote(token: any, subject: any, attributes: any[]): Promise; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/security/voter/voter.ts: -------------------------------------------------------------------------------- 1 | import {forwardRef, Inject, OnModuleInit} from '@nestjs/common'; 2 | import {VoterInterface} from './voter.interface'; 3 | import {AccessEnum} from './access.enum'; 4 | import {VoterRegistry} from './voter-registry'; 5 | 6 | export abstract class Voter implements VoterInterface, OnModuleInit { 7 | 8 | @Inject(forwardRef(() => VoterRegistry)) private readonly voterRegistry: VoterRegistry; 9 | 10 | public onModuleInit() { 11 | this.voterRegistry.register(this); 12 | } 13 | 14 | public async vote(token: any, subject: any, attributes: any[]): Promise { 15 | let vote = AccessEnum.ACCESS_ABSTAIN; 16 | 17 | for (const attribute of attributes) { 18 | if (!this.supports(attribute, subject)) { 19 | continue; 20 | } 21 | // as soon as at least one attribute is supported, default is to deny access 22 | vote = AccessEnum.ACCESS_DENIED; 23 | if (await this.voteOnAttribute(attribute, subject, token)) { 24 | // grant access as soon as at least one attribute returns a positive response 25 | return AccessEnum.ACCESS_GRANTED; 26 | } 27 | } 28 | 29 | return vote; 30 | } 31 | 32 | protected abstract supports(attribute, subject): boolean; 33 | 34 | protected async abstract voteOnAttribute(attribute, subject, token): Promise; 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/app/user/dto/delete-user.dto.ts: -------------------------------------------------------------------------------- 1 | import {DeleteUserInput} from '../../graphql.schema'; 2 | 3 | export class DeleteUserDto extends DeleteUserInput {} 4 | -------------------------------------------------------------------------------- /src/app/user/dto/index.ts: -------------------------------------------------------------------------------- 1 | import { UpdateUserDto } from './update-user.dto'; 2 | import { DeleteUserDto } from './delete-user.dto'; 3 | 4 | export { 5 | UpdateUserDto, 6 | DeleteUserDto 7 | }; 8 | -------------------------------------------------------------------------------- /src/app/user/dto/subscription.dto.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsBoolean, IsOptional} from 'class-validator'; 3 | 4 | export class SubscriptionDto { 5 | 6 | @ApiModelProperty({ 7 | required: false 8 | }) 9 | @IsOptional() 10 | @IsBoolean() 11 | public email?: boolean; 12 | 13 | @ApiModelProperty({ 14 | required: false 15 | }) 16 | @IsOptional() 17 | @IsBoolean() 18 | public push?: boolean; 19 | 20 | @ApiModelProperty({ 21 | required: false 22 | }) 23 | @IsOptional() 24 | @IsBoolean() 25 | public sms?: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/app/user/dto/update-user.dto.ts: -------------------------------------------------------------------------------- 1 | import {UpdateUserInput} from '../../graphql.schema'; 2 | 3 | export class UpdateUserDto extends UpdateUserInput {} 4 | -------------------------------------------------------------------------------- /src/app/user/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.entity'; 2 | export * from './user-email.entity'; 3 | -------------------------------------------------------------------------------- /src/app/user/entity/social.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {ExtendedEntity} from '../../_helpers'; 3 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 4 | 5 | @Entity() 6 | export class SocialEntity extends ExtendedEntity { 7 | 8 | @ApiModelProperty() 9 | @ObjectIdColumn() 10 | public id: string; 11 | 12 | @ApiModelProperty() 13 | @Column() 14 | public userId: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/user/entity/user-email.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsString} from 'class-validator'; 3 | import {ExtendedEntity} from '../../_helpers'; 4 | import {Column, Entity, PrimaryColumn} from 'typeorm'; 5 | 6 | @Entity() 7 | export class UserEmailEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @PrimaryColumn() 11 | public id: string; // user email 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public user_id: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/app/user/entity/user-subscription.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsBoolean, IsOptional, IsString} from 'class-validator'; 3 | import {ExtendedEntity} from '../../_helpers'; 4 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 5 | 6 | @Entity() 7 | export class UserSubscriptionEntity extends ExtendedEntity { 8 | 9 | @ApiModelProperty() 10 | @ObjectIdColumn() 11 | public id: string; 12 | 13 | @ApiModelProperty() 14 | @IsString() 15 | @Column() 16 | public user: string; 17 | 18 | @ApiModelProperty() 19 | @IsBoolean() 20 | @IsOptional() 21 | @Column() 22 | public email: boolean; 23 | 24 | @ApiModelProperty() 25 | @IsBoolean() 26 | @IsOptional() 27 | @Column() 28 | public push = false; 29 | 30 | @ApiModelProperty() 31 | @IsBoolean() 32 | @IsOptional() 33 | @Column() 34 | public sms = false; 35 | } 36 | -------------------------------------------------------------------------------- /src/app/user/entity/user.entity.ts: -------------------------------------------------------------------------------- 1 | import {ApiModelProperty} from '@nestjs/swagger'; 2 | import {IsEmail, IsOptional, IsString, IsUrl, MinLength, Validate, ValidateIf} from 'class-validator'; 3 | import {DateTime} from 'luxon'; 4 | import {ExtendedEntity, passwordHash} from '../../_helpers'; 5 | import {IsUserAlreadyExist} from '../user.validator'; 6 | import {config} from '../../../config'; 7 | import {Column, Entity, ObjectIdColumn} from 'typeorm'; 8 | 9 | @Entity() 10 | export class UserEntity extends ExtendedEntity { 11 | 12 | @ApiModelProperty() 13 | @ObjectIdColumn() 14 | public id: string; 15 | 16 | @ApiModelProperty() 17 | @IsString() 18 | @Column() 19 | public first_name: string; 20 | 21 | @ApiModelProperty() 22 | @IsString() 23 | @Column() 24 | public last_name: string; 25 | 26 | @ApiModelProperty() 27 | @IsEmail() 28 | @IsOptional() 29 | @ValidateIf(o => !o.id) 30 | @Validate(IsUserAlreadyExist, { 31 | message: 'User already exists' 32 | }) 33 | @Column() 34 | public email: string; 35 | 36 | @ApiModelProperty() 37 | @IsString() 38 | @Column() 39 | public phone_num: string; 40 | 41 | @ApiModelProperty() 42 | @IsOptional() 43 | @IsUrl() 44 | @Column() 45 | public profile_img: string; 46 | 47 | @ApiModelProperty() 48 | @MinLength(config.passwordMinLength) 49 | @IsOptional() 50 | @Column() 51 | public password: string; 52 | 53 | @ApiModelProperty() 54 | @Column() 55 | public is_verified = false; 56 | 57 | @ApiModelProperty() 58 | @IsOptional() 59 | @Column() 60 | public provider: string; 61 | 62 | @ApiModelProperty() 63 | @IsOptional() 64 | @Column() 65 | public socialId: string; 66 | 67 | @ApiModelProperty() 68 | @IsOptional() 69 | @Column() 70 | public phone_token: string; 71 | 72 | @Column() 73 | public activationCode: string; 74 | 75 | @Column() 76 | public onlineAt: DateTime; 77 | 78 | hashPassword() { 79 | this.password = passwordHash(this.password); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/app/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.constants'; 2 | -------------------------------------------------------------------------------- /src/app/user/online.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@nestjs/common'; 2 | import {DateTime} from 'luxon'; 3 | import {UserEntity} from './entity'; 4 | import {UserService} from './user.service'; 5 | 6 | @Injectable() 7 | export class OnlineService { 8 | private online = new Map(); 9 | 10 | constructor(private readonly userService: UserService) { 11 | 12 | } 13 | 14 | public isOnline(user: UserEntity): boolean { 15 | return this.online.has(user.id.toString()); 16 | } 17 | 18 | public isOffline({id}: UserEntity): boolean { 19 | return !this.online.has(id.toString()); 20 | } 21 | 22 | public async addUser(user: UserEntity): Promise { 23 | user.onlineAt = DateTime.utc(); 24 | await this.userService.update(user); 25 | this.online.set(user.id.toString(), user); 26 | return this; 27 | } 28 | 29 | public async removeUser(user: UserEntity): Promise { 30 | user.onlineAt = DateTime.utc(); 31 | await this.userService.update(user); 32 | this.online.delete(user.id.toString()); 33 | return this; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/user/user-error.enum.ts: -------------------------------------------------------------------------------- 1 | export enum UserErrorEnum { 2 | NOT_VERIFIED = 100001 3 | } 4 | -------------------------------------------------------------------------------- /src/app/user/user-subscription.service.ts: -------------------------------------------------------------------------------- 1 | import {Inject, Injectable} from '@nestjs/common'; 2 | import {CrudService} from '../../base'; 3 | import {MongoRepository, Repository} from 'typeorm'; 4 | import {UserSubscriptionEntity} from './entity/user-subscription.entity'; 5 | import {USER_SUBSCRIPTION_TOKEN} from './user.constants'; 6 | 7 | @Injectable() 8 | export class UserSubscriptionService extends CrudService { 9 | 10 | constructor(@Inject(USER_SUBSCRIPTION_TOKEN) protected readonly repository: MongoRepository) { 11 | super(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/user/user.command.ts: -------------------------------------------------------------------------------- 1 | import {Injectable } from '@nestjs/common'; 2 | import {Command, Positional} from 'nestjs-command'; 3 | import {DateTime} from 'luxon'; 4 | import {UserService} from './user.service'; 5 | import {UserEntity} from './entity'; 6 | import faker from 'faker'; 7 | import {passwordHash} from '../_helpers'; 8 | import {AppLogger} from '../app.logger'; 9 | import {plainToClass} from 'class-transformer'; 10 | 11 | @Injectable() 12 | export class UserCommand { 13 | 14 | private logger = new AppLogger(UserCommand.name); 15 | 16 | constructor( 17 | private readonly userService: UserService 18 | ) { 19 | faker.locale = 'en_US'; 20 | } 21 | 22 | // example usage: npm run cli -- create:user 100 23 | @Command({ command: 'create:user [amount]', describe: 'create a user' }) 24 | public async create(@Positional({name: 'amount'}) amount) { 25 | amount = parseInt(amount || 10, 10); 26 | this.logger.debug(`[create] execute for amount ${amount}!`); 27 | 28 | this.logger.debug(`[create] delete from db everything with provider "faker"`); 29 | await this.userService.deleteAll({provider: {eq: 'faker'}}); 30 | 31 | const persons: UserEntity[] = []; 32 | for (let i = 0; i < amount; i++) { 33 | const first_name = faker.name.firstName(); 34 | const last_name = faker.name.lastName(); 35 | const person: UserEntity = plainToClass(UserEntity, { 36 | first_name, 37 | last_name, 38 | is_verified: true, 39 | provider: 'faker', 40 | socialId: faker.random.uuid(), 41 | email: faker.internet.email(first_name, last_name).toLowerCase(), 42 | password: faker.internet.password(), 43 | phone_num: faker.phone.phoneNumber(), 44 | profile_img: faker.internet.avatar(), 45 | createdAt: DateTime.utc().toString(), 46 | updatedAt: DateTime.utc().toString() 47 | }); 48 | persons.push(person); 49 | // this.logger.debug(`[create] create random person ppl with provider "faker" as ${JSON.stringify(person)}`); 50 | // await this.userService.create(person); 51 | } 52 | 53 | this.logger.debug(`[create] create ${amount} random ppl with provider "faker"`); 54 | await this.userService.saveAll(persons); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/user/user.constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_TOKEN = 'UserRepositoryToken'; 2 | export const USER_EMAIL_TOKEN = 'UserEmailRepositoryToken'; 3 | export const USER_SUBSCRIPTION_TOKEN = 'UserSubscriptionRepositoryToken'; 4 | export const USER_CMD_REGISTER = 'onUserRegisterCmd'; 5 | export const USER_CMD_REGISTER_VERIFY = 'onUserRegisterVerifyCmd'; 6 | export const USER_CMD_PASSWORD_RESET = 'onUserPasswordRestCmd'; 7 | export const USER_CMD_PASSWORD_NEW = 'onUserPasswordNewCmd'; 8 | -------------------------------------------------------------------------------- /src/app/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Controller, HttpCode, Post, UseGuards} from '@nestjs/common'; 2 | import {MessagePattern} from '@nestjs/microservices'; 3 | import {AuthGuard} from '@nestjs/passport'; 4 | import {ApiBearerAuth, ApiImplicitBody, ApiResponse, ApiUseTags} from '@nestjs/swagger'; 5 | import voucherCodes from 'voucher-code-generator'; 6 | import {config} from '../../config'; 7 | import {User} from '../_helpers/decorators'; 8 | import {mail, renderTemplate} from '../_helpers/mail'; 9 | import {sms} from '../_helpers/sms'; 10 | import {SMSTypeEnum} from '../_helpers/sms/SMSType.enum'; 11 | import {AppLogger} from '../app.logger'; 12 | import {createToken} from '../auth/jwt'; 13 | import {UserEntity} from './entity'; 14 | import {UserCommand} from './user.command'; 15 | import {USER_CMD_PASSWORD_NEW, USER_CMD_PASSWORD_RESET, USER_CMD_REGISTER, USER_CMD_REGISTER_VERIFY} from './user.constants'; 16 | import {UserService} from './user.service'; 17 | import {SubscriptionDto} from './dto/subscription.dto'; 18 | 19 | @Controller('user') 20 | @ApiUseTags('user') 21 | export class UserController { 22 | private logger = new AppLogger(UserController.name); 23 | 24 | constructor( 25 | protected service: UserService, 26 | private userCmd: UserCommand 27 | ) { 28 | 29 | } 30 | 31 | @Post('subscription') 32 | @ApiBearerAuth() 33 | @UseGuards(AuthGuard('jwt')) 34 | @HttpCode(201) 35 | @ApiImplicitBody({required: true, type: SubscriptionDto, name: 'SubscriptionDto'}) 36 | @ApiResponse({status: 201, description: 'ACCEPTED'}) 37 | public async subscription(@Body() data: SubscriptionDto, @User() user: UserEntity): Promise { 38 | try { 39 | const subscription = await this.service.subscription.findOne({where: {user: {eq: user.id.toString()}}}); 40 | await this.service.subscription.patch(subscription.id, data); 41 | } catch (err) { 42 | await this.service.subscription.create({user: user.id.toString(), ...data}); 43 | } 44 | } 45 | 46 | @Post('import') 47 | @ApiBearerAuth() 48 | @UseGuards(AuthGuard('jwt')) 49 | public async importUsers(): Promise { 50 | return this.userCmd.create(20); 51 | } 52 | 53 | @MessagePattern({cmd: USER_CMD_REGISTER}) 54 | public async onUserRegister(user: UserEntity): Promise { 55 | try { 56 | this.logger.debug(`[onUserRegister] Send registration SMS for user ${user.email}`); 57 | const token = voucherCodes.generate({ 58 | pattern: '######', 59 | charset: voucherCodes.charset('numbers') 60 | }).pop(); 61 | user = await this.service.patch(user.id.toString(), {activationCode: token}); 62 | await sms({ 63 | sender: `AVA`, 64 | phoneNumber: user.phone_num, 65 | smsType: SMSTypeEnum.TRANSACTIONAL, 66 | message: `${token} is your AVA verification and the start of something amazing. Lets go!` 67 | }); 68 | this.logger.debug('[onUserRegister] Registration SMS sent'); 69 | } catch (err) { 70 | this.logger.error(`[onUserRegister] SMS not sent, because ${err.message}`, err.stack); 71 | } 72 | } 73 | 74 | @MessagePattern({cmd: USER_CMD_REGISTER_VERIFY}) 75 | public async onUserRegisterVerify(user: UserEntity): Promise { 76 | try { 77 | this.logger.debug(`[onUserRegisterVerify] Send welcome email for user ${user.email}`); 78 | await mail({ 79 | subject: `Welcome ${user.first_name} to ${config.name.toUpperCase()}`, 80 | to: user.email, 81 | html: renderTemplate(`/mail/welcome.twig`, {user, config}) 82 | }); 83 | this.logger.debug('[onUserRegisterVerify] Welcome email sent'); 84 | } catch (err) { 85 | this.logger.error(`[onUserRegisterVerify] Mail not sent, because ${err.message}`, err.stack); 86 | } 87 | } 88 | 89 | @MessagePattern({cmd: USER_CMD_PASSWORD_RESET}) 90 | public async onUserPasswordRest({email}: { email: string }): Promise { 91 | try { 92 | const user = await this.service.findOne({where: {email}}); 93 | this.logger.debug(`[onUserRegister] Send password reset instruction email for user ${user.email}`); 94 | const token = createToken(user.id.toString(), config.session.password_reset.timeout, config.session.password_reset.secret); 95 | await mail({ 96 | subject: `Reset your password`, 97 | to: user.email, 98 | html: renderTemplate(`/mail/password_reset.twig`, {user, config, token}) 99 | }); 100 | this.logger.debug('[onUserRegister] Password reset email sent'); 101 | } catch (err) { 102 | this.logger.error(`[onUserRegister] Mail not sent, because ${JSON.stringify(err.message)}`, err.stack); 103 | } 104 | } 105 | 106 | @MessagePattern({cmd: USER_CMD_PASSWORD_NEW}) 107 | public async onUserPasswordNew(user: UserEntity): Promise { 108 | try { 109 | this.logger.debug(`[onUserRegister] Send password new email for user ${user.email}`); 110 | await mail({ 111 | subject: `You have a new password!`, 112 | to: user.email, 113 | html: renderTemplate(`/mail/password_new.twig`, {user, config}) 114 | }); 115 | this.logger.debug('[onUserRegister] Password new email sent'); 116 | } catch (err) { 117 | this.logger.error(`[onUserRegister] Mail not sent, because ${err.message}`, err.stack); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/app/user/user.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | me: User 3 | } 4 | 5 | type Mutation { 6 | updateUser(updateUserInput: UpdateUserInput): User 7 | deleteUser(deleteUserInput: DeleteUserInput): User 8 | } 9 | 10 | type Subscription { 11 | userCreated: User 12 | userDeleted: User 13 | } 14 | 15 | type User { 16 | id: ID! 17 | first_name: String 18 | last_name: String 19 | email: String 20 | phone_num: String 21 | profile_img: String 22 | password: String 23 | provider: String 24 | phone_token: String 25 | socialId: String 26 | subscriptions: UserSubscription 27 | createdAt: Date 28 | updatedAt: Date 29 | } 30 | 31 | type UserSubscription { 32 | email: Boolean 33 | push: Boolean 34 | sms: Boolean 35 | } 36 | 37 | input UpdateUserInput { 38 | first_name: String 39 | last_name: String 40 | email: String 41 | phone_num: String 42 | profile_img: String 43 | phone_token: String 44 | password: String 45 | } 46 | 47 | input DeleteUserInput { 48 | id: ID! 49 | } 50 | -------------------------------------------------------------------------------- /src/app/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import {Module} from '@nestjs/common'; 2 | import {DatabaseModule} from '../database/database.module'; 3 | import {OnlineService} from './online.service'; 4 | import {UserSubscriptionService} from './user-subscription.service'; 5 | import {UserController} from './user.controller'; 6 | import {userProviders} from './user.providers'; 7 | import {UserService} from './user.service'; 8 | import {IsUserAlreadyExist} from './user.validator'; 9 | import {UserResolver} from './user.resolver'; 10 | import {UserCommand} from './user.command'; 11 | 12 | const PROVIDERS = [ 13 | ...userProviders, 14 | IsUserAlreadyExist, 15 | UserService, 16 | OnlineService, 17 | UserSubscriptionService, 18 | UserResolver, 19 | UserCommand 20 | ]; 21 | 22 | @Module({ 23 | controllers: [UserController], 24 | providers: [...PROVIDERS], 25 | imports: [DatabaseModule], 26 | exports: [UserService, OnlineService] 27 | }) 28 | export class UserModule { 29 | } 30 | -------------------------------------------------------------------------------- /src/app/user/user.providers.ts: -------------------------------------------------------------------------------- 1 | import {DB_CON_TOKEN} from '../database/database.constants'; 2 | import {UserEmailEntity, UserEntity} from './entity'; 3 | import {USER_EMAIL_TOKEN, USER_SUBSCRIPTION_TOKEN, USER_TOKEN} from './user.constants'; 4 | import {UserSubscriptionEntity} from './entity/user-subscription.entity'; 5 | import {Connection} from 'typeorm'; 6 | 7 | export const userProviders = [ 8 | { 9 | provide: USER_TOKEN, 10 | useFactory: (connection: Connection) => connection.getRepository(UserEntity), 11 | inject: [DB_CON_TOKEN] 12 | }, 13 | { 14 | provide: USER_EMAIL_TOKEN, 15 | useFactory: (connection: Connection) => connection.getRepository(UserEmailEntity), 16 | inject: [DB_CON_TOKEN] 17 | }, 18 | { 19 | provide: USER_SUBSCRIPTION_TOKEN, 20 | useFactory: (connection: Connection) => connection.getRepository(UserSubscriptionEntity), 21 | inject: [DB_CON_TOKEN] 22 | } 23 | ]; 24 | -------------------------------------------------------------------------------- /src/app/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import {Args, Mutation, Parent, Query, ResolveProperty, Resolver, Subscription} from '@nestjs/graphql'; 2 | import {PubSub} from 'graphql-subscriptions'; 3 | import {UserService} from './user.service'; 4 | import {DeleteUserDto, UpdateUserDto} from './dto'; 5 | import {UseGuards} from '@nestjs/common'; 6 | import {GraphqlGuard} from '../_helpers/graphql'; 7 | import {UserEntity} from './entity'; 8 | import {User as CurrentUser} from '../_helpers/graphql/user.decorator'; 9 | import {UserSubscriptionEntity} from './entity/user-subscription.entity'; 10 | 11 | @Resolver('User') 12 | @UseGuards(GraphqlGuard) 13 | export class UserResolver { 14 | private pubSub = new PubSub(); 15 | 16 | constructor(private readonly userService: UserService) { 17 | } 18 | 19 | @Query('me') 20 | async getMe(@CurrentUser() user: UserEntity): Promise { 21 | return user; 22 | } 23 | 24 | @Mutation('deleteUser') 25 | async delete(@Args('deleteUserInput') args: DeleteUserDto): Promise { 26 | const deletedUser = await this.userService.delete(args.id); 27 | await this.pubSub.publish('userDeleted', {userDeleted: deletedUser}); 28 | return deletedUser; 29 | } 30 | 31 | @Mutation('updateUser') 32 | async update(@CurrentUser() user: UserEntity, @Args('updateUserInput') args: UpdateUserDto): Promise { 33 | const updatedUser = await this.userService.patch(user.id.toString(), args); 34 | await this.pubSub.publish('userUpdated', {userUpdated: updatedUser}); 35 | return updatedUser; 36 | } 37 | 38 | @Subscription('userCreated') 39 | userCreated() { 40 | return { 41 | subscribe: () => this.pubSub.asyncIterator('userCreated') 42 | }; 43 | } 44 | 45 | @Subscription('userDeleted') 46 | userDeleted() { 47 | return { 48 | subscribe: () => this.pubSub.asyncIterator('userDeleted') 49 | }; 50 | } 51 | 52 | @ResolveProperty('subscriptions') 53 | getHome(@Parent() user: UserEntity): Promise { 54 | return this.userService.subscription.findOne({where: {user: {eq: user.id.toString()}}}); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpException, HttpStatus, Inject, Injectable, NotFoundException} from '@nestjs/common'; 2 | import {DateTime} from 'luxon'; 3 | import {Repository, DeepPartial, MongoRepository} from 'typeorm'; 4 | import {CrudService} from '../../base'; 5 | import {passwordHash, RestException} from '../_helpers'; 6 | import {AppLogger} from '../app.logger'; 7 | import {CredentialsDto} from '../auth/dto/credentials.dto'; 8 | import {UserEmailEntity, UserEntity} from './entity'; 9 | import {UserErrorEnum} from './user-error.enum'; 10 | import {UserSubscriptionService} from './user-subscription.service'; 11 | import {USER_EMAIL_TOKEN, USER_TOKEN} from './user.constants'; 12 | 13 | @Injectable() 14 | export class UserService extends CrudService { 15 | private logger = new AppLogger(UserService.name); 16 | 17 | constructor( 18 | public readonly subscription: UserSubscriptionService, 19 | @Inject(USER_TOKEN) protected readonly repository: MongoRepository, 20 | @Inject(USER_EMAIL_TOKEN) protected readonly userEmailRepository: Repository 21 | ) { 22 | super(); 23 | } 24 | 25 | public async findByEmail(email: string): Promise { 26 | this.logger.debug(`[findByEmail] Looking in users for ${email}`); 27 | const user = await this.findOne({where: {email: {eq: email}}}); 28 | if (user) { 29 | this.logger.debug(`[findByEmail] Found in users an user with id ${user.id}`); 30 | } else { 31 | this.logger.debug(`[findByEmail] Not found in users an user with email ${email}`); 32 | } 33 | return user; 34 | } 35 | 36 | public async login(credentials: CredentialsDto): Promise { 37 | const user = await this.findByEmail(credentials.email); 38 | 39 | if (!user) { 40 | throw new HttpException({ 41 | error: 'User', 42 | message: `User not found` 43 | }, HttpStatus.NOT_FOUND); 44 | } 45 | 46 | if (user.password !== passwordHash(credentials.password)) { 47 | throw new NotFoundException(`User doesn't exists`); 48 | } 49 | 50 | if (!user.is_verified) { 51 | throw new RestException({ 52 | error: 'User', 53 | message: `User is not verified`, 54 | condition: UserErrorEnum.NOT_VERIFIED 55 | }, HttpStatus.PRECONDITION_FAILED); 56 | } 57 | 58 | return user; 59 | } 60 | 61 | public async create(data: DeepPartial): Promise { 62 | const entity = this.repository.create(data); 63 | await this.validate(entity); 64 | entity.hashPassword(); 65 | if (!entity.createdAt) { 66 | entity.createdAt = DateTime.utc(); 67 | } 68 | entity.updatedAt = DateTime.utc(); 69 | const user = await entity.save(); 70 | await this.subscription.create({user: user.id, email: true}); 71 | return user; 72 | } 73 | 74 | public async updatePassword(data: DeepPartial): Promise { 75 | const entity = await this.repository.findOneOrFail(data.id); 76 | entity.password = data.password; 77 | await this.validate(entity); 78 | entity.hashPassword(); 79 | entity.updatedAt = DateTime.utc(); 80 | return this.repository.save(entity); 81 | } 82 | 83 | public async socialRegister(data: DeepPartial) { 84 | const entity = this.repository.create(data); 85 | await this.validate(entity, {skipMissingProperties: true}); 86 | entity.createdAt = DateTime.utc(); 87 | entity.updatedAt = DateTime.utc(); 88 | return this.repository.save(entity); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/app/user/user.validator.ts: -------------------------------------------------------------------------------- 1 | import {ValidatorConstraint, ValidatorConstraintInterface} from 'class-validator'; 2 | import {UserService} from './user.service'; 3 | import {Injectable} from '@nestjs/common'; 4 | 5 | @ValidatorConstraint({ name: 'isUserAlreadyExist', async: true }) 6 | @Injectable() 7 | export class IsUserAlreadyExist implements ValidatorConstraintInterface { 8 | constructor(protected readonly userService: UserService) {} 9 | 10 | public async validate(email: string) { 11 | if (!this.userService) { 12 | return true; 13 | } 14 | const user = await this.userService.findByEmail(email); 15 | return !user; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/assets/media/templates/brochure_01.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 | house_01 143 |
144 | 145 | 146 | 147 |
148 |
Listed At:
149 |
$
150 |
{{home.price}}
151 |
152 | 153 |
154 |
{{home.descr}}
155 | 156 |
{{home.descr}}
157 | 158 |
{{home.descr}}
159 | 160 |
161 | 162 |
163 |
{{home.address_1}}
{{home.address_2}}
164 |
{{home.sqft}} sq. ft.
165 |
{{home.beds}}} Beds
166 |
{{home.baths}} Baths
167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/base/crud.service.ts: -------------------------------------------------------------------------------- 1 | import {forwardRef, HttpException, HttpStatus, Inject} from '@nestjs/common'; 2 | import {validate, ValidatorOptions} from 'class-validator'; 3 | import {DateTime} from 'luxon'; 4 | import {DeepPartial, FindManyOptions, FindOneOptions, MongoRepository, ObjectLiteral} from 'typeorm'; 5 | import {ExtendedEntity, typeormFilterMapper} from '../app/_helpers'; 6 | import {SecurityService} from '../app/security/security.service'; 7 | import {RestVoterActionEnum} from '../app/security/voter'; 8 | import {config} from '../config'; 9 | 10 | export class CrudService { 11 | protected repository: MongoRepository; 12 | @Inject(forwardRef(() => SecurityService)) protected readonly securityService: SecurityService; 13 | 14 | constructor(repository?: MongoRepository) { 15 | if (repository) { 16 | this.repository = repository; 17 | } 18 | } 19 | 20 | public async findAll(options?: FindManyOptions): Promise { 21 | if (options.where) { 22 | options.where = typeormFilterMapper(options); 23 | } 24 | const entities = await this.repository.find(options); 25 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.READ_ALL, entities); 26 | return entities; 27 | } 28 | 29 | public async findOneById(id: string): Promise { 30 | try { 31 | const entity = await this.repository.findOneOrFail(id); 32 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.READ, entity); 33 | return entity; 34 | } catch (e) { 35 | throw new HttpException({ 36 | error: 'Database', 37 | message: 'Item not found' 38 | }, HttpStatus.NOT_FOUND); 39 | } 40 | } 41 | 42 | public async findOne(options?: FindOneOptions): Promise { 43 | if (options.where) { 44 | options.where = typeormFilterMapper(options); 45 | } 46 | const entity = await this.repository.findOne(options); 47 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.READ, entity); 48 | return entity; 49 | } 50 | 51 | public async create(data: DeepPartial): Promise { 52 | const entity: T = this.repository.create(data); 53 | entity.createdAt = DateTime.utc(); 54 | entity.updatedAt = DateTime.utc(); 55 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.CREATE, entity); 56 | await this.validate(entity, { 57 | groups: ['create'] 58 | }); 59 | return entity.save(); 60 | } 61 | 62 | public async saveAll(data: DeepPartial[]): Promise { 63 | return this.repository.save(data); 64 | } 65 | 66 | public async update(data: DeepPartial|T): Promise { 67 | const id: string = String(data.id || ''); 68 | return this.patch(id, data); 69 | } 70 | 71 | public async updateAll(query, data: any): Promise { 72 | if (query) { 73 | query = typeormFilterMapper({where: query }); 74 | } 75 | const response = await this.repository.updateMany(query, data); 76 | return !!response.matchedCount; 77 | } 78 | 79 | public async patch(id: string, data: DeepPartial|T): Promise { 80 | let entity: T = null; 81 | if (data instanceof ExtendedEntity) { 82 | entity = data; 83 | } else { 84 | entity = await this.findOneById(id); 85 | if (data.id) { 86 | delete data.id; 87 | } 88 | this.repository.merge(entity, data); 89 | } 90 | let createdAt = entity.createdAt; 91 | if (!createdAt) { 92 | createdAt = DateTime.utc(); 93 | } 94 | entity.createdAt = createdAt; 95 | entity.updatedAt = DateTime.utc(); 96 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.UPDATE, entity); 97 | await this.validate(entity, { 98 | groups: ['update'] 99 | }); 100 | return entity.save(); 101 | } 102 | 103 | public async delete(id: string): Promise { 104 | const entity: T = await this.findOneById(id); 105 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.UPDATE, entity); 106 | await this.repository.delete(id); 107 | return entity; 108 | } 109 | 110 | public deleteAll(conditions?: ObjectLiteral): Promise { 111 | if (conditions) { 112 | conditions = typeormFilterMapper({where: conditions}); 113 | } 114 | return this.repository.deleteMany(conditions); 115 | } 116 | 117 | 118 | public async softDelete({id}: DeepPartial): Promise { 119 | const entity = await this.findOneById(id as any); 120 | await this.securityService.denyAccessUnlessGranted(RestVoterActionEnum.SOFT_DELETE, entity); 121 | entity.isDeleted = true; 122 | entity.updatedAt = DateTime.utc(); 123 | return entity.save(); 124 | } 125 | 126 | protected async validate(entity: T, options?: ValidatorOptions) { 127 | const errors = await validate(entity, {...config.validator, options} as ValidatorOptions); 128 | if (errors.length) { 129 | throw new HttpException({ 130 | message: errors, 131 | error: 'Validation' 132 | }, HttpStatus.UNPROCESSABLE_ENTITY); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './crud.service'; 2 | export * from './rest.controller'; 3 | -------------------------------------------------------------------------------- /src/base/rest.controller.ts: -------------------------------------------------------------------------------- 1 | import {Body, Delete, Get, Param, Patch, Post, Put, Req} from '@nestjs/common'; 2 | import {DeepPartial} from 'typeorm'; 3 | import {CrudService} from './crud.service'; 4 | import {ExtendedEntity} from '../app/_helpers'; 5 | 6 | export class RestController { 7 | protected service: CrudService; 8 | 9 | @Get('/') 10 | public findAll(@Req() req): Promise { 11 | return this.service.findAll(); 12 | } 13 | 14 | @Get('/:id') 15 | public async findOne(@Param('id') id: string) { 16 | return this.service.findOneById(id); 17 | } 18 | 19 | @Post('/') 20 | public async create(@Body() data: DeepPartial): Promise { 21 | return this.service.create(data); 22 | } 23 | 24 | @Put('/:id') 25 | public async update(@Body() data: DeepPartial): Promise { 26 | return this.service.update(data); 27 | } 28 | 29 | @Patch('/:id') 30 | public async patch(@Param('id') id: string, @Body() data: DeepPartial): Promise { 31 | return this.service.patch(id, data); 32 | } 33 | 34 | @Delete('/:id') 35 | public async delete(@Param('id') id: string): Promise { 36 | return this.service.delete(id); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import exitHook from 'async-exit-hook'; 2 | import {CommandModule, CommandService} from 'nestjs-command'; 3 | import {AppDispatcher, AppLogger} from './app'; 4 | 5 | const logger = new AppLogger('Cli'); 6 | 7 | logger.log(`Start`); 8 | 9 | const dispatcher = new AppDispatcher(); 10 | dispatcher.getContext().then(app => { 11 | app.select(CommandModule).get(CommandService).exec(); 12 | }).catch(e => { 13 | logger.error(e.message, e.stack); 14 | process.exit(1); 15 | }); 16 | 17 | exitHook(() => logger.log('Graceful shutdown the server')); 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import exitHook from 'async-exit-hook'; 2 | import { AppDispatcher, AppLogger } from './app'; 3 | 4 | const logger = new AppLogger('Index'); 5 | 6 | logger.log(`Start`); 7 | 8 | const dispatcher = new AppDispatcher(); 9 | dispatcher.dispatch() 10 | .then(() => logger.log('Everything up')) 11 | .catch(e => { 12 | logger.error(e.message, e.stack); 13 | process.exit(1); 14 | }); 15 | 16 | exitHook(callback => { 17 | dispatcher 18 | .shutdown() 19 | .then(() => { 20 | logger.log('Graceful shutdown the server'); 21 | callback(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/migrations/1554826342372-UserConversation.ts: -------------------------------------------------------------------------------- 1 | import {getMongoManager, MigrationInterface, QueryRunner} from 'typeorm'; 2 | import {UserConversationEntity} from '../app/message/entity'; 3 | 4 | export class UserConversation1554826342372 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | const mm = getMongoManager(); 8 | await mm.updateMany(UserConversationEntity, {}, {$set: {isDeleted: false}}); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migrations/1555006181991-Home.ts: -------------------------------------------------------------------------------- 1 | import {getMongoManager, MigrationInterface, QueryRunner} from 'typeorm'; 2 | import {HomeEntity} from '../app/home/entity'; 3 | 4 | export class Home1555006181991 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | const mm = getMongoManager(); 8 | await mm.updateMany(HomeEntity, {}, {$set: {isDeleted: false}}); 9 | } 10 | 11 | 12 | public async down(queryRunner: QueryRunner): Promise { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/migrations/1555006376049-HomeFavorite.ts: -------------------------------------------------------------------------------- 1 | import {getMongoManager, MigrationInterface, QueryRunner} from 'typeorm'; 2 | import {HomeFavoriteEntity} from '../app/home-favorite/entity'; 3 | 4 | export class HomeFavorite1555006376049 implements MigrationInterface { 5 | 6 | public async up(queryRunner: QueryRunner): Promise { 7 | const mm = getMongoManager(); 8 | await mm.updateMany(HomeFavoriteEntity, {}, {$set: {isDeleted: false}}); 9 | } 10 | 11 | public async down(queryRunner: QueryRunner): Promise { 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/migrations/1555332883287-Conversation.ts: -------------------------------------------------------------------------------- 1 | import {getMongoManager, MigrationInterface, QueryRunner} from 'typeorm'; 2 | import {ConversationEntity, UserConversationEntity} from '../app/message/entity'; 3 | import {ObjectId} from 'mongodb'; 4 | 5 | export class Conversation1555332883287 implements MigrationInterface { 6 | 7 | public async up(queryRunner: QueryRunner): Promise { 8 | const mm = getMongoManager(); 9 | const userConversations = await mm.find(UserConversationEntity, {}); 10 | const checkedConversations = []; 11 | 12 | for (const userConversation of userConversations) { 13 | const isUserConversationChecked = checkedConversations 14 | .find(checkedConversationId => userConversation.conversationId === checkedConversationId); 15 | 16 | if (!isUserConversationChecked) { 17 | const secondUserConversation = userConversations 18 | .find(conversation => { 19 | return userConversation.conversationId === conversation.conversationId && userConversation.userId !== conversation.userId; 20 | }); 21 | 22 | const authorId = userConversation.createdAt.valueOf() < secondUserConversation.createdAt.valueOf() ? 23 | userConversation.userId : secondUserConversation.userId; 24 | 25 | await mm.updateOne(ConversationEntity, { _id: new ObjectId(userConversation.conversationId) }, {$set: {authorId}}); 26 | 27 | checkedConversations.push(userConversation.conversationId); 28 | } 29 | } 30 | } 31 | 32 | public async down(queryRunner: QueryRunner): Promise { 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "sourceMap": false, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "resolveJsonModule": true, 12 | "esModuleInterop": true, 13 | "importHelpers": true, 14 | "target": "es2018", 15 | "allowJs": true, 16 | "baseUrl": "src", 17 | "lib": [ 18 | "esnext", 19 | "dom" 20 | ], 21 | "typeRoots": [ 22 | "node_modules/@types" 23 | ] 24 | }, 25 | "include": [ 26 | "src/**/*" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "callable-types": true, 4 | "class-name": true, 5 | "comment-format": [ 6 | true, 7 | "check-space" 8 | ], 9 | "curly": true, 10 | "eofline": true, 11 | "forin": true, 12 | "import-spacing": true, 13 | "indent": [ 14 | true, 15 | "tabs" 16 | ], 17 | "interface-over-type-literal": true, 18 | "label-position": true, 19 | "max-line-length": [ 20 | true, 21 | 140 22 | ], 23 | "member-access": false, 24 | "member-ordering": [ 25 | true, 26 | { 27 | "order": [ 28 | "public-static-field", 29 | "public-instance-field", 30 | "private-static-field", 31 | "private-instance-field", 32 | "public-constructor", 33 | "private-constructor", 34 | "public-instance-method", 35 | "protected-instance-method", 36 | "private-instance-method" 37 | ] 38 | } 39 | ], 40 | "no-arg": true, 41 | "no-bitwise": false, 42 | "no-console": [ 43 | true, 44 | "debug", 45 | "info", 46 | "time", 47 | "timeEnd", 48 | "trace" 49 | ], 50 | "no-construct": true, 51 | "no-debugger": true, 52 | "no-duplicate-variable": true, 53 | "no-empty": false, 54 | "no-empty-interface": true, 55 | "no-eval": true, 56 | "no-inferrable-types": [true, "ignore-params"], 57 | "no-shadowed-variable": true, 58 | "no-string-literal": false, 59 | "no-string-throw": true, 60 | "no-switch-case-fall-through": true, 61 | "no-trailing-whitespace": true, 62 | "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], 63 | "no-unused-expression": true, 64 | "no-use-before-declare": true, 65 | "no-var-keyword": true, 66 | "object-literal-sort-keys": false, 67 | "one-line": [ 68 | true, 69 | "check-open-brace", 70 | "check-catch", 71 | "check-else", 72 | "check-whitespace" 73 | ], 74 | "prefer-const": true, 75 | "quotemark": [ 76 | true, 77 | "single" 78 | ], 79 | "radix": true, 80 | "semicolon": [ 81 | true, 82 | "always" 83 | ], 84 | "triple-equals": [ 85 | true, 86 | "allow-null-check" 87 | ], 88 | "typedef-whitespace": [ 89 | true, 90 | { 91 | "call-signature": "nospace", 92 | "index-signature": "nospace", 93 | "parameter": "nospace", 94 | "property-declaration": "nospace", 95 | "variable-declaration": "nospace" 96 | } 97 | ], 98 | "unified-signatures": true, 99 | "variable-name": false, 100 | "whitespace": [ 101 | true, 102 | "check-branch", 103 | "check-decl", 104 | "check-operator", 105 | "check-separator", 106 | "check-type" 107 | ] 108 | } 109 | } 110 | --------------------------------------------------------------------------------