chore: initial commit
This commit is contained in:
commit
7d8b88d5d2
1
.commitlintrc.json
Normal file
1
.commitlintrc.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{ "extends": ["@commitlint/config-conventional"] }
|
11
.editorconfig
Normal file
11
.editorconfig
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# For more information see: https://editorconfig.org/
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
HOST='0.0.0.0'
|
||||||
|
PORT='8000'
|
||||||
|
NODE_ENV='development'
|
||||||
|
API_URL='http://localhost:8000'
|
16
.eslintrc.json
Normal file
16
.eslintrc.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"extends": ["conventions", "prettier"],
|
||||||
|
"plugins": ["prettier", "import", "unicorn"],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"import/extensions": ["error", "always"],
|
||||||
|
"unicorn/prevent-abbreviations": "error",
|
||||||
|
"unicorn/prefer-node-protocol": "error"
|
||||||
|
}
|
||||||
|
}
|
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
* text=auto eol=lf
|
20
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/BUG.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: '🐛 Bug Report'
|
||||||
|
about: 'Report an unexpected problem or unintended behavior.'
|
||||||
|
title: '[Bug]'
|
||||||
|
labels: 'bug'
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please provide a clear and concise description of what the bug is. Include
|
||||||
|
screenshots if needed. Please make sure your issue has not already been fixed.
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Steps To Reproduce
|
||||||
|
|
||||||
|
1. Step 1
|
||||||
|
2. Step 2
|
||||||
|
|
||||||
|
## The current behavior
|
||||||
|
|
||||||
|
## The expected behavior
|
18
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
Normal file
18
.github/ISSUE_TEMPLATE/DOCUMENTATION.md
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
name: '📜 Documentation'
|
||||||
|
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).'
|
||||||
|
title: '[Documentation]'
|
||||||
|
labels: 'documentation'
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please make sure your issue has not already been fixed. -->
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
<!-- Please uncomment the type of documentation problem this issue address -->
|
||||||
|
|
||||||
|
<!-- Documentation is Missing -->
|
||||||
|
<!-- Documentation is Confusing -->
|
||||||
|
<!-- Documentation has Typo errors -->
|
||||||
|
|
||||||
|
## Proposal
|
20
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: '✨ Feature Request'
|
||||||
|
about: 'Suggest a new feature idea.'
|
||||||
|
title: '[Feature]'
|
||||||
|
labels: 'feature request'
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please make sure your issue has not already been fixed. -->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
<!-- A clear and concise description of the problem or missing capability... -->
|
||||||
|
|
||||||
|
## Describe the solution you'd like
|
||||||
|
|
||||||
|
<!-- If you have a solution in mind, please describe it. -->
|
||||||
|
|
||||||
|
## Describe alternatives you've considered
|
||||||
|
|
||||||
|
<!-- Have you considered any alternative solutions or workarounds? -->
|
20
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/IMPROVEMENT.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: '🔧 Improvement'
|
||||||
|
about: 'Improve structure/format/performance/refactor/tests of the code.'
|
||||||
|
title: '[Improvement]'
|
||||||
|
labels: 'improvement'
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Please make sure your issue has not already been fixed. -->
|
||||||
|
|
||||||
|
## Type of Improvement
|
||||||
|
|
||||||
|
<!-- Please uncomment the type of improvements this issue address -->
|
||||||
|
|
||||||
|
<!-- Files and Folders Structure -->
|
||||||
|
<!-- Performance -->
|
||||||
|
<!-- Refactoring code -->
|
||||||
|
<!-- Tests -->
|
||||||
|
<!-- Not Sure? -->
|
||||||
|
|
||||||
|
## Proposal
|
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
name: '🙋 Question'
|
||||||
|
about: 'Further information is requested.'
|
||||||
|
title: '[Question]'
|
||||||
|
labels: 'question'
|
||||||
|
---
|
||||||
|
|
||||||
|
### Question
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!-- Please first discuss the change you wish to make via issue before making a change. It might avoid a waste of your time. -->
|
||||||
|
|
||||||
|
## What changes this PR introduce?
|
||||||
|
|
||||||
|
## List any relevant issue numbers
|
||||||
|
|
||||||
|
## Is there anything you'd like reviewers to focus on?
|
25
.github/workflows/build.yml
vendored
Normal file
25
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
name: 'Build'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v3.0.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Build'
|
||||||
|
run: 'npm run build'
|
42
.github/workflows/lint.yml
vendored
Normal file
42
.github/workflows/lint.yml
vendored
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: 'Lint'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [master, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v3.0.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'lint:commit'
|
||||||
|
run: 'npm run lint:commit -- --to "${{ github.sha }}"'
|
||||||
|
|
||||||
|
- name: 'lint:editorconfig'
|
||||||
|
run: 'npm run lint:editorconfig'
|
||||||
|
|
||||||
|
- name: 'lint:markdown'
|
||||||
|
run: 'npm run lint:markdown'
|
||||||
|
|
||||||
|
- name: 'lint:typescript'
|
||||||
|
run: 'npm run lint:typescript'
|
||||||
|
|
||||||
|
- name: 'lint:prettier'
|
||||||
|
run: 'npm run lint:prettier'
|
||||||
|
|
||||||
|
- name: 'lint:dotenv'
|
||||||
|
uses: 'dotenv-linter/action-dotenv-linter@v2'
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.github_token }}
|
40
.github/workflows/release.yml
vendored
Normal file
40
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: 'Release'
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: 'ubuntu-latest'
|
||||||
|
steps:
|
||||||
|
- uses: 'actions/checkout@v3.0.0'
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: 'Import GPG key'
|
||||||
|
uses: 'crazy-max/ghaction-import-gpg@v3.2.0'
|
||||||
|
with:
|
||||||
|
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
|
git-user-signingkey: true
|
||||||
|
git-commit-gpgsign: true
|
||||||
|
|
||||||
|
- name: 'Use Node.js'
|
||||||
|
uses: 'actions/setup-node@v3.0.0'
|
||||||
|
with:
|
||||||
|
node-version: '16.x'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: 'Install'
|
||||||
|
run: 'npm install'
|
||||||
|
|
||||||
|
- name: 'Build'
|
||||||
|
run: 'npm run build'
|
||||||
|
|
||||||
|
- name: 'Release'
|
||||||
|
run: 'npm run release'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
GIT_COMMITTER_NAME: ${{ secrets.GIT_NAME }}
|
||||||
|
GIT_COMMITTER_EMAIL: ${{ secrets.GIT_EMAIL }}
|
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# production
|
||||||
|
build
|
||||||
|
.swc
|
||||||
|
|
||||||
|
# testing
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# envs
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
4
.husky/commit-msg
Executable file
4
.husky/commit-msg
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm run lint:commit -- --edit
|
5
.husky/pre-commit
Executable file
5
.husky/pre-commit
Executable file
@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
npm run lint:staged
|
||||||
|
npm run build
|
6
.lintstagedrc.json
Normal file
6
.lintstagedrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"*": ["editorconfig-checker"],
|
||||||
|
"*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"],
|
||||||
|
"*.{json,jsonc,yml,yaml}": ["prettier --write"],
|
||||||
|
"*.md": ["prettier --write", "markdownlint --dot --fix"]
|
||||||
|
}
|
6
.markdownlint.json
Normal file
6
.markdownlint.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"default": true,
|
||||||
|
"MD013": false,
|
||||||
|
"MD033": false,
|
||||||
|
"MD041": false
|
||||||
|
}
|
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"semi": false,
|
||||||
|
"trailingComma": "none"
|
||||||
|
}
|
37
.releaserc.json
Normal file
37
.releaserc.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"branches": ["master"],
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/npm",
|
||||||
|
{
|
||||||
|
"npmPublish": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/git",
|
||||||
|
{
|
||||||
|
"assets": ["package.json", "package-lock.json"],
|
||||||
|
"message": "chore(release): ${nextRelease.version} [skip ci]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@semantic-release/github",
|
||||||
|
[
|
||||||
|
"@saithodev/semantic-release-backmerge",
|
||||||
|
{
|
||||||
|
"backmergeStrategy": "merge"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
22
.swcrc
Normal file
22
.swcrc
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"jsc": {
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"decorators": true,
|
||||||
|
"dynamicImport": true
|
||||||
|
},
|
||||||
|
"transform": {
|
||||||
|
"legacyDecorator": true,
|
||||||
|
"decoratorMetadata": true
|
||||||
|
},
|
||||||
|
"target": "es2022",
|
||||||
|
"loose": true
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6",
|
||||||
|
"strict": false,
|
||||||
|
"strictMode": true,
|
||||||
|
"lazy": false,
|
||||||
|
"noInterop": false
|
||||||
|
}
|
||||||
|
}
|
8
.vscode/extensions.json
vendored
Normal file
8
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"davidanson.vscode-markdownlint"
|
||||||
|
]
|
||||||
|
}
|
11
.vscode/settings.json
vendored
Normal file
11
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.preferences.importModuleSpecifierEnding": "js",
|
||||||
|
"prettier.configPath": ".prettierrc.json",
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": true
|
||||||
|
},
|
||||||
|
"eslint.options": { "ignorePath": ".gitignore" }
|
||||||
|
}
|
132
CODE_OF_CONDUCT.md
Normal file
132
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
- Demonstrating empathy and kindness toward other people
|
||||||
|
- Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
- Giving and gracefully accepting constructive feedback
|
||||||
|
- Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
- Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
- The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
- Public or private harassment
|
||||||
|
- Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
- Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at
|
||||||
|
contact@divlo.fr.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][mozilla coc].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][faq]. Translations are available
|
||||||
|
at [https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[mozilla coc]: https://github.com/mozilla/diversity
|
||||||
|
[faq]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
56
CONTRIBUTING.md
Normal file
56
CONTRIBUTING.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# 💡 Contributing
|
||||||
|
|
||||||
|
Thanks a lot for your interest in contributing to **Thream/file-uploads-api**! 🎉
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
**Thream** has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated.
|
||||||
|
|
||||||
|
## Open Development
|
||||||
|
|
||||||
|
All work on **Thream/file-uploads-api** happens directly on [GitHub](https://github.com/Thream). Both core team members and external contributors send pull requests which go through the same review process.
|
||||||
|
|
||||||
|
## Types of contributions
|
||||||
|
|
||||||
|
- Reporting a bug.
|
||||||
|
- Suggest a new feature idea.
|
||||||
|
- Correct spelling errors, improvements or additions to documentation files.
|
||||||
|
- Improve structure/format/performance/refactor/tests of the code.
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
- **Please first discuss** the change you wish to make via issues.
|
||||||
|
|
||||||
|
- Ensure your code respect linting.
|
||||||
|
|
||||||
|
- Make sure your **code passes the tests**.
|
||||||
|
|
||||||
|
If you're adding new features to **Thream/file-uploads-api**, please include tests.
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
|
||||||
|
The commit message guidelines respect
|
||||||
|
[@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)
|
||||||
|
and [Semantic Versioning](https://semver.org/) for releases.
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
Types define which kind of changes you made to the project.
|
||||||
|
|
||||||
|
| Types | Description |
|
||||||
|
| -------- | ------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| feat | A new feature. |
|
||||||
|
| fix | A bug fix. |
|
||||||
|
| docs | Documentation only changes. |
|
||||||
|
| style | Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc). |
|
||||||
|
| refactor | A code change that neither fixes a bug nor adds a feature. |
|
||||||
|
| perf | A code change that improves performance. |
|
||||||
|
| test | Adding missing tests or correcting existing tests. |
|
||||||
|
| build | Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm). |
|
||||||
|
| ci | Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs). |
|
||||||
|
| chore | Other changes that don't modify src or test files. |
|
||||||
|
| revert | Reverts a previous commit. |
|
||||||
|
|
||||||
|
### Scopes
|
||||||
|
|
||||||
|
Scopes define what part of the code changed.
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Thream
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<h1 align="center"><a href="https://file-uploads-api.thream.divlo.fr/documentation">Thream/file-uploads-api</a></h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
|
||||||
|
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
|
||||||
|
<a href="./CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg" alt="Contributor Covenant" /></a>
|
||||||
|
<br />
|
||||||
|
<a href="https://github.com/Thream/file-uploads-api/actions/workflows/build.yml"><img src="https://github.com/Thream/file-uploads-api/actions/workflows/build.yml/badge.svg?branch=develop" /></a>
|
||||||
|
<a href="https://github.com/Thream/file-uploads-api/actions/workflows/lint.yml"><img src="https://github.com/Thream/file-uploads-api/actions/workflows/lint.yml/badge.svg?branch=develop" /></a>
|
||||||
|
<br />
|
||||||
|
<a href="https://conventionalcommits.org"><img src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg" alt="Conventional Commits" /></a>
|
||||||
|
<a href="https://github.com/semantic-release/semantic-release"><img src="https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg" alt="semantic-release" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## 📜 About
|
||||||
|
|
||||||
|
Thream's application programming interface (API) to upload files.
|
||||||
|
|
||||||
|
## ⚙️ Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- [Node.js](https://nodejs.org/) >= 16.0.0
|
||||||
|
- [npm](https://www.npmjs.com/) >= 8.0.0
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Clone the repository
|
||||||
|
git clone https://github.com/Thream/file-uploads-api.git
|
||||||
|
|
||||||
|
# Go to the project root
|
||||||
|
cd file-uploads-api
|
||||||
|
|
||||||
|
# Configure environment variables
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Install
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
You will need to configure the environment variables by creating an `.env` file at
|
||||||
|
the root of the project (see `.env.example`).
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Services started
|
||||||
|
|
||||||
|
- API : `http://localhost:8000`
|
||||||
|
|
||||||
|
## 💡 Contributing
|
||||||
|
|
||||||
|
Anyone can help to improve the project, submit a Feature Request, a bug report or
|
||||||
|
even correct a simple spelling mistake.
|
||||||
|
|
||||||
|
The steps to contribute can be found in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
[MIT](./LICENSE)
|
19254
package-lock.json
generated
Normal file
19254
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
73
package.json
Normal file
73
package.json
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"name": "@thream/file-uploads-api",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Thream's application programming interface to upload files.",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Thream/file-uploads-api"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.0.0",
|
||||||
|
"npm": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf ./build && swc ./src --out-dir ./build && tsc",
|
||||||
|
"build:dev": "swc ./src --out-dir ./build --watch",
|
||||||
|
"start": "node 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\"",
|
||||||
|
"lint:commit": "commitlint",
|
||||||
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
|
"lint:markdown": "markdownlint \"**/*.md\" --dot --ignore-path \".gitignore\"",
|
||||||
|
"lint:typescript": "eslint \"**/*.{js,jsx,ts,tsx}\" --ignore-path \".gitignore\"",
|
||||||
|
"lint:prettier": "prettier \".\" --check --ignore-path \".gitignore\"",
|
||||||
|
"lint:staged": "lint-staged",
|
||||||
|
"release": "semantic-release",
|
||||||
|
"postinstall": "husky install"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "0.23.4",
|
||||||
|
"dotenv": "16.0.0",
|
||||||
|
"fastify": "3.28.0",
|
||||||
|
"fastify-cors": "6.0.3",
|
||||||
|
"fastify-helmet": "7.0.1",
|
||||||
|
"fastify-multipart": "5.3.1",
|
||||||
|
"fastify-plugin": "3.0.1",
|
||||||
|
"fastify-rate-limit": "5.8.0",
|
||||||
|
"fastify-sensible": "3.1.2",
|
||||||
|
"fastify-static": "4.6.1",
|
||||||
|
"fastify-swagger": "5.1.0",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"read-pkg": "7.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "16.2.3",
|
||||||
|
"@commitlint/config-conventional": "16.2.1",
|
||||||
|
"@saithodev/semantic-release-backmerge": "2.1.2",
|
||||||
|
"@swc/cli": "0.1.57",
|
||||||
|
"@swc/core": "1.2.164",
|
||||||
|
"@types/busboy": "1.5.0",
|
||||||
|
"@types/http-errors": "1.8.2",
|
||||||
|
"@types/node": "17.0.23",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.18.0",
|
||||||
|
"concurrently": "7.1.0",
|
||||||
|
"cross-env": "7.0.3",
|
||||||
|
"editorconfig-checker": "4.0.2",
|
||||||
|
"eslint": "8.12.0",
|
||||||
|
"eslint-config-conventions": "2.0.0",
|
||||||
|
"eslint-config-prettier": "8.5.0",
|
||||||
|
"eslint-plugin-import": "2.26.0",
|
||||||
|
"eslint-plugin-prettier": "4.0.0",
|
||||||
|
"eslint-plugin-promise": "6.0.0",
|
||||||
|
"eslint-plugin-unicorn": "42.0.0",
|
||||||
|
"husky": "7.0.4",
|
||||||
|
"lint-staged": "12.3.7",
|
||||||
|
"markdownlint-cli": "0.31.1",
|
||||||
|
"nodemon": "2.0.15",
|
||||||
|
"prettier": "2.6.2",
|
||||||
|
"rimraf": "3.0.2",
|
||||||
|
"semantic-release": "19.0.2",
|
||||||
|
"typescript": "4.6.3"
|
||||||
|
}
|
||||||
|
}
|
38
src/application.ts
Normal file
38
src/application.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
import fastify from 'fastify'
|
||||||
|
import fastifyCors from 'fastify-cors'
|
||||||
|
import fastifySwagger from 'fastify-swagger'
|
||||||
|
import fastifyHelmet from 'fastify-helmet'
|
||||||
|
import fastifyRateLimit from 'fastify-rate-limit'
|
||||||
|
import fastifySensible from 'fastify-sensible'
|
||||||
|
import fastifyStatic from 'fastify-static'
|
||||||
|
|
||||||
|
import { services } from './services/index.js'
|
||||||
|
import { swaggerOptions } from './tools/configurations/swaggerOptions.js'
|
||||||
|
import { UPLOADS_URL } from './tools/configurations/index.js'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
export const application = fastify({
|
||||||
|
logger: process.env.NODE_ENV === 'development',
|
||||||
|
ajv: {
|
||||||
|
customOptions: {
|
||||||
|
format: 'full'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await application.register(fastifyCors)
|
||||||
|
await application.register(fastifySensible)
|
||||||
|
await application.register(fastifyHelmet)
|
||||||
|
await application.register(fastifyRateLimit, {
|
||||||
|
max: 200,
|
||||||
|
timeWindow: '1 minute'
|
||||||
|
})
|
||||||
|
await application.register(fastifyStatic, {
|
||||||
|
root: fileURLToPath(UPLOADS_URL),
|
||||||
|
prefix: '/uploads/'
|
||||||
|
})
|
||||||
|
await application.register(fastifySwagger, swaggerOptions)
|
||||||
|
await application.register(services)
|
5
src/index.ts
Normal file
5
src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { application } from './application.js'
|
||||||
|
import { HOST, PORT } from './tools/configurations/index.js'
|
||||||
|
|
||||||
|
const address = await application.listen(PORT, HOST)
|
||||||
|
console.log('\u001B[36m%s\u001B[0m', `🚀 Server listening at ${address}`)
|
43
src/models/utils.ts
Normal file
43
src/models/utils.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { Type } from '@sinclair/typebox'
|
||||||
|
|
||||||
|
export const fastifyErrorsSchema = {
|
||||||
|
400: {
|
||||||
|
statusCode: Type.Literal(400),
|
||||||
|
error: Type.Literal('Bad Request'),
|
||||||
|
message: Type.String()
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
statusCode: Type.Literal(401),
|
||||||
|
error: Type.Literal('Unauthorized'),
|
||||||
|
message: Type.Literal('Unauthorized')
|
||||||
|
},
|
||||||
|
403: {
|
||||||
|
statusCode: Type.Literal(403),
|
||||||
|
error: Type.Literal('Forbidden'),
|
||||||
|
message: Type.Literal('Forbidden')
|
||||||
|
},
|
||||||
|
404: {
|
||||||
|
statusCode: Type.Literal(404),
|
||||||
|
error: Type.Literal('Not Found'),
|
||||||
|
message: Type.Literal('Not Found')
|
||||||
|
},
|
||||||
|
431: {
|
||||||
|
statusCode: Type.Literal(431),
|
||||||
|
error: Type.Literal('Request Header Fields Too Large'),
|
||||||
|
message: Type.String()
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
statusCode: Type.Literal(500),
|
||||||
|
error: Type.Literal('Internal Server Error'),
|
||||||
|
message: Type.Literal('Something went wrong')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fastifyErrors = {
|
||||||
|
400: Type.Object(fastifyErrorsSchema[400]),
|
||||||
|
401: Type.Object(fastifyErrorsSchema[401]),
|
||||||
|
403: Type.Object(fastifyErrorsSchema[403]),
|
||||||
|
404: Type.Object(fastifyErrorsSchema[404]),
|
||||||
|
431: Type.Object(fastifyErrorsSchema[431]),
|
||||||
|
500: Type.Object(fastifyErrorsSchema[500])
|
||||||
|
}
|
7
src/services/index.ts
Normal file
7
src/services/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { FastifyPluginAsync } from 'fastify'
|
||||||
|
|
||||||
|
import { uploadsService } from './uploads/index.js'
|
||||||
|
|
||||||
|
export const services: FastifyPluginAsync = async (fastify) => {
|
||||||
|
await fastify.register(uploadsService)
|
||||||
|
}
|
40
src/services/uploads/guilds/get.ts
Normal file
40
src/services/uploads/guilds/get.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||||
|
import { Static, Type } from '@sinclair/typebox'
|
||||||
|
|
||||||
|
import { fastifyErrors } from '../../../models/utils.js'
|
||||||
|
|
||||||
|
const parameters = Type.Object({
|
||||||
|
file: Type.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type Parameters = Static<typeof parameters>
|
||||||
|
|
||||||
|
export const getServiceSchema: FastifySchema = {
|
||||||
|
tags: ['uploads'] as string[],
|
||||||
|
params: parameters,
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary'
|
||||||
|
},
|
||||||
|
400: fastifyErrors[400],
|
||||||
|
404: fastifyErrors[404],
|
||||||
|
500: fastifyErrors[500]
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const getGuildsUploadsService: FastifyPluginAsync = async (fastify) => {
|
||||||
|
await fastify.route<{
|
||||||
|
Params: Parameters
|
||||||
|
}>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/uploads/guilds/:file',
|
||||||
|
schema: getServiceSchema,
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const { file } = request.params
|
||||||
|
return await reply.sendFile(path.join('guilds', file))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
46
src/services/uploads/guilds/post.ts
Normal file
46
src/services/uploads/guilds/post.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Type } from '@sinclair/typebox'
|
||||||
|
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||||
|
import fastifyMultipart from 'fastify-multipart'
|
||||||
|
|
||||||
|
import { fastifyErrors } from '../../../models/utils.js'
|
||||||
|
import { uploadFile } from '../../../tools/utils/uploadFile.js'
|
||||||
|
import {
|
||||||
|
MAXIMUM_IMAGE_SIZE,
|
||||||
|
SUPPORTED_IMAGE_MIMETYPE
|
||||||
|
} from '../../../tools/configurations/index.js'
|
||||||
|
|
||||||
|
const postServiceSchema: FastifySchema = {
|
||||||
|
description: 'Uploads guild icon',
|
||||||
|
tags: ['uploads'] as string[],
|
||||||
|
consumes: ['multipart/form-data'] as string[],
|
||||||
|
produces: ['application/json'] as string[],
|
||||||
|
response: {
|
||||||
|
201: Type.String(),
|
||||||
|
400: fastifyErrors[400],
|
||||||
|
431: fastifyErrors[431],
|
||||||
|
500: fastifyErrors[500]
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const postGuildsUploadsIconService: FastifyPluginAsync = async (
|
||||||
|
fastify
|
||||||
|
) => {
|
||||||
|
await fastify.register(fastifyMultipart)
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/uploads/guilds',
|
||||||
|
schema: postServiceSchema,
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const file = await uploadFile({
|
||||||
|
fastify,
|
||||||
|
request,
|
||||||
|
folderInUploadsFolder: 'guilds',
|
||||||
|
maximumFileSize: MAXIMUM_IMAGE_SIZE,
|
||||||
|
supportedFileMimetype: SUPPORTED_IMAGE_MIMETYPE
|
||||||
|
})
|
||||||
|
reply.statusCode = 201
|
||||||
|
return file.pathToStoreInDatabase
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
19
src/services/uploads/index.ts
Normal file
19
src/services/uploads/index.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { FastifyPluginAsync } from 'fastify'
|
||||||
|
|
||||||
|
import { getGuildsUploadsService } from './guilds/get.js'
|
||||||
|
import { postGuildsUploadsIconService } from './guilds/post.js'
|
||||||
|
import { getMessagesUploadsService } from './messages/get.js'
|
||||||
|
import { postMessagesUploadsService } from './messages/post.js'
|
||||||
|
import { getUsersUploadsService } from './users/get.js'
|
||||||
|
import { postUsersUploadsLogoService } from './users/post.js'
|
||||||
|
|
||||||
|
export const uploadsService: FastifyPluginAsync = async (fastify) => {
|
||||||
|
await fastify.register(getGuildsUploadsService)
|
||||||
|
await fastify.register(postGuildsUploadsIconService)
|
||||||
|
|
||||||
|
await fastify.register(getMessagesUploadsService)
|
||||||
|
await fastify.register(postMessagesUploadsService)
|
||||||
|
|
||||||
|
await fastify.register(getUsersUploadsService)
|
||||||
|
await fastify.register(postUsersUploadsLogoService)
|
||||||
|
}
|
42
src/services/uploads/messages/get.ts
Normal file
42
src/services/uploads/messages/get.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||||
|
import { Static, Type } from '@sinclair/typebox'
|
||||||
|
|
||||||
|
import { fastifyErrors } from '../../../models/utils.js'
|
||||||
|
|
||||||
|
const parameters = Type.Object({
|
||||||
|
file: Type.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type Parameters = Static<typeof parameters>
|
||||||
|
|
||||||
|
export const getServiceSchema: FastifySchema = {
|
||||||
|
tags: ['uploads'] as string[],
|
||||||
|
params: parameters,
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary'
|
||||||
|
},
|
||||||
|
400: fastifyErrors[400],
|
||||||
|
404: fastifyErrors[404],
|
||||||
|
500: fastifyErrors[500]
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const getMessagesUploadsService: FastifyPluginAsync = async (
|
||||||
|
fastify
|
||||||
|
) => {
|
||||||
|
fastify.route<{
|
||||||
|
Params: Parameters
|
||||||
|
}>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/uploads/messages/:file',
|
||||||
|
schema: getServiceSchema,
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const { file } = request.params
|
||||||
|
return await reply.sendFile(path.join('messages', file))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
42
src/services/uploads/messages/post.ts
Normal file
42
src/services/uploads/messages/post.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Type } from '@sinclair/typebox'
|
||||||
|
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||||
|
import fastifyMultipart from 'fastify-multipart'
|
||||||
|
|
||||||
|
import { fastifyErrors } from '../../../models/utils.js'
|
||||||
|
import { uploadFile } from '../../../tools/utils/uploadFile.js'
|
||||||
|
import { MAXIMUM_IMAGE_SIZE } from '../../../tools/configurations/index.js'
|
||||||
|
|
||||||
|
const postServiceSchema: FastifySchema = {
|
||||||
|
description: 'Uploads message file',
|
||||||
|
tags: ['uploads'] as string[],
|
||||||
|
consumes: ['multipart/form-data'] as string[],
|
||||||
|
produces: ['application/json'] as string[],
|
||||||
|
response: {
|
||||||
|
201: Type.String(),
|
||||||
|
400: fastifyErrors[400],
|
||||||
|
431: fastifyErrors[431],
|
||||||
|
500: fastifyErrors[500]
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const postMessagesUploadsService: FastifyPluginAsync = async (
|
||||||
|
fastify
|
||||||
|
) => {
|
||||||
|
await fastify.register(fastifyMultipart)
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/uploads/messages',
|
||||||
|
schema: postServiceSchema,
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const file = await uploadFile({
|
||||||
|
fastify,
|
||||||
|
request,
|
||||||
|
folderInUploadsFolder: 'messages',
|
||||||
|
maximumFileSize: MAXIMUM_IMAGE_SIZE
|
||||||
|
})
|
||||||
|
reply.statusCode = 201
|
||||||
|
return file.pathToStoreInDatabase
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
40
src/services/uploads/users/get.ts
Normal file
40
src/services/uploads/users/get.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||||
|
import { Static, Type } from '@sinclair/typebox'
|
||||||
|
|
||||||
|
import { fastifyErrors } from '../../../models/utils.js'
|
||||||
|
|
||||||
|
const parameters = Type.Object({
|
||||||
|
file: Type.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
type Parameters = Static<typeof parameters>
|
||||||
|
|
||||||
|
export const getServiceSchema: FastifySchema = {
|
||||||
|
tags: ['uploads'] as string[],
|
||||||
|
params: parameters,
|
||||||
|
response: {
|
||||||
|
200: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'binary'
|
||||||
|
},
|
||||||
|
400: fastifyErrors[400],
|
||||||
|
404: fastifyErrors[404],
|
||||||
|
500: fastifyErrors[500]
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const getUsersUploadsService: FastifyPluginAsync = async (fastify) => {
|
||||||
|
await fastify.route<{
|
||||||
|
Params: Parameters
|
||||||
|
}>({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/uploads/users/:file',
|
||||||
|
schema: getServiceSchema,
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const { file } = request.params
|
||||||
|
return await reply.sendFile(path.join('users', file))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
46
src/services/uploads/users/post.ts
Normal file
46
src/services/uploads/users/post.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { Type } from '@sinclair/typebox'
|
||||||
|
import { FastifyPluginAsync, FastifySchema } from 'fastify'
|
||||||
|
import fastifyMultipart from 'fastify-multipart'
|
||||||
|
|
||||||
|
import { fastifyErrors } from '../../../models/utils.js'
|
||||||
|
import { uploadFile } from '../../../tools/utils/uploadFile.js'
|
||||||
|
import {
|
||||||
|
MAXIMUM_IMAGE_SIZE,
|
||||||
|
SUPPORTED_IMAGE_MIMETYPE
|
||||||
|
} from '../../../tools/configurations/index.js'
|
||||||
|
|
||||||
|
const postServiceSchema: FastifySchema = {
|
||||||
|
description: 'Uploads user logo',
|
||||||
|
tags: ['uploads'] as string[],
|
||||||
|
consumes: ['multipart/form-data'] as string[],
|
||||||
|
produces: ['application/json'] as string[],
|
||||||
|
response: {
|
||||||
|
201: Type.String(),
|
||||||
|
400: fastifyErrors[400],
|
||||||
|
431: fastifyErrors[431],
|
||||||
|
500: fastifyErrors[500]
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const postUsersUploadsLogoService: FastifyPluginAsync = async (
|
||||||
|
fastify
|
||||||
|
) => {
|
||||||
|
await fastify.register(fastifyMultipart)
|
||||||
|
|
||||||
|
fastify.route({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/uploads/users',
|
||||||
|
schema: postServiceSchema,
|
||||||
|
handler: async (request, reply) => {
|
||||||
|
const file = await uploadFile({
|
||||||
|
fastify,
|
||||||
|
request,
|
||||||
|
folderInUploadsFolder: 'users',
|
||||||
|
maximumFileSize: MAXIMUM_IMAGE_SIZE,
|
||||||
|
supportedFileMimetype: SUPPORTED_IMAGE_MIMETYPE
|
||||||
|
})
|
||||||
|
reply.statusCode = 201
|
||||||
|
return file.pathToStoreInDatabase
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
26
src/tools/configurations/index.ts
Normal file
26
src/tools/configurations/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { URL } from 'node:url'
|
||||||
|
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export const PORT = parseInt(process.env.PORT ?? '8000', 10)
|
||||||
|
export const HOST = process.env.HOST ?? '0.0.0.0'
|
||||||
|
export const API_URL = process.env.API_URL ?? `http://${HOST}:${PORT}`
|
||||||
|
|
||||||
|
export const SRC_URL = new URL('../../', import.meta.url)
|
||||||
|
export const ROOT_URL = new URL('../', SRC_URL)
|
||||||
|
export const UPLOADS_URL = new URL('./uploads/', ROOT_URL)
|
||||||
|
|
||||||
|
export const SUPPORTED_IMAGE_MIMETYPE = [
|
||||||
|
'image/png',
|
||||||
|
'image/jpg',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/gif'
|
||||||
|
]
|
||||||
|
|
||||||
|
/** in megabytes */
|
||||||
|
export const MAXIMUM_IMAGE_SIZE = 10
|
||||||
|
|
||||||
|
/** in megabytes */
|
||||||
|
export const MAXIMUM_FILE_SIZE = 100
|
22
src/tools/configurations/swaggerOptions.ts
Normal file
22
src/tools/configurations/swaggerOptions.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import dotenv from 'dotenv'
|
||||||
|
import { readPackage } from 'read-pkg'
|
||||||
|
import { FastifyDynamicSwaggerOptions } from 'fastify-swagger'
|
||||||
|
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const packageJSON = await readPackage()
|
||||||
|
|
||||||
|
export const swaggerOptions: FastifyDynamicSwaggerOptions = {
|
||||||
|
routePrefix: '/documentation',
|
||||||
|
openapi: {
|
||||||
|
info: {
|
||||||
|
title: packageJSON.name,
|
||||||
|
description: packageJSON.description,
|
||||||
|
version: packageJSON.version
|
||||||
|
},
|
||||||
|
tags: [{ name: 'guilds' }, { name: 'messages' }, { name: 'users' }]
|
||||||
|
},
|
||||||
|
exposeRoute: true,
|
||||||
|
staticCSP: true,
|
||||||
|
hideUntagged: true
|
||||||
|
}
|
68
src/tools/utils/uploadFile.ts
Normal file
68
src/tools/utils/uploadFile.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
import { URL } from 'node:url'
|
||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
|
|
||||||
|
import { FastifyInstance, FastifyRequest } from 'fastify'
|
||||||
|
import { Multipart } from 'fastify-multipart'
|
||||||
|
|
||||||
|
import { API_URL, ROOT_URL } from '../configurations/index.js'
|
||||||
|
|
||||||
|
export interface UploadFileOptions {
|
||||||
|
folderInUploadsFolder: 'guilds' | 'messages' | 'users'
|
||||||
|
request: FastifyRequest
|
||||||
|
fastify: FastifyInstance
|
||||||
|
|
||||||
|
/** in megabytes */
|
||||||
|
maximumFileSize: number
|
||||||
|
|
||||||
|
supportedFileMimetype?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadFileResult {
|
||||||
|
pathToStoreInDatabase: string
|
||||||
|
mimetype: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadFile = async (
|
||||||
|
options: UploadFileOptions
|
||||||
|
): Promise<UploadFileResult> => {
|
||||||
|
const {
|
||||||
|
fastify,
|
||||||
|
request,
|
||||||
|
folderInUploadsFolder,
|
||||||
|
maximumFileSize,
|
||||||
|
supportedFileMimetype
|
||||||
|
} = options
|
||||||
|
let files: Multipart[] = []
|
||||||
|
try {
|
||||||
|
files = await request.saveRequestFiles({
|
||||||
|
limits: {
|
||||||
|
files: 1,
|
||||||
|
fileSize: maximumFileSize * 1024 * 1024
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw fastify.httpErrors.requestHeaderFieldsTooLarge(
|
||||||
|
`File should be less than ${maximumFileSize}mb.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (files.length !== 1) {
|
||||||
|
throw fastify.httpErrors.badRequest('You must upload at most one file.')
|
||||||
|
}
|
||||||
|
const file = files[0]
|
||||||
|
if (
|
||||||
|
supportedFileMimetype != null &&
|
||||||
|
!supportedFileMimetype.includes(file.mimetype)
|
||||||
|
) {
|
||||||
|
throw fastify.httpErrors.badRequest(
|
||||||
|
`The file must have a valid type (${supportedFileMimetype.join(', ')}).`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const splitedMimetype = file.mimetype.split('/')
|
||||||
|
const fileExtension = splitedMimetype[1]
|
||||||
|
const filePath = `uploads/${folderInUploadsFolder}/${randomUUID()}.${fileExtension}`
|
||||||
|
const fileURL = new URL(filePath, ROOT_URL)
|
||||||
|
const pathToStoreInDatabase = `${API_URL}/${filePath}`
|
||||||
|
await fs.promises.copyFile(file.filepath, fileURL)
|
||||||
|
return { pathToStoreInDatabase, mimetype: file.mimetype }
|
||||||
|
}
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./build",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
0
uploads/guilds/.gitkeep
Normal file
0
uploads/guilds/.gitkeep
Normal file
0
uploads/messages/.gitkeep
Normal file
0
uploads/messages/.gitkeep
Normal file
0
uploads/users/.gitkeep
Normal file
0
uploads/users/.gitkeep
Normal file
Reference in New Issue
Block a user