feat: migrate from express to fastify
This commit is contained in:
		
							
								
								
									
										4
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.163.1/containers/javascript-node/.devcontainer/base.Dockerfile | ||||
|  | ||||
| ARG VARIANT="16-bullseye" | ||||
| FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} | ||||
							
								
								
									
										21
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "name": "@thream/api", | ||||
|   "dockerComposeFile": "./docker-compose.yml", | ||||
|   "service": "workspace", | ||||
|   "workspaceFolder": "/workspace", | ||||
|   "settings": { | ||||
|     "remote.autoForwardPorts": false | ||||
|   }, | ||||
|   "extensions": [ | ||||
|     "editorconfig.editorconfig", | ||||
|     "esbenp.prettier-vscode", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "davidanson.vscode-markdownlint", | ||||
|     "prisma.prisma", | ||||
|     "mikestead.dotenv", | ||||
|     "ms-azuretools.vscode-docker" | ||||
|   ], | ||||
|   "forwardPorts": [8080, 5555, 5432, 1080], | ||||
|   "postAttachCommand": ["npm", "install"], | ||||
|   "remoteUser": "node" | ||||
| } | ||||
							
								
								
									
										28
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.devcontainer/docker-compose.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| version: '3.0' | ||||
|  | ||||
| services: | ||||
|   workspace: | ||||
|     build: | ||||
|       context: './' | ||||
|       dockerfile: './Dockerfile' | ||||
|     volumes: | ||||
|       - '..:/workspace:cached' | ||||
|     command: 'sleep infinity' | ||||
|  | ||||
|   thream-database: | ||||
|     image: 'postgres:14.0' | ||||
|     environment: | ||||
|       POSTGRES_USER: 'user' | ||||
|       POSTGRES_PASSWORD: 'password' | ||||
|       POSTGRES_DB: 'thream' | ||||
|     volumes: | ||||
|       - 'postgres-data:/var/lib/postgresql/data' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
|   thream-maildev: | ||||
|     image: 'maildev/maildev:1.1.0' | ||||
|     ports: | ||||
|       - '1080:80' | ||||
|  | ||||
| volumes: | ||||
|   postgres-data: | ||||
| @@ -1,8 +1,8 @@ | ||||
| .vscode | ||||
| .git | ||||
| .env | ||||
| build | ||||
| coverage | ||||
| node_modules | ||||
| tmp | ||||
| temp | ||||
| **/__test__/** | ||||
|   | ||||
							
								
								
									
										39
									
								
								.env.example
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								.env.example
									
									
									
									
									
								
							| @@ -1,22 +1,17 @@ | ||||
| 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 | ||||
| COMPOSE_PROJECT_NAME='thream-api' | ||||
| HOST='0.0.0.0' | ||||
| PORT='8080' | ||||
| DATABASE_URL='postgresql://user:password@thream-database:5432/thream' | ||||
| 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' | ||||
|   | ||||
							
								
								
									
										5
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| build | ||||
| node_modules | ||||
| coverage | ||||
| package.json | ||||
| package-lock.json | ||||
							
								
								
									
										16
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| { | ||||
|   "extends": ["standard-with-typescript", "eslint-config-prettier"], | ||||
|   "plugins": ["unicorn", "eslint-plugin-prettier"], | ||||
|   "parserOptions": { | ||||
|     "project": "./tsconfig.json" | ||||
|   }, | ||||
|   "env": { | ||||
|     "node": true, | ||||
|     "jest": true | ||||
|   }, | ||||
|   "rules": { | ||||
|     "prettier/prettier": "error", | ||||
|     "unicorn/prefer-node-protocol": "error", | ||||
|     "unicorn/prevent-abbreviations": "error" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										5
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,12 +13,13 @@ jobs: | ||||
|       - uses: 'actions/checkout@v2' | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.1.5' | ||||
|         uses: 'actions/setup-node@v2.4.1' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|           cache: 'npm' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm ci --cache .npm --prefer-offline' | ||||
|         run: 'npm install' | ||||
|  | ||||
|       - name: 'Build' | ||||
|         run: 'npm run build' | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,15 +13,21 @@ jobs: | ||||
|       - uses: 'actions/checkout@v2' | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.1.5' | ||||
|         uses: 'actions/setup-node@v2.4.1' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|           cache: 'npm' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm ci --cache .npm --prefer-offline' | ||||
|         run: 'npm install' | ||||
|  | ||||
|       - 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' | ||||
|  | ||||
|       - name: 'dotenv-linter' | ||||
|         uses: 'dotenv-linter/action-dotenv-linter@v2' | ||||
|         with: | ||||
|           github_token: ${{ secrets.github_token }} | ||||
|   | ||||
							
								
								
									
										45
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| name: 'Release' | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [master] | ||||
|  | ||||
| jobs: | ||||
|   release: | ||||
|     runs-on: 'ubuntu-latest' | ||||
|     steps: | ||||
|       - uses: 'actions/checkout@v2.3.4' | ||||
|         with: | ||||
|           fetch-depth: 0 | ||||
|           persist-credentials: false | ||||
|  | ||||
|       - name: 'Import GPG key' | ||||
|         uses: 'crazy-max/ghaction-import-gpg@v3.2.0' | ||||
|         with: | ||||
|           gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} | ||||
|           git-user-signingkey: true | ||||
|           git-commit-gpgsign: true | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.4.1' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|           cache: 'npm' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm install' | ||||
|  | ||||
|       - name: 'Build' | ||||
|         run: 'npm run build' | ||||
|  | ||||
|       - name: 'Production migration' | ||||
|         run: 'npm run prisma:migrate:deploy' | ||||
|         env: | ||||
|           DATABASE_URL: ${{ secrets.DATABASE_URL }} | ||||
|  | ||||
|       - name: 'Release' | ||||
|         run: 'npm run release' | ||||
|         env: | ||||
|           GH_TOKEN: ${{ secrets.GH_TOKEN }} | ||||
|           GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} | ||||
|           GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }} | ||||
							
								
								
									
										5
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/test.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,12 +13,13 @@ jobs: | ||||
|       - uses: 'actions/checkout@v2' | ||||
|  | ||||
|       - name: 'Use Node.js' | ||||
|         uses: 'actions/setup-node@v2.1.5' | ||||
|         uses: 'actions/setup-node@v2.4.1' | ||||
|         with: | ||||
|           node-version: '16.x' | ||||
|           cache: 'npm' | ||||
|  | ||||
|       - name: 'Install' | ||||
|         run: 'npm ci --cache .npm --prefer-offline' | ||||
|         run: 'npm install' | ||||
|  | ||||
|       - name: 'Test' | ||||
|         run: 'npm run test' | ||||
|   | ||||
							
								
								
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -15,12 +15,22 @@ coverage | ||||
| # debug | ||||
| npm-debug.log* | ||||
|  | ||||
| # editors | ||||
| .vscode | ||||
| .theia | ||||
| .idea | ||||
| # IDEs and editors | ||||
| /.idea | ||||
| .project | ||||
| .classpath | ||||
| .c9/ | ||||
| *.launch | ||||
| .settings/ | ||||
| *.sublime-workspace | ||||
|  | ||||
| # IDE - VSCode | ||||
| .vscode/* | ||||
| !.vscode/settings.json | ||||
| !.vscode/tasks.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/extensions.json | ||||
|  | ||||
| # misc | ||||
| .DS_Store | ||||
| tmp | ||||
| temp | ||||
| uploads | ||||
|   | ||||
							
								
								
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +0,0 @@ | ||||
| _ | ||||
| @@ -1,8 +1,5 @@ | ||||
| #!/bin/sh | ||||
| . "$(dirname "$0")/_/husky.sh" | ||||
|  | ||||
| npm run lint:docker | ||||
| npm run lint:editorconfig | ||||
| npm run lint:markdown | ||||
| npm run lint:typescript | ||||
| npm run lint:staged | ||||
| npm run build | ||||
|   | ||||
							
								
								
									
										11
									
								
								.lintstagedrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.lintstagedrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "*": ["editorconfig-checker"], | ||||
|   "*.{js,ts,jsx,tsx}": [ | ||||
|     "prettier --write", | ||||
|     "eslint --fix", | ||||
|     "jest --findRelatedTests" | ||||
|   ], | ||||
|   "*.{yml,json}": ["prettier --write"], | ||||
|   "*.{md}": ["prettier --write", "markdownlint --dot --fix"], | ||||
|   "./Dockerfile": ["dockerfilelint"] | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| { | ||||
|   "default": true, | ||||
|   "MD013": false, | ||||
|   "MD024": false, | ||||
|   "MD033": false, | ||||
|   "MD041": false | ||||
| } | ||||
|   | ||||
							
								
								
									
										5
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| build | ||||
| node_modules | ||||
| coverage | ||||
| package.json | ||||
| package-lock.json | ||||
							
								
								
									
										6
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| { | ||||
|   "singleQuote": true, | ||||
|   "jsxSingleQuote": true, | ||||
|   "semi": false, | ||||
|   "trailingComma": "none" | ||||
| } | ||||
							
								
								
									
										37
									
								
								.releaserc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.releaserc.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| { | ||||
|   "branches": ["master"], | ||||
|   "plugins": [ | ||||
|     [ | ||||
|       "@semantic-release/commit-analyzer", | ||||
|       { | ||||
|         "preset": "conventionalcommits" | ||||
|       } | ||||
|     ], | ||||
|     [ | ||||
|       "@semantic-release/release-notes-generator", | ||||
|       { | ||||
|         "preset": "conventionalcommits" | ||||
|       } | ||||
|     ], | ||||
|     [ | ||||
|       "@semantic-release/npm", | ||||
|       { | ||||
|         "npmPublish": false | ||||
|       } | ||||
|     ], | ||||
|     [ | ||||
|       "@semantic-release/git", | ||||
|       { | ||||
|         "assets": ["package.json", "package-lock.json"], | ||||
|         "message": "chore(release): ${nextRelease.version} [skip ci]" | ||||
|       } | ||||
|     ], | ||||
|     "@semantic-release/github", | ||||
|     [ | ||||
|       "@saithodev/semantic-release-backmerge", | ||||
|       { | ||||
|         "backmergeStrategy": "merge" | ||||
|       } | ||||
|     ] | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										11
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.vscode/extensions.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "recommendations": [ | ||||
|     "editorconfig.editorconfig", | ||||
|     "esbenp.prettier-vscode", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "davidanson.vscode-markdownlint", | ||||
|     "prisma.prisma", | ||||
|     "mikestead.dotenv", | ||||
|     "ms-azuretools.vscode-docker" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										35
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| { | ||||
|   "typescript.tsdk": "node_modules/typescript/lib", | ||||
|   "prettier.configPath": ".prettierrc.json", | ||||
|   "editor.formatOnSave": true, | ||||
|   "editor.codeActionsOnSave": { | ||||
|     "source.fixAll": true | ||||
|   }, | ||||
|   "[markdown]": { | ||||
|     "editor.autoClosingBrackets": "always", | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "[yaml]": { | ||||
|     "editor.autoClosingBrackets": "always", | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "[json]": { | ||||
|     "editor.autoClosingBrackets": "always", | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "[jsonc]": { | ||||
|     "editor.autoClosingBrackets": "always", | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "[typescript]": { | ||||
|     "editor.autoClosingBrackets": "always", | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "[javascript]": { | ||||
|     "editor.autoClosingBrackets": "always", | ||||
|     "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|   }, | ||||
|   "[prisma]": { | ||||
|     "editor.defaultFormatter": "Prisma.prisma" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										132
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								CODE_OF_CONDUCT.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | ||||
| # Contributor Covenant Code of Conduct | ||||
|  | ||||
| ## Our Pledge | ||||
|  | ||||
| We as members, contributors, and leaders pledge to make participation in our | ||||
| community a harassment-free experience for everyone, regardless of age, body | ||||
| size, visible or invisible disability, ethnicity, sex characteristics, gender | ||||
| identity and expression, level of experience, education, socio-economic status, | ||||
| nationality, personal appearance, race, religion, or sexual identity | ||||
| and orientation. | ||||
|  | ||||
| We pledge to act and interact in ways that contribute to an open, welcoming, | ||||
| diverse, inclusive, and healthy community. | ||||
|  | ||||
| ## Our Standards | ||||
|  | ||||
| Examples of behavior that contributes to a positive environment for our | ||||
| community include: | ||||
|  | ||||
| - Demonstrating empathy and kindness toward other people | ||||
| - Being respectful of differing opinions, viewpoints, and experiences | ||||
| - Giving and gracefully accepting constructive feedback | ||||
| - Accepting responsibility and apologizing to those affected by our mistakes, | ||||
|   and learning from the experience | ||||
| - Focusing on what is best not just for us as individuals, but for the | ||||
|   overall community | ||||
|  | ||||
| Examples of unacceptable behavior include: | ||||
|  | ||||
| - The use of sexualized language or imagery, and sexual attention or | ||||
|   advances of any kind | ||||
| - Trolling, insulting or derogatory comments, and personal or political attacks | ||||
| - Public or private harassment | ||||
| - Publishing others' private information, such as a physical or email | ||||
|   address, without their explicit permission | ||||
| - Other conduct which could reasonably be considered inappropriate in a | ||||
|   professional setting | ||||
|  | ||||
| ## Enforcement Responsibilities | ||||
|  | ||||
| Community leaders are responsible for clarifying and enforcing our standards of | ||||
| acceptable behavior and will take appropriate and fair corrective action in | ||||
| response to any behavior that they deem inappropriate, threatening, offensive, | ||||
| or harmful. | ||||
|  | ||||
| Community leaders have the right and responsibility to remove, edit, or reject | ||||
| comments, commits, code, wiki edits, issues, and other contributions that are | ||||
| not aligned to this Code of Conduct, and will communicate reasons for moderation | ||||
| decisions when appropriate. | ||||
|  | ||||
| ## Scope | ||||
|  | ||||
| This Code of Conduct applies within all community spaces, and also applies when | ||||
| an individual is officially representing the community in public spaces. | ||||
| Examples of representing our community include using an official e-mail address, | ||||
| posting via an official social media account, or acting as an appointed | ||||
| representative at an online or offline event. | ||||
|  | ||||
| ## Enforcement | ||||
|  | ||||
| Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||
| reported to the community leaders responsible for enforcement at | ||||
| contact@divlo.fr. | ||||
| All complaints will be reviewed and investigated promptly and fairly. | ||||
|  | ||||
| All community leaders are obligated to respect the privacy and security of the | ||||
| reporter of any incident. | ||||
|  | ||||
| ## Enforcement Guidelines | ||||
|  | ||||
| Community leaders will follow these Community Impact Guidelines in determining | ||||
| the consequences for any action they deem in violation of this Code of Conduct: | ||||
|  | ||||
| ### 1. Correction | ||||
|  | ||||
| **Community Impact**: Use of inappropriate language or other behavior deemed | ||||
| unprofessional or unwelcome in the community. | ||||
|  | ||||
| **Consequence**: A private, written warning from community leaders, providing | ||||
| clarity around the nature of the violation and an explanation of why the | ||||
| behavior was inappropriate. A public apology may be requested. | ||||
|  | ||||
| ### 2. Warning | ||||
|  | ||||
| **Community Impact**: A violation through a single incident or series | ||||
| of actions. | ||||
|  | ||||
| **Consequence**: A warning with consequences for continued behavior. No | ||||
| interaction with the people involved, including unsolicited interaction with | ||||
| those enforcing the Code of Conduct, for a specified period of time. This | ||||
| includes avoiding interactions in community spaces as well as external channels | ||||
| like social media. Violating these terms may lead to a temporary or | ||||
| permanent ban. | ||||
|  | ||||
| ### 3. Temporary Ban | ||||
|  | ||||
| **Community Impact**: A serious violation of community standards, including | ||||
| sustained inappropriate behavior. | ||||
|  | ||||
| **Consequence**: A temporary ban from any sort of interaction or public | ||||
| communication with the community for a specified period of time. No public or | ||||
| private interaction with the people involved, including unsolicited interaction | ||||
| with those enforcing the Code of Conduct, is allowed during this period. | ||||
| Violating these terms may lead to a permanent ban. | ||||
|  | ||||
| ### 4. Permanent Ban | ||||
|  | ||||
| **Community Impact**: Demonstrating a pattern of violation of community | ||||
| standards, including sustained inappropriate behavior, harassment of an | ||||
| individual, or aggression toward or disparagement of classes of individuals. | ||||
|  | ||||
| **Consequence**: A permanent ban from any sort of public interaction within | ||||
| the community. | ||||
|  | ||||
| ## Attribution | ||||
|  | ||||
| This Code of Conduct is adapted from the [Contributor Covenant][homepage], | ||||
| version 2.0, available at | ||||
| [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. | ||||
|  | ||||
| Community Impact Guidelines were inspired by | ||||
| [Mozilla's code of conduct enforcement ladder][mozilla coc]. | ||||
|  | ||||
| For answers to common questions about this code of conduct, see the FAQ at | ||||
| [https://www.contributor-covenant.org/faq][faq]. Translations are available | ||||
| at [https://www.contributor-covenant.org/translations][translations]. | ||||
|  | ||||
| [homepage]: https://www.contributor-covenant.org | ||||
| [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html | ||||
| [mozilla coc]: https://github.com/mozilla/diversity | ||||
| [faq]: https://www.contributor-covenant.org/faq | ||||
| [translations]: https://www.contributor-covenant.org/translations | ||||
| @@ -4,7 +4,7 @@ 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. | ||||
| **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](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. | ||||
|  | ||||
| ## Open Development | ||||
|  | ||||
| @@ -14,14 +14,14 @@ All work on **Thream/api** happens directly on [GitHub](https://github.com/Threa | ||||
|  | ||||
| - Reporting a bug. | ||||
| - Suggest a new feature idea. | ||||
| - Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...). | ||||
| - Correct spelling errors, improvements or additions to documentation files. | ||||
| - 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. | ||||
| - **Please first discuss** the change you wish to make via issues. | ||||
|  | ||||
| - Ensure your code respect [Typescript Standard Style](https://www.npmjs.com/package/ts-standard). | ||||
| - Ensure your code respect `eslint` and `prettier`. | ||||
|  | ||||
| - Make sure your **code passes the tests**. | ||||
|  | ||||
| @@ -29,7 +29,9 @@ 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. | ||||
| 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 | ||||
|  | ||||
| @@ -56,17 +58,16 @@ Scopes define what part of the code changed. | ||||
| ### Examples | ||||
|  | ||||
| ```sh | ||||
| git commit -m "feat(users): add POST /users/signup" | ||||
| git commit -m "feat(services): add POST /users/signup" | ||||
| git commit -m "docs(readme): update installation process" | ||||
| git commit -m "fix(messages): should emit events to connected users" | ||||
| git commit -m "fix(services): should emit events to connected users" | ||||
| ``` | ||||
|  | ||||
| ## Directory Structure | ||||
|  | ||||
| ```text | ||||
| ├── email | ||||
| ├── public | ||||
| ├── scripts | ||||
| ├── prisma | ||||
| └── src | ||||
|     ├── models | ||||
|     ├── services | ||||
| @@ -77,8 +78,9 @@ git commit -m "fix(messages): should emit events to connected users" | ||||
| ### Each folder explained | ||||
|  | ||||
| - `email` : email template(s) and translation(s) | ||||
| - `prisma` : contains the prisma schema and migrations | ||||
| - `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) | ||||
|   - `models` : models that represent tables in database as JSON schema | ||||
|   - `services` : all REST API endpoints | ||||
|   - `tools` : configs and utilities | ||||
|   - `typings` : types gloablly used in the project | ||||
| @@ -94,14 +96,9 @@ Here is what potentially look like a folder structure for this service : | ||||
| └── src | ||||
|     └── services | ||||
|         └── channels | ||||
|             ├── __docs__ | ||||
|             │   └── get.yaml | ||||
|             ├── __test__ | ||||
|             │   └── get.test.ts | ||||
|             ├── [channelId] | ||||
|             │   ├── __docs__ | ||||
|             │   │   ├── delete.yaml | ||||
|             │   │   └── put.yaml | ||||
|             │   ├── __test__ | ||||
|             │   │   ├── delete.test.ts | ||||
|             │   │   └── put.test.ts | ||||
| @@ -118,6 +115,7 @@ This folder structure will map to these REST API routes : | ||||
| - 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 folders after `src/services` : is the real path of the routes in the API except | ||||
| folders starting and ending with `__` like `__test__` or `__utils__`. | ||||
|  | ||||
| The filenames correspond to the HTTP methods used (`get`, `post`, `put`, `delete`). | ||||
|   | ||||
							
								
								
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							| @@ -1,11 +1,22 @@ | ||||
| FROM node:14.16.1 | ||||
| RUN npm install --global npm@7 | ||||
|  | ||||
| WORKDIR /api | ||||
|  | ||||
| FROM node:16.11.0 AS dependencies | ||||
| WORKDIR /usr/src/app | ||||
| COPY ./package*.json ./ | ||||
| RUN npm install | ||||
| RUN npm clean-install | ||||
|  | ||||
| FROM node:16.11.0 AS builder | ||||
| WORKDIR /usr/src/app | ||||
| COPY --from=dependencies /usr/src/app/node_modules ./node_modules | ||||
| COPY ./ ./ | ||||
| RUN npx prisma generate | ||||
| RUN npm run build | ||||
|  | ||||
| CMD ["npm", "run", "dev"] | ||||
| FROM node:16.11.0 AS runner | ||||
| WORKDIR /usr/src/app | ||||
| ENV NODE_ENV=production | ||||
| COPY --from=builder /usr/src/app/node_modules ./node_modules | ||||
| COPY --from=builder /usr/src/app/email ./email | ||||
| COPY --from=builder /usr/src/app/build ./build | ||||
| COPY --from=builder /usr/src/app/prisma ./prisma | ||||
| COPY --from=builder /usr/src/app/uploads ./uploads | ||||
| USER node | ||||
| CMD npm run prisma:migrate:deploy && node build/index.js | ||||
|   | ||||
| @@ -1,27 +0,0 @@ | ||||
| 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"] | ||||
							
								
								
									
										61
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,8 +1,4 @@ | ||||
| <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> | ||||
| <h1 align="center"><a href="https://api.thream.divlo.fr/documentation">Thream/api</a></h1> | ||||
|  | ||||
| <p align="center"> | ||||
|   <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a> | ||||
| @@ -14,8 +10,8 @@ | ||||
|   <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://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a> | ||||
|   <a href="https://dependabot.com/"><img src="https://badgen.net/github/dependabot/Thream/api?icon=dependabot" alt="Dependabot badge" /></a> | ||||
| </p> | ||||
|  | ||||
| @@ -29,9 +25,9 @@ This project was bootstrapped with [create-fullstack-app](https://github.com/Div | ||||
|  | ||||
| ### Prerequisites | ||||
|  | ||||
| - [Node.js](https://nodejs.org/) >= 14 | ||||
| - [npm](https://www.npmjs.com/) >= 6 | ||||
| - [MySQL](https://www.mysql.com/) >= 8 | ||||
| - [Node.js](https://nodejs.org/) >= 16.0.0 | ||||
| - [npm](https://www.npmjs.com/) >= 8.0.0 | ||||
| - [PostgreSQL](https://www.postgresql.org/) | ||||
|  | ||||
| ### Installation | ||||
|  | ||||
| @@ -45,41 +41,60 @@ cd api | ||||
| # Configure environment variables | ||||
| cp .env.example .env | ||||
|  | ||||
| # Install dependencies | ||||
| # Install | ||||
| npm install | ||||
| ``` | ||||
|  | ||||
| You will need to configure the environment variables by creating an `.env` file at the root of the project (see `.env.example`). | ||||
| 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/) | ||||
| ### Local Development environment | ||||
|  | ||||
| #### Setup the database | ||||
|  | ||||
| ```sh | ||||
| # Setup and run all the services for you | ||||
| docker-compose up | ||||
| # Create a new user and database | ||||
| psql | ||||
| create database thream_database; | ||||
| create user thream_user with encrypted password 'password'; | ||||
| ALTER USER thream_user WITH SUPERUSER; | ||||
| ``` | ||||
|  | ||||
| Replace `DATABASE_URL` inside `.env` with `postgresql://thream_user:password@localhost:5432/thream_database` | ||||
|  | ||||
| ```sh | ||||
| # Run Prisma migrations | ||||
| npm run prisma:migrate:dev | ||||
| ``` | ||||
|  | ||||
| #### Usage | ||||
|  | ||||
| ```sh | ||||
| # Run API | ||||
| npm run dev | ||||
|  | ||||
| # Run Prisma Studio | ||||
| npm run prisma:studio | ||||
| ``` | ||||
|  | ||||
| ### 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 | ||||
| docker-compose up --build | ||||
| ``` | ||||
|  | ||||
| #### 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` | ||||
| - [PostgreSQL database](https://www.postgresql.org/) | ||||
|  | ||||
| ## 💡 Contributing | ||||
|  | ||||
| Anyone can help to improve the project, submit a Feature Request, a bug report or even correct a simple spelling mistake. | ||||
| 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. | ||||
| The steps to contribute can be found in [CONTRIBUTING.md](./CONTRIBUTING.md). | ||||
|  | ||||
| ## 📄 License | ||||
|  | ||||
|   | ||||
| @@ -1,34 +0,0 @@ | ||||
| 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: | ||||
| @@ -4,48 +4,25 @@ services: | ||||
|     container_name: ${COMPOSE_PROJECT_NAME} | ||||
|     build: | ||||
|       context: './' | ||||
|     env_file: | ||||
|       - '.env' | ||||
|     ports: | ||||
|       - '${PORT}:${PORT}' | ||||
|     depends_on: | ||||
|       - ${DATABASE_HOST} | ||||
|       - 'thream-maildev' | ||||
|       - 'thream-database' | ||||
|     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} | ||||
|       - './uploads:/usr/src/app/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' | ||||
|     container_name: 'thream-database' | ||||
|     image: 'postgres:14.0' | ||||
|     environment: | ||||
|       MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD} | ||||
|       MYSQL_DATABASE: ${DATABASE_NAME} | ||||
|       MYSQL_TCP_PORT: ${DATABASE_PORT} | ||||
|     ports: | ||||
|       - '${DATABASE_PORT}:${DATABASE_PORT}' | ||||
|       POSTGRES_USER: 'user' | ||||
|       POSTGRES_PASSWORD: 'password' | ||||
|       POSTGRES_DB: 'thream' | ||||
|     volumes: | ||||
|       - 'database-volume:/var/lib/mysql' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
|   thream-maildev: | ||||
|     container_name: 'thream-maildev' | ||||
|     image: 'maildev/maildev:1.1.0' | ||||
|     ports: | ||||
|       - '1080:80' | ||||
|       - 'database-volume:/var/lib/postgresql/data' | ||||
|     restart: 'unless-stopped' | ||||
|  | ||||
| volumes: | ||||
|   | ||||
							
								
								
									
										53
									
								
								generators/service/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								generators/service/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| /** @type {import('node-plop').PlopGeneratorConfig} */ | ||||
| exports.serviceGenerator = { | ||||
|   description: 'REST API endpoint', | ||||
|   prompts: [ | ||||
|     { | ||||
|       type: 'input', | ||||
|       name: 'url', | ||||
|       message: 'url' | ||||
|     }, | ||||
|     { | ||||
|       type: 'list', | ||||
|       name: 'httpMethod', | ||||
|       message: 'httpMethod', | ||||
|       choices: ['GET', 'POST', 'PUT', 'DELETE'] | ||||
|     }, | ||||
|     { | ||||
|       type: 'input', | ||||
|       name: 'description', | ||||
|       message: 'description' | ||||
|     }, | ||||
|     { | ||||
|       type: 'list', | ||||
|       name: 'tag', | ||||
|       message: 'tag', | ||||
|       choices: [ | ||||
|         'users', | ||||
|         'guilds', | ||||
|         'channels', | ||||
|         'invitations', | ||||
|         'messages', | ||||
|         'members', | ||||
|         'uploads' | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       type: 'confirm', | ||||
|       name: 'shouldBeAuthenticated', | ||||
|       message: 'shouldBeAuthenticated' | ||||
|     } | ||||
|   ], | ||||
|   actions: [ | ||||
|     { | ||||
|       type: 'add', | ||||
|       path: 'src/services/{{url}}/{{lowerCase httpMethod}}.ts', | ||||
|       templateFile: 'generators/service/service.ts.hbs' | ||||
|     }, | ||||
|     { | ||||
|       type: 'add', | ||||
|       path: 'src/services/{{url}}/__test__/{{lowerCase httpMethod}}.test.ts', | ||||
|       templateFile: 'generators/service/service.test.ts.hbs' | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										26
									
								
								generators/service/service.test.ts.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								generators/service/service.test.ts.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import { application } from 'application.js' | ||||
| {{#if shouldBeAuthenticated}} | ||||
| import { authenticateUserTest } from '__test__/utils/authenticateUserTest.js' | ||||
| {{/if}} | ||||
| import { prismaMock } from '__test__/setup.js' | ||||
|  | ||||
| describe('{{httpMethod}} {{url}}', () => { | ||||
|   it('succeeds', async () => { | ||||
|     // prismaMock.service.findUnique.mockResolvedValue(null) | ||||
|     {{#if shouldBeAuthenticated}} | ||||
|     const { accessToken, user } = await authenticateUserTest() | ||||
|     {{/if}} | ||||
|     const response = await application.inject({ | ||||
|       method: '{{httpMethod}}', | ||||
|       url: '{{url}}', | ||||
|       {{#if shouldBeAuthenticated}} | ||||
|       headers: { | ||||
|         authorization: `Bearer ${accessToken}` | ||||
|       }, | ||||
|       {{/if}} | ||||
|       payload: {} | ||||
|     }) | ||||
|     // const responseJson = response.json() | ||||
|     expect(response.statusCode).toEqual(200) | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										59
									
								
								generators/service/service.ts.hbs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								generators/service/service.ts.hbs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
| import { FastifyPluginAsync, FastifySchema } from 'fastify' | ||||
|  | ||||
| import prisma from 'tools/database/prisma.js' | ||||
| import { fastifyErrors } from 'models/utils.js' | ||||
| {{#if shouldBeAuthenticated}} | ||||
| import authenticateUser from 'tools/plugins/authenticateUser.js' | ||||
| {{/if}} | ||||
|  | ||||
| const body{{sentenceCase httpMethod}}ServiceSchema = Type.Object({ | ||||
|   property: Type.String() | ||||
| }) | ||||
|  | ||||
| type Body{{sentenceCase httpMethod}}ServiceSchemaType = Static<typeof body{{sentenceCase httpMethod}}ServiceSchema> | ||||
|  | ||||
| const {{lowerCase httpMethod}}ServiceSchema: FastifySchema = { | ||||
|   description: '{{description}}', | ||||
|   tags: ['{{tag}}'] as string[], | ||||
|   {{#if shouldBeAuthenticated}} | ||||
|   security: [ | ||||
|     { | ||||
|       bearerAuth: [] | ||||
|     } | ||||
|   ] as Array<{ [key: string]: [] }>, | ||||
|   {{/if}} | ||||
|   body: body{{sentenceCase httpMethod}}ServiceSchema, | ||||
|   response: { | ||||
|     200: Type.Object({}), | ||||
|     400: fastifyErrors[400], | ||||
|     {{#if shouldBeAuthenticated}} | ||||
|     401: fastifyErrors[401], | ||||
|     403: fastifyErrors[403], | ||||
|     {{/if}} | ||||
|     500: fastifyErrors[500] | ||||
|   } | ||||
| } as const | ||||
|  | ||||
| export const {{lowerCase httpMethod}}Service: FastifyPluginAsync = async (fastify) => { | ||||
|   {{#if shouldBeAuthenticated}} | ||||
|   await fastify.register(authenticateUser) | ||||
|  | ||||
|   {{/if}} | ||||
|   fastify.route<{ | ||||
|     Body: Body{{sentenceCase httpMethod}}ServiceSchemaType | ||||
|   }>({ | ||||
|     method: '{{httpMethod}}', | ||||
|     url: '{{url}}', | ||||
|     schema: {{lowerCase httpMethod}}ServiceSchema, | ||||
|     handler: async (request, reply) => { | ||||
|       {{#if shouldBeAuthenticated}} | ||||
|       if (request.user == null) { | ||||
|         throw fastify.httpErrors.forbidden() | ||||
|       } | ||||
|       {{/if}} | ||||
|       reply.statusCode = 200 | ||||
|       return {} | ||||
|     } | ||||
|   }) | ||||
| } | ||||
							
								
								
									
										11
									
								
								jest.config.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								jest.config.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| { | ||||
|   "preset": "ts-jest", | ||||
|   "testEnvironment": "node", | ||||
|   "resolver": "jest-ts-webcompat-resolver", | ||||
|   "setupFiles": ["./__test__/setEnvironmentsVariables.ts"], | ||||
|   "setupFilesAfterEnv": ["./__test__/setup.ts"], | ||||
|   "rootDir": "./src", | ||||
|   "collectCoverage": true, | ||||
|   "coverageDirectory": "../coverage/", | ||||
|   "coverageReporters": ["text", "cobertura"] | ||||
| } | ||||
							
								
								
									
										29145
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										29145
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										188
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										188
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,146 +1,98 @@ | ||||
| { | ||||
|   "name": "@thream/api", | ||||
|   "version": "0.0.0-development", | ||||
|   "version": "0.0.1", | ||||
|   "description": "Thream's application programming interface to stay close with your friends and communities.", | ||||
|   "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" | ||||
|       } | ||||
|     } | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "https://github.com/Thream/api" | ||||
|   }, | ||||
|   "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" | ||||
|   "engines": { | ||||
|     "node": ">=16.0.0", | ||||
|     "npm": ">=8.0.0" | ||||
|   }, | ||||
|   "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\"", | ||||
|     "dev": "concurrently -k -n \"TypeScript,Node\" -p \"[{name}]\" -c \"blue,green\" \"tsc --watch\" \"cross-env NODE_ENV=development nodemon -e js,json,yaml build/index.js\"", | ||||
|     "generate": "plop", | ||||
|     "lint:commit": "commitlint", | ||||
|     "lint:docker": "dockerfilelint './Dockerfile' && dockerfilelint './Dockerfile.production'", | ||||
|     "lint:docker": "dockerfilelint './Dockerfile'", | ||||
|     "lint:editorconfig": "editorconfig-checker", | ||||
|     "lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules", | ||||
|     "lint:typescript": "ts-standard", | ||||
|     "release": "release-it", | ||||
|     "lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'", | ||||
|     "lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'", | ||||
|     "lint:staged": "lint-staged", | ||||
|     "test": "jest", | ||||
|     "prisma:generate": "prisma generate", | ||||
|     "prisma:studio": "prisma studio", | ||||
|     "prisma:migrate:dev": "prisma migrate dev", | ||||
|     "prisma:migrate:deploy": "prisma migrate deploy", | ||||
|     "release": "semantic-release", | ||||
|     "postinstall": "husky install" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@thream/socketio-jwt": "2.1.0", | ||||
|     "axios": "0.21.1", | ||||
|     "@prisma/client": "3.2.1", | ||||
|     "@sinclair/typebox": "0.20.5", | ||||
|     "axios": "0.22.0", | ||||
|     "bcryptjs": "2.4.3", | ||||
|     "cors": "2.8.5", | ||||
|     "dotenv": "8.2.0", | ||||
|     "dotenv": "10.0.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", | ||||
|     "fastify": "3.22.0", | ||||
|     "fastify-cors": "6.0.2", | ||||
|     "fastify-helmet": "5.3.2", | ||||
|     "fastify-multipart": "5.0.2", | ||||
|     "fastify-plugin": "3.0.0", | ||||
|     "fastify-rate-limit": "5.6.2", | ||||
|     "fastify-sensible": "3.1.1", | ||||
|     "fastify-static": "4.4.0", | ||||
|     "fastify-swagger": "4.12.4", | ||||
|     "fastify-url-data": "3.0.3", | ||||
|     "http-errors": "1.8.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" | ||||
|     "nodemailer": "6.6.5", | ||||
|     "read-pkg": "5.2.0", | ||||
|     "socket.io": "4.2.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@commitlint/cli": "12.1.1", | ||||
|     "@commitlint/config-conventional": "12.1.1", | ||||
|     "@release-it/conventional-changelog": "2.0.1", | ||||
|     "@commitlint/cli": "13.2.1", | ||||
|     "@commitlint/config-conventional": "13.2.0", | ||||
|     "@saithodev/semantic-release-backmerge": "1.5.3", | ||||
|     "@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/busboy": "0.3.0", | ||||
|     "@types/ejs": "3.1.0", | ||||
|     "@types/http-errors": "1.8.1", | ||||
|     "@types/jest": "27.0.2", | ||||
|     "@types/jsonwebtoken": "8.5.5", | ||||
|     "@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", | ||||
|     "@types/node": "16.10.3", | ||||
|     "@types/nodemailer": "6.4.4", | ||||
|     "@typescript-eslint/eslint-plugin": "4.33.0", | ||||
|     "concurrently": "6.3.0", | ||||
|     "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", | ||||
|     "eslint": "7.32.0", | ||||
|     "eslint-config-prettier": "8.3.0", | ||||
|     "eslint-config-standard-with-typescript": "21.0.1", | ||||
|     "eslint-plugin-import": "2.24.2", | ||||
|     "eslint-plugin-node": "11.1.0", | ||||
|     "eslint-plugin-prettier": "4.0.0", | ||||
|     "eslint-plugin-promise": "5.1.0", | ||||
|     "eslint-plugin-unicorn": "36.0.0", | ||||
|     "husky": "7.0.2", | ||||
|     "jest": "27.2.5", | ||||
|     "jest-mock-extended": "2.0.4", | ||||
|     "jest-ts-webcompat-resolver": "1.0.0", | ||||
|     "lint-staged": "11.2.1", | ||||
|     "markdownlint-cli": "0.29.0", | ||||
|     "nodemon": "2.0.13", | ||||
|     "plop": "2.7.4", | ||||
|     "prettier": "2.4.1", | ||||
|     "prisma": "3.2.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" | ||||
|     "semantic-release": "18.0.0", | ||||
|     "ts-jest": "27.0.5", | ||||
|     "typescript": "4.4.3" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								plopfile.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								plopfile.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| const { serviceGenerator } = require('./generators/service/index.js') | ||||
|  | ||||
| module.exports = ( | ||||
|   /** @type {import('plop').NodePlopAPI} */ | ||||
|   plop | ||||
| ) => { | ||||
|   plop.setGenerator('service', serviceGenerator) | ||||
| } | ||||
							
								
								
									
										137
									
								
								prisma/migrations/20211009140143_init/migration.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								prisma/migrations/20211009140143_init/migration.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| -- CreateTable | ||||
| CREATE TABLE "User" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "name" VARCHAR(255) NOT NULL, | ||||
|     "email" VARCHAR(255), | ||||
|     "password" TEXT, | ||||
|     "logo" TEXT, | ||||
|     "status" VARCHAR(255), | ||||
|     "biography" TEXT, | ||||
|     "website" VARCHAR(255), | ||||
|     "isConfirmed" BOOLEAN NOT NULL DEFAULT false, | ||||
|     "temporaryToken" TEXT, | ||||
|     "temporaryExpirationToken" TIMESTAMP(3), | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|  | ||||
|     CONSTRAINT "User_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "UserSetting" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "language" VARCHAR(255) NOT NULL, | ||||
|     "theme" VARCHAR(255) NOT NULL, | ||||
|     "isPublicEmail" BOOLEAN NOT NULL DEFAULT false, | ||||
|     "isPublicGuilds" BOOLEAN NOT NULL DEFAULT false, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|  | ||||
|     CONSTRAINT "UserSetting_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "RefreshToken" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "token" TEXT NOT NULL, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|  | ||||
|     CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "OAuth" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "providerId" TEXT NOT NULL, | ||||
|     "provider" VARCHAR(255) NOT NULL, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|  | ||||
|     CONSTRAINT "OAuth_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "Member" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "isOwner" BOOLEAN NOT NULL DEFAULT false, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "userId" INTEGER NOT NULL, | ||||
|     "guildId" INTEGER NOT NULL, | ||||
|  | ||||
|     CONSTRAINT "Member_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "Guild" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "name" VARCHAR(255) NOT NULL, | ||||
|     "icon" TEXT, | ||||
|     "description" TEXT, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|  | ||||
|     CONSTRAINT "Guild_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "Channel" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "name" VARCHAR(255) NOT NULL, | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "guildId" INTEGER NOT NULL, | ||||
|  | ||||
|     CONSTRAINT "Channel_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateTable | ||||
| CREATE TABLE "Message" ( | ||||
|     "id" SERIAL NOT NULL, | ||||
|     "value" TEXT NOT NULL, | ||||
|     "type" VARCHAR(255) NOT NULL DEFAULT E'text', | ||||
|     "mimetype" VARCHAR(255) NOT NULL DEFAULT E'text/plain', | ||||
|     "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
|     "memberId" INTEGER NOT NULL, | ||||
|     "channelId" INTEGER NOT NULL, | ||||
|  | ||||
|     CONSTRAINT "Message_pkey" PRIMARY KEY ("id") | ||||
| ); | ||||
|  | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "User_name_key" ON "User"("name"); | ||||
|  | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); | ||||
|  | ||||
| -- CreateIndex | ||||
| CREATE UNIQUE INDEX "UserSetting_userId_key" ON "UserSetting"("userId"); | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "UserSetting" ADD CONSTRAINT "UserSetting_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "OAuth" ADD CONSTRAINT "OAuth_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Member" ADD CONSTRAINT "Member_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Member" ADD CONSTRAINT "Member_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Channel" ADD CONSTRAINT "Channel_guildId_fkey" FOREIGN KEY ("guildId") REFERENCES "Guild"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Message" ADD CONSTRAINT "Message_memberId_fkey" FOREIGN KEY ("memberId") REFERENCES "Member"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
|  | ||||
| -- AddForeignKey | ||||
| ALTER TABLE "Message" ADD CONSTRAINT "Message_channelId_fkey" FOREIGN KEY ("channelId") REFERENCES "Channel"("id") ON DELETE RESTRICT ON UPDATE CASCADE; | ||||
							
								
								
									
										3
									
								
								prisma/migrations/migration_lock.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								prisma/migrations/migration_lock.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| # Please do not edit this file manually | ||||
| # It should be added in your version-control system (i.e. Git) | ||||
| provider = "postgresql" | ||||
							
								
								
									
										105
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								prisma/schema.prisma
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| datasource db { | ||||
|   provider = "postgres" | ||||
|   url      = env("DATABASE_URL") | ||||
| } | ||||
|  | ||||
| generator client { | ||||
|   provider = "prisma-client-js" | ||||
| } | ||||
|  | ||||
| model User { | ||||
|   id                       Int            @id @default(autoincrement()) | ||||
|   name                     String         @unique @db.VarChar(255) | ||||
|   email                    String?        @unique @db.VarChar(255) | ||||
|   password                 String?        @db.Text | ||||
|   logo                     String?        @db.Text | ||||
|   status                   String?        @db.VarChar(255) | ||||
|   biography                String?        @db.Text | ||||
|   website                  String?        @db.VarChar(255) | ||||
|   isConfirmed              Boolean        @default(false) | ||||
|   temporaryToken           String? | ||||
|   temporaryExpirationToken DateTime? | ||||
|   createdAt                DateTime       @default(now()) | ||||
|   updatedAt                DateTime       @default(now()) @updatedAt | ||||
|   settings                 UserSetting? | ||||
|   refreshTokens            RefreshToken[] | ||||
|   oauths                   OAuth[] | ||||
|   members                  Member[] | ||||
| } | ||||
|  | ||||
| model UserSetting { | ||||
|   id             Int      @id @default(autoincrement()) | ||||
|   language       String   @default("en") @db.VarChar(255) | ||||
|   theme          String   @default("dark") @db.VarChar(255) | ||||
|   isPublicEmail  Boolean  @default(false) | ||||
|   isPublicGuilds Boolean  @default(false) | ||||
|   createdAt      DateTime @default(now()) | ||||
|   updatedAt      DateTime @default(now()) @updatedAt | ||||
|   userId         Int      @unique | ||||
|   user           User?    @relation(fields: [userId], references: [id]) | ||||
| } | ||||
|  | ||||
| model RefreshToken { | ||||
|   id        Int      @id @default(autoincrement()) | ||||
|   token     String   @db.Text | ||||
|   createdAt DateTime @default(now()) | ||||
|   updatedAt DateTime @default(now()) @updatedAt | ||||
|   userId    Int      @unique | ||||
|   user      User?    @relation(fields: [userId], references: [id]) | ||||
| } | ||||
|  | ||||
| model OAuth { | ||||
|   id         Int      @id @default(autoincrement()) | ||||
|   providerId String   @db.Text | ||||
|   provider   String   @db.VarChar(255) | ||||
|   createdAt  DateTime @default(now()) | ||||
|   updatedAt  DateTime @default(now()) @updatedAt | ||||
|   userId     Int      @unique | ||||
|   user       User?    @relation(fields: [userId], references: [id]) | ||||
| } | ||||
|  | ||||
| model Member { | ||||
|   id        Int       @id @default(autoincrement()) | ||||
|   isOwner   Boolean   @default(false) | ||||
|   Message   Message[] | ||||
|   createdAt DateTime  @default(now()) | ||||
|   updatedAt DateTime  @default(now()) @updatedAt | ||||
|   userId    Int       @unique | ||||
|   user      User?     @relation(fields: [userId], references: [id]) | ||||
|   guildId   Int       @unique | ||||
|   guild     Guild?    @relation(fields: [guildId], references: [id]) | ||||
| } | ||||
|  | ||||
| model Guild { | ||||
|   id          Int       @id @default(autoincrement()) | ||||
|   name        String    @db.VarChar(255) | ||||
|   icon        String?   @db.Text | ||||
|   description String?   @db.Text | ||||
|   createdAt   DateTime  @default(now()) | ||||
|   updatedAt   DateTime  @default(now()) @updatedAt | ||||
|   members     Member[] | ||||
|   channels    Channel[] | ||||
| } | ||||
|  | ||||
| model Channel { | ||||
|   id        Int       @id @default(autoincrement()) | ||||
|   name      String    @db.VarChar(255) | ||||
|   createdAt DateTime  @default(now()) | ||||
|   updatedAt DateTime  @default(now()) @updatedAt | ||||
|   guildId   Int       @unique | ||||
|   guild     Guild?    @relation(fields: [guildId], references: [id]) | ||||
|   messages  Message[] | ||||
| } | ||||
|  | ||||
| model Message { | ||||
|   id        Int      @id @default(autoincrement()) | ||||
|   value     String   @db.Text | ||||
|   type      String   @default("text") @db.VarChar(255) | ||||
|   mimetype  String   @default("text/plain") @db.VarChar(255) | ||||
|   createdAt DateTime @default(now()) | ||||
|   updatedAt DateTime @default(now()) @updatedAt | ||||
|   memberId  Int      @unique | ||||
|   member    Member?  @relation(fields: [memberId], references: [id]) | ||||
|   channelId Int      @unique | ||||
|   channel   Channel? @relation(fields: [channelId], references: [id]) | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| 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) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,4 +1,3 @@ | ||||
| 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' | ||||
| @@ -1,11 +1,8 @@ | ||||
| import fsMock from 'mock-fs' | ||||
| import path from 'path' | ||||
| import { Sequelize } from 'sequelize-typescript' | ||||
| import { Database, open } from 'sqlite' | ||||
| import sqlite3 from 'sqlite3' | ||||
| import { PrismaClient } from '@prisma/client' | ||||
| import { mockDeep, mockReset } from 'jest-mock-extended' | ||||
| import { DeepMockProxy } from 'jest-mock-extended/lib/cjs/Mock' | ||||
|  | ||||
| let sqlite: Database | undefined | ||||
| let sequelize: Sequelize | undefined | ||||
| import prisma from '../tools/database/prisma.js' | ||||
|  | ||||
| jest.mock('nodemailer', () => ({ | ||||
|   createTransport: () => { | ||||
| @@ -15,28 +12,13 @@ jest.mock('nodemailer', () => ({ | ||||
|   } | ||||
| })) | ||||
|  | ||||
| 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')] | ||||
|   }) | ||||
| jest.mock('../tools/database/prisma.js', () => ({ | ||||
|   __esModule: true, | ||||
|   default: mockDeep<PrismaClient>() | ||||
| })) | ||||
|  | ||||
| beforeEach(() => { | ||||
|   mockReset(prismaMock) | ||||
| }) | ||||
|  | ||||
| beforeEach(async () => { | ||||
|   await sequelize?.sync({ force: true }) | ||||
| }) | ||||
|  | ||||
| afterEach(async () => { | ||||
|   fsMock.restore() | ||||
| }) | ||||
|  | ||||
| afterAll(async () => { | ||||
|   await sqlite?.close() | ||||
|   await sequelize?.close() | ||||
| }) | ||||
| export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient> | ||||
|   | ||||
| @@ -1,10 +0,0 @@ | ||||
| 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']) | ||||
| }) | ||||
| @@ -1,57 +0,0 @@ | ||||
| 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 } | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/__test__/utils/authenticateUserTest.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/__test__/utils/authenticateUserTest.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { User } from '@prisma/client' | ||||
|  | ||||
| import { refreshTokenExample } from '../../models/RefreshToken.js' | ||||
| import { userExample, UserJWT } from '../../models/User.js' | ||||
| import { userSettingsExample } from '../../models/UserSettings.js' | ||||
| import { | ||||
|   generateAccessToken, | ||||
|   generateRefreshToken | ||||
| } from '../../tools/utils/jwtToken' | ||||
| import { prismaMock } from '../setup' | ||||
|  | ||||
| export const authenticateUserTest = async (): Promise<{ | ||||
|   accessToken: string | ||||
|   refreshToken: string | ||||
|   user: User | ||||
| }> => { | ||||
|   prismaMock.user.findUnique.mockResolvedValue(userExample) | ||||
|   prismaMock.userSetting.findFirst.mockResolvedValue(userSettingsExample) | ||||
|   prismaMock.oAuth.findMany.mockResolvedValue([]) | ||||
|   prismaMock.refreshToken.create.mockResolvedValue(refreshTokenExample) | ||||
|   const userJWT: UserJWT = { | ||||
|     currentStrategy: 'local', | ||||
|     id: 1 | ||||
|   } | ||||
|   const accessToken = generateAccessToken(userJWT) | ||||
|   const refreshToken = await generateRefreshToken(userJWT) | ||||
|   return { accessToken, refreshToken, user: userExample } | ||||
| } | ||||
| @@ -1,8 +0,0 @@ | ||||
| /** formatErrors for testing purpose (no types safety) */ | ||||
| export const formatErrors = (errors: any): string[] => { | ||||
|   try { | ||||
|     return errors.map((e: any) => e.message) | ||||
|   } catch { | ||||
|     return [] | ||||
|   } | ||||
| } | ||||
| @@ -1,5 +0,0 @@ | ||||
| export const wait = async (ms: number): Promise<void> => { | ||||
|   return await new Promise((resolve) => { | ||||
|     setTimeout(resolve, ms) | ||||
|   }) | ||||
| } | ||||
| @@ -1,45 +1,51 @@ | ||||
| import 'express-async-errors' | ||||
| import { fileURLToPath } from 'node:url' | ||||
|  | ||||
| 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 fastify from 'fastify' | ||||
| import fastifyCors from 'fastify-cors' | ||||
| import fastifySwagger from 'fastify-swagger' | ||||
| import fastifyUrlData from 'fastify-url-data' | ||||
| import fastifyHelmet from 'fastify-helmet' | ||||
| import fastifyRateLimit from 'fastify-rate-limit' | ||||
| import fastifySensible from 'fastify-sensible' | ||||
| import fastifyStatic from 'fastify-static' | ||||
|  | ||||
| import { errorHandler } from './tools/middlewares/errorHandler' | ||||
| import { router } from './services' | ||||
| import { NotFoundError } from './tools/errors/NotFoundError' | ||||
| import { TooManyRequestsError } from './tools/errors/TooManyRequestsError' | ||||
| import { services } from './services/index.js' | ||||
| import { swaggerOptions } from './tools/configurations/swaggerOptions.js' | ||||
| import fastifySocketIo from './tools/plugins/socket-io.js' | ||||
| import { UPLOADS_URL } from './tools/configurations/index.js' | ||||
|  | ||||
| const application = express() | ||||
| export const application = fastify({ | ||||
|   logger: process.env.NODE_ENV === 'development' | ||||
| }) | ||||
| 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() | ||||
|       } | ||||
|     }) | ||||
|   ) | ||||
| const main = async (): Promise<void> => { | ||||
|   await application.register(fastifyCors) | ||||
|   await application.register(fastifySensible) | ||||
|   await application.register(fastifyUrlData) | ||||
|   await application.register(fastifySocketIo, { | ||||
|     cors: { | ||||
|       origin: '*', | ||||
|       methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', | ||||
|       preflightContinue: false, | ||||
|       optionsSuccessStatus: 204 | ||||
|     } | ||||
|   }) | ||||
|   await application.register(fastifyHelmet) | ||||
|   await application.register(fastifyRateLimit, { | ||||
|     max: 100, | ||||
|     timeWindow: '1 minute' | ||||
|   }) | ||||
|   await application.register(fastifyStatic, { | ||||
|     root: fileURLToPath(UPLOADS_URL), | ||||
|     prefix: '/uploads/' | ||||
|   }) | ||||
|   await application.register(fastifySwagger, swaggerOptions) | ||||
|   await application.register(services) | ||||
| } | ||||
|  | ||||
| application.use(express.json()) | ||||
| application.use(helmet()) | ||||
| application.use(cors<Request>()) | ||||
| application.use(router) | ||||
| application.use(() => { | ||||
|   throw new NotFoundError() | ||||
| main().catch((error) => { | ||||
|   console.error(error) | ||||
|   process.exit(1) | ||||
| }) | ||||
| application.use(errorHandler) | ||||
|  | ||||
| export default application | ||||
|   | ||||
							
								
								
									
										30
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								src/index.ts
									
									
									
									
									
								
							| @@ -1,22 +1,12 @@ | ||||
| import { authorize } from '@thream/socketio-jwt' | ||||
| import { application } from './application.js' | ||||
| import { HOST, PORT } from './tools/configurations/index.js' | ||||
|  | ||||
| import application from './application' | ||||
| import { socket } from './tools/socket' | ||||
| import { sequelize } from './tools/database/sequelize' | ||||
| const main = async (): Promise<void> => { | ||||
|   const address = await application.listen(PORT, HOST) | ||||
|   console.log('\x1b[36m%s\x1b[0m', `🚀  Server listening at ${address}`) | ||||
| } | ||||
|  | ||||
| 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)) | ||||
| main().catch((error) => { | ||||
|   console.error(error) | ||||
|   process.exit(1) | ||||
| }) | ||||
|   | ||||
| @@ -1,55 +1,23 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   HasMany, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
| import { Type } from '@sinclair/typebox' | ||||
| import { Channel } from '@prisma/client' | ||||
|  | ||||
| import Guild from './Guild' | ||||
| import Message from './Message' | ||||
| import { date, id } from './utils.js' | ||||
| import { guildExample } from './Guild.js' | ||||
|  | ||||
| export const channelTypes = ['text', 'voice'] as const | ||||
| export type ChannelType = typeof channelTypes[number] | ||||
| export const types = [Type.Literal('text')] | ||||
|  | ||||
| @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[] | ||||
| export const channelSchema = { | ||||
|   id, | ||||
|   name: Type.String({ maxLength: 255 }), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   guildId: id | ||||
| } | ||||
|  | ||||
| export const channelExample: Channel = { | ||||
|   id: 1, | ||||
|   name: 'general', | ||||
|   guildId: guildExample.id, | ||||
|   createdAt: new Date(), | ||||
|   updatedAt: new Date() | ||||
| } | ||||
|   | ||||
| @@ -1,45 +1,22 @@ | ||||
| import { Column, DataType, HasMany, Model, Table } from 'sequelize-typescript' | ||||
| import { guildsIconPath } from '../tools/configurations/constants' | ||||
| import { Guild } from '@prisma/client' | ||||
| import { Type } from '@sinclair/typebox' | ||||
|  | ||||
| import Channel from './Channel' | ||||
| import Invitation from './Invitation' | ||||
| import Member from './Member' | ||||
| import { date, id } from './utils.js' | ||||
|  | ||||
| @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[] | ||||
| export const guildSchema = { | ||||
|   id, | ||||
|   name: Type.String({ minLength: 3, maxLength: 30 }), | ||||
|   icon: Type.String({ format: 'uri-reference' }), | ||||
|   description: Type.String({ maxLength: 160 }), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt | ||||
| } | ||||
|  | ||||
| export const guildExample: Guild = { | ||||
|   id: 1, | ||||
|   name: 'GuildExample', | ||||
|   description: 'guild example.', | ||||
|   icon: null, | ||||
|   createdAt: new Date(), | ||||
|   updatedAt: new Date() | ||||
| } | ||||
|   | ||||
| @@ -1,40 +0,0 @@ | ||||
| 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 | ||||
| } | ||||
| @@ -1,48 +1,24 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   HasMany, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
| import { Type } from '@sinclair/typebox' | ||||
| import { Member } from '@prisma/client' | ||||
|  | ||||
| import Channel from './Channel' | ||||
| import Guild from './Guild' | ||||
| import Message from './Message' | ||||
| import User from './User' | ||||
| import { date, id } from './utils.js' | ||||
| import { guildExample } from './Guild.js' | ||||
| import { userExample } from './User.js' | ||||
|  | ||||
| @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[] | ||||
| export const memberSchema = { | ||||
|   id, | ||||
|   isOwner: Type.Boolean({ default: false }), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   userId: id, | ||||
|   guildId: id | ||||
| } | ||||
|  | ||||
| export const memberExample: Member = { | ||||
|   id: 1, | ||||
|   isOwner: true, | ||||
|   userId: userExample.id, | ||||
|   guildId: guildExample.id, | ||||
|   createdAt: new Date(), | ||||
|   updatedAt: new Date() | ||||
| } | ||||
|   | ||||
| @@ -1,51 +1,20 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
| import { Type } from '@sinclair/typebox' | ||||
|  | ||||
| import Channel from './Channel' | ||||
| import Member from './Member' | ||||
| import { date, id } from './utils.js' | ||||
|  | ||||
| export const messageTypes = ['text', 'file'] as const | ||||
| export type MessageType = typeof messageTypes[number] | ||||
| export const types = [Type.Literal('text'), Type.Literal('file')] | ||||
|  | ||||
| @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 | ||||
| export const messageSchema = { | ||||
|   id, | ||||
|   value: Type.String(), | ||||
|   type: Type.Union(types, { default: 'text' }), | ||||
|   mimetype: Type.String({ | ||||
|     maxLength: 255, | ||||
|     default: 'text/plain', | ||||
|     format: 'mimetype' | ||||
|   }), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   memberId: id, | ||||
|   channelId: id | ||||
| } | ||||
|   | ||||
| @@ -1,38 +1,25 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
| import { Type } from '@sinclair/typebox' | ||||
|  | ||||
| import User from './User' | ||||
| import { date, id } from './utils.js' | ||||
|  | ||||
| export const providers = ['google', 'github', 'discord'] as const | ||||
| export const strategies = [...providers, 'local'] as const | ||||
|  | ||||
| export const strategiesTypebox = strategies.map((strategy) => | ||||
|   Type.Literal(strategy) | ||||
| ) | ||||
| export const providersTypebox = providers.map((provider) => | ||||
|   Type.Literal(provider) | ||||
| ) | ||||
|  | ||||
| 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 | ||||
| export const oauthSchema = { | ||||
|   id, | ||||
|   providerId: Type.String(), | ||||
|   provider: Type.Union([...providersTypebox]), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   userId: id | ||||
| } | ||||
|   | ||||
| @@ -1,26 +1,21 @@ | ||||
| import { | ||||
|   BelongsTo, | ||||
|   Column, | ||||
|   DataType, | ||||
|   ForeignKey, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
| import { RefreshToken } from '@prisma/client' | ||||
| import { Type } from '@sinclair/typebox' | ||||
|  | ||||
| import User from './User' | ||||
| import { userExample } from './User.js' | ||||
| import { date, id } from './utils.js' | ||||
|  | ||||
| @Table | ||||
| export default class RefreshToken extends Model { | ||||
|   @Column({ | ||||
|     type: DataType.TEXT, | ||||
|     allowNull: false | ||||
|   }) | ||||
|   token!: string | ||||
|  | ||||
|   @ForeignKey(() => User) | ||||
|   @Column | ||||
|   userId!: number | ||||
|  | ||||
|   @BelongsTo(() => User) | ||||
|   user!: User | ||||
| export const refreshTokensSchema = { | ||||
|   id, | ||||
|   token: Type.String(), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   userId: id | ||||
| } | ||||
|  | ||||
| export const refreshTokenExample: RefreshToken = { | ||||
|   id: 1, | ||||
|   userId: userExample.id, | ||||
|   token: 'sometoken', | ||||
|   createdAt: new Date(), | ||||
|   updatedAt: new Date() | ||||
| } | ||||
|   | ||||
| @@ -1,26 +1,9 @@ | ||||
| import { | ||||
|   Column, | ||||
|   DataType, | ||||
|   HasMany, | ||||
|   HasOne, | ||||
|   Model, | ||||
|   Table | ||||
| } from 'sequelize-typescript' | ||||
| import { User } from '@prisma/client' | ||||
| import { Static, Type } from '@sinclair/typebox' | ||||
|  | ||||
| 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> {} | ||||
| import { AuthenticationStrategy, strategiesTypebox } from './OAuth.js' | ||||
| import { userSettingsSchema } from './UserSettings.js' | ||||
| import { date, id } from './utils.js' | ||||
|  | ||||
| export interface UserJWT { | ||||
|   id: number | ||||
| @@ -33,80 +16,66 @@ export interface UserRequest { | ||||
|   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 | ||||
|   } | ||||
| export const userSchema = { | ||||
|   id, | ||||
|   name: Type.String({ minLength: 1, maxLength: 30 }), | ||||
|   email: Type.String({ minLength: 1, maxLength: 255, format: 'email' }), | ||||
|   password: Type.String(), | ||||
|   logo: Type.String({ format: 'uri-reference' }), | ||||
|   status: Type.String({ maxLength: 255 }), | ||||
|   biography: Type.String(), | ||||
|   website: Type.String({ maxLength: 255, format: 'uri-reference' }), | ||||
|   isConfirmed: Type.Boolean({ default: false }), | ||||
|   temporaryToken: Type.String(), | ||||
|   temporaryExpirationToken: Type.String({ format: 'date-time' }), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt | ||||
| } | ||||
|  | ||||
| export const userPublicSchema = { | ||||
|   id, | ||||
|   name: userSchema.name, | ||||
|   email: Type.Optional(userSchema.email), | ||||
|   logo: Type.Optional(userSchema.logo), | ||||
|   status: Type.Optional(userSchema.status), | ||||
|   biography: Type.Optional(userSchema.biography), | ||||
|   website: Type.Optional(userSchema.website), | ||||
|   isConfirmed: userSchema.isConfirmed, | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   settings: Type.Optional(Type.Object(userSettingsSchema)) | ||||
| } | ||||
|  | ||||
| export const userCurrentSchema = Type.Object({ | ||||
|   user: Type.Object({ | ||||
|     ...userPublicSchema, | ||||
|     currentStrategy: Type.Union([...strategiesTypebox]), | ||||
|     strategies: Type.Array(Type.Union([...strategiesTypebox])) | ||||
|   }) | ||||
| }) | ||||
|  | ||||
| export const bodyUserSchema = Type.Object({ | ||||
|   email: userSchema.email, | ||||
|   name: userSchema.name, | ||||
|   password: userSchema.password, | ||||
|   theme: userSettingsSchema.theme, | ||||
|   language: userSettingsSchema.language | ||||
| }) | ||||
|  | ||||
| export type BodyUserSchemaType = Static<typeof bodyUserSchema> | ||||
|  | ||||
| export const userExample: User = { | ||||
|   id: 1, | ||||
|   name: 'Divlo', | ||||
|   email: 'contact@divlo.fr', | ||||
|   password: 'somepassword', | ||||
|   logo: null, | ||||
|   status: null, | ||||
|   biography: null, | ||||
|   website: null, | ||||
|   isConfirmed: true, | ||||
|   temporaryToken: 'temporaryUUIDtoken', | ||||
|   temporaryExpirationToken: new Date(), | ||||
|   createdAt: new Date(), | ||||
|   updatedAt: new Date() | ||||
| } | ||||
|   | ||||
| @@ -1,65 +0,0 @@ | ||||
| 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 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/models/UserSettings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/models/UserSettings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import { UserSetting } from '@prisma/client' | ||||
| import { Type, Static } from '@sinclair/typebox' | ||||
|  | ||||
| import { date, id } from './utils.js' | ||||
|  | ||||
| export const languages = [Type.Literal('fr'), Type.Literal('en')] | ||||
| export const themes = [Type.Literal('light'), Type.Literal('dark')] | ||||
|  | ||||
| export const userSettingsSchema = { | ||||
|   id, | ||||
|   language: Type.Union(languages, { default: 'en' }), | ||||
|   theme: Type.Union(themes, { default: 'dark' }), | ||||
|   isPublicEmail: Type.Boolean({ default: false }), | ||||
|   isPublicGuilds: Type.Boolean({ default: false }), | ||||
|   createdAt: date.createdAt, | ||||
|   updatedAt: date.updatedAt, | ||||
|   userId: id | ||||
| } | ||||
|  | ||||
| export type Theme = Static<typeof userSettingsSchema.theme> | ||||
| export type Language = Static<typeof userSettingsSchema.language> | ||||
|  | ||||
| export const userSettingsExample: UserSetting = { | ||||
|   id: 1, | ||||
|   theme: 'dark', | ||||
|   language: 'en', | ||||
|   isPublicEmail: false, | ||||
|   isPublicGuilds: false, | ||||
|   userId: 1, | ||||
|   createdAt: new Date(), | ||||
|   updatedAt: new Date() | ||||
| } | ||||
| @@ -1,105 +0,0 @@ | ||||
| -- 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'); | ||||
							
								
								
									
										49
									
								
								src/models/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/models/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { Type } from '@sinclair/typebox' | ||||
|  | ||||
| export const date = { | ||||
|   createdAt: Type.String({ | ||||
|     format: 'date-time', | ||||
|     description: 'Created date time' | ||||
|   }), | ||||
|   updatedAt: Type.String({ | ||||
|     format: 'date-time', | ||||
|     description: 'Last updated date time' | ||||
|   }) | ||||
| } | ||||
|  | ||||
| export const id = Type.Integer({ minimum: 1, description: 'Unique identifier' }) | ||||
|  | ||||
| export const redirectURI = Type.String({ format: 'uri-reference' }) | ||||
|  | ||||
| export const fastifyErrors = { | ||||
|   400: Type.Object({ | ||||
|     statusCode: Type.Literal(400), | ||||
|     error: Type.Literal('Bad Request'), | ||||
|     message: Type.String() | ||||
|   }), | ||||
|   401: Type.Object({ | ||||
|     statusCode: Type.Literal(401), | ||||
|     error: Type.Literal('Unauthorized'), | ||||
|     message: Type.Literal('Unauthorized') | ||||
|   }), | ||||
|   403: Type.Object({ | ||||
|     statusCode: Type.Literal(403), | ||||
|     error: Type.Literal('Forbidden'), | ||||
|     message: Type.Literal('Forbidden') | ||||
|   }), | ||||
|   404: Type.Object({ | ||||
|     statusCode: Type.Literal(404), | ||||
|     error: Type.Literal('Not Found'), | ||||
|     message: Type.Literal('Not Found') | ||||
|   }), | ||||
|   431: Type.Object({ | ||||
|     statusCode: Type.Literal(431), | ||||
|     error: Type.Literal('Request Header Fields Too Large'), | ||||
|     message: Type.String() | ||||
|   }), | ||||
|   500: { | ||||
|     statusCode: Type.Literal(500), | ||||
|     error: Type.Literal('Internal Server Error'), | ||||
|     message: Type.Literal('Something went wrong') | ||||
|   } | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| components: | ||||
|   securitySchemes: | ||||
|     bearerAuth: | ||||
|       type: 'http' | ||||
|       scheme: 'bearer' | ||||
|       bearerFormat: 'JWT' | ||||
| @@ -1,87 +0,0 @@ | ||||
| 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' | ||||
| @@ -1,20 +0,0 @@ | ||||
| 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 | ||||
| @@ -1,25 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,41 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,71 +0,0 @@ | ||||
| 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]) | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,120 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,56 +0,0 @@ | ||||
| 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 }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,33 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,44 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,23 +0,0 @@ | ||||
| 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() | ||||
|   }) | ||||
| }) | ||||
| @@ -1,69 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,60 +0,0 @@ | ||||
| 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() } | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,9 +0,0 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { postMessagesRouter } from './post' | ||||
| import { getMessagesRouter } from './get' | ||||
|  | ||||
| export const messagesChannelsRouter = Router() | ||||
|  | ||||
| messagesChannelsRouter.use('/', postMessagesRouter) | ||||
| messagesChannelsRouter.use('/', getMessagesRouter) | ||||
| @@ -1,122 +0,0 @@ | ||||
| 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 }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,92 +0,0 @@ | ||||
| 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 }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,24 +0,0 @@ | ||||
| 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' | ||||
| @@ -1,42 +0,0 @@ | ||||
| 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 | ||||
|   } | ||||
| } | ||||
| @@ -1,11 +0,0 @@ | ||||
| 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) | ||||
| @@ -1,8 +0,0 @@ | ||||
| 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)) | ||||
| @@ -1,24 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,25 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,48 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,62 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,58 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,182 +0,0 @@ | ||||
| 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 }) | ||||
|       ]) | ||||
|     ) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,31 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,39 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,23 +0,0 @@ | ||||
| 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) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,146 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,43 +0,0 @@ | ||||
| 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) | ||||
|   } | ||||
| ) | ||||
| @@ -1,9 +0,0 @@ | ||||
| import { Router } from 'express' | ||||
|  | ||||
| import { getChannelsRouter } from './get' | ||||
| import { postChannelsRouter } from './post' | ||||
|  | ||||
| export const guildsChannelsRouter = Router() | ||||
|  | ||||
| guildsChannelsRouter.use('/', getChannelsRouter) | ||||
| guildsChannelsRouter.use('/', postChannelsRouter) | ||||
| @@ -1,73 +0,0 @@ | ||||
| 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 }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,57 +0,0 @@ | ||||
| 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 }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,29 +0,0 @@ | ||||
| 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 }) | ||||
|   } | ||||
| ) | ||||
| @@ -1,19 +0,0 @@ | ||||
| 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) | ||||
| @@ -1,31 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,40 +0,0 @@ | ||||
| /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' | ||||
| @@ -1,46 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
| @@ -1,163 +0,0 @@ | ||||
| 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'])) | ||||
|   }) | ||||
| }) | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user