├── .commitlintrc.json ├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── MIGRATION.md ├── README.example.md ├── README.md ├── README.sample.md ├── assets ├── fonts │ ├── Hanuman-Bold.ttf │ ├── Hanuman-Regular.ttf │ ├── KhmerOSmuollight.ttf │ ├── Roboto-Italic.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-MediumItalic.ttf │ └── Roboto-Regular.ttf ├── images │ ├── bloodborne.jpg │ ├── dark-souls.jpg │ └── sekiro.jpg ├── locales │ ├── en │ │ └── translation.json │ └── kh │ │ └── translation.json └── templates │ └── sample.pug ├── e2e ├── auth.e2e-spec.ts └── jest-e2e.json ├── jest.config.js ├── nest-cli.json ├── package.json ├── plopfile.js ├── process.json ├── public └── robots.txt ├── src ├── api │ ├── api.module.ts │ ├── auth │ │ ├── auth.controller.spec.ts │ │ ├── auth.controller.ts │ │ ├── auth.dto.ts │ │ ├── auth.errors.ts │ │ ├── auth.guard.ts │ │ ├── auth.interfaces.ts │ │ ├── auth.module.ts │ │ ├── auth.service.spec.ts │ │ ├── auth.service.ts │ │ ├── auth0.guard.ts │ │ └── google-api.guard.ts │ ├── cron │ │ ├── cron.module.ts │ │ ├── cron.resolver.ts │ │ └── cron.service.ts │ ├── example │ │ ├── download │ │ │ └── download.controller.ts │ │ ├── example.module.ts │ │ ├── exceljs │ │ │ ├── exceljs.controller.ts │ │ │ └── exceljs.service.ts │ │ ├── i18next │ │ │ └── i18next.controller.ts │ │ ├── ioredis │ │ │ ├── ioredis.controller.ts │ │ │ └── ioredis.service.ts │ │ ├── other │ │ │ └── other.controller.ts │ │ ├── pdfmake │ │ │ ├── invoice.iqps.ts │ │ │ ├── pdfmake.controller.ts │ │ │ └── pdfmake.service.ts │ │ ├── sequelize │ │ │ └── sequelize.controller.ts │ │ ├── soap │ │ │ └── soap.controller.ts │ │ ├── tile38 │ │ │ └── tile38.controller.ts │ │ ├── typeorm │ │ │ └── typeorm.controller.ts │ │ └── upload │ │ │ └── upload.controller.ts │ ├── shared │ │ ├── firebase-admin.service.ts │ │ ├── mailer.service.ts │ │ └── shared.module.ts │ ├── soap │ │ ├── holiday-soap │ │ │ ├── holiday-soap.interfaces.ts │ │ │ ├── holiday-soap.module.ts │ │ │ └── holiday-soap.service.ts │ │ └── soap.module.ts │ └── upload │ │ ├── upload.controller.ts │ │ ├── upload.helper.ts │ │ ├── upload.module.ts │ │ └── upload.service.ts ├── app.module.ts ├── common │ ├── classes │ │ ├── dayoff-calculation.class.ts │ │ ├── excel.class.ts │ │ └── pdfmake.class.ts │ ├── constants │ │ └── index.ts │ ├── decorators │ │ ├── api-headers.decorator.ts │ │ ├── auth-user.decorator.ts │ │ ├── auth.decorator.ts │ │ └── dto.decorator.ts │ ├── exceptions │ │ └── app-exception-filter.ts │ ├── guards │ │ ├── api.guard.ts │ │ ├── authenticate.guard.ts │ │ └── authorize.guard.ts │ ├── index.ts │ ├── interceptors │ │ ├── auditing.interceptor.ts │ │ ├── csv-multer.interceptor.ts │ │ └── image-multer.interceptor.ts │ ├── swagger │ │ ├── swagger.config.ts │ │ ├── swagger.description.ts │ │ └── swagger.plugin.ts │ ├── transformers │ │ ├── number.transformer.ts │ │ ├── sanitize-html.transformer.ts │ │ └── string.transformer.ts │ ├── types │ │ ├── auth.ts │ │ ├── base.ts │ │ ├── image.ts │ │ ├── index.ts │ │ └── notification.ts │ ├── utils │ │ ├── index.ts │ │ ├── misc.ts │ │ ├── time-ago-old.ts │ │ └── time-ago.ts │ └── validators │ │ ├── is-greater-or-equal.validator.ts │ │ ├── is-not-blank.validator.ts │ │ └── is-phone-number.validator.ts ├── dynamodb │ ├── base │ │ ├── base.schema.ts │ │ └── index.ts │ ├── dynamodb.module.ts │ ├── index.ts │ └── user │ │ ├── index.ts │ │ ├── user.interface.ts │ │ ├── user.schema.ts │ │ └── user.service.ts ├── entities │ ├── index.ts │ ├── user-profile.entity.ts │ └── user.entity.ts ├── lib │ ├── auth0 │ │ ├── auth0.constant.ts │ │ ├── auth0.decorator.ts │ │ ├── auth0.dto.ts │ │ ├── auth0.module.ts │ │ ├── auth0.provider.ts │ │ ├── auth0.strategy.ts │ │ └── index.ts │ ├── aws │ │ ├── aws.constant.ts │ │ ├── aws.decorator.ts │ │ ├── aws.dto.ts │ │ ├── aws.module.ts │ │ ├── aws.provider.ts │ │ ├── aws.ts │ │ └── index.ts │ ├── config │ │ ├── config.dto.ts │ │ ├── config.module.ts │ │ ├── config.service.ts │ │ └── index.ts │ ├── crypto │ │ ├── crypto.dto.ts │ │ ├── crypto.module.ts │ │ ├── crypto.provider.ts │ │ ├── crypto.service.ts │ │ └── index.ts │ ├── firebase-admin │ │ ├── firebase-admin.constant.ts │ │ ├── firebase-admin.decorator.ts │ │ ├── firebase-admin.dto.ts │ │ ├── firebase-admin.module.ts │ │ ├── firebase-admin.provider.ts │ │ └── index.ts │ ├── google-cloud-storage │ │ ├── google-cloud-storage.constant.ts │ │ ├── google-cloud-storage.decorator.ts │ │ ├── google-cloud-storage.dto.ts │ │ ├── google-cloud-storage.module.ts │ │ ├── google-cloud-storage.provider.ts │ │ ├── google-cloud-storage.ts │ │ └── index.ts │ ├── graphql-request │ │ ├── graphql-request.constant.ts │ │ ├── graphql-request.decorator.ts │ │ ├── graphql-request.dto.ts │ │ ├── graphql-request.module.ts │ │ ├── graphql-request.provider.ts │ │ ├── graphql-request.ts │ │ └── index.ts │ ├── i18next │ │ ├── i18next.constant.ts │ │ ├── i18next.converter.js │ │ ├── i18next.decorator.ts │ │ ├── i18next.helper.ts │ │ ├── i18next.module.ts │ │ ├── i18next.provider.ts │ │ ├── i18next.ts │ │ ├── i18next.typing.ts │ │ └── index.ts │ ├── ioredis │ │ ├── index.ts │ │ ├── ioredis.constant.ts │ │ ├── ioredis.decorator.ts │ │ ├── ioredis.dto.ts │ │ ├── ioredis.module.ts │ │ └── ioredis.provider.ts │ ├── jwt │ │ ├── index.ts │ │ └── jwt.module.ts │ ├── keycloak │ │ ├── index.ts │ │ ├── keycloak.constant.ts │ │ ├── keycloak.decorator.ts │ │ ├── keycloak.dto.ts │ │ ├── keycloak.module.ts │ │ ├── keycloak.provider.ts │ │ └── keycloak.strategy.ts │ ├── mailer │ │ ├── index.ts │ │ ├── mailer.constant.ts │ │ ├── mailer.decorator.ts │ │ ├── mailer.dto.ts │ │ ├── mailer.module.ts │ │ ├── mailer.provider.ts │ │ └── mailer.ts │ ├── media-stream │ │ ├── index.ts │ │ ├── media-stream.constant.ts │ │ ├── media-stream.decorator.ts │ │ ├── media-stream.dto.ts │ │ ├── media-stream.module.ts │ │ ├── media-stream.provider.ts │ │ └── media-stream.ts │ ├── mongoose │ │ ├── index.ts │ │ ├── mongoose.constant.ts │ │ ├── mongoose.decorator.ts │ │ ├── mongoose.dto.ts │ │ ├── mongoose.module.ts │ │ ├── mongoose.provider.ts │ │ └── mongoose.util.ts │ ├── pagination │ │ ├── index.ts │ │ ├── pagination.class.ts │ │ ├── pagination.dto.ts │ │ ├── pagination.interfaces.ts │ │ └── pagination.ts │ ├── sendbird │ │ ├── _base │ │ │ └── index.ts │ │ ├── application │ │ │ ├── application.interface.ts │ │ │ ├── application.service.ts │ │ │ └── index.ts │ │ ├── channel-metadata │ │ │ ├── channel-metadata.interface.ts │ │ │ ├── channel-metadata.service.ts │ │ │ └── index.ts │ │ ├── data-export │ │ │ ├── data-export.interface.ts │ │ │ ├── data-export.service.ts │ │ │ └── index.ts │ │ ├── data-privacy │ │ │ ├── data-privacy.interface.ts │ │ │ ├── data-privacy.service.ts │ │ │ └── index.ts │ │ ├── group-channel │ │ │ ├── group-channel.interface.ts │ │ │ ├── group-channel.service.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── message │ │ │ ├── index.ts │ │ │ ├── message.interface.ts │ │ │ └── message.service.ts │ │ ├── open-channel │ │ │ ├── index.ts │ │ │ ├── open-channel.interface.ts │ │ │ └── open-channel.service.ts │ │ ├── report │ │ │ ├── index.ts │ │ │ ├── report.interface.ts │ │ │ └── report.service.ts │ │ ├── sendbird.constant.ts │ │ ├── sendbird.decorator.ts │ │ ├── sendbird.dto.ts │ │ ├── sendbird.helper.ts │ │ ├── sendbird.module.ts │ │ ├── sendbird.provider.ts │ │ ├── sendbird.ts │ │ ├── user-metadata │ │ │ ├── index.ts │ │ │ ├── user-metadata.interface.ts │ │ │ └── user-metadata.service.ts │ │ ├── user │ │ │ ├── index.ts │ │ │ ├── user.interface.ts │ │ │ └── user.service.ts │ │ └── webhook │ │ │ ├── index.ts │ │ │ ├── webhook.guard.ts │ │ │ ├── webhook.interface.ts │ │ │ └── webhook.service.ts │ ├── sequelize │ │ ├── index.ts │ │ ├── sequelize.constant.ts │ │ ├── sequelize.decorator.ts │ │ ├── sequelize.dto.ts │ │ ├── sequelize.helper.ts │ │ ├── sequelize.module.ts │ │ └── sequelize.service.ts │ ├── social │ │ ├── index.ts │ │ ├── social.constant.ts │ │ ├── social.decorator.ts │ │ ├── social.dto.ts │ │ ├── social.interface.ts │ │ ├── social.module.ts │ │ ├── social.provider.ts │ │ └── social.ts │ ├── socket │ │ ├── auth-socket.gateway.ts │ │ ├── auth-socket.guard.ts │ │ ├── index.ts │ │ ├── redis-io.adapter.ts │ │ ├── socket.filter.ts │ │ ├── socket.gateway.ts │ │ └── socket.module.ts │ ├── tile38 │ │ ├── index.ts │ │ ├── tile38.constant.ts │ │ ├── tile38.decorator.ts │ │ ├── tile38.dto.ts │ │ ├── tile38.interfaces.ts │ │ ├── tile38.module.ts │ │ ├── tile38.provider.ts │ │ └── tile38.ts │ ├── twilio │ │ ├── index.ts │ │ ├── twilio.constant.ts │ │ ├── twilio.decorator.ts │ │ ├── twilio.dto.ts │ │ ├── twilio.module.ts │ │ ├── twilio.provider.ts │ │ └── twilio.ts │ ├── typeorm │ │ ├── base.repository.ts │ │ ├── index.ts │ │ ├── typeorm.dto.ts │ │ ├── typeorm.module.ts │ │ └── typeorm.service.ts │ └── wowza │ │ ├── _base │ │ └── index.ts │ │ ├── index.ts │ │ ├── stream-target │ │ ├── index.ts │ │ ├── stream-target.interface.ts │ │ └── stream-target.service.ts │ │ ├── wowza.constant.ts │ │ ├── wowza.decorator.ts │ │ ├── wowza.dto.ts │ │ ├── wowza.helper.ts │ │ ├── wowza.module.ts │ │ ├── wowza.provider.ts │ │ └── wowza.ts ├── main.ts ├── models │ ├── index.ts │ ├── interfaces │ │ └── user.interface.ts │ └── user.model.ts ├── queries │ └── user │ │ ├── count.sql │ │ ├── find-all.sql │ │ └── find-one.sql ├── repositories │ ├── index.ts │ ├── interfaces │ │ └── user.interface.ts │ └── user.repository.ts └── schemas │ ├── audit.model.ts │ ├── index.ts │ └── user.model.ts ├── tsconfig.build.json ├── tsconfig.json ├── typings ├── node-media-server.d.ts └── pdfmake.d.ts ├── webpack.config.js └── yarn.lock /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "subject-case": [ 5 | 2, 6 | "never", 7 | ["sentence-case", "start-case", "pascal-case", "upper-case"] 8 | ], 9 | "type-enum": [ 10 | 2, 11 | "always", 12 | [ 13 | "build", 14 | "chore", 15 | "ci", 16 | "docs", 17 | "feat", 18 | "fix", 19 | "perf", 20 | "refactor", 21 | "revert", 22 | "style", 23 | "test", 24 | "sample" 25 | ] 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Application (development, production) 2 | NODE_ENV= 3 | PORT= 4 | JWT_SECRET= 5 | 6 | # Mongo Database Credential 7 | MONGO_URI= 8 | 9 | # Relational Database Credential 10 | DB_HOST= 11 | DB_PORT= 12 | DB_SCHEMA= 13 | DB_USERNAME= 14 | DB_PASSWORD= 15 | DB_CONNECTION= 16 | DB_LOGGING= 17 | DB_SYNC= 18 | 19 | # Auth0 20 | AUTH0_DOMAIN= 21 | AUTH0_AUDIENCE= 22 | AUTH0_CLIENT_ID= 23 | AUTH0_CLIENT_SECRET= 24 | 25 | # AWS Credential 26 | AWS_ACCESS_KEY_ID= 27 | AWS_SECRET_ACCESS_KEY= 28 | AWS_REGION= 29 | AWS_S3_BUCKET= 30 | AWS_S3_PREFIX= 31 | AWS_DYNAMODB_PREFIX= 32 | 33 | # Crypto 34 | CRYPTO_ENCRYPTION_KEY= 35 | 36 | # Firebase Admin Credential 37 | FIREBASE_DATABASE_URL= 38 | FIREBASE_CREDENTIAL_PATH= 39 | 40 | # FTP Credential 41 | FTP_HOST= 42 | FTP_USER= 43 | FTP_PASSWORD= 44 | FTP_DESTINATION= 45 | FTP_URL= 46 | 47 | # Google Cloud Storage Credential 48 | GOOGLE_CLOUD_STORAGE_KEY_FILENAME_PATH= 49 | GOOGLE_CLOUD_STORAGE_BUCKET_NAME= 50 | 51 | # Graphql Request 52 | GRAPHQL_REQUEST_ENDPOINT= 53 | GRAPHQL_REQUEST_AUTHENTICATION= 54 | 55 | # Mailer Credential (ethereal, gmail or mandrill) 56 | MAILER_TYPE= 57 | MAILER_ETHEREAL_USERNAME= 58 | MAILER_ETHEREAL_PASSWORD= 59 | MAILER_GMAIL_USERNAME= 60 | MAILER_GMAIL_PASSWORD= 61 | MAILER_MANDRILL_API_KEY= 62 | 63 | # Media Stream 64 | MEDIA_STREAM_HTTP_PORT= 65 | MEDIA_STREAM_AUTH_SECRET= 66 | 67 | # Redis Credential 68 | REDIS_HOST= 69 | REDIS_PORT= 70 | REDIS_AUTH_PASS= 71 | 72 | # Sendbird Credential 73 | SENDBIRD_APP_ID= 74 | SENDBIRD_API_TOKEN= 75 | SENDBIRD_AUTHORIZATION= 76 | 77 | # Social (facebook,twitter,google,linkedin) 78 | SOCIAL_TYPE= 79 | TWITTER_CONSUMER_KEY= 80 | TWITTER_CONSUMER_SECRET= 81 | GOOGLE_CLIENT_ID= 82 | 83 | # Twilio 84 | TWILIO_ACCOUNT_SID= 85 | TWILIO_AUTH_TOKEN= 86 | TWILIO_SMS_FROM= 87 | 88 | # Tile38 89 | TILE38_HOST= 90 | TILE38_PORT= 91 | TILE38_AUTH_PASS= 92 | 93 | # Wowza 94 | WOWZA_API_KEY= 95 | WOWZA_ACCESS_KEY= 96 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://code.visualstudio.com/api/advanced-topics/tslint-eslint-migration 2 | // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/ROADMAP.md 3 | // https://www.robertcooper.me/using-eslint-and-prettier-in-a-typescript-project 4 | // https://stackoverflow.com/questions/60698487/catch-22-with-parseroptions-and-exclude 5 | 6 | module.exports = { 7 | root: true, 8 | env: { node: true }, 9 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 10 | parserOptions: { 11 | extraFileExtensions: ['.json', '.pug'], 12 | project: 'tsconfig.json', 13 | sourceType: 'module', // Allows for the use of imports 14 | ecmaVersion: 2018 // Allows for the parsing of modern ECMAScript features 15 | }, 16 | plugins: ['@typescript-eslint/eslint-plugin'], 17 | extends: [ 18 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 19 | 'plugin:import/errors', 20 | 'plugin:import/typescript', 21 | 'prettier', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 22 | 'plugin:prettier/recommended' // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 23 | ], 24 | rules: { 25 | '@typescript-eslint/interface-name-prefix': 'off', 26 | '@typescript-eslint/explicit-function-return-type': 'off', 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-unused-vars': 'off', 29 | '@typescript-eslint/no-use-before-define': 'off', 30 | '@typescript-eslint/no-non-null-assertion': 'off', 31 | '@typescript-eslint/explicit-module-boundary-types': 'off', 32 | 'no-console': 'off', 33 | 'prettier/prettier': ['error', { endOfLine: 'auto' }], 34 | 'import/no-unresolved': 'off', 35 | 'import/order': [ 36 | 'error', 37 | { 38 | 'newlines-between': 'always', 39 | groups: [['builtin', 'external'], 'internal', ['parent', 'sibling', 'index']], 40 | alphabetize: { 41 | order: 'asc', 42 | caseInsensitive: true 43 | } 44 | } 45 | ], 46 | 47 | }, 48 | settings: { 49 | 'import/internal-regex': '^@api|@common|@dynamodb|@entities|@lib|@models|@queries|@repositories|@schemas|@x/' 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build files 2 | dist 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # environment variables 8 | .env 9 | 10 | # IDE 11 | .awcache 12 | .idea 13 | 14 | # logs 15 | logs 16 | 17 | # misc 18 | .DS_Store 19 | npm-debug.log 20 | 21 | # tests 22 | .nyc_output 23 | coverage 24 | test 25 | 26 | # public 27 | public/* 28 | !public/robots.txt 29 | !public/uploads/.gitkeep 30 | 31 | config/* 32 | documentation/* 33 | website/build/* 34 | @project 35 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": true, 3 | "printWidth": 100, 4 | "semi": true, 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "arrowParens": "avoid", 8 | "trailingComma": "none", 9 | "useTabs": false 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "CoenraadS.bracket-pair-colorizer-2", 4 | "EditorConfig.EditorConfig", 5 | "Tyriar.sort-lines", 6 | "aaron-bond.better-comments", 7 | "ionutvmi.path-autocomplete", 8 | "abierbaum.vscode-file-peek", 9 | "dbaeumer.vscode-eslint", 10 | "eamodio.gitlens", 11 | "esbenp.prettier-vscode", 12 | "formulahendry.code-runner", 13 | "kumar-harsh.graphql-for-vscode", 14 | "mikestead.dotenv", 15 | "rbbit.typescript-hero", 16 | "streetsidesoftware.code-spell-checker", 17 | "vscode-icons-team.vscode-icons", 18 | "xabikos.JavaScriptSnippets" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.formatOnSave": false, 4 | "typescriptHero.imports.organizeOnSave": true, 5 | "typescriptHero.imports.grouping": [ 6 | "Plains", 7 | "Modules", 8 | "/^@api|@common|@dynamodb|@entities|@lib|@models|@queries|@repositories|@schemas/", 9 | "Workspace" 10 | ], 11 | "files.exclude": { 12 | "**/.git": true, 13 | "**/.svn": true, 14 | "**/.hg": true, 15 | "**/CVS": true, 16 | "**/.DS_Store": true, 17 | ".nyc_output": true, 18 | ".vscode": false, 19 | "coverage": true, 20 | "dist": true, 21 | "node_modules": true 22 | }, 23 | "search.exclude": { 24 | "**/node_modules": true, 25 | "**/bower_components": true, 26 | "**/dist": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /assets/fonts/Hanuman-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/Hanuman-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/Hanuman-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/Hanuman-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/KhmerOSmuollight.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/KhmerOSmuollight.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /assets/images/bloodborne.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/images/bloodborne.jpg -------------------------------------------------------------------------------- /assets/images/dark-souls.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/images/dark-souls.jpg -------------------------------------------------------------------------------- /assets/images/sekiro.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dominic-Preap/nest-core/91dbd62a8b59ba0fba5784b08d49d058f1961d09/assets/images/sekiro.jpg -------------------------------------------------------------------------------- /assets/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello": "Hello, Welcome to Cambodia", 3 | "MyName": "Hello, My Name is {{name}}" 4 | } 5 | -------------------------------------------------------------------------------- /assets/locales/kh/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "Hello": "សួស្តី, ស្វាគមន៍មកកាន់ប្រទេសកម្ពុជា", 3 | "MyName": "សួស្តី, ខ្ញុំមានឈ្មោះថា {{name}}" 4 | } 5 | -------------------------------------------------------------------------------- /assets/templates/sample.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title 5 | meta(name='viewport', content='width=device-width, initial-scale=1') 6 | style(type="text/css"). 7 | body { 8 | margin: 0; 9 | } 10 | body 11 | div Customize your own mailer 12 | 13 | 14 | -------------------------------------------------------------------------------- /e2e/auth.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { Test } from '@nestjs/testing'; 3 | import * as request from 'supertest'; 4 | 5 | import { AuthModule } from '../src/api/auth/auth.module'; 6 | import { ConfigModule } from '../src/lib/config'; 7 | import { MongooseModule } from '../src/lib/mongoose'; 8 | 9 | describe('/POST auth/login', () => { 10 | let app: INestApplication; 11 | const authService = { login: () => ['test'] }; 12 | 13 | beforeAll(async () => { 14 | const module = await Test.createTestingModule({ 15 | imports: [AuthModule, ConfigModule, MongooseModule] 16 | }) 17 | // .overrideProvider(AuthService) 18 | // .useValue(authService) 19 | 20 | .compile(); 21 | 22 | app = module.createNestApplication(); 23 | await app.init(); 24 | }); 25 | 26 | it(`login successful`, async () => { 27 | const res = await request(app.getHttpServer()) 28 | .post('/auth/login') 29 | .send({ username: 'richard.houn', password: '11111' }); 30 | 31 | expect(res.status).toBe(201); 32 | expect(res.body).toMatchObject({ 33 | user: { 34 | firstName: 'richard212222', 35 | lastName: 'houn122222', 36 | username: 'richard.houn' 37 | } 38 | }); 39 | }); 40 | 41 | it(`login with no account`, async () => { 42 | const res = await request(app.getHttpServer()).post('/auth/login'); 43 | 44 | expect(res.status).toBe(400); 45 | expect(res.body).toMatchObject({ message: 'Account information provided does not exist.' }); 46 | }); 47 | 48 | it(`login with incorrect password`, async () => { 49 | const res = await request(app.getHttpServer()) 50 | .post('/auth/login') 51 | .send({ username: 'richard.houn', password: 'xxx' }); 52 | 53 | expect(res.status).toBe(400); 54 | expect(res.body).toMatchObject({ message: 'Password is incorrect.' }); 55 | }); 56 | 57 | afterAll(async () => { 58 | await app.close(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /e2e/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js", 6 | "json" 7 | ], 8 | "transform": { 9 | "^.+\\.tsx?$": "/../node_modules/ts-jest/preprocessor.js" 10 | }, 11 | "testRegex": "/e2e/.*\\.(e2e-test|e2e-spec).(ts|tsx|js)$", 12 | "collectCoverageFrom" : ["src/**/*.{js,jsx,tsx,ts}", "!**/node_modules/**", "!**/vendor/**"], 13 | "coverageReporters": ["json", "lcov"] 14 | } 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { pathsToModuleNameMapper } = require('ts-jest/utils'); 2 | const { compilerOptions } = require('./tsconfig'); 3 | 4 | module.exports = { 5 | moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { prefix: '/' }), 6 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 7 | collectCoverageFrom: ['src/**/*.{js,jsx,tsx,ts}', '!**/node_modules/**', '!**/vendor/**'], 8 | coverageReporters: ['json', 'lcov'], 9 | testRegex: '/src/.*\\.(test|spec).(ts|tsx|js)$', 10 | transform: { 11 | '^.+\\.tsx?$': 'ts-jest' 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src", 4 | "compilerOptions": { 5 | "assets": ["**/*.sql"], 6 | "deleteOutDir": true, 7 | "watchAssets": true, 8 | "webpack": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /process.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "backend-core-project", 5 | "script": "./dist/main.js", 6 | "exec_mode": "fork", 7 | "instances": 1, 8 | "env": { 9 | "NODE_ENV": "development" 10 | }, 11 | "env_production": { 12 | "NODE_ENV": "production" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-Agent: * 2 | -------------------------------------------------------------------------------- /src/api/api.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AuthModule } from './auth/auth.module'; 4 | import { CronModule } from './cron/cron.module'; 5 | import { SharedModule } from './shared/shared.module'; 6 | 7 | // import { UploadModule } from './upload/upload.module'; 8 | // import { ExampleModule } from './example/example.module'; 9 | // import { SoapModule } from './soap/soap.module'; 10 | 11 | @Module({ 12 | imports: [ 13 | // -- 14 | AuthModule, 15 | CronModule, 16 | // ExampleModule, 17 | SharedModule 18 | // SoapModule 19 | // UploadModule 20 | ] 21 | }) 22 | export class ApiModule {} 23 | -------------------------------------------------------------------------------- /src/api/auth/auth.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test } from '@nestjs/testing'; 2 | 3 | import { ConfigModule } from '@lib/config'; 4 | import { JwtModule } from '@lib/jwt'; 5 | 6 | import { AuthController } from './auth.controller'; 7 | import { AuthService } from './auth.service'; 8 | 9 | describe('AuthController', () => { 10 | let authController: AuthController; 11 | 12 | beforeEach(async () => { 13 | const module = await Test.createTestingModule({ 14 | imports: [ConfigModule, JwtModule], 15 | controllers: [AuthController], 16 | providers: [ 17 | { 18 | provide: AuthService, 19 | useValue: { 20 | login: jest.fn(() => ({ accessToken: 'ACCESS_TOKEN' })), 21 | authorize: jest.fn(() => ({ token: 'TOKEN' })) 22 | } 23 | } 24 | ] 25 | }).compile(); 26 | 27 | authController = module.get(AuthController); 28 | }); 29 | 30 | describe('login', () => { 31 | it('should return access token', async () => { 32 | const body = { username: 'john', password: 'doe' }; 33 | const result = { accessToken: 'ACCESS_TOKEN' }; 34 | expect(await authController.login(body)).toMatchObject(result); 35 | }); 36 | }); 37 | 38 | describe('authorize', () => { 39 | it('should return token', async () => { 40 | const body = { clientId: 'x' }; 41 | const header = { udid: 'x' }; 42 | const result = { token: 'TOKEN' }; 43 | expect(await authController.authorize(body, header as any)).toMatchObject(result); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/api/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Body, Controller, Post } from '@nestjs/common'; 2 | import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | 4 | import { ApiCustomHeader, ApiCustomHeaders, Authenticate, Authorize } from '@common'; 5 | 6 | import { AuthorizeBody, LoginBody } from './auth.dto'; 7 | import { AuthService } from './auth.service'; 8 | 9 | @ApiTags('Auth') 10 | @Controller('auth') 11 | export class AuthController { 12 | constructor(private readonly service: AuthService) {} 13 | 14 | @Authorize() 15 | @Post('authorize') 16 | @ApiOperation({ summary: 'Authorize' }) 17 | @ApiHeader({ 18 | name: 'Authorization', 19 | description: 'Bearer token authorize using sha1 to encryption(clientId:clientSecret)' 20 | }) 21 | authorize(@Body() { clientId }: AuthorizeBody, @ApiCustomHeaders() header: ApiCustomHeader) { 22 | return this.service.authorize(clientId, header.udid); 23 | } 24 | 25 | @Authenticate() 26 | @ApiHeader({ name: 'token' }) 27 | @Post('login') 28 | @ApiOperation({ summary: 'Login' }) 29 | login(@Body() body: LoginBody) { 30 | return this.service.login(body); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/api/auth/auth.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmptyString } from '@common'; 2 | 3 | export class AuthorizeBody { 4 | @IsNotEmptyString() 5 | readonly clientId!: string; 6 | } 7 | 8 | export class LoginBody { 9 | @IsNotEmptyString() 10 | readonly username!: string; 11 | 12 | @IsNotEmptyString() 13 | readonly password!: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/api/auth/auth.errors.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | 3 | export class InvalidAccountError extends BadRequestException { 4 | constructor() { 5 | super('Invalid username or password.'); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/api/auth/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | ForbiddenException, 5 | Injectable, 6 | UnauthorizedException 7 | } from '@nestjs/common'; 8 | import { Reflector } from '@nestjs/core'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { Request } from 'express'; 11 | 12 | import { JWTPayload } from './auth.interfaces'; 13 | 14 | @Injectable() 15 | export class AuthGuard implements CanActivate { 16 | constructor(private readonly service: JwtService, private readonly reflector: Reflector) {} 17 | 18 | async canActivate(context: ExecutionContext) { 19 | const handler = context.getHandler(); 20 | const roles = this.reflector.get('roles', handler); 21 | 22 | // console.log('role', roles); 23 | if (roles === undefined) return true; 24 | 25 | const req = context.switchToHttp().getRequest(); 26 | const authToken = req.get('authorization') || ''; 27 | const [scheme, token] = authToken.split(' '); 28 | 29 | // console.log(`Scheme: ${scheme}`); 30 | // console.log(`Token: ${token}`); 31 | 32 | const user = await this.checkUserScheme(scheme, token, roles); 33 | (req as any).authUser = user; 34 | 35 | return true; 36 | } 37 | 38 | async checkUserScheme(scheme: string, token: string, roles: string[]) { 39 | if (scheme.toLowerCase() !== 'bearer') 40 | throw new UnauthorizedException('Invalid Authorization Scheme'); 41 | if (!token) throw new UnauthorizedException('Authorization token is missing.'); 42 | 43 | let decoded: JWTPayload; 44 | try { 45 | decoded = await this.service.verifyAsync(token); 46 | console.log('jwtDecoded', decoded); 47 | } catch (e: any) { 48 | throw new UnauthorizedException(e.name + ' ' + e.message); 49 | } 50 | 51 | // TODO: use your own user table 52 | // const user = await User.findById(jwtDecoded.id); 53 | const user = { 54 | id: 1, 55 | username: 'my-username', 56 | password: 'my-password', 57 | isArchived: false, 58 | role: 'admin' 59 | }; 60 | if (!user || user.isArchived) throw new ForbiddenException('Unknown User'); 61 | if (roles.length > 0 && !roles.find(x => x === user.role)) 62 | throw new ForbiddenException('Invalid User Role'); 63 | 64 | return user; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/api/auth/auth.interfaces.ts: -------------------------------------------------------------------------------- 1 | export interface JWTPayload { 2 | id: number; 3 | username: string; 4 | loginDate: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/api/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { AuthController } from './auth.controller'; 4 | import { AuthGuard } from './auth.guard'; 5 | import { AuthService } from './auth.service'; 6 | import { Auth0Guard } from './auth0.guard'; 7 | 8 | @Module({ 9 | controllers: [AuthController], 10 | providers: [AuthService, AuthGuard, Auth0Guard] 11 | }) 12 | export class AuthModule {} 13 | -------------------------------------------------------------------------------- /src/api/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { JwtService } from '@nestjs/jwt'; 2 | import { Test } from '@nestjs/testing'; 3 | 4 | import { ConfigModule } from '@lib/config'; 5 | import { JwtModule } from '@lib/jwt'; 6 | 7 | import { InvalidAccountError } from './auth.errors'; 8 | import { AuthService } from './auth.service'; 9 | 10 | describe('AuthService', () => { 11 | let authService: AuthService; 12 | 13 | beforeEach(async () => { 14 | const module = await Test.createTestingModule({ 15 | imports: [ConfigModule, JwtModule], 16 | providers: [ 17 | AuthService, 18 | { 19 | provide: JwtService, 20 | useValue: { signAsync: jest.fn(() => 'ACCESS_TOKEN') } 21 | } 22 | ] 23 | }).compile(); 24 | 25 | authService = module.get(AuthService); 26 | }); 27 | 28 | describe('login', () => { 29 | it('should return access token', async () => { 30 | const result = await authService.login({ username: 'my-username', password: 'my-password' }); 31 | expect(result).toMatchObject({ accessToken: 'ACCESS_TOKEN' }); 32 | }); 33 | 34 | it('should throw error when account is incorrect', async () => { 35 | try { 36 | await authService.login({ username: 'fake', password: 'wrong' }); 37 | } catch (e: any) { 38 | expect(e).toBeInstanceOf(InvalidAccountError); 39 | expect(e.message).toMatch('Invalid username or password.'); 40 | } 41 | }); 42 | }); 43 | 44 | describe('authorize', () => { 45 | it('should return authorize token', async () => { 46 | const result = await authService.authorize('clientId', 'uuid'); 47 | expect(result).toMatchObject({ token: 'ACCESS_TOKEN' }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/api/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { compare } from 'bcryptjs'; 4 | 5 | import { LoginBody } from './auth.dto'; 6 | import { InvalidAccountError } from './auth.errors'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor(private readonly service: JwtService) {} 11 | 12 | async authorize(clientId: string, udid: string) { 13 | const data = { clientId, udid, timestamp: Date.now() }; 14 | const token = await this.service.signAsync(data, { expiresIn: 60 }); 15 | return { token }; 16 | } 17 | 18 | async login({ username, password }: LoginBody) { 19 | // TODO: use your own user table 20 | // const user = await User.findOne({ username }); 21 | const user = { 22 | id: 1, 23 | username: 'my-username', 24 | password: '$2y$12$yY/PpVYPizAclFCrNI112esVbtr40vkBtCoTSQowHvev/al.rKlW.', // 'my-password', 25 | isArchived: false 26 | }; 27 | 28 | // Check if user exist and active, then compare the password 29 | if (!user || user.isArchived) throw new InvalidAccountError(); 30 | if (!(await compare(password, user.password))) throw new InvalidAccountError(); 31 | 32 | // Generate auth user token 33 | const data = { id: user.id, username, loginDate: new Date().toISOString() }; 34 | const accessToken = await this.service.signAsync(data, { noTimestamp: true }); 35 | return { user, accessToken }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/api/auth/auth0.guard.ts: -------------------------------------------------------------------------------- 1 | import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { Reflector } from '@nestjs/core'; 3 | import { AuthGuard as PassportAuthGuard } from '@nestjs/passport'; 4 | 5 | import { AUTH0_STRATEGY_NAME } from '@lib/auth0'; 6 | 7 | @Injectable() 8 | export class Auth0Guard extends PassportAuthGuard(AUTH0_STRATEGY_NAME) { 9 | constructor(private readonly reflector: Reflector) { 10 | super(); 11 | } 12 | 13 | async canActivate(context: ExecutionContext) { 14 | const handler = context.getHandler(); 15 | const roles = this.reflector.get('roles', handler); 16 | const req = context.switchToHttp().getRequest(); 17 | console.log('role', roles); 18 | 19 | if (roles === undefined) return true; 20 | 21 | await super.canActivate(context); 22 | console.log('req', (req as any).user); 23 | 24 | // NOTE: write custom service to check and validate user by auth0Id 25 | // const user = await this.service.validateUser(req.user.sub); 26 | // (req as any).authUser = user; 27 | 28 | return true; 29 | } 30 | 31 | /** 32 | * Checking request and handle custom 401 exception message 33 | * 34 | * @see https://stackoverflow.com/questions/55820591/nestjs-jwt-authentication-returns-401 35 | */ 36 | handleRequest(err: Error, user: any, info: Error) { 37 | if (err || !user || info) { 38 | const msg = err 39 | ? err.message 40 | : info 41 | ? info.message 42 | : 'Sorry, we were unable to process your request.'; 43 | throw new UnauthorizedException(msg); 44 | } 45 | return user; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/api/auth/google-api.guard.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { 3 | CanActivate, 4 | ExecutionContext, 5 | ForbiddenException, 6 | Injectable, 7 | UnauthorizedException 8 | } from '@nestjs/common'; 9 | import { Request } from 'express'; 10 | import { lastValueFrom } from 'rxjs'; 11 | 12 | @Injectable() 13 | export class GoogleAPIGuard implements CanActivate { 14 | private URL = 'https://oauth2.googleapis.com/tokeninfo'; 15 | private EMAIL = 'cloud-scheduler@testing.iam.gserviceaccount.com'; 16 | 17 | constructor(private readonly http: HttpService) {} 18 | 19 | async canActivate(context: ExecutionContext) { 20 | const req = context.switchToHttp().getRequest(); 21 | const authToken = req.get('authorization') || ''; 22 | const [scheme, token] = authToken.split(' '); 23 | // console.log('scheme', scheme); 24 | // console.log('token', token); 25 | if (scheme.toLowerCase() !== 'bearer') 26 | throw new UnauthorizedException('Invalid Authorization Scheme'); 27 | if (!token) throw new UnauthorizedException('Authorization token is missing.'); 28 | 29 | const data = await this.decode(token); 30 | if (data.email !== this.EMAIL) { 31 | throw new ForbiddenException('Invalid credential'); 32 | } 33 | return true; 34 | } 35 | 36 | async decode(token: string) { 37 | try { 38 | const { data } = await lastValueFrom( 39 | this.http.get(this.URL, { params: { id_token: token } }) 40 | ); 41 | 42 | return data; 43 | } catch (error) { 44 | throw new UnauthorizedException('Cannot validate token from google'); 45 | } 46 | } 47 | } 48 | 49 | interface GoogleTokenInfo { 50 | aud: string; 51 | azp: string; 52 | email: string; 53 | email_verified: boolean; 54 | exp: string; 55 | iat: string; 56 | iss: string; 57 | sub: string; 58 | alg: string; 59 | kid: string; 60 | typ: string; 61 | } 62 | -------------------------------------------------------------------------------- /src/api/cron/cron.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { CronResolver } from './cron.resolver'; 4 | import { CronService } from './cron.service'; 5 | 6 | @Module({ 7 | providers: [CronService, CronResolver] 8 | }) 9 | export class CronModule {} 10 | -------------------------------------------------------------------------------- /src/api/cron/cron.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { readdir, stat, unlink } from 'fs'; 3 | import * as moment from 'moment'; 4 | import { resolve } from 'path'; 5 | 6 | @Injectable() 7 | export class CronResolver { 8 | /** 9 | * NOTE: For Cron Job Only !!! 10 | * Delete any files older than (x) days 11 | */ 12 | removeFiles(path: string, days = 1) { 13 | readdir(path, (err, files) => { 14 | files.forEach(file => { 15 | const filePath = resolve(path, file); 16 | stat(filePath, (err, stat) => { 17 | if (err) return; 18 | const d = moment().diff(moment(stat.mtime), 'days'); 19 | if (d > days) unlink(filePath, () => console.log(`${file} delete`)); 20 | }); 21 | }); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/cron/cron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Cron, CronExpression, SchedulerRegistry, Timeout } from '@nestjs/schedule'; 3 | 4 | // https://crontab.guru/ 5 | // https://cronjob.xyz/ 6 | // http://bradymholt.github.io/cRonstrue/#cronstrue-demo 7 | // https://cronexpressiondescriptor.azurewebsites.net/? 8 | 9 | @Injectable() 10 | export class CronService { 11 | constructor(private readonly schedulerRegistry: SchedulerRegistry) {} 12 | 13 | @Timeout(5000) 14 | handleTimeout() { 15 | // ! Used when you want to start cron jobs or not 16 | this.schedulerRegistry.getCronJobs().forEach(job => job.stop()); 17 | } 18 | 19 | @Cron(CronExpression.EVERY_5_MINUTES) 20 | handleCron() { 21 | console.log('loading every 5 minute.'); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/api/example/download/download.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Res } from '@nestjs/common'; 2 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import * as archiver from 'archiver'; 4 | import { Response } from 'express'; 5 | import * as fs from 'fs'; 6 | import * as path from 'path'; 7 | 8 | @ApiBearerAuth() 9 | @ApiTags('Example - Download') 10 | @Controller('example/download') 11 | export class DownloadController { 12 | @Post() 13 | @ApiOperation({ summary: 'Download Image' }) 14 | async download(@Res() res: Response) { 15 | // CREATE ZIP AND ADD PHOTOS INTO 16 | // ============================== 17 | const images = ['dark-souls.jpg', 'bloodborne.jpg', 'sekiro.jpg']; 18 | const zip = archiver('zip'); 19 | for (const img of images) { 20 | const photoPath = path.resolve('.', 'assets', 'images', img); 21 | if (fs.existsSync(photoPath)) zip.file(photoPath, { name: img }); 22 | } 23 | 24 | // CREATE FILE STREAM THAN PIPE AND CALL FINALIZE 25 | // ============================================== 26 | res.attachment('download.zip'); // set the archive name 27 | zip.on('error', err => res.status(500).send({ error: err.message })); 28 | zip.pipe(res); // this is the streaming magic 29 | zip.finalize(); // finalize the archive 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/api/example/example.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { DownloadController } from './download/download.controller'; 4 | import { ExcelJSController } from './exceljs/exceljs.controller'; 5 | import { ExcelJSService } from './exceljs/exceljs.service'; 6 | import { I18NextController } from './i18next/i18next.controller'; 7 | import { IORedisController } from './ioredis/ioredis.controller'; 8 | import { IORedisService } from './ioredis/ioredis.service'; 9 | import { PDFMakeController } from './pdfmake/pdfmake.controller'; 10 | import { PdfMakeService } from './pdfmake/pdfmake.service'; 11 | import { SequelizeController } from './sequelize/sequelize.controller'; 12 | import { SoapController } from './soap/soap.controller'; 13 | import { Tile38Controller } from './tile38/tile38.controller'; 14 | import { TypeORMController } from './typeorm/typeorm.controller'; 15 | import { UploadController } from './upload/upload.controller'; 16 | 17 | @Module({ 18 | controllers: [ 19 | // ----------------- 20 | DownloadController, 21 | ExcelJSController, 22 | I18NextController, 23 | IORedisController, 24 | PDFMakeController, 25 | SequelizeController, 26 | SoapController, 27 | Tile38Controller, 28 | TypeORMController, 29 | UploadController 30 | ], 31 | providers: [ 32 | // ----------------- 33 | ExcelJSService, 34 | IORedisService, 35 | PdfMakeService 36 | ] 37 | }) 38 | export class ExampleModule {} 39 | -------------------------------------------------------------------------------- /src/api/example/exceljs/exceljs.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Res } from '@nestjs/common'; 2 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import { Response } from 'express'; 4 | 5 | import { ExcelJSService } from './exceljs.service'; 6 | 7 | @ApiBearerAuth() 8 | @ApiTags('Example - ExcelJS') 9 | @Controller('example/ExcelJS') 10 | export class ExcelJSController { 11 | constructor(private readonly service: ExcelJSService) {} 12 | 13 | @Get() 14 | @ApiOperation({ summary: 'Export as Excel File' }) 15 | exportExcel(@Res() res: Response) { 16 | return this.service.start(res); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/api/example/exceljs/exceljs.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | 4 | import { ExcelDocument } from '@common'; 5 | 6 | @Injectable() 7 | export class ExcelJSService { 8 | async start(res: Response) { 9 | // ==================================================== 10 | // Create a new instance of a ExcelDocument class 11 | // ==================================================== 12 | const excel = new ExcelDocument({ 13 | sheetName: 'Report', 14 | columns: [ 15 | // --- 16 | { header: 'Id' }, 17 | { header: 'Name', width: 30 }, 18 | { header: 'Created Date', width: 20, style: { numFmt: 'dd-mmm-yyyy hh:mm AM/PM' } } 19 | ], 20 | rows: data1().map(x => [x.id, x.name, x.createdAt]) 21 | }); 22 | 23 | // ==================================================== 24 | // Create Second Sheet for the Excel File 25 | // ==================================================== 26 | excel.createSheet({ 27 | sheetName: 'Products', 28 | columns: [{ header: 'Month' }, { header: 'Total QTY', style: { numFmt: '0.00%' } }], 29 | rows: data2().map(x => [x.week, x.total]) 30 | }); 31 | 32 | excel.write('Report.xlsx', res); 33 | } 34 | } 35 | 36 | const data1 = () => 37 | new Array(100).fill(0).map((x, i) => ({ id: ++i, name: 'Testing', createdAt: new Date() })); 38 | const data2 = () => 39 | new Array(60000).fill(0).map((x, i) => ({ week: `Week ${++i}`, total: ++i * 2 })); 40 | -------------------------------------------------------------------------------- /src/api/example/i18next/i18next.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Query } from '@nestjs/common'; 2 | import { ApiHeader, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'; 3 | 4 | import { I18Next, i18next, I18NextTranslate } from '@lib/i18next'; 5 | 6 | @ApiTags('Example - I18Next') 7 | @Controller('example/i18next') 8 | export class I18NextController { 9 | @Get('hello') 10 | @ApiHeader({ name: 'Accept-Language', required: false }) 11 | @ApiQuery({ name: 'lng', required: false }) 12 | @ApiOperation({ summary: 'Localization' }) 13 | async translate(@I18Next() i18n: i18next) { 14 | return { message: i18n.t('Hello') }; 15 | } 16 | 17 | @Get('my-name') 18 | @ApiHeader({ name: 'x-language', required: false }) 19 | @ApiQuery({ name: 'lng', required: false }) 20 | @ApiOperation({ summary: 'Localization' }) 21 | async replace(@I18Next() i18n: i18next, @Query('name') name: string) { 22 | return { message: i18n.t('MyName', { replace: { name } }) }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/example/ioredis/ioredis.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Optional } from '@nestjs/common'; 2 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import { Redis } from 'ioredis'; 4 | 5 | import { InjectIORedis } from '@lib/ioredis'; 6 | 7 | @ApiBearerAuth() 8 | @ApiTags('Example - IORedis') 9 | @Controller('example/ioredis') 10 | export class IORedisController { 11 | constructor(@Optional() @InjectIORedis() private readonly redis: Redis) {} 12 | 13 | @Get() 14 | @ApiOperation({ summary: 'Set Redis Expiry Example' }) 15 | async setRedisExpiry() { 16 | const types = ['ex:reminder', 'ex:notification']; 17 | const type = types[Math.floor(Math.random() * types.length)]; 18 | const key = Math.random(); 19 | this.redis 20 | .multi() // Chain multiple redis function 21 | .set(`tmp:reminder:${key}`, JSON.stringify({ name: 'My Name', age: 18 })) // Must be string | tmp:reminder:id 22 | .setex(`${type}:${key}`, 10, null as any) // 10 seconds 23 | .exec(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/api/example/ioredis/ioredis.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as debug from 'debug'; 3 | import { Redis } from 'ioredis'; 4 | 5 | import { InjectIORedis, InjectIORedisPubSub } from '@lib/ioredis'; 6 | 7 | @Injectable() 8 | export class IORedisService { 9 | private log = debug('api:ioredis'); 10 | 11 | constructor( 12 | @InjectIORedisPubSub() private readonly sub: Redis, 13 | @InjectIORedis() private readonly redis: Redis 14 | ) { 15 | this.sub.removeAllListeners('message'); 16 | this.sub.on('message', async (channel: any, message: string) => { 17 | const [, type, key] = message.split(':'); // * Naming Convention : ex:TYPE:KEY 18 | this.log('TYPE', type); 19 | this.log('KEY', key); 20 | 21 | switch (type) { 22 | case 'reminder': 23 | this.userReminder(key); 24 | break; 25 | 26 | case 'notification': 27 | this.publishNotification(key); 28 | break; 29 | 30 | default: 31 | break; 32 | } 33 | }); 34 | } 35 | 36 | async userReminder(value: string) { 37 | // * For more, check at setRedisExpiry() in ioredis.controller.ts 38 | const key = `tmp:reminder:${value}`; 39 | await this.redis.del(key); 40 | this.log('USER REMINDER ALERT'); 41 | } 42 | 43 | async publishNotification(value: string) { 44 | // * For more, check at setRedisExpiry() in ioredis.controller.ts 45 | const key = `tmp:reminder:${value}`; 46 | const data = await this.redis.get(key).then(x => JSON.parse(x || '')); 47 | await this.redis.del(key); 48 | this.log('PUSH NOTIFICATION TO ALL USERS', data); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/api/example/other/other.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post, UseInterceptors } from '@nestjs/common'; 2 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import * as moment from 'moment'; 4 | 5 | import { AuditingInterceptor, DayOffCalculation } from '@common'; 6 | // import { DynamoDBUserService } from '@dynamodb'; 7 | 8 | @ApiBearerAuth() 9 | @ApiTags('Example - Others') 10 | @Controller('example/other') 11 | export class OtherController { 12 | // constructor(private readonly user: DynamoDBUserService) {} 13 | @Get('testing') 14 | @ApiOperation({}) 15 | async testingDynamoDb() { 16 | // await this.user.create({ 17 | // firstName: 'andy', 18 | // lastName: 'kheng', 19 | // email: 'andy.kheng@pathmazing.com', 20 | // status: 'active', 21 | // address: [{ lat: '0.44', lng: '0.55' }], 22 | // profile: { nickName: 'EGXXX', logo: 'zzz.jpg' }, 23 | // tags: ['good', 'boy'] 24 | // }); 25 | // return this.user.findAll({ status: 'active' }); 26 | } 27 | 28 | @Post() 29 | @UseInterceptors(AuditingInterceptor) 30 | @ApiOperation({ summary: 'Capture Request Log into DB' }) 31 | async captureRequestLog() { 32 | return 'will capture this request log'; 33 | } 34 | 35 | @Get() 36 | @ApiOperation({ summary: 'Calculate day off' }) 37 | async calculateDayOff() { 38 | const dayOff = new DayOffCalculation(); 39 | 40 | dayOff.setSchedule({ 41 | mon: [ 42 | { breaks: [{ start: '12:00', end: '13:00' }], works: [{ start: '8:00', end: '17:00' }] } 43 | ], 44 | tue: [ 45 | { breaks: [{ start: '12:00', end: '13:00' }], works: [{ start: '8:00', end: '17:00' }] } 46 | ], 47 | wed: [ 48 | { breaks: [{ start: '12:00', end: '13:00' }], works: [{ start: '8:00', end: '17:00' }] } 49 | ], 50 | thu: [ 51 | { breaks: [{ start: '12:00', end: '13:00' }], works: [{ start: '8:00', end: '17:00' }] } 52 | ], 53 | fri: [ 54 | { breaks: [{ start: '12:00', end: '13:00' }], works: [{ start: '8:00', end: '17:00' }] } 55 | ], 56 | sat: [], 57 | sun: [] 58 | }); 59 | 60 | dayOff.setHolidays([moment().hours(0).add(1, 'day').toDate()]); 61 | 62 | dayOff.setDayOffs( 63 | moment().startOf('day').hour(12).minute(0).toDate(), 64 | moment().startOf('day').hour(12).minute(0).add(4, 'day').toDate(), 65 | 'asia/phnom_penh' 66 | ); 67 | return dayOff.calculate(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/api/example/pdfmake/pdfmake.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param, Res } from '@nestjs/common'; 2 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 3 | import { Response } from 'express'; 4 | 5 | import { PdfMakeService } from './pdfmake.service'; 6 | 7 | @ApiBearerAuth() 8 | @ApiTags('Example - PDFMake') 9 | @Controller('example/pdfmake') 10 | export class PDFMakeController { 11 | constructor(private readonly service: PdfMakeService) {} 12 | 13 | @Get(':type') 14 | @ApiOperation({ summary: 'Create and Download PDF File' }) 15 | pdfMake(@Param('type') type: string, @Res() res: Response) { 16 | switch (type) { 17 | case 'download': 18 | return this.service.download(res); 19 | case 'url': 20 | return this.service.getDataUrl(res); 21 | case 'write': 22 | default: 23 | return this.service.writeFile(res); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/api/example/sequelize/sequelize.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/sequelize'; 3 | import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | import { Sequelize } from 'sequelize-typescript'; 5 | 6 | import { UserModel } from '@models'; 7 | 8 | @ApiBearerAuth() 9 | @ApiTags('Example - Sequelize') 10 | @Controller('example/sequelize') 11 | export class SequelizeController { 12 | constructor(private db: Sequelize, @InjectModel(UserModel) private userModel: typeof UserModel) {} 13 | 14 | @Get('testing') 15 | @ApiOperation({ summary: 'Testing' }) 16 | testing() { 17 | return this.db.query('select 1 + 1 as sum', { plain: true }); 18 | } 19 | 20 | @Post('users') 21 | @ApiOperation({ summary: 'Get users' }) 22 | users() { 23 | // await this.user.create({ 24 | // auth0Id: '1', 25 | // firstName: 'Chanoudom', 26 | // lastName: 'Preap', 27 | // nickName: 'Dominic', 28 | // email: 'dominic@testing.com', 29 | // phone: '08551111111' 30 | // } as any); 31 | 32 | return this.userModel.$findAndCountAll({ firstName: 'dom', limit: 1, offset: 0 }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/api/example/soap/soap.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | 4 | import { HolidaySoapService } from '../../soap/holiday-soap/holiday-soap.service'; 5 | 6 | @ApiTags('Example - Soap') 7 | @Controller('example/soap') 8 | export class SoapController { 9 | constructor(private readonly soap: HolidaySoapService) {} 10 | 11 | @Get() 12 | getHolidayDate() { 13 | return this.soap.GetHolidayDate({ 14 | countryCode: 'Canada', 15 | year: 2018, 16 | holidayCode: 'NEW-YEARS-DAY-ACTUAL' 17 | }); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/api/example/typeorm/typeorm.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Post } from '@nestjs/common'; 2 | import { ApiTags } from '@nestjs/swagger'; 3 | import { InjectRepository } from '@nestjs/typeorm'; 4 | import { Repository } from 'typeorm'; 5 | 6 | import { UserEntity } from '@entities'; 7 | import { UserRepository } from '@repositories'; 8 | 9 | @ApiTags('Example - TypeORM') 10 | @Controller('example/typeorm') 11 | export class TypeORMController { 12 | constructor( 13 | @InjectRepository(UserEntity) private readonly user: Repository, 14 | @InjectRepository(UserRepository) private readonly userRepository: UserRepository 15 | ) {} 16 | 17 | @Get() 18 | async get() { 19 | return this.user.find({ select: ['id', 'email', 'status'], take: 5, order: { id: 'DESC' } }); 20 | } 21 | 22 | @Post() 23 | post() { 24 | return this.user.insert({ 25 | // username: 'my-username', 26 | // password: 'my-password', 27 | // isArchived: false 28 | }); 29 | } 30 | 31 | @Get('repository') 32 | getRepository() { 33 | return this.userRepository.$findAndCountAll({ organizationId: 1, name: 'dom' }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/api/example/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BadRequestException, 3 | Controller, 4 | Post, 5 | UploadedFile, 6 | UploadedFiles, 7 | UseInterceptors 8 | } from '@nestjs/common'; 9 | import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'; 10 | import { ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; 11 | 12 | import { CSVMulterOption, ImageMulterOption } from '@common'; 13 | 14 | // import * as sharp from 'sharp'; 15 | 16 | @ApiTags('Example - Upload') 17 | @Controller('example/upload') 18 | export class UploadController { 19 | @Post('multi-upload') 20 | @UseInterceptors(FilesInterceptor('filename', 10, ImageMulterOption)) 21 | @ApiOperation({ summary: 'Upload Photo' }) 22 | @ApiConsumes('multipart/form-data') 23 | // @ApiImplicitFile({ name: 'filename', required: true, description: 'List of images' }) 24 | multiUpload(@UploadedFiles() files: Express.Multer.File[]) { 25 | if (!files.length) throw new BadRequestException('There are no file.'); 26 | return { message: 'ok' }; 27 | 28 | // TODO: 29 | // sharp.cache(false); 30 | // await sharp('input.jpg') 31 | // .resize(200, 300) 32 | // .jpeg() 33 | // .toFile('output.jpg'); 34 | } 35 | 36 | @Post('single-upload') 37 | @UseInterceptors(FileInterceptor('filename', CSVMulterOption)) 38 | @ApiOperation({ summary: 'Upload Photo via CSV File' }) 39 | @ApiConsumes('multipart/form-data') 40 | // @ApiImplicitFile({ name: 'filename', required: true, description: 'CSV file' }) 41 | singleUpload(@UploadedFile() file: Express.Multer.File) { 42 | if (!file) throw new BadRequestException('There are no files.'); 43 | return { message: 'ok' }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/api/shared/firebase-admin.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import * as admin from 'firebase-admin'; 3 | 4 | import { InjectFirebaseAdmin } from '@lib/firebase-admin'; 5 | 6 | @Injectable() 7 | export class FirebaseAdminService { 8 | constructor(@InjectFirebaseAdmin() private readonly admin: admin.app.App) {} 9 | async notifySample(total: number) { 10 | if (!total) return; 11 | 12 | const tokens = ['sample-token']; // TODO: get token from your database 13 | const payload = this.getPayload('New Sample', `There are ${total} new sample.`); 14 | this.sendPush(tokens, payload); 15 | } 16 | 17 | private sendPush(tokens: string[], payload: admin.messaging.MessagingPayload) { 18 | if (!tokens.length) return; 19 | this.admin 20 | .messaging() 21 | .sendToDevice(tokens, payload) 22 | .then((response: any) => console.log('Successfully sent message:', response)) 23 | .catch((error: string) => console.log('Error sending message:', error)); 24 | } 25 | 26 | private getPayload( 27 | title: string, 28 | body: string, 29 | data: any = {} 30 | ): admin.messaging.MessagingPayload { 31 | return { data, notification: { title, body } }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/api/shared/mailer.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { resolve } from 'path'; 3 | import { compileFile } from 'pug'; 4 | 5 | import { InjectMailer, Mailer } from '@lib/mailer'; 6 | 7 | @Injectable() 8 | export class MailerService { 9 | constructor(@InjectMailer() private readonly mailer: Mailer) {} 10 | 11 | async sendSampleEmail() { 12 | const data = [1, 2, 3, 4]; 13 | const count = 5; 14 | 15 | const compiledFunction = compileFile(resolve('.', 'assets', 'templates', 'sample.pug')); 16 | this.mailer.send({ 17 | from: '"Nest Boilerplate Project" ', // sender address 18 | to: 'someone@example.com', // list of receivers 19 | subject: 'Sample Email', 20 | html: compiledFunction({ count, data }), 21 | attachments: [] 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/api/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | // import { FirebaseAdminService } from './firebase-admin.service'; 4 | // import { MailerService } from './mailer.service'; 5 | 6 | @Global() 7 | @Module({ 8 | providers: [ 9 | /* FirebaseAdminService, MailerService*/ 10 | ], 11 | exports: [ 12 | /* FirebaseAdminService, MailerService */ 13 | ] 14 | }) 15 | export class SharedModule {} 16 | -------------------------------------------------------------------------------- /src/api/soap/holiday-soap/holiday-soap.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | 3 | import { HolidaySoapService } from './holiday-soap.service'; 4 | 5 | @Module({ 6 | providers: [HolidaySoapService], 7 | exports: [HolidaySoapService] 8 | }) 9 | export class HolidaySoapModule {} 10 | -------------------------------------------------------------------------------- /src/api/soap/soap.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { HolidaySoapModule } from './holiday-soap/holiday-soap.module'; 4 | 5 | @Global() 6 | @Module({ 7 | imports: [HolidaySoapModule], 8 | exports: [HolidaySoapModule] 9 | }) 10 | export class SoapModule {} 11 | -------------------------------------------------------------------------------- /src/api/upload/upload.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; 2 | import { FileInterceptor } from '@nestjs/platform-express'; 3 | import { ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; 4 | 5 | import { ApiFileBody, CSVMulterOption, ImageMulterOption } from '@common'; 6 | 7 | import { UploadService } from './upload.service'; 8 | 9 | @Controller('v1/upload') 10 | @ApiTags('Upload') 11 | export class UploadController { 12 | constructor(private readonly service: UploadService) {} 13 | 14 | @Post('image') 15 | @ApiConsumes('multipart/form-data') 16 | @ApiFileBody('filename') 17 | @ApiOperation({ summary: 'Upload Image' }) 18 | @UseInterceptors(FileInterceptor('filename', ImageMulterOption)) 19 | uploadImage(@UploadedFile() file: Express.Multer.File) { 20 | return this.service.upload(file, 'image'); 21 | } 22 | 23 | @Post('csv') 24 | @ApiConsumes('multipart/form-data') 25 | @ApiFileBody('filename') 26 | @ApiOperation({ summary: 'Upload CSV file' }) 27 | @UseInterceptors(FileInterceptor('filename', CSVMulterOption)) 28 | uploadCSV(@UploadedFile() file: Express.Multer.File) { 29 | return this.service.upload(file, 'csv'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/api/upload/upload.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { UploadController } from './upload.controller'; 4 | import { UploadService } from './upload.service'; 5 | 6 | @Global() 7 | @Module({ 8 | controllers: [UploadController], 9 | providers: [UploadService], 10 | exports: [UploadService] 11 | }) 12 | export class UploadModule {} 13 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ScheduleModule } from '@nestjs/schedule'; 3 | 4 | import { DynamoDBModule } from '@dynamodb'; 5 | import { Auth0Module } from '@lib/auth0'; 6 | import { AWSModule } from '@lib/aws'; 7 | import { ConfigModule } from '@lib/config'; 8 | import { CryptoModule } from '@lib/crypto'; 9 | import { FirebaseAdminModule } from '@lib/firebase-admin'; 10 | import { GoogleCloudStorageModule } from '@lib/google-cloud-storage'; 11 | import { GraphQLRequestModule } from '@lib/graphql-request'; 12 | import { I18NextModule } from '@lib/i18next'; 13 | import { IORedisModule } from '@lib/ioredis'; 14 | import { JwtModule } from '@lib/jwt'; 15 | import { KeycloakModule } from '@lib/keycloak'; 16 | import { MailerModule } from '@lib/mailer'; 17 | import { MediaStreamModule } from '@lib/media-stream'; 18 | import { MongooseModule } from '@lib/mongoose'; 19 | import { SendBirdModule } from '@lib/sendbird'; 20 | import { SequelizeModule } from '@lib/sequelize'; 21 | import { SocialModule } from '@lib/social'; 22 | import { SocketModule } from '@lib/socket'; 23 | import { Tile38Module } from '@lib/tile38'; 24 | import { TwilioModule } from '@lib/twilio'; 25 | import { TypeOrmModule } from '@lib/typeorm'; 26 | import { WowzaModule } from '@lib/wowza'; 27 | 28 | import { ApiModule } from './api/api.module'; 29 | 30 | @Module({ 31 | imports: [ 32 | AWSModule, 33 | Auth0Module, 34 | ApiModule, 35 | ConfigModule, 36 | CryptoModule, 37 | DynamoDBModule, 38 | FirebaseAdminModule, 39 | GoogleCloudStorageModule, 40 | GraphQLRequestModule, 41 | I18NextModule, 42 | IORedisModule, 43 | KeycloakModule, 44 | JwtModule, 45 | MailerModule, 46 | MediaStreamModule, 47 | MongooseModule, 48 | ScheduleModule.forRoot(), 49 | SendBirdModule, 50 | SequelizeModule, 51 | SocialModule, 52 | SocketModule, 53 | TypeOrmModule, 54 | Tile38Module, 55 | TwilioModule, 56 | WowzaModule 57 | ] 58 | }) 59 | export class ApplicationModule {} 60 | -------------------------------------------------------------------------------- /src/common/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | 3 | export const ASSETS_PATH = resolve('.', 'assets',); // prettier-ignore 4 | export const TEMP_PATH = resolve('.', 'public', 'upload', 'temp'); // prettier-ignore 5 | -------------------------------------------------------------------------------- /src/common/decorators/api-headers.decorator.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | import { Transform, Type } from 'class-transformer'; 3 | import { Allow, IsOptional, validateOrReject } from 'class-validator'; 4 | import { Request } from 'express'; 5 | import * as moment from 'moment'; 6 | 7 | import { IsNotEmptyString, IsOptionalString } from './dto.decorator'; 8 | 9 | export const ApiCustomHeaders = createParamDecorator( 10 | async (args: unknown, ctx: ExecutionContext) => { 11 | const req = ctx.switchToHttp().getRequest(); 12 | const header = new ApiCustomHeader(req); 13 | try { 14 | await validateOrReject(header); 15 | return header; 16 | } catch (err: any) { 17 | throw new BadRequestException(Object.values(err[0].constraints)); 18 | } 19 | } 20 | ); 21 | 22 | export class ApiCustomHeader { 23 | @IsNotEmptyString() 24 | readonly appVersion!: string; 25 | 26 | @IsOptionalString() 27 | readonly language!: string; 28 | 29 | @IsOptionalString() 30 | readonly latitude!: string; 31 | 32 | @IsOptionalString() 33 | readonly longitude!: string; 34 | 35 | @IsOptionalString() 36 | readonly platform!: string; 37 | 38 | @IsOptionalString() 39 | readonly osVersion!: string; 40 | 41 | @IsOptionalString() 42 | readonly udid!: string; 43 | 44 | @Allow() 45 | readonly ip!: string; 46 | 47 | @IsOptionalString() 48 | readonly timezone!: string; 49 | 50 | @IsOptional() 51 | @Type(() => Date) 52 | @Transform(x => moment(x.value)) 53 | readonly timestamp!: moment.Moment; 54 | 55 | constructor(req: Request) { 56 | this.appVersion = req.get('x-app-version')!; 57 | this.ip = req.get('x-forwarded-for') || req.socket.remoteAddress || '127.0.0.1'; 58 | this.language = ['en', 'km', 'zh'].find(x => x === req.get('x-language')) || 'en'; 59 | this.latitude = req.get('x-latitude') || ''; 60 | this.longitude = req.get('x-longitude') || ''; 61 | this.platform = ['ios', 'android', 'web', 'api'].find(x => x === req.get('x-platform')) || ''; 62 | this.osVersion = req.get('x-os-version') || '0.0.0'; 63 | this.udid = req.get('x-udid') || ''; 64 | this.timezone = req.get('x-timezone') || ''; 65 | this.timestamp = moment(req.get('x-timestamp')).isValid() 66 | ? moment(req.headers['x-timestamp']) 67 | : moment(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/common/decorators/auth-user.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const AuthUser = createParamDecorator( 4 | (args: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().authUser 5 | ); 6 | 7 | // TODO: create your own Authenticate User Interface 8 | export interface AuthUserX { 9 | _id: string; 10 | firstName: string; 11 | lastName: string; 12 | username: string; 13 | password: string; 14 | group: string; 15 | role: string; 16 | isArchived: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /src/common/decorators/auth.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common'; 2 | 3 | export const Auth = (...roles: ('admin' | 'user')[]) => SetMetadata('roles', roles); 4 | -------------------------------------------------------------------------------- /src/common/exceptions/app-exception-filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; 2 | import { Response } from 'express'; 3 | 4 | @Catch(HttpException) 5 | export class AppExceptionFilter implements ExceptionFilter { 6 | catch(exception: HttpException, host: ArgumentsHost) { 7 | const res = host.switchToHttp().getResponse(); 8 | 9 | const statusCode = exception.getStatus(); 10 | const response = exception.getResponse() as { 11 | statusCode: number; 12 | message: string | string[]; 13 | error: string; 14 | }; 15 | const message = Array.isArray(response.message) ? response.message[0] : response.message; 16 | 17 | // TODO: customize your own error handler 18 | // ====================================== 19 | 20 | // ! Display log in server. Can also integrate with Mailer if want to alert to developer 21 | if (statusCode >= 500) { 22 | console.error(exception); 23 | } else { 24 | console.log(response); 25 | } 26 | 27 | res.status(statusCode).json({ statusCode, message }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/common/guards/api.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | ForbiddenException, 5 | Injectable, 6 | UnauthorizedException, 7 | UseGuards 8 | } from '@nestjs/common'; 9 | import { JwtService } from '@nestjs/jwt'; 10 | import { Request } from 'express'; 11 | 12 | /** 13 | * @description The basic authenticate guard. 14 | */ 15 | export const ApiGuard = () => UseGuards(XApiGuard); 16 | 17 | @Injectable() 18 | class XApiGuard implements CanActivate { 19 | constructor(private readonly service: JwtService) {} 20 | 21 | async canActivate(context: ExecutionContext) { 22 | const req = context.switchToHttp().getRequest(); 23 | const key = req.get('x-api-key') || ''; 24 | 25 | const decoded: string = await this.service.verifyAsync(key).catch(e => { 26 | throw new UnauthorizedException(e.name + ' ' + e.message); 27 | }); 28 | 29 | // TODO: use for static token, not recommended unless you know what are you doing 30 | if (decoded !== 'something') throw new ForbiddenException('Invalid API Key'); 31 | return true; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/common/guards/authenticate.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ExecutionContext, 4 | Injectable, 5 | UnauthorizedException, 6 | UseGuards 7 | } from '@nestjs/common'; 8 | import { JwtService } from '@nestjs/jwt'; 9 | import { Request } from 'express'; 10 | 11 | interface JWTDecoded { 12 | timestamp: number; 13 | clientId: string; 14 | uid: string; 15 | } 16 | 17 | /** 18 | * The verification of the credentials of the connection attempt. Or the act of logging a user in. 19 | * @description https://medium.freecodecamp.org/securing-node-js-restful-apis-with-json-web-tokens-9f811a92bb52 20 | */ 21 | export const Authenticate = () => UseGuards(AuthenticateGuard); 22 | 23 | @Injectable() 24 | class AuthenticateGuard implements CanActivate { 25 | constructor(private readonly jwt: JwtService) {} 26 | 27 | async canActivate(context: ExecutionContext) { 28 | const req = context.switchToHttp().getRequest(); 29 | 30 | const authorization = req.get('token') || ''; 31 | 32 | const [scheme, token] = authorization.split(' '); 33 | if (scheme.toLowerCase() !== 'bearer') 34 | throw new UnauthorizedException('Invalid Authorization Scheme'); 35 | if (!token) throw new UnauthorizedException('Invalid Authorization Token'); 36 | 37 | try { 38 | // Decoded token from header 39 | const { clientId } = await this.jwt.verifyAsync(token); 40 | console.log(clientId); 41 | 42 | // TODO: checked if database is SQL or NoSQL 43 | // Find Client App from database to make sure it exist 44 | // const clientApp = await ClientApp.findOne({ where: { clientId } }); 45 | // if (!clientApp) throw new UnauthorizedException('Client application was not found'); 46 | 47 | return true; 48 | } catch (e: any) { 49 | throw new UnauthorizedException(e.name + ' ' + e.message); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/common/guards/authorize.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable, UseGuards } from '@nestjs/common'; 2 | // import * as crypto from 'crypto'; 3 | // import { Request } from 'express'; 4 | 5 | /** 6 | * The act of verifying the access rights of a user to interact with a resource. 7 | * @description https://medium.freecodecamp.org/securing-node-js-restful-apis-with-json-web-tokens-9f811a92bb52 8 | */ 9 | export const Authorize = () => UseGuards(AuthorizeGuard); 10 | 11 | @Injectable() 12 | class AuthorizeGuard implements CanActivate { 13 | async canActivate(context: ExecutionContext) { 14 | // TODO: checked if database is SQL or NoSQL 15 | 16 | // const req = context.switchToHttp().getRequest(); 17 | 18 | // const authorization = req.get('authorization') || ''; 19 | // const { clientId } = req.body; 20 | 21 | // Find Client App from database to make sure it exist 22 | // const clientApp = await ClientApp.findOne({ clientId }); 23 | // if (!clientApp) throw new UnauthorizedException('Client application was not found'); 24 | 25 | // encrypted Client ID and Client Secret and compare with token from header 26 | // const hash = clientApp.clientId + ':' + clientApp.clientSecret; 27 | // const sha1Token = crypto 28 | // .createHash('sha1') 29 | // .update(hash) 30 | // .digest('hex'); 31 | 32 | // const [scheme, token] = authorization.split(' '); 33 | // if (scheme.toLowerCase() !== 'bearer') throw new UnauthorizedException('Invalid Authorization Scheme'); 34 | // if (token !== sha1Token) throw new UnauthorizedException('Invalid Authorization Token'); 35 | 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/common/interceptors/auditing.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; 2 | import { Request } from 'express'; 3 | import { Observable } from 'rxjs'; 4 | import { tap } from 'rxjs/operators'; 5 | 6 | import { AuthUserX } from '@common'; 7 | 8 | // import { Types } from 'mongoose'; 9 | // import { Audit } from '@schema'; 10 | 11 | @Injectable() 12 | export class AuditingInterceptor implements NestInterceptor { 13 | intercept(context: ExecutionContext, next: CallHandler): Observable { 14 | const date = Date.now(); 15 | return next.handle().pipe(tap(result => this.saveAudit(context, date, result))); 16 | } 17 | 18 | private async saveAudit(context: ExecutionContext, date: number, result: any) { 19 | const req = context.switchToHttp().getRequest(); 20 | const { method, url, body, authUser } = req; 21 | if (method === 'GET' || !authUser) return; // Save only apply on authUser or method is not GET 22 | 23 | const className = context.getClass().name; 24 | const handler = context.getHandler().name; 25 | const duration = Date.now() - date; 26 | const time = new Date(); 27 | 28 | const data = { 29 | body: JSON.stringify(body), 30 | className, 31 | duration, 32 | handler, 33 | method, 34 | result: JSON.stringify(result), 35 | time, 36 | url, 37 | // userId: Types.ObjectId(authUser._id), 38 | username: authUser.username 39 | }; 40 | // TODO: create your own Audit table 41 | // await Audit.create(data); 42 | console.log(data); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/common/interceptors/csv-multer.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; 3 | import { Request } from 'express'; 4 | import * as fs from 'fs'; 5 | import * as mime from 'mime-types'; 6 | import * as multer from 'multer'; 7 | import * as path from 'path'; 8 | import * as shell from 'shelljs'; 9 | import * as uuid from 'uuid'; 10 | 11 | // https://github.com/expressjs/multer/issues/170#issuecomment-123362345 12 | // https://github.com/expressjs/multer/issues/114#issuecomment-231591339 13 | 14 | const destination = path.resolve('.', 'public', 'upload', 'csv'); 15 | const allowedExtensions: any = ['csv', 'xls', 'xlsx']; 16 | export const CSVMulterOption: MulterOptions = { 17 | dest: destination, 18 | limits: { 19 | files: 1, // max number of files 20 | fileSize: 5 * 1024 * 1024 // 5 mb 21 | }, 22 | storage: multer.diskStorage({ 23 | destination: (req, file, cb) => cb(null, destination), 24 | filename: (req, file, cb) => 25 | cb(null, `${uuid.v4().replace(/-/g, '')}${path.extname(file.originalname)}`) // mime.extension(file.mimetype) 26 | }), 27 | fileFilter: (req: Request, file, cb) => { 28 | if (!fs.existsSync(destination)) shell.mkdir('-p', destination); 29 | if (!allowedExtensions.includes(mime.extension(file.mimetype))) { 30 | return cb(new BadRequestException('Extension not allowed'), false); 31 | } 32 | return cb(null, true); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/common/interceptors/image-multer.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { BadRequestException } from '@nestjs/common'; 2 | import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'; 3 | import { Request } from 'express'; 4 | import * as fs from 'fs'; 5 | import * as mime from 'mime-types'; 6 | import * as multer from 'multer'; 7 | import * as path from 'path'; 8 | import * as shell from 'shelljs'; 9 | 10 | const destination = path.resolve('.', 'public', 'upload'); 11 | 12 | export const ImageMulterOption: MulterOptions = { 13 | limits: { 14 | files: 100, // max number of files 15 | fileSize: 5 * 1024 * 1024 // 5 mb 16 | }, 17 | storage: multer.diskStorage({ 18 | filename: (req, file, cb) => cb(null, file.originalname), 19 | destination: (req, file, cb) => cb(null, destination) 20 | }), 21 | fileFilter: (req: Request, file, cb) => { 22 | if (!fs.existsSync(destination)) shell.mkdir('-p', destination); 23 | 24 | // TODO: check if upload is required parameters 25 | if (req.params.id === 'test') { 26 | return cb(new BadRequestException('Incorrect Client Key'), false); 27 | } 28 | 29 | // Check allowed extensions 30 | const allowedExtensions: any[] = ['jpg', 'jpeg']; 31 | if (!allowedExtensions.includes(mime.extension(file.mimetype))) { 32 | return cb(new BadRequestException('Extension not allowed'), false); 33 | } 34 | return cb(null, true); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/common/swagger/swagger.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CaseInsensitiveFilterPlugin, 3 | CustomLayoutPlugin, 4 | operationsSorter 5 | } from './swagger.plugin'; 6 | 7 | // For more info about config: 8 | // https://github.com/swagger-api/swagger-ui/blob/master/docs/usage/configuration.md 9 | export const swaggerOptions = { 10 | defaultModelExpandDepth: 3, 11 | defaultModelsExpandDepth: -1, 12 | docExpansion: 'none', 13 | filter: true, 14 | layout: 'CustomLayout', 15 | operationsSorter, 16 | plugins: [CaseInsensitiveFilterPlugin, CustomLayoutPlugin] 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/swagger/swagger.description.ts: -------------------------------------------------------------------------------- 1 | export const swaggerDescription = ` 2 | ## API Documentation Overview 3 | ___ 4 | ### Authorization 5 | The API uses the standard HTTP \`Authorization\` header to pass authentication information. 6 | 7 | 8 | ### Parameters 9 | Many API methods take optional parameters. 10 | * For \`GET\` and \`DELETE\` requests, parameters are passed as query string in the url. 11 | * For \`POST\` and \`PUT\` requests, parameters are encoded as JSON with a Content-Type of \`application/json\` in the header. 12 | ### Supported Request & Response Format 13 | JSON Only -- [No XML](http://s2.quickmeme.com/img/72/72e5b8f58c83b44f09e83ebf05920eeb234d1719ce8911d6e898e46562c47710.jpg) here'; 14 | `; 15 | -------------------------------------------------------------------------------- /src/common/swagger/swagger.plugin.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/swagger-api/swagger-ui/issues/4158 2 | export const operationsSorter = (a, b) => { 3 | const methodsOrder = ['get', 'post', 'put', 'delete', 'patch', 'options', 'trace']; 4 | let result = methodsOrder.indexOf(a.get('method')) - methodsOrder.indexOf(b.get('method')); 5 | // Or if you want to sort the methods alphabetically (delete, get, head, options, ...): 6 | // var result = a.get("method").localeCompare(b.get("method")); 7 | 8 | if (result === 0) { 9 | result = a.get('path').localeCompare(b.get('path')); 10 | } 11 | 12 | return result; 13 | }; 14 | 15 | // https://github.com/swagger-api/swagger-ui/issues/3876 16 | export const CaseInsensitiveFilterPlugin = () => { 17 | return { 18 | fn: { 19 | opsFilter: (taggedOps, phrase) => { 20 | return taggedOps.filter( 21 | (tagObj, tag) => tag.toLowerCase().indexOf(phrase.toLowerCase()) !== -1 22 | ); 23 | } 24 | } 25 | }; 26 | }; 27 | 28 | // https://github.com/swagger-api/swagger-ui/blob/master/docs/customization/custom-layout.md 29 | // https://reactjs.org/docs/react-without-jsx.html 30 | export const CustomLayoutPlugin = (props: any) => { 31 | const { getComponent, React } = props; 32 | const BaseLayout = getComponent('BaseLayout', true); 33 | 34 | const styles = { 35 | root: { 36 | paddingTop: 60 37 | }, 38 | header: { 39 | background: '#20232a', 40 | position: 'fixed', 41 | top: 0, 42 | width: '100%', 43 | zIndex: 100 44 | }, 45 | wrapper: { 46 | maxWidth: 1430, 47 | margin: '0 auto', 48 | padding: '12px 0px 8px' 49 | }, 50 | image: { 51 | height: 35 52 | } 53 | }; 54 | const Image = props.React.createElement('img', { 55 | style: styles.image, 56 | src: 'https://docs.nestjs.com/assets/logo-small.svg' 57 | }); 58 | const Wrapper = React.createElement('div', { style: styles.wrapper }, Image); 59 | const Header = React.createElement('div', { style: styles.header }, Wrapper); 60 | const CustomLayout = React.createElement( 61 | 'div', 62 | { style: styles.root }, 63 | React.createElement(() => Header, null), 64 | React.createElement(BaseLayout, { style: { paddingTop: 60 } }) 65 | ); 66 | 67 | return { components: { CustomLayout: () => CustomLayout } }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/common/transformers/number.transformer.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | 3 | /** 4 | * Transform input to number. default is 0. 5 | */ 6 | export const TransformToNumber = () => Transform(v => (isNaN(+v) ? 0 : +v)); 7 | -------------------------------------------------------------------------------- /src/common/transformers/sanitize-html.transformer.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { decode } from 'html-entities'; 3 | import * as sanitizeHtml from 'sanitize-html'; 4 | 5 | /* 6 | |***************************************************************************************************** 7 | | EXPLANATION: 8 | |***************************************************************************************************** 9 | | decode4(v) decode5(v) 10 | | First we use decode to covert special HTML characters in order to filter out XSS. 11 | | Need to run these both functions, filter out HTML4 & HTML5 12 | | 13 | | Example : 14 | | <script><svg onload=alert(10114)width=100></script> 15 | | \</script\>\<svg/onload=alert(10114) width=100\/> 16 | | 17 | | Return : 18 | |----------------------------------------------------------------------------------------------------- 19 | | sanitizeHtml(x, options) 20 | | finally remove any unwanted HTML attributes and tags 21 | | 22 | | Example :
message
23 | | Return :
message
24 | | 25 | |***************************************************************************************************** 26 | */ 27 | 28 | const decode4 = (text: string) => decode(text, { level: 'html4' }); 29 | const decode5 = (text: string) => decode(text, { level: 'html5' }); 30 | const defaultOptions: sanitizeHtml.IOptions = { 31 | allowedAttributes: { 32 | '*': ['style', 'class', 'href', 'src'] 33 | } 34 | }; 35 | 36 | /** 37 | * cleaning up HTML fragments such as those created by ckeditor and other rich text editors 38 | * 39 | * @see https://www.npmjs.com/package/sanitize-html 40 | * @see https://www.npmjs.com/package/html-entities 41 | */ 42 | export const TransformToSanitizeHtml = (option?: sanitizeHtml.IOptions) => 43 | Transform(({ value }) => sanitizeHtml(decode5(decode4(value)), { ...defaultOptions, ...option })); 44 | -------------------------------------------------------------------------------- /src/common/transformers/string.transformer.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | 3 | /** 4 | * Transform string with trim 5 | */ 6 | export const TransformTrimString = () => 7 | Transform(({ value }) => (typeof value === 'string' ? value.trim() : value)); 8 | -------------------------------------------------------------------------------- /src/common/types/auth.ts: -------------------------------------------------------------------------------- 1 | import { tuple } from './base'; 2 | 3 | export const RoleEnum = tuple('admin', 'supervisor', 'qa'); 4 | export type RoleType = typeof RoleEnum[number]; // union type 5 | -------------------------------------------------------------------------------- /src/common/types/base.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | References 4 | |-------------------------------------------------------------------------- 5 | | https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead 6 | | https://stackoverflow.com/questions/44154009/get-array-of-string-literal-type-values 7 | | 8 | */ 9 | 10 | export type Lit = string | number | boolean | undefined | null | void; 11 | export const tuple = (...args: T) => args; 12 | -------------------------------------------------------------------------------- /src/common/types/image.ts: -------------------------------------------------------------------------------- 1 | import { tuple } from './base'; 2 | 3 | export type ImageSize = 'lg' | 'md' | 'sm' | 'xs'; 4 | 5 | /** 6 | * Used as directory in cloud storage and as default image size when doing resize 7 | * @see check in `upload.service.ts` and `upload.helper.ts` for more details 8 | */ 9 | export type ImageType = typeof ImageEnum[number]; 10 | export const ImageEnum = tuple('temp', 'badge', 'team', 'user'); 11 | -------------------------------------------------------------------------------- /src/common/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth'; 2 | export * from './image'; 3 | export * from './notification'; 4 | -------------------------------------------------------------------------------- /src/common/types/notification.ts: -------------------------------------------------------------------------------- 1 | import { tuple } from './base'; 2 | 3 | export type NotificationType = typeof NotificationTypeEnum[number]; 4 | export const NotificationTypeEnum = tuple( 5 | 'AUS', // AUCTION_START 6 | 'AUE', // AUCTION_END 7 | 'AUO' // AUCTION_OUTBID 8 | ); 9 | 10 | export type NotificationUserType = typeof NotificationUserTypeEnum[number]; 11 | export const NotificationUserTypeEnum = tuple( 12 | 'NCT', // Notification Creator 13 | 'NRV', // Notification Receiver 14 | 'NIR' // Notification Indirect Receiver 15 | ); 16 | 17 | export type NotificationPhotoType = typeof NotificationPhotoTypeEnum[number]; 18 | export const NotificationPhotoTypeEnum = tuple( 19 | 'ORG', // Organization 20 | 'USR' // User 21 | ); 22 | -------------------------------------------------------------------------------- /src/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './misc'; 2 | export * from './time-ago'; 3 | export * from './time-ago-old'; 4 | -------------------------------------------------------------------------------- /src/common/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import * as _moment from 'moment'; 2 | 3 | /** 4 | * Add st, nd, rd and th (ordinal) suffix to a number 5 | * 6 | * @see https://stackoverflow.com/questions/13627308/add-st-nd-rd-and-th-ordinal-suffix-to-a-number 7 | */ 8 | export const nth = (n: number) => 9 | n + (['st', 'nd', 'rd'][((((n + 90) % 100) - 10) % 10) - 1] || 'th'); 10 | 11 | /** 12 | * Moment Helper 13 | * 14 | */ 15 | // prettier-ignore 16 | export const moment = { 17 | dayStart: (m: _moment.Moment) => m.startOf('day'), 18 | dayEnd: (m: _moment.Moment) => m.endOf('day'), 19 | thisWeekStart: (m: _moment.Moment) => m.startOf('isoWeek'), 20 | thisWeekEnd: (m: _moment.Moment) => m.endOf('isoWeek'), 21 | thisMonthStart: (m: _moment.Moment) => m.startOf('month'), 22 | thisMonthEnd: (m: _moment.Moment) => m.endOf('month'), 23 | yesterdayStart: (m: _moment.Moment) => m.subtract(1, 'day').startOf('day'), 24 | yesterdayEnd: (m: _moment.Moment) => m.subtract(1, 'day').endOf('day'), 25 | lastWeekStart: (m: _moment.Moment) => m.subtract(1, 'week').startOf('isoWeek'), 26 | lastWeekEnd: (m: _moment.Moment) => m.subtract(1, 'week').endOf('isoWeek'), 27 | lastMonthStart: (m: _moment.Moment) => m.subtract(1, 'month').startOf('month'), 28 | lastMonthEnd: (m: _moment.Moment) => m.subtract(1, 'month').endOf('month'), 29 | } 30 | -------------------------------------------------------------------------------- /src/common/utils/time-ago-old.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment-timezone'; 2 | 3 | /** 4 | * Calculate time ago from a date 5 | */ 6 | export const getTimeAgoX = (date: Date, timezone = 'Asia/Bangkok') => { 7 | const p = (v: number, type: 'second' | 'minute' | 'hour') => 8 | `${v} ${type}${v > 1 ? 's' : ''} ago`; // pluralize 9 | 10 | const now = moment.tz(timezone); // present 11 | const past = moment.tz(date, timezone); // past 12 | 13 | const seconds = now.diff(past, 'seconds'); 14 | const minutes = now.diff(past, 'minutes'); 15 | const hours = now.diff(past, 'hours'); 16 | const days = now.toDate().getDate() - 1 === past.toDate().getDate(); // compare after 24hour 17 | 18 | if (seconds < 60) { 19 | return p(seconds, 'second'); 20 | } else if (minutes < 60) { 21 | return p(minutes, 'minute'); 22 | } else if (now.isSame(past, 'day')) { 23 | return p(hours, 'hour'); 24 | } else if (days) { 25 | return past.format('[Yesterday at] h:mm A'); // EX: Yesterday at 2:04 PM 26 | } else if (isCurrentWeek(past)) { 27 | return past.format('ddd [at] h:mm A'); // In the same week EX: Wed at 7:00 AM 28 | } else { 29 | return past.format('MMM DD [at] h:mm A'); // Otherwise EX: Jul 16 at 7:00 AM 30 | } 31 | }; 32 | 33 | /** 34 | * Check if date is in current week 35 | */ 36 | function isCurrentWeek(date: moment.Moment) { 37 | const startOfWeek = moment().startOf('isoWeek'); 38 | const endOfWeek = moment().endOf('isoWeek'); 39 | return startOfWeek.isSameOrBefore(date) && endOfWeek.isSameOrAfter(date); 40 | } 41 | -------------------------------------------------------------------------------- /src/common/validators/is-greater-or-equal.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerDecorator, 3 | ValidationArguments, 4 | ValidationOptions, 5 | ValidatorConstraint, 6 | ValidatorConstraintInterface 7 | } from 'class-validator'; 8 | 9 | @ValidatorConstraint({ name: 'isGreaterOrEqual', async: false }) 10 | class IsGreaterOrEqualConstraint implements ValidatorConstraintInterface { 11 | validate(propertyValue: string, args: ValidationArguments) { 12 | return propertyValue >= args.object[args.constraints[0]]; 13 | } 14 | 15 | defaultMessage(args: ValidationArguments) { 16 | return `"${args.property}" must be greaterOrEquals "${args.constraints[0]}"`; 17 | } 18 | } 19 | 20 | export const IsGreaterOrEqual = (property: string, validationOptions?: ValidationOptions) => { 21 | return (object: Record, propertyName: string) => { 22 | registerDecorator({ 23 | target: object.constructor, 24 | propertyName, 25 | options: validationOptions, 26 | constraints: [property], 27 | validator: IsGreaterOrEqualConstraint 28 | }); 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/common/validators/is-not-blank.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerDecorator, 3 | ValidationArguments, 4 | ValidationOptions, 5 | ValidatorConstraint, 6 | ValidatorConstraintInterface 7 | } from 'class-validator'; 8 | 9 | @ValidatorConstraint({ name: 'isNotBlank', async: false }) 10 | class IsNotBlankConstraint implements ValidatorConstraintInterface { 11 | validate(propertyValue: string, args: ValidationArguments) { 12 | return typeof propertyValue === 'string' && propertyValue.trim().length > 0; 13 | } 14 | 15 | defaultMessage(args: ValidationArguments) { 16 | return `${args.property} must not contain empty spaces`; 17 | } 18 | } 19 | 20 | export const IsNotBlank: any = (property: string, validationOptions?: ValidationOptions) => { 21 | return (object: Record, propertyName: string) => { 22 | registerDecorator({ 23 | target: object.constructor, 24 | propertyName, 25 | options: validationOptions, 26 | constraints: [property], 27 | validator: IsNotBlankConstraint 28 | }); 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/common/validators/is-phone-number.validator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | registerDecorator, 3 | ValidationArguments, 4 | ValidationOptions, 5 | ValidatorConstraint, 6 | ValidatorConstraintInterface 7 | } from 'class-validator'; 8 | import { PhoneNumber } from 'libphonenumber-js'; 9 | 10 | @ValidatorConstraint({ name: 'IsPhoneNumberX', async: false }) 11 | class IsPhoneNumberConstraint implements ValidatorConstraintInterface { 12 | validate(propertyValue: PhoneNumber, args: ValidationArguments) { 13 | if (propertyValue && typeof propertyValue === 'object') { 14 | return propertyValue.country ? true : false; 15 | } 16 | return false; 17 | } 18 | 19 | defaultMessage(args: ValidationArguments) { 20 | return `${args.property} is not valid format.`; 21 | } 22 | } 23 | 24 | // phone.countryCallingCode // 855 25 | // phone.nationalNumber 10123456 26 | // phone.number +85510123456 27 | // phone: user.phone 28 | export const IsPhoneNumber = (validationOptions?: ValidationOptions): PropertyDecorator => { 29 | // eslint-disable-next-line @typescript-eslint/ban-types 30 | return (object: object, propertyName: string | symbol) => { 31 | registerDecorator({ 32 | target: object.constructor, 33 | propertyName: propertyName as any, 34 | options: validationOptions, 35 | constraints: [], 36 | validator: IsPhoneNumberConstraint 37 | }); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /src/dynamodb/base/base.schema.ts: -------------------------------------------------------------------------------- 1 | import { attribute } from '@aws/dynamodb-data-mapper-annotations'; 2 | 3 | export class BaseDataObject { 4 | @attribute({ defaultProvider: () => new Date().getTime() }) 5 | createdDate!: number; 6 | 7 | @attribute() 8 | createdBy!: string; 9 | 10 | @attribute({ defaultProvider: () => new Date().getTime() }) 11 | updatedDate!: number; 12 | 13 | @attribute() 14 | updatedBy!: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/dynamodb/base/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.schema'; 2 | -------------------------------------------------------------------------------- /src/dynamodb/dynamodb.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { DynamoDBUserService } from './user/user.service'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [DynamoDBUserService], 8 | exports: [DynamoDBUserService] 9 | }) 10 | export class DynamoDBModule {} 11 | -------------------------------------------------------------------------------- /src/dynamodb/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base'; 2 | export * from './user'; 3 | export * from './dynamodb.module'; 4 | -------------------------------------------------------------------------------- /src/dynamodb/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.schema'; 2 | export * from './user.service'; 3 | -------------------------------------------------------------------------------- /src/dynamodb/user/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FindOneOpt { 2 | status?: string; 3 | email?: string; 4 | logo?: string; 5 | indexName?: string; 6 | } 7 | 8 | export interface FindAllOpt extends FindOneOpt { 9 | limit?: number; 10 | } 11 | -------------------------------------------------------------------------------- /src/dynamodb/user/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { embed } from '@aws/dynamodb-data-mapper'; 2 | import { attribute, autoGeneratedHashKey, table } from '@aws/dynamodb-data-mapper-annotations'; 3 | 4 | import { BaseDataObject } from '../base/base.schema'; 5 | 6 | class Address { 7 | @attribute() 8 | lat?: string; 9 | 10 | @attribute() 11 | lng?: string; 12 | } 13 | 14 | class Profile { 15 | @attribute() 16 | logo?: string; 17 | 18 | @attribute() 19 | nickName?: string; 20 | } 21 | 22 | @table('testing') 23 | export class UserObject extends BaseDataObject { 24 | @autoGeneratedHashKey() 25 | id!: string; 26 | 27 | @attribute() 28 | firstName!: string; 29 | 30 | @attribute() 31 | lastName!: string; 32 | 33 | @attribute() 34 | email?: string; 35 | 36 | @attribute() 37 | status?: string; 38 | 39 | @attribute({ memberType: embed(Address) }) 40 | address?: Array
; 41 | 42 | @attribute({ valueConstructor: Profile }) 43 | profile?: Profile; 44 | 45 | @attribute({ type: 'Collection', memberType: 'Number' }) 46 | tags?: string[]; 47 | } 48 | -------------------------------------------------------------------------------- /src/dynamodb/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { ConditionExpression, equals } from '@aws/dynamodb-expressions'; 2 | import { Injectable } from '@nestjs/common'; 3 | import * as _ from 'lodash'; 4 | 5 | import { AWSLib, InjectAWS } from '@lib/aws'; 6 | 7 | import * as I from './user.interface'; 8 | import { UserObject } from './user.schema'; 9 | 10 | @Injectable() 11 | export class DynamoDBUserService { 12 | constructor(@InjectAWS() private readonly aws: AWSLib) {} 13 | 14 | async create(body: any) { 15 | const data = Object.assign(new UserObject(), body); 16 | return this.aws.mapper.put(data); 17 | } 18 | 19 | async findOne(opt: I.FindOneOpt) { 20 | const result = await this.findAll({ ...opt, limit: 1 }); 21 | return result.length ? result[0] : null; 22 | } 23 | 24 | async findAll(opt: I.FindAllOpt) { 25 | const conditions = this.filter(opt); 26 | 27 | const list = this.aws.mapper.scan(UserObject, { 28 | projection: ['email', 'firstName', 'address'], 29 | indexName: opt.indexName, 30 | filter: { type: 'And', conditions }, 31 | limit: opt.limit 32 | }); 33 | 34 | const items: UserObject[] = []; 35 | for await (const item of list) { 36 | items.push(item); 37 | } 38 | return items; 39 | } 40 | 41 | private filter(opt: I.FindAllOpt) { 42 | const filters: any = [ 43 | // --- 44 | filterByStatus(opt.status), 45 | filterByEmail(opt.email), 46 | filterByLogo(opt.logo) 47 | ]; 48 | return _.compact(filters) as ConditionExpression[]; 49 | } 50 | } 51 | 52 | type T = ConditionExpression | null; 53 | const filterByStatus = (v?: string): T => v ? ({ subject: 'status', ...equals(v) }) : null; // prettier-ignore 54 | const filterByEmail = (v?: string): T => v ? ({ subject: 'email', ...equals(v) }) : null; // prettier-ignore 55 | const filterByLogo = (v?: string): T => v ? ({ subject: 'profile.logo', ...equals(v) }) : null; // prettier-ignore 56 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export { UserEntity } from './user.entity'; 2 | export { UserProfileEntity } from './user-profile.entity'; 3 | -------------------------------------------------------------------------------- /src/entities/user-profile.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn } from 'typeorm'; 2 | 3 | import { UserEntity } from './user.entity'; 4 | 5 | @Entity('UserProfiles') 6 | export class UserProfileEntity { 7 | @PrimaryColumn() 8 | userId!: number; 9 | 10 | @Column() 11 | nickName!: string; 12 | 13 | @OneToOne(() => UserEntity, u => u.profile) 14 | @JoinColumn() 15 | user?: UserEntity; 16 | } 17 | -------------------------------------------------------------------------------- /src/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm'; 2 | 3 | import { UserProfileEntity } from './user-profile.entity'; 4 | 5 | @Entity('Users') 6 | export class UserEntity { 7 | @PrimaryGeneratedColumn() 8 | id!: number; 9 | 10 | @Column() 11 | email!: string; 12 | 13 | @Column() 14 | status!: string; 15 | 16 | @Column() 17 | organizationId!: number; 18 | 19 | @OneToOne(() => UserProfileEntity, p => p.user) 20 | @JoinColumn({ name: 'id' }) 21 | profile!: UserProfileEntity; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/auth0/auth0.constant.ts: -------------------------------------------------------------------------------- 1 | export const AUTH0_STRATEGY_NAME = 'auth0'; 2 | export const AUTH0_TOKEN = 'AUTH0_INJECT_TOKEN'; 3 | -------------------------------------------------------------------------------- /src/lib/auth0/auth0.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext, Inject } from '@nestjs/common'; 2 | 3 | import { AUTH0_TOKEN } from './auth0.constant'; 4 | 5 | export const InjectAuth0ManagementClient = () => Inject(AUTH0_TOKEN); 6 | 7 | export const Auth0User = createParamDecorator( 8 | (args: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user 9 | ); 10 | 11 | export interface Auth0Payload { 12 | iss: string; 13 | sub: string; 14 | aud: string[]; 15 | iat: number; 16 | exp: number; 17 | azp: string; 18 | scope: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/auth0/auth0.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class AuthConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | AUTH0_DOMAIN!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | AUTH0_AUDIENCE!: string; 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | AUTH0_CLIENT_ID!: string; 15 | 16 | @IsNotEmpty() 17 | @IsString() 18 | AUTH0_CLIENT_SECRET!: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/auth0/auth0.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | 4 | import { Auth0ClientProvider, Auth0StrategyProvider } from './auth0.provider'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [PassportModule], 9 | providers: [Auth0ClientProvider, Auth0StrategyProvider], 10 | exports: [Auth0ClientProvider, Auth0StrategyProvider] 11 | }) 12 | export class Auth0Module {} 13 | -------------------------------------------------------------------------------- /src/lib/auth0/auth0.provider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | import { ManagementClient } from 'auth0'; 3 | 4 | import { ConfigService } from '../config'; 5 | import { AUTH0_TOKEN } from './auth0.constant'; 6 | import { AuthConfig } from './auth0.dto'; 7 | import { Auth0Strategy } from './auth0.strategy'; 8 | 9 | export const Auth0ClientProvider: Provider = { 10 | inject: [ConfigService], 11 | provide: AUTH0_TOKEN, 12 | useFactory: async (configService: ConfigService) => { 13 | const config = configService.validate('Auth0Client', AuthConfig); 14 | return new ManagementClient({ 15 | domain: config.AUTH0_DOMAIN, 16 | clientId: config.AUTH0_CLIENT_ID, 17 | clientSecret: config.AUTH0_CLIENT_SECRET 18 | }); 19 | } 20 | }; 21 | 22 | export const Auth0StrategyProvider: Provider = { 23 | inject: [ConfigService], 24 | provide: Auth0Strategy, 25 | useFactory: async (configService: ConfigService) => { 26 | const config = configService.validate('Auth0Module', AuthConfig); 27 | return new Auth0Strategy(config); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /src/lib/auth0/auth0.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { passportJwtSecret } from 'jwks-rsa'; 4 | import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt'; 5 | 6 | import { AUTH0_STRATEGY_NAME } from './auth0.constant'; 7 | import { AuthConfig } from './auth0.dto'; 8 | 9 | /** 10 | * 11 | * 12 | * @see https://dev.to/fullstack_to/use-auth0-to-secure-your-nestjs-application-mbo 13 | * @see https://stackoverflow.com/questions/53426069/getting-user-data-by-using-guards-roles-jwt 14 | */ 15 | @Injectable() 16 | export class Auth0Strategy extends PassportStrategy(Strategy, AUTH0_STRATEGY_NAME) { 17 | constructor(config: AuthConfig) { 18 | super({ 19 | secretOrKeyProvider: passportJwtSecret({ 20 | cache: true, 21 | jwksRequestsPerMinute: 5, 22 | jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json`, 23 | rateLimit: true 24 | }), 25 | algorithm: 'RS256', 26 | audience: config.AUTH0_AUDIENCE, 27 | issuer: `https://${config.AUTH0_DOMAIN}/`, 28 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() 29 | }); 30 | } 31 | 32 | validate(payload: any, done: VerifiedCallback) { 33 | if (!payload) { 34 | done(new UnauthorizedException('Sorry, we were unable to process your request.'), false); 35 | } 36 | return done(null, payload); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/auth0/index.ts: -------------------------------------------------------------------------------- 1 | export { Auth0Module } from './auth0.module'; 2 | export { Auth0Payload, Auth0User, InjectAuth0ManagementClient } from './auth0.decorator'; 3 | export { AUTH0_STRATEGY_NAME } from './auth0.constant'; 4 | -------------------------------------------------------------------------------- /src/lib/aws/aws.constant.ts: -------------------------------------------------------------------------------- 1 | export const AWS_TOKEN = 'AWS_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/aws/aws.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { AWS_TOKEN } from './aws.constant'; 4 | 5 | export const InjectAWS = () => Inject(AWS_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/aws/aws.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class AWSConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | AWS_ACCESS_KEY_ID!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | AWS_SECRET_ACCESS_KEY!: string; 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | AWS_REGION!: string; 15 | 16 | @IsNotEmpty() 17 | @IsString() 18 | AWS_S3_BUCKET!: string; 19 | 20 | @IsNotEmpty() 21 | @IsString() 22 | AWS_S3_PREFIX!: string; 23 | 24 | @IsOptional() 25 | @IsString() 26 | AWS_DYNAMODB_PREFIX!: string; // For dynamodb prefix table 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/aws/aws.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { AWSProvider } from './aws.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [AWSProvider], 8 | exports: [AWSProvider] 9 | }) 10 | export class AWSModule {} 11 | -------------------------------------------------------------------------------- /src/lib/aws/aws.provider.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '../config'; 2 | import { AWSLib } from './aws'; 3 | import { AWS_TOKEN } from './aws.constant'; 4 | import { AWSConfig } from './aws.dto'; 5 | 6 | export const AWSProvider = { 7 | inject: [ConfigService], 8 | provide: AWS_TOKEN, 9 | useFactory: (configService: ConfigService) => { 10 | const config = configService.validate('AWSModule', AWSConfig); 11 | return new AWSLib(config); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/aws/aws.ts: -------------------------------------------------------------------------------- 1 | import { DataMapper } from '@aws/dynamodb-data-mapper'; 2 | import { Logger } from '@nestjs/common'; 3 | import * as AWS from 'aws-sdk'; 4 | 5 | import { AWSConfig } from './aws.dto'; 6 | 7 | export class AWSLib { 8 | public cognito: AWS.CognitoIdentityServiceProvider; 9 | public dynamodb: AWS.DynamoDB; 10 | public ec2: AWS.EC2; 11 | public elasticbeanstalk: AWS.ElasticBeanstalk; 12 | public metadata: AWS.MetadataService; 13 | public mapper: DataMapper; 14 | public s3: AWS.S3; 15 | public ses: AWS.SES; 16 | public sns: AWS.SNS; 17 | 18 | private logger: Logger = new Logger('AWSModule'); 19 | 20 | constructor(public readonly config: AWSConfig) { 21 | AWS.config.update({ 22 | accessKeyId: config.AWS_ACCESS_KEY_ID, 23 | secretAccessKey: config.AWS_SECRET_ACCESS_KEY, 24 | region: config.AWS_REGION 25 | }); 26 | 27 | this.cognito = new AWS.CognitoIdentityServiceProvider(); 28 | this.dynamodb = new AWS.DynamoDB(); 29 | this.ec2 = new AWS.EC2(); 30 | this.elasticbeanstalk = new AWS.ElasticBeanstalk(); 31 | this.metadata = new AWS.MetadataService(); 32 | this.s3 = new AWS.S3(); 33 | this.ses = new AWS.SES(); 34 | this.sns = new AWS.SNS(); 35 | this.mapper = new DataMapper({ 36 | client: this.dynamodb, // the SDK client used to execute operations 37 | tableNamePrefix: this.config.AWS_DYNAMODB_PREFIX // optionally, you can provide a table prefix to keep your dev and prod tables separate 38 | }); 39 | 40 | this.logger.log('AWS loaded'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/aws/index.ts: -------------------------------------------------------------------------------- 1 | export { AWSLib } from './aws'; 2 | export { AWSModule } from './aws.module'; 3 | export { InjectAWS } from './aws.decorator'; 4 | -------------------------------------------------------------------------------- /src/lib/config/config.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsNotEmpty, IsNumber } from 'class-validator'; 3 | 4 | export class ConfigDto { 5 | @IsNotEmpty() 6 | NODE_ENV!: string; 7 | 8 | @IsNotEmpty() 9 | @IsNumber() 10 | @Transform(x => +x.value) 11 | PORT!: number; 12 | 13 | @IsNotEmpty() 14 | JWT_SECRET!: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { ConfigModule as _ConfigModule } from '@nestjs/config'; 3 | 4 | import { ConfigService } from './config.service'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [_ConfigModule.forRoot({})], 9 | providers: [ConfigService], 10 | exports: [ConfigService] 11 | }) 12 | export class ConfigModule {} 13 | -------------------------------------------------------------------------------- /src/lib/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { plainToClass } from 'class-transformer'; 3 | import { validateSync } from 'class-validator'; 4 | 5 | import { ConfigDto } from './config.dto'; 6 | 7 | @Injectable() 8 | export class ConfigService { 9 | readonly env: ConfigDto; 10 | private readonly envConfig: { [prop: string]: string }; 11 | 12 | constructor() { 13 | this.envConfig = process.env as any; 14 | this.env = this.validate('ConfigModule', ConfigDto); 15 | } 16 | 17 | get(key: string): string { 18 | return this.envConfig[key]; 19 | } 20 | 21 | validate(module: string, className: new () => T): T { 22 | const config = plainToClass(className as any, this.envConfig); 23 | const errors = validateSync(config as any, { 24 | whitelist: true, 25 | transform: true, 26 | forbidNonWhitelisted: false 27 | }); 28 | if (errors.length > 0) { 29 | errors.forEach(e => 30 | Logger.error(`${e.constraints![Object.keys(e.constraints!)[0]]}`, undefined, module) 31 | ); 32 | throw new Error(`${module}: Invalid environment config.`); 33 | } 34 | return config as any; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/config/index.ts: -------------------------------------------------------------------------------- 1 | export { ConfigModule } from './config.module'; 2 | export { ConfigService } from './config.service'; 3 | -------------------------------------------------------------------------------- /src/lib/crypto/crypto.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class CryptoConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | CRYPTO_ENCRYPTION_KEY!: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/crypto/crypto.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { CryptoProvider } from './crypto.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [CryptoProvider], 8 | exports: [CryptoProvider] 9 | }) 10 | export class CryptoModule {} 11 | -------------------------------------------------------------------------------- /src/lib/crypto/crypto.provider.ts: -------------------------------------------------------------------------------- 1 | import { Provider } from '@nestjs/common'; 2 | 3 | import { ConfigService } from '../config'; 4 | import { CryptoConfig } from './crypto.dto'; 5 | import { CryptoService } from './crypto.service'; 6 | 7 | export const CryptoProvider: Provider = { 8 | inject: [ConfigService], 9 | provide: CryptoService, 10 | useFactory: async (configService: ConfigService) => { 11 | const config = configService.validate('CryptoModule', CryptoConfig); 12 | return new CryptoService(config); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/crypto/crypto.service.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto'; 2 | 3 | import { CryptoConfig } from './crypto.dto'; 4 | 5 | /** 6 | * Encryption and Decryption 7 | * @see https://cryptobook.nakov.com 8 | */ 9 | export class CryptoService { 10 | constructor(private readonly config: CryptoConfig) {} 11 | 12 | /** 13 | * Encryption using `aes-256-gcm` 14 | * @see https://gist.github.com/AndiDittrich/4629e7db04819244e843 15 | */ 16 | encrypt(text: string) { 17 | // random initialization vector 18 | const iv = crypto.randomBytes(16); 19 | 20 | // random salt 21 | const salt = crypto.randomBytes(64); 22 | 23 | // derive key: 32 byte key length - in assumption the masterkey is a cryptographic and NOT a password there is no need for 24 | // a large number of iterations. It may can replaced by HKDF 25 | const key = crypto.pbkdf2Sync(this.config.CRYPTO_ENCRYPTION_KEY, salt, 2145, 32, 'sha512'); 26 | 27 | // AES 256 GCM Mode 28 | const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); 29 | 30 | // encrypt the given text 31 | const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]); 32 | 33 | // extract the auth tag 34 | const tag = cipher.getAuthTag(); 35 | 36 | // generate output 37 | return Buffer.concat([salt, iv, tag, encrypted]).toString('base64'); 38 | } 39 | 40 | /** 41 | * Decryption using `aes-256-gcm` 42 | */ 43 | decrypt(cipherText: string) { 44 | // base64 decoding 45 | const bData = Buffer.from(cipherText, 'base64'); 46 | 47 | // convert data to buffers 48 | const salt = bData.slice(0, 64); 49 | const iv = bData.slice(64, 80); 50 | const tag = bData.slice(80, 96); 51 | const text = bData.slice(96); 52 | 53 | // derive key using; 32 byte key length 54 | const key = crypto.pbkdf2Sync(this.config.CRYPTO_ENCRYPTION_KEY, salt, 2145, 32, 'sha512'); 55 | 56 | // AES 256 GCM Mode 57 | const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv); 58 | decipher.setAuthTag(tag); 59 | 60 | // encrypt the given text (NOTE: text as any) 61 | const decrypted = decipher.update(text as any, 'base64', 'utf8') + decipher.final('utf8'); 62 | 63 | return decrypted; 64 | } 65 | 66 | /** 67 | * NodeJS create md5 hash from string 68 | */ 69 | createMD5Hex(data: string) { 70 | return crypto.createHash('md5').update(data).digest('hex'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/crypto/index.ts: -------------------------------------------------------------------------------- 1 | export { CryptoModule } from './crypto.module'; 2 | export { CryptoService } from './crypto.service'; 3 | -------------------------------------------------------------------------------- /src/lib/firebase-admin/firebase-admin.constant.ts: -------------------------------------------------------------------------------- 1 | export const FIREBASE_ADMIN_TOKEN = 'FIREBASE_ADMIN_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/firebase-admin/firebase-admin.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { FIREBASE_ADMIN_TOKEN } from './firebase-admin.constant'; 4 | 5 | export const InjectFirebaseAdmin = () => Inject(FIREBASE_ADMIN_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/firebase-admin/firebase-admin.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class FirebaseAdminConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | FIREBASE_DATABASE_URL!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | FIREBASE_CREDENTIAL_PATH!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/firebase-admin/firebase-admin.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { FirebaseAdminProvider } from './firebase-admin.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [FirebaseAdminProvider], 8 | exports: [FirebaseAdminProvider] 9 | }) 10 | export class FirebaseAdminModule {} 11 | -------------------------------------------------------------------------------- /src/lib/firebase-admin/firebase-admin.provider.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin'; 2 | import { existsSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | 5 | import { ConfigService } from '../config'; 6 | import { FIREBASE_ADMIN_TOKEN } from './firebase-admin.constant'; 7 | import { FirebaseAdminConfig } from './firebase-admin.dto'; 8 | 9 | export const FirebaseAdminProvider = { 10 | inject: [ConfigService], 11 | provide: FIREBASE_ADMIN_TOKEN, 12 | useFactory: (configService: ConfigService) => { 13 | const config = configService.validate('FirebaseAdminModule', FirebaseAdminConfig); 14 | 15 | const path = resolve('.', config.FIREBASE_CREDENTIAL_PATH); 16 | if (!existsSync(path)) throw new Error(`Unknown file ${path}`); 17 | 18 | try { 19 | return admin.initializeApp({ 20 | credential: admin.credential.cert(path), 21 | databaseURL: config.FIREBASE_DATABASE_URL 22 | }); 23 | } catch (error) { 24 | return admin.app(); // This will prevent error when using HMR 25 | } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/firebase-admin/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectFirebaseAdmin } from './firebase-admin.decorator'; 2 | export { FirebaseAdminModule } from './firebase-admin.module'; 3 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/google-cloud-storage.constant.ts: -------------------------------------------------------------------------------- 1 | export const GOOGLE_CLOUD_STORAGE_TOKEN = 'GOOGLE_CLOUD_STORAGE_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/google-cloud-storage.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { GOOGLE_CLOUD_STORAGE_TOKEN } from './google-cloud-storage.constant'; 4 | 5 | export const InjectGoogleCloudStorage = () => Inject(GOOGLE_CLOUD_STORAGE_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/google-cloud-storage.dto.ts: -------------------------------------------------------------------------------- 1 | import { Allow, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | import { T } from '@common'; 4 | 5 | export class GoogleCloudStorageConfig { 6 | @IsNotEmpty() 7 | @IsString() 8 | GOOGLE_CLOUD_STORAGE_KEY_FILENAME_PATH!: string; 9 | 10 | @IsNotEmpty() 11 | @IsString() 12 | GOOGLE_CLOUD_STORAGE_BUCKET_NAME!: string; 13 | 14 | @Allow() 15 | private SIZE = new Map([ 16 | // ------- 17 | ['df', '.'], 18 | ['lg', '_l.'], 19 | ['md', '_m.'], 20 | ['sm', '_s.'], 21 | ['xs', '_t.'] 22 | ]); 23 | 24 | get GOOGLE_CLOUD_BASE_URL() { 25 | return `https://storage.googleapis.com/${this.GOOGLE_CLOUD_STORAGE_BUCKET_NAME}`; 26 | } 27 | 28 | get GOOGLE_CLOUD_IMAGE_URL() { 29 | return `https://storage.googleapis.com/${this.GOOGLE_CLOUD_STORAGE_BUCKET_NAME}/image/`; 30 | } 31 | 32 | getTempUrl(fileName: string) { 33 | return `${this.GOOGLE_CLOUD_BASE_URL}/temp/${fileName}?ignoreCache=1`; 34 | } 35 | 36 | getCloudUrl(image: T.ImageType, fileName: string, size?: T.ImageSize) { 37 | if (!fileName) return ''; 38 | 39 | const s = this.SIZE.get(size || 'df')!; 40 | const file = fileName.replace('.', s); 41 | return `${this.GOOGLE_CLOUD_BASE_URL}/${image}/${file}`; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/google-cloud-storage.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { GoogleCloudStorageProvider } from './google-cloud-storage.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [GoogleCloudStorageProvider], 8 | exports: [GoogleCloudStorageProvider] 9 | }) 10 | export class GoogleCloudStorageModule {} 11 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/google-cloud-storage.provider.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '../config'; 2 | import { GoogleCloudStorage } from './google-cloud-storage'; 3 | import { GOOGLE_CLOUD_STORAGE_TOKEN } from './google-cloud-storage.constant'; 4 | import { GoogleCloudStorageConfig } from './google-cloud-storage.dto'; 5 | 6 | export const GoogleCloudStorageProvider = { 7 | inject: [ConfigService], 8 | provide: GOOGLE_CLOUD_STORAGE_TOKEN, 9 | useFactory: (configService: ConfigService) => { 10 | const config = configService.validate('GoogleCloudStorageModule', GoogleCloudStorageConfig); 11 | return new GoogleCloudStorage(config); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/google-cloud-storage.ts: -------------------------------------------------------------------------------- 1 | import { Bucket, Storage } from '@google-cloud/storage'; 2 | import { existsSync } from 'fs'; 3 | import { resolve } from 'path'; 4 | 5 | import { GoogleCloudStorageConfig } from './google-cloud-storage.dto'; 6 | 7 | export class GoogleCloudStorage { 8 | readonly bucket: Bucket; 9 | readonly storage: Storage; 10 | readonly config: GoogleCloudStorageConfig; 11 | 12 | constructor(config: GoogleCloudStorageConfig) { 13 | const keyFilename = resolve('.', config.GOOGLE_CLOUD_STORAGE_KEY_FILENAME_PATH); 14 | if (!existsSync(keyFilename)) throw new Error(`Unknown file ${keyFilename}`); 15 | 16 | this.config = config; 17 | this.storage = new Storage({ keyFilename }); 18 | this.bucket = new Bucket(this.storage, config.GOOGLE_CLOUD_STORAGE_BUCKET_NAME); 19 | 20 | // OLD CODE 21 | // bucket.upload('https://example.com/images/image.png', function(err, file, res) { 22 | // handle upload... 23 | // }); 24 | 25 | // NEW CODE 26 | // const request = require('request'); 27 | // const file = bucket.file(name); 28 | // const writeStream = file.createWriteStream(); 29 | // request(url).pipe(writeStream); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/google-cloud-storage/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectGoogleCloudStorage } from './google-cloud-storage.decorator'; 2 | export { GoogleCloudStorageModule } from './google-cloud-storage.module'; 3 | export { GoogleCloudStorage } from './google-cloud-storage'; 4 | -------------------------------------------------------------------------------- /src/lib/graphql-request/graphql-request.constant.ts: -------------------------------------------------------------------------------- 1 | export const GRAPHQL_REQUEST_TOKEN = 'GRAPHQL_REQUEST_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/graphql-request/graphql-request.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { GRAPHQL_REQUEST_TOKEN } from './graphql-request.constant'; 4 | 5 | export const InjectGraphQLRequest = () => Inject(GRAPHQL_REQUEST_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/graphql-request/graphql-request.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class GraphQLRequestConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | GRAPHQL_REQUEST_ENDPOINT!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | GRAPHQL_REQUEST_AUTHENTICATION!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/graphql-request/graphql-request.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { GraphQLRequestProvider } from './graphql-request.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [GraphQLRequestProvider], 8 | exports: [GraphQLRequestProvider] 9 | }) 10 | export class GraphQLRequestModule {} 11 | -------------------------------------------------------------------------------- /src/lib/graphql-request/graphql-request.provider.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request'; 2 | 3 | import { ConfigService } from '../config'; 4 | import { GraphQLRequest } from './graphql-request'; 5 | import { GRAPHQL_REQUEST_TOKEN } from './graphql-request.constant'; 6 | import { GraphQLRequestConfig } from './graphql-request.dto'; 7 | 8 | export const GraphQLRequestProvider = { 9 | inject: [ConfigService], 10 | provide: GRAPHQL_REQUEST_TOKEN, 11 | useFactory: (configService: ConfigService) => { 12 | const config = configService.validate('GraphQLRequestModule', GraphQLRequestConfig); 13 | const client = new GraphQLClient(config.GRAPHQL_REQUEST_ENDPOINT, { 14 | headers: { authorization: config.GRAPHQL_REQUEST_AUTHENTICATION } 15 | }); 16 | return new GraphQLRequest(client); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/graphql-request/graphql-request.ts: -------------------------------------------------------------------------------- 1 | import { gql, GraphQLClient } from 'graphql-request'; 2 | 3 | export class GraphQLRequest { 4 | constructor(private readonly client: GraphQLClient) {} 5 | 6 | /** 7 | * This is an example API, you should create you own API here 8 | * 9 | * @see https://rickandmortyapi.com/graphql 10 | */ 11 | async getCharacters(opt: GetCharactersOption) { 12 | const query = gql` 13 | query ($page: Int) { 14 | characters(page: $page) { 15 | info { 16 | count 17 | pages 18 | next 19 | prev 20 | } 21 | results { 22 | id 23 | name 24 | status 25 | species 26 | gender 27 | } 28 | } 29 | } 30 | `; 31 | 32 | return this.client.request(query, opt); 33 | } 34 | } 35 | 36 | interface GetCharactersOption { 37 | page: number; 38 | name?: string; 39 | gender?: string; 40 | } 41 | 42 | interface GetCharactersResult { 43 | characters: { 44 | info: { 45 | count: number; 46 | pages: number; 47 | next: number; 48 | prev: number; 49 | }; 50 | results: Array<{ 51 | id: string; 52 | name: string; 53 | status: string; 54 | species: string; 55 | gender: string; 56 | }>; 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/graphql-request/index.ts: -------------------------------------------------------------------------------- 1 | export { GraphQLRequest } from './graphql-request'; 2 | export { GraphQLRequestModule } from './graphql-request.module'; 3 | export { InjectGraphQLRequest } from './graphql-request.decorator'; 4 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.constant.ts: -------------------------------------------------------------------------------- 1 | export const I18NEXT_TOKEN = 'I18NEXT_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.converter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ---------------------------------------------------------------------- 3 | * I18next Type-Safe Converter Function 4 | * 5 | * @example Used in package.json by run the command `yarn i18next` 6 | * @summary A tool to generate typescript enum for i18next from json file 7 | * ---------------------------------------------------------------------- 8 | */ 9 | 10 | const { readdirSync, readFileSync, writeFileSync } = require('fs'); 11 | const { resolve } = require('path'); 12 | 13 | // Get list of files in the locales directory 14 | const directoryPath = resolve('.', 'assets', 'locales', 'en'); 15 | const files = readdirSync(directoryPath); 16 | 17 | const keys = []; 18 | files.forEach(file => { 19 | // Check if fileName is default namespace `translation` 20 | const namespace = file.replace('.json', ''); 21 | const defaultNamespace = namespace === 'translation'; 22 | 23 | // Read file and map json key combine with namespace 24 | const str = readFileSync(resolve(directoryPath, file)); 25 | const data = JSON.parse(str); 26 | const d = Object.keys(data).map(d => (defaultNamespace ? d : `${namespace}:${d}`)); 27 | keys.push(...d); 28 | }); 29 | 30 | // Text with initial information 31 | let text = `/* tslint:disable */ 32 | /* eslint-disable */ 33 | // This file was automatically generated and should not be edited. 34 | 35 | export type I18NextTranslate =`; 36 | 37 | keys.forEach(key => (text += `\n | '${key}'`)); 38 | text += ';'; 39 | 40 | // Write text into typing file back 41 | const resultFile = resolve('.', 'src', 'lib', 'i18next', 'i18next.typing.ts'); 42 | writeFileSync(resultFile, text); 43 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common'; 2 | 3 | export const I18Next = createParamDecorator( 4 | (args: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().i18n 5 | ); 6 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.helper.ts: -------------------------------------------------------------------------------- 1 | import { FormatFunction } from 'i18next'; 2 | 3 | export const format: FormatFunction = (value, format, lng) => { 4 | if (format === 'strong') return `${value}`; 5 | return value; 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { I18NextProvider } from './i18next.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [I18NextProvider], 8 | exports: [I18NextProvider] 9 | }) 10 | export class I18NextModule {} 11 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.provider.ts: -------------------------------------------------------------------------------- 1 | import { HttpAdapterHost } from '@nestjs/core'; 2 | 3 | import { I18NextLib } from './i18next'; 4 | import { I18NEXT_TOKEN } from './i18next.constant'; 5 | 6 | export const I18NextProvider = { 7 | inject: [HttpAdapterHost], 8 | provide: I18NEXT_TOKEN, 9 | useFactory: (httpAdapterHost: HttpAdapterHost) => { 10 | return new I18NextLib(httpAdapterHost); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/lib/i18next/i18next.typing.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // This file was automatically generated and should not be edited. 4 | 5 | export type I18NextTranslate = 'Hello' | 'MyName'; 6 | -------------------------------------------------------------------------------- /src/lib/i18next/index.ts: -------------------------------------------------------------------------------- 1 | import { i18n as _i18n } from 'i18next'; 2 | 3 | export { I18NextModule } from './i18next.module'; 4 | export { I18Next } from './i18next.decorator'; 5 | export * from './i18next.typing'; 6 | 7 | export { i18next as i18n } from './i18next'; // use this instead import i18next directly, cause es6 export are not working 8 | 9 | export type i18next = _i18n; 10 | -------------------------------------------------------------------------------- /src/lib/ioredis/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectIORedis, InjectIORedisPubSub } from './ioredis.decorator'; 2 | export { IORedisModule } from './ioredis.module'; 3 | -------------------------------------------------------------------------------- /src/lib/ioredis/ioredis.constant.ts: -------------------------------------------------------------------------------- 1 | export const IOREDIS_PUB_SUB_TOKEN = 'IOREDIS_PUB_SUB_INJECT_TOKEN'; 2 | export const IOREDIS_TOKEN = 'IOREDIS_INJECT_TOKEN'; 3 | -------------------------------------------------------------------------------- /src/lib/ioredis/ioredis.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { IOREDIS_PUB_SUB_TOKEN, IOREDIS_TOKEN } from './ioredis.constant'; 4 | 5 | export const InjectIORedis = () => Inject(IOREDIS_TOKEN); 6 | export const InjectIORedisPubSub = () => Inject(IOREDIS_PUB_SUB_TOKEN); 7 | -------------------------------------------------------------------------------- /src/lib/ioredis/ioredis.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsNotEmpty, IsNumber, IsString } from 'class-validator'; 3 | 4 | export class IORedisConfig { 5 | @IsNotEmpty() 6 | @IsString() 7 | REDIS_HOST!: string; 8 | 9 | @IsNotEmpty() 10 | @IsNumber() 11 | @Transform(x => +x.value) 12 | REDIS_PORT!: number; 13 | 14 | @IsNotEmpty() 15 | @IsString() 16 | REDIS_AUTH_PASS!: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/ioredis/ioredis.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { IORedisProvider, IORedisPubSubProvider } from './ioredis.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [IORedisProvider, IORedisPubSubProvider], 8 | exports: [IORedisProvider, IORedisPubSubProvider] 9 | }) 10 | export class IORedisModule {} 11 | -------------------------------------------------------------------------------- /src/lib/ioredis/ioredis.provider.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import * as Redis from 'ioredis'; 3 | 4 | import { ConfigService } from '../config'; 5 | import { IOREDIS_PUB_SUB_TOKEN, IOREDIS_TOKEN } from './ioredis.constant'; 6 | import { IORedisConfig } from './ioredis.dto'; 7 | 8 | /* 9 | |-------------------------------------------------------------------------- 10 | | References 11 | |-------------------------------------------------------------------------- 12 | | https://medium.com/@micah1powell/using-redis-keyspace-notifications-for-a-reminder-service-with-node-c05047befec3 13 | | 14 | */ 15 | 16 | const logger = new Logger('IORedisModule'); 17 | 18 | let redis: Redis.Redis; 19 | let subscriber: Redis.Redis; 20 | 21 | export const IORedisProvider = { 22 | inject: [ConfigService], 23 | provide: IOREDIS_TOKEN, 24 | useFactory: (configService: ConfigService) => { 25 | const config = configService.validate('IORedisModule', IORedisConfig); 26 | 27 | // This will prevent reinitialize redis and cause maximum redis connection 28 | if (redis) return redis; 29 | 30 | redis = new Redis({ 31 | host: config.REDIS_HOST, 32 | port: config.REDIS_PORT, 33 | password: config.REDIS_AUTH_PASS 34 | }); 35 | 36 | redis.on('error', e => logger.error(e.message, e.stack)); 37 | redis.on('end', () => logger.warn('Ended')); 38 | redis.on('reconnecting', () => logger.log('Reconnecting')); 39 | redis.on('connect', () => logger.log('Connecting')); 40 | redis.on('ready', () => redis.config('SET', 'notify-keyspace-events', 'Ex')); // ! Important 41 | redis.on('ready', () => logger.log('Connected')); 42 | return redis; 43 | } 44 | }; 45 | 46 | export const IORedisPubSubProvider = { 47 | inject: [ConfigService], 48 | provide: IOREDIS_PUB_SUB_TOKEN, 49 | useFactory: (configService: ConfigService) => { 50 | const config = configService.validate('IORedisModule:PubSub', IORedisConfig); 51 | 52 | // This will prevent reinitialize redis and cause maximum redis connection 53 | if (subscriber) return subscriber; 54 | 55 | subscriber = new Redis({ 56 | host: config.REDIS_HOST, 57 | port: config.REDIS_PORT, 58 | password: config.REDIS_AUTH_PASS 59 | }); 60 | 61 | subscriber.on('ready', () => logger.log('PubSub Connected')); 62 | subscriber.subscribe('__keyevent@0__:expired'); // ! Important 63 | return subscriber; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/lib/jwt/index.ts: -------------------------------------------------------------------------------- 1 | export { JwtModule } from './jwt.module'; 2 | -------------------------------------------------------------------------------- /src/lib/jwt/jwt.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { JwtModule as NestJwtModule } from '@nestjs/jwt'; 3 | 4 | import { ConfigService } from '@lib/config'; 5 | 6 | @Global() 7 | @Module({ 8 | exports: [NestJwtModule], 9 | imports: [ 10 | NestJwtModule.registerAsync({ 11 | inject: [ConfigService], 12 | useFactory: async (configService: ConfigService) => ({ secret: configService.env.JWT_SECRET }) 13 | }) 14 | ] 15 | }) 16 | export class JwtModule {} 17 | -------------------------------------------------------------------------------- /src/lib/keycloak/index.ts: -------------------------------------------------------------------------------- 1 | export { KeycloakModule } from './keycloak.module'; 2 | export { KeycloakPayload, KeycloakUser, InjectKeycloakClient } from './keycloak.decorator'; 3 | export { KEYCLOAK_STRATEGY_NAME } from './keycloak.constant'; 4 | -------------------------------------------------------------------------------- /src/lib/keycloak/keycloak.constant.ts: -------------------------------------------------------------------------------- 1 | export const KEYCLOAK_STRATEGY_NAME = 'keycloak'; 2 | export const KEYCLOAK_TOKEN = 'KEYCLOAK_INJECT_TOKEN'; 3 | -------------------------------------------------------------------------------- /src/lib/keycloak/keycloak.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext, Inject } from '@nestjs/common'; 2 | 3 | import { KEYCLOAK_TOKEN } from './keycloak.constant'; 4 | 5 | export const InjectKeycloakClient = () => Inject(KEYCLOAK_TOKEN); 6 | 7 | export const KeycloakUser = createParamDecorator( 8 | (args: unknown, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user 9 | ); 10 | 11 | export interface KeycloakPayload { 12 | iss: string; 13 | sub: string; 14 | aud: string[]; 15 | iat: number; 16 | exp: number; 17 | azp: string; 18 | scope: string; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/keycloak/keycloak.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class AuthConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | KEYCLOAK_DOMAIN!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | KEYCLOAK_AUDIENCE!: string; 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | KEYCLOAK_REALM!: string; 15 | 16 | @IsNotEmpty() 17 | @IsString() 18 | KEYCLOAK_USERNAME!: string; 19 | 20 | @IsNotEmpty() 21 | @IsString() 22 | KEYCLOAK_PASSWORD!: string; 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/keycloak/keycloak.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PassportModule } from '@nestjs/passport'; 3 | 4 | import { KeycloakClientProvider, KeycloakStrategyProvider } from './keycloak.provider'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [PassportModule], 9 | providers: [KeycloakClientProvider, KeycloakStrategyProvider], 10 | exports: [KeycloakClientProvider, KeycloakStrategyProvider] 11 | }) 12 | export class KeycloakModule {} 13 | -------------------------------------------------------------------------------- /src/lib/keycloak/keycloak.provider.ts: -------------------------------------------------------------------------------- 1 | import Keycloak from '@keycloak/keycloak-admin-client'; 2 | import { Provider } from '@nestjs/common'; 3 | import { Issuer } from 'openid-client'; 4 | 5 | import { ConfigService } from '../config'; 6 | import { KEYCLOAK_TOKEN } from './keycloak.constant'; 7 | import { AuthConfig } from './keycloak.dto'; 8 | import { KeycloakStrategy } from './keycloak.strategy'; 9 | 10 | export const KeycloakStrategyProvider: Provider = { 11 | inject: [ConfigService], 12 | provide: KeycloakStrategy, 13 | useFactory: async (configService: ConfigService) => { 14 | const config = configService.validate('KeycloakModule', AuthConfig); 15 | return new KeycloakStrategy(config); 16 | } 17 | }; 18 | 19 | export const KeycloakClientProvider: Provider = { 20 | inject: [ConfigService], 21 | provide: KEYCLOAK_TOKEN, 22 | useFactory: async (configService: ConfigService) => { 23 | const config = configService.validate('KeycloakClient', AuthConfig); 24 | 25 | const baseUrl = `http://${config.KEYCLOAK_DOMAIN}/auth`; 26 | const kc = new Keycloak({ baseUrl, realmName: config.KEYCLOAK_REALM }); 27 | // Authorize with username / password 28 | // await kc.auth({ 29 | // username: config.KEYCLOAK_USERNAME, 30 | // password: config.KEYCLOAK_PASSWORD, 31 | // grantType: 'password', 32 | // clientId: 'admin-cli' 33 | // }); 34 | 35 | const keycloakIssuer = await Issuer.discover(`${baseUrl}/realms/${config.KEYCLOAK_REALM}`); 36 | const client = new keycloakIssuer.Client({ client_id: 'admin-cli', client_secret: '123' }); 37 | const tokenSet = await client.grant({ 38 | grant_type: 'password', 39 | username: config.KEYCLOAK_USERNAME, 40 | password: config.KEYCLOAK_PASSWORD 41 | }); 42 | 43 | kc.setAccessToken(tokenSet.access_token!); 44 | setInterval(async () => { 45 | const ts = await client.refresh(tokenSet.refresh_token!); 46 | kc.setAccessToken(ts.access_token!); 47 | }, 58 * 1000); // 58 seconds 48 | 49 | return kc; 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/keycloak/keycloak.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { passportJwtSecret } from 'jwks-rsa'; 4 | import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt'; 5 | 6 | import { KEYCLOAK_STRATEGY_NAME } from './keycloak.constant'; 7 | import { AuthConfig } from './keycloak.dto'; 8 | 9 | /** 10 | * 11 | * 12 | * @see https://dev.to/fullstack_to/use-auth0-to-secure-your-nestjs-application-mbo 13 | * @see https://stackoverflow.com/questions/53426069/getting-user-data-by-using-guards-roles-jwt 14 | */ 15 | @Injectable() 16 | export class KeycloakStrategy extends PassportStrategy(Strategy, KEYCLOAK_STRATEGY_NAME) { 17 | constructor(config: AuthConfig) { 18 | const domainUri = `http://${config.KEYCLOAK_DOMAIN}/auth/realms/${config.KEYCLOAK_REALM}`; 19 | super({ 20 | secretOrKeyProvider: passportJwtSecret({ 21 | cache: true, 22 | jwksRequestsPerMinute: 5, 23 | jwksUri: `${domainUri}/protocol/openid-connect/certs`, 24 | rateLimit: true 25 | }), 26 | algorithm: 'RS256', 27 | audience: config.KEYCLOAK_AUDIENCE, 28 | issuer: domainUri, 29 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken() 30 | }); 31 | } 32 | 33 | validate(payload: any, done: VerifiedCallback) { 34 | if (!payload) { 35 | done(new UnauthorizedException('Sorry, we were unable to process your request.'), false); 36 | } 37 | return done(null, payload); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/mailer/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectMailer } from './mailer.decorator'; 2 | export { Mailer } from './mailer'; 3 | export { MailerModule } from './mailer.module'; 4 | -------------------------------------------------------------------------------- /src/lib/mailer/mailer.constant.ts: -------------------------------------------------------------------------------- 1 | export const MAILER_TOKEN = 'MAILER_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/mailer/mailer.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { MAILER_TOKEN } from './mailer.constant'; 4 | 5 | export const InjectMailer = () => Inject(MAILER_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/mailer/mailer.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsIn, IsNotEmpty, IsString, ValidateIf } from 'class-validator'; 2 | 3 | export class MailerConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | @IsIn(['ethereal', 'gmail', 'mandrill']) 7 | MAILER_TYPE!: 'ethereal' | 'gmail' | 'mandrill'; 8 | 9 | // ============================================ 10 | // ETHEREAL EMAIL 11 | // ============================================ 12 | @IsNotEmpty() 13 | @IsString() 14 | @ValidateIf(o => o.MAILER_TYPE === 'ethereal') 15 | MAILER_ETHEREAL_USERNAME!: string; 16 | 17 | @IsNotEmpty() 18 | @IsString() 19 | @ValidateIf(o => o.MAILER_TYPE === 'ethereal') 20 | MAILER_ETHEREAL_PASSWORD!: string; 21 | 22 | // ============================================ 23 | // GMAIL EMAIL 24 | // ============================================ 25 | @IsNotEmpty() 26 | @IsString() 27 | @ValidateIf(o => o.MAILER_TYPE === 'gmail') 28 | MAILER_GMAIL_USERNAME!: string; 29 | 30 | @IsNotEmpty() 31 | @IsString() 32 | @ValidateIf(o => o.MAILER_TYPE === 'gmail') 33 | MAILER_GMAIL_PASSWORD!: string; 34 | 35 | // ============================================ 36 | // MANDRILL EMAIL 37 | // ============================================ 38 | @IsNotEmpty() 39 | @IsString() 40 | @ValidateIf(o => o.MAILER_TYPE === 'mandrill') 41 | MAILER_MANDRILL_API_KEY!: string; 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/mailer/mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { mailerProvider } from './mailer.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [mailerProvider], 8 | exports: [mailerProvider] 9 | }) 10 | export class MailerModule {} 11 | -------------------------------------------------------------------------------- /src/lib/mailer/mailer.provider.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '../config'; 2 | import { Mailer } from './mailer'; 3 | import { MAILER_TOKEN } from './mailer.constant'; 4 | import { MailerConfig } from './mailer.dto'; 5 | 6 | export const mailerProvider = { 7 | inject: [ConfigService], 8 | provide: MAILER_TOKEN, 9 | useFactory: (configService: ConfigService) => { 10 | const config = configService.validate('MailerModule', MailerConfig); 11 | return new Mailer(config); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/media-stream/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectMediaStream } from './media-stream.decorator'; 2 | export { MediaStream } from './media-stream'; 3 | export { MediaStreamModule } from './media-stream.module'; 4 | -------------------------------------------------------------------------------- /src/lib/media-stream/media-stream.constant.ts: -------------------------------------------------------------------------------- 1 | export const MEDIA_STREAM_TOKEN = 'MEDIA_STREAM_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/media-stream/media-stream.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { MEDIA_STREAM_TOKEN } from './media-stream.constant'; 4 | 5 | export const InjectMediaStream = () => Inject(MEDIA_STREAM_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/media-stream/media-stream.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty } from 'class-validator'; 2 | 3 | export class MediaStreamConfig { 4 | @IsNotEmpty() 5 | MEDIA_STREAM_HTTP_PORT!: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/media-stream/media-stream.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { mediaStreamProvider } from './media-stream.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [mediaStreamProvider], 8 | exports: [mediaStreamProvider] 9 | }) 10 | export class MediaStreamModule {} 11 | -------------------------------------------------------------------------------- /src/lib/media-stream/media-stream.provider.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '../config'; 2 | import { MediaStream } from './media-stream'; 3 | import { MEDIA_STREAM_TOKEN } from './media-stream.constant'; 4 | import { MediaStreamConfig } from './media-stream.dto'; 5 | 6 | export const mediaStreamProvider = { 7 | inject: [ConfigService], 8 | provide: MEDIA_STREAM_TOKEN, 9 | useFactory: (configService: ConfigService) => { 10 | const config = configService.validate('MediaStreamModule', MediaStreamConfig); 11 | return new MediaStream(config); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/mongoose/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectModel } from './mongoose.decorator'; 2 | export { MongooseModule } from './mongoose.module'; 3 | -------------------------------------------------------------------------------- /src/lib/mongoose/mongoose.constant.ts: -------------------------------------------------------------------------------- 1 | export const MONGOOSE_TOKEN = 'MONGOOSE_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/mongoose/mongoose.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | import { Model } from 'mongoose'; 3 | 4 | import { getModelToken } from './mongoose.util'; 5 | 6 | export const InjectModel = (model: Model) => Inject(getModelToken(model.modelName)); 7 | -------------------------------------------------------------------------------- /src/lib/mongoose/mongoose.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class MongooseConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | MONGO_URI!: string; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/mongoose/mongoose.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { MongooseProvider, SchemaProviders } from './mongoose.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [MongooseProvider, ...SchemaProviders], 8 | exports: [MongooseProvider, ...SchemaProviders] 9 | }) 10 | export class MongooseModule {} 11 | -------------------------------------------------------------------------------- /src/lib/mongoose/mongoose.provider.ts: -------------------------------------------------------------------------------- 1 | import { FactoryProvider, Logger } from '@nestjs/common'; 2 | import * as mongoose from 'mongoose'; 3 | 4 | import * as schema from '@schemas'; 5 | 6 | import { ConfigService } from '../config'; 7 | import { MONGOOSE_TOKEN } from './mongoose.constant'; 8 | import { MongooseConfig } from './mongoose.dto'; 9 | import { getModelToken } from './mongoose.util'; 10 | 11 | const logger = new Logger('MongooseModule'); 12 | 13 | export const MongooseProvider = { 14 | inject: [ConfigService], 15 | provide: MONGOOSE_TOKEN, 16 | useFactory: async (configService: ConfigService) => { 17 | const { MONGO_URI } = configService.validate('MongooseModule', MongooseConfig); 18 | // https://mongoosejs.com/docs/deprecations 19 | await mongoose.connect(MONGO_URI, { 20 | useNewUrlParser: true, 21 | useUnifiedTopology: true, 22 | useFindAndModify: false, 23 | useCreateIndex: true 24 | }); 25 | 26 | if (mongoose.connection.readyState === 1) { 27 | logger.log('Connection has been established successfully.'); 28 | } else { 29 | logger.error('Unable to connect to the database:'); 30 | } 31 | } 32 | }; 33 | 34 | export const SchemaProviders = Object.values(schema) 35 | .filter(x => x.prototype instanceof mongoose.Model) 36 | .map((model: any) => ({ 37 | provide: getModelToken(model.modelName), 38 | useFactory: () => model 39 | })); 40 | -------------------------------------------------------------------------------- /src/lib/mongoose/mongoose.util.ts: -------------------------------------------------------------------------------- 1 | export function getModelToken(model: string) { 2 | return `${model}ModelToken`; 3 | } 4 | -------------------------------------------------------------------------------- /src/lib/pagination/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pagination.interfaces'; 2 | export * from './pagination.class'; 3 | export * from './pagination'; 4 | export * from './pagination.dto'; 5 | -------------------------------------------------------------------------------- /src/lib/pagination/pagination.class.ts: -------------------------------------------------------------------------------- 1 | import { Type } from '@nestjs/common'; 2 | import { ApiResponse, ApiResponseProperty } from '@nestjs/swagger'; 3 | 4 | import { IPaginationLinks, IPaginationMeta } from './pagination.interfaces'; 5 | 6 | export class Pagination { 7 | constructor( 8 | /** 9 | * a list of items to be returned 10 | */ 11 | public readonly data: PaginationObject[], 12 | /** 13 | * associated meta information (e.g., counts) 14 | */ 15 | public readonly meta: IPaginationMeta, 16 | /** 17 | * associated links 18 | */ 19 | public readonly links: IPaginationLinks 20 | ) {} 21 | } 22 | 23 | export function PaginateResponse(classRef: Type) { 24 | @ApiResponse({}) 25 | abstract class Pagination { 26 | @ApiResponseProperty({ type: [classRef] }) 27 | data!: T[]; 28 | 29 | @ApiResponseProperty({ type: IPaginationMeta }) 30 | meta!: IPaginationMeta; 31 | 32 | @ApiResponseProperty({ type: IPaginationLinks }) 33 | links!: IPaginationLinks; 34 | } 35 | return Pagination as new () => Pagination; 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/pagination/pagination.dto.ts: -------------------------------------------------------------------------------- 1 | import { ApiPropertyOptional } from '@nestjs/swagger'; 2 | import { Transform } from 'class-transformer'; 3 | import { Max, Min } from 'class-validator'; 4 | 5 | /** 6 | * Transform input to number. default is 0. 7 | */ 8 | const TransformToNumber = () => Transform(v => (isNaN(+v) ? 0 : +v)); 9 | 10 | export class PaginationDto { 11 | @ApiPropertyOptional({ default: 10, description: 'Limit query data' }) 12 | @Min(0) 13 | @Max(100) 14 | @Transform(x => x.value || 1) // Prevented from query limit in mongodb, .limit(0) will return all from db 15 | @TransformToNumber() // Don't remove, look the comment above 16 | readonly limit: number = 10; 17 | 18 | @ApiPropertyOptional({ default: 0, description: 'Page query data' }) 19 | @Min(1) 20 | @TransformToNumber() 21 | readonly page: number = 1; 22 | 23 | get offset() { 24 | return this.limit * (this.page - 1); 25 | } 26 | 27 | // @ApiPropertyOptional({ default: 0, description: 'Offset query data ' }) 28 | // @Min(0) 29 | // @TransformToNumber() 30 | // readonly offset: number = 0; 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/pagination/pagination.interfaces.ts: -------------------------------------------------------------------------------- 1 | import { ApiResponseProperty } from '@nestjs/swagger'; 2 | 3 | export interface IPaginationOptions { 4 | /** 5 | * the amount of items to be requested per page 6 | */ 7 | limit: number; 8 | /** 9 | * the page that is requested 10 | */ 11 | page: number; 12 | /** 13 | * a basic route for generating links (i.e., WITHOUT query params) 14 | */ 15 | route?: string; 16 | } 17 | 18 | export abstract class IPaginationMeta { 19 | /** 20 | * the amount of items on this specific page 21 | */ 22 | @ApiResponseProperty({ example: 10 }) 23 | itemCount!: number; 24 | /** 25 | * the total amount of items 26 | */ 27 | @ApiResponseProperty({ example: 20 }) 28 | totalItems!: number; 29 | /** 30 | * the amount of items that were requested per page 31 | */ 32 | @ApiResponseProperty({ example: 10 }) 33 | itemsPerPage!: number; 34 | /** 35 | * the total amount of pages in this paginator 36 | */ 37 | @ApiResponseProperty({ example: 5 }) 38 | totalPages!: number; 39 | /** 40 | * the current page this paginator "points" to 41 | */ 42 | @ApiResponseProperty({ example: 1 }) 43 | currentPage!: number; 44 | } 45 | 46 | export abstract class IPaginationLinks { 47 | /** 48 | * a link to the "first" page 49 | */ 50 | @ApiResponseProperty({ example: 'http://cats.com/cats?limit=10&page=1' }) 51 | first?: string; 52 | /** 53 | * a link to the "previous" page 54 | */ 55 | @ApiResponseProperty({ example: 'http://cats.com/cats?limit=10&page=2' }) 56 | previous?: string; 57 | /** 58 | * a link to the "next" page 59 | */ 60 | @ApiResponseProperty({ example: 'http://cats.com/cats?limit=10&page=3' }) 61 | next?: string; 62 | /** 63 | * a link to the "last" page 64 | */ 65 | @ApiResponseProperty({ example: 'http://cats.com/cats?limit=10&page=5' }) 66 | last?: string; 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/pagination/pagination.ts: -------------------------------------------------------------------------------- 1 | import { stringify } from 'querystring'; 2 | 3 | import { Pagination } from './pagination.class'; 4 | import { IPaginationLinks, IPaginationMeta } from './pagination.interfaces'; 5 | 6 | /** 7 | * Pagination helper for response body 8 | * @see https://github.com/nestjsx/nestjs-typeorm-paginate 9 | */ 10 | export function paginate( 11 | items: any[], 12 | totalItems: number, 13 | currentPage: number, 14 | limit: number, 15 | query: any, 16 | route?: string 17 | ) { 18 | const totalPages = Math.ceil(totalItems / limit); 19 | 20 | const hasFirstPage = route; 21 | const hasPrevPage = route && currentPage > 1; 22 | const hasNextPage = route && currentPage < totalPages; 23 | const hasLastPage = route; 24 | 25 | // prettier-ignore 26 | const routes: IPaginationLinks = { 27 | first: hasFirstPage ? `${route}?${stringify({ ...query, page: 1 })}` : '', 28 | previous: hasPrevPage ? `${route}?${stringify({ ...query, page: currentPage - 1 })}` : '', 29 | next: hasNextPage ? `${route}?${stringify({ ...query, page: currentPage + 1 })}` : '', 30 | last: hasLastPage ? `${route}?${stringify({ ...query, page: totalPages })}` : '' 31 | }; 32 | 33 | const meta: IPaginationMeta = { 34 | totalItems, 35 | itemCount: items.length, 36 | itemsPerPage: limit, 37 | totalPages, 38 | currentPage 39 | }; 40 | return new Pagination(items, meta, routes); 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/sendbird/_base/index.ts: -------------------------------------------------------------------------------- 1 | export interface BaseLimitTokenOption { 2 | /** 3 | * Specifies a token that indicates the starting index of a chunk of results to retrieve. If not specified, the index is set as 0. 4 | */ 5 | token?: string; 6 | 7 | /** 8 | * Specifies the number of results to return per page. Acceptable values are 1 to 100, inclusive. (Default: 10) 9 | * 10 | * @default 10 11 | */ 12 | limit?: number; 13 | } 14 | 15 | export interface BaseNextOption { 16 | /** 17 | * The value for the `token` parameter to retrieve the next page in the result set. 18 | */ 19 | next: string; 20 | } 21 | 22 | export interface Metadata { 23 | [key: string]: any; 24 | } 25 | 26 | export interface Metacounter { 27 | [key: string]: number; 28 | } 29 | 30 | export type Language = 31 | | 'af' 32 | | 'sq' 33 | | 'am' 34 | | 'ar' 35 | | 'hy' 36 | | 'az' 37 | | 'eu' 38 | | 'be' 39 | | 'bn' 40 | | 'bs' 41 | | 'bg' 42 | | 'ca' 43 | | 'ceb' 44 | | 'zh-CN' 45 | | 'zh ' 46 | | 'zh-TW ' 47 | | 'co' 48 | | 'hr' 49 | | 'cs' 50 | | 'da' 51 | | 'nl' 52 | | 'en' 53 | | 'eo' 54 | | 'et' 55 | | 'fi' 56 | | 'fr' 57 | | 'fy' 58 | | 'gl' 59 | | 'ka' 60 | | 'de' 61 | | 'el' 62 | | 'gu' 63 | | 'ht' 64 | | 'ha' 65 | | 'haw' 66 | | 'he ' 67 | | 'iw' 68 | | 'hi' 69 | | 'hmn' 70 | | 'hu' 71 | | 'is' 72 | | 'ig' 73 | | 'id' 74 | | 'ga' 75 | | 'it' 76 | | 'ja' 77 | | 'jv' 78 | | 'kn' 79 | | 'kk' 80 | | 'km' 81 | | 'ko' 82 | | 'ku' 83 | | 'ky' 84 | | 'lo' 85 | | 'la' 86 | | 'lv' 87 | | 'lt' 88 | | 'lb' 89 | | 'mk' 90 | | 'mg' 91 | | 'ms' 92 | | 'ml' 93 | | 'mt' 94 | | 'mi' 95 | | 'mr' 96 | | 'mn' 97 | | 'my' 98 | | 'ne' 99 | | 'no' 100 | | 'ny' 101 | | 'ps' 102 | | 'fa' 103 | | 'pl' 104 | | 'pt' 105 | | 'pa' 106 | | 'ro' 107 | | 'ru' 108 | | 'sm' 109 | | 'gd' 110 | | 'sr' 111 | | 'st' 112 | | 'sn' 113 | | 'sd' 114 | | 'si' 115 | | 'sk' 116 | | 'sl' 117 | | 'so' 118 | | 'es' 119 | | 'su' 120 | | 'sw' 121 | | 'sv' 122 | | 'tl' 123 | | 'tg' 124 | | 'ta' 125 | | 'te' 126 | | 'th' 127 | | 'tr' 128 | | 'uk' 129 | | 'ur' 130 | | 'uz' 131 | | 'vi' 132 | | 'cy' 133 | | 'xh' 134 | | 'yi' 135 | | 'yo' 136 | | 'zu'; 137 | -------------------------------------------------------------------------------- /src/lib/sendbird/application/index.ts: -------------------------------------------------------------------------------- 1 | export * from './application.interface'; 2 | export * from './application.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/channel-metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './channel-metadata.interface'; 2 | export * from './channel-metadata.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/data-export/data-export.service.ts: -------------------------------------------------------------------------------- 1 | import { SendBirdHelper } from '../sendbird.helper'; 2 | import * as I from './data-export.interface'; 3 | 4 | export class DataExportService extends SendBirdHelper { 5 | /** 6 | * List data exports by message, channel, or user 7 | * 8 | * ​@description Retrieves a list of message, channel or user data exports 9 | * @see https://docs.sendbird.com/platform/data_export#3_list_data_exports_by_message_channel_or_user 10 | */ 11 | async list(params: I.ListOption) { 12 | const url = `export/${params.data_type}`; 13 | return this.wrapper(this.http.get(url, { params })); // prettier-ignore 14 | } 15 | 16 | /** 17 | * View a data export 18 | * 19 | * ​@description Retrieves information on a message, channel or user data export. 20 | * @see https://docs.sendbird.com/platform/data_export#3_view_a_data_export 21 | */ 22 | async view(params: I.ViewOption) { 23 | const url = `export/${params.data_type}/${params.request_id}`; 24 | return this.wrapper(this.http.get(url)); 25 | } 26 | 27 | /** 28 | * Register and schedule a data export 29 | * 30 | * ​@description Registers and schedules a message, channel, or user data export. 31 | * @see https://docs.sendbird.com/platform/data_export#3_register_and_schedule_a_data_export 32 | */ 33 | async register(params: I.RegisterOption) { 34 | const url = `export/${params.data_type}`; 35 | return this.wrapper(this.http.post(url, params)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/sendbird/data-export/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-export.interface'; 2 | export * from './data-export.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/data-privacy/data-privacy.service.ts: -------------------------------------------------------------------------------- 1 | import { SendBirdHelper } from '../sendbird.helper'; 2 | import * as I from './data-privacy.interface'; 3 | 4 | export class DataPrivacyService extends SendBirdHelper { 5 | /** 6 | * List GDPR requests 7 | * 8 | * ​@description Retrieves a list of GDPR requests of all types. 9 | * @see https://docs.sendbird.com/platform/data_privacy#3_list_gdpr_requests 10 | */ 11 | async list(params?: I.LisOption) { 12 | const url = `privacy/gdpr`; 13 | return this.wrapper(this.http.get(url, { params })); // prettier-ignore 14 | } 15 | 16 | /** 17 | * View a GDPR request 18 | * 19 | * ​@description Retrieves a specific GDPR request. 20 | * @see https://docs.sendbird.com/platform/data_privacy#3_view_a_gdpr_request 21 | */ 22 | async view(params: I.ViewOption) { 23 | const url = `privacy/gdpr/${params.request_id}`; 24 | return this.wrapper(this.http.get(url)); 25 | } 26 | 27 | /** 28 | * Register a GDPR request 29 | * 30 | * ​@description Registers a specific type of GDPR request to meet the GDPR's requirements. 31 | * @see https://docs.sendbird.com/platform/data_privacy#3_register_a_gdpr_request 32 | */ 33 | async register(params: I.RegisterOption) { 34 | const url = `privacy/gdpr`; 35 | return this.wrapper(this.http.post(url, params)); 36 | } 37 | 38 | /** 39 | * Unregister a GDPR request 40 | * 41 | * ​@description Unregisters a specific GDPR request. 42 | * @see https://docs.sendbird.com/platform/data_privacy#3_unregister_a_gdpr_request 43 | */ 44 | async unregister(params: I.UnregisterOption) { 45 | const url = `privacy/gdpr/${params.request_id}`; 46 | return this.wrapper(this.http.delete(url)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/sendbird/data-privacy/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data-privacy.interface'; 2 | export * from './data-privacy.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/group-channel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './group-channel.interface'; 2 | export * from './group-channel.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectSendBird, UseSendBirdWebhookGuard } from './sendbird.decorator'; 2 | export { SendBird } from './sendbird'; 3 | export { SendBirdModule } from './sendbird.module'; 4 | -------------------------------------------------------------------------------- /src/lib/sendbird/message/index.ts: -------------------------------------------------------------------------------- 1 | export * from './message.interface'; 2 | export * from './message.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/open-channel/index.ts: -------------------------------------------------------------------------------- 1 | export * from './open-channel.interface'; 2 | export * from './open-channel.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/report/index.ts: -------------------------------------------------------------------------------- 1 | export * from './report.interface'; 2 | export * from './report.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/sendbird.constant.ts: -------------------------------------------------------------------------------- 1 | export const SENDBIRD_TOKEN = 'SENDBIRD_INJECT_TOKEN'; 2 | export const SENDBIRD_WEBHOOK_EVENT = 'SendBirdWebhookEvent'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/sendbird.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject, UseGuards } from '@nestjs/common'; 2 | 3 | import { SENDBIRD_TOKEN } from './sendbird.constant'; 4 | import { SendBirdWebhookGuard } from './webhook'; 5 | 6 | export const InjectSendBird = () => Inject(SENDBIRD_TOKEN); 7 | export const UseSendBirdWebhookGuard = () => UseGuards(SendBirdWebhookGuard); 8 | -------------------------------------------------------------------------------- /src/lib/sendbird/sendbird.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class SendBirdConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | SENDBIRD_APP_ID!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | SENDBIRD_API_TOKEN!: string; 11 | 12 | @IsOptional() 13 | @IsString() 14 | SENDBIRD_AUTHORIZATION!: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/sendbird/sendbird.helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { AxiosResponse } from 'axios'; 3 | import * as FormData from 'form-data'; 4 | import { lastValueFrom, Observable } from 'rxjs'; 5 | 6 | export class SendBirdHelper { 7 | constructor(protected readonly http: HttpService) {} 8 | 9 | protected async wrapper(req: Observable>) { 10 | return lastValueFrom(req).then(x => x.data); 11 | } 12 | 13 | protected getFormData(data: { [key: string]: any }, fileName: string) { 14 | const headers = this.http.axiosRef.defaults.headers; 15 | if (!data[fileName]) return { data, headers }; 16 | 17 | const form = new FormData(); 18 | Object.entries(data).forEach(([key, value]) => form.append(key, value)); 19 | return { data: form, headers: { ...headers, ...form.getHeaders() } }; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/sendbird/sendbird.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | import { SendBirdProvider } from './sendbird.provider'; 5 | import { SendBirdWebhookGuard } from './webhook'; 6 | 7 | @Global() 8 | @Module({ 9 | imports: [HttpModule.register({})], 10 | providers: [SendBirdProvider, SendBirdWebhookGuard], 11 | exports: [SendBirdProvider, SendBirdWebhookGuard] 12 | }) 13 | export class SendBirdModule {} 14 | -------------------------------------------------------------------------------- /src/lib/sendbird/sendbird.provider.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | 3 | import { ConfigService } from '../config'; 4 | import { SendBird } from './sendbird'; 5 | import { SENDBIRD_TOKEN } from './sendbird.constant'; 6 | 7 | export const SendBirdProvider = { 8 | inject: [ConfigService, HttpService], 9 | provide: SENDBIRD_TOKEN, 10 | useFactory: (configService: ConfigService, http: HttpService) => new SendBird(configService, http) 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/sendbird/user-metadata/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user-metadata.interface'; 2 | export * from './user-metadata.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/user-metadata/user-metadata.interface.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from '../_base'; 2 | 3 | interface UserIdOption { 4 | /** 5 | * Specifies the ID of the user to retrieve the metadata in. 6 | */ 7 | user_id: string; 8 | } 9 | 10 | interface MetadataOption { 11 | /** 12 | * Specifies a JSON object that stores key-value items. 13 | * The key must not have a comma (,) and its length is limited to 128 bytes. 14 | * The value must be a string and its length is limited to 190 bytes. This property can have up to 5 items. 15 | */ 16 | metadata: Metadata; 17 | } 18 | 19 | interface MetadataKeyOption { 20 | /** 21 | * Specifies the key of metadata item to retrieve the values of. 22 | * If not specified, all items of the metadata are returned. 23 | * If specified, the item which matches the parameter value is returned. Urlencoding a key is recommended. 24 | */ 25 | key?: string; 26 | } 27 | 28 | export interface ViewOption extends UserIdOption, MetadataKeyOption { 29 | /** 30 | * In a query string, specifies an array of one or more keys of metadata items to retrieve the values of. 31 | * If not specified, all items of the metadata are returned. If specified, the items which match the parameter values are returned. 32 | * Urlencoding each key is recommended (for example, `?keys=urlencoded_key_1, urlencoded_key_2`). 33 | */ 34 | keys?: string; 35 | } 36 | 37 | export interface CreateOption extends UserIdOption, MetadataOption {} 38 | 39 | export interface UpdateOption extends UserIdOption, MetadataOption, MetadataKeyOption { 40 | /** 41 | * When updating a specific item by its key 42 | */ 43 | value?: any; 44 | 45 | /** 46 | * Determines whether to add new items in addition to updating existing items. 47 | * If `true`, new key-value items in the metadata property are added when there are no items with the keys. 48 | * The existing items are updated with new values when there are already items with the keys. 49 | * If `false`, only the items of which keys match the ones you specify are updated. (Default: `false`) 50 | */ 51 | upsert?: boolean; 52 | } 53 | 54 | export interface DeleteOption extends UserIdOption, MetadataKeyOption {} 55 | -------------------------------------------------------------------------------- /src/lib/sendbird/user-metadata/user-metadata.service.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from '../_base'; 2 | import { SendBirdHelper } from '../sendbird.helper'; 3 | import * as I from './user-metadata.interface'; 4 | 5 | export class UserMetadataService extends SendBirdHelper { 6 | /** 7 | * View a user metadata 8 | * 9 | * ​@description Retrieves a user metadata's one or more items that are stored in a user. 10 | * @see https://docs.sendbird.com/platform/user_metadata#3_view_a_user_metadata 11 | */ 12 | async view(params: I.ViewOption) { 13 | const url = `users/${params.user_id}/metadata/${params.key || ''}`; 14 | return this.wrapper(this.http.get(url, { params })); // prettier-ignore 15 | } 16 | 17 | /** 18 | * Create a user metadata 19 | * 20 | * ​@description Creates a user metadata's items to store in a user. 21 | * @see https://docs.sendbird.com/platform/user_metadata#3_create_a_user_metadata 22 | */ 23 | async create(params: I.CreateOption) { 24 | const url = `users/${params.user_id}/metadata`; 25 | return this.wrapper(this.http.post(url, params)); 26 | } 27 | 28 | /** 29 | * Update a user metadata 30 | * 31 | * ​@description Updates existing items of a user metadata by their keys, or adds new items to the metadata. 32 | * @see https://docs.sendbird.com/platform/user_metadata#3_update_a_user_metadata 33 | */ 34 | async update(params: I.UpdateOption) { 35 | const url = `users/${params.user_id}/metadata/${params.key || ''}`; 36 | return this.wrapper(this.http.put(url, params)); 37 | } 38 | 39 | /** 40 | * Delete a user metadata 41 | * 42 | * ​@description Deletes a user metadata's one or all items that are stored in a user. 43 | * @see https://docs.sendbird.com/platform/user_metadata#3_delete_a_user_metadata 44 | */ 45 | async delete(params: I.DeleteOption) { 46 | const url = `users/${params.user_id}/metadata`; 47 | return this.wrapper(this.http.delete(url)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/sendbird/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.interface'; 2 | export * from './user.service'; 3 | -------------------------------------------------------------------------------- /src/lib/sendbird/webhook/index.ts: -------------------------------------------------------------------------------- 1 | export * from './webhook.guard'; 2 | export * from './webhook.interface'; 3 | export * from './webhook.service'; 4 | -------------------------------------------------------------------------------- /src/lib/sendbird/webhook/webhook.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common'; 2 | import { createHmac } from 'crypto'; 3 | import { Request } from 'express'; 4 | 5 | import { ConfigService } from '../../config'; 6 | 7 | @Injectable() 8 | export class SendBirdWebhookGuard implements CanActivate { 9 | constructor(private readonly service: ConfigService) {} 10 | 11 | canActivate(context: ExecutionContext) { 12 | const req = context.switchToHttp().getRequest(); 13 | 14 | const userAgent = req.get('user-agent'); 15 | const signature = req.get('x-sendbird-signature'); 16 | 17 | if (!signature || userAgent !== 'sendbird') { 18 | throw new UnauthorizedException(`This request doesn't contain a SendBird signature`); 19 | } 20 | 21 | return this.compare(signature, req.body); 22 | } 23 | 24 | private compare(signature: string, payload: any) { 25 | const secret = this.service.get('SENDBIRD_API_TOKEN'); 26 | const digest = createHmac('sha256', secret).update(JSON.stringify(payload)).digest('hex'); // prettier-ignore 27 | if (signature !== digest) throw new UnauthorizedException('Request signature does not match'); 28 | 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/sendbird/webhook/webhook.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Webhook { 2 | /** 3 | * The information about the webhook configuration. 4 | */ 5 | webhook: { 6 | /** 7 | * Indicates whether webhooks are turned on in your SendBird application or not. (Default: false) 8 | */ 9 | enabled: boolean; 10 | 11 | /** 12 | * The URL of your webhook server to receive payloads for events. 13 | */ 14 | url: string; 15 | 16 | /** 17 | * Indicates whether to include the information on the members of group channels in payloads. (Default: false) 18 | */ 19 | include_members: boolean; 20 | 21 | /** 22 | * An array of subscribed events. 23 | */ 24 | enabled_events: string[]; 25 | 26 | /** 27 | * A list of all supported webhook events. 28 | */ 29 | all_webhook_categories: string[]; 30 | }; 31 | } 32 | 33 | export interface LisOption { 34 | /** 35 | * Determines whether to include a list of all supported webhook events as the `all_webhook_categories` property in the response. (Default: false) 36 | */ 37 | display_all_webhook_categories: boolean; 38 | } 39 | 40 | export interface UpdateOption { 41 | /** 42 | * Determines whether webhooks are turned on in your SendBird application or not. (Default: false) 43 | */ 44 | enabled?: boolean; 45 | 46 | /** 47 | * Specifies the URL of your webhook server to receive payloads for events. 48 | */ 49 | url?: string; 50 | 51 | /** 52 | * Determines whether to include the information on the members of group channels in payloads. (Default: false) 53 | */ 54 | include_members?: boolean; 55 | 56 | /** 57 | * Specifies an array of one or more events for your webhook server to subscribe to. 58 | * If set to an asterisk (`*`) only, the server will subscribe to all supported events. 59 | * If set to an empty array, the server will unsubscribe from all (which indicates turning off webhooks). 60 | */ 61 | enabled_events?: string[]; 62 | } 63 | -------------------------------------------------------------------------------- /src/lib/sendbird/webhook/webhook.service.ts: -------------------------------------------------------------------------------- 1 | import { SendBirdHelper } from '../sendbird.helper'; 2 | import * as I from './webhook.interface'; 3 | 4 | export class WebhookService extends SendBirdHelper { 5 | /** 6 | * Retrieve a list of subscribed events 7 | * 8 | * ​@description Retrieves a list of events for your webhook server to receive payloads for. 9 | * @see https://docs.sendbird.com/platform/webhooks#3_retrieve_a_list_of_subscribed_events 10 | */ 11 | async list(params?: I.LisOption) { 12 | const url = `applications/settings/webhook`; 13 | return this.wrapper(this.http.get(url, { params })); // prettier-ignore 14 | } 15 | 16 | /** 17 | * Choose which events to subscribe to 18 | * 19 | * ​@description Chooses which events for your webhook server to receive payloads for. By subscribing to specific events based on your own needs, you can control the number of HTTP requests to your webhook server. 20 | * @see https://docs.sendbird.com/platform/webhooks#3_choose_which_events_to_subscribe_to 21 | */ 22 | async update(params: I.UpdateOption) { 23 | const url = `applications/settings/webhook`; 24 | return this.wrapper(this.http.put(url, params)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/lib/sequelize/index.ts: -------------------------------------------------------------------------------- 1 | export { SequelizeModule } from './sequelize.module'; 2 | -------------------------------------------------------------------------------- /src/lib/sequelize/sequelize.constant.ts: -------------------------------------------------------------------------------- 1 | export const SEQUELIZE_TOKEN = 'SEQUELIZE_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/sequelize/sequelize.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { SEQUELIZE_TOKEN } from './sequelize.constant'; 4 | 5 | export const InjectSequelize = () => Inject(SEQUELIZE_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/sequelize/sequelize.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; 3 | import { Dialect } from 'sequelize'; 4 | 5 | export class SequelizeConfig { 6 | @IsNotEmpty() 7 | @IsString() 8 | DB_HOST!: string; 9 | 10 | @IsNotEmpty() 11 | @IsNumber() 12 | @Transform(x => +x.value) 13 | DB_PORT!: number; 14 | 15 | @IsNotEmpty() 16 | @IsString() 17 | DB_SCHEMA!: string; 18 | 19 | @IsNotEmpty() 20 | @IsString() 21 | DB_USERNAME!: string; 22 | 23 | @IsNotEmpty() 24 | @IsString() 25 | DB_PASSWORD!: string; 26 | 27 | @IsNotEmpty() 28 | @IsString() 29 | DB_CONNECTION!: Dialect; 30 | 31 | @IsNotEmpty() 32 | @IsBoolean() 33 | @Transform(x => String(x.value).toLowerCase() === 'true') 34 | DB_LOGGING!: boolean; 35 | 36 | @IsNotEmpty() 37 | @IsBoolean() 38 | @Transform(x => String(x.value).toLowerCase() === 'true') 39 | DB_SYNC!: boolean; 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/sequelize/sequelize.helper.ts: -------------------------------------------------------------------------------- 1 | import stack from 'callsites'; 2 | import { readFile } from 'fs'; 3 | import { dirname, resolve } from 'path'; 4 | import { QueryTypes, Sequelize, Transaction } from 'sequelize'; 5 | import * as format from 'string-template'; 6 | import { promisify } from 'util'; 7 | 8 | const readFileAsync = promisify(readFile); 9 | 10 | export interface RunQueryOption { 11 | replacements?: { [key: string]: any }; 12 | substitution?: { [key: string]: any }; 13 | transaction?: Transaction | any; 14 | plain?: boolean; // Return SELECT as a single row 15 | logging?: () => void; 16 | } 17 | 18 | /** 19 | * Read sql file. 20 | */ 21 | export async function ReadSQLFile(sqlPath: string) { 22 | // find dirname where this function was called. 23 | const caller = stack()[0].getFileName() || ''; 24 | const callerDirname = dirname(caller); 25 | 26 | // * ['../'] means we use this function from @common 27 | return readFileAsync(resolve(callerDirname, '../', sqlPath), 'utf8'); 28 | // return readFileAsync(resolve(callerDirname, sqlPath), 'utf8'); 29 | } 30 | 31 | /** 32 | * Execute an SQL query with the database. 33 | */ 34 | export async function RunQuery( 35 | db: Sequelize, 36 | query: string, 37 | options: RunQueryOption 38 | ): Promise { 39 | const { substitution, ...queryOptions } = options; 40 | const sql = format(query, substitution); 41 | return db.query(sql, { type: QueryTypes.SELECT, ...queryOptions }) as any; 42 | } 43 | 44 | /** 45 | * Read an SQL File and execute with the database. 46 | */ 47 | export async function RunSQLQuery( 48 | db: Sequelize, 49 | sqlPath: string, 50 | options: RunQueryOption 51 | ) { 52 | const query = await ReadSQLFile(sqlPath); 53 | return RunQuery(db, query, options); 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/sequelize/sequelize.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { SequelizeModule as _SequelizeModule } from '@nestjs/sequelize'; 3 | 4 | import * as Models from '@models'; 5 | 6 | import { SequelizeConfigService } from './sequelize.service'; 7 | 8 | @Global() 9 | @Module({ 10 | imports: [ 11 | _SequelizeModule.forRootAsync({ useClass: SequelizeConfigService }), 12 | _SequelizeModule.forFeature(Object.values(Models)) 13 | ], 14 | exports: [_SequelizeModule] 15 | }) 16 | export class SequelizeModule {} 17 | -------------------------------------------------------------------------------- /src/lib/sequelize/sequelize.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { SequelizeModuleOptions, SequelizeOptionsFactory } from '@nestjs/sequelize'; 3 | 4 | import { ConfigService } from '@lib/config'; 5 | 6 | import { SequelizeConfig } from './sequelize.dto'; 7 | 8 | @Injectable() 9 | export class SequelizeConfigService implements SequelizeOptionsFactory { 10 | constructor(private readonly configService: ConfigService) {} 11 | 12 | createSequelizeOptions(): SequelizeModuleOptions { 13 | const config = this.configService.validate('SequelizeModule--', SequelizeConfig); 14 | return { 15 | host: config.DB_HOST, 16 | port: config.DB_PORT, 17 | database: config.DB_SCHEMA, 18 | username: config.DB_USERNAME, 19 | password: config.DB_PASSWORD, 20 | dialect: config.DB_CONNECTION, 21 | logging: config.DB_LOGGING ? console.log : false, 22 | autoLoadModels: true, 23 | // synchronize: true, 24 | 25 | // Disable global timestamps: https://stackoverflow.com/questions/39587767/disable-updatedat-update-date-field-in-sequelize-js 26 | define: { timestamps: false }, 27 | 28 | dialectOptions: { 29 | // ! For MSSQL 30 | // encrypt: false, 31 | // connectTimeout: 60000, 32 | // requestTimeout: 60000 33 | 34 | // ! For MySQL 35 | decimalNumbers: true // Convert string decimal https://github.com/sequelize/sequelize/issues/8019, 36 | } 37 | }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/social/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectSocial } from './social.decorator'; 2 | export { SocialModule } from './social.module'; 3 | export { Social } from './social'; 4 | -------------------------------------------------------------------------------- /src/lib/social/social.constant.ts: -------------------------------------------------------------------------------- 1 | export const SOCIAL_TOKEN = 'SOCIAL_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/social/social.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { SOCIAL_TOKEN } from './social.constant'; 4 | 5 | export const InjectSocial = () => Inject(SOCIAL_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/social/social.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; 2 | 3 | export class SocialConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | SOCIAL_TYPE = ''; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | @ValidateIf(o => (o.SOCIAL_TYPE as string).includes('google')) 11 | GOOGLE_CLIENT_ID!: string; 12 | 13 | @IsNotEmpty() 14 | @IsString() 15 | @ValidateIf(o => (o.SOCIAL_TYPE as string).includes('twitter')) 16 | TWITTER_CONSUMER_KEY!: string; 17 | 18 | @IsNotEmpty() 19 | @IsString() 20 | @ValidateIf(o => (o.SOCIAL_TYPE as string).includes('twitter')) 21 | TWITTER_CONSUMER_SECRET!: string; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/social/social.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FacebookException { 2 | error: { 3 | message: string; 4 | type: string; 5 | code: number; 6 | fbtrace_id: string; 7 | }; 8 | } 9 | 10 | export interface FacebookResult { 11 | id: string; 12 | email: string; 13 | first_name: string; 14 | last_name: string; 15 | } 16 | 17 | export interface LinkedinException { 18 | errorCode: number; 19 | message: string; 20 | requestId: string; 21 | status: number; 22 | timestamp: number; 23 | } 24 | 25 | export interface LinkedinResult { 26 | id: string; 27 | firstName: string; 28 | formattedName: string; 29 | formattedPhoneticName: string; 30 | headline: string; 31 | lastName: string; 32 | maidenName: string; 33 | phoneticFirstName: string; 34 | phoneticLastName: string; 35 | pictureUrl: string; 36 | publicProfileUrl: string; 37 | // siteStandardProfileRequest 38 | // location; 39 | // industry; 40 | // positions; 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/social/social.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | import { socialProvider } from './social.provider'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [HttpModule], 9 | providers: [socialProvider], 10 | exports: [socialProvider] 11 | }) 12 | export class SocialModule {} 13 | -------------------------------------------------------------------------------- /src/lib/social/social.provider.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | 3 | import { ConfigService } from '../config'; 4 | import { Social } from './social'; 5 | import { SOCIAL_TOKEN } from './social.constant'; 6 | import { SocialConfig } from './social.dto'; 7 | 8 | export const socialProvider = { 9 | inject: [ConfigService, HttpService], 10 | provide: SOCIAL_TOKEN, 11 | useFactory: (configService: ConfigService, http: HttpService) => { 12 | const config = configService.validate('SocialModule', SocialConfig); 13 | return new Social(config, http); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/socket/auth-socket.gateway.ts: -------------------------------------------------------------------------------- 1 | import { Logger, UseFilters } from '@nestjs/common'; 2 | import { 3 | OnGatewayConnection, 4 | OnGatewayDisconnect, 5 | SubscribeMessage, 6 | WebSocketGateway, 7 | WebSocketServer 8 | } from '@nestjs/websockets'; 9 | import { Server, Socket } from 'socket.io'; 10 | 11 | import { ExceptionFilter } from './socket.filter'; 12 | 13 | @UseFilters(ExceptionFilter) 14 | @WebSocketGateway({ namespace: 'auth', transports: ['websocket'] }) 15 | export class AuthSocketGateway implements OnGatewayConnection, OnGatewayDisconnect { 16 | @WebSocketServer() 17 | private server!: Server; 18 | private logger = new Logger('AuthGateway'); 19 | 20 | handleConnection(socket: Socket) { 21 | // ! TODO: need to validate hash before join room, check VIP project for more info 22 | // const token = socket.handshake.query.token; 23 | // this.service 24 | // .validateHash(token) 25 | // .then(user => { 26 | // const { id, organizationId } = user; 27 | // socket.join(this.organizationRoom(organizationId)); 28 | // socket.join(this.userRoom(id)); 29 | // this.logger.log(`Socket ID: ${id}@${socket.id} connected!`); 30 | // }) 31 | // .catch(x => socket.disconnect()); 32 | } 33 | 34 | handleDisconnect(socket: Socket) { 35 | this.logger.log(`Socket ID: ${socket.id} disconnected!`); 36 | } 37 | 38 | @SubscribeMessage('WELCOME') 39 | onEvent() { 40 | return { data: 'WELCOME AUTH', event: 'WELCOME' }; 41 | } 42 | 43 | emitByUserId(userId: number, event: string, body: any) { 44 | this.server.to(this.userRoom(userId)).emit(event, body); 45 | } 46 | 47 | private userRoom = (userId: number) => `user:${userId}`; 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/socket/auth-socket.guard.ts: -------------------------------------------------------------------------------- 1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; 2 | import { WsException } from '@nestjs/websockets'; 3 | import { Socket } from 'socket.io'; 4 | 5 | // import { verify } from 'jsonwebtoken'; 6 | // import { promisify } from 'util'; 7 | 8 | // import { ConfigService } from '@lib/config'; 9 | 10 | @Injectable() 11 | export class AuthSocketGuard implements CanActivate { 12 | // constructor(private readonly config: ConfigService) {} 13 | 14 | async canActivate(context: ExecutionContext) { 15 | const socket = context.switchToWs().getClient(); 16 | const accessToken = socket.handshake.query.accessToken; 17 | 18 | if (!accessToken) throw new WsException('Missing token.'); 19 | 20 | // const decoded: any = await promisify(verify)(accessToken, this.config.env.JWT_SECRET).catch(e => { 21 | // throw new WsException(e.name + ' ' + e.message); 22 | // }); 23 | // const jwtDecoded: { id: string } = decoded; 24 | 25 | // ! IMPORTANT: performance considering 26 | // const user = await User.findById(jwtDecoded.id); 27 | // if (!user || user.status !== 'active') throw new WsException('Unknown User'); 28 | // socket.join(jwtDecoded.id); 29 | return true; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/socket/index.ts: -------------------------------------------------------------------------------- 1 | export { SocketModule } from './socket.module'; 2 | export { SocketGateway } from './socket.gateway'; 3 | export { AuthSocketGateway } from './auth-socket.gateway'; 4 | export { RedisIoAdapter } from './redis-io.adapter'; 5 | -------------------------------------------------------------------------------- /src/lib/socket/redis-io.adapter.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { IoAdapter } from '@nestjs/platform-socket.io'; 3 | import * as Redis from 'ioredis'; 4 | import { createAdapter, RedisAdapter } from 'socket.io-redis'; 5 | 6 | import { ConfigService } from '@lib/config'; 7 | 8 | let pubClient: Redis.Redis; 9 | let subClient: Redis.Redis; 10 | 11 | export class RedisIoAdapter extends IoAdapter { 12 | private redisAdapter: RedisAdapter; 13 | 14 | constructor(app: INestApplication, config: ConfigService) { 15 | super(app); 16 | 17 | if (!pubClient && !subClient) { 18 | pubClient = new Redis({ 19 | host: config.get('REDIS_HOST'), 20 | port: +config.get('REDIS_PORT'), 21 | password: config.get('REDIS_AUTH_PASS') 22 | }); 23 | subClient = pubClient.duplicate(); 24 | } 25 | this.redisAdapter = createAdapter({ pubClient, subClient }); 26 | } 27 | 28 | createIOServer(port: number, options?: any): any { 29 | const server = super.createIOServer(port, options); 30 | server.adapter(this.redisAdapter); 31 | return server; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/socket/socket.filter.ts: -------------------------------------------------------------------------------- 1 | import { ArgumentsHost, Catch } from '@nestjs/common'; 2 | import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets'; 3 | import { Socket } from 'socket.io'; 4 | 5 | @Catch(WsException) 6 | export class ExceptionFilter extends BaseWsExceptionFilter { 7 | catch(exception: WsException, host: ArgumentsHost) { 8 | // const data = host.switchToWs().getData(); 9 | // console.log('exception', exception); 10 | // console.log('data', data); 11 | const socket = host.switchToWs().getClient(); 12 | socket.emit('exception', { 13 | status: 'error', 14 | message: exception.message || `It's a message from the exception filter` 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/socket/socket.gateway.ts: -------------------------------------------------------------------------------- 1 | import { Logger, UseFilters } from '@nestjs/common'; 2 | import { 3 | OnGatewayConnection, 4 | OnGatewayDisconnect, 5 | SubscribeMessage, 6 | WebSocketGateway, 7 | WebSocketServer, 8 | WsException, 9 | WsResponse 10 | } from '@nestjs/websockets'; 11 | import { Server, Socket } from 'socket.io'; 12 | 13 | import { ExceptionFilter } from './socket.filter'; 14 | 15 | @UseFilters(ExceptionFilter) 16 | @WebSocketGateway({ namespace: 'app' }) 17 | export class SocketGateway implements OnGatewayConnection, OnGatewayDisconnect { 18 | @WebSocketServer() 19 | server!: Server; 20 | private logger = new Logger('AppGateway'); 21 | 22 | handleConnection(socket: Socket) { 23 | this.logger.log(`Socket ID: ${socket.id} connected!`); 24 | } 25 | 26 | handleDisconnect(socket: Socket) { 27 | this.logger.log(`Socket ID: ${socket.id} disconnected!`); 28 | } 29 | 30 | @SubscribeMessage('welcome') 31 | onEvent(): WsResponse { 32 | throw new WsException('Error Testing'); 33 | return { data: 'WELCOME APP', event: 'welcome' }; 34 | } 35 | 36 | emit(eventName: string, body: any) { 37 | this.server.emit(eventName, body); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/socket/socket.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { AuthSocketGateway } from './auth-socket.gateway'; 4 | import { SocketGateway } from './socket.gateway'; 5 | 6 | @Global() 7 | @Module({ 8 | providers: [SocketGateway, AuthSocketGateway], 9 | exports: [SocketGateway, AuthSocketGateway] 10 | }) 11 | export class SocketModule {} 12 | -------------------------------------------------------------------------------- /src/lib/tile38/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectTile38 } from './tile38.decorator'; 2 | export { Tile38Module } from './tile38.module'; 3 | export { Tile38 } from './tile38'; 4 | -------------------------------------------------------------------------------- /src/lib/tile38/tile38.constant.ts: -------------------------------------------------------------------------------- 1 | export const TILE38_TOKEN = 'TILE38_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/tile38/tile38.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { TILE38_TOKEN } from './tile38.constant'; 4 | 5 | export const InjectTile38 = () => Inject(TILE38_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/tile38/tile38.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; 3 | 4 | export class Tile38Config { 5 | @IsNotEmpty() 6 | @IsString() 7 | TILE38_HOST!: string; 8 | 9 | @IsNotEmpty() 10 | @IsNumber() 11 | @Transform(x => +x.value) 12 | TILE38_PORT!: number; 13 | 14 | @IsOptional() 15 | @IsString() 16 | TILE38_AUTH_PASS!: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/tile38/tile38.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { Tile38Provider } from './tile38.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [Tile38Provider], 8 | exports: [Tile38Provider] 9 | }) 10 | export class Tile38Module {} 11 | -------------------------------------------------------------------------------- /src/lib/tile38/tile38.provider.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import * as Redis from 'ioredis'; 3 | 4 | import { ConfigService } from '../config'; 5 | import { Tile38 } from './tile38'; 6 | import { TILE38_TOKEN } from './tile38.constant'; 7 | import { Tile38Config } from './tile38.dto'; 8 | 9 | let redis: Redis.Redis; 10 | 11 | export const Tile38Provider = { 12 | inject: [ConfigService], 13 | provide: TILE38_TOKEN, 14 | useFactory: (configService: ConfigService) => { 15 | const logger = new Logger('Tile38Module'); 16 | const config = configService.validate('Tile38Module', Tile38Config); 17 | 18 | // This will prevent reinitialize redis and cause maximum redis connection 19 | if (redis) return redis; 20 | 21 | redis = new Redis({ 22 | host: config.TILE38_HOST, 23 | port: config.TILE38_PORT, 24 | password: config.TILE38_AUTH_PASS 25 | }); 26 | 27 | redis.on('error', e => logger.error(e.message, e.stack)); 28 | redis.on('end', () => logger.warn('Ended')); 29 | redis.on('reconnecting', () => logger.log('Reconnecting')); 30 | redis.on('connect', () => logger.log('Connecting')); 31 | redis.on('ready', () => logger.log('Connected')); 32 | return new Tile38(redis); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /src/lib/twilio/index.ts: -------------------------------------------------------------------------------- 1 | export { TwilioLib } from './twilio'; 2 | export { TwilioModule } from './twilio.module'; 3 | export { InjectTwilio } from './twilio.decorator'; 4 | -------------------------------------------------------------------------------- /src/lib/twilio/twilio.constant.ts: -------------------------------------------------------------------------------- 1 | export const TWILIO_TOKEN = 'TWILIO_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/twilio/twilio.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { TWILIO_TOKEN } from './twilio.constant'; 4 | 5 | export const InjectTwilio = () => Inject(TWILIO_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/twilio/twilio.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class TwilioConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | TWILIO_ACCOUNT_SID!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | TWILIO_AUTH_TOKEN!: string; 11 | 12 | @IsNotEmpty() 13 | @IsString() 14 | TWILIO_SMS_FROM!: string; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib/twilio/twilio.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { twilioProvider } from './twilio.provider'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [twilioProvider], 8 | exports: [twilioProvider] 9 | }) 10 | export class TwilioModule {} 11 | -------------------------------------------------------------------------------- /src/lib/twilio/twilio.provider.ts: -------------------------------------------------------------------------------- 1 | import { ConfigService } from '../config'; 2 | import { TwilioLib } from './twilio'; 3 | import { TWILIO_TOKEN } from './twilio.constant'; 4 | import { TwilioConfig } from './twilio.dto'; 5 | 6 | export const twilioProvider = { 7 | inject: [ConfigService], 8 | provide: TWILIO_TOKEN, 9 | useFactory: (configService: ConfigService) => { 10 | const config = configService.validate('TwilioModule', TwilioConfig); 11 | return new TwilioLib(config); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/twilio/twilio.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from '@nestjs/common'; 2 | import * as Twilio from 'twilio'; 3 | 4 | import { TwilioConfig } from './twilio.dto'; 5 | 6 | export class TwilioLib { 7 | client: Twilio.Twilio; 8 | 9 | private logger: Logger = new Logger('TwilioModule'); 10 | 11 | constructor(public readonly config: TwilioConfig) { 12 | this.client = Twilio(this.config.TWILIO_ACCOUNT_SID, this.config.TWILIO_AUTH_TOKEN); 13 | this.logger.log('Twilio loaded'); 14 | } 15 | 16 | async sendResetPasswordSMS(to: string, opt: any) { 17 | const { name, code } = opt; 18 | const message = `Hi, ${name}. Here is the code for resetting your password in our application: ${code}.`; 19 | return this.sendSMS(to, message); 20 | } 21 | 22 | async sendSampleSMS(to: string, opt: any) { 23 | const { name } = opt; 24 | const message = `Hi ${name}, Welcome to Nest Core Project`; 25 | return this.sendSMS(to, message); 26 | } 27 | 28 | private async sendSMS(to: string, body: string) { 29 | return this.client.messages.create({ from: this.config.TWILIO_SMS_FROM, to, body }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/typeorm/base.repository.ts: -------------------------------------------------------------------------------- 1 | import stack from 'callsites'; 2 | import { readFile } from 'fs'; 3 | import { dirname, resolve } from 'path'; 4 | import { ObjectLiteral, Repository } from 'typeorm'; 5 | import { promisify } from 'util'; 6 | 7 | const readFileAsync = promisify(readFile); 8 | 9 | /** 10 | * Custom Repository with enhance query raw sql statement 11 | */ 12 | export class BaseRepository extends Repository { 13 | /** 14 | * Run SQL with key value pair parameters 15 | * 16 | * @param sql sql statement 17 | * @param params sql parameter in key value pair 18 | * @see https://github.com/typeorm/typeorm/issues/556#issuecomment-317459125 19 | */ 20 | async $runSQL(sql: string, params: ObjectLiteral): Promise { 21 | const [q, p] = this.manager.connection.driver.escapeQueryWithParameters(sql, params, {}); 22 | return this.query(q, p); 23 | } 24 | 25 | /** 26 | * Run SQL file with key value pair parameters 27 | * 28 | * @param sqlPath sql file path 29 | * @param params sql parameter in key value pair 30 | */ 31 | async $runSQLFile(sqlPath: string, params: ObjectLiteral): Promise { 32 | const sql = await this.$readSQLFile(sqlPath); 33 | console.log(sql); 34 | return this.$runSQL(sql, params); 35 | } 36 | 37 | /** 38 | * Read SQL file 39 | */ 40 | private async $readSQLFile(sqlPath: string) { 41 | // find dirname where this function was called. 42 | const caller = stack()[0].getFileName() || ''; 43 | const callerDirname = dirname(caller); 44 | 45 | // * ['../'] means we use this function from @lib 46 | return readFileAsync(resolve(callerDirname, '../', sqlPath), 'utf8'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/typeorm/index.ts: -------------------------------------------------------------------------------- 1 | export { TypeOrmModule } from './typeorm.module'; 2 | -------------------------------------------------------------------------------- /src/lib/typeorm/typeorm.dto.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'class-transformer'; 2 | import { IsBoolean, IsNotEmpty, IsNumber, IsString } from 'class-validator'; 3 | 4 | export class TypeOrmConfig { 5 | @IsNotEmpty() 6 | @IsString() 7 | DB_HOST!: string; 8 | 9 | @IsNotEmpty() 10 | @IsNumber() 11 | @Transform(x => +x.value) 12 | DB_PORT!: number; 13 | 14 | @IsNotEmpty() 15 | @IsString() 16 | DB_SCHEMA!: string; 17 | 18 | @IsNotEmpty() 19 | @IsString() 20 | DB_USERNAME!: string; 21 | 22 | @IsNotEmpty() 23 | @IsString() 24 | DB_PASSWORD!: string; 25 | 26 | @IsNotEmpty() 27 | @IsString() 28 | DB_CONNECTION!: string; 29 | 30 | @IsNotEmpty() 31 | @IsBoolean() 32 | @Transform(x => String(x.value).toLowerCase() === 'true') 33 | DB_LOGGING!: boolean; 34 | 35 | @IsNotEmpty() 36 | @IsBoolean() 37 | @Transform(x => String(x.value).toLowerCase() === 'true') 38 | DB_SYNC!: boolean; 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/typeorm/typeorm.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { TypeOrmModule as OrmModule } from '@nestjs/typeorm'; 3 | 4 | import * as Entities from '@entities'; 5 | import * as Repositories from '@repositories'; 6 | 7 | import { TypeOrmConfigService } from './typeorm.service'; 8 | 9 | @Global() 10 | @Module({ 11 | imports: [ 12 | OrmModule.forRootAsync({ useClass: TypeOrmConfigService }), 13 | OrmModule.forFeature([...Object.values(Entities), ...Object.values(Repositories)]) 14 | ], 15 | exports: [OrmModule] 16 | }) 17 | export class TypeOrmModule {} 18 | -------------------------------------------------------------------------------- /src/lib/typeorm/typeorm.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; 3 | 4 | import * as Entities from '@entities'; 5 | import { ConfigService } from '@lib/config'; 6 | 7 | import { TypeOrmConfig } from './typeorm.dto'; 8 | 9 | @Injectable() 10 | export class TypeOrmConfigService implements TypeOrmOptionsFactory { 11 | constructor(private readonly configService: ConfigService) {} 12 | 13 | createTypeOrmOptions(): TypeOrmModuleOptions { 14 | const config = this.configService.validate('TypeOrmModule', TypeOrmConfig); 15 | return { 16 | type: config.DB_CONNECTION as any, 17 | host: config.DB_HOST, 18 | port: config.DB_PORT, 19 | username: config.DB_USERNAME, 20 | password: config.DB_PASSWORD, 21 | database: config.DB_SCHEMA, 22 | logging: config.DB_LOGGING, 23 | entities: Object.values(Entities), 24 | keepConnectionAlive: true // ! use this for when using webpack 25 | // synchronize: true DB_SYNC 26 | // connectString: For Oracle Connection "(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(HOST=127.0.0.1)(PORT=1521))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=my_server)))" 27 | }; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/lib/wowza/_base/index.ts: -------------------------------------------------------------------------------- 1 | export interface BasePaginationResult { 2 | pagination: { 3 | total_records: number; 4 | page: number; 5 | per_page: number; 6 | total_pages: number; 7 | page_first_index: number; 8 | page_last_index: number; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/wowza/index.ts: -------------------------------------------------------------------------------- 1 | export { InjectWowza } from './wowza.decorator'; 2 | export { Wowza } from './wowza'; 3 | export { WowzaModule } from './wowza.module'; 4 | -------------------------------------------------------------------------------- /src/lib/wowza/stream-target/index.ts: -------------------------------------------------------------------------------- 1 | export * from './stream-target.interface'; 2 | export * from './stream-target.service'; 3 | -------------------------------------------------------------------------------- /src/lib/wowza/stream-target/stream-target.service.ts: -------------------------------------------------------------------------------- 1 | import { WowzaHelper } from '../wowza.helper'; 2 | import * as I from './stream-target.interface'; 3 | 4 | export class StreamTargetService extends WowzaHelper { 5 | /** 6 | * Fetch all ultra low latency stream targets 7 | * 8 | * @see https://sandbox.cloud.wowza.com/api/current/docs#operation/listUllStreamTargets 9 | */ 10 | async fetchAllULL(params?: I.FetchAllULLOption) { 11 | const url = `api/v1.3/stream_targets/ull`; 12 | return this.wrapper(this.http.get(url, { params })); // prettier-ignore 13 | } 14 | 15 | /** 16 | * Fetch an ultra low latency stream target 17 | * 18 | * @see https://sandbox.cloud.wowza.com/api/current/docs#operation/showUllStreamTarget 19 | */ 20 | async fetchULL(params: I.FetchULLOption) { 21 | const url = `api/v1.3/stream_targets/ull/${params.id}`; 22 | return this.wrapper(this.http.get(url)); 23 | } 24 | 25 | /** 26 | * Create an ultra low latency stream target 27 | * 28 | * @see https://sandbox.cloud.wowza.com/api/current/docs#operation/createUllStreamTarget 29 | */ 30 | async createULL(params?: I.CreateULLOption) { 31 | const url = 'api/v1.3/stream_targets/ull'; 32 | return this.wrapper(this.http.post(url, params)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.constant.ts: -------------------------------------------------------------------------------- 1 | export const WOWZA_TOKEN = 'WOWZA_INJECT_TOKEN'; 2 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.decorator.ts: -------------------------------------------------------------------------------- 1 | import { Inject } from '@nestjs/common'; 2 | 3 | import { WOWZA_TOKEN } from './wowza.constant'; 4 | 5 | export const InjectWowza = () => Inject(WOWZA_TOKEN); 6 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class WowzaConfig { 4 | @IsNotEmpty() 5 | @IsString() 6 | WOWZA_API_KEY!: string; 7 | 8 | @IsNotEmpty() 9 | @IsString() 10 | WOWZA_ACCESS_KEY!: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.helper.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | import { AxiosResponse } from 'axios'; 3 | import { lastValueFrom, Observable } from 'rxjs'; 4 | 5 | export class WowzaHelper { 6 | constructor(protected readonly http: HttpService) {} 7 | 8 | protected async wrapper(req: Observable>) { 9 | return lastValueFrom(req).then(x => x.data); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpModule } from '@nestjs/axios'; 2 | import { Global, Module } from '@nestjs/common'; 3 | 4 | import { WowzaProvider } from './wowza.provider'; 5 | 6 | @Global() 7 | @Module({ 8 | imports: [HttpModule.register({})], 9 | providers: [WowzaProvider], 10 | exports: [WowzaProvider] 11 | }) 12 | export class WowzaModule {} 13 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.provider.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | 3 | import { ConfigService } from '../config'; 4 | import { Wowza } from './wowza'; 5 | import { WOWZA_TOKEN } from './wowza.constant'; 6 | 7 | export const WowzaProvider = { 8 | inject: [ConfigService, HttpService], 9 | provide: WOWZA_TOKEN, 10 | useFactory: (configService: ConfigService, http: HttpService) => new Wowza(configService, http) 11 | }; 12 | -------------------------------------------------------------------------------- /src/lib/wowza/wowza.ts: -------------------------------------------------------------------------------- 1 | import { HttpService } from '@nestjs/axios'; 2 | 3 | import { ConfigService } from '../config'; 4 | import { StreamTargetService } from './stream-target'; 5 | import { WowzaConfig } from './wowza.dto'; 6 | 7 | export class Wowza { 8 | /** 9 | * A stream target is a destination for a stream. Stream targets can be Wowza Streaming Cloud edge resources; custom, external destinations; or ultra low latency target destinations. 10 | */ 11 | public streamTarget: StreamTargetService; 12 | 13 | constructor(private readonly configService: ConfigService, private readonly http: HttpService) { 14 | const config = this.configService.validate('WowzaModule', WowzaConfig); 15 | 16 | this.http.axiosRef.defaults.baseURL = `https://api.cloud.wowza.com`; 17 | this.http.axiosRef.defaults.headers['wsc-api-key'] = config.WOWZA_API_KEY; 18 | this.http.axiosRef.defaults.headers['wsc-access-key'] = config.WOWZA_ACCESS_KEY; 19 | this.http.axiosRef.defaults.headers['Content-Type'] = 'application/json, charset=utf8'; 20 | 21 | // https://github.com/axios/axios/issues/1600#issuecomment-454013644 22 | this.http.axiosRef.interceptors.response.use( 23 | response => response, // response.data 24 | error => Promise.reject(error.response) 25 | ); 26 | 27 | this.streamTarget = new StreamTargetService(this.http); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export { UserModel } from './user.model'; 2 | -------------------------------------------------------------------------------- /src/models/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FindOneOpt { 2 | email?: string; 3 | phone?: string; 4 | firstName?: string; 5 | lastName?: string; 6 | nickName?: string; 7 | positionIds?: number[]; 8 | roleId?: number; 9 | gender?: string; 10 | status?: string; 11 | } 12 | 13 | export interface FindAndCountAllOpt extends FindOneOpt { 14 | order?: string; 15 | limit: number; 16 | offset: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/queries/user/count.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | count(u.id) AS total 3 | 4 | FROM Users AS u 5 | 6 | WHERE 7 | u.id IS NOT NULL 8 | {filterByEqualEmail} 9 | {filterByEqualPhone} 10 | {filterByLikeFirstName} 11 | {filterByLikeLastName} 12 | {filterByLikeNickName} 13 | -------------------------------------------------------------------------------- /src/queries/user/find-all.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | u.id, 3 | u.email, 4 | u.firstName, 5 | u.lastName, 6 | u.nickName, 7 | u.phone 8 | 9 | FROM Users AS u 10 | 11 | WHERE 12 | u.id IS NOT NULL 13 | {filterByEqualEmail} 14 | {filterByEqualPhone} 15 | {filterByLikeFirstName} 16 | {filterByLikeLastName} 17 | {filterByLikeNickName} 18 | 19 | ORDER BY {order} 20 | 21 | {filterLimit} 22 | {filterOffset} 23 | -------------------------------------------------------------------------------- /src/queries/user/find-one.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | u.id, 3 | u.email, 4 | u.firstName, 5 | u.lastName, 6 | u.nickName, 7 | u.phone 8 | 9 | FROM Users AS u 10 | 11 | WHERE 12 | u.id IS NOT NULL 13 | {filterByEqualEmail} 14 | {filterByEqualPhone} 15 | {filterByLikeFirstName} 16 | {filterByLikeLastName} 17 | {filterByLikeNickName} 18 | 19 | LIMIT 1 20 | -------------------------------------------------------------------------------- /src/repositories/index.ts: -------------------------------------------------------------------------------- 1 | export { UserRepository } from './user.repository'; 2 | -------------------------------------------------------------------------------- /src/repositories/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | export interface FindMeOpt { 2 | email?: string; 3 | name?: string; 4 | organizationId?: number; 5 | positionId?: number; 6 | roleId?: number; 7 | status?: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/repositories/user.repository.ts: -------------------------------------------------------------------------------- 1 | import { EntityRepository } from 'typeorm'; 2 | 3 | import { UserEntity, UserProfileEntity } from '@entities'; 4 | import { BaseRepository } from '@lib/typeorm/base.repository'; 5 | 6 | import * as I from './interfaces/user.interface'; 7 | 8 | @EntityRepository(UserEntity) 9 | export class UserRepository extends BaseRepository { 10 | async $findAndCountAll(opt: I.FindMeOpt) { 11 | const { name, email, organizationId, positionId, roleId, status } = opt; 12 | const q = this.createQueryBuilder('u') 13 | // .innerJoinAndSelect('u.profile', 'p') 14 | .innerJoinAndMapMany('u.profile', UserProfileEntity, 'p', 'p.userId = u.id') 15 | .addSelect('u.id', 'id') 16 | .addSelect('email') 17 | .addSelect('p.nickName', 'nickName') 18 | .addSelect('p.firstName', 'firstName') 19 | .addSelect('p.lastName', 'lastName') 20 | .addSelect(`CONCAT(p.firstName,' ', p.lastName)`, 'fullName') 21 | .addSelect('status') 22 | .addSelect('positionId') 23 | .addSelect('organizationId') 24 | .addSelect('roleId') 25 | .where('u.id IS NOT NULL'); 26 | 27 | if (email) q.andWhere('u.email = :email', { email }); // prettier-ignore 28 | if (name) q.andWhere('p.nickName LIKE :name', { name: `%${name}%` }); // prettier-ignore 29 | if (organizationId) q.andWhere('u.organizationId = :organizationId', { organizationId }); // prettier-ignore 30 | if (positionId) q.andWhere('u.positionId = :positionId', { positionId }); // prettier-ignore 31 | if (roleId) q.andWhere('u.roleId = :roleId', { roleId }); // prettier-ignore 32 | if (status) q.andWhere('u.status = :status', { status }); // prettier-ignore 33 | 34 | const [data, total] = await Promise.all([q.getMany(), q.getCount()]); 35 | return { data, total }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/schemas/audit.model.ts: -------------------------------------------------------------------------------- 1 | import { getModelForClass, mongoose, prop } from '@typegoose/typegoose'; 2 | 3 | class AuditSchema { 4 | /** 5 | * Request method (GET, POST, PUT, DELETE) 6 | */ 7 | @prop({ required: true }) 8 | method!: string; 9 | 10 | /** 11 | * Request url 12 | */ 13 | @prop({ required: true }) 14 | url!: string; 15 | 16 | /** 17 | * HTTP request body as JSON stringify 18 | */ 19 | @prop() 20 | body!: string; 21 | 22 | /** 23 | * HTTP response result as JSON stringify 24 | */ 25 | @prop({ required: true }) 26 | result!: string; 27 | 28 | /** 29 | * HTTP request duration 30 | */ 31 | @prop({ required: true }) 32 | duration!: number; 33 | 34 | /** 35 | * Request time 36 | */ 37 | @prop({ required: true }) 38 | time!: Date; 39 | 40 | /** 41 | * User who request the endpoint 42 | */ 43 | @prop({ default: null }) 44 | userId?: mongoose.Types.ObjectId; 45 | 46 | /** 47 | * Username in case the user is delete 48 | */ 49 | @prop() 50 | username!: string; 51 | 52 | /** 53 | * Server controller name 54 | */ 55 | @prop() 56 | className!: string; 57 | 58 | /** 59 | * Server controller method 60 | */ 61 | @prop() 62 | handler!: string; 63 | } 64 | 65 | export const AuditModel = getModelForClass(AuditSchema, { 66 | schemaOptions: { collection: 'Audits' } 67 | }); 68 | -------------------------------------------------------------------------------- /src/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export { AuditModel } from './audit.model'; 2 | export { UserModel } from './user.model'; 3 | -------------------------------------------------------------------------------- /src/schemas/user.model.ts: -------------------------------------------------------------------------------- 1 | import { getModelForClass, prop } from '@typegoose/typegoose'; 2 | 3 | import { T } from '@common'; 4 | 5 | class UserSchema { 6 | @prop({ required: true }) 7 | firstName!: string; 8 | 9 | @prop({ required: true }) 10 | lastName!: string; 11 | 12 | @prop({ required: true }) 13 | username!: string; 14 | 15 | @prop({ required: true }) 16 | password!: string; 17 | 18 | @prop({ enum: T.RoleEnum }) 19 | role!: T.RoleType; 20 | 21 | @prop({ required: true, default: false }) 22 | isArchived!: boolean; 23 | 24 | @prop({ required: true, default: Date.now }) 25 | createdAt!: Date; 26 | 27 | @prop({ required: true }) 28 | createdBy!: string; 29 | 30 | @prop({ required: true, default: Date.now }) 31 | updatedAt!: Date; 32 | 33 | @prop({ required: true }) 34 | updatedBy!: string; 35 | } 36 | 37 | export const UserModel = getModelForClass(UserSchema, { 38 | schemaOptions: { collection: 'Users', timestamps: true } 39 | }); 40 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "noUnusedLocals": true, 8 | "strictNullChecks": true, 9 | "noImplicitAny": false, 10 | "outDir": "dist", 11 | "sourceMap": true, 12 | "target": "es6", 13 | "emitDecoratorMetadata": true, 14 | "experimentalDecorators": true, 15 | "lib": ["dom", "es7"], 16 | "declaration": false, 17 | "removeComments": true, 18 | "noLib": false, 19 | "paths": { 20 | "@api/*": ["src/api/*"], 21 | "@common": ["src/common"], 22 | "@dynamodb": ["src/dynamodb"], 23 | "@entities": ["src/entities"], 24 | "@lib/*": ["src/lib/*"], 25 | "@models": ["src/models"], 26 | "@queries": ["src/queries"], 27 | "@repositories": ["src/repositories"], 28 | "@schemas": ["src/schemas"] 29 | } 30 | }, 31 | "include": ["src/**/*", "typings/**/*.ts"], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = function (options) { 5 | return { 6 | ...options, 7 | entry: ['webpack/hot/poll?100', options.entry], 8 | externals: [ 9 | nodeExternals({ 10 | allowlist: ['webpack/hot/poll?100'], 11 | }), 12 | ], 13 | plugins: [ 14 | ...options.plugins, 15 | new webpack.HotModuleReplacementPlugin(), 16 | ], 17 | }; 18 | }; 19 | --------------------------------------------------------------------------------