Compare commits

..

38 Commits

Author SHA1 Message Date
semantic-release-bot
4de97e23fc
chore(release): 1.2.10 [skip ci] 2023-12-28 04:09:20 +00:00
cec2e803ef
chore: update semantic-release tooling 2023-12-28 05:08:26 +01:00
0847b72ed5
fix: deprecation notice 2023-12-28 05:06:48 +01:00
2aefe73afa
chore: better Prettier config for easier reviews 2023-10-23 23:38:50 +02:00
semantic-release-bot
5591449094
chore(release): 1.2.9 [skip ci] 2023-09-18 20:41:25 +00:00
5532a6a398
fix: update dependencies to latest 2023-09-18 22:36:35 +02:00
2bdcae81b5
build(deps): update latest 2023-08-24 22:17:26 +02:00
cc9fa70498
chore: rename docker-compose.yml to compose.yaml
Ref: https://docs.docker.com/compose/compose-file/03-compose-file/
2023-07-28 12:23:57 +02:00
semantic-release-bot
59cd6083a6
chore(release): 1.2.8 [skip ci] 2023-07-22 14:31:14 +00:00
9a1684e22b
fix: update dependencies to latest 2023-07-22 16:26:27 +02:00
semantic-release-bot
23d2a9da71
chore(release): 1.2.7 [skip ci] 2023-07-02 16:58:58 +00:00
8e238f80c5
docs: use ssh to clone the repository 2023-07-02 18:54:02 +02:00
ca7de85e4b
chore: enable source maps for easier debugging 2023-07-02 18:52:01 +02:00
ab19598edd
fix: update author - Théo LUDWIG 2023-07-02 18:46:58 +02:00
1c1644f243
fix: update dependencies to latest 2023-07-02 18:45:54 +02:00
semantic-release-bot
10110d1a36
chore(release): 1.2.6 [skip ci] 2023-05-13 18:12:41 +00:00
Divlo
73d2da66b2
fix: update dependencies to latest 2023-05-13 20:09:02 +02:00
semantic-release-bot
b07a62de8b
chore(release): 1.2.5 [skip ci] 2023-04-02 21:49:01 +00:00
Divlo
8a327eb7c7
fix: update dependencies to latest 2023-04-02 23:45:47 +02:00
semantic-release-bot
78d7dbdb3f
chore(release): 1.2.4 [skip ci] 2023-01-11 17:04:21 +00:00
Divlo
e30a66eeb6
fix: update dependencies to latest 2023-01-11 18:02:38 +01:00
semantic-release-bot
12dcabccb3
chore(release): 1.2.3 [skip ci] 2022-12-13 10:40:30 +00:00
Divlo
224d3b3764
fix: dependencies security vulnerabilities (fastify) 2022-12-13 10:38:59 +00:00
semantic-release-bot
fdecf5ce1a
chore(release): 1.2.2 [skip ci] 2022-11-08 11:22:53 +00:00
Divlo
6926132a1b
fix: dependencies security vulnerabilities 2022-11-08 11:16:24 +00:00
semantic-release-bot
e1543becc5
chore(release): 1.2.1 [skip ci] 2022-10-04 13:06:50 +00:00
Divlo
b985172cd0
fix: schemaValidationMessages errors in Swagger docs 2022-10-04 14:59:14 +02:00
Divlo
8ac1696ca0
docs: add information for development 2022-10-04 14:58:12 +02:00
Divlo
de34618a7c
chore: simplify Docker setup 2022-08-30 17:57:29 +02:00
Divlo
400dc7ec2a
chore: fix Dockerfile 2022-08-30 17:53:57 +02:00
semantic-release-bot
49ac4f6ca4
chore(release): 1.2.0 [skip ci] 2022-08-29 17:47:08 +00:00
Divlo
8e69511e3e
docs: add oauth2 tag 2022-08-29 17:37:00 +00:00
Divlo
7e305429b4
feat: make JWT refreshTokens more secure
Don't store the token itself in the database, store a UUID, and when refreshing the accessToken, verify the token and verify that in the payload there is a corresponding UUID stored in the database
2022-08-29 17:26:43 +00:00
Divlo
b71da7dcc9
fix: on password reset, delete all refresh tokens 2022-08-29 16:32:24 +00:00
Divlo
a6dd112e4a
refactor: minor changes 2022-08-29 16:10:17 +00:00
Divlo
ab94d1e656
ci: fix prisma:validate error in CI 2022-08-23 23:58:38 +02:00
Divlo
8483cd4772
ci: usage of ubuntu-latest 2022-08-23 23:57:52 +02:00
Divlo
46745e1b7e
build(deps): update latest 2022-08-23 21:53:07 +00:00
160 changed files with 11804 additions and 23758 deletions

View File

@ -1 +1,3 @@
{ "extends": ["@commitlint/config-conventional"] } {
"extends": ["@commitlint/config-conventional"]
}

View File

@ -1,2 +1 @@
ARG VARIANT="16" FROM mcr.microsoft.com/devcontainers/javascript-node:20
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}

View File

@ -0,0 +1,23 @@
services:
workspace:
build:
context: "./"
dockerfile: "./Dockerfile"
volumes:
- "..:/workspace:cached"
command: "sleep infinity"
network_mode: "host"
thream-database:
image: "postgres:15.4"
environment:
POSTGRES_USER: "thream_user"
POSTGRES_PASSWORD: "password"
POSTGRES_DB: "thream"
volumes:
- "thream-postgres-data:/var/lib/postgresql/data"
restart: "unless-stopped"
network_mode: "host"
volumes:
thream-postgres-data:

View File

@ -1,10 +1,14 @@
{ {
"name": "@thream/api", "name": "@thream/api",
"dockerComposeFile": "./docker-compose.yml", "dockerComposeFile": "./compose.yaml",
"service": "workspace", "service": "workspace",
"workspaceFolder": "/workspace", "workspaceFolder": "/workspace",
"customizations": {
"vscode": {
"settings": { "settings": {
"remote.autoForwardPorts": false "remote.autoForwardPorts": false,
"remote.localPortHost": "allInterfaces"
}
}, },
"extensions": [ "extensions": [
"editorconfig.editorconfig", "editorconfig.editorconfig",
@ -14,8 +18,7 @@
"prisma.prisma", "prisma.prisma",
"mikestead.dotenv", "mikestead.dotenv",
"ms-azuretools.vscode-docker" "ms-azuretools.vscode-docker"
], ]
"forwardPorts": [8080, 5555, 5432, 1080], },
"postAttachCommand": ["npm", "install"],
"remoteUser": "node" "remoteUser": "node"
} }

View File

@ -1,30 +0,0 @@
version: '3.0'
services:
workspace:
build:
context: './'
dockerfile: './Dockerfile'
volumes:
- '..:/workspace:cached'
command: 'sleep infinity'
extra_hosts:
- 'host.docker.internal:host-gateway'
thream-database:
image: 'postgres:14.2'
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:

View File

@ -1,9 +1,6 @@
.vscode .*
.git !.npmrc
.env !.swcrc
build build
coverage coverage
.nyc_output
node_modules node_modules
tmp
temp

View File

@ -1,20 +1,29 @@
API_URL=http://localhost:8080 API_URL=http://127.0.0.1:8080
COMPOSE_PROJECT_NAME=thream-api COMPOSE_PROJECT_NAME=thream-api
DATABASE_URL=postgresql://user:password@thream-database:5432/thream
DATABASE_USER=thream_user
DATABASE_PASSWORD=password
DATABASE_NAME=thream
DATABASE_URL=postgresql://thream_user:password@127.0.0.1:5432/thream
EMAIL_HOST=0.0.0.0
EMAIL_PASSWORD=password
EMAIL_PORT=1025
EMAIL_USER=no-reply@thream.fr
DISCORD_CLIENT_ID= DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET= DISCORD_CLIENT_SECRET=
EMAIL_HOST=thream-maildev
EMAIL_PASSWORD=password
EMAIL_PORT=25
EMAIL_USER=no-reply@thream.fr
FILE_UPLOADS_API_KEY=apiKeySecret FILE_UPLOADS_API_KEY=apiKeySecret
FILE_UPLOADS_API_URL=http://host.docker.internal:8000 FILE_UPLOADS_API_URL=http://127.0.0.1:8000
GITHUB_CLIENT_ID= GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET= GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID= GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET= GOOGLE_CLIENT_SECRET=
HOST=0.0.0.0 HOST=0.0.0.0
JWT_ACCESS_EXPIRES_IN=15 minutes JWT_ACCESS_EXPIRES_IN=15 minutes
# You can generate JWT secrets with the `npm run generate:jwt-secret` command.
JWT_ACCESS_SECRET=accessTokenSecret JWT_ACCESS_SECRET=accessTokenSecret
JWT_REFRESH_SECRET=refreshTokenSecret JWT_REFRESH_SECRET=refreshTokenSecret
NODE_ENV=development NODE_ENV=development

View File

@ -1,16 +1,13 @@
{ {
"extends": ["conventions", "prettier"], "extends": ["conventions", "prettier"],
"plugins": ["prettier", "import", "unicorn"], "plugins": ["prettier", "import", "unicorn"],
"parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },
"env": {
"node": true
},
"rules": { "rules": {
"prettier/prettier": "error", "prettier/prettier": "error",
"import/extensions": ["error", "always"], "import/extensions": ["error", "always"],
"unicorn/prevent-abbreviations": "error", "unicorn/prevent-abbreviations": "error"
"unicorn/prefer-node-protocol": "error"
} }
} }

View File

@ -1,8 +1,8 @@
--- ---
name: '🐛 Bug Report' name: "🐛 Bug Report"
about: 'Report an unexpected problem or unintended behavior.' about: "Report an unexpected problem or unintended behavior."
title: '[Bug]' title: "[Bug]"
labels: 'bug' labels: "bug"
--- ---
<!-- <!--

View File

@ -1,8 +1,8 @@
--- ---
name: '📜 Documentation' name: "📜 Documentation"
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: '[Documentation]' title: "[Documentation]"
labels: 'documentation' labels: "documentation"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '✨ Feature Request' name: "✨ Feature Request"
about: 'Suggest a new feature idea.' about: "Suggest a new feature idea."
title: '[Feature]' title: "[Feature]"
labels: 'feature request' labels: "feature request"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🔧 Improvement' name: "🔧 Improvement"
about: 'Improve structure/format/performance/refactor/tests of the code.' about: "Improve structure/format/performance/refactor/tests of the code."
title: '[Improvement]' title: "[Improvement]"
labels: 'improvement' labels: "improvement"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🙋 Question' name: "🙋 Question"
about: 'Further information is requested.' about: "Further information is requested."
title: '[Question]' title: "[Question]"
labels: 'question' labels: "question"
--- ---
### Question ### Question

View File

@ -1,6 +1,6 @@
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. --> <!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
## What changes this PR introduce? # What changes this PR introduce?
## List any relevant issue numbers ## List any relevant issue numbers

View File

@ -1,4 +1,4 @@
name: 'Analyze' name: "Analyze"
on: on:
push: push:
@ -8,20 +8,20 @@ on:
jobs: jobs:
analyze: analyze:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: ['javascript'] language: ["javascript"]
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: "actions/checkout@v4.0.0"
- name: 'Initialize CodeQL' - name: "Initialize CodeQL"
uses: 'github/codeql-action/init@v1' uses: "github/codeql-action/init@v2"
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
- name: 'Perform CodeQL Analysis' - name: "Perform CodeQL Analysis"
uses: 'github/codeql-action/analyze@v1' uses: "github/codeql-action/analyze@v1"

View File

@ -1,4 +1,4 @@
name: 'Build' name: "Build"
on: on:
push: push:
@ -8,18 +8,20 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: "actions/checkout@v4.0.0"
- name: 'Use Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.0.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '16.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install' - name: "Install dependencies"
run: 'npm install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- run: "npm run build:typescript"

View File

@ -1,4 +1,4 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
@ -8,40 +8,33 @@ on:
jobs: jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: "actions/checkout@v4.0.0"
- name: 'Use Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.0.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '16.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install' - name: "Install dependencies"
run: 'npm install' run: "npm clean-install"
- name: 'lint:commit' - name: "lint:commit"
run: 'npm run lint:commit -- --to "${{ github.sha }}"' run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- name: 'lint:editorconfig' - name: "lint:editorconfig"
run: 'npm run lint:editorconfig' run: "npm run lint:editorconfig"
- name: 'lint:markdown' - name: "lint:markdown"
run: 'npm run lint:markdown' run: "npm run lint:markdown"
- name: 'lint:typescript' - name: "lint:eslint"
run: 'npm run lint:typescript' run: "npm run lint:eslint"
- name: 'lint:prettier' - name: "lint:prettier"
run: 'npm run lint:prettier' run: "npm run lint:prettier"
- name: 'lint:dotenv' - name: "prisma:validate"
uses: 'dotenv-linter/action-dotenv-linter@v2' run: "cp .env.example .env && npm run prisma:validate"
with:
github_token: ${{ secrets.github_token }}
- name: 'lint:docker'
uses: 'hadolint/hadolint-action@v1.6.0'
with:
dockerfile: './Dockerfile'

View File

@ -1,4 +1,4 @@
name: 'Release' name: "Release"
on: on:
push: push:
@ -6,34 +6,36 @@ on:
jobs: jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: "actions/checkout@v4.0.0"
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: 'Import GPG key' - name: "Import GPG key"
uses: 'crazy-max/ghaction-import-gpg@v3.2.0' uses: "crazy-max/ghaction-import-gpg@v6.0.0"
with: with:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
git-user-signingkey: true git_user_signingkey: true
git-commit-gpgsign: true git_commit_gpgsign: true
- name: 'Use Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.0.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '16.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install' - name: "Install dependencies"
run: 'npm install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- name: 'Release' - run: "npm run build:typescript"
run: 'npm run release'
- name: "Release"
run: "npm run release"
env: env:
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }} GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}

View File

@ -1,4 +1,4 @@
name: 'Test' name: "Test"
on: on:
push: push:
@ -8,23 +8,23 @@ on:
jobs: jobs:
test: test:
runs-on: 'macos-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.0.0' - uses: "actions/checkout@v4.0.0"
- name: 'Use Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.0.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: '16.x' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install' - name: "Install dependencies"
run: 'npm install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- run: 'cp .env.example .env' - run: "cp .env.example .env"
- name: 'Test' - name: "Test"
run: 'npm run test' run: "npm run test"

1
.gitignore vendored
View File

@ -35,3 +35,4 @@ npm-debug.log*
# misc # misc
.DS_Store .DS_Store
*.hbs

View File

@ -3,3 +3,4 @@
npm run lint:staged npm run lint:staged
npm run build npm run build
npm run build:typescript

View File

@ -2,5 +2,6 @@
"*": ["editorconfig-checker"], "*": ["editorconfig-checker"],
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"], "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
"*.{json,jsonc,yml,yaml}": ["prettier --write"], "*.{json,jsonc,yml,yaml}": ["prettier --write"],
"*.md": ["prettier --write", "markdownlint --dot --fix"] "*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"],
"prisma/schema.prisma": ["prisma validate"]
} }

11
.markdownlint-cli2.jsonc Normal file
View File

@ -0,0 +1,11 @@
{
"config": {
"extends": "markdownlint/style/prettier",
"relative-links": true,
"default": true,
"MD033": false
},
"globs": ["**/*.{md,mdx}"],
"ignores": ["**/node_modules"],
"customRules": ["markdownlint-rule-relative-links"]
}

View File

@ -1,6 +0,0 @@
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false
}

View File

@ -1,5 +0,0 @@
{
"reporter": ["text", "cobertura"],
"src": "./build",
"all": true
}

View File

@ -1,6 +0,0 @@
build
node_modules
coverage
package.json
package-lock.json
*.hbs

View File

@ -1,6 +1,3 @@
{ {
"singleQuote": true, "semi": false
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

15
.swcrc
View File

@ -1,22 +1,13 @@
{ {
"sourceMaps": true,
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",
"decorators": true,
"dynamicImport": true "dynamicImport": true
}, },
"transform": { "target": "esnext"
"legacyDecorator": true,
"decoratorMetadata": true
},
"target": "es2022",
"loose": true
}, },
"module": { "module": {
"type": "es6", "type": "es6"
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
} }
} }

8
.taprc
View File

@ -1,8 +0,0 @@
ts: false
jsx: false
flow: false
check-coverage: false
coverage: false
files:
- 'build/**/*.test.js'

View File

@ -5,7 +5,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": "explicit"
}, },
"eslint.options": { "ignorePath": ".gitignore" }, "eslint.options": { "ignorePath": ".gitignore" },
"[prisma]": { "[prisma]": {

View File

@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at reported to the community leaders responsible for enforcement at
contact@divlo.fr. <contact@theoludwig.fr>.
All complaints will be reviewed and investigated promptly and fairly. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the

View File

@ -29,38 +29,14 @@ If you're adding new features to **Thream/api**, please include tests.
## Commits ## Commits
The commit message guidelines respect The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases.
[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)
and [Semantic Versioning](https://semver.org/) for releases.
### Types
Types define which kind of changes you made to the project.
| Types | Description |
| -------- | ------------------------------------------------------------------------------------------------------------ |
| feat | A new feature. |
| fix | A bug fix. |
| docs | Documentation only changes. |
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
| refactor | A code change that neither fixes a bug nor adds a feature. |
| perf | A code change that improves performance. |
| test | Adding missing tests or correcting existing tests. |
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
| chore | Other changes that don't modify src or test files. |
| revert | Reverts a previous commit. |
### Scopes
Scopes define what part of the code changed.
### Examples ### Examples
```sh ```sh
git commit -m "feat(services): add POST /users/signup" git commit -m "feat: add POST /users/signup"
git commit -m "docs(readme): update installation process" git commit -m "docs(readme): update installation process"
git commit -m "fix(services): should emit events to connected users" git commit -m "fix: should emit events to connected users"
``` ```
## Directory Structure ## Directory Structure
@ -86,13 +62,12 @@ git commit -m "fix(services): should emit events to connected users"
- `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
- `uploads` : uploaded files by users
### Services folder explained with an example ### Services folder explained with an example
We have API REST services for the `channels`. We have API REST services for the `channels`.
Here is what potentially look like a folder structure for this service : Here is what potentially look like a folder structure for this service:
```text ```text
└── src └── src
@ -111,7 +86,7 @@ Here is what potentially look like a folder structure for this service :
└── index.ts └── index.ts
``` ```
This folder structure will map to these REST API routes : This folder structure will map to these REST API routes:
- GET `/channels` - GET `/channels`
- DELETE `/channels/:channelId` - DELETE `/channels/:channelId`
@ -121,3 +96,5 @@ The folders after `src/services` : is the real path of the routes in the API exc
folders starting and ending with `__` like `__test__` or `__utils__`. 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`).
You can generate the boilerplate code for a new service with the `npm run generate` command.

View File

@ -1,23 +1,22 @@
FROM node:16.14.2 AS dependencies FROM node:20.9.0 AS dependencies
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./package*.json ./ COPY ./package*.json ./
RUN npm install RUN npm clean-install
FROM node:16.14.2 AS builder FROM node:20.9.0 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY --from=dependencies /usr/src/app/node_modules ./node_modules COPY --from=dependencies /usr/src/app/node_modules ./node_modules
COPY ./ ./ COPY ./ ./
RUN npm run prisma:generate && npm run build RUN npm run prisma:generate && npm run build
FROM node:16.14.2 AS runner FROM node:20.9.0 AS runner
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NODE_OPTIONS=--enable-source-maps
COPY --from=builder /usr/src/app/node_modules ./node_modules COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app/start.sh ./docker-start.sh
COPY --from=builder /usr/src/app/package.json ./package.json COPY --from=builder /usr/src/app/package.json ./package.json
COPY --from=builder /usr/src/app/email ./email COPY --from=builder /usr/src/app/email ./email
COPY --from=builder /usr/src/app/build ./build COPY --from=builder /usr/src/app/build ./build
COPY --from=builder /usr/src/app/prisma ./prisma COPY --from=builder /usr/src/app/prisma ./prisma
COPY --from=builder /usr/src/app/uploads ./uploads
USER node USER node
CMD ["./docker-start.sh"] CMD npm run prisma:migrate:deploy && node build/index.js

View File

@ -1,4 +1,8 @@
<h1 align="center"><a href="https://api.thream.divlo.fr/documentation">Thream/api</a></h1> <h1 align="center"><a href="https://api.thream.theoludwig.fr/documentation">Thream/api</a></h1>
<p align="center">
<strong>⚠️ This project is not maintained anymore, you can still use the code as you wish and fork it to maintain it yourself.</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>
@ -18,7 +22,7 @@
Thream's Application Programming Interface (API) to stay close with your friends and communities. Thream's Application Programming Interface (API) to stay close with your friends and communities.
It uses [Thream/file-uploads-api](https://github.com/Thream/file-uploads-api) [v1.1.0](https://github.com/Thream/file-uploads-api/releases/tag/v1.1.0). It uses [Thream/file-uploads-api](https://github.com/Thream/file-uploads-api) [v1.1.8](https://github.com/Thream/file-uploads-api/releases/tag/v1.1.8).
## ⚙️ Getting Started ## ⚙️ Getting Started
@ -32,61 +36,81 @@ It uses [Thream/file-uploads-api](https://github.com/Thream/file-uploads-api) [v
```sh ```sh
# Clone the repository # Clone the repository
git clone https://github.com/Thream/api.git git clone git@github.com:Thream/api.git
# Go to the project root # Go to the project root
cd api cd api
# Install dependencies
npm clean-install
# Configure environment variables # Configure environment variables
cp .env.example .env cp .env.example .env
# Install # Generate Prisma client types
npm install npm run prisma:generate
``` ```
You will need to configure the environment variables by creating an `.env` file at ### Database Setup
the root of the project (see `.env.example`).
### Local Development environment
#### Setup the database
```sh ```sh
# Create a new user and database # Create a new user and database
psql psql
create database thream_database; CREATE DATABASE thream;
create user thream_user with encrypted password 'password'; CREATE USER thream_user with encrypted password 'password';
ALTER USER thream_user WITH SUPERUSER; ALTER USER thream_user WITH SUPERUSER;
``` ```
Replace `DATABASE_URL` inside `.env` with `postgresql://thream_user:password@localhost:5432/thream_database` ### Database Production migration
```sh
npm run prisma:migrate:deploy
```
### Local Development environment
Recommended to use [VSCode: Remote development in Containers](https://code.visualstudio.com/docs/remote/containers-tutorial).
#### Database Development migration
```sh ```sh
# Run Prisma migrations # Run Prisma migrations
npm run prisma:migrate:dev npm run prisma:migrate:dev
# Reset the database (WARNING: This will delete all data)
npm run prisma:migrate:reset
``` ```
#### Usage #### Usage
```sh ```sh
# Run API
npm run dev npm run dev
# Run Prisma Studio
npm run prisma:studio
``` ```
### Production environment with [Docker](https://www.docker.com/) ##### Services started
- `api`: <http://127.0.0.1:8080>
- [Maildev](https://maildev.github.io/maildev/): <http://127.0.0.1:1080>
- [Prisma Studio](https://www.prisma.io/studio): <http://127.0.0.1:5555>
##### Commands
```sh ```sh
# Setup and run all the services for you # Build, Lint and Test
docker-compose up --build npm run build
npm run build:typescript
npm run lint:editorconfig
npm run lint:markdown
npm run lint:eslint
npm run lint:prettier
npm run test
``` ```
#### Services started ### Production environment (with [Docker](https://www.docker.com/))
- API : `http://localhost:8080` ```sh
- [PostgreSQL database](https://www.postgresql.org/) docker compose up --build
```
## 💡 Contributing ## 💡 Contributing

27
compose.yaml Normal file
View File

@ -0,0 +1,27 @@
services:
thream-api:
container_name: "thream-api"
image: "thream-api"
restart: "unless-stopped"
network_mode: "host"
build:
context: "./"
env_file: ".env"
depends_on:
- "thream-database"
thream-database:
container_name: "thream-database"
image: "postgres:15.4"
restart: "unless-stopped"
network_mode: "host"
env_file: ".env"
environment:
POSTGRES_USER: ${DATABASE_USER}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- "thream-postgres-data:/var/lib/postgresql/data"
volumes:
thream-postgres-data:

View File

@ -1,29 +0,0 @@
version: '3.0'
services:
thream-api:
container_name: ${COMPOSE_PROJECT_NAME}
build:
context: './'
env_file:
- '.env'
ports:
- '${PORT}:${PORT}'
depends_on:
- 'thream-database'
volumes:
- './uploads:/usr/src/app/uploads'
restart: 'unless-stopped'
thream-database:
container_name: 'thream-database'
image: 'postgres:14.2'
environment:
POSTGRES_USER: 'user'
POSTGRES_PASSWORD: 'password'
POSTGRES_DB: 'thream'
volumes:
- 'database-volume:/var/lib/postgresql/data'
restart: 'unless-stopped'
volumes:
database-volume:

View File

@ -1,4 +0,0 @@
#!/bin/bash
npm run prisma:migrate:deploy
node build/index.js

View File

@ -1,45 +1,45 @@
/** @type {import('node-plop').PlopGeneratorConfig} */ /** @type {import('node-plop').PlopGeneratorConfig} */
export const serviceGenerator = { export const serviceGenerator = {
description: 'REST API endpoint', description: "REST API endpoint",
prompts: [ prompts: [
{ {
type: 'input', type: "input",
name: 'url', name: "url",
message: 'url' message: "url",
}, },
{ {
type: 'list', type: "list",
name: 'httpMethod', name: "httpMethod",
message: 'httpMethod', message: "httpMethod",
choices: ['GET', 'POST', 'PUT', 'DELETE'] choices: ["GET", "POST", "PUT", "DELETE"],
}, },
{ {
type: 'input', type: "input",
name: 'description', name: "description",
message: 'description' message: "description",
}, },
{ {
type: 'list', type: "list",
name: 'tag', name: "tag",
message: 'tag', message: "tag",
choices: ['users', 'guilds', 'channels', 'messages', 'members', 'uploads'] choices: ["users", "oauth2", "guilds", "channels", "messages", "members"],
}, },
{ {
type: 'confirm', type: "confirm",
name: 'shouldBeAuthenticated', name: "shouldBeAuthenticated",
message: 'shouldBeAuthenticated' message: "shouldBeAuthenticated",
} },
], ],
actions: [ actions: [
{ {
type: 'add', type: "add",
path: 'src/services/{{url}}/{{lowerCase httpMethod}}.ts', path: "src/services/{{url}}/{{lowerCase httpMethod}}.ts",
templateFile: 'generators/service/service.ts.hbs' templateFile: "generators/service/service.ts.hbs",
}, },
{ {
type: 'add', type: "add",
path: 'src/services/{{url}}/__test__/{{lowerCase httpMethod}}.test.ts', path: "src/services/{{url}}/__test__/{{lowerCase httpMethod}}.test.ts",
templateFile: 'generators/service/service.test.ts.hbs' templateFile: "generators/service/service.test.ts.hbs",
} },
] ],
} }

View File

@ -1,18 +1,20 @@
import tap from 'tap' import test from 'node:test'
import assert from 'node:assert/strict'
import sinon from 'sinon' import sinon from 'sinon'
import { application } from 'application.js' import { application } from '#src/application.js'
{{#if shouldBeAuthenticated}} {{#if shouldBeAuthenticated}}
import { authenticateUserTest } from '__test__/utils/authenticateUserTest.js' import { authenticateUserTest } from '#src/__test__/utils/authenticateUserTest.js'
{{/if}} {{/if}}
import prisma from 'tools/database/prisma.js' import prisma from '#src/tools/database/prisma.js'
await tap.test('{{httpMethod}} {{url}}', async (t) => { await test('{{httpMethod}} {{url}}', async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test('succeeds', async () => {
{{#if shouldBeAuthenticated}} {{#if shouldBeAuthenticated}}
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
{{/if}} {{/if}}
@ -32,6 +34,6 @@ await tap.test('{{httpMethod}} {{url}}', async (t) => {
payload: {} payload: {}
}) })
// const responseJson = response.json() // const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
}) })
}) })

View File

@ -1,10 +1,10 @@
import { Static, Type } from '@sinclair/typebox' import { Static, Type } from '@sinclair/typebox'
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { FastifyPluginAsync, FastifySchema } from 'fastify'
import prisma from 'tools/database/prisma.js' import prisma from '#src/tools/database/prisma.js'
import { fastifyErrors } from 'models/utils.js' import { fastifyErrors } from '#src/models/utils.js'
{{#if shouldBeAuthenticated}} {{#if shouldBeAuthenticated}}
import authenticateUser from 'tools/plugins/authenticateUser.js' import authenticateUser from '#src/tools/plugins/authenticateUser.js'
{{/if}} {{/if}}
const body{{sentenceCase httpMethod}}ServiceSchema = Type.Object({ const body{{sentenceCase httpMethod}}ServiceSchema = Type.Object({

28257
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +1,110 @@
{ {
"name": "@thream/api", "name": "@thream/api",
"version": "1.1.0", "version": "1.2.10",
"description": "Thream's application programming interface to stay close with your friends and communities.", "description": "Thream's application programming interface to stay close with your friends and communities.",
"private": true, "private": true,
"type": "module", "type": "module",
"repository": { "imports": {
"type": "git", "#src/*": "./build/*"
"url": "https://github.com/Thream/api"
}, },
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0",
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"repository": {
"type": "git",
"url": "https://github.com/Thream/api"
},
"scripts": { "scripts": {
"build": "rimraf ./build && swc ./src --out-dir ./build && tsc", "build": "rimraf ./build && swc ./src --out-dir ./build",
"build:dev": "swc ./src --out-dir ./build --watch", "build:typescript": "tsc",
"start": "node build/index.js", "start": "node --enable-source-maps build/index.js",
"dev": "concurrently -k -n \"TypeScript,Node\" -p \"[{name}]\" -c \"blue,green\" \"npm run build:dev\" \"cross-env NODE_ENV=development nodemon build/index.js\"", "dev:build": "swc ./src --out-dir ./build --watch",
"dev": "concurrently --kill-others --names \"TypeScript,Node,Maildev,Prisma Studio\" \"npm run dev:build\" \"cross-env NODE_ENV=development node --watch --enable-source-maps build/index.js\" \"npm run maildev\" \"npm run prisma:studio\"",
"maildev": "maildev",
"generate": "plop", "generate": "plop",
"generate:jwt-secret": "node --enable-source-maps ./build/scripts/generate-jwt-secret.js",
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint \"**/*.md\" --dot --ignore-path \".gitignore\"", "lint:markdown": "markdownlint-cli2",
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"", "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
"lint:prettier": "prettier \".\" --check", "lint:prettier": "prettier . --check",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "cross-env NODE_ENV=test c8 tap", "test": "cross-env NODE_ENV=test node --enable-source-maps --test build/",
"prisma:validate": "prisma validate",
"prisma:generate": "prisma generate", "prisma:generate": "prisma generate",
"prisma:studio": "prisma studio", "prisma:studio": "prisma studio --browser=none",
"prisma:migrate:dev": "prisma migrate dev", "prisma:migrate:dev": "prisma migrate dev",
"prisma:migrate:deploy": "prisma migrate deploy", "prisma:migrate:deploy": "prisma migrate deploy",
"release": "semantic-release", "release": "semantic-release",
"postinstall": "husky install" "postinstall": "husky install"
}, },
"dependencies": { "dependencies": {
"@fastify/cors": "8.0.0", "@fastify/cors": "8.4.0",
"@fastify/helmet": "9.1.0", "@fastify/helmet": "11.1.1",
"@fastify/multipart": "7.1.0", "@fastify/multipart": "8.0.0",
"@fastify/rate-limit": "7.0.0", "@fastify/rate-limit": "8.0.3",
"@fastify/sensible": "5.1.0", "@fastify/sensible": "5.5.0",
"@fastify/swagger": "7.4.0", "@fastify/swagger": "8.12.0",
"@prisma/client": "3.12.0", "@fastify/swagger-ui": "1.10.1",
"@sinclair/typebox": "0.23.5", "@prisma/client": "5.4.2",
"@thream/socketio-jwt": "3.0.0", "@sinclair/typebox": "0.31.18",
"axios": "0.26.1", "@thream/socketio-jwt": "3.1.3",
"axios": "1.5.1",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"dotenv": "16.0.1", "dotenv": "16.3.1",
"ejs": "3.1.8", "ejs": "3.1.9",
"fastify": "4.2.0", "fastify": "4.24.3",
"fastify-plugin": "3.0.1", "fastify-plugin": "4.5.1",
"form-data": "4.0.0", "form-data": "4.0.0",
"http-errors": "2.0.0", "http-errors": "2.0.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "9.0.2",
"ms": "2.1.3", "ms": "2.1.3",
"nodemailer": "6.7.5", "nodemailer": "6.9.7",
"read-pkg": "7.1.0", "read-pkg": "8.1.0",
"socket.io": "4.5.1" "socket.io": "4.7.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.0.3", "@commitlint/cli": "18.4.3",
"@commitlint/config-conventional": "17.0.3", "@commitlint/config-conventional": "18.4.3",
"@saithodev/semantic-release-backmerge": "2.1.2", "@saithodev/semantic-release-backmerge": "4.0.1",
"@semantic-release/git": "10.0.1", "@semantic-release/git": "10.0.1",
"@swc/cli": "0.1.57", "@swc/cli": "0.1.62",
"@swc/core": "1.2.207", "@swc/core": "1.3.94",
"@types/bcryptjs": "2.4.2", "@tsconfig/strictest": "2.0.2",
"@types/busboy": "1.5.0", "@types/bcryptjs": "2.4.5",
"@types/ejs": "3.1.1", "@types/busboy": "1.5.2",
"@types/http-errors": "1.8.2", "@types/ejs": "3.1.4",
"@types/jsonwebtoken": "8.5.8", "@types/http-errors": "2.0.3",
"@types/ms": "0.7.31", "@types/jsonwebtoken": "9.0.4",
"@types/node": "18.0.0", "@types/ms": "0.7.33",
"@types/nodemailer": "6.4.4", "@types/node": "20.8.7",
"@types/sinon": "10.0.12", "@types/nodemailer": "6.4.13",
"@types/tap": "15.0.7", "@types/sinon": "10.0.20",
"@typescript-eslint/eslint-plugin": "5.30.0", "@typescript-eslint/eslint-plugin": "6.9.0",
"c8": "7.11.3", "@typescript-eslint/parser": "6.9.0",
"concurrently": "7.2.2", "chokidar": "3.5.3",
"concurrently": "8.2.2",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"editorconfig-checker": "4.0.2", "editorconfig-checker": "5.1.1",
"eslint": "8.18.0", "eslint": "8.52.0",
"eslint-config-conventions": "2.0.0", "eslint-config-conventions": "12.0.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.26.0", "eslint-plugin-import": "2.29.0",
"eslint-plugin-prettier": "4.1.0", "eslint-plugin-prettier": "5.0.1",
"eslint-plugin-promise": "6.0.0", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "42.0.0", "eslint-plugin-unicorn": "48.0.1",
"husky": "8.0.1", "husky": "8.0.3",
"lint-staged": "13.0.3", "lint-staged": "15.0.2",
"markdownlint-cli": "0.31.1", "maildev": "2.1.0",
"nodemon": "2.0.18", "markdownlint-cli2": "0.10.0",
"plop": "3.1.1", "markdownlint-rule-relative-links": "2.1.0",
"prettier": "2.7.1", "plop": "4.0.0",
"prisma": "3.12.0", "prettier": "3.0.3",
"rimraf": "3.0.2", "prisma": "5.4.2",
"semantic-release": "19.0.3", "rimraf": "5.0.5",
"sinon": "14.0.0", "semantic-release": "22.0.12",
"tap": "16.3.0", "sinon": "17.0.0",
"typescript": "4.7.4" "typescript": "5.2.2"
} }
} }

View File

@ -1,8 +1,8 @@
import { serviceGenerator } from './generators/service/index.js' import { serviceGenerator } from "./generators/service/index.js"
export default ( export default (
/** @type {import('plop').NodePlopAPI} */ /** @type {import('plop').NodePlopAPI} */
plop plop,
) => { ) => {
plop.setGenerator('service', serviceGenerator) plop.setGenerator("service", serviceGenerator)
} }

View File

@ -1,51 +1,53 @@
import { User } from '@prisma/client' import type { User } from "@prisma/client"
import sinon from 'sinon' import sinon from "sinon"
import { refreshTokenExample } from '../../models/RefreshToken.js' import { refreshTokenExample } from "#src/models/RefreshToken.js"
import { userExample, UserJWT } from '../../models/User.js' import type { UserJWT } from "#src/models/User.js"
import { userSettingsExample } from '../../models/UserSettings.js' import { userExample } from "#src/models/User.js"
import { userSettingsExample } from "#src/models/UserSettings.js"
import { import {
generateAccessToken, generateAccessToken,
generateRefreshToken generateRefreshToken,
} from '../../tools/utils/jwtToken.js' } from "#src/tools/utils/jwtToken.js"
import prisma from '../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
const userStubValue = {
findUnique: async () => {
return userExample
},
}
const userSettingStubValue = {
findFirst: async () => {
return userSettingsExample
},
}
const oAuthStubValue = {
findMany: async () => {
return []
},
}
const refreshTokenStubValue = {
create: async () => {
return refreshTokenExample
},
}
export const authenticateUserTest = async (): Promise<{ export const authenticateUserTest = async (): Promise<{
accessToken: string accessToken: string
refreshToken: string refreshToken: string
user: User user: User
userStubValue: any userStubValue: typeof userStubValue
userSettingStubValue: any userSettingStubValue: typeof userSettingStubValue
oAuthStubValue: any oAuthStubValue: typeof oAuthStubValue
refreshTokenStubValue: any refreshTokenStubValue: typeof refreshTokenStubValue
}> => { }> => {
const userStubValue = { sinon.stub(prisma, "user").value(userStubValue)
findUnique: async () => { sinon.stub(prisma, "userSetting").value(userSettingStubValue)
return userExample sinon.stub(prisma, "oAuth").value(oAuthStubValue)
} sinon.stub(prisma, "refreshToken").value(refreshTokenStubValue)
}
const userSettingStubValue = {
findFirst: async () => {
return userSettingsExample
}
}
const oAuthStubValue = {
findMany: async () => {
return []
}
}
const refreshTokenStubValue = {
create: async () => {
return refreshTokenExample
}
}
sinon.stub(prisma, 'user').value(userStubValue)
sinon.stub(prisma, 'userSetting').value(userSettingStubValue)
sinon.stub(prisma, 'oAuth').value(oAuthStubValue)
sinon.stub(prisma, 'refreshToken').value(refreshTokenStubValue)
const userJWT: UserJWT = { const userJWT: UserJWT = {
currentStrategy: 'Local', currentStrategy: "Local",
id: 1 id: 1,
} }
const accessToken = generateAccessToken(userJWT) const accessToken = generateAccessToken(userJWT)
const refreshToken = await generateRefreshToken(userJWT) const refreshToken = await generateRefreshToken(userJWT)
@ -56,6 +58,6 @@ export const authenticateUserTest = async (): Promise<{
userStubValue, userStubValue,
userSettingStubValue, userSettingStubValue,
oAuthStubValue, oAuthStubValue,
refreshTokenStubValue refreshTokenStubValue,
} }
} }

View File

@ -1,43 +1,75 @@
import dotenv from 'dotenv' import dotenv from "dotenv"
import fastify from 'fastify' import fastify from "fastify"
import fastifyCors from '@fastify/cors' import fastifyCors from "@fastify/cors"
import fastifySwagger from '@fastify/swagger' import fastifySwagger from "@fastify/swagger"
import fastifyHelmet from '@fastify/helmet' import fastifySwaggerUI from "@fastify/swagger-ui"
import fastifyRateLimit from '@fastify/rate-limit' import fastifyHelmet from "@fastify/helmet"
import fastifySensible from '@fastify/sensible' import fastifyRateLimit from "@fastify/rate-limit"
import fastifySensible from "@fastify/sensible"
import { readPackage } from "read-pkg"
import { services } from './services/index.js' import { services } from "#src/services/index.js"
import { swaggerOptions } from './tools/configurations/swaggerOptions.js' import fastifySocketIo from "#src/tools/plugins/socket-io.js"
import fastifySocketIo from './tools/plugins/socket-io.js'
dotenv.config() dotenv.config()
const packageJSON = await readPackage()
export const application = fastify({ export const application = fastify({
logger: process.env.NODE_ENV === 'development', logger: process.env["NODE_ENV"] === "development",
ajv: { ajv: {
customOptions: { customOptions: {
strict: 'log', strict: "log",
keywords: ['kind', 'modifier'], keywords: ["kind", "modifier"],
formats: { formats: {
full: true full: true,
} },
} },
} },
}) })
await application.register(fastifyCors) await application.register(fastifyCors)
await application.register(fastifySensible) await application.register(fastifySensible)
await application.register(fastifySocketIo, { await application.register(fastifySocketIo, {
cors: { cors: {
origin: '*', origin: "*",
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
preflightContinue: false, preflightContinue: false,
optionsSuccessStatus: 204 optionsSuccessStatus: 204,
} },
}) })
await application.register(fastifyHelmet) await application.register(fastifyHelmet)
await application.register(fastifyRateLimit, { await application.register(fastifyRateLimit, {
max: 200, max: 200,
timeWindow: '1 minute' timeWindow: "1 minute",
})
await application.register(fastifySwagger, {
openapi: {
info: {
title: packageJSON.name,
description: packageJSON.description,
version: packageJSON.version,
},
tags: [
{ name: "users" },
{ name: "oauth2" },
{ name: "guilds" },
{ name: "channels" },
{ name: "messages" },
{ name: "members" },
],
components: {
securitySchemes: {
bearerAuth: {
type: "http",
scheme: "bearer",
bearerFormat: "JWT",
},
},
},
},
hideUntagged: true,
})
await application.register(fastifySwaggerUI, {
routePrefix: "/documentation",
staticCSP: true,
}) })
await application.register(fastifySwagger, swaggerOptions)
await application.register(services) await application.register(services)

View File

@ -1,8 +1,8 @@
import { application } from './application.js' import { application } from "#src/application.js"
import { HOST, PORT } from './tools/configurations/index.js' import { HOST, PORT } from "#src/tools/configurations.js"
const address = await application.listen({ const address = await application.listen({
port: PORT, port: PORT,
host: HOST host: HOST,
}) })
console.log('\u001B[36m%s\u001B[0m', `🚀 Server listening at ${address}`) console.log(`Server listening at ${address}`)

View File

@ -1,23 +1,23 @@
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { Channel } from '@prisma/client' import type { Channel } from "@prisma/client"
import { date, id } from './utils.js' import { date, id } from "#src/models/utils.js"
import { guildExample } from './Guild.js' import { guildExample } from "#src/models/Guild.js"
export const types = [Type.Literal('text')] export const types = [Type.Literal("text")]
export const channelSchema = { export const channelSchema = {
id, id,
name: Type.String({ minLength: 1, maxLength: 20 }), name: Type.String({ minLength: 1, maxLength: 20 }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
guildId: id guildId: id,
} }
export const channelExample: Channel = { export const channelExample: Channel = {
id: 1, id: 1,
name: 'general', name: "general",
guildId: guildExample.id, guildId: guildExample.id,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} }

View File

@ -1,28 +1,28 @@
import { Guild } from '@prisma/client' import type { Guild } from "@prisma/client"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { date, id } from './utils.js' import { date, id } from "#src/models/utils.js"
export const guildSchema = { export const guildSchema = {
id, id,
name: Type.String({ minLength: 1, maxLength: 30 }), name: Type.String({ minLength: 1, maxLength: 30 }),
icon: Type.Union([ icon: Type.Union([
Type.String({ format: 'uri-reference', minLength: 1 }), Type.String({ format: "uri-reference", minLength: 1 }),
Type.Null() Type.Null(),
]), ]),
description: Type.Union([ description: Type.Union([
Type.String({ minLength: 1, maxLength: 160 }), Type.String({ minLength: 1, maxLength: 160 }),
Type.Null() Type.Null(),
]), ]),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt updatedAt: date.updatedAt,
} }
export const guildExample: Guild = { export const guildExample: Guild = {
id: 1, id: 1,
name: 'GuildExample', name: "GuildExample",
description: 'guild example.', description: "guild example.",
icon: null, icon: null,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} }

View File

@ -1,9 +1,9 @@
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { Member } from '@prisma/client' import type { Member } from "@prisma/client"
import { date, id } from './utils.js' import { date, id } from "#src/models/utils.js"
import { guildExample } from './Guild.js' import { guildExample } from "#src/models/Guild.js"
import { userExample } from './User.js' import { userExample } from "#src/models/User.js"
export const memberSchema = { export const memberSchema = {
id, id,
@ -11,7 +11,7 @@ export const memberSchema = {
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id, userId: id,
guildId: id guildId: id,
} }
export const memberExample: Member = { export const memberExample: Member = {
@ -20,5 +20,5 @@ export const memberExample: Member = {
userId: userExample.id, userId: userExample.id,
guildId: guildExample.id, guildId: guildExample.id,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} }

View File

@ -1,35 +1,34 @@
import { Message } from '@prisma/client' import type { Message } from "@prisma/client"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { date, id } from './utils.js' import { date, id } from "#src/models/utils.js"
export const types = [Type.Literal('text'), Type.Literal('file')] export const types = [Type.Literal("text"), Type.Literal("file")]
export const messageSchema = { export const messageSchema = {
id, id,
value: Type.String({ value: Type.String({
minLength: 1, minLength: 1,
maxLength: 20_000 maxLength: 20_000,
}), }),
type: Type.Union(types, { default: 'text' }), type: Type.Union(types, { default: "text" }),
mimetype: Type.String({ mimetype: Type.String({
maxLength: 127, maxLength: 127,
default: 'text/plain', default: "text/plain",
format: 'mimetype'
}), }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
memberId: id, memberId: id,
channelId: id channelId: id,
} }
export const messageExample: Message = { export const messageExample: Message = {
id: 1, id: 1,
value: 'Hello, world!', value: "Hello, world!",
type: 'text', type: "text",
mimetype: 'text/plain', mimetype: "text/plain",
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
memberId: 1, memberId: 1,
channelId: 1 channelId: 1,
} }

View File

@ -1,19 +1,19 @@
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { date, id } from './utils.js' import { date, id } from "#src/models/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) => export const strategiesTypebox = strategies.map((strategy) => {
Type.Literal(strategy) return Type.Literal(strategy)
) })
export const providersTypebox = providers.map((provider) => export const providersTypebox = providers.map((provider) => {
Type.Literal(provider) return 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]
export const oauthSchema = { export const oauthSchema = {
id, id,
@ -21,5 +21,5 @@ export const oauthSchema = {
provider: Type.Union([...providersTypebox]), provider: Type.Union([...providersTypebox]),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id userId: id,
} }

View File

@ -1,21 +1,21 @@
import { RefreshToken } from '@prisma/client' import type { RefreshToken } from "@prisma/client"
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
import { userExample } from './User.js' import { userExample } from "#src/models/User.js"
import { date, id } from './utils.js' import { date, id } from "#src/models/utils.js"
export const refreshTokensSchema = { export const refreshTokensSchema = {
id, id,
token: Type.String(), token: Type.String({ format: "uuid" }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id userId: id,
} }
export const refreshTokenExample: RefreshToken = { export const refreshTokenExample: RefreshToken = {
id: 1, id: 1,
userId: userExample.id, userId: userExample.id,
token: 'sometoken', token: "sometokenUUID",
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} }

View File

@ -1,15 +1,21 @@
import { User } from '@prisma/client' import type { User } from "@prisma/client"
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { Type } from "@sinclair/typebox"
import { AuthenticationStrategy, strategiesTypebox } from './OAuth.js' import type { AuthenticationStrategy } from "#src/models/OAuth.js"
import { userSettingsSchema } from './UserSettings.js' import { strategiesTypebox } from "#src/models/OAuth.js"
import { date, id } from './utils.js' import { userSettingsSchema } from "#src/models/UserSettings.js"
import { date, id } from "#src/models/utils.js"
export interface UserJWT { export interface UserJWT {
id: number id: number
currentStrategy: AuthenticationStrategy currentStrategy: AuthenticationStrategy
} }
export interface UserRefreshJWT extends UserJWT {
tokenUUID: string
}
export interface UserRequest { export interface UserRequest {
current: User current: User
currentStrategy: AuthenticationStrategy currentStrategy: AuthenticationStrategy
@ -19,21 +25,21 @@ export interface UserRequest {
export const userSchema = { export const userSchema = {
id, id,
name: Type.String({ minLength: 1, maxLength: 30 }), name: Type.String({ minLength: 1, maxLength: 30 }),
email: Type.String({ minLength: 1, maxLength: 254, format: 'email' }), email: Type.String({ minLength: 1, maxLength: 254, format: "email" }),
password: Type.String({ minLength: 1 }), password: Type.String({ minLength: 1 }),
logo: Type.String({ minLength: 1, format: 'uri-reference' }), logo: Type.String({ minLength: 1, format: "uri-reference" }),
status: Type.String({ minLength: 1, maxLength: 50 }), status: Type.String({ minLength: 1, maxLength: 50 }),
biography: Type.String({ minLength: 1, maxLength: 160 }), biography: Type.String({ minLength: 1, maxLength: 160 }),
website: Type.String({ website: Type.String({
minLength: 1, minLength: 1,
maxLength: 255, maxLength: 255,
format: 'uri' format: "uri",
}), }),
isConfirmed: Type.Boolean({ default: false }), isConfirmed: Type.Boolean({ default: false }),
temporaryToken: Type.String(), temporaryToken: Type.String(),
temporaryExpirationToken: Type.String({ format: 'date-time' }), temporaryExpirationToken: Type.String({ format: "date-time" }),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt updatedAt: date.updatedAt,
} }
export const userPublicWithoutSettingsSchema = { export const userPublicWithoutSettingsSchema = {
@ -46,20 +52,20 @@ export const userPublicWithoutSettingsSchema = {
website: Type.Union([userSchema.website, Type.Null()]), website: Type.Union([userSchema.website, Type.Null()]),
isConfirmed: userSchema.isConfirmed, isConfirmed: userSchema.isConfirmed,
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt updatedAt: date.updatedAt,
} }
export const userPublicSchema = { export const userPublicSchema = {
...userPublicWithoutSettingsSchema, ...userPublicWithoutSettingsSchema,
settings: Type.Object(userSettingsSchema) settings: Type.Object(userSettingsSchema),
} }
export const userCurrentSchema = Type.Object({ export const userCurrentSchema = Type.Object({
user: Type.Object({ user: Type.Object({
...userPublicSchema, ...userPublicSchema,
currentStrategy: Type.Union([...strategiesTypebox]), currentStrategy: Type.Union([...strategiesTypebox]),
strategies: Type.Array(Type.Union([...strategiesTypebox])) strategies: Type.Array(Type.Union([...strategiesTypebox])),
}) }),
}) })
export const bodyUserSchema = Type.Object({ export const bodyUserSchema = Type.Object({
@ -67,23 +73,23 @@ export const bodyUserSchema = Type.Object({
name: userSchema.name, name: userSchema.name,
password: userSchema.password, password: userSchema.password,
theme: userSettingsSchema.theme, theme: userSettingsSchema.theme,
language: userSettingsSchema.language language: userSettingsSchema.language,
}) })
export type BodyUserSchemaType = Static<typeof bodyUserSchema> export type BodyUserSchemaType = Static<typeof bodyUserSchema>
export const userExample: User = { export const userExample: User = {
id: 1, id: 1,
name: 'Divlo', name: "Divlo",
email: 'contact@divlo.fr', email: "contact@divlo.fr",
password: 'somepassword', password: "somepassword",
logo: null, logo: null,
status: null, status: null,
biography: null, biography: null,
website: null, website: null,
isConfirmed: true, isConfirmed: true,
temporaryToken: 'temporaryUUIDtoken', temporaryToken: "temporaryUUIDtoken",
temporaryExpirationToken: new Date(), temporaryExpirationToken: new Date(),
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} }

View File

@ -1,10 +1,11 @@
import { UserSetting } from '@prisma/client' import type { UserSetting } from "@prisma/client"
import { Type, Static } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { Type } from "@sinclair/typebox"
import { date, id } from './utils.js' import { date, id } from "#src/models/utils.js"
export const languages = [Type.Literal('fr'), Type.Literal('en')] export const languages = [Type.Literal("fr"), Type.Literal("en")]
export const themes = [Type.Literal('light'), Type.Literal('dark')] export const themes = [Type.Literal("light"), Type.Literal("dark")]
export const userSettingsSchema = { export const userSettingsSchema = {
id, id,
@ -14,7 +15,7 @@ export const userSettingsSchema = {
isPublicGuilds: Type.Boolean(), isPublicGuilds: Type.Boolean(),
createdAt: date.createdAt, createdAt: date.createdAt,
updatedAt: date.updatedAt, updatedAt: date.updatedAt,
userId: id userId: id,
} }
export type Theme = Static<typeof userSettingsSchema.theme> export type Theme = Static<typeof userSettingsSchema.theme>
@ -22,11 +23,11 @@ export type Language = Static<typeof userSettingsSchema.language>
export const userSettingsExample: UserSetting = { export const userSettingsExample: UserSetting = {
id: 1, id: 1,
theme: 'dark', theme: "dark",
language: 'en', language: "en",
isPublicEmail: false, isPublicEmail: false,
isPublicGuilds: false, isPublicGuilds: false,
userId: 1, userId: 1,
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date() updatedAt: new Date(),
} }

View File

@ -1,51 +1,51 @@
import { Type } from '@sinclair/typebox' import { Type } from "@sinclair/typebox"
export const date = { export const date = {
createdAt: Type.String({ createdAt: Type.String({
format: 'date-time', format: "date-time",
description: 'Created date time' description: "Created date time",
}), }),
updatedAt: Type.String({ updatedAt: Type.String({
format: 'date-time', format: "date-time",
description: 'Last updated date time' description: "Last updated date time",
}) }),
} }
export const id = Type.Integer({ minimum: 1, description: 'Unique identifier' }) export const id = Type.Integer({ minimum: 1, description: "Unique identifier" })
export const redirectURI = Type.String({ format: 'uri-reference' }) export const redirectURI = Type.String({ format: "uri-reference" })
export const fastifyErrorsSchema = { export const fastifyErrorsSchema = {
400: { 400: {
statusCode: Type.Literal(400), statusCode: Type.Literal(400),
error: Type.Literal('Bad Request'), error: Type.Literal("Bad Request"),
message: Type.String() message: Type.String(),
}, },
401: { 401: {
statusCode: Type.Literal(401), statusCode: Type.Literal(401),
error: Type.Literal('Unauthorized'), error: Type.Literal("Unauthorized"),
message: Type.Literal('Unauthorized') message: Type.Literal("Unauthorized"),
}, },
403: { 403: {
statusCode: Type.Literal(403), statusCode: Type.Literal(403),
error: Type.Literal('Forbidden'), error: Type.Literal("Forbidden"),
message: Type.Literal('Forbidden') message: Type.Literal("Forbidden"),
}, },
404: { 404: {
statusCode: Type.Literal(404), statusCode: Type.Literal(404),
error: Type.Literal('Not Found'), error: Type.Literal("Not Found"),
message: Type.Literal('Not Found') message: Type.String(),
}, },
431: { 431: {
statusCode: Type.Literal(431), statusCode: Type.Literal(431),
error: Type.Literal('Request Header Fields Too Large'), error: Type.Literal("Request Header Fields Too Large"),
message: Type.String() message: Type.String(),
}, },
500: { 500: {
statusCode: Type.Literal(500), statusCode: Type.Literal(500),
error: Type.Literal('Internal Server Error'), error: Type.Literal("Internal Server Error"),
message: Type.Literal('Something went wrong') message: Type.Literal("Something went wrong"),
} },
} }
export const fastifyErrors = { export const fastifyErrors = {
@ -54,5 +54,5 @@ export const fastifyErrors = {
403: Type.Object(fastifyErrorsSchema[403]), 403: Type.Object(fastifyErrorsSchema[403]),
404: Type.Object(fastifyErrorsSchema[404]), 404: Type.Object(fastifyErrorsSchema[404]),
431: Type.Object(fastifyErrorsSchema[431]), 431: Type.Object(fastifyErrorsSchema[431]),
500: Type.Object(fastifyErrorsSchema[500]) 500: Type.Object(fastifyErrorsSchema[500]),
} }

View File

@ -0,0 +1,3 @@
import crypto from "node:crypto"
console.log(crypto.randomBytes(256).toString("base64"))

View File

@ -1,28 +1,30 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { channelExample } from '../../../../models/Channel.js'
import { memberExample } from '../../../../models/Member.js'
await tap.test('DELETE /channels/[channelId]', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
await test("DELETE /channels/[channelId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const defaultChannelId = 5 const defaultChannelId = 5
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, },
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId id: defaultChannelId,
} }
}, },
count: async () => { count: async () => {
@ -30,114 +32,114 @@ await tap.test('DELETE /channels/[channelId]', async (t) => {
}, },
delete: async () => { delete: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.id, channelExample.id) assert.strictEqual(responseJson.id, channelExample.id)
t.equal(responseJson.name, channelExample.name) assert.strictEqual(responseJson.name, channelExample.name)
t.equal(responseJson.guildId, channelExample.guildId) assert.strictEqual(responseJson.guildId, channelExample.guildId)
t.equal(responseJson.defaultChannelId, defaultChannelId) assert.strictEqual(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test('fails if there is only one channel', async (t) => { await t.test("fails if there is only one channel", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, },
count: async () => { count: async () => {
return 1 return 1
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
await t.test('fails if the channel is not found', async (t) => { await t.test("fails if the channel is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not found', async (t) => { await t.test("fails if the member is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not owner', async (t) => { await t.test("fails if the member is not owner", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false isOwner: false,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
}) })

View File

@ -1,96 +1,98 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { channelExample } from '../../../../models/Channel.js'
import { memberExample } from '../../../../models/Member.js'
await tap.test('GET /channels/[channelId]', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
await test("GET /channels/[channelId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.channel.id, channelExample.id) assert.strictEqual(responseJson.channel.id, channelExample.id)
t.equal(responseJson.channel.name, channelExample.name) assert.strictEqual(responseJson.channel.name, channelExample.name)
t.equal(responseJson.channel.guildId, channelExample.guildId) assert.strictEqual(responseJson.channel.guildId, channelExample.guildId)
}) })
await t.test('fails with not found member', async (t) => { await t.test("fails with not found member", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Channel not found') assert.strictEqual(responseJson.message, "Channel not found")
}) })
await t.test('fails with not found channel', async (t) => { await t.test("fails with not found channel", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return null return null
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Channel not found') assert.strictEqual(responseJson.message, "Channel not found")
}) })
await t.test('fails with unauthenticated user', async (t) => { await t.test("fails with unauthenticated user", async () => {
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/channels/1' url: "/channels/1",
}) })
t.equal(response.statusCode, 401) assert.strictEqual(response.statusCode, 401)
}) })
}) })

View File

@ -1,129 +1,131 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { channelExample } from '../../../../models/Channel.js'
import { memberExample } from '../../../../models/Member.js'
const newName = 'new channel name' import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
await tap.test('PUT /channels/[channelId]', async (t) => { const newName = "new channel name"
await test("PUT /channels/[channelId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const defaultChannelId = 5 const defaultChannelId = 5
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
}, },
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId id: defaultChannelId,
} }
}, },
update: async () => { update: async () => {
return { return {
...channelExample, ...channelExample,
name: newName name: newName,
}
} }
},
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: newName } payload: { name: newName },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.id, channelExample.id) assert.strictEqual(responseJson.id, channelExample.id)
t.equal(responseJson.name, newName) assert.strictEqual(responseJson.name, newName)
t.equal(responseJson.guildId, channelExample.guildId) assert.strictEqual(responseJson.guildId, channelExample.guildId)
t.equal(responseJson.defaultChannelId, defaultChannelId) assert.strictEqual(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test('fails if the channel is not found', async (t) => { await t.test("fails if the channel is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return null return null
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: newName } payload: { name: newName },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not found', async (t) => { await t.test("fails if the member is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: newName } payload: { name: newName },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not owner', async (t) => { await t.test("fails if the member is not owner", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false isOwner: false,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/channels/${channelExample.id}`, url: `/channels/${channelExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: newName } payload: { name: newName },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
}) })

View File

@ -1,37 +1,38 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { channelSchema } from '../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id channelId: channelSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const deleteServiceSchema: FastifySchema = { const deleteServiceSchema: FastifySchema = {
description: 'DELETE a channel with its id.', description: "DELETE a channel with its id.",
tags: ['channels'] as string[], tags: ["channels"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...channelSchema, ...channelSchema,
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const deleteChannelService: FastifyPluginAsync = async (fastify) => { export const deleteChannelService: FastifyPluginAsync = async (fastify) => {
@ -40,61 +41,61 @@ export const deleteChannelService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'DELETE', method: "DELETE",
url: '/channels/:channelId', url: "/channels/:channelId",
schema: deleteServiceSchema, schema: deleteServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params } = request
const { channelId } = request.params const { channelId } = params
const channelCheck = await prisma.channel.findUnique({ const channelCheck = await prisma.channel.findUnique({
where: { id: channelId } where: { id: channelId },
}) })
if (channelCheck == null) { if (channelCheck == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId: channelCheck.guildId, userId: user.current.id } where: { guildId: channelCheck.guildId, userId: user.current.id },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest('You should be a member owner') throw fastify.httpErrors.badRequest("You should be a member owner")
} }
const channelCount = await prisma.channel.count({ const channelCount = await prisma.channel.count({
where: { guildId: channelCheck.guildId } where: { guildId: channelCheck.guildId },
}) })
if (channelCount <= 1) { if (channelCount <= 1) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
'The guild should have at least one channel' "The guild should have at least one channel",
) )
} }
const channel = await prisma.channel.delete({ const channel = await prisma.channel.delete({
where: { id: channelId } where: { id: channelId },
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId } where: { guildId: member.guildId },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...channel, ...channel,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'channels', event: "channels",
guildId: member.guildId, guildId: member.guildId,
payload: { payload: {
action: 'delete', action: "delete",
item item,
} },
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,36 +1,37 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { channelSchema } from '../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id channelId: channelSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET a channel with its id.', description: "GET a channel with its id.",
tags: ['channels'] as string[], tags: ["channels"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
channel: Type.Object(channelSchema) channel: Type.Object(channelSchema),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getChannelByIdService: FastifyPluginAsync = async (fastify) => { export const getChannelByIdService: FastifyPluginAsync = async (fastify) => {
@ -39,8 +40,8 @@ export const getChannelByIdService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'GET', method: "GET",
url: '/channels/:channelId', url: "/channels/:channelId",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -48,19 +49,19 @@ export const getChannelByIdService: FastifyPluginAsync = async (fastify) => {
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId } where: { id: channelId },
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id } where: { guildId: channel.guildId, userId: request.user.current.id },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
reply.statusCode = 200 reply.statusCode = 200
return { channel } return { channel }
} },
}) })
} }

View File

@ -1,115 +1,117 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { channelExample } from '../../../../../models/Channel.js'
import { memberExample } from '../../../../../models/Member.js'
import { userExample } from '../../../../../models/User.js'
import { messageExample } from '../../../../../models/Message.js'
await tap.test('GET /channels/[channelId]/messages', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { messageExample } from "#src/models/Message.js"
await test("GET /channels/[channelId]/messages", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findMany: async () => { findMany: async () => {
return [messageExample] return [messageExample]
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.length, 1) assert.strictEqual(responseJson.length, 1)
t.equal(responseJson[0].id, messageExample.id) assert.strictEqual(responseJson[0].id, messageExample.id)
t.equal(responseJson[0].value, messageExample.value) assert.strictEqual(responseJson[0].value, messageExample.value)
t.equal(responseJson[0].type, messageExample.type) assert.strictEqual(responseJson[0].type, messageExample.type)
t.equal(responseJson[0].mimetype, messageExample.mimetype) assert.strictEqual(responseJson[0].mimetype, messageExample.mimetype)
t.equal(responseJson[0].member.id, memberExample.id) assert.strictEqual(responseJson[0].member.id, memberExample.id)
t.equal(responseJson[0].member.isOwner, memberExample.isOwner) assert.strictEqual(responseJson[0].member.isOwner, memberExample.isOwner)
t.equal(responseJson[0].member.user.id, userExample.id) assert.strictEqual(responseJson[0].member.user.id, userExample.id)
t.equal(responseJson[0].member.user.name, userExample.name) assert.strictEqual(responseJson[0].member.user.name, userExample.name)
}) })
await t.test('fails with not found channel', async (t) => { await t.test("fails with not found channel", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return null return null
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Channel not found') assert.strictEqual(responseJson.message, "Channel not found")
}) })
await t.test('fails with not found member', async (t) => { await t.test("fails with not found member", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Channel not found') assert.strictEqual(responseJson.message, "Channel not found")
}) })
await t.test('fails with unauthenticated user', async (t) => { await t.test("fails with unauthenticated user", async () => {
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/channels/1/messages` url: `/channels/1/messages`,
}) })
t.equal(response.statusCode, 401) assert.strictEqual(response.statusCode, 401)
}) })
}) })

View File

@ -1,135 +1,137 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { channelExample } from '../../../../../models/Channel.js'
import { memberExample } from '../../../../../models/Member.js'
import { userExample } from '../../../../../models/User.js'
import { messageExample } from '../../../../../models/Message.js'
await tap.test('POST /channels/[channelId]/messages', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { channelExample } from "#src/models/Channel.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { messageExample } from "#src/models/Message.js"
await test("POST /channels/[channelId]/messages", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
create: async () => { create: async () => {
return messageExample return messageExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: messageExample.value } payload: { value: messageExample.value },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 201) assert.strictEqual(response.statusCode, 201)
t.equal(responseJson.id, messageExample.id) assert.strictEqual(responseJson.id, messageExample.id)
t.equal(responseJson.value, messageExample.value) assert.strictEqual(responseJson.value, messageExample.value)
t.equal(responseJson.type, messageExample.type) assert.strictEqual(responseJson.type, messageExample.type)
t.equal(responseJson.mimetype, messageExample.mimetype) assert.strictEqual(responseJson.mimetype, messageExample.mimetype)
t.equal(responseJson.member.id, memberExample.id) assert.strictEqual(responseJson.member.id, memberExample.id)
t.equal(responseJson.member.isOwner, memberExample.isOwner) assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner)
t.equal(responseJson.member.user.id, userExample.id) assert.strictEqual(responseJson.member.user.id, userExample.id)
t.equal(responseJson.member.user.name, userExample.name) assert.strictEqual(responseJson.member.user.name, userExample.name)
}) })
await t.test('fails with no message value', async (t) => { await t.test("fails with no message value", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: {} payload: {},
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
await t.test('fails with not found channel', async (t) => { await t.test("fails with not found channel", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return null return null
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: '/channels/5/messages', url: "/channels/5/messages",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: messageExample.value } payload: { value: messageExample.value },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Channel not found') assert.strictEqual(responseJson.message, "Channel not found")
}) })
await t.test('fails with not found member', async (t) => { await t.test("fails with not found member", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findUnique: async () => { findUnique: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/channels/${channelExample.id}/messages`, url: `/channels/${channelExample.id}/messages`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: messageExample.value } payload: { value: messageExample.value },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Channel not found') assert.strictEqual(responseJson.message, "Channel not found")
}) })
}) })

View File

@ -1,33 +1,34 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { messageSchema } from '../../../../models/Message.js' import { messageSchema } from "#src/models/Message.js"
import { memberSchema } from '../../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema queryPaginationObjectSchema,
} from '../../../../tools/database/pagination.js' } from "#src/tools/database/pagination.js"
import { channelSchema } from '../../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id channelId: channelSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET all the messages of a channel by its id.', description: "GET all the messages of a channel by its id.",
tags: ['messages'] as string[], tags: ["messages"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
@ -37,20 +38,20 @@ const getServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
}) }),
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getMessagesByChannelIdService: FastifyPluginAsync = async ( export const getMessagesByChannelIdService: FastifyPluginAsync = async (
fastify fastify,
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -58,8 +59,8 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
Params: Parameters Params: Parameters
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: 'GET', method: "GET",
url: '/channels/:channelId/messages', url: "/channels/:channelId/messages",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -67,21 +68,21 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId } where: { id: channelId },
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id } where: { guildId: channel.guildId, userId: request.user.current.id },
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const messagesRequest = await prisma.message.findMany({ const messagesRequest = await prisma.message.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
orderBy: { createdAt: 'desc' }, orderBy: { createdAt: "desc" },
where: { channelId } where: { channelId },
}) })
const messages = await Promise.all( const messages = await Promise.all(
messagesRequest.reverse().map(async (message) => { messagesRequest.reverse().map(async (message) => {
@ -97,10 +98,10 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
return { return {
...message, ...message,
@ -108,14 +109,14 @@ export const getMessagesByChannelIdService: FastifyPluginAsync = async (
...member, ...member,
user: { user: {
...member?.user, ...member?.user,
email: null email: null,
},
},
} }
} }),
}
})
) )
reply.statusCode = 200 reply.statusCode = 200
return messages return messages
} },
}) })
} }

View File

@ -1,34 +1,35 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { messageSchema } from '../../../../models/Message.js' import { messageSchema } from "#src/models/Message.js"
import { channelSchema } from '../../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
import { memberSchema } from '../../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id channelId: channelSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const bodyPostServiceSchema = Type.Object({ const bodyPostServiceSchema = Type.Object({
value: messageSchema.value value: messageSchema.value,
}) })
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: description:
'POST a new message (text) in a specific channel using its channelId.', "POST a new message (text) in a specific channel using its channelId.",
tags: ['messages'] as string[], tags: ["messages"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema, body: bodyPostServiceSchema,
params: parametersSchema, params: parametersSchema,
@ -37,19 +38,19 @@ const postServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const postMessageByChannelIdService: FastifyPluginAsync = async ( export const postMessageByChannelIdService: FastifyPluginAsync = async (
fastify fastify,
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -57,8 +58,8 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
Body: BodyPostServiceSchemaType Body: BodyPostServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: 'POST', method: "POST",
url: '/channels/:channelId/messages', url: "/channels/:channelId/messages",
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -66,10 +67,10 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId } where: { id: channelId },
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id }, where: { guildId: channel.guildId, userId: request.user.current.id },
@ -83,23 +84,23 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const { value } = request.body const { value } = request.body
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
value, value,
type: 'text', type: "text",
mimetype: 'text/plain', mimetype: "text/plain",
channelId, channelId,
memberId: memberCheck.id memberId: memberCheck.id,
} },
}) })
const item = { const item = {
...message, ...message,
@ -107,17 +108,17 @@ export const postMessageByChannelIdService: FastifyPluginAsync = async (
...memberCheck, ...memberCheck,
user: { user: {
...memberCheck.user, ...memberCheck.user,
email: null email: null,
} },
} },
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'messages', event: "messages",
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: 'create', item } payload: { action: "create", item },
}) })
reply.statusCode = 201 reply.statusCode = 201
return item return item
} },
}) })
} }

View File

@ -1,32 +1,33 @@
import { Type, Static } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import fastifyMultipart from '@fastify/multipart' import type { FastifyPluginAsync, FastifySchema } from "fastify"
import fastifyMultipart from "@fastify/multipart"
import prisma from '../../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { messageSchema } from '../../../../../models/Message.js' import { messageSchema } from "#src/models/Message.js"
import { memberSchema } from '../../../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
import { channelSchema } from '../../../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
import { uploadFile } from '../../../../../tools/utils/uploadFile.js' import { uploadFile } from "#src/tools/utils/uploadFile.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id channelId: channelSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: description:
'POST a new message (file) in a specific channel using its channelId.', "POST a new message (file) in a specific channel using its channelId.",
tags: ['messages'] as string[], tags: ["messages"] as string[],
consumes: ['multipart/form-data'] as string[], consumes: ["multipart/form-data"] as string[],
produces: ['application/json'] as string[], produces: ["application/json"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -34,20 +35,20 @@ const postServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431], 431: fastifyErrors[431],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async ( export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
fastify fastify,
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -56,8 +57,8 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'POST', method: "POST",
url: '/channels/:channelId/messages/uploads', url: "/channels/:channelId/messages/uploads",
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -65,10 +66,10 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
} }
const { channelId } = request.params const { channelId } = request.params
const channel = await prisma.channel.findUnique({ const channel = await prisma.channel.findUnique({
where: { id: channelId } where: { id: channelId },
}) })
if (channel == null) { if (channel == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId: channel.guildId, userId: request.user.current.id }, where: { guildId: channel.guildId, userId: request.user.current.id },
@ -82,27 +83,27 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const file = await uploadFile({ const file = await uploadFile({
fastify, fastify,
request, request,
folderInUploadsFolder: 'messages' folderInUploadsFolder: "messages",
}) })
const message = await prisma.message.create({ const message = await prisma.message.create({
data: { data: {
value: file.pathToStoreInDatabase, value: file.pathToStoreInDatabase,
type: 'file', type: "file",
mimetype: file.mimetype, mimetype: file.mimetype,
channelId, channelId,
memberId: memberCheck.id memberId: memberCheck.id,
} },
}) })
reply.statusCode = 201 reply.statusCode = 201
const item = { const item = {
@ -111,16 +112,16 @@ export const postMessageUploadsByChannelIdService: FastifyPluginAsync = async (
...memberCheck, ...memberCheck,
user: { user: {
...memberCheck.user, ...memberCheck.user,
email: null email: null,
} },
} },
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'messages', event: "messages",
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: 'create', item } payload: { action: "create", item },
}) })
return item return item
} },
}) })
} }

View File

@ -1,44 +1,45 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { channelSchema } from '../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
const bodyPutServiceSchema = Type.Object({ const bodyPutServiceSchema = Type.Object({
name: channelSchema.name name: channelSchema.name,
}) })
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema> type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
channelId: channelSchema.id channelId: channelSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: 'UPDATE a channel with its id.', description: "UPDATE a channel with its id.",
tags: ['channels'] as string[], tags: ["channels"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
body: bodyPutServiceSchema, body: bodyPutServiceSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...channelSchema, ...channelSchema,
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const putChannelService: FastifyPluginAsync = async (fastify) => { export const putChannelService: FastifyPluginAsync = async (fastify) => {
@ -48,55 +49,55 @@ export const putChannelService: FastifyPluginAsync = async (fastify) => {
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: 'PUT', method: "PUT",
url: '/channels/:channelId', url: "/channels/:channelId",
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params, body } = request
const { channelId } = request.params const { channelId } = params
const { name } = request.body const { name } = body
const channelCheck = await prisma.channel.findUnique({ const channelCheck = await prisma.channel.findUnique({
where: { id: channelId } where: { id: channelId },
}) })
if (channelCheck == null) { if (channelCheck == null) {
throw fastify.httpErrors.notFound('Channel not found') throw fastify.httpErrors.notFound("Channel not found")
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId: channelCheck.guildId, userId: user.current.id } where: { guildId: channelCheck.guildId, userId: user.current.id },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest('You should be a member owner') throw fastify.httpErrors.badRequest("You should be a member owner")
} }
const channel = await prisma.channel.update({ const channel = await prisma.channel.update({
where: { id: channelId }, where: { id: channelId },
data: { name } data: { name },
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId } where: { guildId: member.guildId },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...channel, ...channel,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'channels', event: "channels",
guildId: member.guildId, guildId: member.guildId,
payload: { payload: {
action: 'update', action: "update",
item item,
} },
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,11 +1,11 @@
import { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from "fastify"
import { deleteChannelService } from './[channelId]/delete.js' import { deleteChannelService } from "./[channelId]/delete.js"
import { getChannelByIdService } from './[channelId]/get.js' import { getChannelByIdService } from "./[channelId]/get.js"
import { getMessagesByChannelIdService } from './[channelId]/messages/get.js' import { getMessagesByChannelIdService } from "./[channelId]/messages/get.js"
import { postMessageByChannelIdService } from './[channelId]/messages/post.js' import { postMessageByChannelIdService } from "./[channelId]/messages/post.js"
import { postMessageUploadsByChannelIdService } from './[channelId]/messages/uploads/post.js' import { postMessageUploadsByChannelIdService } from "./[channelId]/messages/uploads/post.js"
import { putChannelService } from './[channelId]/put.js' import { putChannelService } from "./[channelId]/put.js"
export const channelsService: FastifyPluginAsync = async (fastify) => { export const channelsService: FastifyPluginAsync = async (fastify) => {
await fastify.register(getChannelByIdService) await fastify.register(getChannelByIdService)

View File

@ -1,84 +1,89 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { memberExample } from '../../../../models/Member.js'
import { guildExample } from '../../../../models/Guild.js'
await tap.test('DELETE /guilds/[guildId]', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
await test("DELETE /guilds/[guildId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds and delete the guild', async (t) => { await t.test("succeeds and delete the guild", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: true, isOwner: true,
guild: guildExample guild: guildExample,
}
} }
},
}) })
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
delete: async () => { delete: async () => {
return guildExample return guildExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.id, guildExample.id) assert.strictEqual(responseJson.id, guildExample.id)
t.equal(responseJson.name, guildExample.name) assert.strictEqual(responseJson.name, guildExample.name)
t.equal(responseJson.description, guildExample.description) assert.strictEqual(responseJson.description, guildExample.description)
}) })
await t.test("fails if the guild doesn't exist", async (t) => { await t.test("fails if the guild doesn't exist", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the user is not the owner', async (t) => { await t.test("fails if the user is not the owner", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false,
guild: guildExample guild: guildExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
t.equal(responseJson.message, 'You should be an owner of the guild') assert.strictEqual(
responseJson.message,
"You should be an owner of the guild",
)
}) })
}) })

View File

@ -1,82 +1,84 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js' import { application } from "#src/application.js"
import { memberExample } from '../../../../models/Member.js' import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import { guildExample } from '../../../../models/Guild.js' import prisma from "#src/tools/database/prisma.js"
import { userExample } from '../../../../models/User.js' import { memberExample } from "#src/models/Member.js"
import { channelExample } from '../../../../models/Channel.js' import { guildExample } from "#src/models/Guild.js"
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
await tap.test('GET /guilds/[guildId]', async (t) => { await test("GET /guilds/[guildId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken, user } = await authenticateUserTest() const { accessToken, user } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
guild: guildExample, guild: guildExample,
user: userExample user: userExample,
}
} }
},
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId id: defaultChannelId,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.member.id, memberExample.id) assert.strictEqual(responseJson.member.id, memberExample.id)
t.equal(responseJson.member.isOwner, memberExample.isOwner) assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner)
t.equal(responseJson.member.user.name, user.name) assert.strictEqual(responseJson.member.user.name, user.name)
t.equal(responseJson.member.user.email, null) assert.strictEqual(responseJson.member.user.email, null)
t.equal(responseJson.guild.id, guildExample.id) assert.strictEqual(responseJson.guild.id, guildExample.id)
t.equal(responseJson.guild.name, guildExample.name) assert.strictEqual(responseJson.guild.name, guildExample.name)
t.equal(responseJson.guild.defaultChannelId, defaultChannelId) assert.strictEqual(responseJson.guild.defaultChannelId, defaultChannelId)
}) })
await t.test('fails with not found member/guild', async (t) => { await t.test("fails with not found member/guild", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/1', url: "/guilds/1",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Member not found') assert.strictEqual(responseJson.message, "Member not found")
}) })
await t.test('fails with unauthenticated user', async (t) => { await t.test("fails with unauthenticated user", async () => {
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/1' url: "/guilds/1",
}) })
t.equal(response.statusCode, 401) assert.strictEqual(response.statusCode, 401)
}) })
}) })

View File

@ -1,113 +1,118 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js' import { application } from "#src/application.js"
import { memberExample } from '../../../../models/Member.js' import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import { guildExample } from '../../../../models/Guild.js' import prisma from "#src/tools/database/prisma.js"
import { channelExample } from '../../../../models/Channel.js' import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
const newName = 'New guild name' const newName = "New guild name"
const newDescription = 'New guild description' const newDescription = "New guild description"
await tap.test('PUT /guilds/[guildId]', async (t) => { await test("PUT /guilds/[guildId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds and edit the guild', async (t) => { await t.test("succeeds and edit the guild", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: true, isOwner: true,
guild: guildExample guild: guildExample,
}
} }
},
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId id: defaultChannelId,
}
} }
},
}) })
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
update: async () => { update: async () => {
return { return {
...guildExample, ...guildExample,
name: newName, name: newName,
description: newDescription description: newDescription,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { payload: {
name: newName, name: newName,
description: newDescription description: newDescription,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.name, newName) assert.strictEqual(responseJson.name, newName)
t.equal(responseJson.description, newDescription) assert.strictEqual(responseJson.description, newDescription)
t.equal(responseJson.defaultChannelId, defaultChannelId) assert.strictEqual(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test("fails if the guild doesn't exist", async (t) => { await t.test("fails if the guild doesn't exist", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { payload: {
name: newName, name: newName,
description: newDescription description: newDescription,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the user is not the owner', async (t) => { await t.test("fails if the user is not the owner", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false, isOwner: false,
guild: guildExample guild: guildExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/guilds/${guildExample.id}`, url: `/guilds/${guildExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { payload: {
name: newName, name: newName,
description: newDescription description: newDescription,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
t.equal(responseJson.message, 'You should be an owner of the guild') assert.strictEqual(
responseJson.message,
"You should be an owner of the guild",
)
}) })
}) })

View File

@ -1,69 +1,71 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { memberExample } from '../../../../../models/Member.js'
import { guildExample } from '../../../../../models/Guild.js'
import { channelExample } from '../../../../../models/Channel.js'
await tap.test('GET /guilds/[guildId]/channels', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
await test("GET /guilds/[guildId]/channels", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findMany: async () => { findMany: async () => {
return [channelExample] return [channelExample]
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.length, 1) assert.strictEqual(responseJson.length, 1)
t.equal(responseJson[0].id, channelExample.id) assert.strictEqual(responseJson[0].id, channelExample.id)
t.equal(responseJson[0].name, channelExample.name) assert.strictEqual(responseJson[0].name, channelExample.name)
t.equal(responseJson[0].guildId, channelExample.guildId) assert.strictEqual(responseJson[0].guildId, channelExample.guildId)
}) })
await t.test('fails with not found member/guild', async (t) => { await t.test("fails with not found member/guild", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/1/channels', url: "/guilds/1/channels",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Member not found') assert.strictEqual(responseJson.message, "Member not found")
}) })
await t.test('fails with unauthenticated user', async (t) => { await t.test("fails with unauthenticated user", async () => {
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/1/channels' url: "/guilds/1/channels",
}) })
t.equal(response.statusCode, 401) assert.strictEqual(response.statusCode, 401)
}) })
}) })

View File

@ -1,90 +1,92 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js' import { application } from "#src/application.js"
import { memberExample } from '../../../../../models/Member.js' import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import { guildExample } from '../../../../../models/Guild.js' import prisma from "#src/tools/database/prisma.js"
import { channelExample } from '../../../../../models/Channel.js' import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
await tap.test('POST /guilds/[guildId]/channels', async (t) => { await test("POST /guilds/[guildId]/channels", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId id: defaultChannelId,
} }
}, },
create: async () => { create: async () => {
return channelExample return channelExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: channelExample.name } payload: { name: channelExample.name },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 201) assert.strictEqual(response.statusCode, 201)
t.equal(responseJson.id, channelExample.id) assert.strictEqual(responseJson.id, channelExample.id)
t.equal(responseJson.name, channelExample.name) assert.strictEqual(responseJson.name, channelExample.name)
t.equal(responseJson.guildId, channelExample.guildId) assert.strictEqual(responseJson.guildId, channelExample.guildId)
t.equal(responseJson.defaultChannelId, defaultChannelId) assert.strictEqual(responseJson.defaultChannelId, defaultChannelId)
}) })
await t.test('fails if the member is not found', async (t) => { await t.test("fails if the member is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: channelExample.name } payload: { name: channelExample.name },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not owner', async (t) => { await t.test("fails if the member is not owner", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
isOwner: false isOwner: false,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/guilds/${guildExample.id}/channels`, url: `/guilds/${guildExample.id}/channels`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { name: channelExample.name } payload: { name: channelExample.name },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
}) })

View File

@ -1,31 +1,32 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { channelSchema } from '../../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema queryPaginationObjectSchema,
} from '../../../../tools/database/pagination.js' } from "#src/tools/database/pagination.js"
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET all the channels of a guild with its id.', description: "GET all the channels of a guild with its id.",
tags: ['channels'] as string[], tags: ["channels"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
@ -35,12 +36,12 @@ const getServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getChannelsByGuildIdService: FastifyPluginAsync = async ( export const getChannelsByGuildIdService: FastifyPluginAsync = async (
fastify fastify,
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -48,8 +49,8 @@ export const getChannelsByGuildIdService: FastifyPluginAsync = async (
Params: Parameters Params: Parameters
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: 'GET', method: "GET",
url: '/guilds/:guildId/channels', url: "/guilds/:guildId/channels",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -57,19 +58,19 @@ export const getChannelsByGuildIdService: FastifyPluginAsync = async (
} }
const { guildId } = request.params const { guildId } = request.params
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id } where: { guildId, userId: request.user.current.id },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
const channels = await prisma.channel.findMany({ const channels = await prisma.channel.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
where: { where: {
guildId guildId,
} },
}) })
reply.statusCode = 200 reply.statusCode = 200
return channels return channels
} },
}) })
} }

View File

@ -1,45 +1,46 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { channelSchema } from '../../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
import { guildSchema } from '../../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
const bodyPostServiceSchema = Type.Object({ const bodyPostServiceSchema = Type.Object({
name: channelSchema.name name: channelSchema.name,
}) })
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const postChannelServiceSchema: FastifySchema = { const postChannelServiceSchema: FastifySchema = {
description: 'Create a channel.', description: "Create a channel.",
tags: ['channels'] as string[], tags: ["channels"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema, body: bodyPostServiceSchema,
params: parametersSchema, params: parametersSchema,
response: { response: {
201: Type.Object({ 201: Type.Object({
...channelSchema, ...channelSchema,
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const postChannelService: FastifyPluginAsync = async (fastify) => { export const postChannelService: FastifyPluginAsync = async (fastify) => {
@ -49,51 +50,51 @@ export const postChannelService: FastifyPluginAsync = async (fastify) => {
Body: BodyPostServiceSchemaType Body: BodyPostServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: 'POST', method: "POST",
url: '/guilds/:guildId/channels', url: "/guilds/:guildId/channels",
schema: postChannelServiceSchema, schema: postChannelServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params, body } = request
const { guildId } = request.params const { guildId } = params
const { name } = request.body const { name } = body
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: user.current.id } where: { guildId, userId: user.current.id },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest('You should be a member owner') throw fastify.httpErrors.badRequest("You should be a member owner")
} }
const channel = await prisma.channel.create({ const channel = await prisma.channel.create({
data: { data: {
name, name,
guildId guildId,
} },
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId } where: { guildId: member.guildId },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...channel, ...channel,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'channels', event: "channels",
guildId, guildId,
payload: { payload: {
action: 'create', action: "create",
item item,
} },
}) })
reply.statusCode = 201 reply.statusCode = 201
return item return item
} },
}) })
} }

View File

@ -1,24 +1,25 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const deleteServiceSchema: FastifySchema = { const deleteServiceSchema: FastifySchema = {
description: 'DELETE a guild with the guildId.', description: "DELETE a guild with the guildId.",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -27,8 +28,8 @@ const deleteServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => { export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => {
@ -37,8 +38,8 @@ export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'DELETE', method: "DELETE",
url: '/guilds/:guildId', url: "/guilds/:guildId",
schema: deleteServiceSchema, schema: deleteServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -48,30 +49,30 @@ export const deleteGuildByIdService: FastifyPluginAsync = async (fastify) => {
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id }, where: { guildId, userId: request.user.current.id },
include: { include: {
guild: true guild: true,
} },
}) })
if (member == null || member.guild == null) { if (member == null || member.guild == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
'You should be an owner of the guild' "You should be an owner of the guild",
) )
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'guilds', event: "guilds",
guildId: member.guildId, guildId: member.guildId,
payload: { payload: {
action: 'delete', action: "delete",
item: member.guild item: member.guild,
} },
}) })
const guild = await prisma.guild.delete({ const guild = await prisma.guild.delete({
where: { id: member.guildId } where: { id: member.guildId },
}) })
reply.statusCode = 200 reply.statusCode = 200
return guild return guild
} },
}) })
} }

View File

@ -1,58 +1,59 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { memberSchema } from '../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
import { channelSchema } from '../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET a guild member with the guildId.', description: "GET a guild member with the guildId.",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
guild: Type.Object({ guild: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getGuildMemberByIdService: FastifyPluginAsync = async ( export const getGuildMemberByIdService: FastifyPluginAsync = async (
fastify fastify,
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'GET', method: "GET",
url: '/guilds/:guildId', url: "/guilds/:guildId",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -71,17 +72,17 @@ export const getGuildMemberByIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
},
guild: true,
}, },
guild: true
}
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: member.guildId } where: { guildId: member.guildId },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
@ -89,18 +90,18 @@ export const getGuildMemberByIdService: FastifyPluginAsync = async (
const item = { const item = {
guild: { guild: {
...member.guild, ...member.guild,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
}, },
member: { member: {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null email: null,
} },
} },
} }
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,43 +1,44 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import fastifyMultipart from '@fastify/multipart' import type { FastifyPluginAsync, FastifySchema } from "fastify"
import fastifyMultipart from "@fastify/multipart"
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import prisma from '../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { uploadFile } from '../../../../tools/utils/uploadFile.js' import { uploadFile } from "#src/tools/utils/uploadFile.js"
import { guildSchema } from '../../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { channelSchema } from '../../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: 'Edit the icon of the guild with its id', description: "Edit the icon of the guild with its id",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
consumes: ['multipart/form-data'] as string[], consumes: ["multipart/form-data"] as string[],
produces: ['application/json'] as string[], produces: ["application/json"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
431: fastifyErrors[431], 431: fastifyErrors[431],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const putGuildIconById: FastifyPluginAsync = async (fastify) => { export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
@ -48,8 +49,8 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'PUT', method: "PUT",
url: '/guilds/:guildId/icon', url: "/guilds/:guildId/icon",
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -63,14 +64,14 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
const file = await uploadFile({ const file = await uploadFile({
fastify, fastify,
request, request,
folderInUploadsFolder: 'guilds' folderInUploadsFolder: "guilds",
}) })
await prisma.guild.update({ await prisma.guild.update({
where: { id: guildId }, where: { id: guildId },
data: { icon: file.pathToStoreInDatabase } data: { icon: file.pathToStoreInDatabase },
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: guild.id } where: { guildId: guild.id },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
@ -78,15 +79,15 @@ export const putGuildIconById: FastifyPluginAsync = async (fastify) => {
const item = { const item = {
...guild, ...guild,
icon: file.pathToStoreInDatabase, icon: file.pathToStoreInDatabase,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'guilds', event: "guilds",
guildId: guild.id, guildId: guild.id,
payload: { action: 'update', item } payload: { action: "update", item },
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,69 +1,71 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../tools/database/prisma.js'
import { memberExample } from '../../../../../models/Member.js'
import { guildExample } from '../../../../../models/Guild.js'
import { userExample } from '../../../../../models/User.js'
await tap.test('GET /guilds/[guildId]/members', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { userExample } from "#src/models/User.js"
await test("GET /guilds/[guildId]/members", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
}, },
findMany: async () => { findMany: async () => {
return [{ ...memberExample, user: userExample }] return [{ ...memberExample, user: userExample }]
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/guilds/${guildExample.id}/members`, url: `/guilds/${guildExample.id}/members`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.length, 1) assert.strictEqual(responseJson.length, 1)
t.equal(responseJson[0].id, memberExample.id) assert.strictEqual(responseJson[0].id, memberExample.id)
t.equal(responseJson[0].isOwner, memberExample.isOwner) assert.strictEqual(responseJson[0].isOwner, memberExample.isOwner)
t.equal(responseJson[0].user.id, userExample.id) assert.strictEqual(responseJson[0].user.id, userExample.id)
t.equal(responseJson[0].user.name, userExample.name) assert.strictEqual(responseJson[0].user.name, userExample.name)
t.equal(responseJson[0].user.email, null) assert.strictEqual(responseJson[0].user.email, null)
}) })
await t.test('fails with not found member/guild', async (t) => { await t.test("fails with not found member/guild", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/1/members', url: "/guilds/1/members",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'Member not found') assert.strictEqual(responseJson.message, "Member not found")
}) })
await t.test('fails with unauthenticated user', async (t) => { await t.test("fails with unauthenticated user", async () => {
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/1/members' url: "/guilds/1/members",
}) })
t.equal(response.statusCode, 401) assert.strictEqual(response.statusCode, 401)
}) })
}) })

View File

@ -1,32 +1,33 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema queryPaginationObjectSchema,
} from '../../../../tools/database/pagination.js' } from "#src/tools/database/pagination.js"
import { memberSchema } from '../../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET all the members of a guild with its id.', description: "GET all the members of a guild with its id.",
tags: ['members'] as string[], tags: ["members"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
@ -34,19 +35,19 @@ const getServiceSchema: FastifySchema = {
200: Type.Array( 200: Type.Array(
Type.Object({ Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getMembersByGuildIdService: FastifyPluginAsync = async ( export const getMembersByGuildIdService: FastifyPluginAsync = async (
fastify fastify,
) => { ) => {
await fastify.register(authenticateUser) await fastify.register(authenticateUser)
@ -54,8 +55,8 @@ export const getMembersByGuildIdService: FastifyPluginAsync = async (
Params: Parameters Params: Parameters
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: 'GET', method: "GET",
url: '/guilds/:guildId/members', url: "/guilds/:guildId/members",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -63,14 +64,14 @@ export const getMembersByGuildIdService: FastifyPluginAsync = async (
} }
const { guildId } = request.params const { guildId } = request.params
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id } where: { guildId, userId: request.user.current.id },
}) })
if (memberCheck == null) { if (memberCheck == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
const membersRequest = await prisma.member.findMany({ const membersRequest = await prisma.member.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
orderBy: { createdAt: 'asc' }, orderBy: { createdAt: "asc" },
where: { guildId }, where: { guildId },
include: { include: {
user: { user: {
@ -82,22 +83,22 @@ export const getMembersByGuildIdService: FastifyPluginAsync = async (
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
const members = membersRequest.map((member) => { const members = membersRequest.map((member) => {
return { return {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null email: null,
} },
} }
}) })
reply.statusCode = 200 reply.statusCode = 200
return members return members
} },
}) })
} }

View File

@ -1,115 +1,117 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../../tools/database/prisma.js' import { application } from "#src/application.js"
import { memberExample } from '../../../../../../models/Member.js' import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import { guildExample } from '../../../../../../models/Guild.js' import prisma from "#src/tools/database/prisma.js"
import { userExample } from '../../../../../../models/User.js' import { memberExample } from "#src/models/Member.js"
import { channelExample } from '../../../../../../models/Channel.js' import { guildExample } from "#src/models/Guild.js"
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
const defaultChannelId = 5 const defaultChannelId = 5
await tap.test('POST /guilds/[guildId]/members/join', async (t) => { await test("POST /guilds/[guildId]/members/join", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
}, },
create: async () => { create: async () => {
return { ...memberExample, user: userExample } return { ...memberExample, user: userExample }
} },
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return channelExample return channelExample
} },
}) })
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
findUnique: async () => { findUnique: async () => {
return guildExample return guildExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/guilds/${guildExample.id}/members/join`, url: `/guilds/${guildExample.id}/members/join`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 201) assert.strictEqual(response.statusCode, 201)
t.equal(responseJson.id, memberExample.id) assert.strictEqual(responseJson.id, memberExample.id)
t.equal(responseJson.userId, memberExample.userId) assert.strictEqual(responseJson.userId, memberExample.userId)
t.equal(responseJson.user.name, userExample.name) assert.strictEqual(responseJson.user.name, userExample.name)
t.equal(responseJson.user.email, null) assert.strictEqual(responseJson.user.email, null)
t.equal(responseJson.guild.id, guildExample.id) assert.strictEqual(responseJson.guild.id, guildExample.id)
t.equal(responseJson.guild.name, guildExample.name) assert.strictEqual(responseJson.guild.name, guildExample.name)
t.equal(responseJson.guild.defaultChannelId, channelExample.id) assert.strictEqual(responseJson.guild.defaultChannelId, channelExample.id)
}) })
await t.test('fails if the guild is not found', async (t) => { await t.test("fails if the guild is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
findUnique: async () => { findUnique: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/guilds/${guildExample.id}/members/join`, url: `/guilds/${guildExample.id}/members/join`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the user is already in the guild', async (t) => { await t.test("fails if the user is already in the guild", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return memberExample return memberExample
} },
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return { return {
...channelExample, ...channelExample,
id: defaultChannelId id: defaultChannelId,
}
} }
},
}) })
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
findUnique: async () => { findUnique: async () => {
return guildExample return guildExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: `/guilds/${guildExample.id}/members/join`, url: `/guilds/${guildExample.id}/members/join`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
t.equal(responseJson.defaultChannelId, defaultChannelId) assert.strictEqual(responseJson.defaultChannelId, defaultChannelId)
}) })
}) })

View File

@ -1,31 +1,28 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { import { fastifyErrors, fastifyErrorsSchema, id } from "#src/models/utils.js"
fastifyErrors, import authenticateUser from "#src/tools/plugins/authenticateUser.js"
fastifyErrorsSchema, import { guildSchema } from "#src/models/Guild.js"
id import { memberSchema } from "#src/models/Member.js"
} from '../../../../../models/utils.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
import authenticateUser from '../../../../../tools/plugins/authenticateUser.js' import { channelSchema } from "#src/models/Channel.js"
import { guildSchema } from '../../../../../models/Guild.js'
import { memberSchema } from '../../../../../models/Member.js'
import { userPublicWithoutSettingsSchema } from '../../../../../models/User.js'
import { channelSchema } from '../../../../../models/Channel.js'
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: 'Join a guild (create a member).', description: "Join a guild (create a member).",
tags: ['members'] as string[], tags: ["members"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -33,19 +30,19 @@ const postServiceSchema: FastifySchema = {
...memberSchema, ...memberSchema,
guild: Type.Object({ guild: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: id defaultChannelId: id,
}), }),
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}), }),
400: Type.Object({ 400: Type.Object({
...fastifyErrorsSchema[400], ...fastifyErrorsSchema[400],
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const postMemberService: FastifyPluginAsync = async (fastify) => { export const postMemberService: FastifyPluginAsync = async (fastify) => {
@ -54,27 +51,27 @@ export const postMemberService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'POST', method: "POST",
url: '/guilds/:guildId/members/join', url: "/guilds/:guildId/members/join",
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params } = request
const { guildId } = request.params const { guildId } = params
const guild = await prisma.guild.findUnique({ const guild = await prisma.guild.findUnique({
where: { where: {
id: guildId id: guildId,
} },
}) })
if (guild == null) { if (guild == null) {
throw fastify.httpErrors.notFound('Guild not found') throw fastify.httpErrors.notFound("Guild not found")
} }
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { where: {
guildId guildId,
} },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
@ -82,22 +79,22 @@ export const postMemberService: FastifyPluginAsync = async (fastify) => {
const memberCheck = await prisma.member.findFirst({ const memberCheck = await prisma.member.findFirst({
where: { where: {
userId: user.current.id, userId: user.current.id,
guildId: guild.id guildId: guild.id,
} },
}) })
if (memberCheck != null) { if (memberCheck != null) {
throw fastify.httpErrors.createError( throw fastify.httpErrors.createError(
400, 400,
'You are already in the guild', "You are already in the guild",
{ {
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} },
) )
} }
const member = await prisma.member.create({ const member = await prisma.member.create({
data: { data: {
guildId, guildId,
userId: user.current.id userId: user.current.id,
}, },
include: { include: {
user: { user: {
@ -109,33 +106,33 @@ export const postMemberService: FastifyPluginAsync = async (fastify) => {
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
const item = { const item = {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null email: null,
}, },
guild: { guild: {
...guild, ...guild,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} },
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'members', event: "members",
guildId, guildId,
payload: { payload: {
action: 'create', action: "create",
item item,
} },
}) })
reply.statusCode = 201 reply.statusCode = 201
return item return item
} },
}) })
} }

View File

@ -1,80 +1,82 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../../../tools/database/prisma.js'
import { memberExample } from '../../../../../../models/Member.js'
import { guildExample } from '../../../../../../models/Guild.js'
await tap.test('DELETE /guilds/[guildId]/members/leave', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
await test("DELETE /guilds/[guildId]/members/leave", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const member = { const member = {
...memberExample, ...memberExample,
isOwner: false isOwner: false,
} }
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return member return member
}, },
delete: async () => { delete: async () => {
return member return member
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/guilds/${guildExample.id}/members/leave`, url: `/guilds/${guildExample.id}/members/leave`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.id, member.id) assert.strictEqual(responseJson.id, member.id)
t.equal(responseJson.isOwner, member.isOwner) assert.strictEqual(responseJson.isOwner, member.isOwner)
t.equal(responseJson.userId, member.userId) assert.strictEqual(responseJson.userId, member.userId)
}) })
await t.test('fails if the member is not found', async (t) => { await t.test("fails if the member is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/guilds/${guildExample.id}/members/leave`, url: `/guilds/${guildExample.id}/members/leave`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is owner', async (t) => { await t.test("fails if the member is owner", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const member = { const member = {
...memberExample, ...memberExample,
isOwner: true isOwner: true,
} }
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return member return member
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/guilds/${guildExample.id}/members/leave`, url: `/guilds/${guildExample.id}/members/leave`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
}) })

View File

@ -1,25 +1,26 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { memberSchema } from '../../../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const deleteServiceSchema: FastifySchema = { const deleteServiceSchema: FastifySchema = {
description: 'Leave a guild (delete a member).', description: "Leave a guild (delete a member).",
tags: ['members'] as string[], tags: ["members"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -28,8 +29,8 @@ const deleteServiceSchema: FastifySchema = {
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const deleteMemberService: FastifyPluginAsync = async (fastify) => { export const deleteMemberService: FastifyPluginAsync = async (fastify) => {
@ -38,37 +39,37 @@ export const deleteMemberService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'DELETE', method: "DELETE",
url: '/guilds/:guildId/members/leave', url: "/guilds/:guildId/members/leave",
schema: deleteServiceSchema, schema: deleteServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params } = request
const { guildId } = request.params const { guildId } = params
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: user.current.id } where: { guildId, userId: user.current.id },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (member.isOwner) { if (member.isOwner) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
"The member owner can't leave the guild (you can delete it instead)" "The member owner can't leave the guild (you can delete it instead)",
) )
} }
await prisma.member.delete({ where: { id: member.id } }) await prisma.member.delete({ where: { id: member.id } })
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'members', event: "members",
guildId, guildId,
payload: { payload: {
action: 'delete', action: "delete",
item: member item: member,
} },
}) })
reply.statusCode = 200 reply.statusCode = 200
return member return member
} },
}) })
} }

View File

@ -1,47 +1,48 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { parseStringNullish } from '../../../tools/utils/parseStringNullish.js' import { parseStringNullish } from "#src/tools/utils/parseStringNullish.js"
import { channelSchema } from '../../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
guildId: guildSchema.id guildId: guildSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const bodyPutServiceSchema = Type.Object({ const bodyPutServiceSchema = Type.Object({
name: Type.Optional(guildSchema.name), name: Type.Optional(guildSchema.name),
description: Type.Optional(guildSchema.description) description: Type.Optional(guildSchema.description),
}) })
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema> type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: 'Update a guild with the guildId.', description: "Update a guild with the guildId.",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPutServiceSchema, body: bodyPutServiceSchema,
params: parametersSchema, params: parametersSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: channelSchema.id defaultChannelId: channelSchema.id,
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const putGuildByIdService: FastifyPluginAsync = async (fastify) => { export const putGuildByIdService: FastifyPluginAsync = async (fastify) => {
@ -51,8 +52,8 @@ export const putGuildByIdService: FastifyPluginAsync = async (fastify) => {
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: 'PUT', method: "PUT",
url: '/guilds/:guildId', url: "/guilds/:guildId",
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -63,41 +64,44 @@ export const putGuildByIdService: FastifyPluginAsync = async (fastify) => {
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { guildId, userId: request.user.current.id }, where: { guildId, userId: request.user.current.id },
include: { include: {
guild: true guild: true,
} },
}) })
if (member == null || member.guild == null) { if (member == null || member.guild == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (!member.isOwner) { if (!member.isOwner) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
'You should be an owner of the guild' "You should be an owner of the guild",
) )
} }
const guild = await prisma.guild.update({ const guild = await prisma.guild.update({
where: { id: guildId }, where: { id: guildId },
data: { data: {
name: name ?? member.guild.name, name: name ?? member.guild.name,
description: parseStringNullish(member.guild.description, description) description: parseStringNullish(
} member.guild.description,
description,
),
},
}) })
const defaultChannel = await prisma.channel.findFirst({ const defaultChannel = await prisma.channel.findFirst({
where: { guildId: guild.id } where: { guildId: guild.id },
}) })
if (defaultChannel == null) { if (defaultChannel == null) {
throw fastify.httpErrors.internalServerError() throw fastify.httpErrors.internalServerError()
} }
const item = { const item = {
...guild, ...guild,
defaultChannelId: defaultChannel.id defaultChannelId: defaultChannel.id,
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'guilds', event: "guilds",
guildId: guild.id, guildId: guild.id,
payload: { action: 'update', item } payload: { action: "update", item },
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,47 +1,49 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../tools/database/prisma.js'
import { memberExample } from '../../../models/Member.js'
import { guildExample } from '../../../models/Guild.js'
import { channelExample } from '../../../models/Channel.js'
await tap.test('GET /guilds', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
await test("GET /guilds", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
findUnique: async () => { findUnique: async () => {
return guildExample return guildExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findMany: async () => { findMany: async () => {
return [memberExample] return [memberExample]
} },
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
findFirst: async () => { findFirst: async () => {
return channelExample return channelExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds', url: "/guilds",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.length, 1) assert.strictEqual(responseJson.length, 1)
t.equal(responseJson[0].name, guildExample.name) assert.strictEqual(responseJson[0].name, guildExample.name)
t.equal(responseJson[0].description, guildExample.description) assert.strictEqual(responseJson[0].description, guildExample.description)
t.equal(responseJson[0].defaultChannelId, channelExample.id) assert.strictEqual(responseJson[0].defaultChannelId, channelExample.id)
}) })
}) })

View File

@ -1,77 +1,82 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../tools/database/prisma.js'
import { memberExample } from '../../../models/Member.js'
import { guildExample } from '../../../models/Guild.js'
import { channelExample } from '../../../models/Channel.js'
import { userExample } from '../../../models/User.js'
await tap.test('POST /guilds', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { memberExample } from "#src/models/Member.js"
import { guildExample } from "#src/models/Guild.js"
import { channelExample } from "#src/models/Channel.js"
import { userExample } from "#src/models/User.js"
await test("POST /guilds", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken, user } = await authenticateUserTest() const { accessToken, user } = await authenticateUserTest()
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
create: async () => { create: async () => {
return guildExample return guildExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
create: async () => { create: async () => {
return memberExample return memberExample
}, },
findUnique: async () => { findUnique: async () => {
return { return {
...memberExample, ...memberExample,
...userExample ...userExample,
}
} }
},
}) })
sinon.stub(prisma, 'channel').value({ sinon.stub(prisma, "channel").value({
create: async () => { create: async () => {
return channelExample return channelExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: '/guilds', url: "/guilds",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { payload: {
name: guildExample.name, name: guildExample.name,
description: guildExample.description description: guildExample.description,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 201) assert.strictEqual(response.statusCode, 201)
t.equal(responseJson.guild.id, guildExample.id) assert.strictEqual(responseJson.guild.id, guildExample.id)
t.equal(responseJson.guild.name, guildExample.name) assert.strictEqual(responseJson.guild.name, guildExample.name)
t.equal(responseJson.guild.description, guildExample.description) assert.strictEqual(responseJson.guild.description, guildExample.description)
t.equal(responseJson.guild.members.length, 1) assert.strictEqual(responseJson.guild.members.length, 1)
t.equal(responseJson.guild.members[0].userId, user.id) assert.strictEqual(responseJson.guild.members[0].userId, user.id)
t.equal(responseJson.guild.members[0].user.name, user.name) assert.strictEqual(responseJson.guild.members[0].user.name, user.name)
t.equal(responseJson.guild.members[0].guildId, guildExample.id) assert.strictEqual(responseJson.guild.members[0].guildId, guildExample.id)
t.equal(responseJson.guild.members[0].isOwner, memberExample.isOwner) assert.strictEqual(
t.equal(responseJson.guild.channels.length, 1) responseJson.guild.members[0].isOwner,
t.equal(responseJson.guild.channels[0].id, channelExample.id) memberExample.isOwner,
t.equal(responseJson.guild.channels[0].guildId, guildExample.id) )
assert.strictEqual(responseJson.guild.channels.length, 1)
assert.strictEqual(responseJson.guild.channels[0].id, channelExample.id)
assert.strictEqual(responseJson.guild.channels[0].guildId, guildExample.id)
}) })
await t.test('fails with empty name and description', async (t) => { await t.test("fails with empty name and description", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const response = await application.inject({ const response = await application.inject({
method: 'POST', method: "POST",
url: '/guilds', url: "/guilds",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
}) })

View File

@ -1,38 +1,39 @@
import { Type, Static } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors, id } from '../../models/utils.js' import { fastifyErrors, id } from "#src/models/utils.js"
import authenticateUser from '../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationObjectSchema queryPaginationObjectSchema,
} from '../../tools/database/pagination.js' } from "#src/tools/database/pagination.js"
type QuerySchemaType = Static<typeof queryPaginationObjectSchema> type QuerySchemaType = Static<typeof queryPaginationObjectSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET all the guilds of an user.', description: "GET all the guilds of an user.",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
querystring: queryPaginationObjectSchema, querystring: queryPaginationObjectSchema,
response: { response: {
200: Type.Array( 200: Type.Array(
Type.Object({ Type.Object({
...guildSchema, ...guildSchema,
defaultChannelId: id defaultChannelId: id,
}) }),
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getGuilds: FastifyPluginAsync = async (fastify) => { export const getGuilds: FastifyPluginAsync = async (fastify) => {
@ -41,8 +42,8 @@ export const getGuilds: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: 'GET', method: "GET",
url: '/guilds', url: "/guilds",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -51,29 +52,29 @@ export const getGuilds: FastifyPluginAsync = async (fastify) => {
const membersRequest = await prisma.member.findMany({ const membersRequest = await prisma.member.findMany({
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
where: { where: {
userId: request.user.current.id userId: request.user.current.id,
} },
}) })
const guilds = await Promise.all( const guilds = await Promise.all(
membersRequest.map(async (member) => { membersRequest.map(async (member) => {
const channel = await prisma.channel.findFirst({ const channel = await prisma.channel.findFirst({
where: { where: {
guildId: member.guildId guildId: member.guildId,
} },
}) })
const guild = await prisma.guild.findUnique({ const guild = await prisma.guild.findUnique({
where: { where: {
id: member.guildId id: member.guildId,
} },
}) })
return { return {
...guild, ...guild,
defaultChannelId: channel?.id defaultChannelId: channel?.id,
} }
}) }),
) )
reply.statusCode = 200 reply.statusCode = 200
return guilds return guilds
} },
}) })
} }

View File

@ -1,17 +1,17 @@
import { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from "fastify"
import { getGuilds } from './get.js' import { getGuilds } from "./get.js"
import { postGuilds } from './post.js' import { postGuilds } from "./post.js"
import { getGuildsPublic } from './public/get.js' import { getGuildsPublic } from "./public/get.js"
import { getChannelsByGuildIdService } from './[guildId]/channels/get.js' import { getChannelsByGuildIdService } from "./[guildId]/channels/get.js"
import { postChannelService } from './[guildId]/channels/post.js' import { postChannelService } from "./[guildId]/channels/post.js"
import { deleteGuildByIdService } from './[guildId]/delete.js' import { deleteGuildByIdService } from "./[guildId]/delete.js"
import { getGuildMemberByIdService } from './[guildId]/get.js' import { getGuildMemberByIdService } from "./[guildId]/get.js"
import { putGuildIconById } from './[guildId]/icon/put.js' import { putGuildIconById } from "./[guildId]/icon/put.js"
import { getMembersByGuildIdService } from './[guildId]/members/get.js' import { getMembersByGuildIdService } from "./[guildId]/members/get.js"
import { postMemberService } from './[guildId]/members/join/post.js' import { postMemberService } from "./[guildId]/members/join/post.js"
import { deleteMemberService } from './[guildId]/members/leave/delete.js' import { deleteMemberService } from "./[guildId]/members/leave/delete.js"
import { putGuildByIdService } from './[guildId]/put.js' import { putGuildByIdService } from "./[guildId]/put.js"
export const guildsService: FastifyPluginAsync = async (fastify) => { export const guildsService: FastifyPluginAsync = async (fastify) => {
await fastify.register(postGuilds) await fastify.register(postGuilds)

View File

@ -1,29 +1,30 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { channelSchema } from '../../models/Channel.js' import { channelSchema } from "#src/models/Channel.js"
import { memberSchema } from '../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
import { parseStringNullish } from '../../tools/utils/parseStringNullish.js' import { parseStringNullish } from "#src/tools/utils/parseStringNullish.js"
const bodyPostServiceSchema = Type.Object({ const bodyPostServiceSchema = Type.Object({
name: guildSchema.name, name: guildSchema.name,
description: guildSchema.description description: guildSchema.description,
}) })
type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema> type BodyPostServiceSchemaType = Static<typeof bodyPostServiceSchema>
const postServiceSchema: FastifySchema = { const postServiceSchema: FastifySchema = {
description: 'Create a guild.', description: "Create a guild.",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPostServiceSchema, body: bodyPostServiceSchema,
response: { response: {
@ -34,16 +35,16 @@ const postServiceSchema: FastifySchema = {
members: Type.Array( members: Type.Array(
Type.Object({ Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
) ),
}) }),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const postGuilds: FastifyPluginAsync = async (fastify) => { export const postGuilds: FastifyPluginAsync = async (fastify) => {
@ -52,8 +53,8 @@ export const postGuilds: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Body: BodyPostServiceSchemaType Body: BodyPostServiceSchemaType
}>({ }>({
method: 'POST', method: "POST",
url: '/guilds', url: "/guilds",
schema: postServiceSchema, schema: postServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -61,37 +62,37 @@ export const postGuilds: FastifyPluginAsync = async (fastify) => {
} }
const { name, description } = request.body const { name, description } = request.body
const guild = await prisma.guild.create({ const guild = await prisma.guild.create({
data: { name, description: parseStringNullish(description) } data: { name, description: parseStringNullish(description) },
}) })
const channel = await prisma.channel.create({ const channel = await prisma.channel.create({
data: { name: 'general', guildId: guild.id } data: { name: "general", guildId: guild.id },
}) })
const memberCreated = await prisma.member.create({ const memberCreated = await prisma.member.create({
data: { data: {
userId: request.user.current.id, userId: request.user.current.id,
isOwner: true, isOwner: true,
guildId: guild.id guildId: guild.id,
} },
}) })
const members = await Promise.all( const members = await Promise.all(
[memberCreated].map(async (member) => { [memberCreated].map(async (member) => {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: member?.userId } where: { id: member?.userId },
}) })
return { return {
...member, ...member,
user user,
} }
}) }),
) )
reply.statusCode = 201 reply.statusCode = 201
return { return {
guild: { guild: {
...guild, ...guild,
channels: [channel], channels: [channel],
members members,
} },
}
} }
},
}) })
} }

View File

@ -1,39 +1,41 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { guildExample } from '../../../../models/Guild.js'
await tap.test('GET /guilds/public', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { guildExample } from "#src/models/Guild.js"
await test("GET /guilds/public", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
findMany: async () => { findMany: async () => {
return [guildExample] return [guildExample]
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
count: async () => { count: async () => {
return 2 return 2
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: '/guilds/public', url: "/guilds/public",
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.length, 1) assert.strictEqual(responseJson.length, 1)
t.equal(responseJson[0].name, guildExample.name) assert.strictEqual(responseJson[0].name, guildExample.name)
t.equal(responseJson[0].membersCount, 2) assert.strictEqual(responseJson[0].membersCount, 2)
}) })
}) })

View File

@ -1,44 +1,45 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { guildSchema } from '../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
import { import {
getPaginationOptions, getPaginationOptions,
queryPaginationSchema queryPaginationSchema,
} from '../../../tools/database/pagination.js' } from "#src/tools/database/pagination.js"
const querySchema = Type.Object({ const querySchema = Type.Object({
search: Type.Optional(Type.String()), search: Type.Optional(Type.String()),
...queryPaginationSchema ...queryPaginationSchema,
}) })
export type QuerySchemaType = Static<typeof querySchema> export type QuerySchemaType = Static<typeof querySchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: description:
'GET all the public guilds (ordered by descending members count).', "GET all the public guilds (ordered by descending members count).",
tags: ['guilds'] as string[], tags: ["guilds"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
querystring: querySchema, querystring: querySchema,
response: { response: {
200: Type.Array( 200: Type.Array(
Type.Object({ Type.Object({
...guildSchema, ...guildSchema,
membersCount: Type.Integer() membersCount: Type.Integer(),
}) }),
), ),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getGuildsPublic: FastifyPluginAsync = async (fastify) => { export const getGuildsPublic: FastifyPluginAsync = async (fastify) => {
@ -47,8 +48,8 @@ export const getGuildsPublic: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Querystring: QuerySchemaType Querystring: QuerySchemaType
}>({ }>({
method: 'GET', method: "GET",
url: '/guilds/public', url: "/guilds/public",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
@ -58,28 +59,28 @@ export const getGuildsPublic: FastifyPluginAsync = async (fastify) => {
...getPaginationOptions(request.query), ...getPaginationOptions(request.query),
orderBy: { orderBy: {
members: { members: {
_count: 'desc' _count: "desc",
} },
}, },
...(request.query.search != null && { ...(request.query.search != null && {
where: { where: {
name: { contains: request.query.search } name: { contains: request.query.search },
} },
}) }),
}) })
const guilds = await Promise.all( const guilds = await Promise.all(
guildsRequest.map(async (guild) => { guildsRequest.map(async (guild) => {
const membersCount = await prisma.member.count({ const membersCount = await prisma.member.count({
where: { guildId: guild.id } where: { guildId: guild.id },
}) })
return { return {
...guild, ...guild,
membersCount membersCount,
} }
}) }),
) )
reply.statusCode = 200 reply.statusCode = 200
return guilds return guilds
} },
}) })
} }

View File

@ -1,9 +1,9 @@
import { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from "fastify"
import { usersService } from './users/index.js' import { usersService } from "./users/index.js"
import { guildsService } from './guilds/index.js' import { guildsService } from "./guilds/index.js"
import { channelsService } from './channels/index.js' import { channelsService } from "./channels/index.js"
import { messagesService } from './messages/index.js' import { messagesService } from "./messages/index.js"
export const services: FastifyPluginAsync = async (fastify) => { export const services: FastifyPluginAsync = async (fastify) => {
await fastify.register(channelsService) await fastify.register(channelsService)

View File

@ -1,127 +1,129 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { messageExample } from '../../../../models/Message.js'
import { memberExample } from '../../../../models/Member.js'
import { userExample } from '../../../../models/User.js'
import { channelExample } from '../../../../models/Channel.js'
await tap.test('DELETE /messsages/[messageId]', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { messageExample } from "#src/models/Message.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
await test("DELETE /messsages/[messageId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample channel: channelExample,
} }
}, },
delete: async () => { delete: async () => {
return messageExample return messageExample
} },
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.id, messageExample.id) assert.strictEqual(responseJson.id, messageExample.id)
t.equal(responseJson.value, messageExample.value) assert.strictEqual(responseJson.value, messageExample.value)
t.equal(responseJson.type, messageExample.type) assert.strictEqual(responseJson.type, messageExample.type)
t.equal(responseJson.mimetype, messageExample.mimetype) assert.strictEqual(responseJson.mimetype, messageExample.mimetype)
t.equal(responseJson.member.id, memberExample.id) assert.strictEqual(responseJson.member.id, memberExample.id)
t.equal(responseJson.member.isOwner, memberExample.isOwner) assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner)
t.equal(responseJson.member.user.id, userExample.id) assert.strictEqual(responseJson.member.user.id, userExample.id)
t.equal(responseJson.member.user.name, userExample.name) assert.strictEqual(responseJson.member.user.name, userExample.name)
}) })
await t.test('fails if the message is not found', async (t) => { await t.test("fails if the message is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not found', async (t) => { await t.test("fails if the member is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample channel: channelExample,
}
} }
},
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not owner of the message', async (t) => { await t.test("fails if the member is not owner of the message", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const randomUserIdOwnerOfMessage = 14 const randomUserIdOwnerOfMessage = 14
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample channel: channelExample,
}
} }
},
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
userId: randomUserIdOwnerOfMessage userId: randomUserIdOwnerOfMessage,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'DELETE', method: "DELETE",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
} },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
}) })
}) })

View File

@ -1,141 +1,143 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import { authenticateUserTest } from '../../../../__test__/utils/authenticateUserTest.js'
import prisma from '../../../../tools/database/prisma.js'
import { messageExample } from '../../../../models/Message.js'
import { memberExample } from '../../../../models/Member.js'
import { userExample } from '../../../../models/User.js'
import { channelExample } from '../../../../models/Channel.js'
await tap.test('PUT /messsages/[messageId]', async (t) => { import { application } from "#src/application.js"
import { authenticateUserTest } from "#src/__test__/utils/authenticateUserTest.js"
import prisma from "#src/tools/database/prisma.js"
import { messageExample } from "#src/models/Message.js"
import { memberExample } from "#src/models/Member.js"
import { userExample } from "#src/models/User.js"
import { channelExample } from "#src/models/Channel.js"
await test("PUT /messsages/[messageId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = 'some message' const newValue = "some message"
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample channel: channelExample,
} }
}, },
update: async () => { update: async () => {
return { return {
...messageExample, ...messageExample,
value: newValue value: newValue,
}
} }
},
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
user: userExample user: userExample,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: newValue } payload: { value: newValue },
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.id, messageExample.id) assert.strictEqual(responseJson.id, messageExample.id)
t.equal(responseJson.value, newValue) assert.strictEqual(responseJson.value, newValue)
t.equal(responseJson.type, messageExample.type) assert.strictEqual(responseJson.type, messageExample.type)
t.equal(responseJson.mimetype, messageExample.mimetype) assert.strictEqual(responseJson.mimetype, messageExample.mimetype)
t.equal(responseJson.member.id, memberExample.id) assert.strictEqual(responseJson.member.id, memberExample.id)
t.equal(responseJson.member.isOwner, memberExample.isOwner) assert.strictEqual(responseJson.member.isOwner, memberExample.isOwner)
t.equal(responseJson.member.user.id, userExample.id) assert.strictEqual(responseJson.member.user.id, userExample.id)
t.equal(responseJson.member.user.name, userExample.name) assert.strictEqual(responseJson.member.user.name, userExample.name)
}) })
await t.test('fails if the message is not found', async (t) => { await t.test("fails if the message is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = 'some message' const newValue = "some message"
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: newValue } payload: { value: newValue },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test('fails if the member is not found', async (t) => { await t.test("fails if the member is not found", async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = 'some message' const newValue = "some message"
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample channel: channelExample,
}
} }
},
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: newValue } payload: { value: newValue },
}) })
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
}) })
await t.test( await t.test(
'fails if the member is not the owner of the message', "fails if the member is not the owner of the message",
async (t) => { async () => {
const { accessToken } = await authenticateUserTest() const { accessToken } = await authenticateUserTest()
const newValue = 'some message' const newValue = "some message"
const randomUserIdOwnerOfMessage = 14 const randomUserIdOwnerOfMessage = 14
sinon.stub(prisma, 'message').value({ sinon.stub(prisma, "message").value({
findFirst: async () => { findFirst: async () => {
return { return {
...messageExample, ...messageExample,
channel: channelExample channel: channelExample,
}
} }
},
}) })
sinon.stub(prisma, 'member').value({ sinon.stub(prisma, "member").value({
findFirst: async () => { findFirst: async () => {
return { return {
...memberExample, ...memberExample,
userId: randomUserIdOwnerOfMessage userId: randomUserIdOwnerOfMessage,
}
} }
},
}) })
const response = await application.inject({ const response = await application.inject({
method: 'PUT', method: "PUT",
url: `/messages/${messageExample.id}`, url: `/messages/${messageExample.id}`,
headers: { headers: {
authorization: `Bearer ${accessToken}` authorization: `Bearer ${accessToken}`,
}, },
payload: { value: newValue } payload: { value: newValue },
}) })
t.equal(response.statusCode, 400) assert.strictEqual(response.statusCode, 400)
} },
) )
}) })

View File

@ -1,26 +1,27 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { messageSchema } from '../../../models/Message.js' import { messageSchema } from "#src/models/Message.js"
import { memberSchema } from '../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
messageId: messageSchema.id messageId: messageSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: 'UPDATE a message with its id.', description: "UPDATE a message with its id.",
tags: ['messages'] as string[], tags: ["messages"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
params: parametersSchema, params: parametersSchema,
response: { response: {
@ -28,15 +29,15 @@ const putServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const deleteMessageService: FastifyPluginAsync = async (fastify) => { export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
@ -45,28 +46,28 @@ export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
fastify.route<{ fastify.route<{
Params: Parameters Params: Parameters
}>({ }>({
method: 'DELETE', method: "DELETE",
url: '/messages/:messageId', url: "/messages/:messageId",
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params } = request
const { messageId } = request.params const { messageId } = params
const messageCheck = await prisma.message.findFirst({ const messageCheck = await prisma.message.findFirst({
where: { id: messageId }, where: { id: messageId },
include: { include: {
channel: true channel: true,
} },
}) })
if (messageCheck == null || messageCheck.channel == null) { if (messageCheck == null || messageCheck.channel == null) {
throw fastify.httpErrors.notFound('Message not found') throw fastify.httpErrors.notFound("Message not found")
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { where: {
guildId: messageCheck.channel.guildId, guildId: messageCheck.channel.guildId,
userId: user.current.id userId: user.current.id,
}, },
include: { include: {
user: { user: {
@ -78,23 +79,23 @@ export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (member.userId !== user.current.id) { if (member.userId !== user.current.id) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
'You should be the owner of the message' "You should be the owner of the message",
) )
} }
const message = await prisma.message.delete({ const message = await prisma.message.delete({
where: { where: {
id: messageCheck.id id: messageCheck.id,
} },
}) })
const item = { const item = {
...message, ...message,
@ -102,17 +103,17 @@ export const deleteMessageService: FastifyPluginAsync = async (fastify) => {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null email: null,
} },
} },
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'messages', event: "messages",
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: 'delete', item } payload: { action: "delete", item },
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,32 +1,33 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import authenticateUser from '../../../tools/plugins/authenticateUser.js' import authenticateUser from "#src/tools/plugins/authenticateUser.js"
import { messageSchema } from '../../../models/Message.js' import { messageSchema } from "#src/models/Message.js"
import { memberSchema } from '../../../models/Member.js' import { memberSchema } from "#src/models/Member.js"
import { userPublicWithoutSettingsSchema } from '../../../models/User.js' import { userPublicWithoutSettingsSchema } from "#src/models/User.js"
const bodyPutServiceSchema = Type.Object({ const bodyPutServiceSchema = Type.Object({
value: messageSchema.value value: messageSchema.value,
}) })
type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema> type BodyPutServiceSchemaType = Static<typeof bodyPutServiceSchema>
const parametersSchema = Type.Object({ const parametersSchema = Type.Object({
messageId: messageSchema.id messageId: messageSchema.id,
}) })
type Parameters = Static<typeof parametersSchema> type Parameters = Static<typeof parametersSchema>
const putServiceSchema: FastifySchema = { const putServiceSchema: FastifySchema = {
description: 'UPDATE a message with its id.', description: "UPDATE a message with its id.",
tags: ['messages'] as string[], tags: ["messages"] as string[],
security: [ security: [
{ {
bearerAuth: [] bearerAuth: [],
} },
] as Array<{ [key: string]: [] }>, ] as Array<{ [key: string]: [] }>,
body: bodyPutServiceSchema, body: bodyPutServiceSchema,
params: parametersSchema, params: parametersSchema,
@ -35,15 +36,15 @@ const putServiceSchema: FastifySchema = {
...messageSchema, ...messageSchema,
member: Type.Object({ member: Type.Object({
...memberSchema, ...memberSchema,
user: Type.Object(userPublicWithoutSettingsSchema) user: Type.Object(userPublicWithoutSettingsSchema),
}) }),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
401: fastifyErrors[401], 401: fastifyErrors[401],
403: fastifyErrors[403], 403: fastifyErrors[403],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const putMessageService: FastifyPluginAsync = async (fastify) => { export const putMessageService: FastifyPluginAsync = async (fastify) => {
@ -53,29 +54,29 @@ export const putMessageService: FastifyPluginAsync = async (fastify) => {
Body: BodyPutServiceSchemaType Body: BodyPutServiceSchemaType
Params: Parameters Params: Parameters
}>({ }>({
method: 'PUT', method: "PUT",
url: '/messages/:messageId', url: "/messages/:messageId",
schema: putServiceSchema, schema: putServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
if (request.user == null) { if (request.user == null) {
throw fastify.httpErrors.forbidden() throw fastify.httpErrors.forbidden()
} }
const { user } = request const { user, params, body } = request
const { messageId } = request.params const { messageId } = params
const { value } = request.body const { value } = body
const messageCheck = await prisma.message.findFirst({ const messageCheck = await prisma.message.findFirst({
where: { id: messageId, type: 'text' }, where: { id: messageId, type: "text" },
include: { include: {
channel: true channel: true,
} },
}) })
if (messageCheck == null || messageCheck.channel == null) { if (messageCheck == null || messageCheck.channel == null) {
throw fastify.httpErrors.notFound('Message not found') throw fastify.httpErrors.notFound("Message not found")
} }
const member = await prisma.member.findFirst({ const member = await prisma.member.findFirst({
where: { where: {
guildId: messageCheck.channel.guildId, guildId: messageCheck.channel.guildId,
userId: user.current.id userId: user.current.id,
}, },
include: { include: {
user: { user: {
@ -87,26 +88,26 @@ export const putMessageService: FastifyPluginAsync = async (fastify) => {
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
} },
} },
}) })
if (member == null) { if (member == null) {
throw fastify.httpErrors.notFound('Member not found') throw fastify.httpErrors.notFound("Member not found")
} }
if (member.userId !== user.current.id) { if (member.userId !== user.current.id) {
throw fastify.httpErrors.badRequest( throw fastify.httpErrors.badRequest(
'You should be the owner of the message' "You should be the owner of the message",
) )
} }
const message = await prisma.message.update({ const message = await prisma.message.update({
where: { where: {
id: messageCheck.id id: messageCheck.id,
}, },
data: { data: {
value value,
} },
}) })
const item = { const item = {
...message, ...message,
@ -114,17 +115,17 @@ export const putMessageService: FastifyPluginAsync = async (fastify) => {
...member, ...member,
user: { user: {
...member.user, ...member.user,
email: null email: null,
} },
} },
} }
await fastify.io.emitToMembers({ await fastify.io.emitToMembers({
event: 'messages', event: "messages",
guildId: item.member.guildId, guildId: item.member.guildId,
payload: { action: 'update', item } payload: { action: "update", item },
}) })
reply.statusCode = 200 reply.statusCode = 200
return item return item
} },
}) })
} }

View File

@ -1,7 +1,7 @@
import { FastifyPluginAsync } from 'fastify' import type { FastifyPluginAsync } from "fastify"
import { deleteMessageService } from './[messageId]/delete.js' import { deleteMessageService } from "./[messageId]/delete.js"
import { putMessageService } from './[messageId]/put.js' import { putMessageService } from "./[messageId]/put.js"
export const messagesService: FastifyPluginAsync = async (fastify) => { export const messagesService: FastifyPluginAsync = async (fastify) => {
await fastify.register(putMessageService) await fastify.register(putMessageService)

View File

@ -1,54 +1,56 @@
import tap from 'tap' import test from "node:test"
import sinon from 'sinon' import assert from "node:assert/strict"
import { application } from '../../../../application.js' import sinon from "sinon"
import prisma from '../../../../tools/database/prisma.js'
import { userExample } from '../../../../models/User.js'
import { userSettingsExample } from '../../../../models/UserSettings.js'
await tap.test('GET /users/[userId]', async (t) => { import { application } from "#src/application.js"
import prisma from "#src/tools/database/prisma.js"
import { userExample } from "#src/models/User.js"
import { userSettingsExample } from "#src/models/UserSettings.js"
await test("GET /users/[userId]", async (t) => {
t.afterEach(() => { t.afterEach(() => {
sinon.restore() sinon.restore()
}) })
await t.test('succeeds', async (t) => { await t.test("succeeds", async () => {
sinon.stub(prisma, 'guild').value({ sinon.stub(prisma, "guild").value({
findMany: async () => { findMany: async () => {
return [] return []
} },
}) })
sinon.stub(prisma, 'user').value({ sinon.stub(prisma, "user").value({
findUnique: async () => { findUnique: async () => {
return userExample return userExample
} },
}) })
sinon.stub(prisma, 'userSetting').value({ sinon.stub(prisma, "userSetting").value({
findFirst: async () => { findFirst: async () => {
return userSettingsExample return userSettingsExample
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/users/${userExample.id}` url: `/users/${userExample.id}`,
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 200) assert.strictEqual(response.statusCode, 200)
t.equal(responseJson.user.id, userExample.id) assert.strictEqual(responseJson.user.id, userExample.id)
t.equal(responseJson.user.name, userExample.name) assert.strictEqual(responseJson.user.name, userExample.name)
}) })
await t.test('fails with not found user', async (t) => { await t.test("fails with not found user", async () => {
sinon.stub(prisma, 'userSetting').value({ sinon.stub(prisma, "userSetting").value({
findFirst: async () => { findFirst: async () => {
return null return null
} },
}) })
const response = await application.inject({ const response = await application.inject({
method: 'GET', method: "GET",
url: `/users/1` url: `/users/1`,
}) })
const responseJson = response.json() const responseJson = response.json()
t.equal(response.statusCode, 404) assert.strictEqual(response.statusCode, 404)
t.equal(responseJson.message, 'User not found') assert.strictEqual(responseJson.message, "User not found")
}) })
}) })

View File

@ -1,72 +1,74 @@
import { Static, Type } from '@sinclair/typebox' import type { Static } from "@sinclair/typebox"
import { FastifyPluginAsync, FastifySchema } from 'fastify' import { Type } from "@sinclair/typebox"
import type { FastifyPluginAsync, FastifySchema } from "fastify"
import prisma from '../../../tools/database/prisma.js' import prisma from "#src/tools/database/prisma.js"
import { fastifyErrors } from '../../../models/utils.js' import { fastifyErrors } from "#src/models/utils.js"
import { userPublicSchema } from '../../../models/User.js' import { userPublicSchema } from "#src/models/User.js"
import { guildSchema } from '../../../models/Guild.js' import { guildSchema } from "#src/models/Guild.js"
const parametersGetUserSchema = Type.Object({ const parametersGetUserSchema = Type.Object({
userId: userPublicSchema.id userId: userPublicSchema.id,
}) })
type ParametersGetUser = Static<typeof parametersGetUserSchema> type ParametersGetUser = Static<typeof parametersGetUserSchema>
const getServiceSchema: FastifySchema = { const getServiceSchema: FastifySchema = {
description: 'GET the public user informations with its id', description: "GET the public user informations with its id",
tags: ['users'] as string[], tags: ["users"] as string[],
params: parametersGetUserSchema, params: parametersGetUserSchema,
response: { response: {
200: Type.Object({ 200: Type.Object({
user: Type.Object(userPublicSchema), user: Type.Object(userPublicSchema),
guilds: Type.Array(Type.Object(guildSchema)) guilds: Type.Array(Type.Object(guildSchema)),
}), }),
400: fastifyErrors[400], 400: fastifyErrors[400],
404: fastifyErrors[404], 404: fastifyErrors[404],
500: fastifyErrors[500] 500: fastifyErrors[500],
} },
} as const } as const
export const getUserById: FastifyPluginAsync = async (fastify) => { export const getUserById: FastifyPluginAsync = async (fastify) => {
await fastify.route<{ await fastify.route<{
Params: ParametersGetUser Params: ParametersGetUser
}>({ }>({
method: 'GET', method: "GET",
url: '/users/:userId', url: "/users/:userId",
schema: getServiceSchema, schema: getServiceSchema,
handler: async (request, reply) => { handler: async (request, reply) => {
const { userId } = request.params const { userId } = request.params
const settings = await prisma.userSetting.findFirst({ const settings = await prisma.userSetting.findFirst({
where: { userId } where: { userId },
}) })
if (settings == null) { if (settings == null) {
throw fastify.httpErrors.notFound('User not found') throw fastify.httpErrors.notFound("User not found")
} }
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { where: {
id: userId id: userId,
}, },
select: { select: {
id: true, id: true,
name: true, name: true,
email: settings.isPublicEmail, email: settings.isPublicEmail,
isConfirmed: true,
logo: true, logo: true,
status: true, status: true,
biography: true, biography: true,
website: true, website: true,
createdAt: true, createdAt: true,
updatedAt: true updatedAt: true,
} },
}) })
if (user == null) { if (user == null) {
throw fastify.httpErrors.notFound('User not found') throw fastify.httpErrors.notFound("User not found")
} }
reply.statusCode = 200 reply.statusCode = 200
return { return {
user: { user: {
...user, ...user,
email: user.email ?? null, email: user.email ?? null,
settings settings,
}, },
guilds: !settings.isPublicGuilds guilds: !settings.isPublicGuilds
? [] ? []
@ -75,12 +77,12 @@ export const getUserById: FastifyPluginAsync = async (fastify) => {
where: { where: {
members: { members: {
some: { some: {
userId userId,
} },
} },
} },
}) }),
}
} }
},
}) })
} }

Some files were not shown because too many files have changed in this diff Show More