feat: migrate from express to fastify
This commit is contained in:
parent
714cc643ba
commit
b77e602358
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"]
|
||||
}
|
29099
package-lock.json
generated
29099
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
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Thream/api"
|
||||
},
|
||||
"gitlab": {
|
||||
"release": false
|
||||
},
|
||||
"npm": {
|
||||
"publish": false
|
||||
},
|
||||
"hooks": {
|
||||
"before:init": [
|
||||
"npm run lint:docker",
|
||||
"npm run lint:editorconfig",
|
||||
"npm run lint:markdown",
|
||||
"npm run lint:typescript",
|
||||
"npm run build",
|
||||
"npm run test"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
"@release-it/conventional-changelog": {
|
||||
"preset": "angular",
|
||||
"infile": "CHANGELOG.md"
|
||||
}
|
||||
}
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest",
|
||||
"testEnvironment": "node",
|
||||
"setupFiles": [
|
||||
"./__test__/setEnvsVars.ts"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"./__test__/setup.ts"
|
||||
],
|
||||
"rootDir": "./src",
|
||||
"collectCoverage": true,
|
||||
"coverageDirectory": "../coverage/",
|
||||
"coverageReporters": [
|
||||
"text",
|
||||
"cobertura"
|
||||
]
|
||||
},
|
||||
"ts-standard": {
|
||||
"ignore": [
|
||||
"build",
|
||||
"coverage",
|
||||
"node_modules",
|
||||
"uploads"
|
||||
],
|
||||
"envs": [
|
||||
"node",
|
||||
"jest"
|
||||
],
|
||||
"report": "stylish"
|
||||
"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