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
|
.vscode
|
||||||
.git
|
.git
|
||||||
|
.env
|
||||||
build
|
build
|
||||||
coverage
|
coverage
|
||||||
node_modules
|
node_modules
|
||||||
tmp
|
tmp
|
||||||
temp
|
temp
|
||||||
**/__test__/**
|
|
||||||
|
39
.env.example
39
.env.example
@ -1,22 +1,17 @@
|
|||||||
COMPOSE_PROJECT_NAME=thream-api
|
COMPOSE_PROJECT_NAME='thream-api'
|
||||||
PORT=8080
|
HOST='0.0.0.0'
|
||||||
API_BASE_URL=http://localhost:8080
|
PORT='8080'
|
||||||
DATABASE_DIALECT=mysql
|
DATABASE_URL='postgresql://user:password@thream-database:5432/thream'
|
||||||
DATABASE_HOST=thream-database
|
JWT_ACCESS_EXPIRES_IN='15 minutes'
|
||||||
DATABASE_NAME=thream
|
JWT_ACCESS_SECRET='accessTokenSecret'
|
||||||
DATABASE_USER=root
|
JWT_REFRESH_SECRET='refreshTokenSecret'
|
||||||
DATABASE_PASSWORD=password
|
DISCORD_CLIENT_ID=''
|
||||||
DATABASE_PORT=3306
|
DISCORD_CLIENT_SECRET=''
|
||||||
JWT_ACCESS_EXPIRES_IN=15 minutes
|
GITHUB_CLIENT_ID=''
|
||||||
JWT_ACCESS_SECRET=accessTokenSecret
|
GITHUB_CLIENT_SECRET=''
|
||||||
JWT_REFRESH_SECRET=refreshTokenSecret
|
GOOGLE_CLIENT_ID=''
|
||||||
DISCORD_CLIENT_ID=
|
GOOGLE_CLIENT_SECRET=''
|
||||||
DISCORD_CLIENT_SECRET=
|
EMAIL_HOST='thream-maildev'
|
||||||
GITHUB_CLIENT_ID=
|
EMAIL_USER='no-reply@thream.fr'
|
||||||
GITHUB_CLIENT_SECRET=
|
EMAIL_PASSWORD='password'
|
||||||
GOOGLE_CLIENT_ID=
|
EMAIL_PORT='25'
|
||||||
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'
|
- uses: 'actions/checkout@v2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Use Node.js'
|
||||||
uses: 'actions/setup-node@v2.1.5'
|
uses: 'actions/setup-node@v2.4.1'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install'
|
||||||
run: 'npm ci --cache .npm --prefer-offline'
|
run: 'npm install'
|
||||||
|
|
||||||
- name: 'Build'
|
- name: 'Build'
|
||||||
run: 'npm run 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'
|
- uses: 'actions/checkout@v2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Use Node.js'
|
||||||
uses: 'actions/setup-node@v2.1.5'
|
uses: 'actions/setup-node@v2.4.1'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install'
|
||||||
run: 'npm ci --cache .npm --prefer-offline'
|
run: 'npm install'
|
||||||
|
|
||||||
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
- run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
- run: 'npm run lint:editorconfig'
|
- run: 'npm run lint:editorconfig'
|
||||||
- run: 'npm run lint:markdown'
|
- run: 'npm run lint:markdown'
|
||||||
- run: 'npm run lint:docker'
|
- run: 'npm run lint:docker'
|
||||||
- run: 'npm run lint:typescript'
|
- 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'
|
- uses: 'actions/checkout@v2'
|
||||||
|
|
||||||
- name: 'Use Node.js'
|
- name: 'Use Node.js'
|
||||||
uses: 'actions/setup-node@v2.1.5'
|
uses: 'actions/setup-node@v2.4.1'
|
||||||
with:
|
with:
|
||||||
node-version: '16.x'
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
- name: 'Install'
|
- name: 'Install'
|
||||||
run: 'npm ci --cache .npm --prefer-offline'
|
run: 'npm install'
|
||||||
|
|
||||||
- name: 'Test'
|
- name: 'Test'
|
||||||
run: 'npm run test'
|
run: 'npm run test'
|
||||||
|
22
.gitignore
vendored
22
.gitignore
vendored
@ -15,12 +15,22 @@ coverage
|
|||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
||||||
# editors
|
# IDEs and editors
|
||||||
.vscode
|
/.idea
|
||||||
.theia
|
.project
|
||||||
.idea
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
tmp
|
uploads
|
||||||
temp
|
|
||||||
|
1
.husky/.gitignore
vendored
1
.husky/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
_
|
|
@ -1,8 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
npm run lint:docker
|
npm run lint:staged
|
||||||
npm run lint:editorconfig
|
|
||||||
npm run lint:markdown
|
|
||||||
npm run lint:typescript
|
|
||||||
npm run build
|
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,
|
"default": true,
|
||||||
"MD013": false,
|
"MD013": false,
|
||||||
"MD024": false,
|
|
||||||
"MD033": false,
|
"MD033": false,
|
||||||
"MD041": 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
|
## 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
|
## Open Development
|
||||||
|
|
||||||
@ -14,14 +14,14 @@ All work on **Thream/api** happens directly on [GitHub](https://github.com/Threa
|
|||||||
|
|
||||||
- Reporting a bug.
|
- Reporting a bug.
|
||||||
- Suggest a new feature idea.
|
- 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.
|
- Improve structure/format/performance/refactor/tests of the code.
|
||||||
|
|
||||||
## Pull Requests
|
## 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**.
|
- Make sure your **code passes the tests**.
|
||||||
|
|
||||||
@ -29,7 +29,9 @@ If you're adding new features to **Thream/api**, please include tests.
|
|||||||
|
|
||||||
## Commits
|
## 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
|
### Types
|
||||||
|
|
||||||
@ -56,17 +58,16 @@ Scopes define what part of the code changed.
|
|||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
```sh
|
```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 "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
|
## Directory Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
├── email
|
├── email
|
||||||
├── public
|
├── prisma
|
||||||
├── scripts
|
|
||||||
└── src
|
└── src
|
||||||
├── models
|
├── models
|
||||||
├── services
|
├── services
|
||||||
@ -77,8 +78,9 @@ git commit -m "fix(messages): should emit events to connected users"
|
|||||||
### Each folder explained
|
### Each folder explained
|
||||||
|
|
||||||
- `email` : email template(s) and translation(s)
|
- `email` : email template(s) and translation(s)
|
||||||
|
- `prisma` : contains the prisma schema and migrations
|
||||||
- `src` : all source files
|
- `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
|
- `services` : all REST API endpoints
|
||||||
- `tools` : configs and utilities
|
- `tools` : configs and utilities
|
||||||
- `typings` : types gloablly used in the project
|
- `typings` : types gloablly used in the project
|
||||||
@ -94,14 +96,9 @@ Here is what potentially look like a folder structure for this service :
|
|||||||
└── src
|
└── src
|
||||||
└── services
|
└── services
|
||||||
└── channels
|
└── channels
|
||||||
├── __docs__
|
|
||||||
│ └── get.yaml
|
|
||||||
├── __test__
|
├── __test__
|
||||||
│ └── get.test.ts
|
│ └── get.test.ts
|
||||||
├── [channelId]
|
├── [channelId]
|
||||||
│ ├── __docs__
|
|
||||||
│ │ ├── delete.yaml
|
|
||||||
│ │ └── put.yaml
|
|
||||||
│ ├── __test__
|
│ ├── __test__
|
||||||
│ │ ├── delete.test.ts
|
│ │ ├── delete.test.ts
|
||||||
│ │ └── put.test.ts
|
│ │ └── put.test.ts
|
||||||
@ -118,6 +115,7 @@ This folder structure will map to these REST API routes :
|
|||||||
- DELETE `/channels/:channelId`
|
- DELETE `/channels/:channelId`
|
||||||
- PUT `/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`).
|
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
|
FROM node:16.11.0 AS dependencies
|
||||||
RUN npm install --global npm@7
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
WORKDIR /api
|
|
||||||
|
|
||||||
COPY ./package*.json ./
|
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 ./ ./
|
COPY ./ ./
|
||||||
|
RUN npx prisma generate
|
||||||
RUN npm run build
|
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>
|
<h1 align="center"><a href="https://api.thream.divlo.fr/documentation">Thream/api</a></h1>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>Thream's application programming interface to stay close with your friends and communities.</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
|
<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/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>
|
<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 />
|
<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://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>
|
<a href="https://dependabot.com/"><img src="https://badgen.net/github/dependabot/Thream/api?icon=dependabot" alt="Dependabot badge" /></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -29,9 +25,9 @@ This project was bootstrapped with [create-fullstack-app](https://github.com/Div
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org/) >= 14
|
- [Node.js](https://nodejs.org/) >= 16.0.0
|
||||||
- [npm](https://www.npmjs.com/) >= 6
|
- [npm](https://www.npmjs.com/) >= 8.0.0
|
||||||
- [MySQL](https://www.mysql.com/) >= 8
|
- [PostgreSQL](https://www.postgresql.org/)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@ -45,41 +41,60 @@ cd api
|
|||||||
# Configure environment variables
|
# Configure environment variables
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
|
|
||||||
# Install dependencies
|
# Install
|
||||||
npm 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
|
```sh
|
||||||
# Setup and run all the services for you
|
# Create a new user and database
|
||||||
docker-compose up
|
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/)
|
### Production environment with [Docker](https://www.docker.com/)
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Setup and run all the services for you
|
# Setup and run all the services for you
|
||||||
docker-compose --file=docker-compose.production.yml up
|
docker-compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Services started
|
#### Services started
|
||||||
|
|
||||||
- API : `http://localhost:8080`
|
- API : `http://localhost:8080`
|
||||||
- [MySQL database](https://www.mysql.com/)
|
- [PostgreSQL database](https://www.postgresql.org/)
|
||||||
|
|
||||||
#### Services started only in Development environment
|
|
||||||
|
|
||||||
- [phpmyadmin](https://www.phpmyadmin.net/) : `http://localhost:8000`
|
|
||||||
- [MailDev](https://maildev.github.io/maildev/) : `http://localhost:1080`
|
|
||||||
|
|
||||||
## 💡 Contributing
|
## 💡 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
|
## 📄 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}
|
container_name: ${COMPOSE_PROJECT_NAME}
|
||||||
build:
|
build:
|
||||||
context: './'
|
context: './'
|
||||||
|
env_file:
|
||||||
|
- '.env'
|
||||||
ports:
|
ports:
|
||||||
- '${PORT}:${PORT}'
|
- '${PORT}:${PORT}'
|
||||||
depends_on:
|
depends_on:
|
||||||
- ${DATABASE_HOST}
|
- 'thream-database'
|
||||||
- 'thream-maildev'
|
|
||||||
volumes:
|
volumes:
|
||||||
- './:/api'
|
- './uploads:/usr/src/app/uploads'
|
||||||
restart: 'unless-stopped'
|
|
||||||
|
|
||||||
thream-phpmyadmin:
|
|
||||||
container_name: 'thream-phpmyadmin'
|
|
||||||
image: 'phpmyadmin/phpmyadmin:5.0.4'
|
|
||||||
environment:
|
|
||||||
PMA_HOST: ${DATABASE_HOST}
|
|
||||||
PMA_PORT: ${DATABASE_PORT}
|
|
||||||
PMA_USER: ${DATABASE_USER}
|
|
||||||
PMA_PASSWORD: ${DATABASE_PASSWORD}
|
|
||||||
ports:
|
|
||||||
- '8000:80'
|
|
||||||
depends_on:
|
|
||||||
- ${DATABASE_HOST}
|
|
||||||
restart: 'unless-stopped'
|
restart: 'unless-stopped'
|
||||||
|
|
||||||
thream-database:
|
thream-database:
|
||||||
container_name: ${DATABASE_HOST}
|
container_name: 'thream-database'
|
||||||
image: 'mysql:8.0.23'
|
image: 'postgres:14.0'
|
||||||
command: '--default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci'
|
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: ${DATABASE_PASSWORD}
|
POSTGRES_USER: 'user'
|
||||||
MYSQL_DATABASE: ${DATABASE_NAME}
|
POSTGRES_PASSWORD: 'password'
|
||||||
MYSQL_TCP_PORT: ${DATABASE_PORT}
|
POSTGRES_DB: 'thream'
|
||||||
ports:
|
|
||||||
- '${DATABASE_PORT}:${DATABASE_PORT}'
|
|
||||||
volumes:
|
volumes:
|
||||||
- 'database-volume:/var/lib/mysql'
|
- 'database-volume:/var/lib/postgresql/data'
|
||||||
restart: 'unless-stopped'
|
|
||||||
|
|
||||||
thream-maildev:
|
|
||||||
container_name: 'thream-maildev'
|
|
||||||
image: 'maildev/maildev:1.1.0'
|
|
||||||
ports:
|
|
||||||
- '1080:80'
|
|
||||||
restart: 'unless-stopped'
|
restart: 'unless-stopped'
|
||||||
|
|
||||||
volumes:
|
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",
|
"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,
|
"private": true,
|
||||||
"release-it": {
|
"repository": {
|
||||||
"git": {
|
"type": "git",
|
||||||
"commit": false,
|
"url": "https://github.com/Thream/api"
|
||||||
"push": false,
|
|
||||||
"tag": false
|
|
||||||
},
|
},
|
||||||
"gitlab": {
|
"engines": {
|
||||||
"release": false
|
"node": ">=16.0.0",
|
||||||
},
|
"npm": ">=8.0.0"
|
||||||
"npm": {
|
|
||||||
"publish": false
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"before:init": [
|
|
||||||
"npm run lint:docker",
|
|
||||||
"npm run lint:editorconfig",
|
|
||||||
"npm run lint:markdown",
|
|
||||||
"npm run lint:typescript",
|
|
||||||
"npm run build",
|
|
||||||
"npm run test"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"plugins": {
|
|
||||||
"@release-it/conventional-changelog": {
|
|
||||||
"preset": "angular",
|
|
||||||
"infile": "CHANGELOG.md"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"preset": "ts-jest",
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"setupFiles": [
|
|
||||||
"./__test__/setEnvsVars.ts"
|
|
||||||
],
|
|
||||||
"setupFilesAfterEnv": [
|
|
||||||
"./__test__/setup.ts"
|
|
||||||
],
|
|
||||||
"rootDir": "./src",
|
|
||||||
"collectCoverage": true,
|
|
||||||
"coverageDirectory": "../coverage/",
|
|
||||||
"coverageReporters": [
|
|
||||||
"text",
|
|
||||||
"cobertura"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ts-standard": {
|
|
||||||
"ignore": [
|
|
||||||
"build",
|
|
||||||
"coverage",
|
|
||||||
"node_modules",
|
|
||||||
"uploads"
|
|
||||||
],
|
|
||||||
"envs": [
|
|
||||||
"node",
|
|
||||||
"jest"
|
|
||||||
],
|
|
||||||
"report": "stylish"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rimraf ./build && tsc",
|
"build": "rimraf ./build && tsc",
|
||||||
"start": "cross-env NODE_ENV=production node build/index.js",
|
"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:commit": "commitlint",
|
||||||
"lint:docker": "dockerfilelint './Dockerfile' && dockerfilelint './Dockerfile.production'",
|
"lint:docker": "dockerfilelint './Dockerfile'",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore node_modules",
|
"lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
|
||||||
"lint:typescript": "ts-standard",
|
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
||||||
"release": "release-it",
|
"lint:staged": "lint-staged",
|
||||||
"test": "jest",
|
"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"
|
"postinstall": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@thream/socketio-jwt": "2.1.0",
|
"@prisma/client": "3.2.1",
|
||||||
"axios": "0.21.1",
|
"@sinclair/typebox": "0.20.5",
|
||||||
|
"axios": "0.22.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"cors": "2.8.5",
|
"dotenv": "10.0.0",
|
||||||
"dotenv": "8.2.0",
|
|
||||||
"ejs": "3.1.6",
|
"ejs": "3.1.6",
|
||||||
"express": "4.17.1",
|
"fastify": "3.22.0",
|
||||||
"express-async-errors": "3.1.1",
|
"fastify-cors": "6.0.2",
|
||||||
"express-fileupload": "1.2.1",
|
"fastify-helmet": "5.3.2",
|
||||||
"express-rate-limit": "5.2.6",
|
"fastify-multipart": "5.0.2",
|
||||||
"express-validator": "6.10.0",
|
"fastify-plugin": "3.0.0",
|
||||||
"helmet": "4.5.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",
|
"jsonwebtoken": "8.5.1",
|
||||||
"morgan": "1.10.0",
|
|
||||||
"ms": "2.1.3",
|
"ms": "2.1.3",
|
||||||
"mysql2": "2.2.5",
|
"nodemailer": "6.6.5",
|
||||||
"nodemailer": "6.5.0",
|
"read-pkg": "5.2.0",
|
||||||
"reflect-metadata": "0.1.13",
|
"socket.io": "4.2.0"
|
||||||
"sequelize": "6.6.2",
|
|
||||||
"sequelize-typescript": "2.1.0",
|
|
||||||
"socket.io": "4.0.1",
|
|
||||||
"swagger-jsdoc": "6.1.0",
|
|
||||||
"swagger-ui-express": "4.1.6",
|
|
||||||
"uuid": "8.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "12.1.1",
|
"@commitlint/cli": "13.2.1",
|
||||||
"@commitlint/config-conventional": "12.1.1",
|
"@commitlint/config-conventional": "13.2.0",
|
||||||
"@release-it/conventional-changelog": "2.0.1",
|
"@saithodev/semantic-release-backmerge": "1.5.3",
|
||||||
"@types/bcryptjs": "2.4.2",
|
"@types/bcryptjs": "2.4.2",
|
||||||
"@types/cors": "2.8.10",
|
"@types/busboy": "0.3.0",
|
||||||
"@types/ejs": "3.0.6",
|
"@types/ejs": "3.1.0",
|
||||||
"@types/express": "4.17.11",
|
"@types/http-errors": "1.8.1",
|
||||||
"@types/express-fileupload": "1.1.6",
|
"@types/jest": "27.0.2",
|
||||||
"@types/express-rate-limit": "5.1.1",
|
"@types/jsonwebtoken": "8.5.5",
|
||||||
"@types/jest": "26.0.22",
|
|
||||||
"@types/jsonwebtoken": "8.5.1",
|
|
||||||
"@types/mock-fs": "4.13.0",
|
|
||||||
"@types/morgan": "1.9.2",
|
|
||||||
"@types/ms": "0.7.31",
|
"@types/ms": "0.7.31",
|
||||||
"@types/node": "14.14.41",
|
"@types/node": "16.10.3",
|
||||||
"@types/nodemailer": "6.4.1",
|
"@types/nodemailer": "6.4.4",
|
||||||
"@types/server-destroy": "1.0.1",
|
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||||
"@types/supertest": "2.0.11",
|
"concurrently": "6.3.0",
|
||||||
"@types/swagger-jsdoc": "6.0.0",
|
|
||||||
"@types/swagger-ui-express": "4.1.2",
|
|
||||||
"@types/uuid": "8.3.0",
|
|
||||||
"@types/validator": "13.1.3",
|
|
||||||
"concurrently": "6.0.2",
|
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"dockerfilelint": "1.8.0",
|
"dockerfilelint": "1.8.0",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"husky": "6.0.0",
|
"eslint": "7.32.0",
|
||||||
"jest": "26.6.3",
|
"eslint-config-prettier": "8.3.0",
|
||||||
"markdownlint-cli": "0.27.1",
|
"eslint-config-standard-with-typescript": "21.0.1",
|
||||||
"mock-fs": "4.13.0",
|
"eslint-plugin-import": "2.24.2",
|
||||||
"nodemon": "2.0.7",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"release-it": "14.6.1",
|
"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",
|
"rimraf": "3.0.2",
|
||||||
"server-destroy": "1.0.1",
|
"semantic-release": "18.0.0",
|
||||||
"socket.io-client": "4.0.1",
|
"ts-jest": "27.0.5",
|
||||||
"sqlite": "4.0.21",
|
"typescript": "4.4.3"
|
||||||
"sqlite3": "5.0.2",
|
|
||||||
"supertest": "6.1.3",
|
|
||||||
"ts-jest": "26.5.5",
|
|
||||||
"ts-standard": "10.0.0",
|
|
||||||
"typescript": "4.2.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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_EXPIRES_IN = '15 minutes'
|
||||||
process.env.JWT_ACCESS_SECRET = 'accessTokenSecret'
|
process.env.JWT_ACCESS_SECRET = 'accessTokenSecret'
|
||||||
process.env.JWT_REFRESH_SECRET = 'refreshTokenSecret'
|
process.env.JWT_REFRESH_SECRET = 'refreshTokenSecret'
|
@ -1,11 +1,8 @@
|
|||||||
import fsMock from 'mock-fs'
|
import { PrismaClient } from '@prisma/client'
|
||||||
import path from 'path'
|
import { mockDeep, mockReset } from 'jest-mock-extended'
|
||||||
import { Sequelize } from 'sequelize-typescript'
|
import { DeepMockProxy } from 'jest-mock-extended/lib/cjs/Mock'
|
||||||
import { Database, open } from 'sqlite'
|
|
||||||
import sqlite3 from 'sqlite3'
|
|
||||||
|
|
||||||
let sqlite: Database | undefined
|
import prisma from '../tools/database/prisma.js'
|
||||||
let sequelize: Sequelize | undefined
|
|
||||||
|
|
||||||
jest.mock('nodemailer', () => ({
|
jest.mock('nodemailer', () => ({
|
||||||
createTransport: () => {
|
createTransport: () => {
|
||||||
@ -15,28 +12,13 @@ jest.mock('nodemailer', () => ({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
beforeAll(async () => {
|
jest.mock('../tools/database/prisma.js', () => ({
|
||||||
sqlite = await open({
|
__esModule: true,
|
||||||
filename: ':memory:',
|
default: mockDeep<PrismaClient>()
|
||||||
driver: sqlite3.Database
|
}))
|
||||||
})
|
|
||||||
sequelize = new Sequelize({
|
beforeEach(() => {
|
||||||
dialect: process.env.DATABASE_DIALECT,
|
mockReset(prismaMock)
|
||||||
storage: process.env.DATABASE_DIALECT === 'sqlite' ? ':memory:' : undefined,
|
|
||||||
logging: false,
|
|
||||||
models: [path.join(__dirname, '..', 'models')]
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
export const prismaMock = prisma as unknown as DeepMockProxy<PrismaClient>
|
||||||
await sequelize?.sync({ force: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
fsMock.restore()
|
|
||||||
})
|
|
||||||
|
|
||||||
afterAll(async () => {
|
|
||||||
await sqlite?.close()
|
|
||||||
await sequelize?.close()
|
|
||||||
})
|
|
||||||
|
@ -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 dotenv from 'dotenv'
|
||||||
import express, { Request } from 'express'
|
import fastify from 'fastify'
|
||||||
import rateLimit from 'express-rate-limit'
|
import fastifyCors from 'fastify-cors'
|
||||||
import helmet from 'helmet'
|
import fastifySwagger from 'fastify-swagger'
|
||||||
import morgan from 'morgan'
|
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 { services } from './services/index.js'
|
||||||
import { router } from './services'
|
import { swaggerOptions } from './tools/configurations/swaggerOptions.js'
|
||||||
import { NotFoundError } from './tools/errors/NotFoundError'
|
import fastifySocketIo from './tools/plugins/socket-io.js'
|
||||||
import { TooManyRequestsError } from './tools/errors/TooManyRequestsError'
|
import { UPLOADS_URL } from './tools/configurations/index.js'
|
||||||
|
|
||||||
const application = express()
|
export const application = fastify({
|
||||||
|
logger: process.env.NODE_ENV === 'development'
|
||||||
|
})
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
const main = async (): Promise<void> => {
|
||||||
application.use(morgan<Request>('dev'))
|
await application.register(fastifyCors)
|
||||||
} else if (process.env.NODE_ENV === 'production') {
|
await application.register(fastifySensible)
|
||||||
const requestPerSecond = 2
|
await application.register(fastifyUrlData)
|
||||||
const seconds = 60
|
await application.register(fastifySocketIo, {
|
||||||
const windowMs = seconds * 1000
|
cors: {
|
||||||
application.enable('trust proxy')
|
origin: '*',
|
||||||
application.use(
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||||
rateLimit({
|
preflightContinue: false,
|
||||||
windowMs,
|
optionsSuccessStatus: 204
|
||||||
max: seconds * requestPerSecond,
|
|
||||||
handler: () => {
|
|
||||||
throw new TooManyRequestsError()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
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())
|
main().catch((error) => {
|
||||||
application.use(helmet())
|
console.error(error)
|
||||||
application.use(cors<Request>())
|
process.exit(1)
|
||||||
application.use(router)
|
|
||||||
application.use(() => {
|
|
||||||
throw new NotFoundError()
|
|
||||||
})
|
})
|
||||||
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'
|
const main = async (): Promise<void> => {
|
||||||
import { socket } from './tools/socket'
|
const address = await application.listen(PORT, HOST)
|
||||||
import { sequelize } from './tools/database/sequelize'
|
console.log('\x1b[36m%s\x1b[0m', `🚀 Server listening at ${address}`)
|
||||||
|
}
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT ?? '8080', 10)
|
main().catch((error) => {
|
||||||
|
console.error(error)
|
||||||
sequelize
|
process.exit(1)
|
||||||
.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))
|
|
||||||
|
@ -1,55 +1,23 @@
|
|||||||
import {
|
import { Type } from '@sinclair/typebox'
|
||||||
BelongsTo,
|
import { Channel } from '@prisma/client'
|
||||||
Column,
|
|
||||||
DataType,
|
|
||||||
ForeignKey,
|
|
||||||
HasMany,
|
|
||||||
Model,
|
|
||||||
Table
|
|
||||||
} from 'sequelize-typescript'
|
|
||||||
|
|
||||||
import Guild from './Guild'
|
import { date, id } from './utils.js'
|
||||||
import Message from './Message'
|
import { guildExample } from './Guild.js'
|
||||||
|
|
||||||
export const channelTypes = ['text', 'voice'] as const
|
export const types = [Type.Literal('text')]
|
||||||
export type ChannelType = typeof channelTypes[number]
|
|
||||||
|
|
||||||
@Table
|
export const channelSchema = {
|
||||||
export default class Channel extends Model {
|
id,
|
||||||
@Column({
|
name: Type.String({ maxLength: 255 }),
|
||||||
type: DataType.STRING,
|
createdAt: date.createdAt,
|
||||||
allowNull: false
|
updatedAt: date.updatedAt,
|
||||||
})
|
guildId: id
|
||||||
name!: string
|
}
|
||||||
|
|
||||||
@Column({
|
export const channelExample: Channel = {
|
||||||
type: DataType.STRING,
|
id: 1,
|
||||||
allowNull: false,
|
name: 'general',
|
||||||
defaultValue: 'text'
|
guildId: guildExample.id,
|
||||||
})
|
createdAt: new Date(),
|
||||||
type!: ChannelType
|
updatedAt: new Date()
|
||||||
|
|
||||||
@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[]
|
|
||||||
}
|
}
|
||||||
|
@ -1,45 +1,22 @@
|
|||||||
import { Column, DataType, HasMany, Model, Table } from 'sequelize-typescript'
|
import { Guild } from '@prisma/client'
|
||||||
import { guildsIconPath } from '../tools/configurations/constants'
|
import { Type } from '@sinclair/typebox'
|
||||||
|
|
||||||
import Channel from './Channel'
|
import { date, id } from './utils.js'
|
||||||
import Invitation from './Invitation'
|
|
||||||
import Member from './Member'
|
|
||||||
|
|
||||||
@Table
|
export const guildSchema = {
|
||||||
export default class Guild extends Model {
|
id,
|
||||||
@Column({
|
name: Type.String({ minLength: 3, maxLength: 30 }),
|
||||||
type: DataType.STRING,
|
icon: Type.String({ format: 'uri-reference' }),
|
||||||
allowNull: false
|
description: Type.String({ maxLength: 160 }),
|
||||||
})
|
createdAt: date.createdAt,
|
||||||
name!: string
|
updatedAt: date.updatedAt
|
||||||
|
}
|
||||||
@Column({
|
|
||||||
type: DataType.STRING,
|
export const guildExample: Guild = {
|
||||||
allowNull: false,
|
id: 1,
|
||||||
defaultValue: ''
|
name: 'GuildExample',
|
||||||
})
|
description: 'guild example.',
|
||||||
description!: string
|
icon: null,
|
||||||
|
createdAt: new Date(),
|
||||||
@Column({
|
updatedAt: new Date()
|
||||||
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[]
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
import { Type } from '@sinclair/typebox'
|
||||||
BelongsTo,
|
import { Member } from '@prisma/client'
|
||||||
Column,
|
|
||||||
DataType,
|
|
||||||
ForeignKey,
|
|
||||||
HasMany,
|
|
||||||
Model,
|
|
||||||
Table
|
|
||||||
} from 'sequelize-typescript'
|
|
||||||
|
|
||||||
import Channel from './Channel'
|
import { date, id } from './utils.js'
|
||||||
import Guild from './Guild'
|
import { guildExample } from './Guild.js'
|
||||||
import Message from './Message'
|
import { userExample } from './User.js'
|
||||||
import User from './User'
|
|
||||||
|
|
||||||
@Table
|
export const memberSchema = {
|
||||||
export default class Member extends Model {
|
id,
|
||||||
@Column({
|
isOwner: Type.Boolean({ default: false }),
|
||||||
type: DataType.BOOLEAN,
|
createdAt: date.createdAt,
|
||||||
allowNull: false,
|
updatedAt: date.updatedAt,
|
||||||
defaultValue: false
|
userId: id,
|
||||||
})
|
guildId: id
|
||||||
isOwner!: boolean
|
}
|
||||||
|
|
||||||
@ForeignKey(() => Channel)
|
export const memberExample: Member = {
|
||||||
@Column
|
id: 1,
|
||||||
lastVisitedChannelId!: number
|
isOwner: true,
|
||||||
|
userId: userExample.id,
|
||||||
@BelongsTo(() => Channel)
|
guildId: guildExample.id,
|
||||||
channel!: Channel
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
@ForeignKey(() => User)
|
|
||||||
@Column
|
|
||||||
userId!: number
|
|
||||||
|
|
||||||
@BelongsTo(() => User)
|
|
||||||
user!: User
|
|
||||||
|
|
||||||
@ForeignKey(() => Guild)
|
|
||||||
@Column
|
|
||||||
guildId!: number
|
|
||||||
|
|
||||||
@BelongsTo(() => Guild)
|
|
||||||
guild!: Guild
|
|
||||||
|
|
||||||
@HasMany(() => Message, { onDelete: 'CASCADE' })
|
|
||||||
messages!: Message[]
|
|
||||||
}
|
}
|
||||||
|
@ -1,51 +1,20 @@
|
|||||||
import {
|
import { Type } from '@sinclair/typebox'
|
||||||
BelongsTo,
|
|
||||||
Column,
|
|
||||||
DataType,
|
|
||||||
ForeignKey,
|
|
||||||
Model,
|
|
||||||
Table
|
|
||||||
} from 'sequelize-typescript'
|
|
||||||
|
|
||||||
import Channel from './Channel'
|
import { date, id } from './utils.js'
|
||||||
import Member from './Member'
|
|
||||||
|
|
||||||
export const messageTypes = ['text', 'file'] as const
|
export const types = [Type.Literal('text'), Type.Literal('file')]
|
||||||
export type MessageType = typeof messageTypes[number]
|
|
||||||
|
|
||||||
@Table
|
export const messageSchema = {
|
||||||
export default class Message extends Model {
|
id,
|
||||||
@Column({
|
value: Type.String(),
|
||||||
type: DataType.TEXT,
|
type: Type.Union(types, { default: 'text' }),
|
||||||
allowNull: false
|
mimetype: Type.String({
|
||||||
})
|
maxLength: 255,
|
||||||
value!: string
|
default: 'text/plain',
|
||||||
|
format: 'mimetype'
|
||||||
@Column({
|
}),
|
||||||
type: DataType.STRING,
|
createdAt: date.createdAt,
|
||||||
allowNull: false,
|
updatedAt: date.updatedAt,
|
||||||
defaultValue: 'text'
|
memberId: id,
|
||||||
})
|
channelId: id
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,25 @@
|
|||||||
import {
|
import { Type } from '@sinclair/typebox'
|
||||||
BelongsTo,
|
|
||||||
Column,
|
|
||||||
DataType,
|
|
||||||
ForeignKey,
|
|
||||||
Model,
|
|
||||||
Table
|
|
||||||
} from 'sequelize-typescript'
|
|
||||||
|
|
||||||
import User from './User'
|
import { date, id } from './utils.js'
|
||||||
|
|
||||||
export const providers = ['google', 'github', 'discord'] as const
|
export const providers = ['google', 'github', 'discord'] as const
|
||||||
export const strategies = [...providers, 'local'] 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 ProviderOAuth = typeof providers[number]
|
||||||
export type AuthenticationStrategy = typeof strategies[number]
|
export type AuthenticationStrategy = typeof strategies[number]
|
||||||
|
|
||||||
@Table
|
export const oauthSchema = {
|
||||||
export default class OAuth extends Model {
|
id,
|
||||||
@Column({
|
providerId: Type.String(),
|
||||||
type: DataType.STRING,
|
provider: Type.Union([...providersTypebox]),
|
||||||
allowNull: false
|
createdAt: date.createdAt,
|
||||||
})
|
updatedAt: date.updatedAt,
|
||||||
provider!: ProviderOAuth
|
userId: id
|
||||||
|
|
||||||
@Column({
|
|
||||||
type: DataType.TEXT,
|
|
||||||
allowNull: false
|
|
||||||
})
|
|
||||||
providerId!: string
|
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
|
||||||
@Column
|
|
||||||
userId!: number
|
|
||||||
|
|
||||||
@BelongsTo(() => User)
|
|
||||||
user!: User
|
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,21 @@
|
|||||||
import {
|
import { RefreshToken } from '@prisma/client'
|
||||||
BelongsTo,
|
import { Type } from '@sinclair/typebox'
|
||||||
Column,
|
|
||||||
DataType,
|
|
||||||
ForeignKey,
|
|
||||||
Model,
|
|
||||||
Table
|
|
||||||
} from 'sequelize-typescript'
|
|
||||||
|
|
||||||
import User from './User'
|
import { userExample } from './User.js'
|
||||||
|
import { date, id } from './utils.js'
|
||||||
|
|
||||||
@Table
|
export const refreshTokensSchema = {
|
||||||
export default class RefreshToken extends Model {
|
id,
|
||||||
@Column({
|
token: Type.String(),
|
||||||
type: DataType.TEXT,
|
createdAt: date.createdAt,
|
||||||
allowNull: false
|
updatedAt: date.updatedAt,
|
||||||
})
|
userId: id
|
||||||
token!: string
|
}
|
||||||
|
|
||||||
@ForeignKey(() => User)
|
export const refreshTokenExample: RefreshToken = {
|
||||||
@Column
|
id: 1,
|
||||||
userId!: number
|
userId: userExample.id,
|
||||||
|
token: 'sometoken',
|
||||||
@BelongsTo(() => User)
|
createdAt: new Date(),
|
||||||
user!: User
|
updatedAt: new Date()
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,9 @@
|
|||||||
import {
|
import { User } from '@prisma/client'
|
||||||
Column,
|
import { Static, Type } from '@sinclair/typebox'
|
||||||
DataType,
|
|
||||||
HasMany,
|
|
||||||
HasOne,
|
|
||||||
Model,
|
|
||||||
Table
|
|
||||||
} from 'sequelize-typescript'
|
|
||||||
|
|
||||||
import Member from './Member'
|
import { AuthenticationStrategy, strategiesTypebox } from './OAuth.js'
|
||||||
import OAuth, { AuthenticationStrategy } from './OAuth'
|
import { userSettingsSchema } from './UserSettings.js'
|
||||||
import RefreshToken from './RefreshToken'
|
import { date, id } from './utils.js'
|
||||||
import UserSetting from './UserSetting'
|
|
||||||
import { deleteObjectAttributes } from '../tools/utils/deleteObjectAttributes'
|
|
||||||
import { usersLogoPath } from '../tools/configurations/constants'
|
|
||||||
|
|
||||||
export const userHiddenAttributes = [
|
|
||||||
'password',
|
|
||||||
'tempToken',
|
|
||||||
'tempExpirationToken'
|
|
||||||
] as const
|
|
||||||
export type UserHiddenAttributes = typeof userHiddenAttributes[number]
|
|
||||||
export interface UserToJSON extends Omit<User, UserHiddenAttributes> {}
|
|
||||||
|
|
||||||
export interface UserJWT {
|
export interface UserJWT {
|
||||||
id: number
|
id: number
|
||||||
@ -33,80 +16,66 @@ export interface UserRequest {
|
|||||||
accessToken: string
|
accessToken: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@Table
|
export const userSchema = {
|
||||||
export default class User extends Model {
|
id,
|
||||||
@Column({
|
name: Type.String({ minLength: 1, maxLength: 30 }),
|
||||||
type: DataType.STRING,
|
email: Type.String({ minLength: 1, maxLength: 255, format: 'email' }),
|
||||||
allowNull: false
|
password: Type.String(),
|
||||||
})
|
logo: Type.String({ format: 'uri-reference' }),
|
||||||
name!: string
|
status: Type.String({ maxLength: 255 }),
|
||||||
|
biography: Type.String(),
|
||||||
@Column({
|
website: Type.String({ maxLength: 255, format: 'uri-reference' }),
|
||||||
type: DataType.STRING,
|
isConfirmed: Type.Boolean({ default: false }),
|
||||||
allowNull: true
|
temporaryToken: Type.String(),
|
||||||
})
|
temporaryExpirationToken: Type.String({ format: 'date-time' }),
|
||||||
email?: string
|
createdAt: date.createdAt,
|
||||||
|
updatedAt: date.updatedAt
|
||||||
@Column({
|
}
|
||||||
type: DataType.TEXT,
|
|
||||||
allowNull: true
|
export const userPublicSchema = {
|
||||||
})
|
id,
|
||||||
password?: string
|
name: userSchema.name,
|
||||||
|
email: Type.Optional(userSchema.email),
|
||||||
@Column({
|
logo: Type.Optional(userSchema.logo),
|
||||||
type: DataType.STRING,
|
status: Type.Optional(userSchema.status),
|
||||||
allowNull: false,
|
biography: Type.Optional(userSchema.biography),
|
||||||
defaultValue: ''
|
website: Type.Optional(userSchema.website),
|
||||||
})
|
isConfirmed: userSchema.isConfirmed,
|
||||||
status!: string
|
createdAt: date.createdAt,
|
||||||
|
updatedAt: date.updatedAt,
|
||||||
@Column({
|
settings: Type.Optional(Type.Object(userSettingsSchema))
|
||||||
type: DataType.STRING,
|
}
|
||||||
allowNull: false,
|
|
||||||
defaultValue: ''
|
export const userCurrentSchema = Type.Object({
|
||||||
})
|
user: Type.Object({
|
||||||
biography!: string
|
...userPublicSchema,
|
||||||
|
currentStrategy: Type.Union([...strategiesTypebox]),
|
||||||
@Column({
|
strategies: Type.Array(Type.Union([...strategiesTypebox]))
|
||||||
type: DataType.TEXT,
|
})
|
||||||
allowNull: false,
|
})
|
||||||
defaultValue: `${usersLogoPath.name}/default.png`
|
|
||||||
})
|
export const bodyUserSchema = Type.Object({
|
||||||
logo!: string
|
email: userSchema.email,
|
||||||
|
name: userSchema.name,
|
||||||
@Column({
|
password: userSchema.password,
|
||||||
type: DataType.BOOLEAN,
|
theme: userSettingsSchema.theme,
|
||||||
allowNull: false,
|
language: userSettingsSchema.language
|
||||||
defaultValue: false
|
})
|
||||||
})
|
|
||||||
isConfirmed!: boolean
|
export type BodyUserSchemaType = Static<typeof bodyUserSchema>
|
||||||
|
|
||||||
@Column({
|
export const userExample: User = {
|
||||||
type: DataType.TEXT,
|
id: 1,
|
||||||
allowNull: true
|
name: 'Divlo',
|
||||||
})
|
email: 'contact@divlo.fr',
|
||||||
tempToken?: string | null
|
password: 'somepassword',
|
||||||
|
logo: null,
|
||||||
@Column({
|
status: null,
|
||||||
type: DataType.BIGINT,
|
biography: null,
|
||||||
allowNull: true
|
website: null,
|
||||||
})
|
isConfirmed: true,
|
||||||
tempExpirationToken?: number | null
|
temporaryToken: 'temporaryUUIDtoken',
|
||||||
|
temporaryExpirationToken: new Date(),
|
||||||
@HasMany(() => RefreshToken, { onDelete: 'CASCADE' })
|
createdAt: new Date(),
|
||||||
refreshTokens!: RefreshToken[]
|
updatedAt: new Date()
|
||||||
|
|
||||||
@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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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