chore: initial commit
This commit is contained in:
		
							
								
								
									
										1
									
								
								.commitlintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.commitlintrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| { "extends": ["@commitlint/config-conventional"] } | ||||
							
								
								
									
										8
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| .vscode | ||||
| .git | ||||
| build | ||||
| coverage | ||||
| node_modules | ||||
| tmp | ||||
| temp | ||||
| **/__test__/** | ||||
							
								
								
									
										11
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| # For more information see: https://editorconfig.org/ | ||||
|  | ||||
| root = true | ||||
|  | ||||
| [*] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| end_of_line = lf | ||||
| charset = utf-8 | ||||
| trim_trailing_whitespace = true | ||||
| insert_final_newline = true | ||||
							
								
								
									
										22
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.env.example
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| COMPOSE_PROJECT_NAME=thream-api | ||||
| PORT=8080 | ||||
| API_BASE_URL=http://localhost:8080 | ||||
| DATABASE_DIALECT=mysql | ||||
| DATABASE_HOST=thream-database | ||||
| DATABASE_NAME=thream | ||||
| DATABASE_USER=root | ||||
| DATABASE_PASSWORD=password | ||||
| DATABASE_PORT=3306 | ||||
| JWT_ACCESS_EXPIRES_IN=15 minutes | ||||
| JWT_ACCESS_SECRET=accessTokenSecret | ||||
| JWT_REFRESH_SECRET=refreshTokenSecret | ||||
| DISCORD_CLIENT_ID= | ||||
| DISCORD_CLIENT_SECRET= | ||||
| GITHUB_CLIENT_ID= | ||||
| GITHUB_CLIENT_SECRET= | ||||
| GOOGLE_CLIENT_ID= | ||||
| GOOGLE_CLIENT_SECRET= | ||||
| EMAIL_HOST=thream-maildev | ||||
| EMAIL_USER=no-reply@thream.fr | ||||
| EMAIL_PASSWORD=password | ||||
| EMAIL_PORT=25 | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/BUG.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| --- | ||||
| name: '🐛 Bug Report' | ||||
| about: 'Report an unexpected problem or unintended behavior.' | ||||
| title: '[Bug]' | ||||
| labels: 'bug' | ||||
| --- | ||||
|  | ||||
| <!-- | ||||
|   Please provide a clear and concise description of what the bug is. Include | ||||
|   screenshots if needed. Please make sure your issue has not already been fixed. | ||||
| --> | ||||
|  | ||||
| ## Steps To Reproduce | ||||
|  | ||||
| 1. Step 1 | ||||
| 2. Step 2 | ||||
|  | ||||
| ## The current behavior | ||||
|  | ||||
| ## The expected behavior | ||||
							
								
								
									
										18
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.github/ISSUE_TEMPLATE/DOCUMENTATION.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| --- | ||||
| name: '📜 Documentation' | ||||
| about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' | ||||
| title: '[Documentation]' | ||||
| labels: 'documentation' | ||||
| --- | ||||
|  | ||||
| <!-- Please make sure your issue has not already been fixed. --> | ||||
|  | ||||
| ## Documentation | ||||
|  | ||||
| <!-- Please uncomment the type of documentation problem this issue address --> | ||||
|  | ||||
| <!-- Documentation is Missing --> | ||||
| <!-- Documentation is Confusing --> | ||||
| <!-- Documentation has Typo errors --> | ||||
|  | ||||
| ## Proposal | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| --- | ||||
| name: '✨ Feature Request' | ||||
| about: 'Suggest a new feature idea.' | ||||
| title: '[Feature]' | ||||
| labels: 'feature request' | ||||
| --- | ||||
|  | ||||
| <!-- Please make sure your issue has not already been fixed. --> | ||||
|  | ||||
| ## Description | ||||
|  | ||||
| <!-- A clear and concise description of the problem or missing capability... --> | ||||
|  | ||||
| ## Describe the solution you'd like | ||||
|  | ||||
| <!-- If you have a solution in mind, please describe it. --> | ||||
|  | ||||
| ## Describe alternatives you've considered | ||||
|  | ||||
| <!-- Have you considered any alternative solutions or workarounds? --> | ||||
							
								
								
									
										20
									
								
								.github/ISSUE_TEMPLATE/IMPROVEMENT.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.github/ISSUE_TEMPLATE/IMPROVEMENT.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| --- | ||||
| name: '🔧 Improvement' | ||||
| about: 'Improve structure/format/performance/refactor/tests of the code.' | ||||
| title: '[Improvement]' | ||||
| labels: 'improvement' | ||||
| --- | ||||
|  | ||||
| <!-- Please make sure your issue has not already been fixed. --> | ||||
|  | ||||
| ## Type of Improvement | ||||
|  | ||||
| <!-- Please uncomment the type of improvements this issue address --> | ||||
|  | ||||
| <!-- Files and Folders Structure --> | ||||
| <!-- Performance --> | ||||
| <!-- Refactoring code --> | ||||
| <!-- Tests --> | ||||
| <!-- Not Sure? --> | ||||
|  | ||||
| ## Proposal | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/QUESTION.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/QUESTION.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| --- | ||||
| name: '🙋 Question' | ||||
| about: 'Further information is requested.' | ||||
| title: '[Question]' | ||||
| labels: 'question' | ||||
| --- | ||||
|  | ||||
| ### Question | ||||
							
								
								
									
										7
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> | ||||
|  | ||||
| ## What changes this PR introduce? | ||||
|  | ||||
| ## List any relevant issue numbers | ||||
|  | ||||
| ## Is there anything you'd like reviewers to focus on? | ||||
							
								
								
									
										27
									
								
								.github/workflows/analyze.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/analyze.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| name: 'Analyze' | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| jobs: | ||||
|   analyze: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|  | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         language: ['javascript'] | ||||
|  | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v2.3.4' | ||||
|  | ||||
|       - name: 'Initialize CodeQL' | ||||
|         uses: 'github/codeql-action/init@v1' | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|  | ||||
|       - name: 'Perform CodeQL Analysis' | ||||
|         uses: 'github/codeql-action/analyze@v1' | ||||
							
								
								
									
										24
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| name: 'Build' | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v2' | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.1.5' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm ci --cache .npm --prefer-offline' | ||||
|  | ||||
|       - name: 'Build' | ||||
|         run: 'npm run build' | ||||
							
								
								
									
										27
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| name: 'Lint' | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| jobs: | ||||
|   lint: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v2' | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.1.5' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm ci --cache .npm --prefer-offline' | ||||
|  | ||||
|       - run: 'npm run lint:commit -- --to "${{ github.sha }}"' | ||||
|       - run: 'npm run lint:editorconfig' | ||||
|       - run: 'npm run lint:markdown' | ||||
|       - run: 'npm run lint:docker' | ||||
|       - run: 'npm run lint:typescript' | ||||
							
								
								
									
										24
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| name: 'Test' | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master, develop] | ||||
|   pull_request: | ||||
|     branches: [master, develop] | ||||
|  | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v2' | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.1.5' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm ci --cache .npm --prefer-offline' | ||||
|  | ||||
|       - name: 'Test' | ||||
|         run: 'npm run test' | ||||
							
								
								
									
										26
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| # dependencies | ||||
| node_modules | ||||
| .npm | ||||
|  | ||||
| # production | ||||
| build | ||||
|  | ||||
| # testing | ||||
| coverage | ||||
|  | ||||
| # envs | ||||
| .env | ||||
| .env.production | ||||
|  | ||||
| # debug | ||||
| npm-debug.log* | ||||
|  | ||||
| # editors | ||||
| .vscode | ||||
| .theia | ||||
| .idea | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| tmp | ||||
| temp | ||||
							
								
								
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| _ | ||||
							
								
								
									
										4
									
								
								.husky/commit-msg
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								.husky/commit-msg
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| #!/bin/sh | ||||
| . "$(dirname "$0")/_/husky.sh" | ||||
|  | ||||
| npm run lint:commit -- --edit | ||||
							
								
								
									
										8
									
								
								.husky/pre-commit
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										8
									
								
								.husky/pre-commit
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| #!/bin/sh | ||||
| . "$(dirname "$0")/_/husky.sh" | ||||
|  | ||||
| npm run lint:docker | ||||
| npm run lint:editorconfig | ||||
| npm run lint:markdown | ||||
| npm run lint:typescript | ||||
| npm run build | ||||
							
								
								
									
										7
									
								
								.markdownlint.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.markdownlint.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| { | ||||
|   "default": true, | ||||
|   "MD013": false, | ||||
|   "MD024": false, | ||||
|   "MD033": false, | ||||
|   "MD041": false | ||||
| } | ||||
							
								
								
									
										123
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								CONTRIBUTING.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| # 💡 Contributing | ||||
|  | ||||
| Thanks a lot for your interest in contributing to **Thream/api**! 🎉 | ||||
|  | ||||
| ## Code of Conduct | ||||
|  | ||||
| **Thream** has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](https://github.com/Thream/Thream/blob/master/.github/CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. | ||||
|  | ||||
| ## Open Development | ||||
|  | ||||
| All work on **Thream/api** happens directly on [GitHub](https://github.com/Thream). Both core team members and external contributors send pull requests which go through the same review process. | ||||
|  | ||||
| ## Types of contributions | ||||
|  | ||||
| - Reporting a bug. | ||||
| - Suggest a new feature idea. | ||||
| - Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...). | ||||
| - Improve structure/format/performance/refactor/tests of the code. | ||||
|  | ||||
| ## Pull Requests | ||||
|  | ||||
| - **Please first discuss** the change you wish to make via [issue](https://github.com/Thream/api/issues) before making a change. It might avoid a waste of your time. | ||||
|  | ||||
| - Ensure your code respect [Typescript Standard Style](https://www.npmjs.com/package/ts-standard). | ||||
|  | ||||
| - Make sure your **code passes the tests**. | ||||
|  | ||||
| If you're adding new features to **Thream/api**, please include tests. | ||||
|  | ||||
| ## Commits | ||||
|  | ||||
| The commit message guidelines respect [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional) and [Semantic Versioning](https://semver.org/) for releases. | ||||
|  | ||||
| ### Types | ||||
|  | ||||
| Types define which kind of changes you made to the project. | ||||
|  | ||||
| | Types    | Description                                                                                                  | | ||||
| | -------- | ------------------------------------------------------------------------------------------------------------ | | ||||
| | feat     | A new feature.                                                                                               | | ||||
| | fix      | A bug fix.                                                                                                   | | ||||
| | docs     | Documentation only changes.                                                                                  | | ||||
| | style    | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc).      | | ||||
| | refactor | A code change that neither fixes a bug nor adds a feature.                                                   | | ||||
| | perf     | A code change that improves performance.                                                                     | | ||||
| | test     | Adding missing tests or correcting existing tests.                                                           | | ||||
| | build    | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm).         | | ||||
| | ci       | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). | | ||||
| | chore    | Other changes that don't modify src or test files.                                                           | | ||||
| | revert   | Reverts a previous commit.                                                                                   | | ||||
|  | ||||
| ### Scopes | ||||
|  | ||||
| Scopes define what part of the code changed. | ||||
|  | ||||
| ### Examples | ||||
|  | ||||
| ```sh | ||||
| git commit -m "feat(users): add POST /users/signup" | ||||
| git commit -m "docs(readme): update installation process" | ||||
| git commit -m "fix(messages): should emit events to connected users" | ||||
| ``` | ||||
|  | ||||
| ## Directory Structure | ||||
|  | ||||
| ```text | ||||
| ├── email | ||||
| ├── public | ||||
| ├── scripts | ||||
| └── src | ||||
|     ├── models | ||||
|     ├── services | ||||
|     ├── tools | ||||
|     └── typings | ||||
| ``` | ||||
|  | ||||
| ### Each folder explained | ||||
|  | ||||
| - `email` : email template(s) and translation(s) | ||||
| - `src` : all source files | ||||
|   - `models` : models that represent tables in database (there is a `_data.sql` file to have dummy data to work with in development mode) | ||||
|   - `services` : all REST API endpoints | ||||
|   - `tools` : configs and utilities | ||||
|   - `typings` : types gloablly used in the project | ||||
| - `uploads` : uploaded files by users | ||||
|  | ||||
| ### Services folder explained with an example | ||||
|  | ||||
| We have API REST services for the `channels`. | ||||
|  | ||||
| Here is what potentially look like a folder structure for this service : | ||||
|  | ||||
| ```text | ||||
| └── src | ||||
|     └── services | ||||
|         └── channels | ||||
|             ├── __docs__ | ||||
|             │   └── get.yaml | ||||
|             ├── __test__ | ||||
|             │   └── get.test.ts | ||||
|             ├── [channelId] | ||||
|             │   ├── __docs__ | ||||
|             │   │   ├── delete.yaml | ||||
|             │   │   └── put.yaml | ||||
|             │   ├── __test__ | ||||
|             │   │   ├── delete.test.ts | ||||
|             │   │   └── put.test.ts | ||||
|             │   ├── delete.ts | ||||
|             │   ├── index.ts | ||||
|             │   └── put.ts | ||||
|             ├── get.ts | ||||
|             └── index.ts | ||||
| ``` | ||||
|  | ||||
| This folder structure will map to these REST API routes : | ||||
|  | ||||
| - GET `/channels` | ||||
| - DELETE `/channels/:channelId` | ||||
| - PUT `/channels/:channelId` | ||||
|  | ||||
| The folders after `src/services` : is the real path of the routes in the API except folders starting and ending with `__` like `__docs__`, `__test__` or `__utils__`. | ||||
|  | ||||
| The filenames correspond to the HTTP methods used (`get`, `post`, `put`, `delete`). | ||||
							
								
								
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| FROM node:14.16.1 | ||||
| RUN npm install --global npm@7 | ||||
|  | ||||
| WORKDIR /api | ||||
|  | ||||
| COPY ./package*.json ./ | ||||
| RUN npm install | ||||
| COPY ./ ./ | ||||
| RUN npm run build | ||||
|  | ||||
| CMD ["npm", "run", "dev"] | ||||
							
								
								
									
										27
									
								
								Dockerfile.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								Dockerfile.production
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| ARG NODE_VERSION=14.16.1 | ||||
|  | ||||
| FROM node:${NODE_VERSION} AS dependencies | ||||
| RUN npm install --global npm@7 | ||||
| WORKDIR /api | ||||
| COPY ./package*.json ./ | ||||
| RUN npm clean-install | ||||
|  | ||||
| FROM node:${NODE_VERSION} AS builder | ||||
| WORKDIR /api | ||||
| COPY ./ ./ | ||||
| COPY --from=dependencies /api/node_modules ./node_modules | ||||
| RUN npm run build | ||||
|  | ||||
| FROM node:${NODE_VERSION} AS runner | ||||
| WORKDIR /api | ||||
| ENV NODE_ENV=production | ||||
|  | ||||
| COPY --from=builder /api/node_modules ./node_modules | ||||
| COPY --from=builder /api/build ./build | ||||
| COPY --from=builder /api/email ./email | ||||
| COPY --from=builder /api/uploads ./uploads | ||||
|  | ||||
| RUN chown --recursive node /api/build | ||||
| USER node | ||||
|  | ||||
| CMD ["node", "build/index.js"] | ||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) Thream | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										86
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| <h1 align="center"><a href="https://api.thream.divlo.fr/docs">Thream/api</a></h1> | ||||
|  | ||||
| <p align="center"> | ||||
|   <strong>Thream's application programming interface to stay close with your friends and communities.</strong> | ||||
| </p> | ||||
|  | ||||
| <p align="center"> | ||||
|   <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a> | ||||
|   <a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a> | ||||
|   <a href="./CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a> | ||||
|   <br /> | ||||
|   <a href="https://github.com/Thream/api/actions/workflows/analyze.yml"><img src="https://github.com/Thream/api/actions/workflows/analyze.yml/badge.svg?branch=develop" /></a> | ||||
|   <a href="https://github.com/Thream/api/actions/workflows/build.yml"><img src="https://github.com/Thream/api/actions/workflows/build.yml/badge.svg?branch=develop" /></a> | ||||
|   <a href="https://github.com/Thream/api/actions/workflows/lint.yml"><img src="https://github.com/Thream/api/actions/workflows/lint.yml/badge.svg?branch=develop" /></a> | ||||
|   <a href="https://github.com/Thream/api/actions/workflows/test.yml"><img src="https://github.com/Thream/api/actions/workflows/test.yml/badge.svg?branch=develop" /></a> | ||||
|   <br /> | ||||
|   <a href="https://www.npmjs.com/package/ts-standard"><img alt="TypeScript Standard Style" src="https://camo.githubusercontent.com/f87caadb70f384c0361ec72ccf07714ef69a5c0a/68747470733a2f2f62616467656e2e6e65742f62616467652f636f64652532307374796c652f74732d7374616e646172642f626c75653f69636f6e3d74797065736372697074"/></a> | ||||
|   <a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a> | ||||
|   <a href="https://dependabot.com/"><img src="https://badgen.net/github/dependabot/Thream/api?icon=dependabot" alt="Dependabot badge" /></a> | ||||
| </p> | ||||
|  | ||||
| ## 📜 About | ||||
|  | ||||
| Thream's application programming interface to stay close with your friends and communities. | ||||
|  | ||||
| This project was bootstrapped with [create-fullstack-app](https://github.com/Divlo/create-fullstack-app). | ||||
|  | ||||
| ## ⚙️ Getting Started | ||||
|  | ||||
| ### Prerequisites | ||||
|  | ||||
| - [Node.js](https://nodejs.org/) >= 14 | ||||
| - [npm](https://www.npmjs.com/) >= 6 | ||||
| - [MySQL](https://www.mysql.com/) >= 8 | ||||
|  | ||||
| ### Installation | ||||
|  | ||||
| ```sh | ||||
| # Clone the repository | ||||
| git clone https://github.com/Thream/api.git | ||||
|  | ||||
| # Go to the project root | ||||
| cd api | ||||
|  | ||||
| # Configure environment variables | ||||
| cp .env.example .env | ||||
|  | ||||
| # Install dependencies | ||||
| npm install | ||||
| ``` | ||||
|  | ||||
| You will need to configure the environment variables by creating an `.env` file at the root of the project (see `.env.example`). | ||||
|  | ||||
| ### Development environment with [Docker](https://www.docker.com/) | ||||
|  | ||||
| ```sh | ||||
| # Setup and run all the services for you | ||||
| docker-compose up | ||||
| ``` | ||||
|  | ||||
| ### Production environment with [Docker](https://www.docker.com/) | ||||
|  | ||||
| ```sh | ||||
| # Setup and run all the services for you | ||||
| docker-compose --file=docker-compose.production.yml up | ||||
| ``` | ||||
|  | ||||
| #### Services started | ||||
|  | ||||
| - API : `http://localhost:8080` | ||||
| - [MySQL database](https://www.mysql.com/) | ||||
|  | ||||
| #### Services started only in Development environment | ||||
|  | ||||
| - [phpmyadmin](https://www.phpmyadmin.net/) : `http://localhost:8000` | ||||
| - [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080` | ||||
|  | ||||
| ## 💡 Contributing | ||||
|  | ||||
| Anyone can help to improve the project, submit a Feature Request, a bug report or even correct a simple spelling mistake. | ||||
|  | ||||
| The steps to contribute can be found in the [CONTRIBUTING.md](./CONTRIBUTING.md) file. | ||||
|  | ||||
| ## 📄 License | ||||
|  | ||||
| [MIT](./LICENSE) | ||||
							
								
								
									
										34
									
								
								docker-compose.production.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								docker-compose.production.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| version: '3.0' | ||||
| services: | ||||
|   thream-api: | ||||
|     container_name: ${COMPOSE_PROJECT_NAME} | ||||
|     build: | ||||
|       context: './' | ||||
|       dockerfile: './Dockerfile.production' | ||||
|     environment: | ||||
|       PORT: ${PORT} | ||||
|     env_file: './.env' | ||||
|     ports: | ||||
|       - '${PORT}:${PORT}' | ||||
|     depends_on: | ||||
|       - ${DATABASE_HOST} | ||||
|     volumes: | ||||
|       - './uploads:/api/uploads' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
|   thream-database: | ||||
|     container_name: ${DATABASE_HOST} | ||||
|     image: 'mysql:8.0.23' | ||||
|     command: '--default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci' | ||||
|     environment: | ||||
|       MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD} | ||||
|       MYSQL_DATABASE: ${DATABASE_NAME} | ||||
|       MYSQL_TCP_PORT: ${DATABASE_PORT} | ||||
|     ports: | ||||
|       - '${DATABASE_PORT}:${DATABASE_PORT}' | ||||
|     volumes: | ||||
|       - 'database-volume:/var/lib/mysql' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
| volumes: | ||||
|   database-volume: | ||||
							
								
								
									
										52
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| version: '3.0' | ||||
| services: | ||||
|   thream-api: | ||||
|     container_name: ${COMPOSE_PROJECT_NAME} | ||||
|     build: | ||||
|       context: './' | ||||
|     ports: | ||||
|       - '${PORT}:${PORT}' | ||||
|     depends_on: | ||||
|       - ${DATABASE_HOST} | ||||
|       - 'thream-maildev' | ||||
|     volumes: | ||||
|       - './:/api' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
|   thream-phpmyadmin: | ||||
|     container_name: 'thream-phpmyadmin' | ||||
|     image: 'phpmyadmin/phpmyadmin:5.0.4' | ||||
|     environment: | ||||
|       PMA_HOST: ${DATABASE_HOST} | ||||
|       PMA_PORT: ${DATABASE_PORT} | ||||
|       PMA_USER: ${DATABASE_USER} | ||||
|       PMA_PASSWORD: ${DATABASE_PASSWORD} | ||||
|     ports: | ||||
|       - '8000:80' | ||||
|     depends_on: | ||||
|       - ${DATABASE_HOST} | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
|   thream-database: | ||||
|     container_name: ${DATABASE_HOST} | ||||
|     image: 'mysql:8.0.23' | ||||
|     command: '--default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci' | ||||
|     environment: | ||||
|       MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD} | ||||
|       MYSQL_DATABASE: ${DATABASE_NAME} | ||||
|       MYSQL_TCP_PORT: ${DATABASE_PORT} | ||||
|     ports: | ||||
|       - '${DATABASE_PORT}:${DATABASE_PORT}' | ||||
|     volumes: | ||||
|       - 'database-volume:/var/lib/mysql' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
|   thream-maildev: | ||||
|     container_name: 'thream-maildev' | ||||
|     image: 'maildev/maildev:1.1.0' | ||||
|     ports: | ||||
|       - '1080:80' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
| volumes: | ||||
|   database-volume: | ||||
							
								
								
									
										148
									
								
								email/email-template.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								email/email-template.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,148 @@ | ||||
| <center> | ||||
|   <table | ||||
|     border="0" | ||||
|     cellpadding="20" | ||||
|     cellspacing="0" | ||||
|     height="100%" | ||||
|     width="100%" | ||||
|     style="background-color: <%= theme.backgroundPrimary %>" | ||||
|   > | ||||
|     <tbody> | ||||
|       <tr> | ||||
|         <td align="center" valign="top"> | ||||
|           <table | ||||
|             border="0" | ||||
|             cellpadding="0" | ||||
|             cellspacing="0" | ||||
|             width="100%" | ||||
|             style="max-width: 600px; border-radius: 6px" | ||||
|           > | ||||
|             <tbody> | ||||
|               <tr> | ||||
|                 <td align="center" valign="top"> | ||||
|                   <table | ||||
|                     border="0" | ||||
|                     cellpadding="0" | ||||
|                     cellspacing="0" | ||||
|                     width="100%" | ||||
|                     style="max-width: 600px" | ||||
|                   > | ||||
|                     <tbody> | ||||
|                       <tr> | ||||
|                         <td> | ||||
|                           <h1 | ||||
|                             style=" | ||||
|                               font-family: Poppins, Arial, Helvetica, sans-serif; | ||||
|                               color: <%= theme.colorSecondary %>; | ||||
|                               font-size: 28px; | ||||
|                               line-height: 110%; | ||||
|                               margin-bottom: 30px; | ||||
|                               margin-top: 0; | ||||
|                               padding: 0; | ||||
|                             " | ||||
|                           > | ||||
|                             Thream | ||||
|                           </h1> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     </tbody> | ||||
|                   </table> | ||||
|                 </td> | ||||
|               </tr> | ||||
|               <tr> | ||||
|                 <td align="center" valign="top"> | ||||
|                   <table | ||||
|                     border="0" | ||||
|                     cellpadding="0" | ||||
|                     cellspacing="0" | ||||
|                     width="100%" | ||||
|                     style="max-width: 600px; border-radius: 6px" | ||||
|                   > | ||||
|                     <tbody> | ||||
|                       <tr> | ||||
|                         <td | ||||
|                           align="left" | ||||
|                           valign="top" | ||||
|                           style=" | ||||
|                             line-height: 150%; | ||||
|                             font-family: Helvetica; | ||||
|                             font-size: 14px; | ||||
|                             color: rgb(222, 222, 222); | ||||
|                             padding: 30px; | ||||
|                             box-shadow: 0px 0px 6px 6px rgba(0, 0, 0, 0.25); | ||||
|                             border: 1px solid black; | ||||
|                             border-radius: 1rem; | ||||
|                           " | ||||
|                         > | ||||
|                           <h2 | ||||
|                             style=" | ||||
|                               font-size: 22px; | ||||
|                               line-height: 28px; | ||||
|                               margin: 0 0 12px 0; | ||||
|                               color: <%= theme.colorSecondary %>; | ||||
|                             " | ||||
|                           > | ||||
|                             <%= text.subtitle %> | ||||
|                           </h2> | ||||
|                           <a | ||||
|                             href="<%= text.url %>" | ||||
|                             style=" | ||||
|                               display: inline-block; | ||||
|                               font-weight: 500; | ||||
|                               font-size: 16px; | ||||
|                               line-height: 42px; | ||||
|                               font-family: 'Helvetica', Arial, sans-serif; | ||||
|                               width: auto; | ||||
|                               white-space: nowrap; | ||||
|                               height: 42px; | ||||
|                               margin: 12px 5px 12px 0; | ||||
|                               padding: 0 22px; | ||||
|                               text-decoration: none; | ||||
|                               text-align: center; | ||||
|                               border: 0; | ||||
|                               border-radius: 3px; | ||||
|                               vertical-align: top; | ||||
|                               background-color: <%= theme.backgroundPrimary %>; | ||||
|                               border: 1px solid <%= theme.colorPrimary %>; | ||||
|                             " | ||||
|                             target="_blank" | ||||
|                             rel="noopener noreferrer" | ||||
|                             ><span | ||||
|                               style=" | ||||
|                                 display: inline; | ||||
|                                 font-family: 'Helvetica', Arial, sans-serif; | ||||
|                                 text-decoration: none; | ||||
|                                 font-weight: 500; | ||||
|                                 font-style: normal; | ||||
|                                 font-size: 16px; | ||||
|                                 line-height: 42px; | ||||
|                                 border: none; | ||||
|                                 color: <%= theme.colorPrimary %>; | ||||
|                               " | ||||
|                               ><%= text.button %></span | ||||
|                             ></a | ||||
|                           > | ||||
|                           <br /> | ||||
|                           <div> | ||||
|                             <p | ||||
|                               style=" | ||||
|                                 padding: 0 0 10px 0; | ||||
|                                 color: <%= theme.colorSecondary %>; | ||||
|                               " | ||||
|                             > | ||||
|                               <%= text.footer %> | ||||
|                             </p> | ||||
|                           </div> | ||||
|                         </td> | ||||
|                       </tr> | ||||
|                     </tbody> | ||||
|                   </table> | ||||
|                 </td> | ||||
|               </tr> | ||||
|             </tbody> | ||||
|           </table> | ||||
|         </td> | ||||
|       </tr> | ||||
|     </tbody> | ||||
|   </table> | ||||
| </center> | ||||
							
								
								
									
										8
									
								
								email/locales/en/confirm-email.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								email/locales/en/confirm-email.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| { | ||||
|   "subject": "Confirm email", | ||||
|   "renderOptions": { | ||||
|     "subtitle": "Please confirm your email to signin", | ||||
|     "button": "Yes, I confirm", | ||||
|     "footer": "If you received this message by mistake, just delete it. Your email will not be confirmed if you do not click on the confirmation link above." | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								email/locales/en/reset-password.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								email/locales/en/reset-password.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| { | ||||
|   "subject": "Reset password", | ||||
|   "renderOptions": { | ||||
|     "subtitle": "Please confirm password reset", | ||||
|     "button": "Yes, I change my password", | ||||
|     "footer": "If you received this message by mistake, just delete it. Your password will not be reset if you do not click on the link above. Also, for the security of your account, the password reset is available for a period of 1 hour, after this time, the reset will no longer be possible." | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								email/locales/fr/confirm-email.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								email/locales/fr/confirm-email.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| { | ||||
|   "subject": "Confirmez votre email", | ||||
|   "renderOptions": { | ||||
|     "subtitle": "Veuillez confirmer votre adresse email pour vous connecter", | ||||
|     "button": "Oui, je confirme", | ||||
|     "footer": "Si vous avez reçu ce message par erreur, supprimez-le simplement. Votre email ne sera pas confirmé si vous ne cliquez pas sur le lien de confirmation ci-dessus." | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								email/locales/fr/reset-password.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								email/locales/fr/reset-password.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| { | ||||
|   "subject": "Réinitialiser le mot de passe", | ||||
|   "renderOptions": { | ||||
|     "subtitle": "Veuillez confirmer la réinitialisation du mot de passe", | ||||
|     "button": "Oui, je change mon mot de passe", | ||||
|     "footer": "Si vous avez reçu ce message par erreur, supprimez-le simplement. Votre mot de passe ne sera pas réinitialisé si vous ne cliquez pas sur le lien ci-dessus. Aussi, pour la sécurité de votre compte, la réinitialisation du mot de passe est disponible pour une durée de 1 heure, après ce temps, la réinitialisation ne sera plus possible." | ||||
|   } | ||||
| } | ||||
							
								
								
									
										29649
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										29649
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										146
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| { | ||||
|   "name": "@thream/api", | ||||
|   "version": "0.0.0-development", | ||||
|   "private": true, | ||||
|   "release-it": { | ||||
|     "git": { | ||||
|       "commit": false, | ||||
|       "push": false, | ||||
|       "tag": false | ||||
|     }, | ||||
|     "gitlab": { | ||||
|       "release": false | ||||
|     }, | ||||
|     "npm": { | ||||
|       "publish": false | ||||
|     }, | ||||
|     "hooks": { | ||||
|       "before:init": [ | ||||
|         "npm run lint:docker", | ||||
|         "npm run lint:editorconfig", | ||||
|         "npm run lint:markdown", | ||||
|         "npm run lint:typescript", | ||||
|         "npm run build", | ||||
|         "npm run test" | ||||
|       ] | ||||
|     }, | ||||
|     "plugins": { | ||||
|       "@release-it/conventional-changelog": { | ||||
|         "preset": "angular", | ||||
|         "infile": "CHANGELOG.md" | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   "jest": { | ||||
|     "preset": "ts-jest", | ||||
|     "testEnvironment": "node", | ||||
|     "setupFiles": [ | ||||
|       "./__test__/setEnvsVars.ts" | ||||
|     ], | ||||
|     "setupFilesAfterEnv": [ | ||||
|       "./__test__/setup.ts" | ||||
|     ], | ||||
|     "rootDir": "./src", | ||||
|     "collectCoverage": true, | ||||
|     "coverageDirectory": "../coverage/", | ||||
|     "coverageReporters": [ | ||||
|       "text", | ||||
|       "cobertura" | ||||
|     ] | ||||
|   }, | ||||
|   "ts-standard": { | ||||
|     "ignore": [ | ||||
|       "build", | ||||
|       "coverage", | ||||
|       "node_modules", | ||||
|       "uploads" | ||||
|     ], | ||||
|     "envs": [ | ||||
|       "node", | ||||
|       "jest" | ||||
|     ], | ||||
|     "report": "stylish" | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build": "rimraf ./build && tsc", | ||||
|     "start": "cross-env NODE_ENV=production node build/index.js", | ||||
|     "dev": "concurrently --kill-others --names \"TypeScript,Node\" --prefix \"[{name}]\" --prefix-colors \"blue,green\" \"tsc --watch\" \"cross-env NODE_ENV=development nodemon -e js,json,yaml build/index.js\"", | ||||
|     "lint:commit": "commitlint", | ||||
|     "lint:docker": "dockerfilelint './Dockerfile' && dockerfilelint './Dockerfile.production'", | ||||
|     "lint:editorconfig": "editorconfig-checker", | ||||
|     "lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules", | ||||
|     "lint:typescript": "ts-standard", | ||||
|     "release": "release-it", | ||||
|     "test": "jest", | ||||
|     "postinstall": "husky install" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@thream/socketio-jwt": "2.1.0", | ||||
|     "axios": "0.21.1", | ||||
|     "bcryptjs": "2.4.3", | ||||
|     "cors": "2.8.5", | ||||
|     "dotenv": "8.2.0", | ||||
|     "ejs": "3.1.6", | ||||
|     "express": "4.17.1", | ||||
|     "express-async-errors": "3.1.1", | ||||
|     "express-fileupload": "1.2.1", | ||||
|     "express-rate-limit": "5.2.6", | ||||
|     "express-validator": "6.10.0", | ||||
|     "helmet": "4.5.0", | ||||
|     "jsonwebtoken": "8.5.1", | ||||
|     "morgan": "1.10.0", | ||||
|     "ms": "2.1.3", | ||||
|     "mysql2": "2.2.5", | ||||
|     "nodemailer": "6.5.0", | ||||
|     "reflect-metadata": "0.1.13", | ||||
|     "sequelize": "6.6.2", | ||||
|     "sequelize-typescript": "2.1.0", | ||||
|     "socket.io": "4.0.1", | ||||
|     "swagger-jsdoc": "6.1.0", | ||||
|     "swagger-ui-express": "4.1.6", | ||||
|     "uuid": "8.3.2" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@commitlint/cli": "12.1.1", | ||||
|     "@commitlint/config-conventional": "12.1.1", | ||||
|     "@release-it/conventional-changelog": "2.0.1", | ||||
|     "@types/bcryptjs": "2.4.2", | ||||
|     "@types/cors": "2.8.10", | ||||
|     "@types/ejs": "3.0.6", | ||||
|     "@types/express": "4.17.11", | ||||
|     "@types/express-fileupload": "1.1.6", | ||||
|     "@types/express-rate-limit": "5.1.1", | ||||
|     "@types/jest": "26.0.22", | ||||
|     "@types/jsonwebtoken": "8.5.1", | ||||
|     "@types/mock-fs": "4.13.0", | ||||
|     "@types/morgan": "1.9.2", | ||||
|     "@types/ms": "0.7.31", | ||||
|     "@types/node": "14.14.41", | ||||
|     "@types/nodemailer": "6.4.1", | ||||
|     "@types/server-destroy": "1.0.1", | ||||
|     "@types/supertest": "2.0.11", | ||||
|     "@types/swagger-jsdoc": "6.0.0", | ||||
|     "@types/swagger-ui-express": "4.1.2", | ||||
|     "@types/uuid": "8.3.0", | ||||
|     "@types/validator": "13.1.3", | ||||
|     "concurrently": "6.0.2", | ||||
|     "cross-env": "7.0.3", | ||||
|     "dockerfilelint": "1.8.0", | ||||
|     "editorconfig-checker": "4.0.2", | ||||
|     "husky": "6.0.0", | ||||
|     "jest": "26.6.3", | ||||
|     "markdownlint-cli": "0.27.1", | ||||
|     "mock-fs": "4.13.0", | ||||
|     "nodemon": "2.0.7", | ||||
|     "release-it": "14.6.1", | ||||
|     "rimraf": "3.0.2", | ||||
|     "server-destroy": "1.0.1", | ||||
|     "socket.io-client": "4.0.1", | ||||
|     "sqlite": "4.0.21", | ||||
|     "sqlite3": "5.0.2", | ||||
|     "supertest": "6.1.3", | ||||
|     "ts-jest": "26.5.5", | ||||
|     "ts-standard": "10.0.0", | ||||
|     "typescript": "4.2.4" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/__test__/application.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/__test__/application.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../application' | ||||
| import { usersLogoPath } from '../tools/configurations/constants' | ||||
|  | ||||
| describe('application', () => { | ||||
|   it("returns a 404 on route that doesn't exist", async () => { | ||||
|     return await request(application).post('/404routenotfound').send().expect(404) | ||||
|   }) | ||||
|  | ||||
|   it('returns a 200 success code for users images', async () => { | ||||
|     return await request(application) | ||||
|       .get(`${usersLogoPath.name}/default.png`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										4
									
								
								src/__test__/setEnvsVars.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/__test__/setEnvsVars.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| process.env.DATABASE_DIALECT = 'sqlite' | ||||
| process.env.JWT_ACCESS_EXPIRES_IN = '15 minutes' | ||||
| process.env.JWT_ACCESS_SECRET = 'accessTokenSecret' | ||||
| process.env.JWT_REFRESH_SECRET = 'refreshTokenSecret' | ||||
							
								
								
									
										42
									
								
								src/__test__/setup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/__test__/setup.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import fsMock from 'mock-fs' | ||||
| import path from 'path' | ||||
| import { Sequelize } from 'sequelize-typescript' | ||||
| import { Database, open } from 'sqlite' | ||||
| import sqlite3 from 'sqlite3' | ||||
|  | ||||
| let sqlite: Database | undefined | ||||
| let sequelize: Sequelize | undefined | ||||
|  | ||||
| jest.mock('nodemailer', () => ({ | ||||
|   createTransport: () => { | ||||
|     return { | ||||
|       sendMail: jest.fn(async () => {}) | ||||
|     } | ||||
|   } | ||||
| })) | ||||
|  | ||||
| beforeAll(async () => { | ||||
|   sqlite = await open({ | ||||
|     filename: ':memory:', | ||||
|     driver: sqlite3.Database | ||||
|   }) | ||||
|   sequelize = new Sequelize({ | ||||
|     dialect: process.env.DATABASE_DIALECT, | ||||
|     storage: process.env.DATABASE_DIALECT === 'sqlite' ? ':memory:' : undefined, | ||||
|     logging: false, | ||||
|     models: [path.join(__dirname, '..', 'models')] | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| beforeEach(async () => { | ||||
|   await sequelize?.sync({ force: true }) | ||||
| }) | ||||
|  | ||||
| afterEach(async () => { | ||||
|   fsMock.restore() | ||||
| }) | ||||
|  | ||||
| afterAll(async () => { | ||||
|   await sqlite?.close() | ||||
|   await sequelize?.close() | ||||
| }) | ||||
							
								
								
									
										10
									
								
								src/__test__/utils/__test__/formatErrors.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/__test__/utils/__test__/formatErrors.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import { formatErrors } from '../formatErrors' | ||||
|  | ||||
| test('__test__/utils/formatErrors', () => { | ||||
|   expect(formatErrors('randomSring')).toEqual([]) | ||||
|   const errors = [ | ||||
|     { message: 'some error message' }, | ||||
|     { message: 'another error' } | ||||
|   ] | ||||
|   expect(formatErrors(errors)).toEqual(['some error message', 'another error']) | ||||
| }) | ||||
							
								
								
									
										57
									
								
								src/__test__/utils/authenticateUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/__test__/utils/authenticateUser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../../application' | ||||
| import User from '../../models/User' | ||||
|  | ||||
| interface AuthenticateUserOptions { | ||||
|   name?: string | ||||
|   email?: string | ||||
|   password?: string | ||||
|   shouldBeConfirmed?: boolean | ||||
|   alreadySignedUp?: boolean | ||||
| } | ||||
|  | ||||
| export async function authenticateUserTest ( | ||||
|   options: AuthenticateUserOptions = {} | ||||
| ): Promise<{ | ||||
|     accessToken: string | ||||
|     refreshToken: string | ||||
|     expiresIn: string | ||||
|     type: 'Bearer' | ||||
|     userId: number | ||||
|   }> { | ||||
|   const { | ||||
|     name = 'John', | ||||
|     email = 'contact@test.com', | ||||
|     shouldBeConfirmed = true, | ||||
|     password = 'test', | ||||
|     alreadySignedUp = false | ||||
|   } = options | ||||
|  | ||||
|   if (!alreadySignedUp) { | ||||
|     const { body: signupBody } = await request(application) | ||||
|       .post('/users/signup') | ||||
|       .send({ name, email, password }) | ||||
|       .expect(201) | ||||
|     let signinResponse: any = { body: {} } | ||||
|     if (shouldBeConfirmed) { | ||||
|       const user = await User.findOne({ where: { id: signupBody.user.id } }) | ||||
|       await request(application) | ||||
|         .get(`/users/confirmEmail?tempToken=${user?.tempToken as string}`) | ||||
|         .send() | ||||
|         .expect(200) | ||||
|       signinResponse = await request(application) | ||||
|         .post('/users/signin') | ||||
|         .send({ email, password }) | ||||
|         .expect(200) | ||||
|     } | ||||
|  | ||||
|     return { ...signinResponse.body, userId: signupBody.user.id } | ||||
|   } | ||||
|   const signinResponse = await request(application) | ||||
|     .post('/users/signin') | ||||
|     .send({ email, password }) | ||||
|     .expect(200) | ||||
|   const user = await User.findOne({ where: { email } }) | ||||
|   return { ...signinResponse.body, userId: user?.id } | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/__test__/utils/formatErrors.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/__test__/utils/formatErrors.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| /** formatErrors for testing purpose (no types safety) */ | ||||
| export const formatErrors = (errors: any): string[] => { | ||||
|   try { | ||||
|     return errors.map((e: any) => e.message) | ||||
|   } catch { | ||||
|     return [] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/__test__/utils/wait.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/__test__/utils/wait.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export const wait = async (ms: number): Promise<void> => { | ||||
|   return await new Promise((resolve) => { | ||||
|     setTimeout(resolve, ms) | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/application.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/application.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import 'express-async-errors' | ||||
|  | ||||
| import cors from 'cors' | ||||
| import dotenv from 'dotenv' | ||||
| import express, { Request } from 'express' | ||||
| import rateLimit from 'express-rate-limit' | ||||
| import helmet from 'helmet' | ||||
| import morgan from 'morgan' | ||||
|  | ||||
| import { errorHandler } from './tools/middlewares/errorHandler' | ||||
| import { router } from './services' | ||||
| import { NotFoundError } from './tools/errors/NotFoundError' | ||||
| import { TooManyRequestsError } from './tools/errors/TooManyRequestsError' | ||||
|  | ||||
| const application = express() | ||||
| dotenv.config() | ||||
|  | ||||
| if (process.env.NODE_ENV === 'development') { | ||||
|   application.use(morgan<Request>('dev')) | ||||
| } else if (process.env.NODE_ENV === 'production') { | ||||
|   const requestPerSecond = 2 | ||||
|   const seconds = 60 | ||||
|   const windowMs = seconds * 1000 | ||||
|   application.enable('trust proxy') | ||||
|   application.use( | ||||
|     rateLimit({ | ||||
|       windowMs, | ||||
|       max: seconds * requestPerSecond, | ||||
|       handler: () => { | ||||
|         throw new TooManyRequestsError() | ||||
|       } | ||||
|     }) | ||||
|   ) | ||||
| } | ||||
|  | ||||
| application.use(express.json()) | ||||
| application.use(helmet()) | ||||
| application.use(cors<Request>()) | ||||
| application.use(router) | ||||
| application.use(() => { | ||||
|   throw new NotFoundError() | ||||
| }) | ||||
| application.use(errorHandler) | ||||
|  | ||||
| export default application | ||||
							
								
								
									
										22
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import { authorize } from '@thream/socketio-jwt' | ||||
|  | ||||
| import application from './application' | ||||
| import { socket } from './tools/socket' | ||||
| import { sequelize } from './tools/database/sequelize' | ||||
|  | ||||
| const PORT = parseInt(process.env.PORT ?? '8080', 10) | ||||
|  | ||||
| sequelize | ||||
|   .sync() | ||||
|   .then(() => { | ||||
|     const server = application.listen(PORT, () => { | ||||
|       console.log('\x1b[36m%s\x1b[0m', `🚀  Server listening on port ${PORT}.`) | ||||
|     }) | ||||
|     socket.init(server) | ||||
|     socket.io?.use( | ||||
|       authorize({ | ||||
|         secret: process.env.JWT_ACCESS_SECRET | ||||
|       }) | ||||
|     ) | ||||
|   }) | ||||
|   .catch((error) => console.error(error)) | ||||
							
								
								
									
										55
									
								
								src/models/Channel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/models/Channel.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   HasMany, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import Guild from './Guild' | ||||
| import Message from './Message' | ||||
|  | ||||
| export const channelTypes = ['text', 'voice'] as const | ||||
| export type ChannelType = typeof channelTypes[number] | ||||
|  | ||||
| @Table | ||||
| export default class Channel extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   name!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: 'text' | ||||
|   }) | ||||
|   type!: ChannelType | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: '' | ||||
|   }) | ||||
|   description!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BOOLEAN, | ||||
|     allowNull: false, | ||||
|     defaultValue: false | ||||
|   }) | ||||
|   isDefault!: boolean | ||||
|  | ||||
|   @ForeignKey(() => Guild) | ||||
|   @Column | ||||
|   guildId!: number | ||||
|  | ||||
|   @BelongsTo(() => Guild) | ||||
|   guild!: Guild | ||||
|  | ||||
|   @HasMany(() => Message) | ||||
|   messages!: Message[] | ||||
| } | ||||
							
								
								
									
										45
									
								
								src/models/Guild.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/models/Guild.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import { Column, DataType, HasMany, Model, Table } from 'sequelize-typescript' | ||||
| import { guildsIconPath } from '../tools/configurations/constants' | ||||
|  | ||||
| import Channel from './Channel' | ||||
| import Invitation from './Invitation' | ||||
| import Member from './Member' | ||||
|  | ||||
| @Table | ||||
| export default class Guild extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   name!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: '' | ||||
|   }) | ||||
|   description!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: false, | ||||
|     defaultValue: `${guildsIconPath.name}/default.png` | ||||
|   }) | ||||
|   icon!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BOOLEAN, | ||||
|     allowNull: false, | ||||
|     defaultValue: false | ||||
|   }) | ||||
|   isPublic!: boolean | ||||
|  | ||||
|   @HasMany(() => Member, { onDelete: 'CASCADE' }) | ||||
|   members!: Member[] | ||||
|  | ||||
|   @HasMany(() => Invitation, { onDelete: 'CASCADE' }) | ||||
|   invitations!: Invitation[] | ||||
|  | ||||
|   @HasMany(() => Channel) | ||||
|   channels!: Channel[] | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/models/Invitation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/models/Invitation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import Guild from './Guild' | ||||
|  | ||||
| @Table | ||||
| export default class Invitation extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   value!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BIGINT, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   /** expiresIn is how long, in milliseconds, until the invitation expires. Note: 0 = never expires */ | ||||
|   expiresIn!: number | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BOOLEAN, | ||||
|     allowNull: false, | ||||
|     defaultValue: false | ||||
|   }) | ||||
|   isPublic!: boolean | ||||
|  | ||||
|   @ForeignKey(() => Guild) | ||||
|   @Column | ||||
|   guildId!: number | ||||
|  | ||||
|   @BelongsTo(() => Guild, { onDelete: 'CASCADE' }) | ||||
|   guild!: Guild | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/models/Member.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/models/Member.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   HasMany, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import Channel from './Channel' | ||||
| import Guild from './Guild' | ||||
| import Message from './Message' | ||||
| import User from './User' | ||||
|  | ||||
| @Table | ||||
| export default class Member extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.BOOLEAN, | ||||
|     allowNull: false, | ||||
|     defaultValue: false | ||||
|   }) | ||||
|   isOwner!: boolean | ||||
|  | ||||
|   @ForeignKey(() => Channel) | ||||
|   @Column | ||||
|   lastVisitedChannelId!: number | ||||
|  | ||||
|   @BelongsTo(() => Channel) | ||||
|   channel!: Channel | ||||
|  | ||||
|   @ForeignKey(() => User) | ||||
|   @Column | ||||
|   userId!: number | ||||
|  | ||||
|   @BelongsTo(() => User) | ||||
|   user!: User | ||||
|  | ||||
|   @ForeignKey(() => Guild) | ||||
|   @Column | ||||
|   guildId!: number | ||||
|  | ||||
|   @BelongsTo(() => Guild) | ||||
|   guild!: Guild | ||||
|  | ||||
|   @HasMany(() => Message, { onDelete: 'CASCADE' }) | ||||
|   messages!: Message[] | ||||
| } | ||||
							
								
								
									
										51
									
								
								src/models/Message.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/models/Message.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import Channel from './Channel' | ||||
| import Member from './Member' | ||||
|  | ||||
| export const messageTypes = ['text', 'file'] as const | ||||
| export type MessageType = typeof messageTypes[number] | ||||
|  | ||||
| @Table | ||||
| export default class Message extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   value!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: 'text' | ||||
|   }) | ||||
|   type!: MessageType | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: 'text/plain' | ||||
|   }) | ||||
|   mimetype!: string | ||||
|  | ||||
|   @ForeignKey(() => Member) | ||||
|   @Column | ||||
|   memberId!: number | ||||
|  | ||||
|   @BelongsTo(() => Member) | ||||
|   member!: Member | ||||
|  | ||||
|   @ForeignKey(() => Channel) | ||||
|   @Column | ||||
|   channelId!: number | ||||
|  | ||||
|   @BelongsTo(() => Channel) | ||||
|   channel!: Channel | ||||
| } | ||||
							
								
								
									
										38
									
								
								src/models/OAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/models/OAuth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import User from './User' | ||||
|  | ||||
| export const providers = ['google', 'github', 'discord'] as const | ||||
| export const strategies = [...providers, 'local'] as const | ||||
|  | ||||
| export type ProviderOAuth = typeof providers[number] | ||||
| export type AuthenticationStrategy = typeof strategies[number] | ||||
|  | ||||
| @Table | ||||
| export default class OAuth extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   provider!: ProviderOAuth | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   providerId!: string | ||||
|  | ||||
|   @ForeignKey(() => User) | ||||
|   @Column | ||||
|   userId!: number | ||||
|  | ||||
|   @BelongsTo(() => User) | ||||
|   user!: User | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/models/RefreshToken.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/models/RefreshToken.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import User from './User' | ||||
|  | ||||
| @Table | ||||
| export default class RefreshToken extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   token!: string | ||||
|  | ||||
|   @ForeignKey(() => User) | ||||
|   @Column | ||||
|   userId!: number | ||||
|  | ||||
|   @BelongsTo(() => User) | ||||
|   user!: User | ||||
| } | ||||
							
								
								
									
										112
									
								
								src/models/User.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/models/User.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| import { | ||||
|   Column, | ||||
|   DataType, | ||||
|   HasMany, | ||||
|   HasOne, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import Member from './Member' | ||||
| import OAuth, { AuthenticationStrategy } from './OAuth' | ||||
| import RefreshToken from './RefreshToken' | ||||
| import UserSetting from './UserSetting' | ||||
| import { deleteObjectAttributes } from '../tools/utils/deleteObjectAttributes' | ||||
| import { usersLogoPath } from '../tools/configurations/constants' | ||||
|  | ||||
| export const userHiddenAttributes = [ | ||||
|   'password', | ||||
|   'tempToken', | ||||
|   'tempExpirationToken' | ||||
| ] as const | ||||
| export type UserHiddenAttributes = typeof userHiddenAttributes[number] | ||||
| export interface UserToJSON extends Omit<User, UserHiddenAttributes> {} | ||||
|  | ||||
| export interface UserJWT { | ||||
|   id: number | ||||
|   currentStrategy: AuthenticationStrategy | ||||
| } | ||||
|  | ||||
| export interface UserRequest { | ||||
|   current: User | ||||
|   currentStrategy: AuthenticationStrategy | ||||
|   accessToken: string | ||||
| } | ||||
|  | ||||
| @Table | ||||
| export default class User extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   name!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: true | ||||
|   }) | ||||
|   email?: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: true | ||||
|   }) | ||||
|   password?: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: '' | ||||
|   }) | ||||
|   status!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: '' | ||||
|   }) | ||||
|   biography!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: false, | ||||
|     defaultValue: `${usersLogoPath.name}/default.png` | ||||
|   }) | ||||
|   logo!: string | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BOOLEAN, | ||||
|     allowNull: false, | ||||
|     defaultValue: false | ||||
|   }) | ||||
|   isConfirmed!: boolean | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: true | ||||
|   }) | ||||
|   tempToken?: string | null | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BIGINT, | ||||
|     allowNull: true | ||||
|   }) | ||||
|   tempExpirationToken?: number | null | ||||
|  | ||||
|   @HasMany(() => RefreshToken, { onDelete: 'CASCADE' }) | ||||
|   refreshTokens!: RefreshToken[] | ||||
|  | ||||
|   @HasMany(() => OAuth, { onDelete: 'CASCADE' }) | ||||
|   OAuths!: OAuth[] | ||||
|  | ||||
|   @HasMany(() => Member, { onDelete: 'CASCADE' }) | ||||
|   members!: Member[] | ||||
|  | ||||
|   @HasOne(() => UserSetting, { onDelete: 'CASCADE' }) | ||||
|   settings!: UserSetting | ||||
|  | ||||
|   toJSON (): UserToJSON { | ||||
|     const attributes = Object.assign({}, this.get()) | ||||
|     return deleteObjectAttributes(attributes, userHiddenAttributes) as UserToJSON | ||||
|   } | ||||
| } | ||||
							
								
								
									
										65
									
								
								src/models/UserSetting.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/models/UserSetting.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
|  | ||||
| import User from './User' | ||||
| import { deleteObjectAttributes } from '../tools/utils/deleteObjectAttributes' | ||||
|  | ||||
| export const userSettingHiddenAttributes = [ | ||||
|   'createdAt', | ||||
|   'updatedAt', | ||||
|   'userId', | ||||
|   'id' | ||||
| ] as const | ||||
| export type UserSettingHiddenAttributes = typeof userSettingHiddenAttributes[number] | ||||
| export interface UserSettingToJSON | ||||
|   extends Omit<UserSetting, UserSettingHiddenAttributes> {} | ||||
|  | ||||
| export const languages = ['fr', 'en'] as const | ||||
| export type Language = typeof languages[number] | ||||
|  | ||||
| export const themes = ['light', 'dark'] as const | ||||
| export type Theme = typeof themes[number] | ||||
|  | ||||
| @Table | ||||
| export default class UserSetting extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: 'en' | ||||
|   }) | ||||
|   language!: Language | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.STRING, | ||||
|     allowNull: false, | ||||
|     defaultValue: 'dark' | ||||
|   }) | ||||
|   theme!: Theme | ||||
|  | ||||
|   @Column({ | ||||
|     type: DataType.BOOLEAN, | ||||
|     defaultValue: false | ||||
|   }) | ||||
|   isPublicEmail!: boolean | ||||
|  | ||||
|   @ForeignKey(() => User) | ||||
|   @Column | ||||
|   userId?: number | ||||
|  | ||||
|   @BelongsTo(() => User) | ||||
|   user!: User | ||||
|  | ||||
|   toJSON (): UserSettingToJSON { | ||||
|     const attributes = Object.assign({}, this.get()) | ||||
|     return deleteObjectAttributes( | ||||
|       attributes, | ||||
|       userSettingHiddenAttributes | ||||
|     ) as UserSettingToJSON | ||||
|   } | ||||
| } | ||||
							
								
								
									
										105
									
								
								src/models/_data.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/models/_data.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| -- All users have the password `test` | ||||
| INSERT INTO `Users` (`id`, `name`, `email`, `password`, `status`, `biography`, `logo`, `isConfirmed`, `tempToken`, `tempExpirationToken`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 'Divlo', 'contact@divlo.fr', '$2a$12$rdXfja1jtd88bgvKs4Pbl.yBBFJZP5Y0TcmqOCPm8Fy3BmQCnJHG2', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:47:36', '2021-03-04 12:48:30'), | ||||
| (2, 'Divlo2', 'divlogaming@gmail.com', '$2a$12$/aIvPyRbp/WUXN1FHwo0w.pBtT1dNls01L8SClpDXbBccjWD33trm', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:47:53', '2021-03-04 12:48:32'), | ||||
| (3, 'John Doe', 'johndoe@gmail.com', '$2a$12$3Qif9pviwoLLtTAQZqir7u4stLNU6E053EvDeso16aqvuahi7w1se', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:48:24', '2021-03-04 12:48:35'), | ||||
| (4, 'User', 'user@example.com', '$2a$12$SdgnEhy22aNQXwBRNDy/XeUNWLvu/MneA1Xfs2dtNhai.m/gP9xNi', '', '', '/uploads/users/default.png', 1, NULL, NULL, '2021-03-04 12:49:58', '2021-03-04 12:50:04'); | ||||
|  | ||||
| INSERT INTO `UserSettings` (`id`, `language`, `theme`, `isPublicEmail`, `userId`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 'en', 'dark', 0, 1, '2021-03-04 12:47:36', '2021-03-04 12:47:36'), | ||||
| (2, 'fr', 'dark', 0, 2, '2021-03-04 12:47:53', '2021-03-04 12:47:53'), | ||||
| (3, 'en', 'light', 0, 3, '2021-03-04 12:48:24', '2021-03-04 12:48:24'), | ||||
| (4, 'fr', 'light', 0, 4, '2021-03-04 12:49:58', '2021-03-04 12:49:58'); | ||||
|  | ||||
| INSERT INTO `Guilds` (`id`, `name`, `description`, `icon`, `isPublic`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 'Ligue.dev', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:51:27', '2021-03-04 12:51:27'), | ||||
| (2, 'Docstring', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:51:39', '2021-03-04 12:51:39'), | ||||
| (3, 'Read The Docs', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:51:50', '2021-03-04 12:51:50'), | ||||
| (4, 'Les Joies du Code', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:09', '2021-03-04 12:52:09'), | ||||
| (5, 'Firecamp', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:19', '2021-03-04 12:52:19'), | ||||
| (6, 'CodinGame', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:30', '2021-03-04 12:52:30'), | ||||
| (7, 'Leon AI', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:38', '2021-03-04 12:52:38'), | ||||
| (8, 'Academind', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:45', '2021-03-04 12:52:45'), | ||||
| (9, 'StandardJS', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:52:57', '2021-03-04 12:52:57'), | ||||
| (10, 'Next.js', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:53:08', '2021-03-04 12:53:08'), | ||||
| (11, 'Tailwind CSS', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:54:58', '2021-03-04 12:54:58'), | ||||
| (12, 'Vue Land', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:04', '2021-03-04 12:55:04'), | ||||
| (13, 'Nuxt.js', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:11', '2021-03-04 12:55:11'), | ||||
| (14, 'Reactiflux', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:16', '2021-03-04 12:55:16'), | ||||
| (15, 'Deno', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:25', '2021-03-04 12:55:25'), | ||||
| (16, 'fastify', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:33', '2021-03-04 12:55:33'), | ||||
| (17, 'MandarineTS', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:48', '2021-03-04 12:55:48'), | ||||
| (18, 'Olivia', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:55:56', '2021-03-04 12:55:56'), | ||||
| (19, 'yarnpkg', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:19', '2021-03-04 12:56:19'), | ||||
| (20, 'Qovery', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:25', '2021-03-04 12:56:25'), | ||||
| (21, 'The Design Collective', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:46', '2021-03-04 12:56:46'), | ||||
| (22, 'Tauri Apps', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:56:52', '2021-03-04 12:56:52'), | ||||
| (23, 'microsoft-python', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:06', '2021-03-04 12:57:06'), | ||||
| (24, 'AppBrewery', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:17', '2021-03-04 12:57:17'), | ||||
| (25, 'OpenSauced', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:23', '2021-03-04 12:57:23'), | ||||
| (26, 'Devsters', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:39', '2021-03-04 12:57:39'), | ||||
| (27, 'Coding Roads', '', '/uploads/guilds/default.png', 0, '2021-03-04 12:57:49', '2021-03-04 12:57:49'); | ||||
|  | ||||
| INSERT INTO `Channels` (`id`, `name`, `type`, `description`, `isDefault`, `guildId`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 'general', 'text', '', 1, 1, '2021-03-04 12:51:27', '2021-03-04 12:51:27'), | ||||
| (2, 'general', 'text', '', 1, 2, '2021-03-04 12:51:39', '2021-03-04 12:51:39'), | ||||
| (3, 'general', 'text', '', 1, 3, '2021-03-04 12:51:50', '2021-03-04 12:51:50'), | ||||
| (4, 'general', 'text', '', 1, 4, '2021-03-04 12:52:09', '2021-03-04 12:52:09'), | ||||
| (5, 'general', 'text', '', 1, 5, '2021-03-04 12:52:19', '2021-03-04 12:52:19'), | ||||
| (6, 'general', 'text', '', 1, 6, '2021-03-04 12:52:30', '2021-03-04 12:52:30'), | ||||
| (7, 'general', 'text', '', 1, 7, '2021-03-04 12:52:38', '2021-03-04 12:52:38'), | ||||
| (8, 'general', 'text', '', 1, 8, '2021-03-04 12:52:45', '2021-03-04 12:52:45'), | ||||
| (9, 'general', 'text', '', 1, 9, '2021-03-04 12:52:57', '2021-03-04 12:52:57'), | ||||
| (10, 'general', 'text', '', 1, 10, '2021-03-04 12:53:08', '2021-03-04 12:53:08'), | ||||
| (11, 'general', 'text', '', 1, 11, '2021-03-04 12:54:58', '2021-03-04 12:54:58'), | ||||
| (12, 'general', 'text', '', 1, 12, '2021-03-04 12:55:04', '2021-03-04 12:55:04'), | ||||
| (13, 'general', 'text', '', 1, 13, '2021-03-04 12:55:11', '2021-03-04 12:55:11'), | ||||
| (14, 'general', 'text', '', 1, 14, '2021-03-04 12:55:16', '2021-03-04 12:55:16'), | ||||
| (15, 'general', 'text', '', 1, 15, '2021-03-04 12:55:26', '2021-03-04 12:55:26'), | ||||
| (16, 'general', 'text', '', 1, 16, '2021-03-04 12:55:33', '2021-03-04 12:55:33'), | ||||
| (17, 'general', 'text', '', 1, 17, '2021-03-04 12:55:48', '2021-03-04 12:55:48'), | ||||
| (18, 'general', 'text', '', 1, 18, '2021-03-04 12:55:56', '2021-03-04 12:55:56'), | ||||
| (19, 'general', 'text', '', 1, 19, '2021-03-04 12:56:19', '2021-03-04 12:56:19'), | ||||
| (20, 'general', 'text', '', 1, 20, '2021-03-04 12:56:25', '2021-03-04 12:56:25'), | ||||
| (21, 'general', 'text', '', 1, 21, '2021-03-04 12:56:46', '2021-03-04 12:56:46'), | ||||
| (22, 'general', 'text', '', 1, 22, '2021-03-04 12:56:52', '2021-03-04 12:56:52'), | ||||
| (23, 'general', 'text', '', 1, 23, '2021-03-04 12:57:06', '2021-03-04 12:57:06'), | ||||
| (24, 'general', 'text', '', 1, 24, '2021-03-04 12:57:17', '2021-03-04 12:57:17'), | ||||
| (25, 'general', 'text', '', 1, 25, '2021-03-04 12:57:23', '2021-03-04 12:57:23'), | ||||
| (26, 'general', 'text', '', 1, 26, '2021-03-04 12:57:39', '2021-03-04 12:57:39'), | ||||
| (27, 'general', 'text', '', 1, 27, '2021-03-04 12:57:49', '2021-03-04 12:57:49'); | ||||
|  | ||||
| INSERT INTO `Invitations` (`id`, `value`, `expiresIn`, `isPublic`, `guildId`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 'firstinvitation', 0, 1, 1, '2021-03-04 13:09:06', '2021-03-04 13:09:06'); | ||||
|  | ||||
| INSERT INTO `Members` (`id`, `isOwner`, `lastVisitedChannelId`, `userId`, `guildId`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 1, 1, 1, 1, '2021-03-04 12:51:27', '2021-03-04 12:51:27'), | ||||
| (2, 1, 2,  1, 2, '2021-03-04 12:51:39', '2021-03-04 12:51:39'), | ||||
| (3, 1, 3, 1, 3, '2021-03-04 12:51:50', '2021-03-04 12:51:50'), | ||||
| (4, 1, 4, 1, 4, '2021-03-04 12:52:09', '2021-03-04 12:52:09'), | ||||
| (5, 1, 5, 1, 5, '2021-03-04 12:52:19', '2021-03-04 12:52:19'), | ||||
| (6, 1, 6, 1, 6, '2021-03-04 12:52:30', '2021-03-04 12:52:30'), | ||||
| (7, 1, 7, 1, 7, '2021-03-04 12:52:38', '2021-03-04 12:52:38'), | ||||
| (8, 1, 8, 1, 8, '2021-03-04 12:52:45', '2021-03-04 12:52:45'), | ||||
| (9, 1, 9, 1, 9, '2021-03-04 12:52:57', '2021-03-04 12:52:57'), | ||||
| (10, 1, 10, 1, 10, '2021-03-04 12:53:08', '2021-03-04 12:53:08'), | ||||
| (11, 1, 11, 1, 11, '2021-03-04 12:54:58', '2021-03-04 12:54:58'), | ||||
| (12, 1, 12, 1, 12, '2021-03-04 12:55:04', '2021-03-04 12:55:04'), | ||||
| (13, 1, 13, 1, 13, '2021-03-04 12:55:11', '2021-03-04 12:55:11'), | ||||
| (14, 1, 14, 1, 14, '2021-03-04 12:55:16', '2021-03-04 12:55:16'), | ||||
| (15, 1, 15, 1, 15, '2021-03-04 12:55:26', '2021-03-04 12:55:26'), | ||||
| (16, 1, 16, 1, 16, '2021-03-04 12:55:33', '2021-03-04 12:55:33'), | ||||
| (17, 1, 17, 1, 17, '2021-03-04 12:55:48', '2021-03-04 12:55:48'), | ||||
| (18, 1, 18, 1, 18, '2021-03-04 12:55:56', '2021-03-04 12:55:56'), | ||||
| (19, 1, 19, 1, 19, '2021-03-04 12:56:19', '2021-03-04 12:56:19'), | ||||
| (20, 1, 20, 1, 20, '2021-03-04 12:56:25', '2021-03-04 12:56:25'), | ||||
| (21, 1, 21, 1, 21, '2021-03-04 12:56:46', '2021-03-04 12:56:46'), | ||||
| (22, 1, 22, 1, 22, '2021-03-04 12:56:52', '2021-03-04 12:56:52'), | ||||
| (23, 1, 23, 1, 23, '2021-03-04 12:57:06', '2021-03-04 12:57:06'), | ||||
| (24, 1, 24, 1, 24, '2021-03-04 12:57:17', '2021-03-04 12:57:17'), | ||||
| (25, 1, 25, 1, 25, '2021-03-04 12:57:23', '2021-03-04 12:57:23'), | ||||
| (26, 1, 26, 1, 26, '2021-03-04 12:57:39', '2021-03-04 12:57:39'), | ||||
| (27, 1, 27, 1, 27, '2021-03-04 12:57:49', '2021-03-04 12:57:49'); | ||||
|  | ||||
| INSERT INTO `Messages` (`id`, `value`, `type`, `mimetype`, `memberId`, `channelId`, `createdAt`, `updatedAt`) VALUES | ||||
| (1, 'Hello world!', 'text', 'text/plain', 1, 1, '2021-03-04 13:08:22', '2021-03-04 13:08:22'); | ||||
							
								
								
									
										6
									
								
								src/services/__docs__/components.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/services/__docs__/components.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| components: | ||||
|   securitySchemes: | ||||
|     bearerAuth: | ||||
|       type: 'http' | ||||
|       scheme: 'bearer' | ||||
|       bearerFormat: 'JWT' | ||||
							
								
								
									
										87
									
								
								src/services/__docs__/errors-definitions.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/services/__docs__/errors-definitions.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| definitions: | ||||
|   BadRequestError: | ||||
|     '400': | ||||
|       description: 'Bad Request' | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               errors: | ||||
|                 type: 'array' | ||||
|                 items: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     message: | ||||
|                       type: 'string' | ||||
|                     field: | ||||
|                       type: 'string' | ||||
|                   required: | ||||
|                     - 'message' | ||||
|  | ||||
|   UnauthorizedError: | ||||
|     '401': | ||||
|       description: 'Unauthorized: Token is missing or invalid Bearer' | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               errors: | ||||
|                 type: 'array' | ||||
|                 items: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     message: | ||||
|                       type: 'string' | ||||
|                       enum: ['Unauthorized: Token is missing or invalid Bearer'] | ||||
|  | ||||
|   ForbiddenError: | ||||
|     '403': | ||||
|       description: 'Forbidden' | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               errors: | ||||
|                 type: 'array' | ||||
|                 items: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     message: | ||||
|                       type: 'string' | ||||
|                       enum: ['Forbidden'] | ||||
|  | ||||
|   NotFoundError: | ||||
|     '404': | ||||
|       description: 'Not Found' | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               errors: | ||||
|                 type: 'array' | ||||
|                 items: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     message: | ||||
|                       type: 'string' | ||||
|                       enum: ['Not Found'] | ||||
|  | ||||
|   PayloadTooLargeError: | ||||
|     '413': | ||||
|       description: 'Payload Too Large' | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               errors: | ||||
|                 type: 'array' | ||||
|                 items: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     message: | ||||
|                       type: 'string' | ||||
							
								
								
									
										20
									
								
								src/services/__docs__/utils.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/services/__docs__/utils.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| definitions: | ||||
|   PaginateModel: | ||||
|     type: 'object' | ||||
|     properties: | ||||
|       hasMore: | ||||
|         type: 'boolean' | ||||
|       totalItems: | ||||
|         type: 'number' | ||||
|       itemsPerPage: | ||||
|         type: 'number' | ||||
|       page: | ||||
|         type: 'number' | ||||
|   PaginateModelParameters: | ||||
|     'parameters': | ||||
|       - name: 'itemsPerPage' | ||||
|         in: 'query' | ||||
|         required: false | ||||
|       - name: 'page' | ||||
|         in: 'query' | ||||
|         required: false | ||||
							
								
								
									
										25
									
								
								src/services/channels/[channelId]/__docs__/delete.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/services/channels/[channelId]/__docs__/delete.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| /channels/{channelId}: | ||||
|   delete: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'channels' | ||||
|     summary: 'DELETE a channel with its id' | ||||
|     parameters: | ||||
|       - name: 'channelId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     deletedChannelId: | ||||
|                       type: 'number' | ||||
							
								
								
									
										41
									
								
								src/services/channels/[channelId]/__docs__/put.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/services/channels/[channelId]/__docs__/put.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| /channels/{channelId}: | ||||
|   put: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'channels' | ||||
|     summary: 'UPDATE a channel with its id' | ||||
|     parameters: | ||||
|       - name: 'channelId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     requestBody: | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               name: | ||||
|                 type: 'string' | ||||
|                 minLength: 3 | ||||
|                 maxLength: 30 | ||||
|               description: | ||||
|                 type: 'string' | ||||
|                 maxLength: 160 | ||||
|               isDefault: | ||||
|                 type: 'boolean' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     channel: | ||||
|                       allOf: | ||||
|                         - $ref: '#/definitions/Channel' | ||||
							
								
								
									
										71
									
								
								src/services/channels/[channelId]/__test__/delete.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/services/channels/[channelId]/__test__/delete.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../application' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import { errorsMessages } from '../delete' | ||||
| import { createChannels } from '../../__test__/utils/createChannel' | ||||
|  | ||||
| describe('DELETE /channels/:channelId', () => { | ||||
|   it('succeeds and delete the channel', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToDelete = result.channels[0] | ||||
|     const response = await request(application) | ||||
|       .delete(`/channels/${channelToDelete.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.deletedChannelId).toEqual(channelToDelete.id) | ||||
|     const foundChannel = await Channel.findOne({ | ||||
|       where: { id: channelToDelete.id } | ||||
|     }) | ||||
|     expect(foundChannel).toBeNull() | ||||
|   }) | ||||
|  | ||||
|   it("fails if the channel doesn't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .delete('/channels/23') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it('fails if the user is not the owner', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToDelete = result.channels[0] | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .delete(`/channels/${channelToDelete.id as number}`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it("fails if it's the default channel", async () => { | ||||
|     const result = await createChannels([]) | ||||
|     const defaultChannel = await Channel.findOne({ | ||||
|       where: { guildId: result.guild.id as number, isDefault: true } | ||||
|     }) | ||||
|     expect(defaultChannel).not.toBeNull() | ||||
|     const response = await request(application) | ||||
|       .delete(`/channels/${defaultChannel?.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([errorsMessages.channel.shouldNotBeTheDefault]) | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										120
									
								
								src/services/channels/[channelId]/__test__/put.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/services/channels/[channelId]/__test__/put.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../application' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import { commonErrorsMessages } from '../../../../tools/configurations/constants' | ||||
| import { randomString } from '../../../../tools/utils/random' | ||||
| import { createChannels } from '../../__test__/utils/createChannel' | ||||
|  | ||||
| describe('PUT /channels/:channelId', () => { | ||||
|   it('succeeds and edit name/description of the channel', async () => { | ||||
|     const name = 'general-updated' | ||||
|     const description = 'general-description' | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToEdit = result.channels[0] | ||||
|     const response = await request(application) | ||||
|       .put(`/channels/${channelToEdit.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name, description }) | ||||
|       .expect(200) | ||||
|     expect(response.body.channel.name).toEqual(name) | ||||
|     expect(response.body.channel.description).toEqual(description) | ||||
|   }) | ||||
|  | ||||
|   it('succeeds and set default channel to true', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToEdit = result.channels[0] | ||||
|     const response = await request(application) | ||||
|       .put(`/channels/${channelToEdit.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ isDefault: true }) | ||||
|       .expect(200) | ||||
|     const defaultChannels = await Channel.findAll({ | ||||
|       where: { guildId: result.guild.id as number, isDefault: true } | ||||
|     }) | ||||
|     expect(defaultChannels.length).toEqual(1) | ||||
|     expect(response.body.channel.name).toEqual(channel1.name) | ||||
|     expect(response.body.channel.isDefault).toBeTruthy() | ||||
|   }) | ||||
|  | ||||
|   it('succeeds with invalid slug name', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToEdit = result.channels[0] | ||||
|     const name = 'random channel' | ||||
|     const response = await request(application) | ||||
|       .put(`/channels/${channelToEdit.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name }) | ||||
|       .expect(200) | ||||
|     expect(response.body.channel.name).toEqual(name) | ||||
|     expect(response.body.channel.isDefault).toBeFalsy() | ||||
|   }) | ||||
|  | ||||
|   it('fails with too long description', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToEdit = result.channels[0] | ||||
|     const response = await request(application) | ||||
|       .put(`/channels/${channelToEdit.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ description: randomString(170) }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         commonErrorsMessages.charactersLength('description', { max: 160 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with too long name', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToEdit = result.channels[0] | ||||
|     const response = await request(application) | ||||
|       .put(`/channels/${channelToEdit.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name: ' random channel name ' + randomString(35) }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         commonErrorsMessages.charactersLength('name', { max: 30, min: 3 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the channel doesn't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .put('/channels/23') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it('fails if the user is not the owner', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channelToRemove = result.channels[0] | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .put(`/channels/${channelToRemove.id as number}`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										56
									
								
								src/services/channels/[channelId]/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/services/channels/[channelId]/delete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../tools/middlewares/authenticateUser' | ||||
| import Channel from '../../../models/Channel' | ||||
| import Member from '../../../models/Member' | ||||
| import { BadRequestError } from '../../../tools/errors/BadRequestError' | ||||
| import { ForbiddenError } from '../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../tools/errors/NotFoundError' | ||||
| import { deleteMessages } from '../../../tools/utils/deleteFiles' | ||||
| import Message from '../../../models/Message' | ||||
| import { emitToMembers } from '../../../tools/socket/emitEvents' | ||||
|  | ||||
| export const errorsMessages = { | ||||
|   channel: { | ||||
|     shouldNotBeTheDefault: 'The channel to delete should not be the default' | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const deleteByIdChannelsRouter = Router() | ||||
|  | ||||
| deleteByIdChannelsRouter.delete( | ||||
|   '/channels/:channelId', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { channelId } = req.params as { channelId: string } | ||||
|     const channel = await Channel.findOne({ | ||||
|       where: { id: channelId }, | ||||
|       include: [Message] | ||||
|     }) | ||||
|     if (channel == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId: channel.guildId, isOwner: true } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     if (channel.isDefault) { | ||||
|       throw new BadRequestError(errorsMessages.channel.shouldNotBeTheDefault) | ||||
|     } | ||||
|     const deletedChannelId = channel.id | ||||
|     await deleteMessages(channel.messages) | ||||
|     await channel.destroy() | ||||
|     await emitToMembers({ | ||||
|       event: 'channels', | ||||
|       guildId: channel.guildId, | ||||
|       payload: { action: 'delete', item: channel } | ||||
|     }) | ||||
|     return res.status(200).json({ deletedChannelId }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										33
									
								
								src/services/channels/[channelId]/messages/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/services/channels/[channelId]/messages/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| /channels/{channelId}/messages: | ||||
|   get: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'messages' | ||||
|     summary: 'GET all the messages of a channel' | ||||
|     parameters: | ||||
|       - name: 'channelId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     allOf: | ||||
|       - $ref: '#/definitions/PaginateModelParameters' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   allOf: | ||||
|                     - $ref: '#/definitions/PaginateModel' | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     rows: | ||||
|                       type: 'array' | ||||
|                       items: | ||||
|                         allOf: | ||||
|                           - $ref: '#/definitions/Message' | ||||
|                           - $ref: '#/definitions/User' | ||||
| @@ -0,0 +1,44 @@ | ||||
| /channels/{channelId}/messages: | ||||
|   post: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'messages' | ||||
|     summary: 'Create a new message' | ||||
|     parameters: | ||||
|       - name: 'channelId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     requestBody: | ||||
|       content: | ||||
|         multipart/form-data: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               value: | ||||
|                 type: 'string' | ||||
|                 minLength: 1 | ||||
|                 maxLength: 50_000 | ||||
|               type: | ||||
|                 allOf: | ||||
|                   - $ref: '#/definitions/MessageType' | ||||
|               file: | ||||
|                 type: 'string' | ||||
|                 format: 'binary' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - $ref: '#/definitions/PayloadTooLargeError' | ||||
|         - '201': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     message: | ||||
|                       allOf: | ||||
|                         - $ref: '#/definitions/Message' | ||||
|                         - $ref: '#/definitions/User' | ||||
| @@ -0,0 +1,23 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../../../../../application' | ||||
| import { createMessages } from '../../../../messages/__test__/utils/createMessages' | ||||
|  | ||||
| describe('GET /channels/:channelId/messages', () => { | ||||
|   it('should get all the messages of the channel', async () => { | ||||
|     const messages = ['Hello world!', 'some random message'] | ||||
|     const result = await createMessages(messages) | ||||
|     const response = await request(application) | ||||
|       .get(`/channels/${result.channelId}/messages`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.hasMore).toBeFalsy() | ||||
|     expect(response.body.totalItems).toEqual(messages.length) | ||||
|     expect(response.body.rows[0].value).toEqual(messages[0]) | ||||
|     expect(response.body.rows[1].value).toEqual(messages[1]) | ||||
|     expect(response.body.rows[1].user).not.toBeNull() | ||||
|     expect(response.body.rows[1].user.id).toEqual(result.user.id) | ||||
|     expect(response.body.rows[1].user.password).not.toBeDefined() | ||||
|   }) | ||||
| }) | ||||
| @@ -0,0 +1,69 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../../application' | ||||
| import { createChannels } from '../../../__test__/utils/createChannel' | ||||
|  | ||||
| const channel1 = { name: 'general1', description: 'testing' } | ||||
|  | ||||
| describe('POST /channels/:channelId/messages', () => { | ||||
|   it('succeeds and create the message', async () => { | ||||
|     const value = 'my awesome message' | ||||
|     const result = await createChannels([channel1]) | ||||
|     expect(result.channels.length).toEqual(1) | ||||
|     const channel = result.channels[0] | ||||
|     const response = await request(application) | ||||
|       .post(`/channels/${channel.id as number}/messages`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value, type: 'text' }) | ||||
|       .expect(201) | ||||
|     expect(response.body.message).not.toBeNull() | ||||
|     expect(response.body.message.value).toEqual(value) | ||||
|     expect(response.body.message.type).toEqual('text') | ||||
|     expect(response.body.message.user).not.toBeNull() | ||||
|     expect(response.body.message.user.id).toEqual(result.user.id) | ||||
|   }) | ||||
|  | ||||
|   it('fails with empty message', async () => { | ||||
|     const result = await createChannels([channel1]) | ||||
|     expect(result.channels.length).toEqual(1) | ||||
|     const channel = result.channels[0] | ||||
|     const response1 = await request(application) | ||||
|       .post(`/channels/${channel.id as number}/messages`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ type: 'text' }) | ||||
|       .expect(400) | ||||
|     const response2 = await request(application) | ||||
|       .post(`/channels/${channel.id as number}/messages`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ type: 'file' }) | ||||
|       .expect(400) | ||||
|     expect(response1.body.errors.length).toEqual(1) | ||||
|     expect(response2.body.errors.length).toEqual(1) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the channel doesn't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .post('/channels/2/messages') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ type: 'text', value: 'awesome' }) | ||||
|       .expect(404) | ||||
|     expect(response.body.errors.length).toEqual(1) | ||||
|   }) | ||||
|  | ||||
|   it('fails if the user is not in the guild with this channel', async () => { | ||||
|     const result = await createChannels([channel1]) | ||||
|     const channel = result.channels[0] | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .post(`/channels/${channel.id as number}/messages`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ value: 'some random message', type: 'text' }) | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										60
									
								
								src/services/channels/[channelId]/messages/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/services/channels/[channelId]/messages/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import Member from '../../../../models/Member' | ||||
| import Message from '../../../../models/Message' | ||||
| import { paginateModel } from '../../../../tools/database/paginateModel' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
| import User from '../../../../models/User' | ||||
|  | ||||
| export const getMessagesRouter = Router() | ||||
|  | ||||
| getMessagesRouter.get( | ||||
|   '/channels/:channelId/messages', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const { itemsPerPage, page } = req.query as { | ||||
|       itemsPerPage: string | ||||
|       page: string | ||||
|     } | ||||
|     const { channelId } = req.params as { channelId: string } | ||||
|     const user = req.user.current | ||||
|     const channel = await Channel.findOne({ where: { id: channelId } }) | ||||
|     if (channel == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId: channel.guildId } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     member.lastVisitedChannelId = channel.id | ||||
|     await member.save() | ||||
|     const result = await paginateModel({ | ||||
|       Model: Message, | ||||
|       queryOptions: { itemsPerPage, page }, | ||||
|       findOptions: { | ||||
|         order: [['createdAt', 'DESC']], | ||||
|         include: [{ model: Member, include: [User] }], | ||||
|         where: { | ||||
|           channelId: channel.id | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     return res.status(200).json({ | ||||
|       hasMore: result.hasMore, | ||||
|       totalItems: result.totalItems, | ||||
|       itemsPerPage: result.itemsPerPage, | ||||
|       page: result.page, | ||||
|       rows: result.rows.reverse().map((row: any) => { | ||||
|         return { ...row.toJSON(), user: row.member.user.toJSON() } | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										9
									
								
								src/services/channels/[channelId]/messages/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/services/channels/[channelId]/messages/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { postMessagesRouter } from './post' | ||||
| import { getMessagesRouter } from './get' | ||||
|  | ||||
| export const messagesChannelsRouter = Router() | ||||
|  | ||||
| messagesChannelsRouter.use('/', postMessagesRouter) | ||||
| messagesChannelsRouter.use('/', getMessagesRouter) | ||||
							
								
								
									
										122
									
								
								src/services/channels/[channelId]/messages/post.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/services/channels/[channelId]/messages/post.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
| import { body } from 'express-validator' | ||||
| import fileUpload from 'express-fileupload' | ||||
| import { v4 as uuidv4 } from 'uuid' | ||||
| import path from 'path' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import { validateRequest } from '../../../../tools/middlewares/validateRequest' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import Member from '../../../../models/Member' | ||||
| import Message, { MessageType, messageTypes } from '../../../../models/Message' | ||||
| import { | ||||
|   commonErrorsMessages, | ||||
|   fileUploadOptions, | ||||
|   messagesFilePath, | ||||
|   tempPath | ||||
| } from '../../../../tools/configurations/constants' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
| import { onlyPossibleValuesValidation } from '../../../../tools/validations/onlyPossibleValuesValidation' | ||||
| import { deleteAllFilesInDirectory } from '../../../../tools/utils/deleteFiles' | ||||
| import { PayloadTooLargeError } from '../../../../tools/errors/PayloadTooLargeError' | ||||
| import { BadRequestError } from '../../../../tools/errors/BadRequestError' | ||||
| import { emitToMembers } from '../../../../tools/socket/emitEvents' | ||||
|  | ||||
| export const errorsMessages = { | ||||
|   type: { | ||||
|     shouldNotBeEmpty: 'Type should not be empty' | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const postMessagesRouter = Router() | ||||
|  | ||||
| postMessagesRouter.post( | ||||
|   '/channels/:channelId/messages', | ||||
|   authenticateUser, | ||||
|   fileUpload(fileUploadOptions), | ||||
|   [ | ||||
|     body('value') | ||||
|       .optional({ nullable: true }) | ||||
|       .trim() | ||||
|       .escape() | ||||
|       .isLength({ min: 1, max: 50_000 }) | ||||
|       .withMessage( | ||||
|         commonErrorsMessages.charactersLength('value', { min: 1, max: 50_000 }) | ||||
|       ), | ||||
|     body('type') | ||||
|       .notEmpty() | ||||
|       .withMessage(errorsMessages.type.shouldNotBeEmpty) | ||||
|       .trim() | ||||
|       .isString() | ||||
|       .custom(async (type: MessageType) => { | ||||
|         return await onlyPossibleValuesValidation(messageTypes, 'type', type) | ||||
|       }) | ||||
|   ], | ||||
|   validateRequest, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { value, type } = req.body as { | ||||
|       value?: string | ||||
|       type: MessageType | ||||
|     } | ||||
|     const file = req.files?.file | ||||
|     const { channelId } = req.params as { channelId: string } | ||||
|     const channel = await Channel.findOne({ | ||||
|       where: { id: channelId, type: 'text' } | ||||
|     }) | ||||
|     if (channel == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId: channel.guildId } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     if ( | ||||
|       (type === 'file' && file == null) || | ||||
|       (type === 'text' && value == null) | ||||
|     ) { | ||||
|       throw new BadRequestError("You can't send an empty message") | ||||
|     } | ||||
|     let filename: string | null = null | ||||
|     let mimetype = 'text/plain' | ||||
|     if ( | ||||
|       value == null && | ||||
|       type === 'file' && | ||||
|       file != null && | ||||
|       !Array.isArray(file) | ||||
|     ) { | ||||
|       if (file.truncated) { | ||||
|         await deleteAllFilesInDirectory(tempPath) | ||||
|         throw new PayloadTooLargeError( | ||||
|           commonErrorsMessages.tooLargeFile('file') | ||||
|         ) | ||||
|       } | ||||
|       mimetype = file.mimetype | ||||
|       const splitedMimetype = mimetype.split('/') | ||||
|       const fileExtension = splitedMimetype[1] | ||||
|       filename = `${uuidv4()}.${fileExtension}` | ||||
|       await file.mv(path.join(messagesFilePath.filePath, filename)) | ||||
|       await deleteAllFilesInDirectory(tempPath) | ||||
|     } | ||||
|     const messageCreated = await Message.create({ | ||||
|       value: filename != null ? `${messagesFilePath.name}/${filename}` : value, | ||||
|       type, | ||||
|       mimetype, | ||||
|       memberId: member.id, | ||||
|       channelId: channel.id | ||||
|     }) | ||||
|     const message = { ...messageCreated.toJSON(), user: req.user.current } | ||||
|     await emitToMembers({ | ||||
|       event: 'messages', | ||||
|       guildId: member.guildId, | ||||
|       payload: { action: 'create', item: message } | ||||
|     }) | ||||
|     return res.status(201).json({ message }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										92
									
								
								src/services/channels/[channelId]/put.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/services/channels/[channelId]/put.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
| import { body } from 'express-validator' | ||||
|  | ||||
| import { authenticateUser } from '../../../tools/middlewares/authenticateUser' | ||||
| import { validateRequest } from '../../../tools/middlewares/validateRequest' | ||||
| import Channel from '../../../models/Channel' | ||||
| import Member from '../../../models/Member' | ||||
| import { commonErrorsMessages } from '../../../tools/configurations/constants' | ||||
| import { ForbiddenError } from '../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../tools/errors/NotFoundError' | ||||
| import { emitToMembers } from '../../../tools/socket/emitEvents' | ||||
|  | ||||
| export const putByIdChannelsRouter = Router() | ||||
|  | ||||
| putByIdChannelsRouter.put( | ||||
|   '/channels/:channelId', | ||||
|   authenticateUser, | ||||
|   [ | ||||
|     body('name') | ||||
|       .optional({ nullable: true }) | ||||
|       .isString() | ||||
|       .trim() | ||||
|       .escape() | ||||
|       .isLength({ max: 30, min: 3 }) | ||||
|       .withMessage( | ||||
|         commonErrorsMessages.charactersLength('name', { max: 30, min: 3 }) | ||||
|       ), | ||||
|     body('description') | ||||
|       .optional({ nullable: true }) | ||||
|       .trim() | ||||
|       .escape() | ||||
|       .isLength({ max: 160 }) | ||||
|       .withMessage( | ||||
|         commonErrorsMessages.charactersLength('description', { max: 160 }) | ||||
|       ), | ||||
|     body('isDefault').optional({ nullable: true }).isBoolean() | ||||
|   ], | ||||
|   validateRequest, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { channelId } = req.params as { channelId: string } | ||||
|     const { name, description, isDefault } = req.body as { | ||||
|       name?: string | ||||
|       description?: string | ||||
|       isDefault?: boolean | ||||
|     } | ||||
|     const channel = await Channel.findOne({ | ||||
|       where: { id: channelId } | ||||
|     }) | ||||
|     if (channel == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId: channel.guildId, isOwner: true } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     channel.name = name ?? channel.name | ||||
|     channel.description = description ?? channel.description | ||||
|     if (isDefault != null) { | ||||
|       const defaultChannel = await Channel.findOne({ | ||||
|         where: { isDefault: true, guildId: member.guildId } | ||||
|       }) | ||||
|       if (isDefault && defaultChannel != null) { | ||||
|         defaultChannel.isDefault = false | ||||
|         channel.isDefault = true | ||||
|         await defaultChannel.save() | ||||
|         const defaultChannelMembers = await Member.findAll({ | ||||
|           where: { | ||||
|             guildId: member.guildId, | ||||
|             lastVisitedChannelId: defaultChannel.id | ||||
|           } | ||||
|         }) | ||||
|         for (const defaultChannelMember of defaultChannelMembers) { | ||||
|           defaultChannelMember.lastVisitedChannelId = channel.id | ||||
|           await defaultChannelMember.save() | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     await channel.save() | ||||
|     await emitToMembers({ | ||||
|       event: 'channels', | ||||
|       guildId: channel.guildId, | ||||
|       payload: { action: 'update', item: channel } | ||||
|     }) | ||||
|     return res.status(200).json({ channel }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										24
									
								
								src/services/channels/__docs__/_definitions.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/services/channels/__docs__/_definitions.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| definitions: | ||||
|   Channel: | ||||
|     type: 'object' | ||||
|     properties: | ||||
|       id: | ||||
|         type: 'integer' | ||||
|         description: 'Unique id' | ||||
|       name: | ||||
|         type: 'string' | ||||
|       type: | ||||
|         type: 'string' | ||||
|         enum: ['text', 'voice'] | ||||
|       description: | ||||
|         type: 'string' | ||||
|       isDefault: | ||||
|         type: 'boolean' | ||||
|       guildId: | ||||
|         type: 'integer' | ||||
|       createdAt: | ||||
|         type: 'string' | ||||
|         format: 'date-time' | ||||
|       updatedAt: | ||||
|         type: 'string' | ||||
|         format: 'date-time' | ||||
							
								
								
									
										42
									
								
								src/services/channels/__test__/utils/createChannel.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/services/channels/__test__/utils/createChannel.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../../../../application' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import { | ||||
|   createGuild, | ||||
|   CreateGuildResult | ||||
| } from '../../../guilds/__test__/utils/createGuild' | ||||
|  | ||||
| interface ChannelOptions { | ||||
|   name: string | ||||
|   description: string | ||||
| } | ||||
|  | ||||
| interface CreateChannelsResult extends CreateGuildResult { | ||||
|   channels: Channel[] | ||||
| } | ||||
|  | ||||
| export const createChannels = async ( | ||||
|   channels: ChannelOptions[] | ||||
| ): Promise<CreateChannelsResult> => { | ||||
|   const result = await createGuild({ | ||||
|     guild: { description: 'description', name: 'guild' }, | ||||
|     user: { | ||||
|       email: 'test@test.com', | ||||
|       name: 'Test' | ||||
|     } | ||||
|   }) | ||||
|   const channelsResponses: Channel[] = [] | ||||
|   for (const { name, description } of channels) { | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name, description }) | ||||
|       .expect(201) | ||||
|     channelsResponses.push(response.body.channel) | ||||
|   } | ||||
|   return { | ||||
|     ...result, | ||||
|     channels: channelsResponses | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								src/services/channels/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/services/channels/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { deleteByIdChannelsRouter } from './[channelId]/delete' | ||||
| import { messagesChannelsRouter } from './[channelId]/messages' | ||||
| import { putByIdChannelsRouter } from './[channelId]/put' | ||||
|  | ||||
| export const channelsRouter = Router() | ||||
|  | ||||
| channelsRouter.use('/', deleteByIdChannelsRouter) | ||||
| channelsRouter.use('/', putByIdChannelsRouter) | ||||
| channelsRouter.use('/', messagesChannelsRouter) | ||||
							
								
								
									
										8
									
								
								src/services/docs/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/services/docs/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { Router } from 'express' | ||||
| import swaggerUi from 'swagger-ui-express' | ||||
|  | ||||
| import { swaggerSpecification } from '../../tools/configurations/swaggerSpecification' | ||||
|  | ||||
| export const documentationRouter = Router() | ||||
|  | ||||
| documentationRouter.use('/documentation', swaggerUi.serve, swaggerUi.setup(swaggerSpecification)) | ||||
							
								
								
									
										24
									
								
								src/services/guilds/[guildId]/__docs__/delete.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/services/guilds/[guildId]/__docs__/delete.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| /guilds/{guildId}: | ||||
|   delete: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'guilds' | ||||
|     summary: 'DELETE a guild with its id' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     deletedGuildId: | ||||
|                       type: 'number' | ||||
							
								
								
									
										25
									
								
								src/services/guilds/[guildId]/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/services/guilds/[guildId]/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| /guilds/{guildId}: | ||||
|   get: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'guilds' | ||||
|     summary: 'GET a guild with its id' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     guild: | ||||
|                       allOf: | ||||
|                         - $ref: '#/definitions/Guild' | ||||
							
								
								
									
										48
									
								
								src/services/guilds/[guildId]/__docs__/put.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/services/guilds/[guildId]/__docs__/put.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| /guilds/{guildId}: | ||||
|   put: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'guilds' | ||||
|     summary: 'Update a guild with its id' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     requestBody: | ||||
|       content: | ||||
|         multipart/form-data: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               name: | ||||
|                 type: 'string' | ||||
|                 minLength: 3 | ||||
|                 maxLength: 30 | ||||
|               description: | ||||
|                 type: 'string' | ||||
|                 maxLength: 160 | ||||
|               icon: | ||||
|                 type: 'string' | ||||
|                 format: 'binary' | ||||
|               isPublic: | ||||
|                 type: 'boolean' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     guild: | ||||
|                       allOf: | ||||
|                         - $ref: '#/definitions/Guild' | ||||
|                       type: 'object' | ||||
|                       properties: | ||||
|                         publicInvitation: | ||||
|                           type: 'string' | ||||
							
								
								
									
										62
									
								
								src/services/guilds/[guildId]/__test__/delete.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/services/guilds/[guildId]/__test__/delete.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../application' | ||||
| import Guild from '../../../../models/Guild' | ||||
| import { createGuild } from '../../__test__/utils/createGuild' | ||||
|  | ||||
| describe('DELETE /guilds/:guildId', () => { | ||||
|   it('succeeds and delete the guild', async () => { | ||||
|     const name = 'guild' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .delete(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.deletedGuildId).toEqual(result.guild.id) | ||||
|     const foundGuild = await Guild.findOne({ where: { id: result?.guild.id as number } }) | ||||
|     expect(foundGuild).toBeNull() | ||||
|   }) | ||||
|  | ||||
|   it("fails if the guild doesn't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .delete('/guilds/23') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the user isn't the owner", async () => { | ||||
|     const name = 'guild' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .delete(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										58
									
								
								src/services/guilds/[guildId]/__test__/get.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/services/guilds/[guildId]/__test__/get.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../application' | ||||
| import { createGuild } from '../../__test__/utils/createGuild' | ||||
|  | ||||
| describe('GET /guilds/:guildId', () => { | ||||
|   it('succeeds and get the guild', async () => { | ||||
|     const name = 'guild' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .get(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.guild.name).toEqual(name) | ||||
|     expect(response.body.guild.description).toEqual(description) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the user isn't a member", async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'testing', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .get(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the guild doesn't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .get('/guilds/23') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										182
									
								
								src/services/guilds/[guildId]/__test__/put.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/services/guilds/[guildId]/__test__/put.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../application' | ||||
| import Guild from '../../../../models/Guild' | ||||
| import Invitation from '../../../../models/Invitation' | ||||
| import { commonErrorsMessages } from '../../../../tools/configurations/constants' | ||||
| import { randomString } from '../../../../tools/utils/random' | ||||
| import { createGuild } from '../../__test__/utils/createGuild' | ||||
|  | ||||
| describe('PUT /guilds/:guildId', () => { | ||||
|   it('succeeds and edit the guild', async () => { | ||||
|     const name = 'guild' | ||||
|     const newName = 'guildtest' | ||||
|     const description = 'testing' | ||||
|     const newDescription = 'new description' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name: newName, description: newDescription }) | ||||
|       .expect(200) | ||||
|     expect(response.body.guild.name).toEqual(newName) | ||||
|     expect(response.body.guild.description).toEqual(newDescription) | ||||
|     expect(response.body.guild.publicInvitation).toBeNull() | ||||
|     const foundGuild = await Guild.findOne({ | ||||
|       where: { id: result?.guild.id as number } | ||||
|     }) | ||||
|     expect(foundGuild?.name).toEqual(newName) | ||||
|     expect(foundGuild?.description).toEqual(newDescription) | ||||
|   }) | ||||
|  | ||||
|   it('succeeds and create/delete public invitations', async () => { | ||||
|     const name = 'guild' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const resIsPublic = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ isPublic: true }) | ||||
|       .expect(200) | ||||
|     expect(resIsPublic.body.guild.isPublic).toBeTruthy() | ||||
|     expect(typeof resIsPublic.body.guild.publicInvitation).toBe('string') | ||||
|     const publicInvitation = await Invitation.findOne({ | ||||
|       where: { isPublic: true, guildId: result?.guild.id as number } | ||||
|     }) | ||||
|     expect(publicInvitation).not.toBeNull() | ||||
|     expect(publicInvitation?.expiresIn).toEqual(0) | ||||
|  | ||||
|     const resIsNotPublic = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ isPublic: false }) | ||||
|       .expect(200) | ||||
|     expect(resIsNotPublic.body.guild.isPublic).toBeFalsy() | ||||
|     expect(resIsNotPublic.body.guild.publicInvitation).toBeNull() | ||||
|     const notPublicInvitation = await Invitation.findOne({ | ||||
|       where: { isPublic: false, guildId: result?.guild.id as number } | ||||
|     }) | ||||
|     expect(notPublicInvitation).toBeNull() | ||||
|   }) | ||||
|  | ||||
|   it("fails if the user isn't the owner", async () => { | ||||
|     const name = 'guild' | ||||
|     const newName = 'guildtest' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ name: newName }) | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the guild doesn't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const response = await request(application) | ||||
|       .put('/guilds/23') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ name: 'kjdjhdjh' }) | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it('fails with invalid name', async () => { | ||||
|     const name = 'guild' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name: randomString(35) }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         commonErrorsMessages.charactersLength('name', { max: 30, min: 3 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with name already used', async () => { | ||||
|     const { guild } = await createGuild({ | ||||
|       guild: { description: 'testing', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'testing', name: 'guild2' }, | ||||
|       user: { | ||||
|         email: 'test@test2.com', | ||||
|         name: 'Test2' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name: guild.name }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Name already used'])) | ||||
|   }) | ||||
|  | ||||
|   it('fails with invalid description', async () => { | ||||
|     const name = 'guild' | ||||
|     const description = 'testing' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description, name }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .put(`/guilds/${result.guild.id as number}`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ description: randomString(165) }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         commonErrorsMessages.charactersLength('description', { max: 160 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										31
									
								
								src/services/guilds/[guildId]/channels/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/services/guilds/[guildId]/channels/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| /guilds/{guildId}/channels: | ||||
|   get: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'channels' | ||||
|     summary: 'GET all the channels of a guild' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     allOf: | ||||
|       - $ref: '#/definitions/PaginateModelParameters' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   allOf: | ||||
|                     - $ref: '#/definitions/PaginateModel' | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     rows: | ||||
|                       type: 'array' | ||||
|                       items: | ||||
|                         allOf: | ||||
|                           - $ref: '#/definitions/Channel' | ||||
							
								
								
									
										39
									
								
								src/services/guilds/[guildId]/channels/__docs__/post.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/services/guilds/[guildId]/channels/__docs__/post.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| /guilds/{guildId}/channels: | ||||
|   post: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'channels' | ||||
|     summary: 'Create a channel' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     requestBody: | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               name: | ||||
|                 type: 'string' | ||||
|                 minLength: 3 | ||||
|                 maxLength: 30 | ||||
|               description: | ||||
|                 type: 'string' | ||||
|                 maxLength: 160 | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '201': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     channel: | ||||
|                       allOf: | ||||
|                         - $ref: '#/definitions/Channel' | ||||
							
								
								
									
										23
									
								
								src/services/guilds/[guildId]/channels/__test__/get.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/services/guilds/[guildId]/channels/__test__/get.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../../../../../application' | ||||
| import { createChannels } from '../../../../channels/__test__/utils/createChannel' | ||||
|  | ||||
| describe('GET /guilds/:guildId/channels', () => { | ||||
|   it('should get all the channels of the guild', async () => { | ||||
|     const channel1 = { name: 'general1', description: 'testing' } | ||||
|     const channel2 = { name: 'general2', description: 'testing' } | ||||
|     const result = await createChannels([channel1, channel2]) | ||||
|     const response = await request(application) | ||||
|       .get(`/guilds/${result.guild.id as number}/channels/`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.hasMore).toBeFalsy() | ||||
|     expect(response.body.rows.length).toEqual(3) | ||||
|     expect(response.body.rows[0].name).toEqual(channel2.name) | ||||
|     expect(response.body.rows[0].description).toEqual(channel2.description) | ||||
|     expect(response.body.rows[1].name).toEqual(channel1.name) | ||||
|     expect(response.body.rows[1].description).toEqual(channel1.description) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										146
									
								
								src/services/guilds/[guildId]/channels/__test__/post.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										146
									
								
								src/services/guilds/[guildId]/channels/__test__/post.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,146 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../../application' | ||||
| import { commonErrorsMessages } from '../../../../../tools/configurations/constants' | ||||
| import { randomString } from '../../../../../tools/utils/random' | ||||
| import { createGuild } from '../../../__test__/utils/createGuild' | ||||
| import { errorsMessages } from '../post' | ||||
|  | ||||
| describe('POST /guilds/:guildId/channels', () => { | ||||
|   it('succeeds with valid name/description', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const name = 'channel-name' | ||||
|     const description = 'testing channel creation' | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name, description }) | ||||
|       .expect(201) | ||||
|     expect(response.body.channel).not.toBeNull() | ||||
|     expect(response.body.channel.guildId).not.toBeUndefined() | ||||
|     expect(response.body.channel.name).toBe(name) | ||||
|     expect(response.body.channel.description).toBe(description) | ||||
|   }) | ||||
|  | ||||
|   it('succeeds with only channel name', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const name = 'channel-name' | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name }) | ||||
|       .expect(201) | ||||
|     expect(response.body.channel).not.toBeNull() | ||||
|     expect(response.body.channel.name).toBe(name) | ||||
|   }) | ||||
|  | ||||
|   it('succeeds with invalid slug name', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const name = 'channel name' | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name, description: 'testing' }) | ||||
|       .expect(201) | ||||
|     expect(response.body.channel).not.toBeNull() | ||||
|     expect(response.body.channel.name).toBe(name) | ||||
|   }) | ||||
|  | ||||
|   it('fails without name', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ description: 'testing channel creation' }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(3) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         errorsMessages.name.isRequired, | ||||
|         commonErrorsMessages.charactersLength('name', { min: 3, max: 30 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with invalid description', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ name: 'channel-name', description: randomString(170) }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         commonErrorsMessages.charactersLength('description', { max: 160 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the user isn't the owner", async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const name = 'channel-name' | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/channels`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ name, description: 'testing channel creation' }) | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
|  | ||||
|   it("fails if the guild does't exist", async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const name = 'channel-name' | ||||
|     const response = await request(application) | ||||
|       .post('/guilds/1/channels') | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ name, description: 'testing channel creation' }) | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										43
									
								
								src/services/guilds/[guildId]/channels/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/services/guilds/[guildId]/channels/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import Member from '../../../../models/Member' | ||||
| import { paginateModel } from '../../../../tools/database/paginateModel' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
|  | ||||
| export const getChannelsRouter = Router() | ||||
|  | ||||
| getChannelsRouter.get( | ||||
|   '/guilds/:guildId/channels', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const { itemsPerPage, page } = req.query as { | ||||
|       itemsPerPage: string | ||||
|       page: string | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const channels = await paginateModel({ | ||||
|       Model: Channel, | ||||
|       queryOptions: { itemsPerPage, page }, | ||||
|       findOptions: { | ||||
|         order: [['createdAt', 'DESC']], | ||||
|         where: { | ||||
|           guildId: member.guildId | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     return res.status(200).json(channels) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										9
									
								
								src/services/guilds/[guildId]/channels/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/services/guilds/[guildId]/channels/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { getChannelsRouter } from './get' | ||||
| import { postChannelsRouter } from './post' | ||||
|  | ||||
| export const guildsChannelsRouter = Router() | ||||
|  | ||||
| guildsChannelsRouter.use('/', getChannelsRouter) | ||||
| guildsChannelsRouter.use('/', postChannelsRouter) | ||||
							
								
								
									
										73
									
								
								src/services/guilds/[guildId]/channels/post.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/services/guilds/[guildId]/channels/post.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
| import { body } from 'express-validator' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import { validateRequest } from '../../../../tools/middlewares/validateRequest' | ||||
| import Channel from '../../../../models/Channel' | ||||
| import Member from '../../../../models/Member' | ||||
| import { commonErrorsMessages } from '../../../../tools/configurations/constants' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
| import { emitToMembers } from '../../../../tools/socket/emitEvents' | ||||
|  | ||||
| export const errorsMessages = { | ||||
|   name: { | ||||
|     isRequired: 'Name is required' | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const postChannelsRouter = Router() | ||||
|  | ||||
| postChannelsRouter.post( | ||||
|   '/guilds/:guildId/channels', | ||||
|   authenticateUser, | ||||
|   [ | ||||
|     body('name') | ||||
|       .notEmpty() | ||||
|       .withMessage(errorsMessages.name.isRequired) | ||||
|       .isString() | ||||
|       .trim() | ||||
|       .escape() | ||||
|       .isLength({ max: 30, min: 3 }) | ||||
|       .withMessage( | ||||
|         commonErrorsMessages.charactersLength('name', { max: 30, min: 3 }) | ||||
|       ), | ||||
|     body('description') | ||||
|       .optional({ nullable: true }) | ||||
|       .trim() | ||||
|       .escape() | ||||
|       .isLength({ max: 160 }) | ||||
|       .withMessage( | ||||
|         commonErrorsMessages.charactersLength('description', { max: 160 }) | ||||
|       ) | ||||
|   ], | ||||
|   validateRequest, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { name, description = '' } = req.body as { | ||||
|       name: string | ||||
|       description?: string | ||||
|     } | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId, isOwner: true } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const channel = await Channel.create({ | ||||
|       name, | ||||
|       description, | ||||
|       guildId: member.guildId | ||||
|     }) | ||||
|     await emitToMembers({ | ||||
|       event: 'channels', | ||||
|       guildId: member.guildId, | ||||
|       payload: { action: 'create', item: channel } | ||||
|     }) | ||||
|     return res.status(201).json({ channel }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										57
									
								
								src/services/guilds/[guildId]/delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								src/services/guilds/[guildId]/delete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../tools/middlewares/authenticateUser' | ||||
| import Guild from '../../../models/Guild' | ||||
| import Member from '../../../models/Member' | ||||
| import { ForbiddenError } from '../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../tools/errors/NotFoundError' | ||||
| import { guildsIconPath } from '../../../tools/configurations/constants' | ||||
| import { deleteFile, deleteMessages } from '../../../tools/utils/deleteFiles' | ||||
| import Channel from '../../../models/Channel' | ||||
| import Message from '../../../models/Message' | ||||
| import { emitToMembers } from '../../../tools/socket/emitEvents' | ||||
|  | ||||
| export const deleteByIdGuildsRouter = Router() | ||||
|  | ||||
| deleteByIdGuildsRouter.delete( | ||||
|   '/guilds/:guildId', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId, isOwner: true }, | ||||
|       include: [Guild] | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const deletedGuildId = member.guild.id | ||||
|     await emitToMembers({ | ||||
|       event: 'guilds', | ||||
|       guildId: member.guildId, | ||||
|       payload: { action: 'delete', item: member.guild } | ||||
|     }) | ||||
|     await deleteFile({ | ||||
|       basePath: guildsIconPath, | ||||
|       valueSavedInDatabase: member.guild.icon | ||||
|     }) | ||||
|     const members = await Member.findAll({ where: { guildId: deletedGuildId } }) | ||||
|     for (const member of members) { | ||||
|       await member.destroy() | ||||
|     } | ||||
|     const channels = await Channel.findAll({ | ||||
|       where: { guildId }, | ||||
|       include: [Message] | ||||
|     }) | ||||
|     for (const channel of channels) { | ||||
|       await deleteMessages(channel.messages) | ||||
|       await channel.destroy() | ||||
|     } | ||||
|     await member.guild.destroy() | ||||
|     return res.status(200).json({ deletedGuildId }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										29
									
								
								src/services/guilds/[guildId]/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src/services/guilds/[guildId]/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../tools/middlewares/authenticateUser' | ||||
| import Guild from '../../../models/Guild' | ||||
| import Member from '../../../models/Member' | ||||
| import { ForbiddenError } from '../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../tools/errors/NotFoundError' | ||||
|  | ||||
| export const getByIdGuildsRouter = Router() | ||||
|  | ||||
| getByIdGuildsRouter.get( | ||||
|   '/guilds/:guildId', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId }, | ||||
|       include: [Guild] | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     return res.status(200).json({ guild: member.guild }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										19
									
								
								src/services/guilds/[guildId]/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/services/guilds/[guildId]/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { deleteByIdGuildsRouter } from './delete' | ||||
| import { getByIdGuildsRouter } from './get' | ||||
| import { putByIdGuildsRouter } from './put' | ||||
|  | ||||
| import { guildsChannelsRouter } from './channels' | ||||
| import { guildsInvitationsRouter } from './invitations' | ||||
| import { guildsMembersRouter } from './members' | ||||
|  | ||||
| export const guildsGetByIdRouter = Router() | ||||
|  | ||||
| guildsGetByIdRouter.use('/', getByIdGuildsRouter) | ||||
| guildsGetByIdRouter.use('/', deleteByIdGuildsRouter) | ||||
| guildsGetByIdRouter.use('/', putByIdGuildsRouter) | ||||
|  | ||||
| guildsGetByIdRouter.use('/', guildsChannelsRouter) | ||||
| guildsGetByIdRouter.use('/', guildsInvitationsRouter) | ||||
| guildsGetByIdRouter.use('/', guildsMembersRouter) | ||||
							
								
								
									
										31
									
								
								src/services/guilds/[guildId]/invitations/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/services/guilds/[guildId]/invitations/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| /guilds/{guildId}/invitations: | ||||
|   get: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'invitations' | ||||
|     summary: 'GET all the invitations of a guild' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     allOf: | ||||
|       - $ref: '#/definitions/PaginateModelParameters' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   allOf: | ||||
|                     - $ref: '#/definitions/PaginateModel' | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     rows: | ||||
|                       type: 'array' | ||||
|                       items: | ||||
|                         allOf: | ||||
|                           - $ref: '#/definitions/Invitation' | ||||
							
								
								
									
										40
									
								
								src/services/guilds/[guildId]/invitations/__docs__/post.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/services/guilds/[guildId]/invitations/__docs__/post.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| /guilds/{guildId}/invitations: | ||||
|   post: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'invitations' | ||||
|     summary: 'Create an invitation' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     requestBody: | ||||
|       content: | ||||
|         application/json: | ||||
|           schema: | ||||
|             type: 'object' | ||||
|             properties: | ||||
|               value: | ||||
|                 type: 'string' | ||||
|                 minLength: 1 | ||||
|                 maxLength: 250 | ||||
|               expiresIn: | ||||
|                 type: 'integer' | ||||
|               isPublic: | ||||
|                 type: 'boolean' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - $ref: '#/definitions/NotFoundError' | ||||
|         - '201': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     invitation: | ||||
|                       allOf: | ||||
|                         - $ref: '#/definitions/Invitation' | ||||
| @@ -0,0 +1,46 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../../../../../application' | ||||
| import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../../__test__/utils/formatErrors' | ||||
| import { createInvitation } from '../../../../invitations/__test__/utils/createInvitation' | ||||
|  | ||||
| describe('GET /guilds/:guildId/invitations', () => { | ||||
|   it('should get all the invitations of the guild', async () => { | ||||
|     const value1 = 'awesome' | ||||
|     const value2 = 'awesomevalue' | ||||
|     const result = await createInvitation({ value: value1 }) | ||||
|     await createInvitation({ | ||||
|       value: value2, | ||||
|       guildId: result?.guild.id | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .get(`/guilds/${result?.guild.id as number}/invitations`) | ||||
|       .set( | ||||
|         'Authorization', | ||||
|         `${result?.user.type as string} ${result?.user.accessToken as string}` | ||||
|       ) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.hasMore).toBeFalsy() | ||||
|     expect(response.body.rows.length).toEqual(2) | ||||
|     expect(response.body.rows[0].value).toEqual(value2) | ||||
|     expect(response.body.rows[1].value).toEqual(value1) | ||||
|   }) | ||||
|  | ||||
|   it('fails if the user is not the owner', async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const result = await createInvitation() | ||||
|     const response = await request(application) | ||||
|       .get(`/guilds/${result?.guild.id as number}/invitations`) | ||||
|       .set( | ||||
|         'Authorization', | ||||
|         `${userToken.type as string} ${userToken.accessToken}` | ||||
|       ) | ||||
|       .send() | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										163
									
								
								src/services/guilds/[guildId]/invitations/__test__/post.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										163
									
								
								src/services/guilds/[guildId]/invitations/__test__/post.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,163 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser' | ||||
| import { formatErrors } from '../../../../../__test__/utils/formatErrors' | ||||
| import application from '../../../../../application' | ||||
| import { createGuild } from '../../../__test__/utils/createGuild' | ||||
| import { errorsMessages } from '../post' | ||||
| import { commonErrorsMessages } from '../../../../../tools/configurations/constants' | ||||
|  | ||||
| describe('POST /guilds/:guildId/invitations', () => { | ||||
|   it('succeeds and create the invitation', async () => { | ||||
|     const value = 'random' | ||||
|     const expiresIn = 0 | ||||
|     const isPublic = false | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value, expiresIn, isPublic }) | ||||
|       .expect(201) | ||||
|     expect(response.body.invitation.value).toEqual(value) | ||||
|     expect(response.body.invitation.expiresIn).toEqual(expiresIn) | ||||
|     expect(response.body.invitation.isPublic).toEqual(isPublic) | ||||
|   }) | ||||
|  | ||||
|   it('fails with empty value', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ expiresIn: 0 }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(3) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([ | ||||
|         errorsMessages.value.shouldNotBeEmpty, | ||||
|         errorsMessages.value.mustBeSlug, | ||||
|         commonErrorsMessages.charactersLength('value', { max: 250, min: 1 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with invalid slug value', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value: 'random value' }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([errorsMessages.value.mustBeSlug]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails with negative expiresIn', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value: 'awesome', expiresIn: -42 }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([errorsMessages.expiresIn.mustBeGreaterOrEqual]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails if the invitation slug value already exists', async () => { | ||||
|     const value = 'awesome' | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value }) | ||||
|       .expect(201) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Value already used'])) | ||||
|   }) | ||||
|  | ||||
|   it('fails with isPublic: true - if there is already a public invitation for this guild', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value: 'awesome', isPublic: true }) | ||||
|       .expect(201) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send({ value: 'awesome2', isPublic: true }) | ||||
|       .expect(400) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual( | ||||
|       expect.arrayContaining([errorsMessages.public.alreadyHasInvitation]) | ||||
|     ) | ||||
|   }) | ||||
|  | ||||
|   it('fails if the user is not the owner', async () => { | ||||
|     const userToken = await authenticateUserTest() | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .post(`/guilds/${result.guild.id as number}/invitations`) | ||||
|       .set('Authorization', `${userToken.type} ${userToken.accessToken}`) | ||||
|       .send({ value: 'value' }) | ||||
|       .expect(404) | ||||
|     const errors = formatErrors(response.body.errors) | ||||
|     expect(errors.length).toEqual(1) | ||||
|     expect(errors).toEqual(expect.arrayContaining(['Not Found'])) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										43
									
								
								src/services/guilds/[guildId]/invitations/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/services/guilds/[guildId]/invitations/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import Invitation from '../../../../models/Invitation' | ||||
| import Member from '../../../../models/Member' | ||||
| import { paginateModel } from '../../../../tools/database/paginateModel' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
|  | ||||
| export const getInvitationsRouter = Router() | ||||
|  | ||||
| getInvitationsRouter.get( | ||||
|   '/guilds/:guildId/invitations', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const { itemsPerPage, page } = req.query as { | ||||
|       itemsPerPage: string | ||||
|       page: string | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId, isOwner: true } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const invitations = await paginateModel({ | ||||
|       Model: Invitation, | ||||
|       queryOptions: { itemsPerPage, page }, | ||||
|       findOptions: { | ||||
|         order: [['createdAt', 'DESC']], | ||||
|         where: { | ||||
|           guildId: member.guildId | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     return res.status(200).json(invitations) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										9
									
								
								src/services/guilds/[guildId]/invitations/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/services/guilds/[guildId]/invitations/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { postInvitationsRouter } from './post' | ||||
| import { getInvitationsRouter } from './get' | ||||
|  | ||||
| export const guildsInvitationsRouter = Router() | ||||
|  | ||||
| guildsInvitationsRouter.use('/', postInvitationsRouter) | ||||
| guildsInvitationsRouter.use('/', getInvitationsRouter) | ||||
							
								
								
									
										89
									
								
								src/services/guilds/[guildId]/invitations/post.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								src/services/guilds/[guildId]/invitations/post.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
| import { body } from 'express-validator' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import { validateRequest } from '../../../../tools/middlewares/validateRequest' | ||||
| import Invitation from '../../../../models/Invitation' | ||||
| import Member from '../../../../models/Member' | ||||
| import { commonErrorsMessages } from '../../../../tools/configurations/constants' | ||||
| import { alreadyUsedValidation } from '../../../../tools/validations/alreadyUsedValidation' | ||||
| import { BadRequestError } from '../../../../tools/errors/BadRequestError' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
|  | ||||
| export const errorsMessages = { | ||||
|   value: { | ||||
|     mustBeSlug: 'Value must be a slug', | ||||
|     shouldNotBeEmpty: 'Value should not be empty' | ||||
|   }, | ||||
|   expiresIn: { | ||||
|     mustBeGreaterOrEqual: 'ExpiresIn must be >= 0' | ||||
|   }, | ||||
|   public: { | ||||
|     alreadyHasInvitation: 'There is already a public invitation for this guild' | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const postInvitationsRouter = Router() | ||||
|  | ||||
| postInvitationsRouter.post( | ||||
|   '/guilds/:guildId/invitations', | ||||
|   authenticateUser, | ||||
|   [ | ||||
|     body('value') | ||||
|       .notEmpty() | ||||
|       .withMessage(errorsMessages.value.shouldNotBeEmpty) | ||||
|       .trim() | ||||
|       .escape() | ||||
|       .isLength({ max: 250, min: 1 }) | ||||
|       .withMessage( | ||||
|         commonErrorsMessages.charactersLength('value', { max: 250, min: 1 }) | ||||
|       ) | ||||
|       .isSlug() | ||||
|       .withMessage(errorsMessages.value.mustBeSlug) | ||||
|       .custom(async (value: string) => { | ||||
|         return await alreadyUsedValidation(Invitation, 'value', value) | ||||
|       }), | ||||
|     body('expiresIn') | ||||
|       .optional({ nullable: true }) | ||||
|       .isInt({ min: 0 }) | ||||
|       .withMessage(errorsMessages.expiresIn.mustBeGreaterOrEqual), | ||||
|     body('isPublic').optional({ nullable: true }).isBoolean() | ||||
|   ], | ||||
|   validateRequest, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { value, expiresIn = 0, isPublic = false } = req.body as { | ||||
|       value: string | ||||
|       expiresIn?: number | ||||
|       isPublic?: boolean | ||||
|     } | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId, isOwner: true } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const foundInvitation = await Invitation.findOne({ | ||||
|       where: { isPublic: true, guildId: member.guildId } | ||||
|     }) | ||||
|     if (isPublic && foundInvitation != null) { | ||||
|       throw new BadRequestError(errorsMessages.public.alreadyHasInvitation) | ||||
|     } | ||||
|     let expiresInValue = expiresIn | ||||
|     if (expiresInValue > 0) { | ||||
|       expiresInValue += Date.now() | ||||
|     } | ||||
|     const invitation = await Invitation.create({ | ||||
|       value, | ||||
|       expiresIn, | ||||
|       isPublic, | ||||
|       guildId: member.guildId | ||||
|     }) | ||||
|     return res.status(201).json({ invitation }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										32
									
								
								src/services/guilds/[guildId]/members/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/services/guilds/[guildId]/members/__docs__/get.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| /guilds/{guildId}/members: | ||||
|   get: | ||||
|     security: | ||||
|       - bearerAuth: [] | ||||
|     tags: | ||||
|       - 'members' | ||||
|     summary: 'GET all the members of a guild' | ||||
|     parameters: | ||||
|       - name: 'guildId' | ||||
|         in: 'path' | ||||
|         required: true | ||||
|     allOf: | ||||
|       - $ref: '#/definitions/PaginateModelParameters' | ||||
|     responses: | ||||
|       allOf: | ||||
|         - $ref: '#/definitions/UnauthorizedError' | ||||
|         - $ref: '#/definitions/ForbiddenError' | ||||
|         - $ref: '#/definitions/BadRequestError' | ||||
|         - '200': | ||||
|             content: | ||||
|               application/json: | ||||
|                 schema: | ||||
|                   allOf: | ||||
|                     - $ref: '#/definitions/PaginateModel' | ||||
|                   type: 'object' | ||||
|                   properties: | ||||
|                     rows: | ||||
|                       type: 'array' | ||||
|                       items: | ||||
|                         allOf: | ||||
|                           - $ref: '#/definitions/Member' | ||||
|                           - $ref: '#/definitions/User' | ||||
							
								
								
									
										35
									
								
								src/services/guilds/[guildId]/members/__test__/get.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/services/guilds/[guildId]/members/__test__/get.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import request from 'supertest' | ||||
|  | ||||
| import application from '../../../../../application' | ||||
| import Member from '../../../../../models/Member' | ||||
| import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUser' | ||||
| import { createGuild } from '../../../__test__/utils/createGuild' | ||||
|  | ||||
| describe('GET /guilds/:guildId/members', () => { | ||||
|   it('should get all the members of a guild', async () => { | ||||
|     const result = await createGuild({ | ||||
|       guild: { description: 'description', name: 'guild' }, | ||||
|       user: { | ||||
|         email: 'test@test.com', | ||||
|         name: 'Test' | ||||
|       } | ||||
|     }) | ||||
|     const userToken = await authenticateUserTest() | ||||
|     await Member.create({ | ||||
|       userId: userToken.userId, | ||||
|       guildId: result.guild.id, | ||||
|       lastVisitedChannelId: 1 | ||||
|     }) | ||||
|     const response = await request(application) | ||||
|       .get(`/guilds/${result.guild.id as number}/members`) | ||||
|       .set('Authorization', `${result.user.type} ${result.user.accessToken}`) | ||||
|       .send() | ||||
|       .expect(200) | ||||
|     expect(response.body.hasMore).toBeFalsy() | ||||
|     expect(response.body.totalItems).toEqual(2) | ||||
|     expect(response.body.rows[0].guildId).toEqual(result.guild.id) | ||||
|     expect(response.body.rows[1].guildId).toEqual(result.guild.id) | ||||
|     expect(response.body.rows[1].user).not.toBeNull() | ||||
|     expect(response.body.rows[1].user.password).not.toBeDefined() | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										50
									
								
								src/services/guilds/[guildId]/members/get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/services/guilds/[guildId]/members/get.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { Request, Response, Router } from 'express' | ||||
|  | ||||
| import { authenticateUser } from '../../../../tools/middlewares/authenticateUser' | ||||
| import Member from '../../../../models/Member' | ||||
| import { paginateModel } from '../../../../tools/database/paginateModel' | ||||
| import { ForbiddenError } from '../../../../tools/errors/ForbiddenError' | ||||
| import { NotFoundError } from '../../../../tools/errors/NotFoundError' | ||||
|  | ||||
| export const getMembersRouter = Router() | ||||
|  | ||||
| getMembersRouter.get( | ||||
|   '/guilds/:guildId/members', | ||||
|   authenticateUser, | ||||
|   async (req: Request, res: Response) => { | ||||
|     if (req.user == null) { | ||||
|       throw new ForbiddenError() | ||||
|     } | ||||
|     const { itemsPerPage, page } = req.query as { | ||||
|       itemsPerPage: string | ||||
|       page: string | ||||
|     } | ||||
|     const user = req.user.current | ||||
|     const { guildId } = req.params as { guildId: string } | ||||
|     const member = await Member.findOne({ | ||||
|       where: { userId: user.id, guildId } | ||||
|     }) | ||||
|     if (member == null) { | ||||
|       throw new NotFoundError() | ||||
|     } | ||||
|     const result = await paginateModel({ | ||||
|       Model: Member, | ||||
|       queryOptions: { itemsPerPage, page }, | ||||
|       findOptions: { | ||||
|         order: [['createdAt', 'DESC']], | ||||
|         where: { | ||||
|           guildId: member.guildId | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|     return res.status(200).json({ | ||||
|       hasMore: result.hasMore, | ||||
|       totalItems: result.totalItems, | ||||
|       itemsPerPage: result.itemsPerPage, | ||||
|       page: result.page, | ||||
|       rows: result.rows.map((row) => { | ||||
|         return { ...row.toJSON(), user: user.toJSON() } | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
							
								
								
									
										7
									
								
								src/services/guilds/[guildId]/members/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/services/guilds/[guildId]/members/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { getMembersRouter } from './get' | ||||
|  | ||||
| export const guildsMembersRouter = Router() | ||||
|  | ||||
| guildsMembersRouter.use('/', getMembersRouter) | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user