├── .commitlintrc.yml ├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── github-actions.yml │ └── slack-notify.yml ├── .gitignore ├── .huskyrc.json ├── .markdownlintrc ├── .npmrc ├── .prettierrc ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── ecosystem.config.js ├── env ├── .env.example └── test.env ├── jest.config.js ├── nest-cli.json ├── package.json ├── public └── favicon.ico ├── schema.gql ├── src ├── agenda │ ├── agenda.module.ts │ ├── agenda.resolver.ts │ ├── agenda.service.ts │ ├── dtos │ │ ├── create-agenda.input.ts │ │ └── update-agenda.input.ts │ ├── interfaces │ │ └── agenda.interface.ts │ ├── models │ │ └── agenda.model.ts │ └── schemas │ │ └── agenda.schemas.ts ├── announcements │ ├── announcements.module.ts │ ├── announcements.resolver.ts │ ├── announcements.service.ts │ ├── dtos │ │ ├── create-announcement.input.ts │ │ └── update-announcement.input.ts │ ├── interfaces │ │ └── announcement.interface.ts │ ├── models │ │ └── announcements.model.ts │ └── schemas │ │ └── announcements.schema.ts ├── app.module.ts ├── auth │ ├── auth.module.ts │ ├── auth.resolver.ts │ ├── auth.service.ts │ ├── dtos │ │ ├── change-password.input.ts │ │ ├── login.input.ts │ │ ├── register.input.ts │ │ └── validate-totp.input.ts │ ├── interfaces │ │ ├── jwt.interface.ts │ │ ├── login.interface.ts │ │ ├── recaptcha.interface.ts │ │ └── validate.interface.ts │ ├── jwt.strategy.ts │ └── models │ │ ├── ip-model.ts │ │ ├── recovery-code.model.ts │ │ └── totp.model.ts ├── bandwagon │ ├── bandwagon.module.ts │ ├── bandwagon.resolver.ts │ ├── bandwagon.service.ts │ ├── interfaces │ │ ├── bandwagon-params.interface.ts │ │ ├── service-info.interface.ts │ │ └── usage-stats.interface.ts │ └── models │ │ ├── service-info.model.ts │ │ └── usage-stats.model.ts ├── best-albums │ ├── best-albums.module.ts │ ├── best-albums.resolver.ts │ ├── best-albums.service.ts │ ├── dtos │ │ ├── create-best-album.input.ts │ │ └── update-best-album.input.ts │ ├── interfaces │ │ └── best-albums.interface.ts │ ├── models │ │ └── best-albums.model.ts │ └── schemas │ │ └── best-albums.schema.ts ├── config │ ├── config.module.ts │ ├── config.service.ts │ └── interfaces │ │ └── bandwagon-keys.interface.ts ├── covers │ ├── covers.module.ts │ ├── covers.resolver.ts │ ├── covers.service.ts │ ├── dtos │ │ ├── create-cover.input.ts │ │ └── update-cover.input.ts │ ├── interfaces │ │ └── covers.interface.ts │ ├── models │ │ └── covers.model.ts │ └── schemas │ │ └── covers.schema.ts ├── database │ ├── database.module.ts │ └── models │ │ ├── batch-delete.model.ts │ │ └── batch-update.model.ts ├── global-setting │ ├── dtos │ │ └── update-global-setting.input.ts │ ├── global-setting.module.ts │ ├── global-setting.resolver.ts │ ├── global-setting.service.ts │ ├── interfaces │ │ └── global-setting.interface.ts │ ├── models │ │ └── global-setting.model.ts │ └── schemas │ │ └── global-setting.schema.ts ├── graphql │ └── graphqls.module.ts ├── live-tours │ ├── dtos │ │ ├── create-live-tour.input.ts │ │ └── update-live-tour.input.ts │ ├── interfaces │ │ └── live-tours.interface.ts │ ├── live-tours.module.ts │ ├── live-tours.resolver.ts │ ├── live-tours.service.ts │ ├── models │ │ └── live-tours.model.ts │ └── schemas │ │ └── live-tours.schema.ts ├── main.ts ├── mottos │ ├── dtos │ │ ├── create-motto.input.ts │ │ └── update-motto.input.ts │ ├── interfaces │ │ └── motto.interface.ts │ ├── models │ │ └── mottos.model.ts │ ├── mottos.module.ts │ ├── mottos.resolver.ts │ ├── mottos.service.ts │ └── schemas │ │ └── mottos.schema.ts ├── open-sources │ ├── dtos │ │ ├── create-open-source.input.ts │ │ └── update-open-source.input.ts │ ├── interfaces │ │ └── open-sources.interface.ts │ ├── models │ │ └── open-sources.model.ts │ ├── open-sources.module.ts │ ├── open-sources.resolver.ts │ ├── open-sources.service.ts │ └── schemas │ │ └── open-sources.schema.ts ├── player │ ├── dtos │ │ ├── create-player.input.ts │ │ └── update-player.input.ts │ ├── interfaces │ │ └── player.interface.ts │ ├── models │ │ └── player.model.ts │ ├── player.module.ts │ ├── player.resolver.ts │ ├── player.service.ts │ └── schemas │ │ └── player.schema.ts ├── post-statistics │ ├── dtos │ │ └── create-post-statistics.input.ts │ ├── interfaces │ │ └── post-statistics.interface.ts │ ├── models │ │ ├── post-statistics-group.model.ts │ │ └── post-statistics.model.ts │ ├── post-statistics.module.ts │ ├── post-statistics.resolver.ts │ ├── post-statistics.service.ts │ └── schemas │ │ └── post-statistics.schema.ts ├── posts │ ├── dtos │ │ ├── create-post.input.ts │ │ ├── pagination.input.ts │ │ └── update-post.input.ts │ ├── interfaces │ │ └── posts.interface.ts │ ├── models │ │ ├── archive.model.ts │ │ ├── post.model.ts │ │ ├── posts.model.ts │ │ └── tags.model.ts │ ├── posts.module.ts │ ├── posts.resolver.ts │ ├── posts.service.ts │ └── schemas │ │ └── posts.schema.ts ├── shared │ ├── constants.ts │ ├── decorators │ │ ├── req.decorator.ts │ │ └── roles.decorator.ts │ ├── filters │ │ └── graqhql-exception.filter.ts │ ├── guard │ │ ├── GraphQLAuth.guard.ts │ │ └── roles.guard.ts │ ├── interceptors │ │ └── delay.interceptor.ts │ ├── interfaces │ │ └── exchange-position.input.ts │ ├── logger │ │ ├── logger.config.ts │ │ └── logger.module.ts │ ├── middlewares │ │ └── middleware.config.ts │ ├── pipes │ │ └── GraphQLValidation.pipe.ts │ └── utils.ts ├── users │ ├── dtos │ │ └── update-user.input.ts │ ├── interfaces │ │ └── user.interface.ts │ ├── models │ │ └── user.model.ts │ ├── schemas │ │ └── users.schema.ts │ ├── users.module.ts │ ├── users.resolver.ts │ └── users.service.ts └── yancey-music │ ├── dtos │ ├── create-yancey-music.input.ts │ └── update-yancey-music.input.ts │ ├── interfaces │ └── yancey-music.interface.ts │ ├── models │ └── yancey-music.model.ts │ ├── schemas │ └── yancey-music.schema.ts │ ├── yancey-music.module.ts │ ├── yancey-music.resolver.ts │ └── yancey-music.service.ts ├── test ├── agenda.e2e-spec.ts ├── announcements.e2e-spec.ts ├── auth.DO_NOT_TEST_ME.ts ├── best-albums.e2e-spec.ts ├── blog-statistics.e2e-spec.ts ├── covers.e2e-spec.ts ├── global-setting.e2e-spec.ts ├── jest-e2e.json ├── live-tours.e2e-spec.ts ├── mottos.e2e-spec.ts ├── open-sources.e2e-spec.ts ├── player.e2e-spec.ts ├── posts.e2e-spec.ts └── yancey-music.e2e-spec.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | extends: 2 | - '@commitlint/config-conventional' 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # produtions 2 | dist/ 3 | build/ 4 | 5 | # test 6 | test/ 7 | 8 | # jest config 9 | jest.config.js -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "jest": true 6 | }, 7 | "extends": ["airbnb-base", "plugin:nestjs/recommended"], 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["@typescript-eslint", "nestjs", "jest"], 18 | "rules": { 19 | "semi": "off", 20 | "import/prefer-default-export": "off", 21 | "implicit-arrow-linebreak": "off", 22 | "camelcase": "off", 23 | "no-underscore-dangle": "off", 24 | "no-shadow": "off", 25 | "function-paren-newline": "off", 26 | "object-curly-newline": "off", 27 | "arrow-parens": "off", 28 | "space-before-function-paren": "off", 29 | "func-names": "off", 30 | "operator-linebreak": "off", 31 | "indent": "off", 32 | "import/no-extraneous-dependencies": "off", 33 | "import/extensions": "off", // FIXME: 34 | "class-methods-use-this": "off", // FIXME: 35 | "no-unused-vars": "off" // FIXME: 36 | }, 37 | "settings": { 38 | "import/resolver": { 39 | "node": { 40 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request,** please make sure the following is done: 2 | 3 | 1. Fork the repository and create your branch from master. 4 | 5 | 2. Run yarn install in the repository root. 6 | 7 | 3. If you've fixed a bug or added code that should be tested, add tests! 8 | 9 | 4. Ensure the test suite passes (yarn test). Tip: yarn test --watch TestName is helpful in development. 10 | 11 | 5. Run yarn test:cov to generate coverage report. It supports the same options as yarn test. 12 | 13 | 6. If you need a debugger, run yarn test:debug TestName, open chrome://inspect, and press "Inspect". 14 | 15 | 7. Format your code with prettier (yarn prettier). 16 | 17 | 8. Make sure your code lints (yarn lint). Tip: yarn lint to only check changed files. 18 | 19 | 9. If you haven't already, complete the CLA. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' 9 | directory: '/' 10 | schedule: 11 | interval: 'weekly' 12 | target-branch: 'develop' 13 | commit-message: 14 | prefix: 'deps' 15 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '28 13 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.github/workflows/github-actions.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions for Blog BE Next 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | 11 | - name: Use Node.js 12.x 12 | uses: actions/setup-node@v2 13 | with: 14 | node-version: '12.x' 15 | 16 | - name: Get yarn cache directory path 17 | id: yarn-cache-dir-path 18 | run: echo "::set-output name=dir::$(yarn cache dir)" 19 | 20 | - uses: actions/cache@v2 21 | id: yarn-cache 22 | with: 23 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 25 | restore-keys: | 26 | ${{ runner.os }}-yarn- 27 | 28 | - name: Install dependencies 29 | run: yarn 30 | 31 | - name: Pre compilation 32 | run: yarn build 33 | 34 | # テスト成功時はこちらのステップが実行される 35 | - name: Slack Notification on Success at Testing Stage 36 | if: success() 37 | uses: rtCamp/action-slack-notify@v2 38 | env: 39 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOKS }} 40 | SLACK_CHANNEL: github-actions 41 | SLACK_TITLE: Test Success 42 | SLACK_COLOR: good 43 | SLACK_FOOTER: 'Powered by Yancey Inc. and its affiliates.' 44 | 45 | # テスト失敗時はこちらのステップが実行される 46 | - name: Slack Notification on Failure at Testing Stage 47 | uses: rtCamp/action-slack-notify@v2 48 | if: failure() 49 | env: 50 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOKS }} 51 | SLACK_CHANNEL: github-actions 52 | SLACK_TITLE: Test Failure 53 | SLACK_COLOR: danger 54 | SLACK_FOOTER: 'Powered by Yancey Inc. and its affiliates.' 55 | 56 | deployment: 57 | runs-on: ubuntu-latest 58 | needs: test 59 | if: startsWith(github.ref, 'refs/tags/v') 60 | steps: 61 | - name: Deploy to server 62 | uses: appleboy/ssh-action@v0.1.4 63 | with: 64 | host: ${{ secrets.HOST }} 65 | username: ${{ secrets.USERNAME }} 66 | password: ${{ secrets.PASSWORD }} 67 | script: sh ./blog-be-next-deploy.sh 68 | 69 | # テスト成功時はこちらのステップが実行される 70 | - name: Slack Notification on Success at Deployment Stage 71 | if: success() 72 | uses: rtCamp/action-slack-notify@v2 73 | env: 74 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOKS }} 75 | SLACK_CHANNEL: github-actions 76 | SLACK_TITLE: Deploy Success 77 | SLACK_COLOR: good 78 | SLACK_FOOTER: 'Powered by Yancey Inc. and its affiliates.' 79 | 80 | # テスト失敗時はこちらのステップが実行される 81 | - name: Slack Notification on Failure at Deployment Stage 82 | uses: rtCamp/action-slack-notify@v2 83 | if: failure() 84 | env: 85 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOKS }} 86 | SLACK_CHANNEL: github-actions 87 | SLACK_TITLE: Deploy Failure 88 | SLACK_COLOR: danger 89 | SLACK_FOOTER: 'Powered by Yancey Inc. and its affiliates.' 90 | -------------------------------------------------------------------------------- /.github/workflows/slack-notify.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Slack Notification on Pushing or Pull Requesting 3 | jobs: 4 | slackNotification: 5 | name: Slack Notification when Pushing 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Slack Notification when Pushing 10 | uses: rtCamp/action-slack-notify@v2 11 | env: 12 | SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOKS }} 13 | SLACK_CHANNEL: github-actions 14 | SLACK_TITLE: 'A ${{ github.event_name }} action for ${{ github.repository }}' 15 | SLACK_FOOTER: 'Powered by Yancey Inc. and its affiliates.' 16 | SLACK_COLOR: ${{ job.status }} 17 | SLACK_MESSAGE: '${{ github.actor }} is ${{ github.event_name }}ing the ${{ github.ref }} to ${{ github.repository }}.' 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # Dotenv environment variables file 58 | .env 59 | production.env 60 | development.env 61 | 62 | # Next.js build output 63 | .next 64 | 65 | # Production 66 | /dist 67 | /build 68 | 69 | # Document 70 | /documentation -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 4 | "pre-commit": "lint-staged" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.markdownlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "MD033": { 3 | "allowed_elements": [ 4 | "div", 5 | "img", 6 | "a", 7 | ] 8 | }, 9 | "MD013": false 10 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "proseWrap": "preserve", 6 | "semi": false, 7 | "printWidth": 100 8 | } 9 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # Below is a list of people and organizations that have contributed 2 | # to the Lighthouse project. Names should be added to the list like so: 3 | # 4 | # Name/Organization 5 | 6 | Yancey Leo 7 | Blog Environment Group 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | [Blog Environment Group](https://github.com/Yancey-Blog) has adopted a Code of Conduct that we expect project participants to adhere to. 4 | Please [read the full text](https://code.fb.com/codeofconduct/) so that you can understand what actions will and will not be tolerated. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Blog BE Next 2 | 3 | Want to contribute to Blog BE Next? There are a few things you need to know. 4 | 5 | We wrote a [contribution guide](https://reactjs.org/contributing/how-to-contribute.html) to help you get started. 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Yancey Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you discover a security issue in Blog BE Next, please report it by sending an email to [developer@yanceyleo.com](mailto:developer@yanceyleo.com). 4 | 5 | This will allow us to assess the risk, and make a fix available before we add a bug report to the GitHub repository. 6 | 7 | Thanks for helping make Blog BE Next safe for everyone! 8 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'blog-be', 5 | script: './dist/main.js', 6 | watch: ['./dist/'], 7 | ignore_watch: ['node_modules'], 8 | autorestart: true, 9 | max_memory_restart: '1G', 10 | instances: 2, 11 | exec_mode: 'cluster', 12 | env: { 13 | NODE_ENV: 'production', 14 | }, 15 | }, 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /env/.env.example: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Rename this file to development.env and development.env and save to the env file folder. 3 | # Then replace those environment variables with your own. 4 | # 5 | # Both DATABASE_USER and DATABASE_PWD are optional parameters in development.env and test.env file. 6 | # Furthermore, NEED_SIMULATE_NETWORK_THROTTLE is only taken efforts in the development environment. 7 | ############################################################################### 8 | 9 | NODE_ENV= 10 | APP_PORT= 11 | DATABASE_HOST= 12 | DATABASE_PORT= 13 | DATABASE_USER= 14 | DATABASE_PWD= 15 | DATABASE_COLLECTION= 16 | BANDWAGON_SECRET_KEY= 17 | BANDWAGON_SERVER_ID= 18 | IP_STACK_ACCESS_KEY= 19 | JWT_SECRET_KEY= 20 | JWT_EXPIRES_TIME= 21 | GOOGLE_RECAPTCHA_KEY= 22 | NEED_SIMULATE_NETWORK_THROTTLE= 23 | -------------------------------------------------------------------------------- /env/test.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | DATABASE_COLLECTION=blog_test -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/t6/29n5ypp9651499l3zpr33srh0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | collectCoverage: true, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | collectCoverageFrom: ['**/*.ts', '!**/node_modules/**'], 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: './test/coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | coveragePathIgnorePatterns: ['/node_modules/'], 31 | 32 | // A list of reporter names that Jest uses when writing coverage reports 33 | coverageReporters: ['text', 'lcov', 'json', 'clover'], 34 | 35 | // An object that configures minimum threshold enforcement for coverage results 36 | // coverageThreshold: null, 37 | 38 | // A path to a custom dependency extractor 39 | // dependencyExtractor: null, 40 | 41 | // Make calling deprecated APIs throw helpful error messages 42 | // errorOnDeprecated: false, 43 | 44 | // Force coverage collection from ignored files using an array of glob patterns 45 | // forceCoverageMatch: [], 46 | 47 | // A path to a module which exports an async function that is triggered once before all test suites 48 | // globalSetup: null, 49 | 50 | // A path to a module which exports an async function that is triggered once after all test suites 51 | // globalTeardown: null, 52 | 53 | // A set of global variables that need to be available in all test environments 54 | // globals: {}, 55 | 56 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 57 | // maxWorkers: "50%", 58 | 59 | // An array of directory names to be searched recursively up from the requiring module's location 60 | // moduleDirectories: [ 61 | // "node_modules" 62 | // ], 63 | 64 | // An array of file extensions your modules use 65 | moduleFileExtensions: [ 66 | 'js', 67 | 'json', 68 | 'ts', 69 | //"jsx", 70 | // "tsx", 71 | // "node" 72 | ], 73 | 74 | // A map from regular expressions to module names that allow to stub out resources with a single module 75 | // moduleNameMapper: {}, 76 | 77 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 78 | // modulePathIgnorePatterns: [], 79 | 80 | // Activates notifications for test results 81 | // notify: false, 82 | 83 | // An enum that specifies notification mode. Requires { notify: true } 84 | // notifyMode: "failure-change", 85 | 86 | // A preset that is used as a base for Jest's configuration 87 | // preset: null, 88 | 89 | // Run tests from one or more projects 90 | // projects: null, 91 | 92 | // Use this configuration option to add custom reporters to Jest 93 | // reporters: undefined, 94 | 95 | // Automatically reset mock state between every test 96 | // resetMocks: false, 97 | 98 | // Reset the module registry before running each individual test 99 | // resetModules: false, 100 | 101 | // A path to a custom resolver 102 | // resolver: null, 103 | 104 | // Automatically restore mock state between every test 105 | // restoreMocks: false, 106 | 107 | // The root directory that Jest should scan for tests and modules within 108 | // rootDir: 'test', 109 | 110 | // A list of paths to directories that Jest should use to search for files in 111 | roots: ['test', 'src'], 112 | 113 | // Allows you to use a custom runner instead of Jest's default test runner 114 | // runner: "jest-runner", 115 | 116 | // The paths to modules that run some code to configure or set up the testing environment before each test 117 | // setupFiles: [], 118 | 119 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 120 | // setupFilesAfterEnv: [], 121 | 122 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 123 | // snapshotSerializers: [], 124 | 125 | // The test environment that will be used for testing 126 | testEnvironment: 'node', 127 | 128 | // Options that will be passed to the testEnvironment 129 | // testEnvironmentOptions: {}, 130 | 131 | // Adds a location field to test results 132 | // testLocationInResults: false, 133 | 134 | // The glob patterns Jest uses to detect test files 135 | // testMatch: [ 136 | // "**/__tests__/**/*.[jt]s?(x)", 137 | // "**/?(*.)+(spec|test).[tj]s?(x)" 138 | // ], 139 | 140 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 141 | // testPathIgnorePatterns: [ 142 | // "/node_modules/" 143 | // ], 144 | 145 | // The regexp pattern or array of patterns that Jest uses to detect test files 146 | testRegex: ['.spec.ts$', '.e2e-spec.ts'], 147 | 148 | // This option allows the use of a custom results processor 149 | // testResultsProcessor: null, 150 | 151 | // This option allows use of a custom test runner 152 | // testRunner: "jasmine2", 153 | 154 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 155 | // testURL: "http://localhost", 156 | 157 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 158 | // timers: "real", 159 | 160 | // A map from regular expressions to paths to transformers 161 | transform: { 162 | '^.+\\.(t|j)s$': 'ts-jest', 163 | }, 164 | 165 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 166 | // transformIgnorePatterns: [ 167 | // "/node_modules/" 168 | // ], 169 | 170 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 171 | // unmockedModulePathPatterns: undefined, 172 | 173 | // Indicates whether each individual test should be reported during the run 174 | verbose: true, 175 | 176 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 177 | // watchPathIgnorePatterns: [], 178 | 179 | // Whether to use watchman for file crawling 180 | // watchman: true, 181 | } 182 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "src", 3 | "collection": "@nestjs/schematics" 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blog-be-next", 3 | "version": "2.0.3", 4 | "description": "The backend platform for Yancey blog.", 5 | "repository": "git@github.com:Yancey-Blog/blog-be-next.git", 6 | "author": "YanceyOfficial ", 7 | "license": "MIT", 8 | "scripts": { 9 | "clear": "rimraf dist", 10 | "build": "NODE_ENV=production yarn clear && nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "watch": "NODE_ENV=development nest start --watch", 16 | "commit": "npx git-cz", 17 | "push": "git push --follow-tags origin master", 18 | "release:major": "standard-version --release-as major && yarn push", 19 | "release:minor": "standard-version --release-as minor && yarn push", 20 | "release:patch": "standard-version --release-as patch && yarn push", 21 | "document": "npx compodoc -p tsconfig.json -s", 22 | "lint": "eslint src --ext .ts,.tsx", 23 | "test": "jest", 24 | "test:watch": "jest --watch", 25 | "test:cov": "jest --coverage", 26 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 27 | "test:e2e": "jest --config ./test/jest-e2e.json", 28 | "codecov": "node_modules/.bin/codecov -t 7b33b616-c7c6-465f-a858-91ca93071f98" 29 | }, 30 | "dependencies": { 31 | "@apollo/gateway": "^0.43.1", 32 | "@azure/storage-blob": "^12.8.0", 33 | "@nestjs/axios": "^0.0.3", 34 | "@nestjs/common": "^8.0.6", 35 | "@nestjs/config": "^1.0.1", 36 | "@nestjs/core": "^8.0.6", 37 | "@nestjs/graphql": "^9.1.1", 38 | "@nestjs/jwt": "^8.0.0", 39 | "@nestjs/mongoose": "^9.0.1", 40 | "@nestjs/passport": "^8.0.1", 41 | "@nestjs/platform-express": "^8.0.6", 42 | "apollo-server-core": "^3.5.0", 43 | "apollo-server-express": "^3.5.0", 44 | "bcrypt": "^5.0.1", 45 | "class-transformer": "^0.4.0", 46 | "class-validator": "^0.13.1", 47 | "dotenv": "^10.0.0", 48 | "graphql": "^15.7.2", 49 | "helmet": "^4.6.0", 50 | "joi": "^17.4.2", 51 | "luxon": "^2.0.2", 52 | "mongoose": "^6.0.6", 53 | "morgan": "^1.10.0", 54 | "nest-winston": "^1.6.0", 55 | "passport": "^0.5.0", 56 | "passport-jwt": "^4.0.0", 57 | "qrcode": "^1.4.4", 58 | "reflect-metadata": "^0.1.13", 59 | "request-ip": "^2.1.3", 60 | "rxjs": "^7.3.0", 61 | "serve-favicon": "^2.5.0", 62 | "speakeasy": "^2.0.0", 63 | "ts-morph": "^12.2.0", 64 | "ua-parser-js": "^1.0.2", 65 | "uuid": "^8.3.2", 66 | "winston": "^3.3.3", 67 | "yancey-js-util": "^3.0.0" 68 | }, 69 | "devDependencies": { 70 | "@commitlint/cli": "^14.1.0", 71 | "@commitlint/config-conventional": "^14.1.0", 72 | "@nestjs/cli": "^8.1.1", 73 | "@nestjs/schematics": "^8.0.3", 74 | "@nestjs/testing": "^8.0.6", 75 | "@types/bcrypt": "^5.0.0", 76 | "@types/csurf": "^1.11.2", 77 | "@types/jest": "^27.0.1", 78 | "@types/luxon": "^2.0.7", 79 | "@types/morgan": "^1.9.3", 80 | "@types/multer": "^1.4.7", 81 | "@types/node": "^16.9.2", 82 | "@types/passport-jwt": "^3.0.6", 83 | "@types/qrcode": "^1.4.1", 84 | "@types/ramda": "^0.27.44", 85 | "@types/request-ip": "^0.0.37", 86 | "@types/serve-favicon": "^2.5.3", 87 | "@types/speakeasy": "^2.0.6", 88 | "@types/supertest": "^2.0.11", 89 | "@types/ua-parser-js": "^0.7.36", 90 | "@types/uuid": "^8.3.1", 91 | "@typescript-eslint/eslint-plugin": "^5.3.1", 92 | "@typescript-eslint/parser": "^5.3.1", 93 | "codecov": "^3.8.2", 94 | "cz-conventional-changelog": "^3.3.0", 95 | "eslint": "^8.2.0", 96 | "eslint-config-airbnb": "^18.2.1", 97 | "eslint-config-airbnb-base": "^15.0.0", 98 | "eslint-plugin-import": "^2.24.2", 99 | "eslint-plugin-jest": "^25.2.4", 100 | "eslint-plugin-nestjs": "^1.2.3", 101 | "husky": "^4.3.8", 102 | "jest": "^27.2.0", 103 | "lint-staged": "^11.1.2", 104 | "prettier": "^2.4.1", 105 | "rimraf": "^3.0.2", 106 | "standard-version": "^9.3.1", 107 | "supertest": "^6.1.6", 108 | "ts-jest": "^27.0.5", 109 | "ts-loader": "^9.2.5", 110 | "ts-node": "^10.2.1", 111 | "tsconfig-paths": "^3.11.0", 112 | "typescript": "^4.4.3" 113 | }, 114 | "config": { 115 | "commitizen": { 116 | "path": "node_modules/cz-conventional-changelog" 117 | } 118 | }, 119 | "lint-staged": { 120 | "*.{ts,js}": [ 121 | "prettier --write", 122 | "yarn lint", 123 | "git add" 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yancey-Blog/blog-be-next/81edaa329400fd677a8f10f435f8725476d39227/public/favicon.ico -------------------------------------------------------------------------------- /src/agenda/agenda.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { AgendaResolver } from './agenda.resolver' 4 | import { AgendaService } from './agenda.service' 5 | import { AgendaSchema } from './schemas/agenda.schemas' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'Agenda', schema: AgendaSchema }])], 9 | providers: [AgendaService, AgendaResolver], 10 | }) 11 | export class AgendaModule {} 12 | -------------------------------------------------------------------------------- /src/agenda/agenda.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { AgendaService } from './agenda.service' 4 | import { AgendaModel } from './models/agenda.model' 5 | import { CreateAgendaInput } from './dtos/create-agenda.input' 6 | import { UpdateAgendaInput } from './dtos/update-agenda.input' 7 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 8 | 9 | @Resolver(() => AgendaModel) 10 | export class AgendaResolver { 11 | constructor(private readonly agendaService: AgendaService) { 12 | this.agendaService = agendaService 13 | } 14 | 15 | @Query(() => [AgendaModel]) 16 | @UseGuards(JwtAuthGuard) 17 | public async getAgenda() { 18 | return this.agendaService.findAll() 19 | } 20 | 21 | @Mutation(() => AgendaModel) 22 | @UseGuards(JwtAuthGuard) 23 | public async createAgenda(@Args('input') input: CreateAgendaInput) { 24 | return this.agendaService.create(input) 25 | } 26 | 27 | @Mutation(() => AgendaModel) 28 | @UseGuards(JwtAuthGuard) 29 | public async updateAgendaById(@Args('input') input: UpdateAgendaInput) { 30 | return this.agendaService.update(input) 31 | } 32 | 33 | @Mutation(() => AgendaModel) 34 | @UseGuards(JwtAuthGuard) 35 | public async deleteAgendaById(@Args({ name: 'id', type: () => ID }) id: string) { 36 | return this.agendaService.deleteOneById(id) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/agenda/agenda.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { Model } from 'mongoose' 3 | import { InjectModel } from '@nestjs/mongoose' 4 | import { Agenda } from './interfaces/agenda.interface' 5 | import { CreateAgendaInput } from './dtos/create-agenda.input' 6 | import { UpdateAgendaInput } from './dtos/update-agenda.input' 7 | 8 | @Injectable() 9 | export class AgendaService { 10 | constructor( 11 | @InjectModel('Agenda') 12 | private readonly agendaModel: Model, 13 | ) { 14 | this.agendaModel = agendaModel 15 | } 16 | 17 | public async findAll() { 18 | return this.agendaModel.find({}).sort({ updatedAt: -1 }) 19 | } 20 | 21 | public async create(dto: CreateAgendaInput) { 22 | return this.agendaModel.create(dto) 23 | } 24 | 25 | public async update(dto: UpdateAgendaInput) { 26 | const { id, ...rest } = dto 27 | return this.agendaModel.findByIdAndUpdate(id, rest, { new: true }) 28 | } 29 | 30 | public async deleteOneById(id: string) { 31 | return this.agendaModel.findByIdAndDelete(id) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/agenda/dtos/create-agenda.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsDateString, IsBoolean } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateAgendaInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsDateString() 13 | @IsNotEmpty() 14 | public readonly startDate: string 15 | 16 | @Field() 17 | @IsBoolean() 18 | public readonly allDay: boolean 19 | 20 | @Field({ nullable: true }) 21 | public readonly notes?: string 22 | 23 | @Field({ nullable: true }) 24 | public readonly endDate?: string 25 | 26 | @Field({ nullable: true }) 27 | public readonly rRule?: string 28 | 29 | @Field({ nullable: true }) 30 | public readonly exDate?: string 31 | } 32 | -------------------------------------------------------------------------------- /src/agenda/dtos/update-agenda.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsUUID } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdateAgendaInput { 6 | @Field() 7 | @IsUUID() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field({ nullable: true }) 12 | public readonly title?: string 13 | 14 | @Field({ nullable: true }) 15 | public readonly startDate?: string 16 | 17 | @Field({ nullable: true }) 18 | public readonly endDate?: string 19 | 20 | @Field({ nullable: true }) 21 | public readonly allDay?: boolean 22 | 23 | @Field({ nullable: true }) 24 | public readonly notes?: string 25 | 26 | @Field({ nullable: true }) 27 | public readonly rRule?: string 28 | 29 | @Field({ nullable: true }) 30 | public readonly exDate?: string 31 | } 32 | -------------------------------------------------------------------------------- /src/agenda/interfaces/agenda.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface Agenda extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly startDate: string 7 | readonly endDate?: string 8 | readonly rRule?: string 9 | readonly exDate?: string 10 | readonly createdAt: Date 11 | readonly updatedAt: Date 12 | } 13 | -------------------------------------------------------------------------------- /src/agenda/models/agenda.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class AgendaModel { 5 | @Field({ nullable: false }) 6 | public _id: string 7 | 8 | @Field({ nullable: false }) 9 | public title: string 10 | 11 | @Field({ nullable: false }) 12 | public startDate: Date 13 | 14 | @Field({ nullable: true }) 15 | public endDate?: Date 16 | 17 | @Field() 18 | public allDay: boolean 19 | 20 | @Field({ nullable: true }) 21 | public notes?: string 22 | 23 | @Field({ nullable: true }) 24 | public rRule?: string 25 | 26 | @Field({ nullable: true }) 27 | public exDate?: Date 28 | 29 | @Field({ nullable: false }) 30 | public createdAt: Date 31 | 32 | @Field({ nullable: false }) 33 | public updatedAt: Date 34 | } 35 | -------------------------------------------------------------------------------- /src/agenda/schemas/agenda.schemas.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const AgendaSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | startDate: { 15 | type: Date, 16 | required: true, 17 | }, 18 | endDate: { 19 | type: Date, 20 | required: false, 21 | }, 22 | allDay: { 23 | type: Boolean, 24 | required: true, 25 | }, 26 | notes: { 27 | type: String, 28 | required: false, 29 | default: '', 30 | }, 31 | rRule: { 32 | type: String, 33 | required: false, 34 | }, 35 | exDate: { 36 | type: Date, 37 | required: false, 38 | }, 39 | }, 40 | { 41 | collection: 'agenda', 42 | timestamps: true, 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /src/announcements/announcements.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { AnnouncementsResolver } from './announcements.resolver' 4 | import { AnnouncementsService } from './announcements.service' 5 | import { AnnouncementSchema } from './schemas/announcements.schema' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'Announcement', schema: AnnouncementSchema }])], 9 | providers: [AnnouncementsService, AnnouncementsResolver], 10 | }) 11 | export class AnnouncementsModule {} 12 | -------------------------------------------------------------------------------- /src/announcements/announcements.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { AnnouncementsService } from './announcements.service' 4 | import { AnnouncementModel } from './models/announcements.model' 5 | import { CreateAnnouncementInput } from './dtos/create-announcement.input' 6 | import { UpdateAnnouncementInput } from './dtos/update-announcement.input' 7 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 10 | 11 | @Resolver(() => AnnouncementModel) 12 | export class AnnouncementsResolver { 13 | constructor(private readonly announcementsService: AnnouncementsService) { 14 | this.announcementsService = announcementsService 15 | } 16 | 17 | @Query(() => [AnnouncementModel]) 18 | public async getAnnouncements(): Promise { 19 | return this.announcementsService.findAll() 20 | } 21 | 22 | @Query(() => AnnouncementModel) 23 | public async getAnnouncementById( 24 | @Args({ name: 'id', type: () => ID }) id: string, 25 | ): Promise { 26 | return this.announcementsService.findOneById(id) 27 | } 28 | 29 | @Mutation(() => AnnouncementModel) 30 | @UseGuards(JwtAuthGuard) 31 | public async createAnnouncement( 32 | @Args('input') input: CreateAnnouncementInput, 33 | ): Promise { 34 | return this.announcementsService.create(input) 35 | } 36 | 37 | @Mutation(() => AnnouncementModel) 38 | @UseGuards(JwtAuthGuard) 39 | public async updateAnnouncementById( 40 | @Args('input') input: UpdateAnnouncementInput, 41 | ): Promise { 42 | return this.announcementsService.update(input) 43 | } 44 | 45 | @Mutation(() => [AnnouncementModel]) 46 | @UseGuards(JwtAuthGuard) 47 | public async exchangePositionAnnouncement( 48 | @Args('input') input: ExchangePositionInput, 49 | ): Promise { 50 | return this.announcementsService.exchangePosition(input) 51 | } 52 | 53 | @Mutation(() => AnnouncementModel) 54 | @UseGuards(JwtAuthGuard) 55 | public async deleteAnnouncementById( 56 | @Args({ name: 'id', type: () => ID }) id: string, 57 | ): Promise { 58 | return this.announcementsService.deleteOneById(id) 59 | } 60 | 61 | @Mutation(() => BatchDeleteModel) 62 | @UseGuards(JwtAuthGuard) 63 | public async deleteAnnouncements(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 64 | return this.announcementsService.batchDelete(ids) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/announcements/announcements.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { Model } from 'mongoose' 3 | import { InjectModel } from '@nestjs/mongoose' 4 | import { Announcement } from './interfaces/announcement.interface' 5 | import { CreateAnnouncementInput } from './dtos/create-announcement.input' 6 | import { UpdateAnnouncementInput } from './dtos/update-announcement.input' 7 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 8 | 9 | @Injectable() 10 | export class AnnouncementsService { 11 | constructor( 12 | @InjectModel('Announcement') 13 | private readonly announcementModel: Model, 14 | ) { 15 | this.announcementModel = announcementModel 16 | } 17 | 18 | public async findAll() { 19 | return this.announcementModel.find().sort({ weight: -1 }) 20 | } 21 | 22 | public async findOneById(id: string) { 23 | return this.announcementModel.findById(id) 24 | } 25 | 26 | public async create(input: CreateAnnouncementInput) { 27 | const all = await this.findAll() 28 | const weight = all[0] ? all[0].weight : 0 29 | return this.announcementModel.create({ ...input, weight: weight + 1 }) 30 | } 31 | 32 | public async update(input: UpdateAnnouncementInput) { 33 | const { id, content } = input 34 | 35 | return this.announcementModel.findByIdAndUpdate( 36 | id, 37 | { 38 | content, 39 | }, 40 | { new: true }, 41 | ) 42 | } 43 | 44 | public async exchangePosition(input: ExchangePositionInput) { 45 | const { id, exchangedId, weight, exchangedWeight } = input 46 | 47 | const exchanged = await this.announcementModel.findByIdAndUpdate( 48 | exchangedId, 49 | { 50 | weight, 51 | }, 52 | { new: true }, 53 | ) 54 | 55 | const curr = await this.announcementModel.findByIdAndUpdate( 56 | id, 57 | { 58 | weight: exchangedWeight, 59 | }, 60 | { new: true }, 61 | ) 62 | 63 | return [exchanged, curr] 64 | } 65 | 66 | public async deleteOneById(id: string) { 67 | return this.announcementModel.findByIdAndDelete(id) 68 | } 69 | 70 | public async batchDelete(ids: string[]) { 71 | const res = await this.announcementModel.deleteMany({ 72 | _id: { $in: ids }, 73 | }) 74 | 75 | return { 76 | ...res, 77 | ids, 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/announcements/dtos/create-announcement.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | 4 | @InputType({ description: 'The input type for creating an announcement.' }) 5 | export class CreateAnnouncementInput { 6 | @Field({ 7 | description: 'Announcement content.', 8 | nullable: false, 9 | }) 10 | @IsString() 11 | @IsNotEmpty() 12 | public readonly content: string 13 | } 14 | -------------------------------------------------------------------------------- /src/announcements/dtos/update-announcement.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdateAnnouncementInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly content: string 15 | } 16 | -------------------------------------------------------------------------------- /src/announcements/interfaces/announcement.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface Announcement extends Document { 4 | readonly _id: string 5 | readonly content: string 6 | readonly weight: number 7 | readonly createdAt: Date 8 | readonly updatedAt: Date 9 | } 10 | -------------------------------------------------------------------------------- /src/announcements/models/announcements.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class AnnouncementModel { 5 | @Field({ nullable: false }) 6 | public _id: string 7 | 8 | @Field({ nullable: false }) 9 | public content: string 10 | 11 | @Field({ nullable: false }) 12 | public weight: number 13 | 14 | @Field({ nullable: false }) 15 | public createdAt: Date 16 | 17 | @Field({ nullable: false }) 18 | public updatedAt: Date 19 | } 20 | -------------------------------------------------------------------------------- /src/announcements/schemas/announcements.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const AnnouncementSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | content: { 11 | type: String, 12 | required: true, 13 | }, 14 | weight: { 15 | type: Number, 16 | require: true, 17 | }, 18 | }, 19 | { 20 | collection: 'announcement', 21 | timestamps: true, 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { APP_PIPE, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core' 3 | import { GraphQLValidationPipe } from './shared/pipes/GraphQLValidation.pipe' 4 | import { RolesGuard } from './shared/guard/roles.guard' 5 | import { DelayInterceptor } from './shared/interceptors/delay.interceptor' 6 | import { ConfigModule } from './config/config.module' 7 | import { DataBaseModule } from './database/database.module' 8 | import { GraphqlModule } from './graphql/graphqls.module' 9 | import { AuthModule } from './auth/auth.module' 10 | import { UsersModule } from './users/users.module' 11 | import { AnnouncementsModule } from './announcements/announcements.module' 12 | import { OpenSourcesModule } from './open-sources/open-sources.module' 13 | import { BandwagonModule } from './bandwagon/bandwagon.module' 14 | import { LiveToursModule } from './live-tours/live-tours.module' 15 | import { YanceyMusicModule } from './yancey-music/yancey-music.module' 16 | import { BestAlbumsModule } from './best-albums/best-albums.module' 17 | import { PlayerModule } from './player/player.module' 18 | import { AgendaModule } from './agenda/agenda.module' 19 | import { PostsModule } from './posts/posts.module' 20 | import { MottosModule } from './mottos/mottos.module' 21 | import { CoversModule } from './covers/covers.module' 22 | import { GlobalSettingModule } from './global-setting/global-setting.module' 23 | import { PostStatisticsModule } from './post-statistics/post-statistics.module' 24 | import { LoggerModule } from './shared/logger/logger.module' 25 | 26 | @Module({ 27 | imports: [ 28 | ConfigModule, 29 | GraphqlModule, 30 | DataBaseModule, 31 | LoggerModule, 32 | AuthModule, 33 | UsersModule, 34 | AnnouncementsModule, 35 | OpenSourcesModule, 36 | BandwagonModule, 37 | LiveToursModule, 38 | YanceyMusicModule, 39 | BestAlbumsModule, 40 | PlayerModule, 41 | AgendaModule, 42 | PostsModule, 43 | MottosModule, 44 | CoversModule, 45 | GlobalSettingModule, 46 | PostStatisticsModule, 47 | ], 48 | 49 | providers: [ 50 | { 51 | provide: APP_PIPE, 52 | useClass: GraphQLValidationPipe, 53 | }, 54 | 55 | { 56 | provide: APP_GUARD, 57 | useClass: RolesGuard, 58 | }, 59 | 60 | { 61 | provide: APP_INTERCEPTOR, 62 | useClass: DelayInterceptor, 63 | }, 64 | ], 65 | }) 66 | export class AppModule {} 67 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { HttpModule } from '@nestjs/axios' 3 | import { JwtModule } from '@nestjs/jwt' 4 | import { PassportModule } from '@nestjs/passport' 5 | import { AuthResolver } from './auth.resolver' 6 | import { AuthService } from './auth.service' 7 | import { UsersModule } from '../users/users.module' 8 | import { JwtStrategy } from './jwt.strategy' 9 | import { ConfigModule } from '../config/config.module' 10 | import { ConfigService } from '../config/config.service' 11 | 12 | const PassPortModule = PassportModule.register({ 13 | defaultStrategy: 'jwt', 14 | }) 15 | 16 | @Module({ 17 | imports: [ 18 | HttpModule.registerAsync({ 19 | useFactory: () => ({ 20 | timeout: 10000, 21 | maxRedirects: 5, 22 | }), 23 | }), 24 | ConfigModule, 25 | PassPortModule, 26 | UsersModule, 27 | JwtModule.registerAsync({ 28 | useFactory: async (configService: ConfigService) => ({ 29 | secret: configService.getJWTSecretKey(), 30 | signOptions: { expiresIn: configService.getJWTExpiresTime() }, 31 | }), 32 | inject: [ConfigService], 33 | }), 34 | ], 35 | providers: [AuthResolver, AuthService, JwtStrategy], 36 | exports: [AuthService, PassPortModule], 37 | }) 38 | export class AuthModule {} 39 | -------------------------------------------------------------------------------- /src/auth/auth.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation } from '@nestjs/graphql' 3 | import { Request } from 'express' 4 | import { AuthService } from './auth.service' 5 | import { UserModel } from '../users/models/user.model' 6 | import { TOTPModel } from './models/totp.model' 7 | import { IPModel } from './models/ip-model' 8 | import { RecoveryCodeModel } from './models/recovery-code.model' 9 | import { LoginInput } from './dtos/login.input' 10 | import { RegisterInput } from './dtos/register.input' 11 | import { ValidateTOTPInput } from './dtos/validate-totp.input' 12 | import { ChangePasswordInput } from './dtos/change-password.input' 13 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 14 | import { ReqDecorator } from '../shared/decorators/req.decorator' 15 | 16 | @Resolver(() => UserModel) 17 | export class AuthResolver { 18 | constructor(private readonly authService: AuthService) { 19 | this.authService = authService 20 | } 21 | 22 | @Query(() => UserModel) 23 | public async login(@Args('input') input: LoginInput) { 24 | return this.authService.login(input) 25 | } 26 | 27 | // 暂时不开放注册 28 | // @Mutation(() => UserModel) 29 | // public async register(@Args('input') input: RegisterInput) { 30 | // return this.authService.register(input) 31 | // } 32 | 33 | @Mutation(() => TOTPModel) 34 | @UseGuards(JwtAuthGuard) 35 | public async createTOTP(@ReqDecorator() req: Request) { 36 | return this.authService.createTOTP(req.headers.authorization) 37 | } 38 | 39 | @Mutation(() => UserModel) 40 | @UseGuards(JwtAuthGuard) 41 | public async validateTOTP(@Args('input') input: ValidateTOTPInput, @ReqDecorator() req: Request) { 42 | return this.authService.validateTOTP(input, req.headers.authorization) 43 | } 44 | 45 | @Mutation(() => RecoveryCodeModel) 46 | @UseGuards(JwtAuthGuard) 47 | public async createRecoveryCodes(@ReqDecorator() req: Request) { 48 | return this.authService.createRecoveryCodes(req.headers.authorization) 49 | } 50 | 51 | @Mutation(() => UserModel) 52 | @UseGuards(JwtAuthGuard) 53 | public async validateRecoveryCode( 54 | @Args('input') input: ValidateTOTPInput, 55 | @ReqDecorator() req: Request, 56 | ) { 57 | return this.authService.validateRecoveryCode(input, req.headers.authorization) 58 | } 59 | 60 | @Mutation(() => UserModel) 61 | @UseGuards(JwtAuthGuard) 62 | public async changePassword( 63 | @Args('input') input: ChangePasswordInput, 64 | @ReqDecorator() req: Request, 65 | ) { 66 | return this.authService.changePassword(input, req.headers.authorization) 67 | } 68 | 69 | @Mutation(() => IPModel) 70 | @UseGuards(JwtAuthGuard) 71 | public async loginStatistics(@ReqDecorator() req: Request) { 72 | return this.authService.loginStatistics(req) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/auth/dtos/change-password.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsString } from 'class-validator' 3 | 4 | @InputType() 5 | export class ChangePasswordInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly oldPassword: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly newPassword: string 15 | } 16 | -------------------------------------------------------------------------------- /src/auth/dtos/login.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsEmail } from 'class-validator' 3 | 4 | @InputType() 5 | export class LoginInput { 6 | @Field() 7 | @IsEmail() 8 | @IsNotEmpty() 9 | public readonly email: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly password: string 15 | 16 | @Field() 17 | @IsString() 18 | @IsNotEmpty() 19 | public readonly token: string 20 | } 21 | -------------------------------------------------------------------------------- /src/auth/dtos/register.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsEmail, MinLength, MaxLength, NotContains } from 'class-validator' 3 | 4 | @InputType() 5 | export class RegisterInput { 6 | @Field() 7 | @IsEmail() 8 | @IsNotEmpty() 9 | public readonly email: string 10 | 11 | @Field() 12 | @IsString() 13 | @NotContains('@') 14 | @IsNotEmpty() 15 | public readonly username: string 16 | 17 | @Field() 18 | @IsString() 19 | @MinLength(6) 20 | @MaxLength(20) 21 | @IsNotEmpty() 22 | public readonly password: string 23 | } 24 | -------------------------------------------------------------------------------- /src/auth/dtos/validate-totp.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsString } from 'class-validator' 3 | 4 | @InputType() 5 | export class ValidateTOTPInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly code: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly key: string 15 | } 16 | -------------------------------------------------------------------------------- /src/auth/interfaces/jwt.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Payload { 2 | sub: string 3 | email: string 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/interfaces/login.interface.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../../users/interfaces/user.interface' 2 | 3 | interface Authorization { 4 | authorization: string 5 | } 6 | 7 | export type LoginRes = Authorization & Omit 8 | -------------------------------------------------------------------------------- /src/auth/interfaces/recaptcha.interface.ts: -------------------------------------------------------------------------------- 1 | export interface GoogleRecaptchaRes { 2 | success: boolean 3 | score: number // the score for this request (0.0 - 1.0) 4 | action: string // the action name for this request (important to verify) 5 | challenge_ts: string // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ) 6 | hostname: string // the hostname of the site where the reCAPTCHA was solved 7 | 'error-codes'?: string[] // optional 8 | } 9 | -------------------------------------------------------------------------------- /src/auth/interfaces/validate.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Validate { 2 | userId: string 3 | email: string 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt' 2 | import { PassportStrategy } from '@nestjs/passport' 3 | import { Injectable } from '@nestjs/common' 4 | import { Payload } from './interfaces/jwt.interface' 5 | import { Validate } from './interfaces/validate.interface' 6 | import { ConfigService } from '../config/config.service' 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy) { 10 | constructor(configService: ConfigService) { 11 | const JWT_SECRET_KEY = configService.getJWTSecretKey() 12 | 13 | super({ 14 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 15 | ignoreExpiration: false, 16 | secretOrKey: JWT_SECRET_KEY, 17 | }) 18 | } 19 | 20 | public async validate(payload: Payload): Promise { 21 | const signup = { userId: payload.sub, email: payload.email } 22 | return signup 23 | } 24 | } 25 | 26 | // JwtStrategy 用于保护指定接口 27 | -------------------------------------------------------------------------------- /src/auth/models/ip-model.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | 3 | import { Field, ObjectType } from '@nestjs/graphql' 4 | 5 | @ObjectType() 6 | export class Language { 7 | @Field() 8 | public readonly code: string 9 | 10 | @Field() 11 | public readonly name: string 12 | 13 | @Field() 14 | public readonly native: string 15 | } 16 | 17 | @ObjectType() 18 | export class Location { 19 | @Field() 20 | public readonly geoname_id: number 21 | 22 | @Field() 23 | public readonly capital: string 24 | 25 | @Field(() => [Language]) 26 | public readonly languages: Language[] 27 | 28 | @Field() 29 | public readonly country_flag: string 30 | 31 | @Field() 32 | public readonly country_flag_emoji: string 33 | 34 | @Field() 35 | public readonly country_flag_emoji_unicode: string 36 | 37 | @Field() 38 | public readonly calling_code: string 39 | 40 | @Field() 41 | public readonly is_eu: boolean 42 | } 43 | 44 | @ObjectType() 45 | export class Browser { 46 | @Field() 47 | public readonly name: string 48 | 49 | @Field() 50 | public readonly version: string 51 | 52 | @Field() 53 | public readonly major: string 54 | } 55 | 56 | @ObjectType() 57 | export class OS { 58 | @Field() 59 | public readonly name: string 60 | 61 | @Field() 62 | public readonly version: string 63 | } 64 | 65 | @ObjectType() 66 | export class IPModel { 67 | @Field() 68 | public readonly ip: string 69 | 70 | @Field() 71 | public readonly type: string 72 | 73 | @Field() 74 | public readonly continent_code: string 75 | 76 | @Field() 77 | public readonly continent_name: string 78 | 79 | @Field() 80 | public readonly country_code: string 81 | 82 | @Field() 83 | public readonly country_name: string 84 | 85 | @Field() 86 | public readonly region_code: string 87 | 88 | @Field() 89 | public readonly city: string 90 | 91 | @Field() 92 | public readonly zip: string 93 | 94 | @Field() 95 | public readonly latitude: number 96 | 97 | @Field() 98 | public readonly longitude: number 99 | 100 | @Field(() => Location) 101 | public readonly location: Location 102 | 103 | @Field(() => Browser) 104 | public readonly browser: Browser 105 | 106 | @Field(() => OS) 107 | public readonly os: OS 108 | 109 | @Field() 110 | public readonly loginTime: string 111 | } 112 | -------------------------------------------------------------------------------- /src/auth/models/recovery-code.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | import { IsArray } from 'class-validator' 3 | 4 | @ObjectType() 5 | export class RecoveryCodeModel { 6 | @Field(() => [String]) 7 | @IsArray() 8 | public readonly recoveryCodes: string[] 9 | } 10 | -------------------------------------------------------------------------------- /src/auth/models/totp.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class TOTPModel { 5 | @Field() 6 | public readonly qrcode: string 7 | 8 | @Field() 9 | public readonly key: string 10 | } 11 | -------------------------------------------------------------------------------- /src/bandwagon/bandwagon.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { HttpModule } from '@nestjs/axios' 3 | import { BandwagonService } from './bandwagon.service' 4 | import { BandwagonResolver } from './bandwagon.resolver' 5 | 6 | @Module({ 7 | imports: [ 8 | HttpModule.registerAsync({ 9 | useFactory: () => ({ 10 | timeout: 5000, 11 | maxRedirects: 5, 12 | }), 13 | }), 14 | ], 15 | providers: [BandwagonService, BandwagonResolver], 16 | }) 17 | export class BandwagonModule {} 18 | -------------------------------------------------------------------------------- /src/bandwagon/bandwagon.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Resolver, Query } from '@nestjs/graphql' 3 | import { BandwagonService } from './bandwagon.service' 4 | import { ServiceInfoModel } from './models/service-info.model' 5 | import { UsageStatesModel } from './models/usage-stats.model' 6 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 7 | 8 | @Resolver('Bandwagon') 9 | export class BandwagonResolver { 10 | constructor(private readonly bandwagonService: BandwagonService) { 11 | this.bandwagonService = bandwagonService 12 | } 13 | 14 | @Query(() => ServiceInfoModel) 15 | @UseGuards(JwtAuthGuard) 16 | public getBanwagonServiceInfo() { 17 | return this.bandwagonService.getServiceInfo() 18 | } 19 | 20 | @Query(() => [UsageStatesModel]) 21 | @UseGuards(JwtAuthGuard) 22 | public getBanwagonUsageStats() { 23 | return this.bandwagonService.getUsageStats() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/bandwagon/bandwagon.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { HttpService } from '@nestjs/axios' 3 | import { Observable } from 'rxjs' 4 | import { map } from 'rxjs/operators' 5 | import { BandwagonParams } from './interfaces/bandwagon-params.interface' 6 | import { ServiceInfo } from './interfaces/service-info.interface' 7 | import { UsageStats } from './interfaces/usage-stats.interface' 8 | import { ConfigService } from '../config/config.service' 9 | import { BANDWAGON_SERVICE_INFO_URL, BANDWAGON_USAGE_STATS_URL } from '../shared/constants' 10 | 11 | @Injectable() 12 | export class BandwagonService { 13 | private readonly params: BandwagonParams 14 | 15 | constructor(private readonly httpService: HttpService, configService: ConfigService) { 16 | this.httpService = httpService 17 | const { BANDWAGON_SECRET_KEY, BANDWAGON_SERVER_ID } = configService.getBandwagonKeys() 18 | this.params = { veid: BANDWAGON_SERVER_ID, api_key: BANDWAGON_SECRET_KEY } 19 | } 20 | 21 | public getServiceInfo(): Observable { 22 | return this.httpService 23 | .get(BANDWAGON_SERVICE_INFO_URL, { 24 | params: this.params, 25 | }) 26 | .pipe(map((response) => response.data)) 27 | } 28 | 29 | public getUsageStats(): Observable { 30 | return this.httpService 31 | .get(BANDWAGON_USAGE_STATS_URL, { params: this.params }) 32 | .pipe(map((response) => response.data.data)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/bandwagon/interfaces/bandwagon-params.interface.ts: -------------------------------------------------------------------------------- 1 | export interface BandwagonParams { 2 | veid: string 3 | api_key: string 4 | } 5 | -------------------------------------------------------------------------------- /src/bandwagon/interfaces/service-info.interface.ts: -------------------------------------------------------------------------------- 1 | export interface ServiceInfo { 2 | vm_type: string 3 | ve_status: string 4 | ve_mac1: string 5 | ve_used_disk_space_b: string 6 | ve_disk_quota_gb: string 7 | is_cpu_throttled: string 8 | ssh_port: number 9 | live_hostname: string 10 | load_average: string 11 | mem_available_kb: number 12 | swap_total_kb: number 13 | swap_available_kb: number 14 | hostname: string 15 | node_ip: string 16 | node_alias: string 17 | node_location: string 18 | node_location_id: string 19 | node_datacenter: string 20 | location_ipv6_ready: boolean 21 | plan: string 22 | plan_monthly_data: number 23 | monthly_data_multiplier: number 24 | plan_disk: number 25 | plan_ram: number 26 | plan_swap: number 27 | plan_max_ipv6s: number 28 | os: string 29 | email: string 30 | data_counter: number 31 | data_next_reset: number 32 | ip_addresses: string[] 33 | private_ip_addresses: string[] 34 | ip_nullroutes: string[] 35 | iso1: string | null 36 | iso2: string | null 37 | available_isos: string[] 38 | plan_private_network_available: boolean 39 | location_private_network_available: boolean 40 | rdns_api_available: boolean 41 | ptr: { [index: string]: string | null } 42 | suspended: boolean 43 | policy_violation: boolean 44 | suspension_count: number | null 45 | total_abuse_points: number 46 | max_abuse_points: number 47 | error: number 48 | veid: number 49 | } 50 | -------------------------------------------------------------------------------- /src/bandwagon/interfaces/usage-stats.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UsageStatsItem { 2 | timestamp: string 3 | network_in_bytes: string 4 | network_out_bytes: string 5 | disk_read_bytes: string 6 | disk_write_bytes: string 7 | cpu_usage: string 8 | } 9 | 10 | export interface UsageStats { 11 | data: UsageStatsItem[] 12 | } 13 | -------------------------------------------------------------------------------- /src/bandwagon/models/service-info.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | import { IsString, IsNumber, IsBoolean, IsNotEmpty, IsArray } from 'class-validator' 3 | 4 | @ObjectType() 5 | export class ServiceInfoModel { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly vm_type: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly ve_status: string 15 | 16 | @Field() 17 | @IsString() 18 | @IsNotEmpty() 19 | public readonly ve_mac1: string 20 | 21 | @Field() 22 | @IsString() 23 | @IsNotEmpty() 24 | public readonly ve_used_disk_space_b: string 25 | 26 | @Field() 27 | @IsString() 28 | @IsNotEmpty() 29 | public readonly is_cpu_throttled: string 30 | 31 | @Field() 32 | @IsNumber() 33 | @IsNotEmpty() 34 | public readonly ssh_port: number 35 | 36 | @Field() 37 | @IsString() 38 | @IsNotEmpty() 39 | public readonly live_hostname: string 40 | 41 | @Field() 42 | @IsString() 43 | @IsNotEmpty() 44 | public readonly load_average: string 45 | 46 | @Field() 47 | @IsNumber() 48 | @IsNotEmpty() 49 | public readonly mem_available_kb: number 50 | 51 | @Field() 52 | @IsNumber() 53 | @IsNotEmpty() 54 | public readonly swap_total_kb: number 55 | 56 | @Field() 57 | @IsNumber() 58 | @IsNotEmpty() 59 | public readonly swap_available_kb: number 60 | 61 | @Field() 62 | @IsString() 63 | @IsNotEmpty() 64 | public readonly hostname: string 65 | 66 | @Field() 67 | @IsString() 68 | @IsNotEmpty() 69 | public readonly node_ip: string 70 | 71 | @Field() 72 | @IsString() 73 | @IsNotEmpty() 74 | public readonly node_alias: string 75 | 76 | @Field() 77 | @IsString() 78 | @IsNotEmpty() 79 | public readonly node_location: string 80 | 81 | @Field() 82 | @IsString() 83 | @IsNotEmpty() 84 | public readonly node_location_id: string 85 | 86 | @Field() 87 | @IsString() 88 | @IsNotEmpty() 89 | public readonly node_datacenter: string 90 | 91 | @Field() 92 | @IsBoolean() 93 | @IsNotEmpty() 94 | public readonly location_ipv6_ready: boolean 95 | 96 | @Field() 97 | @IsString() 98 | @IsNotEmpty() 99 | public readonly plan: string 100 | 101 | @Field() 102 | @IsNumber() 103 | @IsNotEmpty() 104 | public readonly plan_monthly_data: number 105 | 106 | @Field() 107 | @IsNumber() 108 | @IsNotEmpty() 109 | public readonly monthly_data_multiplier: number 110 | 111 | @Field() 112 | @IsNumber() 113 | @IsNotEmpty() 114 | public readonly plan_disk: number 115 | 116 | @Field() 117 | @IsNumber() 118 | @IsNotEmpty() 119 | public readonly plan_ram: number 120 | 121 | @Field() 122 | @IsNumber() 123 | @IsNotEmpty() 124 | public readonly plan_swap: number 125 | 126 | @Field() 127 | @IsNumber() 128 | @IsNotEmpty() 129 | public readonly plan_max_ipv6s: number 130 | 131 | @Field() 132 | @IsString() 133 | @IsNotEmpty() 134 | public readonly os: string 135 | 136 | @Field() 137 | @IsString() 138 | @IsNotEmpty() 139 | public readonly email: string 140 | 141 | @Field() 142 | @IsNumber() 143 | @IsNotEmpty() 144 | public readonly data_counter: number 145 | 146 | @Field() 147 | @IsNumber() 148 | @IsNotEmpty() 149 | public readonly data_next_reset: number 150 | 151 | @Field(() => [String]) 152 | @IsArray() 153 | public readonly ip_addresses: string[] 154 | 155 | @Field(() => [String]) 156 | @IsArray() 157 | public readonly private_ip_addresses: string[] 158 | 159 | @Field(() => [String]) 160 | @IsArray() 161 | public readonly ip_nullroutes: string[] 162 | 163 | @Field({ nullable: true }) 164 | @IsString() 165 | public readonly iso1: string | null 166 | 167 | @Field({ nullable: true }) 168 | @IsString() 169 | public readonly iso2: string | null 170 | 171 | @Field(() => [String]) 172 | @IsArray() 173 | public readonly available_isos: string[] 174 | 175 | @Field() 176 | @IsBoolean() 177 | @IsNotEmpty() 178 | public readonly plan_private_network_available: boolean 179 | 180 | @Field() 181 | @IsBoolean() 182 | @IsNotEmpty() 183 | public readonly location_private_network_available: boolean 184 | 185 | @Field() 186 | @IsBoolean() 187 | @IsNotEmpty() 188 | public readonly rdns_api_available: boolean 189 | 190 | // FIXME: 191 | @Field(() => String) 192 | public readonly ptr: { [index: string]: string | null } 193 | 194 | @Field() 195 | @IsBoolean() 196 | @IsNotEmpty() 197 | public readonly suspended: boolean 198 | 199 | @Field() 200 | @IsBoolean() 201 | @IsNotEmpty() 202 | public readonly policy_violation: boolean 203 | 204 | @Field({ nullable: true }) 205 | @IsNumber() 206 | @IsNotEmpty() 207 | public readonly suspension_count: number | null 208 | 209 | @Field() 210 | @IsNumber() 211 | @IsNotEmpty() 212 | public readonly max_abuse_points: number 213 | 214 | @Field() 215 | @IsNumber() 216 | @IsNotEmpty() 217 | public readonly error: number 218 | 219 | @Field() 220 | @IsNumber() 221 | @IsNotEmpty() 222 | public readonly veid: number 223 | } 224 | -------------------------------------------------------------------------------- /src/bandwagon/models/usage-stats.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | 4 | @ObjectType() 5 | export class UsageStatesModel { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly timestamp: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly network_in_bytes: string 15 | 16 | @Field() 17 | @IsString() 18 | @IsNotEmpty() 19 | public readonly network_out_bytes: string 20 | 21 | @Field() 22 | @IsString() 23 | @IsNotEmpty() 24 | public readonly disk_read_bytes: string 25 | 26 | @Field() 27 | @IsString() 28 | @IsNotEmpty() 29 | public readonly disk_write_bytes: string 30 | 31 | @Field() 32 | @IsString() 33 | @IsNotEmpty() 34 | public readonly cpu_usage: string 35 | } 36 | -------------------------------------------------------------------------------- /src/best-albums/best-albums.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { BestAlbumSchema } from './schemas/best-albums.schema' 4 | import { BestAlbumsResolver } from './best-albums.resolver' 5 | import { BestAlbumsService } from './best-albums.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'BestAlbum', schema: BestAlbumSchema }])], 9 | providers: [BestAlbumsResolver, BestAlbumsService], 10 | }) 11 | export class BestAlbumsModule {} 12 | -------------------------------------------------------------------------------- /src/best-albums/best-albums.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { BestAlbumsService } from './best-albums.service' 4 | import { BestAlbumModel } from './models/best-albums.model' 5 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 6 | import { CreateBestAlbumInput } from './dtos/create-best-album.input' 7 | import { UpdateBestAlbumInput } from './dtos/update-best-album.input' 8 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 9 | 10 | @Resolver(() => BestAlbumModel) 11 | export class BestAlbumsResolver { 12 | constructor(private readonly bestAlbumsService: BestAlbumsService) { 13 | this.bestAlbumsService = bestAlbumsService 14 | } 15 | 16 | @Query(() => [BestAlbumModel]) 17 | public async getBestAlbums() { 18 | return this.bestAlbumsService.findAll() 19 | } 20 | 21 | @Query(() => BestAlbumModel) 22 | public async getBestAlbumById(@Args({ name: 'id', type: () => ID }) id: string) { 23 | return this.bestAlbumsService.findOneById(id) 24 | } 25 | 26 | @Mutation(() => BestAlbumModel) 27 | @UseGuards(JwtAuthGuard) 28 | public async createBestAlbum(@Args('input') input: CreateBestAlbumInput) { 29 | return this.bestAlbumsService.create(input) 30 | } 31 | 32 | @Mutation(() => BestAlbumModel) 33 | @UseGuards(JwtAuthGuard) 34 | public async updateBestAlbumById(@Args('input') input: UpdateBestAlbumInput) { 35 | return this.bestAlbumsService.update(input) 36 | } 37 | 38 | @Mutation(() => BestAlbumModel) 39 | @UseGuards(JwtAuthGuard) 40 | public async deleteBestAlbumById(@Args({ name: 'id', type: () => ID }) id: string) { 41 | return this.bestAlbumsService.deleteOneById(id) 42 | } 43 | 44 | @Mutation(() => BatchDeleteModel) 45 | @UseGuards(JwtAuthGuard) 46 | public async deleteBestAlbums(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 47 | return this.bestAlbumsService.batchDelete(ids) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/best-albums/best-albums.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { CreateBestAlbumInput } from './dtos/create-best-album.input' 5 | import { UpdateBestAlbumInput } from './dtos/update-best-album.input' 6 | import { BestAlbumModel } from './models/best-albums.model' 7 | import { BestAlbum } from './interfaces/best-albums.interface' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | 10 | @Injectable() 11 | export class BestAlbumsService { 12 | constructor( 13 | @InjectModel('BestAlbum') 14 | private readonly bestAlbumModel: Model, 15 | ) { 16 | this.bestAlbumModel = bestAlbumModel 17 | } 18 | 19 | public async findAll(): Promise { 20 | return this.bestAlbumModel.find({}).sort({ updatedAt: -1 }) 21 | } 22 | 23 | public async findOneById(id: string): Promise { 24 | return this.bestAlbumModel.findById(id) 25 | } 26 | 27 | public async create(bestAlbumInput: CreateBestAlbumInput): Promise { 28 | return this.bestAlbumModel.create(bestAlbumInput) 29 | } 30 | 31 | public async update(bestAlbumInput: UpdateBestAlbumInput): Promise { 32 | const { id, ...rest } = bestAlbumInput 33 | return this.bestAlbumModel.findByIdAndUpdate(id, rest, { new: true }) 34 | } 35 | 36 | public async deleteOneById(id: string): Promise { 37 | return this.bestAlbumModel.findByIdAndDelete(id) 38 | } 39 | 40 | public async batchDelete(ids: string[]): Promise { 41 | const res = await this.bestAlbumModel.deleteMany({ 42 | _id: { $in: ids }, 43 | }) 44 | 45 | return { 46 | ...res, 47 | ids, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/best-albums/dtos/create-best-album.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsUrl, IsDate } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateBestAlbumInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly artist: string 15 | 16 | @Field() 17 | @IsUrl() 18 | @IsNotEmpty() 19 | public readonly coverUrl: string 20 | 21 | @Field() 22 | @IsUrl() 23 | @IsNotEmpty() 24 | public readonly mvUrl: string 25 | 26 | @Field() 27 | @IsDate() 28 | @IsNotEmpty() 29 | public readonly releaseDate: Date 30 | } 31 | -------------------------------------------------------------------------------- /src/best-albums/dtos/update-best-album.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsUUID } from 'class-validator' 3 | import { CreateBestAlbumInput } from './create-best-album.input' 4 | 5 | @InputType() 6 | export class UpdateBestAlbumInput extends CreateBestAlbumInput { 7 | @Field() 8 | @IsUUID() 9 | @IsNotEmpty() 10 | public readonly id: string 11 | } 12 | -------------------------------------------------------------------------------- /src/best-albums/interfaces/best-albums.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface BestAlbum extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly artist: string 7 | readonly coverUrl: string 8 | readonly mvUrl: string 9 | readonly releaseDate: Date 10 | readonly createdAt: Date 11 | readonly updatedAt: Date 12 | } 13 | -------------------------------------------------------------------------------- /src/best-albums/models/best-albums.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class BestAlbumModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly title: string 10 | 11 | @Field() 12 | public readonly artist: string 13 | 14 | @Field() 15 | public readonly coverUrl: string 16 | 17 | @Field() 18 | public readonly mvUrl: string 19 | 20 | @Field() 21 | public readonly releaseDate: Date 22 | 23 | @Field() 24 | public readonly createdAt: Date 25 | 26 | @Field() 27 | public readonly updatedAt: Date 28 | } 29 | -------------------------------------------------------------------------------- /src/best-albums/schemas/best-albums.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const BestAlbumSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | artist: { 15 | type: String, 16 | required: true, 17 | }, 18 | coverUrl: { 19 | type: String, 20 | required: true, 21 | }, 22 | mvUrl: { 23 | type: String, 24 | required: true, 25 | }, 26 | releaseDate: { 27 | type: Date, 28 | required: true, 29 | }, 30 | }, 31 | { 32 | collection: 'best_album', 33 | timestamps: true, 34 | }, 35 | ) 36 | -------------------------------------------------------------------------------- /src/config/config.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common' 2 | import { ConfigService } from './config.service' 3 | 4 | @Global() 5 | @Module({ 6 | providers: [ 7 | { 8 | provide: ConfigService, 9 | useValue: new ConfigService(['env/.env', `env/${process.env.NODE_ENV || 'development'}.env`]), 10 | }, 11 | ], 12 | exports: [ConfigService], 13 | }) 14 | export class ConfigModule {} 15 | -------------------------------------------------------------------------------- /src/config/config.service.ts: -------------------------------------------------------------------------------- 1 | import dotenv, { DotenvParseOutput } from 'dotenv' 2 | import Joi, { ObjectSchema } from 'joi' 3 | import fs from 'fs' 4 | import { BandwagonKey } from './interfaces/bandwagon-keys.interface' 5 | 6 | export type EnvConfig = Record 7 | 8 | export class ConfigService { 9 | public readonly isEnvProduction: boolean 10 | 11 | public readonly isEnvDevelopment: boolean 12 | 13 | public readonly isEnvTest: boolean 14 | 15 | private readonly envConfig: EnvConfig 16 | 17 | constructor(filePaths: string[]) { 18 | let config: DotenvParseOutput = {} 19 | filePaths.forEach((filePath) => { 20 | const _config = dotenv.parse(fs.readFileSync(filePath)) 21 | config = { ...config, ..._config } 22 | }) 23 | this.envConfig = this.validateEnvFile(config) 24 | this.isEnvProduction = this.getNodeEnv() === 'production' 25 | this.isEnvDevelopment = this.getNodeEnv() === 'development' 26 | this.isEnvTest = this.getNodeEnv() === 'test' 27 | } 28 | 29 | public getNodeEnv(): string { 30 | return this.get('NODE_ENV') 31 | } 32 | 33 | public getMongoURI(): string { 34 | const host = this.get('DATABASE_HOST') 35 | const port = this.get('DATABASE_PORT') 36 | const userName = this.get('DATABASE_USER') 37 | const userPwd = this.get('DATABASE_PWD') 38 | const collection = this.get('DATABASE_COLLECTION') 39 | 40 | const prefix = 'mongodb://' 41 | const auth = `${userName}:${userPwd}@` 42 | const connection = `${host}:${port}/${collection}` 43 | 44 | return this.isEnvProduction ? `${prefix}${auth}${connection}` : `${prefix}${connection}` 45 | } 46 | 47 | public getBandwagonKeys(): BandwagonKey { 48 | return { 49 | BANDWAGON_SECRET_KEY: this.get('BANDWAGON_SECRET_KEY'), 50 | BANDWAGON_SERVER_ID: this.get('BANDWAGON_SERVER_ID'), 51 | } 52 | } 53 | 54 | public getIpStackAccessKey(): string { 55 | return this.get('IP_STACK_ACCESS_KEY') 56 | } 57 | 58 | public getJWTSecretKey(): string { 59 | return this.get('JWT_SECRET_KEY') 60 | } 61 | 62 | public getJWTExpiresTime(): number { 63 | return this.get('JWT_EXPIRES_TIME') 64 | } 65 | 66 | public needSimulateNetworkThrottle(): boolean { 67 | return this.get('NEED_SIMULATE_NETWORK_THROTTLE') 68 | } 69 | 70 | public getGoogleRecaptchaKey(): string { 71 | return this.get('GOOGLE_RECAPTCHA_KEY') 72 | } 73 | 74 | private validateEnvFile(envConfig: EnvConfig): EnvConfig { 75 | const envVarsSchema: ObjectSchema = Joi.object({ 76 | NODE_ENV: Joi.string() 77 | .valid('development', 'production', 'test') 78 | .default('development') 79 | .required(), 80 | APP_PORT: Joi.number().default(3002).required(), 81 | DATABASE_HOST: Joi.string().required(), 82 | DATABASE_PORT: Joi.number().default(27017).required(), 83 | DATABASE_USER: this.isEnvProduction ? Joi.string().required() : Joi.string().optional(), 84 | DATABASE_PWD: this.isEnvProduction ? Joi.string().required() : Joi.string().optional(), 85 | DATABASE_COLLECTION: Joi.string().required(), 86 | BANDWAGON_SECRET_KEY: Joi.string().required(), 87 | BANDWAGON_SERVER_ID: Joi.string().required(), 88 | IP_STACK_ACCESS_KEY: Joi.string().required(), 89 | JWT_SECRET_KEY: Joi.string().required(), 90 | JWT_EXPIRES_TIME: Joi.number().required(), 91 | GOOGLE_RECAPTCHA_KEY: Joi.string().required(), 92 | NEED_SIMULATE_NETWORK_THROTTLE: Joi.boolean().optional(), 93 | }) 94 | 95 | const { error, value: validatedEnvConfig } = envVarsSchema.validate(envConfig) 96 | if (error) { 97 | throw new Error(`Config validation error: ${error.message}`) 98 | } 99 | return validatedEnvConfig 100 | } 101 | 102 | private get(key: string): T { 103 | return this.envConfig[key] 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/config/interfaces/bandwagon-keys.interface.ts: -------------------------------------------------------------------------------- 1 | export interface BandwagonKey { 2 | BANDWAGON_SECRET_KEY: string 3 | BANDWAGON_SERVER_ID: string 4 | } 5 | -------------------------------------------------------------------------------- /src/covers/covers.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { CoverSchema } from './schemas/covers.schema' 4 | import { CoversResolver } from './covers.resolver' 5 | import { CoversService } from './covers.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'Cover', schema: CoverSchema }])], 9 | providers: [CoversResolver, CoversService], 10 | }) 11 | export class CoversModule {} 12 | -------------------------------------------------------------------------------- /src/covers/covers.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { CoversService } from './covers.service' 4 | import { CoverModel } from './models/covers.model' 5 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 6 | import { BatchUpdateModel } from '../database/models/batch-update.model' 7 | import { CreateCoverInput } from './dtos/create-cover.input' 8 | import { UpdateCoverInput } from './dtos/update-cover.input' 9 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 10 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 11 | 12 | @Resolver(() => CoverModel) 13 | export class CoversResolver { 14 | constructor(private readonly coversService: CoversService) { 15 | this.coversService = coversService 16 | } 17 | 18 | @Query(() => [CoverModel]) 19 | public async getAllPublicCovers(): Promise { 20 | return this.coversService.findAllPubilc() 21 | } 22 | 23 | @Query(() => [CoverModel]) 24 | @UseGuards(JwtAuthGuard) 25 | public async getCovers() { 26 | return this.coversService.findAll() 27 | } 28 | 29 | @Query(() => CoverModel) 30 | @UseGuards(JwtAuthGuard) 31 | public async getCoverById(@Args({ name: 'id', type: () => ID }) id: string) { 32 | return this.coversService.findOneById(id) 33 | } 34 | 35 | @Mutation(() => CoverModel) 36 | @UseGuards(JwtAuthGuard) 37 | public async createCover(@Args('input') input: CreateCoverInput) { 38 | return this.coversService.create(input) 39 | } 40 | 41 | @Mutation(() => CoverModel) 42 | @UseGuards(JwtAuthGuard) 43 | public async updateCoverById(@Args('input') input: UpdateCoverInput) { 44 | return this.coversService.update(input) 45 | } 46 | 47 | @Mutation(() => [CoverModel]) 48 | @UseGuards(JwtAuthGuard) 49 | public async exchangePositionCover( 50 | @Args('input') input: ExchangePositionInput, 51 | ): Promise { 52 | return this.coversService.exchangePosition(input) 53 | } 54 | 55 | @Mutation(() => CoverModel) 56 | @UseGuards(JwtAuthGuard) 57 | public async deleteCoverById(@Args({ name: 'id', type: () => ID }) id: string) { 58 | return this.coversService.deleteOneById(id) 59 | } 60 | 61 | @Mutation(() => BatchDeleteModel) 62 | @UseGuards(JwtAuthGuard) 63 | public async deleteCovers(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 64 | return this.coversService.batchDelete(ids) 65 | } 66 | 67 | @Mutation(() => BatchUpdateModel) 68 | @UseGuards(JwtAuthGuard) 69 | public async publicCovers(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 70 | return this.coversService.batchUpdate(ids) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/covers/covers.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { CreateCoverInput } from './dtos/create-cover.input' 5 | import { UpdateCoverInput } from './dtos/update-cover.input' 6 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 7 | import { CoverModel } from './models/covers.model' 8 | import { Cover } from './interfaces/covers.interface' 9 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 10 | import { BatchUpdateModel } from '../database/models/batch-update.model' 11 | 12 | @Injectable() 13 | export class CoversService { 14 | constructor( 15 | @InjectModel('Cover') 16 | private readonly coverModel: Model, 17 | ) { 18 | this.coverModel = coverModel 19 | } 20 | 21 | public async findAllPubilc() { 22 | return this.coverModel.find({ isPublic: { $ne: false } }).sort({ weight: -1 }) 23 | } 24 | 25 | public async findAll(): Promise { 26 | return this.coverModel.find({}).sort({ updatedAt: -1 }) 27 | } 28 | 29 | public async findOneById(id: string): Promise { 30 | return this.coverModel.findById(id) 31 | } 32 | 33 | public async create(input: CreateCoverInput): Promise { 34 | const all = await this.findAll() 35 | const weight = all[0] ? all[0].weight : 0 36 | return this.coverModel.create({ ...input, weight: weight + 1 }) 37 | } 38 | 39 | public async update(playerInput: UpdateCoverInput): Promise { 40 | const { id, ...rest } = playerInput 41 | return this.coverModel.findByIdAndUpdate(id, rest, { new: true }) 42 | } 43 | 44 | public async exchangePosition(input: ExchangePositionInput) { 45 | const { id, exchangedId, weight, exchangedWeight } = input 46 | 47 | const exchanged = await this.coverModel.findByIdAndUpdate( 48 | exchangedId, 49 | { 50 | weight, 51 | }, 52 | { new: true }, 53 | ) 54 | 55 | const curr = await this.coverModel.findByIdAndUpdate( 56 | id, 57 | { 58 | weight: exchangedWeight, 59 | }, 60 | { new: true }, 61 | ) 62 | 63 | return [exchanged, curr] 64 | } 65 | 66 | public async deleteOneById(id: string): Promise { 67 | return this.coverModel.findByIdAndDelete(id) 68 | } 69 | 70 | public async batchDelete(ids: string[]): Promise { 71 | const res = await this.coverModel.deleteMany({ 72 | _id: { $in: ids }, 73 | }) 74 | 75 | return { 76 | ...res, 77 | ids, 78 | } 79 | } 80 | 81 | public async batchUpdate(ids: string[]): Promise { 82 | const res = await this.coverModel.updateMany( 83 | { 84 | _id: { $in: ids }, 85 | }, 86 | { 87 | $set: { isPublic: false }, 88 | }, 89 | ) 90 | 91 | return { 92 | ...res, 93 | ids, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/covers/dtos/create-cover.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsUrl, IsBoolean } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateCoverInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsUrl() 13 | @IsNotEmpty() 14 | public readonly coverUrl: string 15 | 16 | @Field() 17 | @IsBoolean() 18 | @IsNotEmpty() 19 | public readonly isPublic: boolean 20 | } 21 | -------------------------------------------------------------------------------- /src/covers/dtos/update-cover.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsUUID, IsNotEmpty, IsBoolean } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdateCoverInput { 6 | @Field() 7 | @IsUUID() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field({ nullable: true }) 12 | public readonly title?: string 13 | 14 | @Field({ nullable: true }) 15 | public readonly coverUrl?: string 16 | 17 | @Field({ nullable: true }) 18 | @IsBoolean() 19 | public readonly isPublic?: boolean 20 | } 21 | -------------------------------------------------------------------------------- /src/covers/interfaces/covers.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface Cover extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly coverUrl: string 7 | readonly weight: number 8 | readonly isPublic: boolean 9 | readonly createdAt: Date 10 | readonly updatedAt: Date 11 | } 12 | -------------------------------------------------------------------------------- /src/covers/models/covers.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class CoverModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly title: string 10 | 11 | @Field() 12 | public readonly coverUrl: string 13 | 14 | @Field() 15 | public readonly weight: number 16 | 17 | @Field() 18 | public readonly isPublic: boolean 19 | 20 | @Field() 21 | public readonly createdAt: Date 22 | 23 | @Field() 24 | public readonly updatedAt: Date 25 | } 26 | -------------------------------------------------------------------------------- /src/covers/schemas/covers.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const CoverSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | coverUrl: { 15 | type: String, 16 | required: true, 17 | }, 18 | weight: { 19 | type: Number, 20 | require: true, 21 | }, 22 | isPublic: { 23 | type: Boolean, 24 | required: true, 25 | default: true, 26 | }, 27 | }, 28 | { 29 | collection: 'cover', 30 | timestamps: true, 31 | }, 32 | ) 33 | -------------------------------------------------------------------------------- /src/database/database.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { ConfigService } from '../config/config.service' 4 | 5 | @Module({ 6 | imports: [ 7 | MongooseModule.forRootAsync({ 8 | useFactory: async (configService: ConfigService) => ({ 9 | uri: configService.getMongoURI(), 10 | useUnifiedTopology: true, 11 | useNewUrlParser: true, 12 | }), 13 | inject: [ConfigService], 14 | }), 15 | ], 16 | }) 17 | export class DataBaseModule {} 18 | -------------------------------------------------------------------------------- /src/database/models/batch-delete.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, ID } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class BatchDeleteModel { 5 | @Field({ nullable: true }) 6 | public readonly ok?: number 7 | 8 | @Field({ nullable: true }) 9 | public readonly n?: number 10 | 11 | @Field({ nullable: true }) 12 | public readonly deletedCount?: number 13 | 14 | @Field(() => [ID]) 15 | public readonly ids: string[] 16 | } 17 | -------------------------------------------------------------------------------- /src/database/models/batch-update.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, ID } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class BatchUpdateModel { 5 | @Field({ nullable: true }) 6 | public readonly ok?: number 7 | 8 | @Field({ nullable: true }) 9 | public readonly n?: number 10 | 11 | @Field({ nullable: true }) 12 | public readonly nModified?: number 13 | 14 | @Field(() => [ID]) 15 | public readonly ids: string[] 16 | } 17 | -------------------------------------------------------------------------------- /src/global-setting/dtos/update-global-setting.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsUUID, IsNotEmpty, IsBoolean } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdateGlobalSettingInput { 6 | @Field() 7 | @IsUUID() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field({ nullable: true }) 12 | public readonly releasePostId?: string 13 | 14 | @Field({ nullable: true }) 15 | public readonly cvPostId?: string 16 | 17 | @Field({ nullable: true }) 18 | public readonly isGrayTheme?: boolean 19 | } 20 | -------------------------------------------------------------------------------- /src/global-setting/global-setting.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { GlobalSettingSchema } from './schemas/global-setting.schema' 4 | import { GlobalSettingResolver } from './global-setting.resolver' 5 | import { GlobalSettingService } from './global-setting.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'GlobalSetting', schema: GlobalSettingSchema }])], 9 | providers: [GlobalSettingResolver, GlobalSettingService], 10 | }) 11 | export class GlobalSettingModule {} 12 | -------------------------------------------------------------------------------- /src/global-setting/global-setting.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation } from '@nestjs/graphql' 3 | import { GlobalSettingService } from './global-setting.service' 4 | import { GlobalSettingModel } from './models/global-setting.model' 5 | import { UpdateGlobalSettingInput } from './dtos/update-global-setting.input' 6 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 7 | 8 | @Resolver(() => GlobalSettingModel) 9 | export class GlobalSettingResolver { 10 | constructor(private readonly globalSettingService: GlobalSettingService) { 11 | this.globalSettingService = globalSettingService 12 | } 13 | 14 | @Query(() => GlobalSettingModel) 15 | public async getGlobalSetting() { 16 | return this.globalSettingService.findOne() 17 | } 18 | 19 | @Mutation(() => GlobalSettingModel) 20 | @UseGuards(JwtAuthGuard) 21 | public async updateGlobalSettingById(@Args('input') input: UpdateGlobalSettingInput) { 22 | return this.globalSettingService.update(input) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/global-setting/global-setting.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { UpdateGlobalSettingInput } from './dtos/update-global-setting.input' 5 | import { GlobalSettingModel } from './models/global-setting.model' 6 | import { GlobalSetting } from './interfaces/global-setting.interface' 7 | 8 | @Injectable() 9 | export class GlobalSettingService { 10 | constructor( 11 | @InjectModel('GlobalSetting') 12 | private readonly globalSettingService: Model, 13 | ) { 14 | this.globalSettingService = globalSettingService 15 | } 16 | 17 | public async findOne(): Promise { 18 | const res = await this.globalSettingService.find({}) 19 | 20 | if (res.length !== 0) return res[0] 21 | 22 | return this.globalSettingService.create({}) 23 | } 24 | 25 | public async update(globalSettingInput: UpdateGlobalSettingInput): Promise { 26 | const { id, ...rest } = globalSettingInput 27 | return this.globalSettingService.findByIdAndUpdate(id, rest, { new: true }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/global-setting/interfaces/global-setting.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface GlobalSetting extends Document { 4 | readonly _id: string 5 | readonly releasePostId: string 6 | readonly cvPostId: string 7 | readonly isGrayTheme: boolean 8 | readonly createdAt: Date 9 | readonly updatedAt: Date 10 | } 11 | -------------------------------------------------------------------------------- /src/global-setting/models/global-setting.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class GlobalSettingModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field(() => ID) 9 | public readonly releasePostId: string 10 | 11 | @Field(() => ID) 12 | public readonly cvPostId: string 13 | 14 | @Field() 15 | public readonly isGrayTheme: boolean 16 | 17 | @Field() 18 | public readonly createdAt: Date 19 | 20 | @Field() 21 | public readonly updatedAt: Date 22 | } 23 | -------------------------------------------------------------------------------- /src/global-setting/schemas/global-setting.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const GlobalSettingSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | releasePostId: { 11 | default: '', 12 | type: String, 13 | required: false, 14 | }, 15 | cvPostId: { 16 | default: '', 17 | type: String, 18 | required: false, 19 | }, 20 | isGrayTheme: { 21 | default: false, 22 | type: Boolean, 23 | required: false, 24 | }, 25 | }, 26 | { 27 | collection: 'global-setting', 28 | timestamps: true, 29 | }, 30 | ) 31 | -------------------------------------------------------------------------------- /src/graphql/graphqls.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { GraphQLModule } from '@nestjs/graphql' 3 | import { ValidationError } from 'apollo-server-express' 4 | import { ApolloServerPluginLandingPageLocalDefault } from 'apollo-server-core' 5 | import { ConfigModule } from '../config/config.module' 6 | import { ConfigService } from '../config/config.service' 7 | import { SCHEMA_GQL_FILE_NAME } from '../shared/constants' 8 | import { configCORS } from '../shared/utils' 9 | 10 | @Module({ 11 | imports: [ 12 | GraphQLModule.forRootAsync({ 13 | imports: [ConfigModule], 14 | useFactory: async (configService: ConfigService) => ({ 15 | debug: !configService.isEnvProduction, 16 | playground: false, 17 | introspection: !configService.isEnvProduction, 18 | installSubscriptionHandlers: true, 19 | useGlobalPrefix: true, 20 | typePaths: ['./**/*.gql'], 21 | autoSchemaFile: SCHEMA_GQL_FILE_NAME, 22 | context: ({ req }) => ({ req }), 23 | formatError(error: ValidationError) { 24 | const { 25 | message, 26 | extensions: { code }, 27 | } = error 28 | return configService.isEnvProduction 29 | ? { 30 | code, 31 | message, 32 | timestamp: new Date(), 33 | } 34 | : error 35 | }, 36 | plugins: [ 37 | !configService.isEnvProduction && ApolloServerPluginLandingPageLocalDefault(), 38 | ].filter(Boolean), 39 | cors: configCORS(configService.isEnvProduction), 40 | }), 41 | 42 | inject: [ConfigService], 43 | }), 44 | ], 45 | }) 46 | export class GraphqlModule {} 47 | -------------------------------------------------------------------------------- /src/live-tours/dtos/create-live-tour.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsDate } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateLiveTourInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly posterUrl: string 15 | 16 | @Field() 17 | @IsDate() 18 | @IsNotEmpty() 19 | public readonly showTime: Date 20 | } 21 | -------------------------------------------------------------------------------- /src/live-tours/dtos/update-live-tour.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | import { CreateLiveTourInput } from './create-live-tour.input' 4 | 5 | @InputType() 6 | export class UpdateLiveTourInput extends CreateLiveTourInput { 7 | @Field() 8 | @IsString() 9 | @IsNotEmpty() 10 | public readonly id: string 11 | } 12 | -------------------------------------------------------------------------------- /src/live-tours/interfaces/live-tours.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface LiveTour extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly posterUrl: string 7 | readonly showTime: Date 8 | readonly createdAt: Date 9 | readonly updatedAt: Date 10 | } 11 | -------------------------------------------------------------------------------- /src/live-tours/live-tours.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { LiveToursSchema } from './schemas/live-tours.schema' 4 | import { LiveToursService } from './live-tours.service' 5 | import { LiveToursResolver } from './live-tours.resolver' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'LiveTour', schema: LiveToursSchema }])], 9 | providers: [LiveToursResolver, LiveToursService], 10 | }) 11 | export class LiveToursModule {} 12 | -------------------------------------------------------------------------------- /src/live-tours/live-tours.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { LiveToursService } from './live-tours.service' 4 | import { LiveTourModel } from './models/live-tours.model' 5 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 6 | import { CreateLiveTourInput } from './dtos/create-live-tour.input' 7 | import { UpdateLiveTourInput } from './dtos/update-live-tour.input' 8 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 9 | 10 | @Resolver(() => LiveTourModel) 11 | export class LiveToursResolver { 12 | constructor(private readonly liveToursService: LiveToursService) { 13 | this.liveToursService = liveToursService 14 | } 15 | 16 | @Query(() => [LiveTourModel]) 17 | public async getLiveTours() { 18 | return this.liveToursService.findAll() 19 | } 20 | 21 | @Query(() => LiveTourModel) 22 | public async getLiveTourById(@Args({ name: 'id', type: () => ID }) id: string) { 23 | return this.liveToursService.findOneById(id) 24 | } 25 | 26 | @Mutation(() => LiveTourModel) 27 | @UseGuards(JwtAuthGuard) 28 | public async createLiveTour(@Args('input') input: CreateLiveTourInput) { 29 | return this.liveToursService.create(input) 30 | } 31 | 32 | @Mutation(() => LiveTourModel) 33 | @UseGuards(JwtAuthGuard) 34 | public async updateLiveTourById(@Args('input') input: UpdateLiveTourInput) { 35 | return this.liveToursService.update(input) 36 | } 37 | 38 | @Mutation(() => LiveTourModel) 39 | @UseGuards(JwtAuthGuard) 40 | public async deleteLiveTourById(@Args({ name: 'id', type: () => ID }) id: string) { 41 | return this.liveToursService.deleteOneById(id) 42 | } 43 | 44 | @Mutation(() => BatchDeleteModel) 45 | @UseGuards(JwtAuthGuard) 46 | public async deleteLiveTours(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 47 | return this.liveToursService.batchDelete(ids) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/live-tours/live-tours.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { CreateLiveTourInput } from './dtos/create-live-tour.input' 5 | import { UpdateLiveTourInput } from './dtos/update-live-tour.input' 6 | import { LiveTourModel } from './models/live-tours.model' 7 | import { LiveTour } from './interfaces/live-tours.interface' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | 10 | @Injectable() 11 | export class LiveToursService { 12 | constructor( 13 | @InjectModel('LiveTour') 14 | private readonly liveTourModel: Model, 15 | ) { 16 | this.liveTourModel = liveTourModel 17 | } 18 | 19 | public async findAll(): Promise { 20 | return this.liveTourModel.find({}).sort({ updatedAt: -1 }) 21 | } 22 | 23 | public async findOneById(id: string): Promise { 24 | return this.liveTourModel.findById(id) 25 | } 26 | 27 | public async create(liveTourInput: CreateLiveTourInput): Promise { 28 | return this.liveTourModel.create(liveTourInput) 29 | } 30 | 31 | public async update(liveTourInput: UpdateLiveTourInput): Promise { 32 | const { id, ...rest } = liveTourInput 33 | return this.liveTourModel.findByIdAndUpdate(id, rest, { new: true }) 34 | } 35 | 36 | public async deleteOneById(id: string): Promise { 37 | return this.liveTourModel.findByIdAndDelete(id) 38 | } 39 | 40 | public async batchDelete(ids: string[]): Promise { 41 | const res = await this.liveTourModel.deleteMany({ 42 | _id: { $in: ids }, 43 | }) 44 | 45 | return { 46 | ...res, 47 | ids, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/live-tours/models/live-tours.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class LiveTourModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly title: string 10 | 11 | @Field() 12 | public readonly posterUrl: string 13 | 14 | @Field() 15 | public readonly showTime: Date 16 | 17 | @Field() 18 | public readonly createdAt: Date 19 | 20 | @Field() 21 | public readonly updatedAt: Date 22 | } 23 | -------------------------------------------------------------------------------- /src/live-tours/schemas/live-tours.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const LiveToursSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | posterUrl: { 15 | type: String, 16 | required: true, 17 | }, 18 | showTime: { 19 | type: Date, 20 | required: true, 21 | }, 22 | }, 23 | { 24 | collection: 'live_tours', 25 | timestamps: true, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core' 2 | import { NestExpressApplication } from '@nestjs/platform-express' 3 | import { configLogger } from './shared/logger/logger.config' 4 | import { configMiddlewares } from './shared/middlewares/middleware.config' 5 | import { AppModule } from './app.module' 6 | 7 | const bootstrap = async () => { 8 | const app = await NestFactory.create(AppModule, { logger: false }) 9 | app.setGlobalPrefix('beg') 10 | configMiddlewares(app) 11 | configLogger(app) 12 | await app.listen(process.env.port || 3002) 13 | } 14 | 15 | bootstrap() 16 | -------------------------------------------------------------------------------- /src/mottos/dtos/create-motto.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsNumber } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateMottoInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly content: string 10 | } 11 | -------------------------------------------------------------------------------- /src/mottos/dtos/update-motto.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdateMottoInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly content: string 15 | } 16 | -------------------------------------------------------------------------------- /src/mottos/interfaces/motto.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface Motto extends Document { 4 | readonly _id: string 5 | readonly content: string 6 | readonly weight: number 7 | readonly createdAt: Date 8 | readonly updatedAt: Date 9 | } 10 | -------------------------------------------------------------------------------- /src/mottos/models/mottos.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class MottoModel { 5 | @Field({ nullable: false }) 6 | public _id: string 7 | 8 | @Field({ nullable: false }) 9 | public content: string 10 | 11 | @Field({ nullable: false }) 12 | public weight: number 13 | 14 | @Field({ nullable: false }) 15 | public createdAt: Date 16 | 17 | @Field({ nullable: false }) 18 | public updatedAt: Date 19 | } 20 | -------------------------------------------------------------------------------- /src/mottos/mottos.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { MottosResolver } from './mottos.resolver' 4 | import { MottosService } from './mottos.service' 5 | import { MottoSchema } from './schemas/mottos.schema' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'Motto', schema: MottoSchema }])], 9 | providers: [MottosService, MottosResolver], 10 | }) 11 | export class MottosModule {} 12 | -------------------------------------------------------------------------------- /src/mottos/mottos.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { MottosService } from './mottos.service' 4 | import { MottoModel } from './models/mottos.model' 5 | import { CreateMottoInput } from './dtos/create-motto.input' 6 | import { UpdateMottoInput } from './dtos/update-motto.input' 7 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 10 | 11 | @Resolver(() => MottoModel) 12 | export class MottosResolver { 13 | constructor(private readonly mottosService: MottosService) { 14 | this.mottosService = mottosService 15 | } 16 | 17 | @Query(() => [MottoModel]) 18 | public async getMottos(): Promise { 19 | return this.mottosService.findAll() 20 | } 21 | 22 | @Query(() => MottoModel) 23 | public async getMottoById(@Args({ name: 'id', type: () => ID }) id: string): Promise { 24 | return this.mottosService.findOneById(id) 25 | } 26 | 27 | @Mutation(() => MottoModel) 28 | @UseGuards(JwtAuthGuard) 29 | public async createMotto(@Args('input') input: CreateMottoInput): Promise { 30 | return this.mottosService.create(input) 31 | } 32 | 33 | @Mutation(() => MottoModel) 34 | @UseGuards(JwtAuthGuard) 35 | public async updateMottoById(@Args('input') input: UpdateMottoInput): Promise { 36 | return this.mottosService.update(input) 37 | } 38 | 39 | @Mutation(() => [MottoModel]) 40 | @UseGuards(JwtAuthGuard) 41 | public async exchangePositionMotto( 42 | @Args('input') input: ExchangePositionInput, 43 | ): Promise { 44 | return this.mottosService.exchangePosition(input) 45 | } 46 | 47 | @Mutation(() => MottoModel) 48 | @UseGuards(JwtAuthGuard) 49 | public async deleteMottoById( 50 | @Args({ name: 'id', type: () => ID }) id: string, 51 | ): Promise { 52 | return this.mottosService.deleteOneById(id) 53 | } 54 | 55 | @Mutation(() => BatchDeleteModel) 56 | @UseGuards(JwtAuthGuard) 57 | public async deleteMottos(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 58 | return this.mottosService.batchDelete(ids) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/mottos/mottos.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { Model } from 'mongoose' 3 | import { InjectModel } from '@nestjs/mongoose' 4 | import { Motto } from './interfaces/motto.interface' 5 | import { CreateMottoInput } from './dtos/create-motto.input' 6 | import { UpdateMottoInput } from './dtos/update-motto.input' 7 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 8 | 9 | @Injectable() 10 | export class MottosService { 11 | constructor( 12 | @InjectModel('Motto') 13 | private readonly mottoModel: Model, 14 | ) { 15 | this.mottoModel = mottoModel 16 | } 17 | 18 | public async findAll() { 19 | return this.mottoModel.find().sort({ weight: -1 }) 20 | } 21 | 22 | public async findOneById(id: string) { 23 | return this.mottoModel.findById(id) 24 | } 25 | 26 | public async create(input: CreateMottoInput) { 27 | const all = await this.findAll() 28 | const weight = all[0] ? all[0].weight : 0 29 | return this.mottoModel.create({ ...input, weight: weight + 1 }) 30 | } 31 | 32 | public async update(input: UpdateMottoInput) { 33 | const { id, content } = input 34 | 35 | return this.mottoModel.findByIdAndUpdate( 36 | id, 37 | { 38 | content, 39 | }, 40 | { new: true }, 41 | ) 42 | } 43 | 44 | public async exchangePosition(input: ExchangePositionInput) { 45 | const { id, exchangedId, weight, exchangedWeight } = input 46 | 47 | const exchanged = await this.mottoModel.findByIdAndUpdate( 48 | exchangedId, 49 | { 50 | weight, 51 | }, 52 | { new: true }, 53 | ) 54 | 55 | const curr = await this.mottoModel.findByIdAndUpdate( 56 | id, 57 | { 58 | weight: exchangedWeight, 59 | }, 60 | { new: true }, 61 | ) 62 | 63 | return [exchanged, curr] 64 | } 65 | 66 | public async deleteOneById(id: string) { 67 | return this.mottoModel.findByIdAndDelete(id) 68 | } 69 | 70 | public async batchDelete(ids: string[]) { 71 | const res = await this.mottoModel.deleteMany({ 72 | _id: { $in: ids }, 73 | }) 74 | 75 | return { 76 | ...res, 77 | ids, 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/mottos/schemas/mottos.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const MottoSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | content: { 11 | type: String, 12 | required: true, 13 | }, 14 | weight: { 15 | type: Number, 16 | require: true, 17 | }, 18 | }, 19 | { 20 | collection: 'motto', 21 | timestamps: true, 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /src/open-sources/dtos/create-open-source.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsUrl } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateOpenSourceInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly description: string 15 | 16 | @Field() 17 | @IsUrl() 18 | @IsNotEmpty() 19 | public readonly url: string 20 | 21 | @Field() 22 | @IsUrl() 23 | @IsNotEmpty() 24 | public readonly posterUrl: string 25 | } 26 | -------------------------------------------------------------------------------- /src/open-sources/dtos/update-open-source.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | import { CreateOpenSourceInput } from './create-open-source.input' 4 | 5 | @InputType() 6 | export class UpdateOpenSourceInput extends CreateOpenSourceInput { 7 | @Field() 8 | @IsString() 9 | @IsNotEmpty() 10 | public readonly id: string 11 | } 12 | -------------------------------------------------------------------------------- /src/open-sources/interfaces/open-sources.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface OpenSource extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly description: string 7 | readonly url: string 8 | readonly posterUrl: string 9 | readonly createdAt: Date 10 | readonly updatedAt: Date 11 | } 12 | -------------------------------------------------------------------------------- /src/open-sources/models/open-sources.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class OpenSourceModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly title: string 10 | 11 | @Field() 12 | public readonly description: string 13 | 14 | @Field() 15 | public readonly url: string 16 | 17 | @Field() 18 | public readonly posterUrl: string 19 | 20 | @Field() 21 | public readonly createdAt: Date 22 | 23 | @Field() 24 | public readonly updatedAt: Date 25 | } 26 | -------------------------------------------------------------------------------- /src/open-sources/open-sources.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { OpenSourceSchema } from './schemas/open-sources.schema' 4 | import { OpenSourcesResolver } from './open-sources.resolver' 5 | import { OpenSourcesService } from './open-sources.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'OpenSource', schema: OpenSourceSchema }])], 9 | providers: [OpenSourcesResolver, OpenSourcesService], 10 | }) 11 | export class OpenSourcesModule {} 12 | -------------------------------------------------------------------------------- /src/open-sources/open-sources.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { OpenSourcesService } from './open-sources.service' 4 | import { OpenSourceModel } from './models/open-sources.model' 5 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 6 | import { CreateOpenSourceInput } from './dtos/create-open-source.input' 7 | import { UpdateOpenSourceInput } from './dtos/update-open-source.input' 8 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 9 | 10 | @Resolver(() => OpenSourceModel) 11 | export class OpenSourcesResolver { 12 | constructor(private readonly openSourcesService: OpenSourcesService) { 13 | this.openSourcesService = openSourcesService 14 | } 15 | 16 | @Query(() => [OpenSourceModel]) 17 | public async getOpenSources() { 18 | return this.openSourcesService.findAll() 19 | } 20 | 21 | @Query(() => OpenSourceModel) 22 | public async getOpenSourceById(@Args({ name: 'id', type: () => ID }) id: string) { 23 | return this.openSourcesService.findOneById(id) 24 | } 25 | 26 | @Mutation(() => OpenSourceModel) 27 | @UseGuards(JwtAuthGuard) 28 | public async createOpenSource(@Args('input') input: CreateOpenSourceInput) { 29 | return this.openSourcesService.create(input) 30 | } 31 | 32 | @Mutation(() => OpenSourceModel) 33 | @UseGuards(JwtAuthGuard) 34 | public async updateOpenSourceById(@Args('input') input: UpdateOpenSourceInput) { 35 | return this.openSourcesService.update(input) 36 | } 37 | 38 | @Mutation(() => OpenSourceModel) 39 | @UseGuards(JwtAuthGuard) 40 | public async deleteOpenSourceById(@Args({ name: 'id', type: () => ID }) id: string) { 41 | return this.openSourcesService.deleteOneById(id) 42 | } 43 | 44 | @Mutation(() => BatchDeleteModel) 45 | @UseGuards(JwtAuthGuard) 46 | public async deleteOpenSources(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 47 | return this.openSourcesService.batchDelete(ids) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/open-sources/open-sources.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { CreateOpenSourceInput } from './dtos/create-open-source.input' 5 | import { UpdateOpenSourceInput } from './dtos/update-open-source.input' 6 | import { OpenSourceModel } from './models/open-sources.model' 7 | import { OpenSource } from './interfaces/open-sources.interface' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | 10 | @Injectable() 11 | export class OpenSourcesService { 12 | constructor( 13 | @InjectModel('OpenSource') 14 | private readonly openSourceModel: Model, 15 | ) { 16 | this.openSourceModel = openSourceModel 17 | } 18 | 19 | public async findAll(): Promise { 20 | return this.openSourceModel.find({}).sort({ updatedAt: -1 }) 21 | } 22 | 23 | public async findOneById(id: string): Promise { 24 | return this.openSourceModel.findById(id) 25 | } 26 | 27 | public async create(openSourceInput: CreateOpenSourceInput): Promise { 28 | return this.openSourceModel.create(openSourceInput) 29 | } 30 | 31 | public async update(openSourceInput: UpdateOpenSourceInput): Promise { 32 | const { id, ...rest } = openSourceInput 33 | return this.openSourceModel.findByIdAndUpdate(id, rest, { new: true }) 34 | } 35 | 36 | public async deleteOneById(id: string): Promise { 37 | return this.openSourceModel.findByIdAndDelete(id) 38 | } 39 | 40 | public async batchDelete(ids: string[]): Promise { 41 | const res = await this.openSourceModel.deleteMany({ 42 | _id: { $in: ids }, 43 | }) 44 | 45 | return { 46 | ...res, 47 | ids, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/open-sources/schemas/open-sources.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const OpenSourceSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | description: { 15 | type: String, 16 | required: true, 17 | }, 18 | url: { 19 | type: String, 20 | required: true, 21 | }, 22 | posterUrl: { 23 | type: String, 24 | required: true, 25 | }, 26 | }, 27 | { 28 | collection: 'open_source', 29 | timestamps: true, 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /src/player/dtos/create-player.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsUrl, IsBoolean } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreatePlayerInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly artist: string 15 | 16 | @Field() 17 | @IsString() 18 | @IsNotEmpty() 19 | public readonly lrc: string 20 | 21 | @Field() 22 | @IsUrl() 23 | @IsNotEmpty() 24 | public readonly coverUrl: string 25 | 26 | @Field() 27 | @IsUrl() 28 | @IsNotEmpty() 29 | public readonly musicFileUrl: string 30 | 31 | @Field() 32 | @IsBoolean() 33 | @IsNotEmpty() 34 | public readonly isPublic: boolean 35 | } 36 | -------------------------------------------------------------------------------- /src/player/dtos/update-player.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsUUID, IsNotEmpty, IsBoolean } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdatePlayerInput { 6 | @Field() 7 | @IsUUID() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field({ nullable: true }) 12 | public readonly title?: string 13 | 14 | @Field({ nullable: true }) 15 | public readonly artist?: string 16 | 17 | @Field({ nullable: true }) 18 | public readonly lrc?: string 19 | 20 | @Field({ nullable: true }) 21 | public readonly coverUrl?: string 22 | 23 | @Field({ nullable: true }) 24 | public readonly musicFileUrl?: string 25 | 26 | @Field({ nullable: true }) 27 | @IsBoolean() 28 | public readonly isPublic?: boolean 29 | } 30 | -------------------------------------------------------------------------------- /src/player/interfaces/player.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface Player extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly artist: string 7 | readonly lrc: string 8 | readonly coverUrl: string 9 | readonly musicFileUrl: string 10 | readonly isPublic: boolean 11 | readonly weight: number 12 | readonly createdAt: Date 13 | readonly updatedAt: Date 14 | } 15 | -------------------------------------------------------------------------------- /src/player/models/player.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class PlayerModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly title: string 10 | 11 | @Field() 12 | public readonly artist: string 13 | 14 | @Field() 15 | public readonly lrc: string 16 | 17 | @Field() 18 | public readonly coverUrl: string 19 | 20 | @Field() 21 | public readonly musicFileUrl: string 22 | 23 | @Field() 24 | public readonly isPublic: boolean 25 | 26 | @Field() 27 | public readonly weight: number 28 | 29 | @Field() 30 | public readonly createdAt: Date 31 | 32 | @Field() 33 | public readonly updatedAt: Date 34 | } 35 | -------------------------------------------------------------------------------- /src/player/player.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { PlayerSchema } from './schemas/player.schema' 4 | import { PlayerResolver } from './player.resolver' 5 | import { PlayerService } from './player.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'Player', schema: PlayerSchema }])], 9 | providers: [PlayerResolver, PlayerService], 10 | }) 11 | export class PlayerModule {} 12 | -------------------------------------------------------------------------------- /src/player/player.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { PlayerService } from './player.service' 4 | import { PlayerModel } from './models/player.model' 5 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 6 | import { BatchUpdateModel } from '../database/models/batch-update.model' 7 | import { CreatePlayerInput } from './dtos/create-player.input' 8 | import { UpdatePlayerInput } from './dtos/update-player.input' 9 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 10 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 11 | 12 | @Resolver(() => PlayerModel) 13 | export class PlayerResolver { 14 | constructor(private readonly playerService: PlayerService) { 15 | this.playerService = playerService 16 | } 17 | 18 | @Query(() => [PlayerModel]) 19 | public async players(): Promise { 20 | return this.playerService.findAllPubilc() 21 | } 22 | 23 | @Query(() => [PlayerModel]) 24 | @UseGuards(JwtAuthGuard) 25 | public async getPlayers() { 26 | return this.playerService.findAll() 27 | } 28 | 29 | @Query(() => PlayerModel) 30 | @UseGuards(JwtAuthGuard) 31 | public async getPlayerById(@Args({ name: 'id', type: () => ID }) id: string) { 32 | return this.playerService.findOneById(id) 33 | } 34 | 35 | @Mutation(() => PlayerModel) 36 | @UseGuards(JwtAuthGuard) 37 | public async createPlayer(@Args('input') input: CreatePlayerInput) { 38 | return this.playerService.create(input) 39 | } 40 | 41 | @Mutation(() => PlayerModel) 42 | @UseGuards(JwtAuthGuard) 43 | public async updatePlayerById(@Args('input') input: UpdatePlayerInput) { 44 | return this.playerService.update(input) 45 | } 46 | 47 | @Mutation(() => [PlayerModel]) 48 | @UseGuards(JwtAuthGuard) 49 | public async exchangePositionPlayer( 50 | @Args('input') input: ExchangePositionInput, 51 | ): Promise { 52 | return this.playerService.exchangePosition(input) 53 | } 54 | 55 | @Mutation(() => PlayerModel) 56 | @UseGuards(JwtAuthGuard) 57 | public async deletePlayerById(@Args({ name: 'id', type: () => ID }) id: string) { 58 | return this.playerService.deleteOneById(id) 59 | } 60 | 61 | @Mutation(() => BatchDeleteModel) 62 | @UseGuards(JwtAuthGuard) 63 | public async deletePlayers(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 64 | return this.playerService.batchDelete(ids) 65 | } 66 | 67 | @Mutation(() => BatchUpdateModel) 68 | @UseGuards(JwtAuthGuard) 69 | public async offlinePlayers(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 70 | return this.playerService.batchUpdate(ids) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/player/player.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { CreatePlayerInput } from './dtos/create-player.input' 5 | import { UpdatePlayerInput } from './dtos/update-player.input' 6 | import { PlayerModel } from './models/player.model' 7 | import { Player } from './interfaces/player.interface' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | import { BatchUpdateModel } from '../database/models/batch-update.model' 10 | import { ExchangePositionInput } from '../shared/interfaces/exchange-position.input' 11 | 12 | @Injectable() 13 | export class PlayerService { 14 | constructor( 15 | @InjectModel('Player') 16 | private readonly playerModel: Model, 17 | ) { 18 | this.playerModel = playerModel 19 | } 20 | 21 | public async findAllPubilc() { 22 | return this.playerModel.find({ isPublic: { $ne: false } }).sort({ updatedAt: -1 }) 23 | } 24 | 25 | public async findAll(): Promise { 26 | return this.playerModel.find({}).sort({ updatedAt: -1 }) 27 | } 28 | 29 | public async findOneById(id: string): Promise { 30 | return this.playerModel.findById(id) 31 | } 32 | 33 | public async create(playerInput: CreatePlayerInput): Promise { 34 | const all = await this.findAll() 35 | const weight = all[0] ? all[0].weight : 0 36 | return this.playerModel.create({ ...playerInput, weight: weight + 1 }) 37 | } 38 | 39 | public async update(playerInput: UpdatePlayerInput): Promise { 40 | const { id, ...rest } = playerInput 41 | return this.playerModel.findByIdAndUpdate(id, rest, { new: true }) 42 | } 43 | 44 | public async exchangePosition(input: ExchangePositionInput) { 45 | const { id, exchangedId, weight, exchangedWeight } = input 46 | 47 | const exchanged = await this.playerModel.findByIdAndUpdate( 48 | exchangedId, 49 | { 50 | weight, 51 | }, 52 | { new: true }, 53 | ) 54 | 55 | const curr = await this.playerModel.findByIdAndUpdate( 56 | id, 57 | { 58 | weight: exchangedWeight, 59 | }, 60 | { new: true }, 61 | ) 62 | 63 | return [exchanged, curr] 64 | } 65 | 66 | public async deleteOneById(id: string): Promise { 67 | return this.playerModel.findByIdAndDelete(id) 68 | } 69 | 70 | public async batchDelete(ids: string[]): Promise { 71 | const res = await this.playerModel.deleteMany({ 72 | _id: { $in: ids }, 73 | }) 74 | 75 | return { 76 | ...res, 77 | ids, 78 | } 79 | } 80 | 81 | public async batchUpdate(ids: string[]): Promise { 82 | const res = await this.playerModel.updateMany( 83 | { 84 | _id: { $in: ids }, 85 | }, 86 | { 87 | $set: { isPublic: false }, 88 | }, 89 | ) 90 | 91 | return { 92 | ...res, 93 | ids, 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/player/schemas/player.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const PlayerSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | artist: { 15 | type: String, 16 | required: true, 17 | }, 18 | lrc: { 19 | type: String, 20 | required: true, 21 | }, 22 | coverUrl: { 23 | type: String, 24 | required: true, 25 | }, 26 | musicFileUrl: { 27 | type: String, 28 | required: true, 29 | }, 30 | isPublic: { 31 | type: Boolean, 32 | required: true, 33 | default: true, 34 | }, 35 | weight: { 36 | type: Number, 37 | require: true, 38 | }, 39 | }, 40 | { 41 | collection: 'player', 42 | timestamps: true, 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /src/post-statistics/dtos/create-post-statistics.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreatePostStatisticsInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly postId: string 10 | 11 | @Field() 12 | @IsString() 13 | @IsNotEmpty() 14 | public readonly postName: string 15 | 16 | @Field() 17 | @IsString() 18 | @IsNotEmpty() 19 | public readonly scenes: string 20 | } 21 | -------------------------------------------------------------------------------- /src/post-statistics/interfaces/post-statistics.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface PostStatistics extends Document { 4 | readonly _id: string 5 | readonly postId: string 6 | readonly postName: string 7 | readonly scenes: string 8 | readonly createdAt: Date 9 | readonly updatedAt: Date 10 | } 11 | -------------------------------------------------------------------------------- /src/post-statistics/models/post-statistics-group.model.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import { Field, ObjectType } from '@nestjs/graphql' 3 | 4 | @ObjectType() 5 | export class PostStatisticsGroupItemModel { 6 | @Field({ nullable: false }) 7 | public postId: string 8 | 9 | @Field({ nullable: false }) 10 | public postName: string 11 | 12 | @Field({ nullable: false }) 13 | public scenes: string 14 | 15 | @Field({ nullable: false }) 16 | public operatedAt: Date 17 | } 18 | 19 | @ObjectType() 20 | export class PostStatisticsGroupModel { 21 | @Field({ nullable: false }) 22 | public _id: string 23 | 24 | @Field({ nullable: false }) 25 | public count: number 26 | 27 | @Field(() => [PostStatisticsGroupItemModel], { nullable: false }) 28 | public items: PostStatisticsGroupItemModel[] 29 | } 30 | -------------------------------------------------------------------------------- /src/post-statistics/models/post-statistics.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class PostStatisticsModel { 5 | @Field({ nullable: false }) 6 | public _id: string 7 | 8 | @Field({ nullable: false }) 9 | public postId: string 10 | 11 | @Field({ nullable: false }) 12 | public postName: string 13 | 14 | @Field({ nullable: false }) 15 | public scenes: string 16 | 17 | @Field({ nullable: false }) 18 | public createdAt: Date 19 | 20 | @Field({ nullable: false }) 21 | public updatedAt: Date 22 | } 23 | -------------------------------------------------------------------------------- /src/post-statistics/post-statistics.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { PostStatisticsResolver } from './post-statistics.resolver' 4 | import { PostStatisticsService } from './post-statistics.service' 5 | import { PostStatisticsSchema } from './schemas/post-statistics.schema' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'PostStatistics', schema: PostStatisticsSchema }])], 9 | providers: [PostStatisticsService, PostStatisticsResolver], 10 | }) 11 | export class PostStatisticsModule {} 12 | -------------------------------------------------------------------------------- /src/post-statistics/post-statistics.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation } from '@nestjs/graphql' 3 | import { PostStatisticsService } from './post-statistics.service' 4 | import { PostStatisticsModel } from './models/post-statistics.model' 5 | import { PostStatisticsGroupModel } from './models/post-statistics-group.model' 6 | import { CreatePostStatisticsInput } from './dtos/create-post-statistics.input' 7 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 8 | 9 | @Resolver() 10 | export class PostStatisticsResolver { 11 | constructor(private readonly postStatisticsService: PostStatisticsService) { 12 | this.postStatisticsService = postStatisticsService 13 | } 14 | 15 | @Query(() => [PostStatisticsGroupModel]) 16 | @UseGuards(JwtAuthGuard) 17 | public async getPostStatistics(): Promise { 18 | return this.postStatisticsService.findAll() 19 | } 20 | 21 | @Mutation(() => PostStatisticsModel) 22 | @UseGuards(JwtAuthGuard) 23 | public async createPostStatistics( 24 | @Args('input') input: CreatePostStatisticsInput, 25 | ): Promise { 26 | return this.postStatisticsService.create(input) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/post-statistics/post-statistics.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { Model } from 'mongoose' 3 | import { InjectModel } from '@nestjs/mongoose' 4 | import { PostStatistics } from './interfaces/post-statistics.interface' 5 | import { CreatePostStatisticsInput } from './dtos/create-post-statistics.input' 6 | 7 | @Injectable() 8 | export class PostStatisticsService { 9 | constructor( 10 | @InjectModel('PostStatistics') 11 | private readonly postStatisticsModel: Model, 12 | ) { 13 | this.postStatisticsModel = postStatisticsModel 14 | } 15 | 16 | public async findAll() { 17 | const res = await this.postStatisticsModel 18 | .aggregate([ 19 | { 20 | $group: { 21 | _id: { $dateToString: { format: '%Y-%m-%d', date: '$createdAt' } }, 22 | count: { $sum: 1 }, 23 | items: { 24 | $push: { 25 | postId: '$postId', 26 | postName: '$postName', 27 | scenes: '$scenes', 28 | operatedAt: '$createdAt', 29 | }, 30 | }, 31 | }, 32 | }, 33 | ]) 34 | .sort({ _id: -1 }) 35 | 36 | return res 37 | } 38 | 39 | public async create(input: CreatePostStatisticsInput) { 40 | return this.postStatisticsModel.create(input) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/post-statistics/schemas/post-statistics.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const PostStatisticsSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | postId: { 11 | type: String, 12 | required: true, 13 | }, 14 | postName: { 15 | type: String, 16 | required: true, 17 | }, 18 | scenes: { 19 | type: String, 20 | require: true, 21 | }, 22 | }, 23 | { 24 | collection: 'post_statistics', 25 | timestamps: true, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /src/posts/dtos/create-post.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field, } from '@nestjs/graphql' 2 | import { 3 | IsString, 4 | IsNotEmpty, 5 | IsUrl, 6 | IsDate, 7 | IsArray, 8 | ArrayNotEmpty, 9 | ArrayUnique, 10 | MaxLength, 11 | MinLength, 12 | } from 'class-validator' 13 | 14 | @InputType() 15 | export class CreatePostInput { 16 | @Field({ nullable: false }) 17 | @IsString() 18 | @IsUrl({ protocols: ['https'], require_protocol: true }) 19 | @IsNotEmpty() 20 | public readonly posterUrl: string 21 | 22 | @Field({ nullable: false }) 23 | @IsString() 24 | @MinLength(1) 25 | @MaxLength(20) 26 | @IsNotEmpty() 27 | public readonly title: string 28 | 29 | @Field({ nullable: false }) 30 | @IsString() 31 | @IsNotEmpty() 32 | public readonly summary: string 33 | 34 | @Field({ nullable: false }) 35 | @IsString() 36 | @IsNotEmpty() 37 | public readonly content: string 38 | 39 | @Field(() => [String], { nullable: false }) 40 | @IsArray() 41 | @IsString({ each: true }) 42 | @ArrayNotEmpty() 43 | @ArrayUnique() 44 | @IsNotEmpty() 45 | public readonly tags: string[] 46 | 47 | @Field({ nullable: false }) 48 | @IsDate() 49 | @IsNotEmpty() 50 | public readonly lastModifiedDate: Date 51 | 52 | @Field({ nullable: true }) 53 | public readonly isPublic?: boolean 54 | } 55 | -------------------------------------------------------------------------------- /src/posts/dtos/pagination.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsNumber } from 'class-validator' 3 | 4 | @InputType() 5 | export class PaginationInput { 6 | @Field() 7 | @IsNumber() 8 | @IsNotEmpty() 9 | public readonly page: number 10 | 11 | @Field() 12 | @IsNumber() 13 | @IsNotEmpty() 14 | public readonly pageSize: number 15 | 16 | @Field({ nullable: true }) 17 | public readonly title?: string 18 | 19 | @Field({ nullable: true }) 20 | public readonly tag?: string 21 | } 22 | -------------------------------------------------------------------------------- /src/posts/dtos/update-post.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsUUID } from 'class-validator' 3 | 4 | @InputType() 5 | export class UpdatePostInput { 6 | @Field({ nullable: true }) 7 | @IsUUID() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field({ nullable: true }) 12 | public readonly posterUrl?: string 13 | 14 | @Field({ nullable: true }) 15 | public readonly title?: string 16 | 17 | @Field({ nullable: true }) 18 | public readonly summary?: string 19 | 20 | @Field({ nullable: true }) 21 | public readonly content?: string 22 | 23 | @Field(() => [String], { nullable: true }) 24 | public readonly tags?: string[] 25 | 26 | @Field({ nullable: true }) 27 | public readonly like?: number 28 | 29 | @Field({ nullable: true }) 30 | public readonly pv?: number 31 | 32 | @Field({ nullable: true }) 33 | public readonly lastModifiedDate?: Date 34 | 35 | @Field({ nullable: true }) 36 | public readonly isPublic?: boolean 37 | } 38 | -------------------------------------------------------------------------------- /src/posts/interfaces/posts.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface Post { 4 | readonly _id: string 5 | readonly posterUrl: string 6 | readonly title: string 7 | readonly summary: string 8 | readonly content: string 9 | readonly tags: string[] 10 | readonly lastModifiedDate: Date 11 | readonly like: number 12 | readonly pv: number 13 | readonly isPublic: boolean 14 | readonly createdAt: Date 15 | readonly updatedAt: Date 16 | readonly prev: Post | null 17 | readonly next: Post | null 18 | } 19 | 20 | export type PostDocument = Post & Document 21 | -------------------------------------------------------------------------------- /src/posts/models/archive.model.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import { Field, ObjectType, ID } from '@nestjs/graphql' 3 | 4 | @ObjectType() 5 | export class DayModel { 6 | @Field(() => ID) 7 | public readonly id: string 8 | 9 | @Field() 10 | public readonly title: string 11 | 12 | @Field() 13 | public readonly pv: number 14 | 15 | @Field() 16 | public readonly createdAt: Date 17 | } 18 | 19 | @ObjectType() 20 | export class MonthModel { 21 | @Field() 22 | public readonly month: number 23 | 24 | @Field(() => [DayModel]) 25 | public readonly days: DayModel 26 | } 27 | 28 | @ObjectType() 29 | export class ArchiveModel { 30 | @Field() 31 | public readonly _id: number 32 | 33 | @Field(() => [MonthModel]) 34 | public readonly months: MonthModel 35 | } 36 | -------------------------------------------------------------------------------- /src/posts/models/post.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType, Int } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class PostItemModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly posterUrl: string 10 | 11 | @Field() 12 | public readonly title: string 13 | 14 | @Field() 15 | public readonly summary: string 16 | 17 | @Field() 18 | public readonly content: string 19 | 20 | @Field(() => [String]) 21 | public readonly tags: string[] 22 | 23 | @Field() 24 | public readonly lastModifiedDate: Date 25 | 26 | @Field(() => Int) 27 | public readonly like: number 28 | 29 | @Field(() => Int) 30 | public readonly pv: number 31 | 32 | @Field() 33 | public readonly isPublic: boolean 34 | 35 | @Field() 36 | public readonly createdAt: Date 37 | 38 | @Field() 39 | public readonly updatedAt: Date 40 | 41 | @Field(() => PostItemModel, { nullable: true }) 42 | public readonly prev: PostItemModel | null 43 | 44 | @Field(() => PostItemModel, { nullable: true }) 45 | public readonly next: PostItemModel | null 46 | } 47 | -------------------------------------------------------------------------------- /src/posts/models/posts.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | import { PostItemModel } from './post.model' 3 | 4 | @ObjectType() 5 | export class PostModel { 6 | @Field() 7 | public readonly total: number 8 | 9 | @Field() 10 | public readonly page: number 11 | 12 | @Field() 13 | public readonly pageSize: number 14 | 15 | @Field(() => [PostItemModel]) 16 | public readonly items: PostItemModel[] 17 | } 18 | -------------------------------------------------------------------------------- /src/posts/models/tags.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class TagsModel { 5 | @Field(() => [String]) 6 | public readonly tags: string[] 7 | } 8 | -------------------------------------------------------------------------------- /src/posts/posts.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { PostSchema } from './schemas/posts.schema' 4 | import { PostsResolver } from './posts.resolver' 5 | import { PostsService } from './posts.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'Post', schema: PostSchema }])], 9 | providers: [PostsResolver, PostsService], 10 | }) 11 | export class PostsModule {} 12 | -------------------------------------------------------------------------------- /src/posts/posts.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID, Int } from '@nestjs/graphql' 3 | import { PostsService } from './posts.service' 4 | import { PostModel } from './models/posts.model' 5 | import { PostItemModel } from './models/post.model' 6 | import { ArchiveModel } from './models/archive.model' 7 | import { TagsModel } from './models/tags.model' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | import { CreatePostInput } from './dtos/create-post.input' 10 | import { UpdatePostInput } from './dtos/update-post.input' 11 | import { PaginationInput } from './dtos/pagination.input' 12 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 13 | 14 | @Resolver() 15 | export class PostsResolver { 16 | constructor(private readonly postsService: PostsService) { 17 | this.postsService = postsService 18 | } 19 | 20 | @Query(() => PostModel) 21 | public async posts(@Args('input') input: PaginationInput) { 22 | return this.postsService.findPublicByPagination(input) 23 | } 24 | 25 | @Query(() => PostModel) 26 | @UseGuards(JwtAuthGuard) 27 | public async getPosts(@Args('input') input: PaginationInput) { 28 | return this.postsService.findByPagination(input) 29 | } 30 | 31 | @Query(() => PostItemModel) 32 | public async getPostById(@Args({ name: 'id', type: () => ID }) id: string) { 33 | return this.postsService.findOneById(id) 34 | } 35 | 36 | @Mutation(() => PostItemModel) 37 | @UseGuards(JwtAuthGuard) 38 | public async createPost(@Args('input') input: CreatePostInput) { 39 | return this.postsService.create(input) 40 | } 41 | 42 | @Mutation(() => PostItemModel) 43 | @UseGuards(JwtAuthGuard) 44 | public async updatePostById(@Args('input') input: UpdatePostInput) { 45 | return this.postsService.update(input) 46 | } 47 | 48 | @Mutation(() => PostItemModel) 49 | @UseGuards(JwtAuthGuard) 50 | public async deletePostById(@Args({ name: 'id', type: () => ID }) id: string) { 51 | return this.postsService.deleteOneById(id) 52 | } 53 | 54 | @Mutation(() => BatchDeleteModel) 55 | @UseGuards(JwtAuthGuard) 56 | public async deletePosts(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 57 | return this.postsService.batchDelete(ids) 58 | } 59 | 60 | @Mutation(() => PostItemModel) 61 | public async updatePV(@Args({ name: 'id', type: () => ID }) id: string) { 62 | return this.postsService.updatePV(id) 63 | } 64 | 65 | @Mutation(() => PostItemModel) 66 | public async updateLike(@Args({ name: 'id', type: () => ID }) id: string) { 67 | return this.postsService.updateLike(id) 68 | } 69 | 70 | @Query(() => [PostItemModel]) 71 | public async getTopPVPosts(@Args({ name: 'limit', type: () => Int }) limit: number) { 72 | return this.postsService.getTopPVPosts(limit) 73 | } 74 | 75 | @Query(() => [PostItemModel]) 76 | public async getTopLikePosts(@Args({ name: 'limit', type: () => Int }) limit: number) { 77 | return this.postsService.getTopLikePosts(limit) 78 | } 79 | 80 | @Query(() => TagsModel) 81 | public async getAllTags() { 82 | return this.postsService.getAllTags() 83 | } 84 | 85 | @Query(() => [ArchiveModel]) 86 | public async archive() { 87 | return this.postsService.archive() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/posts/posts.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { ForbiddenError } from 'apollo-server-express' 4 | import { Model } from 'mongoose' 5 | import { CreatePostInput } from './dtos/create-post.input' 6 | import { UpdatePostInput } from './dtos/update-post.input' 7 | import { PaginationInput } from './dtos/pagination.input' 8 | import { PostModel } from './models/posts.model' 9 | import { PostItemModel } from './models/post.model' 10 | import { ArchiveModel } from './models/archive.model' 11 | import { TagsModel } from './models/tags.model' 12 | import { PostDocument } from './interfaces/posts.interface' 13 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 14 | 15 | @Injectable() 16 | export class PostsService { 17 | constructor( 18 | @InjectModel('Post') 19 | private readonly postModel: Model, 20 | ) { 21 | this.postModel = postModel 22 | } 23 | 24 | private async getTotalCount(): Promise { 25 | return this.postModel.countDocuments() 26 | } 27 | 28 | public async findPublicByPagination(input: PaginationInput): Promise { 29 | const { page, pageSize, title, tag } = input 30 | 31 | const params = { 32 | title: { $regex: !title ? '' : title, $options: 'i' }, 33 | isPublic: { $ne: false }, 34 | } 35 | 36 | const _params = tag ? { ...params, tags: tag } : params 37 | 38 | const count = await this.postModel.find(_params).count() 39 | const res = await this.postModel 40 | .find(_params) 41 | .sort({ createdAt: -1 }) 42 | .skip((page - 1) * pageSize) 43 | .limit(pageSize) 44 | 45 | return { 46 | total: count, 47 | page, 48 | pageSize, 49 | items: res, 50 | } 51 | } 52 | 53 | public async findByPagination(input: PaginationInput): Promise { 54 | const { page, pageSize, title } = input 55 | 56 | const total = await this.getTotalCount() 57 | const items = await this.postModel 58 | .find({ title: { $regex: !title ? '' : title, $options: 'i' } }) 59 | .sort({ createdAt: -1 }) 60 | .skip((page - 1) * pageSize) 61 | .limit(pageSize) 62 | 63 | return { 64 | total, 65 | page, 66 | pageSize, 67 | items, 68 | } 69 | } 70 | 71 | public async findOneById(id: string): Promise { 72 | const curr = await this.postModel.findById(id) 73 | if (!curr || curr.isPublic === false) { 74 | throw new ForbiddenError('Sorry, we couldn’t find this post.') 75 | } 76 | 77 | const prev = await this.postModel 78 | .find({ createdAt: { $lt: curr.createdAt }, isPublic: { $ne: false } }) 79 | .sort({ createdAt: -1 }) 80 | .limit(1) 81 | 82 | const next = await this.postModel 83 | .find({ createdAt: { $gt: curr.createdAt }, isPublic: { $ne: false } }) 84 | .limit(1) 85 | 86 | const res = { 87 | ...curr.toObject(), 88 | prev: prev[0] ? prev[0] : null, 89 | next: next[0] ? next[0] : null, 90 | } 91 | 92 | return res 93 | } 94 | 95 | public async create(postInput: CreatePostInput): Promise { 96 | return this.postModel.create(postInput) 97 | } 98 | 99 | public async update(postInput: UpdatePostInput): Promise { 100 | const { id, ...rest } = postInput 101 | return this.postModel.findByIdAndUpdate(id, rest, { new: true }) 102 | } 103 | 104 | public async deleteOneById(id: string): Promise { 105 | return this.postModel.findByIdAndDelete(id) 106 | } 107 | 108 | public async batchDelete(ids: string[]): Promise { 109 | const res = await this.postModel.deleteMany({ 110 | _id: { $in: ids }, 111 | }) 112 | 113 | return { 114 | ...res, 115 | ids, 116 | } 117 | } 118 | 119 | public async updatePV(id: string): Promise { 120 | const { pv } = await this.findOneById(id) 121 | return this.postModel.findByIdAndUpdate(id, { pv: pv + 1 }, { new: true }) 122 | } 123 | 124 | public async updateLike(id: string): Promise { 125 | const { like } = await this.findOneById(id) 126 | return this.postModel.findByIdAndUpdate(id, { like: like + 1 }, { new: true }) 127 | } 128 | 129 | public async getTopPVPosts(limit: number): Promise { 130 | return this.postModel 131 | .find({ isPublic: { $ne: false } }) 132 | .sort({ pv: -1, _id: -1 }) 133 | .limit(limit) 134 | } 135 | 136 | public async getTopLikePosts(limit: number): Promise { 137 | return this.postModel 138 | .find({ isPublic: { $ne: false } }) 139 | .sort({ like: -1, _id: -1 }) 140 | .limit(limit) 141 | } 142 | 143 | public async getAllTags(): Promise { 144 | const posts = await this.postModel.find({ isPublic: { $ne: false } }, { tags: 1 }) 145 | const arr = [] 146 | posts.forEach((post) => arr.push(...post.tags)) 147 | 148 | return { 149 | tags: [...new Set(arr)], 150 | } 151 | } 152 | 153 | public async archive(): Promise { 154 | const res = await this.postModel.aggregate([ 155 | { $match: { isPublic: { $ne: false } } }, 156 | { 157 | $group: { 158 | _id: { 159 | year: { $year: '$createdAt' }, 160 | month: { $month: '$createdAt' }, 161 | id: '$_id', 162 | title: '$title', 163 | pv: '$pv', 164 | createdAt: '$createdAt', 165 | }, 166 | }, 167 | }, 168 | { 169 | $group: { 170 | _id: { year: '$_id.year', month: '$_id.month' }, 171 | days: { 172 | $push: { 173 | id: '$_id.id', 174 | title: '$_id.title', 175 | pv: '$_id.pv', 176 | createdAt: '$_id.createdAt', 177 | }, 178 | }, 179 | }, 180 | }, 181 | { 182 | $sort: { _id: -1 }, 183 | }, 184 | { 185 | $group: { 186 | _id: '$_id.year', 187 | months: { $push: { month: '$_id.month', days: '$days' } }, 188 | }, 189 | }, 190 | { 191 | $sort: { _id: -1 }, 192 | }, 193 | ]) 194 | 195 | return res 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/posts/schemas/posts.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const PostSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | posterUrl: { 11 | type: String, 12 | required: true, 13 | }, 14 | title: { 15 | type: String, 16 | required: true, 17 | }, 18 | summary: { 19 | type: String, 20 | required: true, 21 | }, 22 | content: { 23 | type: String, 24 | required: true, 25 | }, 26 | tags: { 27 | type: Array, 28 | required: true, 29 | }, 30 | lastModifiedDate: { 31 | type: Date, 32 | required: true, 33 | }, 34 | like: { 35 | type: Number, 36 | default: 0, 37 | required: true, 38 | }, 39 | pv: { 40 | type: Number, 41 | default: 0, 42 | required: true, 43 | }, 44 | isPublic: { 45 | type: Boolean, 46 | default: true, 47 | required: true, 48 | }, 49 | }, 50 | { 51 | collection: 'post', 52 | timestamps: true, 53 | }, 54 | ) 55 | -------------------------------------------------------------------------------- /src/shared/constants.ts: -------------------------------------------------------------------------------- 1 | export const BANDWAGON_URL = 'https://api.64clouds.com/v1' 2 | 3 | export const BANDWAGON_SERVICE_INFO_URL = `${BANDWAGON_URL}/getLiveServiceInfo` 4 | 5 | export const BANDWAGON_USAGE_STATS_URL = `${BANDWAGON_URL}/getRawUsageStats` 6 | 7 | export const GOOGLE_RECAPTCHA_URL = 'https://www.google.com/recaptcha/api/siteverify' 8 | 9 | export const IP_STACK_URL = 'http://api.ipstack.com/' 10 | 11 | export const SCHEMA_GQL_FILE_NAME = 'schema.gql' 12 | 13 | export const TOTP_ENCODE = 'base32' 14 | 15 | export const CORS_ORIGINS_PRODUCTION = [/\.?yanceyleo\.com$/, /\.?yancey\.app$/] 16 | 17 | export const CORS_ORIGINS_UN_PRODUCTION = ['http://localhost:3000', 'http://localhost:3001'] 18 | 19 | export const BASE_IMAGE_EXTENSIONS = ['jpeg', 'jpg', 'png', 'gif'] 20 | -------------------------------------------------------------------------------- /src/shared/decorators/req.decorator.ts: -------------------------------------------------------------------------------- 1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common' 2 | import { GqlExecutionContext } from '@nestjs/graphql' 3 | 4 | export const ReqDecorator = createParamDecorator( 5 | (data: unknown, ctx: ExecutionContext) => GqlExecutionContext.create(ctx).getContext().req, 6 | ) 7 | -------------------------------------------------------------------------------- /src/shared/decorators/roles.decorator.ts: -------------------------------------------------------------------------------- 1 | import { SetMetadata } from '@nestjs/common' 2 | 3 | export const Roles = (...roles: string[]) => SetMetadata('roles', roles) 4 | -------------------------------------------------------------------------------- /src/shared/filters/graqhql-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Catch, 3 | ArgumentsHost, // 获取客户端参数 4 | HttpException, 5 | } from '@nestjs/common' 6 | import { GqlExceptionFilter, GqlArgumentsHost } from '@nestjs/graphql' 7 | import { Logger } from 'winston' 8 | 9 | @Catch(HttpException) 10 | export class GraphQLExceptionFilter implements GqlExceptionFilter { 11 | constructor(private readonly logger: Logger) { 12 | this.logger = logger 13 | } 14 | 15 | public catch(exception: HttpException, host: ArgumentsHost) { 16 | this.logger.error(exception.toString()) 17 | 18 | GqlArgumentsHost.create(host) 19 | return exception 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/shared/guard/GraphQLAuth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common' 2 | import { AuthGuard } from '@nestjs/passport' 3 | import { GqlExecutionContext } from '@nestjs/graphql' 4 | import { AuthenticationError } from 'apollo-server-express' 5 | import { ConfigService } from '../../config/config.service' 6 | 7 | @Injectable() 8 | export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate { 9 | private isEnvTest: boolean 10 | 11 | constructor(configService: ConfigService) { 12 | super() 13 | this.isEnvTest = configService.isEnvTest 14 | } 15 | 16 | public async canActivate(context: ExecutionContext): Promise { 17 | // 测试环境下跳过 token 校验 18 | if (this.isEnvTest) { 19 | return true 20 | } 21 | 22 | try { 23 | return (await super.canActivate(context)) as boolean 24 | } catch (e) { 25 | throw new AuthenticationError('The session has expired, please login again.') 26 | } 27 | } 28 | 29 | public getRequest(context: ExecutionContext) { 30 | const ctx = GqlExecutionContext.create(context) 31 | return ctx.getContext().req 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/shared/guard/roles.guard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | CanActivate, 4 | ExecutionContext, 5 | // UnauthorizedException, 6 | } from '@nestjs/common' 7 | import { Observable, of } from 'rxjs' 8 | import { Reflector } from '@nestjs/core' 9 | 10 | @Injectable() 11 | export class RolesGuard implements CanActivate { 12 | constructor(private readonly reflector: Reflector) { 13 | this.reflector = reflector 14 | } 15 | 16 | public canActivate(context: ExecutionContext): Observable { 17 | // eslint-disable-next-line 18 | const roles = this.reflector.get('roles', context.getHandler()) 19 | 20 | // const request = context.switchToHttp().getRequest() 21 | // const { 22 | // headers: { authorization }, 23 | // } = request 24 | 25 | // if (roles && roles.includes('admin') && !authorization) { 26 | // throw new UnauthorizedException() 27 | // } 28 | 29 | return of(true) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/shared/interceptors/delay.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common' 2 | import { sleep } from 'yancey-js-util' 3 | import { ConfigService } from '../../config/config.service' 4 | 5 | @Injectable() 6 | export class DelayInterceptor implements NestInterceptor { 7 | private needSimulateNetworkThrottle: boolean 8 | 9 | private isEnvDevelopment: boolean 10 | 11 | constructor(private readonly configService: ConfigService) { 12 | this.needSimulateNetworkThrottle = configService.needSimulateNetworkThrottle() 13 | this.isEnvDevelopment = configService.isEnvDevelopment 14 | } 15 | 16 | public async intercept(context: ExecutionContext, next: CallHandler): Promise { 17 | if (this.isEnvDevelopment && this.needSimulateNetworkThrottle) { 18 | await sleep() 19 | } 20 | 21 | return next.handle() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/shared/interfaces/exchange-position.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsNotEmpty, IsUUID, IsNumber } from 'class-validator' 3 | 4 | @InputType() 5 | export class ExchangePositionInput { 6 | @Field() 7 | @IsUUID() 8 | @IsNotEmpty() 9 | public readonly id: string 10 | 11 | @Field() 12 | @IsUUID() 13 | @IsNotEmpty() 14 | public readonly exchangedId: string 15 | 16 | @Field() 17 | @IsNumber() 18 | @IsNotEmpty() 19 | public readonly weight: number 20 | 21 | @Field() 22 | @IsNumber() 23 | @IsNotEmpty() 24 | public readonly exchangedWeight: number 25 | } 26 | -------------------------------------------------------------------------------- /src/shared/logger/logger.config.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common' 2 | import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston' 3 | import { GraphQLExceptionFilter } from '../filters/graqhql-exception.filter' 4 | 5 | export const configLogger = (app: INestApplication) => { 6 | const nestWinston = app.get(WINSTON_MODULE_NEST_PROVIDER) 7 | app.useLogger(nestWinston) 8 | app.useGlobalFilters(new GraphQLExceptionFilter(nestWinston.logger)) 9 | } 10 | -------------------------------------------------------------------------------- /src/shared/logger/logger.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { WinstonModule, utilities } from 'nest-winston' 3 | import * as winston from 'winston' 4 | import { DateTime } from 'luxon' 5 | 6 | const { 7 | errors, 8 | combine, 9 | json, 10 | timestamp, 11 | ms, 12 | prettyPrint, 13 | colorize, 14 | label, 15 | splat, 16 | } = winston.format 17 | 18 | @Module({ 19 | imports: [ 20 | WinstonModule.forRoot({ 21 | format: combine( 22 | colorize(), 23 | errors(), 24 | json(), 25 | timestamp({ format: 'HH:mm:ss YY/MM/DD' }), 26 | ms(), 27 | prettyPrint(), 28 | label({ 29 | label: '[BEG SERVICE] ', 30 | }), 31 | splat(), 32 | ), 33 | transports: [ 34 | new winston.transports.File({ 35 | level: 'error', 36 | filename: `error-${DateTime.now().toISODate()}.log`, 37 | dirname: 'logs', 38 | maxsize: 5000000, 39 | }), 40 | new winston.transports.Console({ 41 | level: 'debug', 42 | format: combine(utilities.format.nestLike()), 43 | }), 44 | 45 | new winston.transports.File({ 46 | filename: `application-${DateTime.now().toISODate()}.log`, 47 | dirname: 'logs', 48 | maxsize: 5000000, 49 | }), 50 | ], 51 | }), 52 | ], 53 | }) 54 | export class LoggerModule {} 55 | -------------------------------------------------------------------------------- /src/shared/middlewares/middleware.config.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common' 2 | import path from 'path' 3 | import serveFavicon from 'serve-favicon' 4 | import morgan from 'morgan' 5 | import helmet from 'helmet' 6 | 7 | export const configMiddlewares = (app: INestApplication) => { 8 | app.use(serveFavicon(path.join(process.cwd(), 'public/favicon.ico'))) 9 | app.use(morgan('combined')) 10 | app.use( 11 | helmet({ contentSecurityPolicy: process.env.NODE_ENV === 'production' ? undefined : false }), 12 | ) 13 | app.enableCors({}) 14 | } 15 | -------------------------------------------------------------------------------- /src/shared/pipes/GraphQLValidation.pipe.ts: -------------------------------------------------------------------------------- 1 | import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common' 2 | import { validate } from 'class-validator' 3 | import { plainToClass } from 'class-transformer' 4 | import { UserInputError } from 'apollo-server-express' 5 | 6 | @Injectable() 7 | export class GraphQLValidationPipe implements PipeTransform { 8 | public async transform(value: T, { metatype }: ArgumentMetadata) { 9 | if (!metatype || !this.toValidate(metatype)) { 10 | return value 11 | } 12 | const object = plainToClass(metatype, value) 13 | const errors = await validate(object) 14 | if (errors.length > 0) { 15 | const message = errors 16 | .map((validationError) => Object.values(validationError.constraints)) 17 | .flat() 18 | .join('; ') 19 | throw new UserInputError(message) 20 | } 21 | return value 22 | } 23 | 24 | private toValidate(metatype: Function): boolean { 25 | const types: Function[] = [String, Boolean, Number, Array, Object] 26 | return !types.includes(metatype) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | import QRCode from 'qrcode' 2 | import jwt from 'jsonwebtoken' 3 | import { ApolloError } from 'apollo-server-express' 4 | import { randomSeries } from 'yancey-js-util' 5 | import bcrypt from 'bcrypt' 6 | import { Payload } from '../auth/interfaces/jwt.interface' 7 | import { CORS_ORIGINS_PRODUCTION, CORS_ORIGINS_UN_PRODUCTION } from './constants' 8 | 9 | export const jsonStringify = (obj: T) => JSON.stringify(obj).replace(/"([^(")"]+)":/g, '$1:') 10 | 11 | export const generateQRCode = async (url: string) => { 12 | try { 13 | return await QRCode.toDataURL(url) 14 | } catch (err) { 15 | throw new ApolloError('Generate QR code failed!') 16 | } 17 | } 18 | 19 | export const generateRecoveryCodes = () => { 20 | const codes = Array.from({ length: 10 }, () => '') 21 | 22 | for (let i = 0; i < 10; i += 1) { 23 | const token = randomSeries(8, 8) 24 | const series = `${token.slice(0, 4)} ${token.slice(4)}` 25 | codes[i] = series 26 | } 27 | 28 | return codes 29 | } 30 | 31 | export const decodeJWT = (token: string) => jwt.decode(token.slice(7)) as Payload 32 | 33 | export const encryptPassword = (password: string) => bcrypt.hashSync(password, 10) 34 | 35 | export const configCORS = (isEnvProduction: boolean) => ({ 36 | origin: isEnvProduction ? CORS_ORIGINS_PRODUCTION : CORS_ORIGINS_UN_PRODUCTION, 37 | methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', 38 | credentials: true, 39 | preflightContinue: false, 40 | optionsSuccessStatus: 204, 41 | allowedHeaders: '*', 42 | }) 43 | -------------------------------------------------------------------------------- /src/users/dtos/update-user.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IPModel } from '../../auth/models/ip-model' 3 | 4 | @InputType() 5 | export class UpdateUserInput { 6 | @Field({ nullable: true }) 7 | public readonly id?: string 8 | 9 | @Field({ nullable: true }) 10 | public readonly isTOTP?: boolean 11 | 12 | @Field({ nullable: true }) 13 | public readonly totpSecret?: string 14 | 15 | @Field(() => [String], { nullable: true }) 16 | public readonly recoveryCodes?: string[] 17 | 18 | @Field({ nullable: true }) 19 | public readonly password?: string 20 | 21 | @Field(() => [String], { nullable: true }) 22 | public readonly loginStatistics?: IPModel[] 23 | 24 | @Field({ nullable: true }) 25 | public readonly name?: string 26 | 27 | @Field({ nullable: true }) 28 | public readonly location?: string 29 | 30 | @Field({ nullable: true }) 31 | public readonly organization?: string 32 | 33 | @Field({ nullable: true }) 34 | public readonly website?: string 35 | 36 | @Field({ nullable: true }) 37 | public readonly bio?: string 38 | 39 | @Field({ nullable: true }) 40 | public readonly avatarUrl?: string 41 | } 42 | -------------------------------------------------------------------------------- /src/users/interfaces/user.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | import { IPModel } from '../../auth/models/ip-model' 3 | 4 | export enum TwoFactorAuthentications { 5 | TOTP, 6 | SMS, 7 | } 8 | 9 | export enum Roles { 10 | SUPERUSER = 0b0000000000000, 11 | ADMIN = 0b0000000000001, 12 | USER = 0b0000000000010, 13 | NOT_CERTIFIED = 0b0000000000100, 14 | } 15 | 16 | export interface User extends Document { 17 | password: string 18 | readonly _id: string 19 | readonly username: string 20 | readonly email: string 21 | readonly role: Roles 22 | readonly name: string 23 | readonly location: string 24 | readonly organization: string 25 | readonly website: string 26 | readonly bio: string 27 | readonly avatarUrl: string 28 | readonly isTOTP: boolean 29 | readonly totpSecret: string 30 | readonly recoveryCodes: string[] 31 | readonly loginStatistics: IPModel[] 32 | readonly createdAt: Date 33 | readonly updatedAt: Date 34 | isValidPassword(plainPwd: string, encryptedPwd: string): boolean 35 | } 36 | -------------------------------------------------------------------------------- /src/users/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | import { IPModel } from '../../auth/models/ip-model' 3 | 4 | @ObjectType() 5 | export class UserModel { 6 | @Field(() => ID) 7 | public readonly _id: string 8 | 9 | @Field() 10 | public readonly authorization: string 11 | 12 | @Field() 13 | public readonly username: string 14 | 15 | @Field() 16 | public readonly email: string 17 | 18 | @Field() 19 | public readonly password: string 20 | 21 | @Field() 22 | public readonly role: number 23 | 24 | @Field() 25 | public readonly name: string 26 | 27 | @Field() 28 | public readonly location: string 29 | 30 | @Field() 31 | public readonly organization: string 32 | 33 | @Field() 34 | public readonly website: string 35 | 36 | @Field() 37 | public readonly bio: string 38 | 39 | @Field() 40 | public readonly avatarUrl: string 41 | 42 | @Field() 43 | public readonly isTOTP: boolean 44 | 45 | @Field() 46 | public readonly totpSecret: string 47 | 48 | @Field(() => [String]) 49 | public readonly recoveryCodes: string[] 50 | 51 | @Field(() => [IPModel]) 52 | public readonly loginStatistics: IPModel[] 53 | 54 | @Field() 55 | public readonly createdAt: Date 56 | 57 | @Field() 58 | public readonly updatedAt: Date 59 | } 60 | -------------------------------------------------------------------------------- /src/users/schemas/users.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | import bcrypt from 'bcrypt' 4 | import { encryptPassword } from '../../shared/utils' 5 | import { IPModel } from '../../auth/models/ip-model' 6 | import { Roles, User } from '../interfaces/user.interface' 7 | 8 | export const UserSchema = new mongoose.Schema( 9 | { 10 | _id: { 11 | type: String, 12 | default: v4, 13 | }, 14 | email: { 15 | type: String, 16 | required: true, 17 | }, 18 | username: { 19 | default: '', 20 | type: String, 21 | required: true, 22 | }, 23 | password: { 24 | type: String, 25 | required: true, 26 | }, 27 | role: { 28 | default: Roles.NOT_CERTIFIED, 29 | type: Number, 30 | required: true, 31 | }, 32 | name: { 33 | default: '', 34 | type: String, 35 | required: false, 36 | }, 37 | location: { 38 | default: '', 39 | type: String, 40 | required: false, 41 | }, 42 | organization: { 43 | default: '', 44 | type: String, 45 | required: false, 46 | }, 47 | website: { 48 | default: '', 49 | type: String, 50 | required: false, 51 | }, 52 | bio: { 53 | default: '', 54 | type: String, 55 | required: false, 56 | }, 57 | avatarUrl: { 58 | default: '', 59 | type: String, 60 | required: false, 61 | }, 62 | isTOTP: { 63 | default: false, 64 | type: Boolean, 65 | required: true, 66 | }, 67 | totpSecret: { 68 | type: String, 69 | required: false, 70 | }, 71 | recoveryCodes: { 72 | type: [String], 73 | required: false, 74 | }, 75 | loginStatistics: { 76 | type: [], 77 | required: false, 78 | }, 79 | }, 80 | { 81 | collection: 'user', 82 | timestamps: true, 83 | }, 84 | ) 85 | 86 | UserSchema.pre('save', function (next) { 87 | this.password = encryptPassword(this.password) 88 | next() 89 | }) 90 | 91 | UserSchema.methods.isValidPassword = function (password: string): boolean { 92 | return bcrypt.compareSync(password, this.password) 93 | } 94 | -------------------------------------------------------------------------------- /src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { UserResolver } from './users.resolver' 4 | import { UsersService } from './users.service' 5 | import { UserSchema } from './schemas/users.schema' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'User', schema: UserSchema }])], 9 | providers: [UserResolver, UsersService], 10 | exports: [UsersService], 11 | }) 12 | export class UsersModule {} 13 | -------------------------------------------------------------------------------- /src/users/users.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Request } from 'express' 3 | import { Args, Resolver, Mutation } from '@nestjs/graphql' 4 | import { UsersService } from './users.service' 5 | import { UserModel } from './models/user.model' 6 | import { UpdateUserInput } from './dtos/update-user.input' 7 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 8 | import { ReqDecorator } from '../shared/decorators/req.decorator' 9 | import { decodeJWT } from '../shared/utils' 10 | 11 | @Resolver(() => UserModel) 12 | export class UserResolver { 13 | constructor(private readonly usersService: UsersService) { 14 | this.usersService = usersService 15 | } 16 | 17 | @Mutation(() => UserModel) 18 | @UseGuards(JwtAuthGuard) 19 | public async updateUser(@Args('input') input: UpdateUserInput, @ReqDecorator() req: Request) { 20 | const { sub: userId } = decodeJWT(req.headers.authorization) 21 | return this.usersService.updateUser({ ...input, id: userId }) 22 | } 23 | 24 | @Mutation(() => UserModel) 25 | @UseGuards(JwtAuthGuard) 26 | public async updateUserName( 27 | @Args({ name: 'username', type: () => String }) username: string, 28 | @ReqDecorator() req: Request, 29 | ) { 30 | return this.usersService.updateUserName(username, req) 31 | } 32 | 33 | @Mutation(() => UserModel) 34 | @UseGuards(JwtAuthGuard) 35 | public async updateEmail( 36 | @Args({ name: 'email', type: () => String }) email: string, 37 | @ReqDecorator() req: Request, 38 | ) { 39 | return this.usersService.updateEmail(email, req) 40 | } 41 | 42 | @Mutation(() => UserModel) 43 | @UseGuards(JwtAuthGuard) 44 | public async deleteAccount(@ReqDecorator() req: Request) { 45 | return this.usersService.deleteOneById(req) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/users/users.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model } from 'mongoose' 4 | import { Request } from 'express' 5 | import { ForbiddenError } from 'apollo-server-express' 6 | import { User } from './interfaces/user.interface' 7 | import { UpdateUserInput } from './dtos/update-user.input' 8 | import { RegisterInput } from '../auth/dtos/register.input' 9 | import { decodeJWT } from '../shared/utils' 10 | 11 | @Injectable() 12 | export class UsersService { 13 | constructor( 14 | @InjectModel('User') 15 | private readonly userModel: Model, 16 | ) { 17 | this.userModel = userModel 18 | } 19 | 20 | public async getUserCount(): Promise { 21 | return this.userModel.estimatedDocumentCount() 22 | } 23 | 24 | public async findOneById(id: string): Promise { 25 | return this.userModel.findById(id) 26 | } 27 | 28 | public async findOneByEmail(email: string): Promise { 29 | return this.userModel.findOne({ email }) 30 | } 31 | 32 | public async findOneByUserName(username: string): Promise { 33 | return this.userModel.findOne({ username }) 34 | } 35 | 36 | public async create(input: RegisterInput): Promise { 37 | return this.userModel.create(input) 38 | } 39 | 40 | public async updateUser(input: UpdateUserInput): Promise { 41 | const { id, ...rest } = input 42 | return this.userModel.findByIdAndUpdate(id, rest, { new: true }) 43 | } 44 | 45 | public async updateUserName(username: string, req: Request): Promise { 46 | const { sub: id } = decodeJWT(req.headers.authorization) 47 | const user = await this.findOneByUserName(username) 48 | 49 | if (!user) { 50 | return this.userModel.findByIdAndUpdate(id, { username }, { new: true }) 51 | } 52 | 53 | throw new ForbiddenError(`The username「${username}」 has been used.`) 54 | } 55 | 56 | public async updateEmail(email: string, req: Request): Promise { 57 | const { sub: id } = decodeJWT(req.headers.authorization) 58 | const user = await this.findOneByEmail(email) 59 | 60 | if (!user) { 61 | return this.userModel.findByIdAndUpdate(id, { email }, { new: true }) 62 | } 63 | throw new ForbiddenError(`The email「${email}」 has been used.`) 64 | } 65 | 66 | public async deleteOneById(req: Request): Promise { 67 | const { sub: id } = decodeJWT(req.headers.authorization) 68 | return this.userModel.findByIdAndDelete(id) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/yancey-music/dtos/create-yancey-music.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty, IsUrl, IsDate } from 'class-validator' 3 | 4 | @InputType() 5 | export class CreateYanceyMusicInput { 6 | @Field() 7 | @IsString() 8 | @IsNotEmpty() 9 | public readonly title: string 10 | 11 | @Field() 12 | @IsUrl() 13 | @IsNotEmpty() 14 | public readonly soundCloudUrl: string 15 | 16 | @Field() 17 | @IsUrl() 18 | @IsNotEmpty() 19 | public readonly posterUrl: string 20 | 21 | @Field() 22 | @IsDate() 23 | @IsNotEmpty() 24 | public readonly releaseDate: Date 25 | } 26 | -------------------------------------------------------------------------------- /src/yancey-music/dtos/update-yancey-music.input.ts: -------------------------------------------------------------------------------- 1 | import { InputType, Field } from '@nestjs/graphql' 2 | import { IsString, IsNotEmpty } from 'class-validator' 3 | import { CreateYanceyMusicInput } from './create-yancey-music.input' 4 | 5 | @InputType() 6 | export class UpdateYanceyMusicInput extends CreateYanceyMusicInput { 7 | @Field() 8 | @IsString() 9 | @IsNotEmpty() 10 | public readonly id: string 11 | } 12 | -------------------------------------------------------------------------------- /src/yancey-music/interfaces/yancey-music.interface.ts: -------------------------------------------------------------------------------- 1 | import { Document } from 'mongoose' 2 | 3 | export interface YanceyMusic extends Document { 4 | readonly _id: string 5 | readonly title: string 6 | readonly soundCloudUrl: string 7 | readonly posterUrl: string 8 | readonly releaseDate: Date 9 | readonly createdAt: Date 10 | readonly updatedAt: Date 11 | } 12 | -------------------------------------------------------------------------------- /src/yancey-music/models/yancey-music.model.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from '@nestjs/graphql' 2 | 3 | @ObjectType() 4 | export class YanceyMusicModel { 5 | @Field(() => ID) 6 | public readonly _id: string 7 | 8 | @Field() 9 | public readonly title: string 10 | 11 | @Field() 12 | public readonly soundCloudUrl: string 13 | 14 | @Field() 15 | public readonly posterUrl: string 16 | 17 | @Field() 18 | public readonly releaseDate: Date 19 | 20 | @Field() 21 | public readonly createdAt: Date 22 | 23 | @Field() 24 | public readonly updatedAt: Date 25 | } 26 | -------------------------------------------------------------------------------- /src/yancey-music/schemas/yancey-music.schema.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import { v4 } from 'uuid' 3 | 4 | export const YanceyMusicSchema = new mongoose.Schema( 5 | { 6 | _id: { 7 | type: String, 8 | default: v4, 9 | }, 10 | title: { 11 | type: String, 12 | required: true, 13 | }, 14 | soundCloudUrl: { 15 | type: String, 16 | required: true, 17 | }, 18 | posterUrl: { 19 | type: String, 20 | required: true, 21 | }, 22 | releaseDate: { 23 | type: Date, 24 | required: true, 25 | }, 26 | }, 27 | { 28 | collection: 'yancey_music', 29 | timestamps: true, 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /src/yancey-music/yancey-music.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common' 2 | import { MongooseModule } from '@nestjs/mongoose' 3 | import { YanceyMusicSchema } from './schemas/yancey-music.schema' 4 | import { YanceyMusicResolver } from './yancey-music.resolver' 5 | import { YanceyMusicService } from './yancey-music.service' 6 | 7 | @Module({ 8 | imports: [MongooseModule.forFeature([{ name: 'YanceyMusic', schema: YanceyMusicSchema }])], 9 | providers: [YanceyMusicResolver, YanceyMusicService], 10 | }) 11 | export class YanceyMusicModule {} 12 | -------------------------------------------------------------------------------- /src/yancey-music/yancey-music.resolver.ts: -------------------------------------------------------------------------------- 1 | import { UseGuards } from '@nestjs/common' 2 | import { Args, Query, Resolver, Mutation, ID } from '@nestjs/graphql' 3 | import { YanceyMusicService } from './yancey-music.service' 4 | import { YanceyMusicModel } from './models/yancey-music.model' 5 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 6 | import { CreateYanceyMusicInput } from './dtos/create-yancey-music.input' 7 | import { UpdateYanceyMusicInput } from './dtos/update-yancey-music.input' 8 | import { JwtAuthGuard } from '../shared/guard/GraphQLAuth.guard' 9 | 10 | @Resolver(() => YanceyMusicModel) 11 | export class YanceyMusicResolver { 12 | constructor(private readonly yanceyMusicsService: YanceyMusicService) { 13 | this.yanceyMusicsService = yanceyMusicsService 14 | } 15 | 16 | @Query(() => [YanceyMusicModel]) 17 | public async getYanceyMusic() { 18 | return this.yanceyMusicsService.findAll() 19 | } 20 | 21 | @Query(() => YanceyMusicModel) 22 | public async getYanceyMusicById(@Args({ name: 'id', type: () => ID }) id: string) { 23 | return this.yanceyMusicsService.findOneById(id) 24 | } 25 | 26 | @Mutation(() => YanceyMusicModel) 27 | @UseGuards(JwtAuthGuard) 28 | public async createYanceyMusic(@Args('input') input: CreateYanceyMusicInput) { 29 | return this.yanceyMusicsService.create(input) 30 | } 31 | 32 | @Mutation(() => YanceyMusicModel) 33 | @UseGuards(JwtAuthGuard) 34 | public async updateYanceyMusicById(@Args('input') input: UpdateYanceyMusicInput) { 35 | return this.yanceyMusicsService.update(input) 36 | } 37 | 38 | @Mutation(() => YanceyMusicModel) 39 | @UseGuards(JwtAuthGuard) 40 | public async deleteYanceyMusicById(@Args({ name: 'id', type: () => ID }) id: string) { 41 | return this.yanceyMusicsService.deleteOneById(id) 42 | } 43 | 44 | @Mutation(() => BatchDeleteModel) 45 | @UseGuards(JwtAuthGuard) 46 | public async deleteYanceyMusic(@Args({ name: 'ids', type: () => [ID] }) ids: string[]) { 47 | return this.yanceyMusicsService.batchDelete(ids) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/yancey-music/yancey-music.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common' 2 | import { InjectModel } from '@nestjs/mongoose' 3 | import { Model, Document } from 'mongoose' 4 | import { CreateYanceyMusicInput } from './dtos/create-yancey-music.input' 5 | import { UpdateYanceyMusicInput } from './dtos/update-yancey-music.input' 6 | import { YanceyMusicModel } from './models/yancey-music.model' 7 | import { YanceyMusic } from './interfaces/yancey-music.interface' 8 | import { BatchDeleteModel } from '../database/models/batch-delete.model' 9 | 10 | @Injectable() 11 | export class YanceyMusicService { 12 | constructor( 13 | @InjectModel('YanceyMusic') 14 | private readonly yanceyMusicModel: Model, 15 | ) { 16 | this.yanceyMusicModel = yanceyMusicModel 17 | } 18 | 19 | public async findAll(): Promise { 20 | return this.yanceyMusicModel.find({}).sort({ updatedAt: -1 }) 21 | } 22 | 23 | public async findOneById(id: string): Promise { 24 | return this.yanceyMusicModel.findById(id) 25 | } 26 | 27 | public async create(yanceyMusicInput: CreateYanceyMusicInput): Promise { 28 | return this.yanceyMusicModel.create(yanceyMusicInput) 29 | } 30 | 31 | public async update(yanceyMusicInput: UpdateYanceyMusicInput): Promise { 32 | const { id, ...rest } = yanceyMusicInput 33 | return this.yanceyMusicModel.findByIdAndUpdate(id, rest, { new: true }) 34 | } 35 | 36 | public async deleteOneById(id: string): Promise { 37 | return this.yanceyMusicModel.findByIdAndDelete(id) 38 | } 39 | 40 | public async batchDelete(ids: string[]): Promise { 41 | const res = await this.yanceyMusicModel.deleteMany({ 42 | _id: { $in: ids }, 43 | }) 44 | 45 | return { 46 | ...res, 47 | ids, 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/agenda.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NestApplication } from '@nestjs/core' 2 | import { Test, TestingModule } from '@nestjs/testing' 3 | import { MongooseModule } from '@nestjs/mongoose' 4 | import { GraphQLModule } from '@nestjs/graphql' 5 | import request from 'supertest' 6 | import { SCHEMA_GQL_FILE_NAME } from '../src/shared/constants' 7 | import { ConfigModule } from '../src/config/config.module' 8 | import { ConfigService } from '../src/config/config.service' 9 | import { AgendaModule } from '../src/agenda/agenda.module' 10 | import { AgendaModel } from '../src/agenda/models/agenda.model' 11 | import { CreateAgendaInput } from '../src/agenda/dtos/create-agenda.input' 12 | import { UpdateAgendaInput } from '../src/agenda/dtos/update-agenda.input' 13 | import { BatchUpdateModel } from '../src/database/models/batch-update.model' 14 | 15 | describe('AgendaController (e2e)', () => { 16 | let app: NestApplication 17 | beforeAll(async () => { 18 | const moduleFixture: TestingModule = await Test.createTestingModule({ 19 | imports: [ 20 | ConfigModule, 21 | AgendaModule, 22 | MongooseModule.forRootAsync({ 23 | useFactory: async (configService: ConfigService) => ({ 24 | uri: configService.getMongoURI(), 25 | useFindAndModify: false, 26 | useUnifiedTopology: true, 27 | useNewUrlParser: true, 28 | useCreateIndex: true, 29 | }), 30 | inject: [ConfigService], 31 | }), 32 | GraphQLModule.forRoot({ 33 | autoSchemaFile: SCHEMA_GQL_FILE_NAME, 34 | }), 35 | ], 36 | }).compile() 37 | app = moduleFixture.createNestApplication() 38 | await app.init() 39 | }) 40 | 41 | afterAll(async () => { 42 | await app.close() 43 | }) 44 | 45 | const createdData: CreateAgendaInput = { 46 | title: 'metting', 47 | startDate: new Date().toJSON(), 48 | allDay: false, 49 | } 50 | 51 | let id = '' 52 | 53 | const updatedData: UpdateAgendaInput = { 54 | id, 55 | title: 'exercise', 56 | startDate: new Date().toJSON(), 57 | } 58 | 59 | const createDataString = JSON.stringify(createdData).replace(/"([^(")"]+)":/g, '$1:') 60 | 61 | // CREATE_ONE 62 | it('createAgenda', () => { 63 | const createOneTypeDefs = ` 64 | mutation CreateAgenda { 65 | createAgenda(input: ${createDataString}) { 66 | _id 67 | title 68 | startDate 69 | } 70 | }` 71 | 72 | return request(app.getHttpServer()) 73 | .post('/graphql') 74 | .send({ 75 | operationName: null, 76 | query: createOneTypeDefs, 77 | }) 78 | .expect(({ body }) => { 79 | const testData: AgendaModel = body.data.createAgenda 80 | 81 | id = testData._id 82 | 83 | expect(testData.title).toBe(createdData.title) 84 | }) 85 | .expect(200) 86 | }) 87 | 88 | // READ_ALL 89 | it('getAgenda', () => { 90 | const getAllTypeDefs = ` 91 | query GetAgenda { 92 | getAgenda { 93 | _id 94 | title 95 | startDate 96 | } 97 | }` 98 | 99 | return request(app.getHttpServer()) 100 | .post('/graphql') 101 | .send({ 102 | operationName: null, 103 | query: getAllTypeDefs, 104 | }) 105 | .expect(({ body }) => { 106 | const testData: AgendaModel[] = body.data.getAgenda 107 | 108 | const firstData = testData[0] 109 | 110 | expect(testData.length).toBeGreaterThan(0) 111 | expect(firstData._id).toBe(id) 112 | expect(firstData.title).toBe(createdData.title) 113 | }) 114 | .expect(200) 115 | }) 116 | 117 | // UPDATE_ONE 118 | it('updateAgendaById', () => { 119 | const updateDataString = JSON.stringify({ ...updatedData, id }).replace(/"([^(")"]+)":/g, '$1:') 120 | 121 | const updateOneByIdTypeDefs = ` 122 | mutation UpdateAgendaById { 123 | updateAgendaById(input: ${updateDataString}) { 124 | _id 125 | title 126 | startDate 127 | } 128 | }` 129 | 130 | return request(app.getHttpServer()) 131 | .post('/graphql') 132 | .send({ 133 | operationName: null, 134 | query: updateOneByIdTypeDefs, 135 | }) 136 | .expect(({ body }) => { 137 | const testData: AgendaModel = body.data.updateAgendaById 138 | 139 | expect(testData.title).toBe(updatedData.title) 140 | }) 141 | .expect(200) 142 | }) 143 | 144 | // DELETE_ONE 145 | it('deleteAgendaById', () => { 146 | const deleteOneByIdTypeDefs = ` 147 | mutation DeleteAgendaById { 148 | deleteAgendaById(id: "${id}") { 149 | _id 150 | title 151 | startDate 152 | } 153 | }` 154 | 155 | return request(app.getHttpServer()) 156 | .post('/graphql') 157 | .send({ 158 | operationName: null, 159 | query: deleteOneByIdTypeDefs, 160 | }) 161 | .expect(({ body }) => { 162 | const testData: AgendaModel = body.data.deleteAgendaById 163 | 164 | expect(testData.title).toBe(updatedData.title) 165 | }) 166 | .expect(200) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /test/auth.DO_NOT_TEST_ME.ts: -------------------------------------------------------------------------------- 1 | import { NestApplication } from '@nestjs/core' 2 | import { Test, TestingModule } from '@nestjs/testing' 3 | import { MongooseModule } from '@nestjs/mongoose' 4 | import { GraphQLModule } from '@nestjs/graphql' 5 | import request from 'supertest' 6 | import { randomSeries } from 'yancey-js-util' 7 | import { SCHEMA_GQL_FILE_NAME } from '../src/shared/constants' 8 | import { ConfigModule } from '../src/config/config.module' 9 | import { ConfigService } from '../src/config/config.service' 10 | import { AuthModule } from '../src/auth/auth.module' 11 | import { UserModel } from '../src/users/models/user.model' 12 | import { LoginInput } from '../src/auth/dtos/login.input' 13 | import { RegisterInput } from '../src/auth/dtos/register.input' 14 | 15 | describe('AuthController (e2e)', () => { 16 | let app: NestApplication 17 | beforeAll(async () => { 18 | const moduleFixture: TestingModule = await Test.createTestingModule({ 19 | imports: [ 20 | ConfigModule, 21 | AuthModule, 22 | MongooseModule.forRootAsync({ 23 | useFactory: async (configService: ConfigService) => ({ 24 | uri: configService.getMongoURI(), 25 | useFindAndModify: false, 26 | useUnifiedTopology: true, 27 | useNewUrlParser: true, 28 | useCreateIndex: true, 29 | }), 30 | inject: [ConfigService], 31 | }), 32 | GraphQLModule.forRoot({ 33 | autoSchemaFile: SCHEMA_GQL_FILE_NAME, 34 | }), 35 | ], 36 | }).compile() 37 | app = moduleFixture.createNestApplication() 38 | await app.init() 39 | }) 40 | 41 | afterAll(async () => { 42 | await app.close() 43 | }) 44 | 45 | const loginData: LoginInput = { 46 | email: `${randomSeries(10)}@example.com`, 47 | password: 'abcd1234,', 48 | token: '', 49 | } 50 | 51 | const registerData: RegisterInput = { 52 | ...loginData, 53 | username: randomSeries(10), 54 | } 55 | 56 | const registerDataString = JSON.stringify(registerData).replace(/"([^(")"]+)":/g, '$1:') 57 | 58 | const loginDataString = JSON.stringify(loginData).replace(/"([^(")"]+)":/g, '$1:') 59 | 60 | let id = '' 61 | 62 | // REGISTER 63 | it('register', async () => { 64 | const registerTypeDefs = ` 65 | mutation Register { 66 | register(input: ${registerDataString}) { 67 | _id 68 | email 69 | authorization 70 | role 71 | avatarUrl 72 | username 73 | isTOTP 74 | createdAt 75 | updatedAt 76 | } 77 | }` 78 | 79 | return request(app.getHttpServer()) 80 | .post('/graphql') 81 | .send({ 82 | operationName: null, 83 | query: registerTypeDefs, 84 | }) 85 | .expect(({ body }) => { 86 | const testData: UserModel = body.data.register 87 | id = testData._id 88 | expect(testData.username).toBe(registerData.username) 89 | expect(testData.email).toBe(registerData.email) 90 | }) 91 | .expect(200) 92 | }) 93 | 94 | // LOGIN 95 | it('login', async () => { 96 | const loginTypeDefs = ` 97 | query Login { 98 | login(input: ${loginDataString}) { 99 | _id 100 | email 101 | authorization 102 | role 103 | avatarUrl 104 | username 105 | isTOTP 106 | createdAt 107 | updatedAt 108 | } 109 | }` 110 | 111 | return request(app.getHttpServer()) 112 | .post('/graphql') 113 | .send({ 114 | operationName: null, 115 | query: loginTypeDefs, 116 | }) 117 | .expect(({ body }) => { 118 | const testData: UserModel = body.data.login 119 | 120 | expect(testData._id).toBe(id) 121 | expect(testData.email).toBe(loginData.email) 122 | }) 123 | .expect(200) 124 | }) 125 | }) 126 | -------------------------------------------------------------------------------- /test/blog-statistics.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NestApplication } from '@nestjs/core' 2 | import { Test, TestingModule } from '@nestjs/testing' 3 | import { MongooseModule } from '@nestjs/mongoose' 4 | import { GraphQLModule } from '@nestjs/graphql' 5 | import request from 'supertest' 6 | import { SCHEMA_GQL_FILE_NAME } from '../src/shared/constants' 7 | import { ConfigModule } from '../src/config/config.module' 8 | import { ConfigService } from '../src/config/config.service' 9 | import { PostStatisticsModule } from '../src/post-statistics/post-statistics.module' 10 | import { PostStatisticsModel } from '../src/post-statistics/models/post-statistics.model' 11 | import { PostStatisticsGroupModel } from '../src/post-statistics/models/post-statistics-group.model' 12 | import { CreatePostStatisticsInput } from '../src/post-statistics/dtos/create-post-statistics.input' 13 | 14 | describe('PostStatisticsController (e2e)', () => { 15 | let app: NestApplication 16 | beforeAll(async () => { 17 | const moduleFixture: TestingModule = await Test.createTestingModule({ 18 | imports: [ 19 | ConfigModule, 20 | PostStatisticsModule, 21 | MongooseModule.forRootAsync({ 22 | useFactory: async (configService: ConfigService) => ({ 23 | uri: configService.getMongoURI(), 24 | useFindAndModify: false, 25 | useUnifiedTopology: true, 26 | useNewUrlParser: true, 27 | useCreateIndex: true, 28 | }), 29 | inject: [ConfigService], 30 | }), 31 | GraphQLModule.forRoot({ 32 | autoSchemaFile: SCHEMA_GQL_FILE_NAME, 33 | }), 34 | ], 35 | }).compile() 36 | 37 | app = moduleFixture.createNestApplication() 38 | await app.init() 39 | }) 40 | 41 | afterAll(async () => { 42 | await app.close() 43 | }) 44 | 45 | const createdData: CreatePostStatisticsInput = { 46 | postId: '36f27dc5-9adc-4ded-918f-d1bf9dc1ad4a', 47 | postName: 'postName', 48 | scenes: 'switched to public', 49 | } 50 | 51 | let id = '' 52 | 53 | const createDataString = JSON.stringify(createdData).replace(/"([^(")"]+)":/g, '$1:') 54 | 55 | // CREATE_ONE 56 | it('createPostStatistics', async () => { 57 | const createOneTypeDefs = ` 58 | mutation CreatePostStatistics { 59 | createPostStatistics(input: ${createDataString}) { 60 | _id 61 | postId 62 | postName 63 | scenes 64 | } 65 | }` 66 | 67 | return request(app.getHttpServer()) 68 | .post('/graphql') 69 | .send({ 70 | operationName: null, 71 | query: createOneTypeDefs, 72 | }) 73 | .expect(({ body }) => { 74 | const testData: PostStatisticsModel = body.data.createPostStatistics 75 | id = testData._id 76 | expect(testData.postId).toBe(createdData.postId) 77 | expect(testData.postName).toBe(createdData.postName) 78 | expect(testData.scenes).toBe(createdData.scenes) 79 | }) 80 | .expect(200) 81 | }) 82 | 83 | // READ_ALL 84 | it('getPostStatistics', async () => { 85 | const getAllTypeDefs = ` 86 | query GetPostStatistics { 87 | getPostStatistics { 88 | _id 89 | count 90 | items { 91 | postId 92 | postName 93 | scenes 94 | operatedAt 95 | } 96 | } 97 | }` 98 | 99 | return request(app.getHttpServer()) 100 | .post('/graphql') 101 | .send({ 102 | operationName: null, 103 | query: getAllTypeDefs, 104 | }) 105 | .expect(({ body }) => { 106 | const testData: PostStatisticsGroupModel[] = body.data.getPostStatistics 107 | 108 | const firstData = testData[0] 109 | 110 | expect(testData.length).toBeGreaterThan(0) 111 | expect(firstData.count).toBeGreaterThan(0) 112 | }) 113 | .expect(200) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/global-setting.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NestApplication } from '@nestjs/core' 2 | import { Test, TestingModule } from '@nestjs/testing' 3 | import { MongooseModule } from '@nestjs/mongoose' 4 | import { GraphQLModule } from '@nestjs/graphql' 5 | import request from 'supertest' 6 | import { SCHEMA_GQL_FILE_NAME } from '../src/shared/constants' 7 | import { ConfigModule } from '../src/config/config.module' 8 | import { ConfigService } from '../src/config/config.service' 9 | import { GlobalSettingModule } from '../src/global-setting/global-setting.module' 10 | import { GlobalSettingModel } from '../src/global-setting/models/global-setting.model' 11 | import { UpdateGlobalSettingInput } from '../src/global-setting/dtos/update-global-setting.input' 12 | 13 | describe('GlobalSettingController (e2e)', () => { 14 | let app: NestApplication 15 | beforeAll(async () => { 16 | const moduleFixture: TestingModule = await Test.createTestingModule({ 17 | imports: [ 18 | ConfigModule, 19 | GlobalSettingModule, 20 | MongooseModule.forRootAsync({ 21 | useFactory: async (configService: ConfigService) => ({ 22 | uri: configService.getMongoURI(), 23 | useFindAndModify: false, 24 | useUnifiedTopology: true, 25 | useNewUrlParser: true, 26 | }), 27 | inject: [ConfigService], 28 | }), 29 | GraphQLModule.forRoot({ 30 | autoSchemaFile: SCHEMA_GQL_FILE_NAME, 31 | }), 32 | ], 33 | }).compile() 34 | 35 | app = moduleFixture.createNestApplication() 36 | await app.init() 37 | }) 38 | 39 | afterAll(async () => { 40 | await app.close() 41 | }) 42 | 43 | let id = '' 44 | 45 | // READ_ALL 46 | it('getGlobalSetting', async () => { 47 | const getAllTypeDefs = ` 48 | query GetGlobalSetting { 49 | getGlobalSetting { 50 | _id 51 | releasePostId 52 | cvPostId 53 | isGrayTheme 54 | createdAt 55 | updatedAt 56 | } 57 | }` 58 | 59 | return request(app.getHttpServer()) 60 | .post('/graphql') 61 | .send({ 62 | operationName: null, 63 | query: getAllTypeDefs, 64 | }) 65 | .expect(({ body }) => { 66 | const testData: GlobalSettingModel = body.data.getGlobalSetting 67 | id = testData._id 68 | 69 | expect(testData._id).toBe(id) 70 | }) 71 | .expect(200) 72 | }) 73 | 74 | const updatedData: UpdateGlobalSettingInput = { 75 | id, 76 | releasePostId: '36f27dc5-9adc-4ded-918f-d1bf9dc1ad4a', 77 | cvPostId: '36f27dc5-9adc-4ded-918f-d1bf9dc1ad4b', 78 | isGrayTheme: true, 79 | } 80 | 81 | // CREATE_ONE 82 | it('updateGlobalSettingById', async () => { 83 | const updateOneTypeDefs = ` 84 | mutation UpdateGlobalSettingById { 85 | updateGlobalSettingById(input: ${JSON.stringify({ ...updatedData, id }).replace( 86 | /"([^(")"]+)":/g, 87 | '$1:', 88 | )}) { 89 | _id 90 | releasePostId 91 | cvPostId 92 | isGrayTheme 93 | createdAt 94 | updatedAt 95 | } 96 | }` 97 | 98 | return request(app.getHttpServer()) 99 | .post('/graphql') 100 | .send({ 101 | operationName: null, 102 | query: updateOneTypeDefs, 103 | }) 104 | .expect(({ body }) => { 105 | const testData: GlobalSettingModel = body.data.updateGlobalSettingById 106 | expect(testData.releasePostId).toBe(updatedData.releasePostId) 107 | expect(testData.cvPostId).toBe(updatedData.cvPostId) 108 | expect(testData.isGrayTheme).toBeTruthy() 109 | }) 110 | .expect(200) 111 | }) 112 | }) 113 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": "../test", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/mottos.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { NestApplication } from '@nestjs/core' 2 | import { Test, TestingModule } from '@nestjs/testing' 3 | import { MongooseModule } from '@nestjs/mongoose' 4 | import { GraphQLModule } from '@nestjs/graphql' 5 | import request from 'supertest' 6 | import { SCHEMA_GQL_FILE_NAME } from '../src/shared/constants' 7 | import { ConfigModule } from '../src/config/config.module' 8 | import { ConfigService } from '../src/config/config.service' 9 | import { MottosModule } from '../src/mottos/mottos.module' 10 | import { MottoModel } from '../src/mottos/models/mottos.model' 11 | import { CreateMottoInput } from '../src/mottos/dtos/create-motto.input' 12 | import { UpdateMottoInput } from '../src/mottos/dtos/update-motto.input' 13 | import { BatchDeleteModel } from '../src/database/models/batch-delete.model' 14 | 15 | describe('MottosController (e2e)', () => { 16 | let app: NestApplication 17 | beforeAll(async () => { 18 | const moduleFixture: TestingModule = await Test.createTestingModule({ 19 | imports: [ 20 | ConfigModule, 21 | MottosModule, 22 | MongooseModule.forRootAsync({ 23 | useFactory: async (configService: ConfigService) => ({ 24 | uri: configService.getMongoURI(), 25 | useFindAndModify: false, 26 | useUnifiedTopology: true, 27 | useNewUrlParser: true, 28 | useCreateIndex: true, 29 | }), 30 | inject: [ConfigService], 31 | }), 32 | GraphQLModule.forRoot({ 33 | autoSchemaFile: SCHEMA_GQL_FILE_NAME, 34 | }), 35 | ], 36 | }).compile() 37 | 38 | app = moduleFixture.createNestApplication() 39 | await app.init() 40 | }) 41 | 42 | afterAll(async () => { 43 | await app.close() 44 | }) 45 | 46 | const createdData: CreateMottoInput = { 47 | content: 'blog-be-next', 48 | } 49 | 50 | let id = '' 51 | 52 | const updatedData: UpdateMottoInput = { 53 | id, 54 | content: 'blog-be-cms', 55 | } 56 | 57 | const createDataString = JSON.stringify(createdData).replace(/"([^(")"]+)":/g, '$1:') 58 | 59 | // CREATE_ONE 60 | it('createMotto', async () => { 61 | const createOneTypeDefs = ` 62 | mutation CreateMotto { 63 | createMotto(input: ${createDataString}) { 64 | _id 65 | content 66 | } 67 | }` 68 | 69 | return request(app.getHttpServer()) 70 | .post('/graphql') 71 | .send({ 72 | operationName: null, 73 | query: createOneTypeDefs, 74 | }) 75 | .expect(({ body }) => { 76 | const testData: MottoModel = body.data.createMotto 77 | id = testData._id 78 | expect(testData.content).toBe(createdData.content) 79 | }) 80 | .expect(200) 81 | }) 82 | 83 | // EXCHANGE 84 | it('exchangePositionMotto', async () => { 85 | const exchangeTypeDefs = ` 86 | mutation ExchangePositionMotto { 87 | exchangePositionMotto(input: ${JSON.stringify({ 88 | id, 89 | exchangedId: id, 90 | weight: 1, 91 | exchangedWeight: 1, 92 | }).replace(/"([^(")"]+)":/g, '$1:')}) { 93 | _id 94 | content 95 | } 96 | }` 97 | 98 | return request(app.getHttpServer()) 99 | .post('/graphql') 100 | .send({ 101 | operationName: null, 102 | query: exchangeTypeDefs, 103 | }) 104 | .expect(({ body }) => { 105 | const testData: MottoModel[] = body.data.exchangePositionMotto 106 | const firstData = testData[0] 107 | expect(firstData.content).toBe(createdData.content) 108 | }) 109 | .expect(200) 110 | }) 111 | 112 | // READ_ALL 113 | it('getMottos', async () => { 114 | const getAllTypeDefs = ` 115 | query GetMottos { 116 | getMottos { 117 | _id 118 | content 119 | } 120 | }` 121 | 122 | return request(app.getHttpServer()) 123 | .post('/graphql') 124 | .send({ 125 | operationName: null, 126 | query: getAllTypeDefs, 127 | }) 128 | .expect(({ body }) => { 129 | const testData: MottoModel[] = body.data.getMottos 130 | 131 | const firstData = testData[0] 132 | 133 | expect(testData.length).toBeGreaterThan(0) 134 | expect(firstData._id).toBe(id) 135 | expect(firstData.content).toBe(createdData.content) 136 | }) 137 | .expect(200) 138 | }) 139 | 140 | // READ_ONE 141 | it('getMottoById', async () => { 142 | const getOneByIdTypeDefs = ` 143 | query GetMottoById { 144 | getMottoById(id: "${id}") { 145 | _id 146 | content 147 | } 148 | }` 149 | 150 | return request(app.getHttpServer()) 151 | .post('/graphql') 152 | .send({ 153 | operationName: null, 154 | query: getOneByIdTypeDefs, 155 | }) 156 | .expect(({ body }) => { 157 | const testData: MottoModel = body.data.getMottoById 158 | 159 | expect(testData._id).toBe(id) 160 | expect(testData.content).toBe(createdData.content) 161 | }) 162 | .expect(200) 163 | }) 164 | 165 | // UPDATE_ONE 166 | it('updateMottoById', async () => { 167 | const updateDataString = JSON.stringify({ ...updatedData, id }).replace(/"([^(")"]+)":/g, '$1:') 168 | 169 | const updateOneByIdTypeDefs = ` 170 | mutation UpdateMottoById { 171 | updateMottoById(input: ${updateDataString}) { 172 | _id 173 | content 174 | } 175 | }` 176 | 177 | return request(app.getHttpServer()) 178 | .post('/graphql') 179 | .send({ 180 | operationName: null, 181 | query: updateOneByIdTypeDefs, 182 | }) 183 | .expect(({ body }) => { 184 | const testData: MottoModel = body.data.updateMottoById 185 | expect(testData.content).toBe(updatedData.content) 186 | }) 187 | .expect(200) 188 | }) 189 | 190 | // DELETE_ONE 191 | it('deleteMottoById', async () => { 192 | const deleteOneByIdTypeDefs = ` 193 | mutation DeleteMottoById { 194 | deleteMottoById(id: "${id}") { 195 | _id 196 | content 197 | } 198 | }` 199 | 200 | return request(app.getHttpServer()) 201 | .post('/graphql') 202 | .send({ 203 | operationName: null, 204 | query: deleteOneByIdTypeDefs, 205 | }) 206 | .expect(({ body }) => { 207 | const testData: MottoModel = body.data.deleteMottoById 208 | 209 | expect(testData.content).toBe(updatedData.content) 210 | }) 211 | .expect(200) 212 | }) 213 | 214 | // BATCH_DELETE 215 | it('deleteMottos', async () => { 216 | const batchDeleteTypeDefs = ` 217 | mutation DeleteMottos { 218 | deleteMottos(ids: ["${id}"]) { 219 | ok 220 | n 221 | deletedCount 222 | } 223 | }` 224 | 225 | return request(app.getHttpServer()) 226 | .post('/graphql') 227 | .send({ 228 | operationName: null, 229 | query: batchDeleteTypeDefs, 230 | }) 231 | .expect(({ body }) => { 232 | const testData: BatchDeleteModel = body.data.deleteMottos 233 | expect(testData.ok).toBe(1) 234 | expect(testData.n).toBe(0) 235 | expect(testData.deletedCount).toBe(0) 236 | }) 237 | .expect(200) 238 | }) 239 | }) 240 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"], 3 | "extends": "./tsconfig.json" 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "commonjs", 5 | "incremental": true, 6 | "removeComments": true, 7 | "sourceMap": true, 8 | "outDir": "./dist", 9 | "baseUrl": "./", 10 | "esModuleInterop": true, 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "paths": { 14 | "apollo-cache-control": ["node_modules/apollo-cache-control"] 15 | } 16 | }, 17 | "include": ["src"], 18 | "exclude": ["node_modules", "dist"] 19 | } 20 | --------------------------------------------------------------------------------