Compare commits
133 Commits
fix/env-se
...
revert-334
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de308f9cef | ||
|
|
887dac5285 | ||
|
|
f34d896095 | ||
|
|
e95ebb9226 | ||
|
|
57365eeae0 | ||
|
|
b22bd97818 | ||
|
|
b953b32ff4 | ||
|
|
0eacd6763b | ||
|
|
8499ac7fec | ||
|
|
4adac4af38 | ||
|
|
fd162e242c | ||
|
|
3e83828722 | ||
|
|
f7dc36e3f1 | ||
|
|
a7566dfd86 | ||
|
|
d4d7a20fbd | ||
|
|
dfb281bcf7 | ||
|
|
c62482e81f | ||
|
|
886847ab7b | ||
|
|
a268cab11e | ||
|
|
e9509b9fa1 | ||
|
|
8db452089c | ||
|
|
a1764023f3 | ||
|
|
b08b63dc73 | ||
|
|
a9a4ebf595 | ||
|
|
a8e279db28 | ||
|
|
d09a3e9237 | ||
|
|
efa40cf6ea | ||
|
|
1a3d9f18ab | ||
|
|
653ccd3240 | ||
|
|
c0806cfd07 | ||
|
|
008eb6b77b | ||
|
|
ac60843183 | ||
|
|
3c3fb1e4a9 | ||
|
|
88212e8cfe | ||
|
|
191fa376d2 | ||
|
|
6efae3a395 | ||
|
|
cb8678f07f | ||
|
|
b32b0f9bcb | ||
|
|
5a91fb53b2 | ||
|
|
b0b6edc58e | ||
|
|
8c57d81718 | ||
|
|
10bb68a538 | ||
|
|
d4d1e27ba9 | ||
|
|
d5c887f311 | ||
|
|
ce7adf6da3 | ||
|
|
c626fb9241 | ||
|
|
f21ed30e10 | ||
|
|
b55970cc7a | ||
|
|
74ad2e43a4 | ||
|
|
2d6282cf8b | ||
|
|
e255c46455 | ||
|
|
15c2c7bb5b | ||
|
|
71bcd22444 | ||
|
|
2d104160f2 | ||
|
|
f7c1825de5 | ||
|
|
2c1fd5d711 | ||
|
|
085fbb2a9b | ||
|
|
05f2d8817b | ||
|
|
81fbb22c51 | ||
|
|
01cf59c663 | ||
|
|
5c8ebaff3e | ||
|
|
0e70c28324 | ||
|
|
88f6a4ae26 | ||
|
|
610538ca02 | ||
|
|
8970ff5c68 | ||
|
|
d1a564d5b8 | ||
|
|
8bb1d19c07 | ||
|
|
c1efa381f0 | ||
|
|
29171d1b6f | ||
|
|
e869d49e16 | ||
|
|
6496bea846 | ||
|
|
39842559b5 | ||
|
|
51efb35aa6 | ||
|
|
9402bb9285 | ||
|
|
5a516f7242 | ||
|
|
3b217d78e7 | ||
|
|
8e153b38dc | ||
|
|
6f38bfb148 | ||
|
|
82b6e08d68 | ||
|
|
31fd6567b7 | ||
|
|
25177bd635 | ||
|
|
6928eb7992 | ||
|
|
8300f9a0a2 | ||
|
|
525ba77739 | ||
|
|
6bc748a267 | ||
|
|
5230d2d3b8 | ||
|
|
c3531c9d8b | ||
|
|
b29c04c28d | ||
|
|
b2af353941 | ||
|
|
9dbce74f5e | ||
|
|
db1cf5cc08 | ||
|
|
09360abf81 | ||
|
|
355bd62b8d | ||
|
|
5650de1183 | ||
|
|
2ee8614b93 | ||
|
|
5632334c9a | ||
|
|
780dd8a713 | ||
|
|
7db3c6d290 | ||
|
|
c765270dfe | ||
|
|
03f667c21d | ||
|
|
f79f3078dc | ||
|
|
6e29a2f6d4 | ||
|
|
6304fd50c3 | ||
|
|
2ec29c47ad | ||
|
|
399a238bf4 | ||
|
|
b20ab72298 | ||
|
|
f723e6496a | ||
|
|
8c0aff8863 | ||
|
|
64c5077506 | ||
|
|
2afc87847d | ||
|
|
878ec833ce | ||
|
|
039de8015f | ||
|
|
f67b366b90 | ||
|
|
6f35574d68 | ||
|
|
77e8a36ab0 | ||
|
|
d7cc9f5dbc | ||
|
|
fc3e3aeaec | ||
|
|
4ba135f3b9 | ||
|
|
24894e05dc | ||
|
|
e2b668bee2 | ||
|
|
f112c46bb4 | ||
|
|
331d482b22 | ||
|
|
b07243f131 | ||
|
|
84b0c30d64 | ||
|
|
e3dd9e99a1 | ||
|
|
e3091cb6db | ||
|
|
270f796683 | ||
|
|
24c6bce02d | ||
|
|
2db567589f | ||
|
|
1fe83ebdc8 | ||
|
|
8320d4f222 | ||
|
|
e76c1bc64c | ||
|
|
81a7e23a12 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
**/*/node_modules
|
||||||
@@ -13,6 +13,7 @@ SESSION_SECRET='add some secret here'
|
|||||||
# Hoppscotch App Domain Config
|
# Hoppscotch App Domain Config
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||||
|
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||||
|
|
||||||
# Google Auth Config
|
# Google Auth Config
|
||||||
GOOGLE_CLIENT_ID="************************************************"
|
GOOGLE_CLIENT_ID="************************************************"
|
||||||
@@ -31,6 +32,7 @@ MICROSOFT_CLIENT_ID="************************************************"
|
|||||||
MICROSOFT_CLIENT_SECRET="************************************************"
|
MICROSOFT_CLIENT_SECRET="************************************************"
|
||||||
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
||||||
MICROSOFT_SCOPE="user.read"
|
MICROSOFT_SCOPE="user.read"
|
||||||
|
MICROSOFT_TENANT="common"
|
||||||
|
|
||||||
# Mailer config
|
# Mailer config
|
||||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
||||||
|
|||||||
66
.github/workflows/release-push-docker.yml
vendored
Normal file
66
.github/workflows/release-push-docker.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
name: "Push containers to Docker Hub on release"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- '*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
run: cp .env.example .env
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: backend
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_BACKEND_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: app
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_FRONTEND_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: sh_admin
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_SH_ADMIN_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Build and push `${{ secrets.DOCKER_AIO_CONTAINER_NAME }}`
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./prod.Dockerfile
|
||||||
|
target: aio
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:latest
|
||||||
|
${{ secrets.DOCKER_ORG_NAME }}/${{ secrets.DOCKER_AIO_CONTAINER_NAME }}:${{ github.ref_name }}
|
||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -2,9 +2,9 @@ name: Node.js CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, staging]
|
branches: [main, staging, "release/**"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, staging]
|
branches: [main, staging, "release/**"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
42
.github/workflows/ui.yml
vendored
Normal file
42
.github/workflows/ui.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Deploy to Netlify (ui)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
# run this workflow only if an update is made to the ui package
|
||||||
|
paths:
|
||||||
|
- "packages/hoppscotch-ui/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: Deploy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup environment
|
||||||
|
run: mv .env.example .env
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v2.2.4
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
run_install: true
|
||||||
|
|
||||||
|
- name: Setup node
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node }}
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Build site
|
||||||
|
run: pnpm run generate-ui
|
||||||
|
|
||||||
|
# Deploy the ui site with netlify-cli
|
||||||
|
- name: Deploy to Netlify (ui)
|
||||||
|
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||||
|
env:
|
||||||
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||||
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false
|
semi: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 80,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ We as members, contributors, and leaders pledge to make participation in our
|
|||||||
community a harassment-free experience for everyone, regardless of age, body
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
identity and expression, level of experience, education, socio-economic status,
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
nationality, personal appearance, race, religion, or sexual identity
|
nationality, personal appearance, race, caste, color, religion, or sexual
|
||||||
and orientation.
|
identity and orientation.
|
||||||
|
|
||||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
diverse, inclusive, and healthy community.
|
diverse, inclusive, and healthy community.
|
||||||
@@ -22,17 +22,17 @@ community include:
|
|||||||
* Giving and gracefully accepting constructive feedback
|
* Giving and gracefully accepting constructive feedback
|
||||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
and learning from the experience
|
and learning from the experience
|
||||||
* Focusing on what is best not just for us as individuals, but for the
|
* Focusing on what is best not just for us as individuals, but for the overall
|
||||||
overall community
|
community
|
||||||
|
|
||||||
Examples of unacceptable behavior include:
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
* The use of sexualized language or imagery, and sexual attention or
|
* The use of sexualized language or imagery, and sexual attention or advances of
|
||||||
advances of any kind
|
any kind
|
||||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
* Public or private harassment
|
* Public or private harassment
|
||||||
* Publishing others' private information, such as a physical or email
|
* Publishing others' private information, such as a physical or email address,
|
||||||
address, without their explicit permission
|
without their explicit permission
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
professional setting
|
professional setting
|
||||||
|
|
||||||
@@ -82,15 +82,15 @@ behavior was inappropriate. A public apology may be requested.
|
|||||||
|
|
||||||
### 2. Warning
|
### 2. Warning
|
||||||
|
|
||||||
**Community Impact**: A violation through a single incident or series
|
**Community Impact**: A violation through a single incident or series of
|
||||||
of actions.
|
actions.
|
||||||
|
|
||||||
**Consequence**: A warning with consequences for continued behavior. No
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
interaction with the people involved, including unsolicited interaction with
|
interaction with the people involved, including unsolicited interaction with
|
||||||
those enforcing the Code of Conduct, for a specified period of time. This
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
includes avoiding interactions in community spaces as well as external channels
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
like social media. Violating these terms may lead to a temporary or
|
like social media. Violating these terms may lead to a temporary or permanent
|
||||||
permanent ban.
|
ban.
|
||||||
|
|
||||||
### 3. Temporary Ban
|
### 3. Temporary Ban
|
||||||
|
|
||||||
@@ -109,20 +109,24 @@ Violating these terms may lead to a permanent ban.
|
|||||||
standards, including sustained inappropriate behavior, harassment of an
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
individual, or aggression toward or disparagement of classes of individuals.
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
**Consequence**: A permanent ban from any sort of public interaction within
|
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||||
the community.
|
community.
|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
version 2.0, available at
|
version 2.1, available at
|
||||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
Community Impact Guidelines were inspired by
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
||||||
https://www.contributor-covenant.org/translations.
|
[https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.1]: https://www.contributor-covenant.org/version/2/1/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
|
||||||
|
|||||||
180
README.md
180
README.md
@@ -2,23 +2,18 @@
|
|||||||
<a href="https://hoppscotch.io">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<img
|
||||||
src="https://avatars.githubusercontent.com/u/56705483"
|
src="https://avatars.githubusercontent.com/u/56705483"
|
||||||
alt="Hoppscotch Logo"
|
alt="Hoppscotch"
|
||||||
height="64"
|
height="64"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
<h3>
|
<h3>
|
||||||
<b>
|
<b>
|
||||||
Hoppscotch
|
Hoppscotch
|
||||||
</b>
|
</b>
|
||||||
</h3>
|
</h3>
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<b>
|
<b>
|
||||||
Open source API development ecosystem
|
Open Source API Development Ecosystem
|
||||||
</b>
|
</b>
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
|
[](CODE_OF_CONDUCT.md) [](https://hoppscotch.io) [](https://github.com/hoppscotch/hoppscotch/actions) [](https://twitter.com/share?text=%F0%9F%91%BD%20Hoppscotch%20%E2%80%A2%20Open%20source%20API%20development%20ecosystem%20-%20Helps%20you%20create%20requests%20faster,%20saving%20precious%20time%20on%20development.&url=https://hoppscotch.io&hashtags=hoppscotch&via=hoppscotch_io)
|
||||||
@@ -34,23 +29,18 @@
|
|||||||
</p>
|
</p>
|
||||||
<br />
|
<br />
|
||||||
<p>
|
<p>
|
||||||
<a href="https://hoppscotch.io/#gh-light-mode-only" target="_blank">
|
<a href="https://hoppscotch.io">
|
||||||
<img
|
<picture>
|
||||||
src="./packages/hoppscotch-common/public/images/banner-light.png"
|
<source media="(prefers-color-scheme: dark)" srcset="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||||
alt="Hoppscotch"
|
<source media="(prefers-color-scheme: light)" srcset="./packages/hoppscotch-common/public/images/banner-light.png">
|
||||||
width="100%"
|
<img alt="Hoppscotch" src="./packages/hoppscotch-common/public/images/banner-dark.png">
|
||||||
/>
|
</picture>
|
||||||
</a>
|
|
||||||
<a href="https://hoppscotch.io/#gh-dark-mode-only" target="_blank">
|
|
||||||
<img
|
|
||||||
src="./packages/hoppscotch-common/public/images/banner-dark.png"
|
|
||||||
alt="Hoppscotch"
|
|
||||||
width="100%"
|
|
||||||
/>
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
_We highly recommend you take a look at the [**Hoppscotch Documentation**](https://docs.hoppscotch.io) to learn more about the app._
|
||||||
|
|
||||||
#### **Support**
|
#### **Support**
|
||||||
|
|
||||||
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
[](https://hoppscotch.io/discord) [](https://hoppscotch.io/telegram) [](https://github.com/hoppscotch/hoppscotch/discussions)
|
||||||
@@ -59,9 +49,9 @@
|
|||||||
|
|
||||||
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
❤️ **Lightweight:** Crafted with minimalistic UI design.
|
||||||
|
|
||||||
⚡️ **Fast:** Send requests and get/copy responses in real-time.
|
⚡️ **Fast:** Send requests and get responses in real time.
|
||||||
|
|
||||||
**HTTP Methods**
|
🗄️ **HTTP Methods:** Request methods define the type of action you are requesting to be performed.
|
||||||
|
|
||||||
- `GET` - Requests retrieve resource information
|
- `GET` - Requests retrieve resource information
|
||||||
- `POST` - The server creates a new entry in a database
|
- `POST` - The server creates a new entry in a database
|
||||||
@@ -74,17 +64,15 @@
|
|||||||
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
- `TRACE` - Performs a message loop-back test along the path to the target resource
|
||||||
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
- `<custom>` - Some APIs use custom request methods such as `LIST`. Type in your custom methods.
|
||||||
|
|
||||||
🌈 **Make it yours:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
🌈 **Theming:** Customizable combinations for background, foreground, and accent colors — [customize now](https://hoppscotch.io/settings).
|
||||||
|
|
||||||
**Theming**
|
- Choose a theme: System preference, Light, Dark, and Black
|
||||||
|
- Choose accent colors: Green, Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
||||||
- Choose a theme: System (default), Light, Dark, and Black
|
|
||||||
- Choose accent color: Green (default), Teal, Blue, Indigo, Purple, Yellow, Orange, Red, and Pink
|
|
||||||
- Distraction-free Zen mode
|
- Distraction-free Zen mode
|
||||||
|
|
||||||
_Customized themes are synced with cloud / local session_
|
_Customized themes are synced with your cloud/local session._
|
||||||
|
|
||||||
🔥 **PWA:** Install as a [PWA](https://web.dev/what-are-pwas/) on your device.
|
🔥 **PWA:** Install as a [Progressive Web App](https://web.dev/progressive-web-apps) on your device.
|
||||||
|
|
||||||
- Instant loading with Service Workers
|
- Instant loading with Service Workers
|
||||||
- Offline support
|
- Offline support
|
||||||
@@ -107,7 +95,7 @@ _Customized themes are synced with cloud / local session_
|
|||||||
|
|
||||||
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
|
📡 **Server-Sent Events:** Receive a stream of updates from a server over an HTTP connection without resorting to polling.
|
||||||
|
|
||||||
🌩 **Socket.IO:** Send and Receive data with SocketIO server.
|
🌩 **Socket.IO:** Send and Receive data with the SocketIO server.
|
||||||
|
|
||||||
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
🦟 **MQTT:** Subscribe and Publish to topics of an MQTT Broker.
|
||||||
|
|
||||||
@@ -127,7 +115,7 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- OAuth 2.0
|
- OAuth 2.0
|
||||||
- OIDC Access Token/PKCE
|
- OIDC Access Token/PKCE
|
||||||
|
|
||||||
📢 **Headers:** Describes the format the body of your request is being sent as.
|
📢 **Headers:** Describes the format the body of your request is being sent in.
|
||||||
|
|
||||||
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
📫 **Parameters:** Use request parameters to set varying parts in simulated requests.
|
||||||
|
|
||||||
@@ -137,14 +125,14 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- FormData, JSON, and many more
|
- FormData, JSON, and many more
|
||||||
- Toggle between key-value and RAW input parameter list
|
- Toggle between key-value and RAW input parameter list
|
||||||
|
|
||||||
👋 **Response:** Contains the status line, headers, and the message/response body.
|
📮 **Response:** Contains the status line, headers, and the message/response body.
|
||||||
|
|
||||||
- Copy response to clipboard
|
- Copy the response to the clipboard
|
||||||
- Download response as a file
|
- Download the response as a file
|
||||||
- View response headers
|
- View response headers
|
||||||
- View raw and preview of HTML, image, JSON, XML responses
|
- View raw and preview HTML, image, JSON, and XML responses
|
||||||
|
|
||||||
⏰ **History:** Request entries are synced with cloud / local session storage to restore with a single click.
|
⏰ **History:** Request entries are synced with your cloud/local session storage.
|
||||||
|
|
||||||
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
📁 **Collections:** Keep your API requests organized with collections and folders. Reuse them with a single click.
|
||||||
|
|
||||||
@@ -152,7 +140,32 @@ _Customized themes are synced with cloud / local session_
|
|||||||
- Nested folders
|
- Nested folders
|
||||||
- Export and import as a file or GitHub gist
|
- Export and import as a file or GitHub gist
|
||||||
|
|
||||||
_Collections are synced with cloud / local session storage_
|
_Collections are synced with your cloud/local session storage._
|
||||||
|
|
||||||
|
📜 **Pre-Request Scripts:** Snippets of code associated with a request that is executed before the request is sent.
|
||||||
|
|
||||||
|
- Set environment variables
|
||||||
|
- Include timestamp in the request headers
|
||||||
|
- Send a random alphanumeric string in the URL parameters
|
||||||
|
- Any JavaScript functions
|
||||||
|
|
||||||
|
👨👩👧👦 **Teams:** Helps you collaborate across your teams to design, develop, and test APIs faster.
|
||||||
|
|
||||||
|
- Create unlimited teams
|
||||||
|
- Create unlimited shared collections
|
||||||
|
- Create unlimited team members
|
||||||
|
- Role-based access control
|
||||||
|
- Cloud sync
|
||||||
|
- Multiple devices
|
||||||
|
|
||||||
|
👥 **Workspaces:** Organize your personal and team collections environments into workspaces. Easily switch between workspaces to manage multiple projects.
|
||||||
|
|
||||||
|
- Create unlimited workspaces
|
||||||
|
- Switch between personal and team workspaces
|
||||||
|
|
||||||
|
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
|
||||||
|
|
||||||
|
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
|
||||||
|
|
||||||
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
🌐 **Proxy:** Enable Proxy Mode from Settings to access blocked APIs.
|
||||||
|
|
||||||
@@ -161,60 +174,31 @@ _Collections are synced with cloud / local session storage_
|
|||||||
- Access APIs served in non-HTTPS (`http://`) endpoints
|
- Access APIs served in non-HTTPS (`http://`) endpoints
|
||||||
- Use your Proxy URL
|
- Use your Proxy URL
|
||||||
|
|
||||||
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**_
|
_Official proxy server is hosted by Hoppscotch - **[GitHub](https://github.com/hoppscotch/proxyscotch)** - **[Privacy Policy](https://docs.hoppscotch.io/support/privacy)**._
|
||||||
|
|
||||||
📜 **Pre-Request Scripts β:** Snippets of code associated with a request that is executed before the request is sent.
|
|
||||||
|
|
||||||
- Set environment variables
|
|
||||||
- Include timestamp in the request headers
|
|
||||||
- Send a random alphanumeric string in the URL parameters
|
|
||||||
- Any JavaScript functions
|
|
||||||
|
|
||||||
📄 **API Documentation:** Create and share dynamic API documentation easily, quickly.
|
|
||||||
|
|
||||||
1. Add your requests to Collections and Folders
|
|
||||||
2. Export Collections and easily share your APIs with the rest of your team
|
|
||||||
3. Import Collections and Generate Documentation on-the-go
|
|
||||||
|
|
||||||
⌨️ **Keyboard Shortcuts:** Optimized for efficiency.
|
|
||||||
|
|
||||||
> **[Read our documentation on Keyboard Shortcuts](https://docs.hoppscotch.io/documentation/features/shortcuts)**
|
|
||||||
|
|
||||||
🌎 **i18n:** Experience the app in your language.
|
🌎 **i18n:** Experience the app in your language.
|
||||||
|
|
||||||
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md), and the process for submitting pull requests to us.
|
Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) for details on our [`CODE OF CONDUCT`](CODE_OF_CONDUCT.md) and the process for submitting pull requests to us.
|
||||||
|
|
||||||
📦 **Add-ons:** Official add-ons for hoppscotch.
|
☁️ **Auth + Sync:** Sign in and sync your data in real-time across all your devices.
|
||||||
|
|
||||||
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch
|
**Sign in with:**
|
||||||
- **[CLI β](https://github.com/hoppscotch/hopp-cli)** - A CLI solution for Hoppscotch
|
|
||||||
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that simplifies access to Hoppscotch
|
|
||||||
|
|
||||||
[ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
|
|
||||||
|
|
||||||
> **Extensions fixes `CORS` issues.**
|
|
||||||
|
|
||||||
- **[Hopp-Doc-Gen](https://github.com/hoppscotch/hopp-doc-gen)** - An API doc generator CLI for Hoppscotch
|
|
||||||
|
|
||||||
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
|
|
||||||
|
|
||||||
☁️ **Auth + Sync:** Sign in and sync your data in real-time.
|
|
||||||
|
|
||||||
**Sign in with**
|
|
||||||
|
|
||||||
- GitHub
|
- GitHub
|
||||||
- Google
|
- Google
|
||||||
- Microsoft
|
- Microsoft
|
||||||
- Email
|
- Email
|
||||||
|
- SSO (Single Sign-On)[^EE]
|
||||||
|
|
||||||
**Synchronize your data**
|
**🔄 Synchronize your data:** Handoff to continue tasks on your other devices.
|
||||||
|
|
||||||
|
- Workspaces
|
||||||
- History
|
- History
|
||||||
- Collections
|
- Collections
|
||||||
- Environments
|
- Environments
|
||||||
- Settings
|
- Settings
|
||||||
|
|
||||||
✅ **Post-Request Tests β:** Write tests associated with a request that is executed after the request's response.
|
✅ **Post-Request Tests:** Write tests associated with a request that is executed after the request's response.
|
||||||
|
|
||||||
- Check the status code as an integer
|
- Check the status code as an integer
|
||||||
- Filter response headers
|
- Filter response headers
|
||||||
@@ -222,7 +206,7 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
- Set environment variables
|
- Set environment variables
|
||||||
- Write JavaScript code
|
- Write JavaScript code
|
||||||
|
|
||||||
🌱 **Environments** : Environment variables allow you to store and reuse values in your requests and scripts.
|
🌱 **Environments:** Environment variables allow you to store and reuse values in your requests and scripts.
|
||||||
|
|
||||||
- Unlimited environments and variables
|
- Unlimited environments and variables
|
||||||
- Initialize through the pre-request script
|
- Initialize through the pre-request script
|
||||||
@@ -241,22 +225,31 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
👨👩👧👦 **Teams β:** Helps you collaborate across your team to design, develop, and test APIs faster.
|
|
||||||
|
|
||||||
- Unlimited teams
|
|
||||||
- Unlimited shared collections
|
|
||||||
- Unlimited team members
|
|
||||||
- Role-based access control
|
|
||||||
- Cloud sync
|
|
||||||
- Multiple devices
|
|
||||||
|
|
||||||
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
🚚 **Bulk Edit:** Edit key-value pairs in bulk.
|
||||||
|
|
||||||
- Entries are separated by newline
|
- Entries are separated by newline
|
||||||
- Keys and values are separated by `:`
|
- Keys and values are separated by `:`
|
||||||
- Prepend `#` to any row you want to add but keep disabled
|
- Prepend `#` to any row you want to add but keep disabled
|
||||||
|
|
||||||
**For more features, please read our [documentation](https://docs.hoppscotch.io).**
|
🎛️ **Admin dashboard:** Manage your team and invite members.
|
||||||
|
|
||||||
|
- Insights
|
||||||
|
- Manage users
|
||||||
|
- Manage teams
|
||||||
|
|
||||||
|
📦 **Add-ons:** Official add-ons for hoppscotch.
|
||||||
|
|
||||||
|
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
|
||||||
|
- **[Proxy](https://github.com/hoppscotch/proxyscotch)** - A simple proxy server created for Hoppscotch.
|
||||||
|
- **[Browser Extensions](https://github.com/hoppscotch/hoppscotch-extension)** - Browser extensions that enhance your Hoppscotch experience.
|
||||||
|
|
||||||
|
[ **Firefox**](https://addons.mozilla.org/en-US/firefox/addon/hoppscotch) | [ **Chrome**](https://chrome.google.com/webstore/detail/hoppscotch-extension-for-c/amknoiejhlmhancpahfcfcfhllgkpbld)
|
||||||
|
|
||||||
|
> **Extensions fix `CORS` issues.**
|
||||||
|
|
||||||
|
_Add-ons are developed and maintained under **[Hoppscotch Organization](https://github.com/hoppscotch)**._
|
||||||
|
|
||||||
|
**For a complete list of features, please read our [documentation](https://docs.hoppscotch.io).**
|
||||||
|
|
||||||
## **Demo**
|
## **Demo**
|
||||||
|
|
||||||
@@ -268,18 +261,9 @@ _Add-ons are developed and maintained under **[Hoppscotch Organization](https://
|
|||||||
2. Click "Send" to simulate the request
|
2. Click "Send" to simulate the request
|
||||||
3. View the response
|
3. View the response
|
||||||
|
|
||||||
## **Built with**
|
|
||||||
|
|
||||||
- [HTML](https://developer.mozilla.org/en-US/docs/Web/HTML)
|
|
||||||
- [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS), [SCSS](https://sass-lang.com), [Windi CSS](https://windicss.org)
|
|
||||||
- [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript)
|
|
||||||
- [TypeScript](https://www.typescriptlang.org)
|
|
||||||
- [Vue](https://vuejs.org)
|
|
||||||
- [Vite](https://vitejs.dev)
|
|
||||||
|
|
||||||
## **Developing**
|
## **Developing**
|
||||||
|
|
||||||
Follow our [self-hosting guide](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
Follow our [self-hosting documentation](https://docs.hoppscotch.io/documentation/self-host/getting-started) to get started with the development environment.
|
||||||
|
|
||||||
## **Contributing**
|
## **Contributing**
|
||||||
|
|
||||||
@@ -297,7 +281,7 @@ See the [`CHANGELOG`](CHANGELOG.md) file for details.
|
|||||||
|
|
||||||
## **Authors**
|
## **Authors**
|
||||||
|
|
||||||
This project exists thanks to all the people who contribute — [contribute](CONTRIBUTING.md).
|
This project owes its existence to the collective efforts of all those who contribute — [contribute now](CONTRIBUTING.md).
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
<a href="https://github.com/hoppscotch/hoppscotch/graphs/contributors">
|
||||||
@@ -309,4 +293,6 @@ This project exists thanks to all the people who contribute — [contribute](CON
|
|||||||
|
|
||||||
## **License**
|
## **License**
|
||||||
|
|
||||||
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) - see the [`LICENSE`](LICENSE) file for details.
|
This project is licensed under the [MIT License](https://opensource.org/licenses/MIT) — see the [`LICENSE`](LICENSE) file for details.
|
||||||
|
|
||||||
|
[^EE]: Enterprise edition feature. [Learn more](https://docs.hoppscotch.io/documentation/self-host/getting-started).
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
This document outlines security procedures and general policies for the Hoppscotch project.
|
This document outlines security procedures and general policies for the Hoppscotch project.
|
||||||
|
|
||||||
1. [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
- [Security Policy](#security-policy)
|
||||||
3. [Incident response process](#incident-response-process)
|
- [Reporting a security vulnerability](#reporting-a-security-vulnerability)
|
||||||
|
- [Incident response process](#incident-response-process)
|
||||||
|
|
||||||
## Reporting a security vulnerability
|
## Reporting a security vulnerability
|
||||||
|
|
||||||
|
|||||||
@@ -9,26 +9,24 @@ Before you start working on a new language, please look through the [open pull r
|
|||||||
if there is no existing translation, you can create a new one by following these steps:
|
if there is no existing translation, you can create a new one by following these steps:
|
||||||
|
|
||||||
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
1. **[Fork the repository](https://github.com/hoppscotch/hoppscotch/fork).**
|
||||||
2. **Checkout the `i18n` branch for latest translations.**
|
2. **Checkout the `main` branch for latest translations.**
|
||||||
3. **Create a new branch for your translation with base branch `i18n`.**
|
3. **Create a new branch for your translation with base branch `main`.**
|
||||||
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
4. **Create target language file in the [`/packages/hoppscotch-common/locales`](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-common/locales) directory.**
|
||||||
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
|
5. **Copy the contents of the source file [`/packages/hoppscotch-common/locales/en.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/locales/en.json) to the target language file.**
|
||||||
6. **Translate the strings in the target language file.**
|
6. **Translate the strings in the target language file.**
|
||||||
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
|
7. **Add your language entry to [`/packages/hoppscotch-common/languages.json`](https://github.com/hoppscotch/hoppscotch/blob/main/packages/hoppscotch-common/languages.json).**
|
||||||
8. **Save & commit changes.**
|
8. **Save and commit changes.**
|
||||||
9. **Send a pull request.**
|
9. **Send a pull request.**
|
||||||
|
|
||||||
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
|
_You may send a pull request before all steps above are complete: e.g., you may want to ask for help with translations, or getting tests to pass. However, your pull request will not be merged until all steps above are complete._
|
||||||
|
|
||||||
`i18n` branch will be merged into `main` branch once every week.
|
|
||||||
|
|
||||||
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
|
Completing an initial translation of the whole site is a fairly large task. One way to break that task up is to work with other translators through pull requests on your fork. You can also [add collaborators to your fork](https://help.github.com/en/github/setting-up-and-managing-your-github-user-account/inviting-collaborators-to-a-personal-repository) if you'd like to invite other translators to commit directly to your fork and share responsibility for merging pull requests.
|
||||||
|
|
||||||
## Updating a translation
|
## Updating a translation
|
||||||
|
|
||||||
### Corrections
|
### Corrections
|
||||||
|
|
||||||
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, open an issue.
|
If you notice spelling or grammar errors, typos, or opportunities for better phrasing, open a pull request with your suggested fix. If you see a problem that you aren't sure of or don't have time to fix, [open an issue](https://github.com/hoppscotch/hoppscotch/issues/new/choose).
|
||||||
|
|
||||||
### Broken links
|
### Broken links
|
||||||
|
|
||||||
|
|||||||
11
aio.Caddyfile
Normal file
11
aio.Caddyfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
:3000 {
|
||||||
|
try_files {path} /
|
||||||
|
root * /site/selfhost-web
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
:3100 {
|
||||||
|
try_files {path} /
|
||||||
|
root * /site/sh-admin
|
||||||
|
file_server
|
||||||
|
}
|
||||||
72
aio_run.mjs
Normal file
72
aio_run.mjs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/local/bin/node
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { execSync, spawn } from "child_process"
|
||||||
|
import fs from "fs"
|
||||||
|
import process from "process"
|
||||||
|
|
||||||
|
function runChildProcessWithPrefix(command, args, prefix) {
|
||||||
|
const childProcess = spawn(command, args);
|
||||||
|
|
||||||
|
childProcess.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString().trim().split('\n');
|
||||||
|
output.forEach((line) => {
|
||||||
|
console.log(`${prefix} | ${line}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.stderr.on('data', (data) => {
|
||||||
|
const error = data.toString().trim().split('\n');
|
||||||
|
error.forEach((line) => {
|
||||||
|
console.error(`${prefix} | ${line}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('close', (code) => {
|
||||||
|
console.log(`${prefix} Child process exited with code ${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('error', (stuff) => {
|
||||||
|
console.log("error")
|
||||||
|
console.log(stuff)
|
||||||
|
})
|
||||||
|
|
||||||
|
return childProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
const envFileContent = Object.entries(process.env)
|
||||||
|
.filter(([env]) => env.startsWith("VITE_"))
|
||||||
|
.map(([env, val]) => `${env}=${
|
||||||
|
(val.startsWith("\"") && val.endsWith("\""))
|
||||||
|
? val
|
||||||
|
: `"${val}"`
|
||||||
|
}`)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
fs.writeFileSync("build.env", envFileContent)
|
||||||
|
|
||||||
|
execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
|
||||||
|
|
||||||
|
fs.rmSync("build.env")
|
||||||
|
|
||||||
|
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
||||||
|
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
|
||||||
|
|
||||||
|
caddyProcess.on("exit", (code) => {
|
||||||
|
console.log(`Exiting process because Caddy Server exited with code ${code}`)
|
||||||
|
process.exit(code)
|
||||||
|
})
|
||||||
|
|
||||||
|
backendProcess.on("exit", (code) => {
|
||||||
|
console.log(`Exiting process because Backend Server exited with code ${code}`)
|
||||||
|
process.exit(code)
|
||||||
|
})
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log("SIGINT received, exiting...")
|
||||||
|
|
||||||
|
caddyProcess.kill("SIGINT")
|
||||||
|
backendProcess.kill("SIGINT")
|
||||||
|
|
||||||
|
process.exit(0)
|
||||||
|
})
|
||||||
@@ -8,23 +8,25 @@ services:
|
|||||||
hoppscotch-backend:
|
hoppscotch-backend:
|
||||||
container_name: hoppscotch-backend
|
container_name: hoppscotch-backend
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-backend/Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
target: prod
|
target: backend
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3000
|
- PORT=3170
|
||||||
volumes:
|
volumes:
|
||||||
- ./packages/hoppscotch-backend/:/usr/src/app
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
|
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-db
|
hoppscotch-db:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "3170:3000"
|
- "3170:3170"
|
||||||
|
|
||||||
# The main hoppscotch app. This will be hosted at port 3000
|
# The main hoppscotch app. This will be hosted at port 3000
|
||||||
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
||||||
@@ -32,8 +34,9 @@ services:
|
|||||||
hoppscotch-app:
|
hoppscotch-app:
|
||||||
container_name: hoppscotch-app
|
container_name: hoppscotch-app
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
target: app
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -47,8 +50,9 @@ services:
|
|||||||
hoppscotch-sh-admin:
|
hoppscotch-sh-admin:
|
||||||
container_name: hoppscotch-sh-admin
|
container_name: hoppscotch-sh-admin
|
||||||
build:
|
build:
|
||||||
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
dockerfile: prod.Dockerfile
|
||||||
context: .
|
context: .
|
||||||
|
target: sh_admin
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -56,16 +60,91 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3100:8080"
|
- "3100:8080"
|
||||||
|
|
||||||
|
# The service that spins up all 3 services at once in one container
|
||||||
|
hoppscotch-aio:
|
||||||
|
container_name: hoppscotch-aio
|
||||||
|
build:
|
||||||
|
dockerfile: prod.Dockerfile
|
||||||
|
context: .
|
||||||
|
target: aio
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
depends_on:
|
||||||
|
hoppscotch-db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
- "3100:3100"
|
||||||
|
- "3170:3170"
|
||||||
|
|
||||||
# The preset DB service, you can delete/comment the below lines if
|
# The preset DB service, you can delete/comment the below lines if
|
||||||
# you are using an external postgres instance
|
# you are using an external postgres instance
|
||||||
# This will be exposed at port 5432
|
# This will be exposed at port 5432
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
image: postgres
|
image: postgres:15
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
user: postgres
|
||||||
environment:
|
environment:
|
||||||
|
# The default user defined by the docker image
|
||||||
|
POSTGRES_USER: postgres
|
||||||
# NOTE: Please UPDATE THIS PASSWORD!
|
# NOTE: Please UPDATE THIS PASSWORD!
|
||||||
POSTGRES_PASSWORD: testpass
|
POSTGRES_PASSWORD: testpass
|
||||||
POSTGRES_DB: hoppscotch
|
POSTGRES_DB: hoppscotch
|
||||||
|
healthcheck:
|
||||||
|
test:
|
||||||
|
[
|
||||||
|
"CMD-SHELL",
|
||||||
|
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
|
||||||
|
]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
# All the services listed below are deprececated
|
||||||
|
hoppscotch-old-backend:
|
||||||
|
container_name: hoppscotch-old-backend
|
||||||
|
build:
|
||||||
|
dockerfile: packages/hoppscotch-backend/Dockerfile
|
||||||
|
context: .
|
||||||
|
target: prod
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||||
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
|
- PORT=3000
|
||||||
|
volumes:
|
||||||
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
|
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
|
- /usr/src/app/node_modules/
|
||||||
|
depends_on:
|
||||||
|
hoppscotch-db:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "3170:3000"
|
||||||
|
|
||||||
|
hoppscotch-old-app:
|
||||||
|
container_name: hoppscotch-old-app
|
||||||
|
build:
|
||||||
|
dockerfile: packages/hoppscotch-selfhost-web/Dockerfile
|
||||||
|
context: .
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
depends_on:
|
||||||
|
- hoppscotch-old-backend
|
||||||
|
ports:
|
||||||
|
- "3000:8080"
|
||||||
|
|
||||||
|
hoppscotch-old-sh-admin:
|
||||||
|
container_name: hoppscotch-old-sh-admin
|
||||||
|
build:
|
||||||
|
dockerfile: packages/hoppscotch-sh-admin/Dockerfile
|
||||||
|
context: .
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
depends_on:
|
||||||
|
- hoppscotch-old-backend
|
||||||
|
ports:
|
||||||
|
- "3100:8080"
|
||||||
|
|||||||
14
healthcheck.sh
Normal file
14
healthcheck.sh
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
curlCheck() {
|
||||||
|
if ! curl -s --head "$1" | head -n 1 | grep -q "HTTP/1.[01] [23].."; then
|
||||||
|
echo "URL request failed!"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "URL request succeeded!"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
curlCheck "http://localhost:3000"
|
||||||
|
curlCheck "http://localhost:3100"
|
||||||
|
curlCheck "http://localhost:3170/ping"
|
||||||
@@ -32,5 +32,14 @@
|
|||||||
"@types/node": "^17.0.24",
|
"@types/node": "^17.0.24",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"http-server": "^14.1.1"
|
"http-server": "^14.1.1"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"packageExtensions": {
|
||||||
|
"httpsnippet@^3.0.1": {
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "6.12.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.2.0",
|
"@codemirror/language": "^6.9.0",
|
||||||
"@lezer/highlight": "^1.0.0",
|
"@lezer/highlight": "^1.1.6",
|
||||||
"@lezer/lr": "^1.2.0"
|
"@lezer/lr": "^1.3.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.1.0",
|
"@lezer/generator": "^1.5.0",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"rollup": "^2.70.2",
|
"rollup": "^2.70.2",
|
||||||
"rollup-plugin-dts": "^4.2.1",
|
"rollup-plugin-dts": "^4.2.1",
|
||||||
|
|||||||
24
packages/dioc/.gitignore
vendored
Normal file
24
packages/dioc/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
141
packages/dioc/README.md
Normal file
141
packages/dioc/README.md
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
# dioc
|
||||||
|
|
||||||
|
A small and lightweight dependency injection / inversion of control system.
|
||||||
|
|
||||||
|
### About
|
||||||
|
|
||||||
|
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
|
||||||
|
|
||||||
|
### Demo
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Service, Container } from "dioc"
|
||||||
|
|
||||||
|
// Here is a simple service, which you can define by extending the Service class
|
||||||
|
// and providing an ID static field (of type string)
|
||||||
|
export class PersistenceService extends Service {
|
||||||
|
// This should be unique for each container
|
||||||
|
public static ID = "PERSISTENCE_SERVICE"
|
||||||
|
|
||||||
|
public read(key: string): string | undefined {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
public write(key: string, value: string) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TodoServiceEvent =
|
||||||
|
| { type: "TODO_CREATED"; index: number }
|
||||||
|
| { type: "TODO_DELETED"; index: number }
|
||||||
|
|
||||||
|
// Services have a built in event system
|
||||||
|
// Define the generic argument to say what are the possible emitted values
|
||||||
|
export class TodoService extends Service<TodoServiceEvent> {
|
||||||
|
public static ID = "TODO_SERVICE"
|
||||||
|
|
||||||
|
// Inject persistence service into this service
|
||||||
|
private readonly persistence = this.bind(PersistenceService)
|
||||||
|
|
||||||
|
public todos = []
|
||||||
|
|
||||||
|
// Service constructors cannot have arguments
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTodo(text: string) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// You can access services via the bound fields
|
||||||
|
this.persistence.write("todos", JSON.stringify(this.todos))
|
||||||
|
|
||||||
|
// This is how you emit an event
|
||||||
|
this.emit({
|
||||||
|
type: "TODO_CREATED",
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeTodo(index: number) {
|
||||||
|
// ...
|
||||||
|
|
||||||
|
this.emit({
|
||||||
|
type: "TODO_DELETED",
|
||||||
|
index,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Services need a container to run in
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
// You can initialize and get services using Container#bind
|
||||||
|
// It will automatically initialize the service (and its dependencies)
|
||||||
|
const todoService = container.bind(TodoService) // Returns an instance of TodoService
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo (Unit Test)
|
||||||
|
|
||||||
|
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { TestContainer } from "dioc/testing"
|
||||||
|
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
|
||||||
|
import { describe, it, expect, vi } from "vitest"
|
||||||
|
|
||||||
|
describe("TodoService", () => {
|
||||||
|
it("addTodo writes to persistence", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const writeFn = vi.fn()
|
||||||
|
|
||||||
|
// The first parameter is the service to mock and the second parameter
|
||||||
|
// is the mocked service fields and functions
|
||||||
|
container.bindMock(PersistenceService, {
|
||||||
|
read: () => undefined, // Not really important for this test
|
||||||
|
write: writeFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// the peristence service bind in TodoService will now use the
|
||||||
|
// above defined mocked implementation
|
||||||
|
const todoService = container.bind(TodoService)
|
||||||
|
|
||||||
|
todoService.addTodo("sup")
|
||||||
|
|
||||||
|
expect(writeFn).toHaveBeenCalledOnce()
|
||||||
|
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Demo (Vue)
|
||||||
|
|
||||||
|
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
|
||||||
|
|
||||||
|
In the app entry point:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createApp } from "vue"
|
||||||
|
import { diocPlugin } from "dioc/vue"
|
||||||
|
|
||||||
|
const app = createApp()
|
||||||
|
|
||||||
|
app.use(diocPlugin, {
|
||||||
|
container: new Container(), // You can pass in the container you want to provide to the components here
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
In your Vue components:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
import { TodoService } from "./demo.ts" // The above demo
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
|
||||||
|
const todoService = useService(TodoService) // Returns an instance of the TodoService class
|
||||||
|
</script>
|
||||||
|
```
|
||||||
2
packages/dioc/index.d.ts
vendored
Normal file
2
packages/dioc/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/main.d.ts"
|
||||||
|
export * from "./dist/main.d.ts"
|
||||||
147
packages/dioc/lib/container.ts
Normal file
147
packages/dioc/lib/container.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Service } from "./service"
|
||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the current container instance in the current operating context.
|
||||||
|
*
|
||||||
|
* NOTE: This should not be used outside of dioc library code
|
||||||
|
*/
|
||||||
|
export let currentContainer: Container | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The events emitted by the container
|
||||||
|
*
|
||||||
|
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
|
||||||
|
* `SERVICE_INIT` - emitted when a service is initialized
|
||||||
|
*/
|
||||||
|
export type ContainerEvent =
|
||||||
|
| {
|
||||||
|
type: 'SERVICE_BIND';
|
||||||
|
|
||||||
|
/** The Service ID of the service being bounded (the dependency) */
|
||||||
|
boundeeID: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Service ID of the bounder that is binding the boundee (the dependent)
|
||||||
|
*
|
||||||
|
* NOTE: This will be undefined if the service is bound directly to the container
|
||||||
|
*/
|
||||||
|
bounderID: string | undefined
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: 'SERVICE_INIT';
|
||||||
|
|
||||||
|
/** The Service ID of the service being initialized */
|
||||||
|
serviceID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
|
||||||
|
*/
|
||||||
|
export class Container {
|
||||||
|
/** Used during the `bind` operation to detect circular dependencies */
|
||||||
|
private bindStack: string[] = []
|
||||||
|
|
||||||
|
/** The map of bound services to their IDs */
|
||||||
|
protected boundMap = new Map<string, Service<unknown>>()
|
||||||
|
|
||||||
|
/** The RxJS observable representing the event stream */
|
||||||
|
protected event$ = new Subject<ContainerEvent>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a container has the given service bound
|
||||||
|
* @param service The service to check for
|
||||||
|
*/
|
||||||
|
public hasBound<
|
||||||
|
T extends typeof Service<any> & { ID: string }
|
||||||
|
>(service: T): boolean {
|
||||||
|
return this.boundMap.has(service.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the service bound to the container with the given ID or if not found, undefined.
|
||||||
|
*
|
||||||
|
* NOTE: This is an advanced method and should not be used as much as possible.
|
||||||
|
*
|
||||||
|
* @param serviceID The ID of the service to get
|
||||||
|
*/
|
||||||
|
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
|
||||||
|
return this.boundMap.get(serviceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a service to the container. This is equivalent to marking a service as a dependency.
|
||||||
|
* @param service The class reference of a service to bind
|
||||||
|
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
|
||||||
|
*/
|
||||||
|
public bind<T extends typeof Service<any> & { ID: string }>(
|
||||||
|
service: T,
|
||||||
|
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
|
||||||
|
): InstanceType<T> {
|
||||||
|
// We need to store the current container in a variable so that we can restore it after the bind operation
|
||||||
|
const oldCurrentContainer = currentContainer;
|
||||||
|
currentContainer = this;
|
||||||
|
|
||||||
|
// If the service is already bound, return the existing instance
|
||||||
|
if (this.hasBound(service)) {
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_BIND',
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
|
||||||
|
})
|
||||||
|
|
||||||
|
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect circular dependency and throw error
|
||||||
|
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
|
||||||
|
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
|
||||||
|
|
||||||
|
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the service ID onto the bind stack to detect circular dependencies
|
||||||
|
this.bindStack.push(service.ID)
|
||||||
|
|
||||||
|
// Initialize the service and emit events
|
||||||
|
|
||||||
|
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
|
||||||
|
const instance: Service<any> = new (service as any)()
|
||||||
|
|
||||||
|
this.boundMap.set(service.ID, instance)
|
||||||
|
|
||||||
|
this.bindStack.pop()
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_INIT',
|
||||||
|
serviceID: service.ID,
|
||||||
|
})
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: 'SERVICE_BIND',
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: bounder?.ID
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Restore the current container
|
||||||
|
currentContainer = oldCurrentContainer;
|
||||||
|
|
||||||
|
// We expect the return type to match the service definition
|
||||||
|
return instance as InstanceType<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator of the currently bound service IDs and their instances
|
||||||
|
*/
|
||||||
|
public getBoundServices(): IterableIterator<[string, Service<any>]> {
|
||||||
|
return this.boundMap.entries()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the public container event stream
|
||||||
|
*/
|
||||||
|
public getEventStream(): Observable<ContainerEvent> {
|
||||||
|
return this.event$.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/dioc/lib/main.ts
Normal file
2
packages/dioc/lib/main.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./container"
|
||||||
|
export * from "./service"
|
||||||
65
packages/dioc/lib/service.ts
Normal file
65
packages/dioc/lib/service.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { Observable, Subject } from 'rxjs'
|
||||||
|
import { Container, currentContainer } from './container'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Dioc service that can bound to a container and can bind dependency services.
|
||||||
|
*
|
||||||
|
* NOTE: Services cannot have a constructor that takes arguments.
|
||||||
|
*
|
||||||
|
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
|
||||||
|
*/
|
||||||
|
export abstract class Service<EventDef = {}> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal event stream of the service
|
||||||
|
*/
|
||||||
|
private event$ = new Subject<EventDef>()
|
||||||
|
|
||||||
|
/** The container the service is bound to */
|
||||||
|
#container: Container
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (!currentContainer) {
|
||||||
|
throw new Error(
|
||||||
|
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#container = currentContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a dependency service into this service.
|
||||||
|
* @param service The class reference of the service to bind
|
||||||
|
*/
|
||||||
|
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
|
||||||
|
if (!currentContainer) {
|
||||||
|
throw new Error('No currentContainer defined.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the container the service is bound to
|
||||||
|
*/
|
||||||
|
protected getContainer(): Container {
|
||||||
|
return this.#container
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits an event on the service's event stream
|
||||||
|
* @param event The event to emit
|
||||||
|
*/
|
||||||
|
protected emit(event: EventDef) {
|
||||||
|
this.event$.next(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the event stream of the service
|
||||||
|
*/
|
||||||
|
public getEventStream(): Observable<EventDef> {
|
||||||
|
|
||||||
|
return this.event$.asObservable()
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/dioc/lib/testing.ts
Normal file
33
packages/dioc/lib/testing.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Container, Service } from "./main";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A container that can be used for writing tests, contains additional methods
|
||||||
|
* for binding suitable for writing tests. (see `bindMock`).
|
||||||
|
*/
|
||||||
|
export class TestContainer extends Container {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a mock service to the container.
|
||||||
|
*
|
||||||
|
* @param service
|
||||||
|
* @param mock
|
||||||
|
*/
|
||||||
|
public bindMock<
|
||||||
|
T extends typeof Service<any> & { ID: string },
|
||||||
|
U extends Partial<InstanceType<T>>
|
||||||
|
>(service: T, mock: U): U {
|
||||||
|
if (this.boundMap.has(service.ID)) {
|
||||||
|
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.boundMap.set(service.ID, mock as any)
|
||||||
|
|
||||||
|
this.event$.next({
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: service.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/dioc/lib/vue.ts
Normal file
34
packages/dioc/lib/vue.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Plugin, inject } from "vue"
|
||||||
|
import { Container } from "./container"
|
||||||
|
import { Service } from "./service"
|
||||||
|
|
||||||
|
const VUE_CONTAINER_KEY = Symbol()
|
||||||
|
|
||||||
|
// TODO: Some Vue version issue with plugin generics is breaking type checking
|
||||||
|
/**
|
||||||
|
* The Vue Dioc Plugin, this allows the composables to work and access the container
|
||||||
|
*
|
||||||
|
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
|
||||||
|
*/
|
||||||
|
export const diocPlugin: Plugin = {
|
||||||
|
install(app, { container }) {
|
||||||
|
app.provide(VUE_CONTAINER_KEY, container)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A composable that binds a service to a Vue Component
|
||||||
|
*
|
||||||
|
* @param service The class reference of the service to bind
|
||||||
|
*/
|
||||||
|
export function useService<
|
||||||
|
T extends typeof Service<any> & { ID: string }
|
||||||
|
>(service: T): InstanceType<T> {
|
||||||
|
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
throw new Error("Container not found, did you forget to install the dioc plugin?")
|
||||||
|
}
|
||||||
|
|
||||||
|
return container.bind(service)
|
||||||
|
}
|
||||||
54
packages/dioc/package.json
Normal file
54
packages/dioc/package.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"name": "dioc",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"index.d.ts"
|
||||||
|
],
|
||||||
|
"main": "./dist/counter.umd.cjs",
|
||||||
|
"module": "./dist/counter.js",
|
||||||
|
"types": "./index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/main.d.ts",
|
||||||
|
"require": "./dist/index.cjs",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./vue": {
|
||||||
|
"types": "./dist/vue.d.ts",
|
||||||
|
"require": "./dist/vue.cjs",
|
||||||
|
"import": "./dist/vue.js"
|
||||||
|
},
|
||||||
|
"./testing": {
|
||||||
|
"types": "./dist/testing.d.ts",
|
||||||
|
"require": "./dist/testing.cjs",
|
||||||
|
"import": "./dist/testing.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build && tsc --emitDeclarationOnly",
|
||||||
|
"prepare": "pnpm run build",
|
||||||
|
"test": "vitest run",
|
||||||
|
"do-test": "pnpm run test",
|
||||||
|
"test:watch": "vitest"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"vite": "^4.0.4",
|
||||||
|
"vitest": "^0.29.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.25"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
262
packages/dioc/test/container.spec.ts
Normal file
262
packages/dioc/test/container.spec.ts
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import { it, expect, describe, vi } from "vitest"
|
||||||
|
import { Service } from "../lib/service"
|
||||||
|
import { Container, currentContainer, ContainerEvent } from "../lib/container"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// Marked public to allow for testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Container", () => {
|
||||||
|
describe("getBoundServiceWithID", () => {
|
||||||
|
it("returns the service instance if it is bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const service = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns undefined if the service is not bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bind", () => {
|
||||||
|
it("correctly binds the service to it", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const service = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||||
|
expect(service.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("after bind, the current container is set back to its previous value", () => {
|
||||||
|
const originalValue = currentContainer
|
||||||
|
|
||||||
|
const container = new Container()
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(currentContainer).toBe(originalValue)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("dependent services are registered in the same container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||||
|
expect(serviceB.serviceA.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
const serviceA2 = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceA).toBe(serviceA2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("binding an initialized service as a dependency returns the same instance", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_INIT") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const instance = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceA.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_BIND") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
// We only care about the bind event of TestServiceA
|
||||||
|
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("hasBound", () => {
|
||||||
|
it("returns true if the given service is bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
container.bind(TestServiceA)
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns false if the given service is not bound to the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns true when the service is bound because it is a dependency of another service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getEventStream", () => {
|
||||||
|
it("returns an observable which emits events correctly when services are initialized", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_INIT") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceA.ID,
|
||||||
|
})
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||||
|
type: "SERVICE_INIT",
|
||||||
|
serviceID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an observable which emits events correctly when services are bound", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<
|
||||||
|
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||||
|
void
|
||||||
|
>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
if (ev.type === "SERVICE_BIND") {
|
||||||
|
serviceFunc(ev)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: TestServiceB.ID,
|
||||||
|
})
|
||||||
|
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceB.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getBoundServices", () => {
|
||||||
|
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const instanceB = container.bind(TestServiceB)
|
||||||
|
const instanceA = instanceB.serviceA
|
||||||
|
|
||||||
|
expect(Array.from(container.getBoundServices())).toEqual([
|
||||||
|
[TestServiceA.ID, instanceA],
|
||||||
|
[TestServiceB.ID, instanceB],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an empty iterator if no services are bound", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
expect(Array.from(container.getBoundServices())).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
66
packages/dioc/test/service.spec.ts
Normal file
66
packages/dioc/test/service.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { Service, Container } from "../lib/main"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service<"test"> {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// Marked public to allow for testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
|
||||||
|
public emitTestEvent() {
|
||||||
|
this.emit("test")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Service", () => {
|
||||||
|
describe("constructor", () => {
|
||||||
|
it("throws an error if the service is initialized without a container", () => {
|
||||||
|
expect(() => new TestServiceA()).toThrowError(
|
||||||
|
"Tried to initialize service with no container (ID: TestServiceA)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("bind", () => {
|
||||||
|
it("correctly binds the dependency service using the container", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
expect(serviceB.serviceA).toBe(serviceA)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getContainer", () => {
|
||||||
|
it("returns the container the service is bound to", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceA = container.bind(TestServiceA)
|
||||||
|
|
||||||
|
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
|
||||||
|
expect(serviceA.getContainer()).toBe(container)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("getEventStream", () => {
|
||||||
|
it("returns the valid event stream of the service", () => {
|
||||||
|
const container = new Container()
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn()
|
||||||
|
|
||||||
|
serviceB.getEventStream().subscribe(serviceFunc)
|
||||||
|
|
||||||
|
serviceB.emitTestEvent()
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith("test")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
92
packages/dioc/test/test-container.spec.ts
Normal file
92
packages/dioc/test/test-container.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest"
|
||||||
|
import { TestContainer } from "../lib/testing"
|
||||||
|
import { Service } from "../lib/service"
|
||||||
|
import { ContainerEvent } from "../lib/container"
|
||||||
|
|
||||||
|
class TestServiceA extends Service {
|
||||||
|
public static ID = "TestServiceA"
|
||||||
|
|
||||||
|
public test() {
|
||||||
|
return "real"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestServiceB extends Service {
|
||||||
|
public static ID = "TestServiceB"
|
||||||
|
|
||||||
|
// declared public to help with testing
|
||||||
|
public readonly serviceA = this.bind(TestServiceA)
|
||||||
|
|
||||||
|
public test() {
|
||||||
|
return this.serviceA.test()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("TestContainer", () => {
|
||||||
|
describe("bindMock", () => {
|
||||||
|
it("returns the fake service defined", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeService = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = container.bindMock(TestServiceA, fakeService)
|
||||||
|
|
||||||
|
expect(result).toBe(fakeService)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("new services bound to the container get the mock service", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
const serviceB = container.bind(TestServiceB)
|
||||||
|
|
||||||
|
expect(serviceB.serviceA).toBe(fakeServiceA)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("container emits SERVICE_BIND event", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceFunc = vi.fn<[ContainerEvent, void]>()
|
||||||
|
|
||||||
|
container.getEventStream().subscribe((ev) => {
|
||||||
|
serviceFunc(ev)
|
||||||
|
})
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||||
|
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||||
|
type: "SERVICE_BIND",
|
||||||
|
boundeeID: TestServiceA.ID,
|
||||||
|
bounderID: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if service already bound", () => {
|
||||||
|
const container = new TestContainer()
|
||||||
|
|
||||||
|
const fakeServiceA = {
|
||||||
|
test: () => "fake",
|
||||||
|
}
|
||||||
|
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
container.bindMock(TestServiceA, fakeServiceA)
|
||||||
|
}).toThrowError(
|
||||||
|
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
2
packages/dioc/testing.d.ts
vendored
Normal file
2
packages/dioc/testing.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/testing.d.ts"
|
||||||
|
export * from "./dist/testing.d.ts"
|
||||||
21
packages/dioc/tsconfig.json
Normal file
21
packages/dioc/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ESNext", "DOM"],
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["lib"]
|
||||||
|
}
|
||||||
16
packages/dioc/vite.config.ts
Normal file
16
packages/dioc/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: {
|
||||||
|
index: './lib/main.ts',
|
||||||
|
vue: './lib/vue.ts',
|
||||||
|
testing: './lib/testing.ts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['vue'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
7
packages/dioc/vitest.config.ts
Normal file
7
packages/dioc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vitest/config"
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
test: {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
2
packages/dioc/vue.d.ts
vendored
Normal file
2
packages/dioc/vue.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default } from "./dist/vue.d.ts"
|
||||||
|
export * from "./dist/vue.d.ts"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.5",
|
"version": "2023.8.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/passport": "^9.0.0",
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/platform-express": "^9.2.1",
|
||||||
"@nestjs/throttler": "^4.0.0",
|
"@nestjs/throttler": "^4.0.0",
|
||||||
"@prisma/client": "^4.7.1",
|
"@prisma/client": "^4.16.2",
|
||||||
"apollo-server-express": "^3.11.1",
|
"apollo-server-express": "^3.11.1",
|
||||||
"apollo-server-plugin-base": "^3.7.1",
|
"apollo-server-plugin-base": "^3.7.1",
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-microsoft": "^1.0.0",
|
"passport-microsoft": "^1.0.0",
|
||||||
"prisma": "^4.7.1",
|
"prisma": "^4.16.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^7.6.0"
|
"rxjs": "^7.6.0"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ datasource db {
|
|||||||
|
|
||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
binaryTargets = ["native", "debian-openssl-1.1.x"]
|
binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
|
|||||||
@@ -411,6 +411,23 @@ export class AdminResolver {
|
|||||||
return deletedTeam.right;
|
return deletedTeam.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Revoke a team Invite by Invite ID',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async revokeTeamInviteByAdmin(
|
||||||
|
@Args({
|
||||||
|
name: 'inviteID',
|
||||||
|
description: 'Team Invite ID',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
inviteID: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const invite = await this.adminService.revokeTeamInviteByID(inviteID);
|
||||||
|
if (E.isLeft(invite)) throwErr(invite.left);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|
||||||
@Subscription(() => InvitedUser, {
|
@Subscription(() => InvitedUser, {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
ONLY_ONE_ADMIN_ACCOUNT,
|
ONLY_ONE_ADMIN_ACCOUNT,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
USER_IS_ADMIN,
|
USER_IS_ADMIN,
|
||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
@@ -181,7 +182,7 @@ export class AdminService {
|
|||||||
* @returns an array team invitations
|
* @returns an array team invitations
|
||||||
*/
|
*/
|
||||||
async pendingInvitationCountInTeam(teamID: string) {
|
async pendingInvitationCountInTeam(teamID: string) {
|
||||||
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
const invitations = await this.teamInvitationService.getTeamInvitations(
|
||||||
teamID,
|
teamID,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -236,11 +237,11 @@ export class AdminService {
|
|||||||
const user = await this.userService.findUserByEmail(userEmail);
|
const user = await this.userService.findUserByEmail(userEmail);
|
||||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||||
|
|
||||||
const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
|
const teamMember = await this.teamService.getTeamMemberTE(
|
||||||
teamID,
|
teamID,
|
||||||
user.value.uid,
|
user.value.uid,
|
||||||
)();
|
)();
|
||||||
if (E.left(isUserAlreadyMember)) {
|
if (E.isLeft(teamMember)) {
|
||||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||||
teamID,
|
teamID,
|
||||||
userEmail,
|
userEmail,
|
||||||
@@ -248,6 +249,18 @@ export class AdminService {
|
|||||||
);
|
);
|
||||||
if (E.isLeft(addedUser)) return E.left(addedUser.left);
|
if (E.isLeft(addedUser)) return E.left(addedUser.left);
|
||||||
|
|
||||||
|
const userInvitation =
|
||||||
|
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
|
||||||
|
userEmail,
|
||||||
|
teamID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isRight(userInvitation)) {
|
||||||
|
await this.teamInvitationService.revokeInvitation(
|
||||||
|
userInvitation.right.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return E.right(addedUser.right);
|
return E.right(addedUser.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,4 +417,19 @@ export class AdminService {
|
|||||||
if (E.isLeft(team)) return E.left(team.left);
|
if (E.isLeft(team)) return E.left(team.left);
|
||||||
return E.right(team.right);
|
return E.right(team.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Revoke a team invite by ID
|
||||||
|
* @param inviteID Team Invite ID
|
||||||
|
* @returns an Either of boolean or error
|
||||||
|
*/
|
||||||
|
async revokeTeamInviteByID(inviteID: string) {
|
||||||
|
const teamInvite = await this.teamInvitationService.revokeInvitation(
|
||||||
|
inviteID,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamInvite)) return E.left(teamInvite.left);
|
||||||
|
|
||||||
|
return E.right(teamInvite.right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
9
packages/hoppscotch-backend/src/app.controller.ts
Normal file
9
packages/hoppscotch-backend/src/app.controller.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Controller('ping')
|
||||||
|
export class AppController {
|
||||||
|
@Get()
|
||||||
|
ping(): string {
|
||||||
|
return 'Success';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import { UserCollectionModule } from './user-collection/user-collection.module';
|
|||||||
import { ShortcodeModule } from './shortcode/shortcode.module';
|
import { ShortcodeModule } from './shortcode/shortcode.module';
|
||||||
import { COOKIES_NOT_FOUND } from './errors';
|
import { COOKIES_NOT_FOUND } from './errors';
|
||||||
import { ThrottlerModule } from '@nestjs/throttler';
|
import { ThrottlerModule } from '@nestjs/throttler';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -81,5 +82,6 @@ import { ThrottlerModule } from '@nestjs/throttler';
|
|||||||
ShortcodeModule,
|
ShortcodeModule,
|
||||||
],
|
],
|
||||||
providers: [GQLComplexityPlugin],
|
providers: [GQLComplexityPlugin],
|
||||||
|
controllers: [AppController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
|
InternalServerErrorException,
|
||||||
Post,
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Req,
|
|
||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
@@ -19,12 +19,18 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
|
|||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
|
||||||
import { authCookieHandler, throwHTTPErr } from './helper';
|
import {
|
||||||
|
AuthProvider,
|
||||||
|
authCookieHandler,
|
||||||
|
authProviderCheck,
|
||||||
|
throwHTTPErr,
|
||||||
|
} from './helper';
|
||||||
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
import { GoogleSSOGuard } from './guards/google-sso.guard';
|
||||||
import { GithubSSOGuard } from './guards/github-sso.guard';
|
import { GithubSSOGuard } from './guards/github-sso.guard';
|
||||||
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
|
||||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
@@ -39,6 +45,9 @@ export class AuthController {
|
|||||||
@Body() authData: SignInMagicDto,
|
@Body() authData: SignInMagicDto,
|
||||||
@Query('origin') origin: string,
|
@Query('origin') origin: string,
|
||||||
) {
|
) {
|
||||||
|
if (!authProviderCheck(AuthProvider.EMAIL))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
const deviceIdToken = await this.authService.signInMagicLink(
|
const deviceIdToken = await this.authService.signInMagicLink(
|
||||||
authData.email,
|
authData.email,
|
||||||
origin,
|
origin,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { RTJwtStrategy } from './strategies/rt-jwt.strategy';
|
|||||||
import { GoogleStrategy } from './strategies/google.strategy';
|
import { GoogleStrategy } from './strategies/google.strategy';
|
||||||
import { GithubStrategy } from './strategies/github.strategy';
|
import { GithubStrategy } from './strategies/github.strategy';
|
||||||
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
||||||
|
import { AuthProvider, authProviderCheck } from './helper';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -26,9 +27,9 @@ import { MicrosoftStrategy } from './strategies/microsoft.strategy';
|
|||||||
AuthService,
|
AuthService,
|
||||||
JwtStrategy,
|
JwtStrategy,
|
||||||
RTJwtStrategy,
|
RTJwtStrategy,
|
||||||
GoogleStrategy,
|
...(authProviderCheck(AuthProvider.GOOGLE) ? [GoogleStrategy] : []),
|
||||||
GithubStrategy,
|
...(authProviderCheck(AuthProvider.GITHUB) ? [GithubStrategy] : []),
|
||||||
MicrosoftStrategy,
|
...(authProviderCheck(AuthProvider.MICROSOFT) ? [MicrosoftStrategy] : []),
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ export class AuthService {
|
|||||||
url = process.env.VITE_BASE_URL;
|
url = process.env.VITE_BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendAuthEmail(email, {
|
await this.mailerService.sendEmail(email, {
|
||||||
template: 'code-your-own',
|
template: 'code-your-own',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GithubSSOGuard extends AuthGuard('github') {
|
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.GITHUB))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleSSOGuard extends AuthGuard('google') {
|
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.GOOGLE))
|
||||||
|
throwHTTPErr({ message: AUTH_PROVIDER_NOT_SPECIFIED, statusCode: 404 });
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftSSOGuard extends AuthGuard('microsoft') {
|
export class MicrosoftSSOGuard
|
||||||
|
extends AuthGuard('microsoft')
|
||||||
|
implements CanActivate
|
||||||
|
{
|
||||||
|
canActivate(
|
||||||
|
context: ExecutionContext,
|
||||||
|
): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
|
if (!authProviderCheck(AuthProvider.MICROSOFT))
|
||||||
|
throwHTTPErr({
|
||||||
|
message: AUTH_PROVIDER_NOT_SPECIFIED,
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
|
||||||
|
return super.canActivate(context);
|
||||||
|
}
|
||||||
|
|
||||||
getAuthenticateOptions(context: ExecutionContext) {
|
getAuthenticateOptions(context: ExecutionContext) {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { ForbiddenException, HttpException, HttpStatus } from '@nestjs/common';
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AuthError } from 'src/types/AuthError';
|
import { AuthError } from 'src/types/AuthError';
|
||||||
import { AuthTokens } from 'src/types/AuthTokens';
|
import { AuthTokens } from 'src/types/AuthTokens';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import * as cookie from 'cookie';
|
import * as cookie from 'cookie';
|
||||||
import { COOKIES_NOT_FOUND } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED, COOKIES_NOT_FOUND } from 'src/errors';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
|
||||||
enum AuthTokenType {
|
enum AuthTokenType {
|
||||||
ACCESS_TOKEN = 'access_token',
|
ACCESS_TOKEN = 'access_token',
|
||||||
@@ -16,6 +17,13 @@ export enum Origin {
|
|||||||
APP = 'app',
|
APP = 'app',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AuthProvider {
|
||||||
|
GOOGLE = 'GOOGLE',
|
||||||
|
GITHUB = 'GITHUB',
|
||||||
|
MICROSOFT = 'MICROSOFT',
|
||||||
|
EMAIL = 'EMAIL',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function allows throw to be used as an expression
|
* This function allows throw to be used as an expression
|
||||||
* @param errMessage Message present in the error message
|
* @param errMessage Message present in the error message
|
||||||
@@ -97,3 +105,25 @@ export const subscriptionContextCookieParser = (rawCookies: string) => {
|
|||||||
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
refresh_token: cookies[AuthTokenType.REFRESH_TOKEN],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if given auth provider is present in the VITE_ALLOWED_AUTH_PROVIDERS env variable
|
||||||
|
*
|
||||||
|
* @param provider Provider we want to check the presence of
|
||||||
|
* @returns Boolean if provider specified is present or not
|
||||||
|
*/
|
||||||
|
export function authProviderCheck(provider: string) {
|
||||||
|
if (!provider) {
|
||||||
|
throwErr(AUTH_PROVIDER_NOT_SPECIFIED);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envVariables = process.env.VITE_ALLOWED_AUTH_PROVIDERS
|
||||||
|
? process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',').map((provider) =>
|
||||||
|
provider.trim().toUpperCase(),
|
||||||
|
)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!envVariables.includes(provider.toUpperCase())) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
|||||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||||
scope: [process.env.MICROSOFT_SCOPE],
|
scope: [process.env.MICROSOFT_SCOPE],
|
||||||
passReqToCallback: true,
|
tenant: process.env.MICROSOFT_TENANT,
|
||||||
store: true,
|
store: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,31 @@ export const AUTH_FAIL = 'auth/fail';
|
|||||||
export const JSON_INVALID = 'json_invalid';
|
export const JSON_INVALID = 'json_invalid';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to delete an user data document from fb firestore but failed.
|
* Auth Provider not specified
|
||||||
|
* (Auth)
|
||||||
|
*/
|
||||||
|
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_NOT_FOUND_KEY_AUTH_PROVIDERS =
|
||||||
|
'"VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_EMPTY_AUTH_PROVIDERS =
|
||||||
|
'"VITE_ALLOWED_AUTH_PROVIDERS" is empty in .env file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" contains unsupported provider in .env file
|
||||||
|
*/
|
||||||
|
export const ENV_NOT_SUPPORT_AUTH_PROVIDERS =
|
||||||
|
'"VITE_ALLOWED_AUTH_PROVIDERS" contains an unsupported auth provider in .env file';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tried to delete a user data document from fb firestore but failed.
|
||||||
* (FirebaseService)
|
* (FirebaseService)
|
||||||
*/
|
*/
|
||||||
export const USER_FB_DOCUMENT_DELETION_FAILED =
|
export const USER_FB_DOCUMENT_DELETION_FAILED =
|
||||||
@@ -231,7 +255,7 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
|
|||||||
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
*/
|
*/
|
||||||
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
|
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
|
||||||
@@ -262,7 +286,7 @@ export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
|||||||
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
|
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform action on a request when the user is not even member of the team
|
* Tried to perform an action on a request when the user is not even a member of the team
|
||||||
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
|
||||||
*/
|
*/
|
||||||
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
|
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
|
||||||
@@ -307,11 +331,18 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
|
|||||||
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalid or non-existent TEAM ENVIRONMMENT ID
|
* Invalid or non-existent TEAM ENVIRONMENT ID
|
||||||
* (TeamEnvironmentsService)
|
* (TeamEnvironmentsService)
|
||||||
*/
|
*/
|
||||||
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalid TEAM ENVIRONMENT name
|
||||||
|
* (TeamEnvironmentsService)
|
||||||
|
*/
|
||||||
|
export const TEAM_ENVIRONMENT_SHORT_NAME =
|
||||||
|
'team_environment/short_name' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user is not a member of the team of the given environment
|
* The user is not a member of the team of the given environment
|
||||||
* (GqlTeamEnvTeamGuard)
|
* (GqlTeamEnvTeamGuard)
|
||||||
@@ -340,7 +371,7 @@ export const USER_SETTINGS_NULL_SETTINGS =
|
|||||||
'user_settings/null_settings' as const;
|
'user_settings/null_settings' as const;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Global environment doesnt exists for the user
|
* Global environment doesn't exist for the user
|
||||||
* (UserEnvironmentsService)
|
* (UserEnvironmentsService)
|
||||||
*/
|
*/
|
||||||
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =
|
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
UserMagicLinkMailDescription,
|
UserMagicLinkMailDescription,
|
||||||
} from './MailDescriptions';
|
} from './MailDescriptions';
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { EMAIL_FAILED } from 'src/errors';
|
import { EMAIL_FAILED } from 'src/errors';
|
||||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||||
|
|
||||||
@@ -35,33 +34,14 @@ export class MailerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends an email to the given email address given a mail description
|
* Sends an email to the given email address given a mail description
|
||||||
* @param to The email address to be sent to (NOTE: this is not validated)
|
* @param to Receiver's email id
|
||||||
* @param mailDesc Definition of what email to be sent
|
* @param mailDesc Definition of what email to be sent
|
||||||
|
* @returns Response if email was send successfully or not
|
||||||
*/
|
*/
|
||||||
sendMail(
|
async sendEmail(
|
||||||
to: string,
|
to: string,
|
||||||
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
||||||
) {
|
) {
|
||||||
return TE.tryCatch(
|
|
||||||
async () => {
|
|
||||||
await this.nestMailerService.sendMail({
|
|
||||||
to,
|
|
||||||
template: mailDesc.template,
|
|
||||||
subject: this.resolveSubjectForMailDesc(mailDesc),
|
|
||||||
context: mailDesc.variables,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => EMAIL_FAILED,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param to Receiver's email id
|
|
||||||
* @param mailDesc Details of email to be sent for Magic-Link auth
|
|
||||||
* @returns Response if email was send successfully or not
|
|
||||||
*/
|
|
||||||
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
|
|
||||||
try {
|
try {
|
||||||
await this.nestMailerService.sendMail({
|
await this.nestMailerService.sendMail({
|
||||||
to,
|
to,
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import * as cookieParser from 'cookie-parser';
|
|||||||
import { VersioningType } from '@nestjs/common';
|
import { VersioningType } from '@nestjs/common';
|
||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import { emitGQLSchemaFile } from './gql-schema';
|
import { emitGQLSchemaFile } from './gql-schema';
|
||||||
|
import { checkEnvironmentAuthProvider } from './utils';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||||
console.log(`Port: ${process.env.PORT}`);
|
console.log(`Port: ${process.env.PORT}`);
|
||||||
|
|
||||||
|
checkEnvironmentAuthProvider();
|
||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
orderBy: {
|
orderBy: {
|
||||||
createdOn: 'desc',
|
createdOn: 'desc',
|
||||||
},
|
},
|
||||||
skip: 1,
|
skip: args.cursor ? 1 : 0,
|
||||||
take: args.take,
|
take: args.take,
|
||||||
cursor: args.cursor ? { id: args.cursor } : undefined,
|
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from '@nestjs/core';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as O from 'fp-ts/Option';
|
|
||||||
import * as S from 'fp-ts/string';
|
|
||||||
import { pipe } from 'fp-ts/function';
|
|
||||||
import {
|
|
||||||
getAnnotatedRequiredRoles,
|
|
||||||
getGqlArg,
|
|
||||||
getUserFromGQLContext,
|
|
||||||
throwErr,
|
|
||||||
} from 'src/utils';
|
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
@@ -19,6 +9,10 @@ import {
|
|||||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { TeamMemberRole } from '@prisma/client';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A guard which checks whether the caller of a GQL Operation
|
* A guard which checks whether the caller of a GQL Operation
|
||||||
@@ -33,50 +27,31 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
|
|||||||
private readonly teamService: TeamService,
|
private readonly teamService: TeamService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||||
TE.Do,
|
'requiresTeamRole',
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
|
||||||
|
|
||||||
TE.bindW('requiredRoles', () =>
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
pipe(
|
|
||||||
getAnnotatedRequiredRoles(this.reflector, context),
|
|
||||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('user', () =>
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
pipe(
|
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
||||||
getUserFromGQLContext(context),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('envID', () =>
|
const { id } = gqlExecCtx.getArgs<{ id: string }>();
|
||||||
pipe(
|
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID);
|
||||||
getGqlArg('id', context),
|
|
||||||
O.fromPredicate(S.isString),
|
|
||||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('membership', ({ envID, user }) =>
|
const teamEnvironment =
|
||||||
pipe(
|
await this.teamEnvironmentService.getTeamEnvironment(id);
|
||||||
this.teamEnvironmentService.getTeamEnvironment(envID),
|
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
|
|
||||||
TE.chainW((env) =>
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(env.teamID, user.uid),
|
|
||||||
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.map(({ membership, requiredRoles }) =>
|
const member = await this.teamService.getTeamMember(
|
||||||
requiredRoles.includes(membership.role),
|
teamEnvironment.right.teamID,
|
||||||
),
|
user.uid,
|
||||||
|
);
|
||||||
|
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
|
||||||
|
|
||||||
TE.getOrElse(throwErr),
|
return requireRoles.includes(member.role);
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class CreateTeamEnvironmentArgs {
|
||||||
|
@Field({
|
||||||
|
name: 'name',
|
||||||
|
description: 'Name of the Team Environment',
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team',
|
||||||
|
})
|
||||||
|
teamID: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'variables',
|
||||||
|
description: 'JSON string of the variables object',
|
||||||
|
})
|
||||||
|
variables: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class UpdateTeamEnvironmentArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'id',
|
||||||
|
description: 'ID of the Team Environment',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
@Field({
|
||||||
|
name: 'name',
|
||||||
|
description: 'Name of the Team Environment',
|
||||||
|
})
|
||||||
|
name: string;
|
||||||
|
@Field({
|
||||||
|
name: 'variables',
|
||||||
|
description: 'JSON string of the variables object',
|
||||||
|
})
|
||||||
|
variables: string;
|
||||||
|
}
|
||||||
@@ -13,6 +13,11 @@ import { throwErr } from 'src/utils';
|
|||||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import {
|
||||||
|
CreateTeamEnvironmentArgs,
|
||||||
|
UpdateTeamEnvironmentArgs,
|
||||||
|
} from './input-type.args';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => 'TeamEnvironment')
|
@Resolver(() => 'TeamEnvironment')
|
||||||
@@ -29,29 +34,18 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
createTeamEnvironment(
|
async createTeamEnvironment(
|
||||||
@Args({
|
@Args() args: CreateTeamEnvironmentArgs,
|
||||||
name: 'name',
|
|
||||||
description: 'Name of the Team Environment',
|
|
||||||
})
|
|
||||||
name: string,
|
|
||||||
@Args({
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
teamID: string,
|
|
||||||
@Args({
|
|
||||||
name: 'variables',
|
|
||||||
description: 'JSON string of the variables object',
|
|
||||||
})
|
|
||||||
variables: string,
|
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return this.teamEnvironmentsService.createTeamEnvironment(
|
const teamEnvironment =
|
||||||
name,
|
await this.teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamID,
|
args.name,
|
||||||
variables,
|
args.teamID,
|
||||||
)();
|
args.variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||||
|
return teamEnvironment.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -59,7 +53,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
deleteTeamEnvironment(
|
async deleteTeamEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -67,10 +61,12 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
return pipe(
|
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
this.teamEnvironmentsService.deleteTeamEnvironment(id),
|
id,
|
||||||
TE.getOrElse(throwErr),
|
);
|
||||||
)();
|
|
||||||
|
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
||||||
|
return isDeleted.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -79,28 +75,19 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
updateTeamEnvironment(
|
async updateTeamEnvironment(
|
||||||
@Args({
|
@Args()
|
||||||
name: 'id',
|
args: UpdateTeamEnvironmentArgs,
|
||||||
description: 'ID of the Team Environment',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
id: string,
|
|
||||||
@Args({
|
|
||||||
name: 'name',
|
|
||||||
description: 'Name of the Team Environment',
|
|
||||||
})
|
|
||||||
name: string,
|
|
||||||
@Args({
|
|
||||||
name: 'variables',
|
|
||||||
description: 'JSON string of the variables object',
|
|
||||||
})
|
|
||||||
variables: string,
|
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return pipe(
|
const updatedTeamEnvironment =
|
||||||
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
|
await this.teamEnvironmentsService.updateTeamEnvironment(
|
||||||
TE.getOrElse(throwErr),
|
args.id,
|
||||||
)();
|
args.name,
|
||||||
|
args.variables,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
|
||||||
|
return updatedTeamEnvironment.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -108,7 +95,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
deleteAllVariablesFromTeamEnvironment(
|
async deleteAllVariablesFromTeamEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -116,10 +103,13 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return pipe(
|
const teamEnvironment =
|
||||||
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
|
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
TE.getOrElse(throwErr),
|
id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||||
|
return teamEnvironment.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamEnvironment, {
|
@Mutation(() => TeamEnvironment, {
|
||||||
@@ -127,7 +117,7 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
createDuplicateEnvironment(
|
async createDuplicateEnvironment(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
description: 'ID of the Team Environment',
|
description: 'ID of the Team Environment',
|
||||||
@@ -135,10 +125,12 @@ export class TeamEnvironmentsResolver {
|
|||||||
})
|
})
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<TeamEnvironment> {
|
): Promise<TeamEnvironment> {
|
||||||
return pipe(
|
const res = await this.teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
this.teamEnvironmentsService.createDuplicateEnvironment(id),
|
id,
|
||||||
TE.getOrElse(throwErr),
|
);
|
||||||
)();
|
|
||||||
|
if (E.isLeft(res)) throwErr(res.left);
|
||||||
|
return res.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
|
|||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TeamEnvironmentsService } from './team-environments.service';
|
import { TeamEnvironmentsService } from './team-environments.service';
|
||||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
JSON_INVALID,
|
||||||
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||||
|
} from 'src/errors';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
|
||||||
@@ -31,125 +35,81 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('TeamEnvironmentsService', () => {
|
describe('TeamEnvironmentsService', () => {
|
||||||
describe('getTeamEnvironment', () => {
|
describe('getTeamEnvironment', () => {
|
||||||
test('queries the db with the id', async () => {
|
test('should successfully return a TeamEnvironment with valid ID', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
|
teamEnvironment,
|
||||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
|
||||||
|
|
||||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
where: {
|
|
||||||
id: '123',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
test('requests prisma to reject the query promise if not found', async () => {
|
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
teamEnvironment.id,
|
||||||
|
|
||||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
|
||||||
|
|
||||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
rejectOnNotFound: true,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
expect(result).toEqualRight(teamEnvironment);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a Some of the correct environment if exists', async () => {
|
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
|
'RejectOnNotFound',
|
||||||
|
);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||||
|
teamEnvironment.id,
|
||||||
expect(result).toEqualSome(teamEnvironment);
|
);
|
||||||
});
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
|
||||||
test('should return a None if the environment does not exist', async () => {
|
|
||||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
|
||||||
|
|
||||||
expect(result).toBeNone();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createTeamEnvironment', () => {
|
describe('createTeamEnvironment', () => {
|
||||||
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
|
test('should successfully create and return a new team environment given valid inputs', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqual(<TeamEnvironment>{
|
expect(result).toEqualRight({
|
||||||
id: teamEnvironment.id,
|
...teamEnvironment,
|
||||||
name: teamEnvironment.name,
|
|
||||||
teamID: teamEnvironment.teamID,
|
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject if given team ID is invalid', async () => {
|
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
|
'12',
|
||||||
await expect(
|
|
||||||
teamEnvironmentsService.createTeamEnvironment(
|
|
||||||
teamEnvironment.name,
|
|
||||||
'invalidteamid',
|
|
||||||
JSON.stringify(teamEnvironment.variables),
|
|
||||||
),
|
|
||||||
).rejects.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should reject if provided team environment name is not a string', async () => {
|
|
||||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
teamEnvironmentsService.createTeamEnvironment(
|
|
||||||
null as any,
|
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
),
|
);
|
||||||
).rejects.toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should reject if provided variable is not a string', async () => {
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
teamEnvironmentsService.createTeamEnvironment(
|
|
||||||
teamEnvironment.name,
|
|
||||||
teamEnvironment.teamID,
|
|
||||||
null as any,
|
|
||||||
),
|
|
||||||
).rejects.toBeDefined();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
teamEnvironment.teamID,
|
teamEnvironment.teamID,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/created`,
|
`team_environment/${teamEnvironment.teamID}/created`,
|
||||||
result,
|
{
|
||||||
|
...teamEnvironment,
|
||||||
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteTeamEnvironment', () => {
|
describe('deleteTeamEnvironment', () => {
|
||||||
test('should resolve to true given a valid team environment ID', async () => {
|
test('should successfully delete a TeamEnvironment with a valid ID', async () => {
|
||||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(true);
|
expect(result).toEqualRight(true);
|
||||||
});
|
});
|
||||||
@@ -159,7 +119,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -169,7 +129,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/deleted`,
|
`team_environment/${teamEnvironment.teamID}/deleted`,
|
||||||
@@ -182,7 +142,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateVariablesInTeamEnvironment', () => {
|
describe('updateVariablesInTeamEnvironment', () => {
|
||||||
test('should add new variable to a team environment', async () => {
|
test('should successfully add new variable to a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: 'value' }],
|
variables: [{ key: 'value' }],
|
||||||
@@ -192,7 +152,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }]),
|
JSON.stringify([{ key: 'value' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -200,7 +160,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should add new variable to already existing list of variables in a team environment', async () => {
|
test('should successfully add new variable to already existing list of variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
||||||
@@ -210,7 +170,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -218,7 +178,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should edit existing variables in a team environment', async () => {
|
test('should successfully edit existing variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: '1234' }],
|
variables: [{ key: '1234' }],
|
||||||
@@ -228,7 +188,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: '1234' }]),
|
JSON.stringify([{ key: '1234' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -236,22 +196,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should delete existing variable in a team environment', async () => {
|
test('should successfully edit name of an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
|
||||||
teamEnvironment.id,
|
|
||||||
teamEnvironment.name,
|
|
||||||
JSON.stringify([{}]),
|
|
||||||
)();
|
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
|
||||||
...teamEnvironment,
|
|
||||||
variables: JSON.stringify([{}]),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should edit name of an existing team environment', async () => {
|
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
variables: [{ key: '123' }],
|
variables: [{ key: '123' }],
|
||||||
@@ -261,7 +206,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: '123' }]),
|
JSON.stringify([{ key: '123' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -269,14 +214,24 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||||
|
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||||
|
teamEnvironment.id,
|
||||||
|
'12',
|
||||||
|
JSON.stringify([{ key: 'value' }]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify(teamEnvironment.variables),
|
JSON.stringify(teamEnvironment.variables),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -288,7 +243,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
teamEnvironment.name,
|
teamEnvironment.name,
|
||||||
JSON.stringify([{ key: 'value' }]),
|
JSON.stringify([{ key: 'value' }]),
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
@@ -301,13 +256,13 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
||||||
test('should delete all variables in a team environment', async () => {
|
test('should successfully delete all variables in a team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
...teamEnvironment,
|
||||||
@@ -315,13 +270,13 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
'invalidid',
|
'invalidid',
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -332,7 +287,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
const result =
|
const result =
|
||||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
@@ -345,33 +300,33 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createDuplicateEnvironment', () => {
|
describe('createDuplicateEnvironment', () => {
|
||||||
test('should duplicate an existing team environment', async () => {
|
test('should successfully duplicate an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||||
teamEnvironment,
|
teamEnvironment,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualRight(<TeamEnvironment>{
|
expect(result).toEqualRight(<TeamEnvironment>{
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
variables: JSON.stringify(teamEnvironment.variables),
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
});
|
});
|
||||||
@@ -382,19 +337,19 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
)();
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_environment/${teamEnvironment.teamID}/created`,
|
`team_environment/${teamEnvironment.teamID}/created`,
|
||||||
{
|
{
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
variables: JSON.stringify([{}]),
|
variables: JSON.stringify([{}]),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { pipe } from 'fp-ts/function';
|
import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TO from 'fp-ts/TaskOption';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as A from 'fp-ts/Array';
|
|
||||||
import { Prisma } from '@prisma/client';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { TeamEnvironment } from './team-environments.model';
|
import { TeamEnvironment } from './team-environments.model';
|
||||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||||
|
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||||
|
} from 'src/errors';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { isValidLength } from 'src/utils';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamEnvironmentsService {
|
export class TeamEnvironmentsService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -17,219 +16,218 @@ export class TeamEnvironmentsService {
|
|||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
getTeamEnvironment(id: string) {
|
TITLE_LENGTH = 3;
|
||||||
return TO.tryCatch(() =>
|
|
||||||
this.prisma.teamEnvironment.findFirst({
|
/**
|
||||||
where: { id },
|
* TeamEnvironments are saved in the DB in the following way
|
||||||
rejectOnNotFound: true,
|
* [{ key: value }, { key: value },....]
|
||||||
}),
|
*
|
||||||
);
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Typecast a database TeamEnvironment to a TeamEnvironment model
|
||||||
|
* @param teamEnvironment database TeamEnvironment
|
||||||
|
* @returns TeamEnvironment model
|
||||||
|
*/
|
||||||
|
private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment {
|
||||||
|
return {
|
||||||
|
id: teamEnvironment.id,
|
||||||
|
name: teamEnvironment.name,
|
||||||
|
teamID: teamEnvironment.teamID,
|
||||||
|
variables: JSON.stringify(teamEnvironment.variables),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
createTeamEnvironment(name: string, teamID: string, variables: string) {
|
/**
|
||||||
return pipe(
|
* Get details of a TeamEnvironment.
|
||||||
() =>
|
*
|
||||||
this.prisma.teamEnvironment.create({
|
* @param id TeamEnvironment ID
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async getTeamEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const teamEnvironment =
|
||||||
|
await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
return E.right(teamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new TeamEnvironment.
|
||||||
|
*
|
||||||
|
* @param name name of new TeamEnvironment
|
||||||
|
* @param teamID teamID of new TeamEnvironment
|
||||||
|
* @param variables JSONified string of contents of new TeamEnvironment
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async createTeamEnvironment(name: string, teamID: string, variables: string) {
|
||||||
|
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
||||||
|
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
|
|
||||||
|
const result = await this.prisma.teamEnvironment.create({
|
||||||
data: {
|
data: {
|
||||||
name: name,
|
name: name,
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
variables: JSON.parse(variables),
|
variables: JSON.parse(variables),
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
T.chainFirst(
|
|
||||||
(environment) => () =>
|
const createdTeamEnvironment = this.cast(result);
|
||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_environment/${environment.teamID}/created`,
|
`team_environment/${createdTeamEnvironment.teamID}/created`,
|
||||||
<TeamEnvironment>{
|
createdTeamEnvironment,
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
T.map((data) => {
|
|
||||||
return <TeamEnvironment>{
|
|
||||||
id: data.id,
|
|
||||||
name: data.name,
|
|
||||||
teamID: data.teamID,
|
|
||||||
variables: JSON.stringify(data.variables),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return E.right(createdTeamEnvironment);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTeamEnvironment(id: string) {
|
/**
|
||||||
return pipe(
|
* Delete a TeamEnvironment.
|
||||||
TE.tryCatch(
|
*
|
||||||
() =>
|
* @param id TeamEnvironment ID
|
||||||
this.prisma.teamEnvironment.delete({
|
* @returns Either of boolean or error message
|
||||||
|
*/
|
||||||
|
async deleteTeamEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const result = await this.prisma.teamEnvironment.delete({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
const deletedTeamEnvironment = this.cast(result);
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_environment/${environment.teamID}/deleted`,
|
`team_environment/${deletedTeamEnvironment.teamID}/deleted`,
|
||||||
<TeamEnvironment>{
|
deletedTeamEnvironment,
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map((data) => true),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTeamEnvironment(id: string, name: string, variables: string) {
|
/**
|
||||||
return pipe(
|
* Update a TeamEnvironment.
|
||||||
TE.tryCatch(
|
*
|
||||||
() =>
|
* @param id TeamEnvironment ID
|
||||||
this.prisma.teamEnvironment.update({
|
* @param name TeamEnvironment name
|
||||||
|
* @param variables JSONified string of contents of new TeamEnvironment
|
||||||
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async updateTeamEnvironment(id: string, name: string, variables: string) {
|
||||||
|
try {
|
||||||
|
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
||||||
|
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||||
|
|
||||||
|
const result = await this.prisma.teamEnvironment.update({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
variables: JSON.parse(variables),
|
variables: JSON.parse(variables),
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
const updatedTeamEnvironment = this.cast(result);
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_environment/${environment.teamID}/updated`,
|
`team_environment/${updatedTeamEnvironment.teamID}/updated`,
|
||||||
<TeamEnvironment>{
|
updatedTeamEnvironment,
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return E.right(updatedTeamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteAllVariablesFromTeamEnvironment(id: string) {
|
/**
|
||||||
return pipe(
|
* Clear contents of a TeamEnvironment.
|
||||||
TE.tryCatch(
|
*
|
||||||
() =>
|
* @param id TeamEnvironment ID
|
||||||
this.prisma.teamEnvironment.update({
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async deleteAllVariablesFromTeamEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const result = await this.prisma.teamEnvironment.update({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
data: {
|
data: {
|
||||||
variables: [],
|
variables: [],
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
const teamEnvironment = this.cast(result);
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_environment/${environment.teamID}/updated`,
|
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||||
<TeamEnvironment>{
|
teamEnvironment,
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return E.right(teamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createDuplicateEnvironment(id: string) {
|
/**
|
||||||
return pipe(
|
* Create a duplicate of a existing TeamEnvironment.
|
||||||
TE.tryCatch(
|
*
|
||||||
() =>
|
* @param id TeamEnvironment ID
|
||||||
this.prisma.teamEnvironment.findFirst({
|
* @returns Either of a TeamEnvironment or error message
|
||||||
|
*/
|
||||||
|
async createDuplicateEnvironment(id: string) {
|
||||||
|
try {
|
||||||
|
const environment = await this.prisma.teamEnvironment.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
rejectOnNotFound: true,
|
rejectOnNotFound: true,
|
||||||
}),
|
});
|
||||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
|
||||||
),
|
const result = await this.prisma.teamEnvironment.create({
|
||||||
TE.chain((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.prisma.teamEnvironment.create({
|
|
||||||
data: {
|
data: {
|
||||||
name: environment.name,
|
name: environment.name,
|
||||||
teamID: environment.teamID,
|
teamID: environment.teamID,
|
||||||
variables: environment.variables as Prisma.JsonArray,
|
variables: environment.variables as Prisma.JsonArray,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
),
|
|
||||||
),
|
const duplicatedTeamEnvironment = this.cast(result);
|
||||||
TE.chainFirst((environment) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_environment/${environment.teamID}/created`,
|
`team_environment/${duplicatedTeamEnvironment.teamID}/created`,
|
||||||
<TeamEnvironment>{
|
duplicatedTeamEnvironment,
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(
|
|
||||||
(environment) =>
|
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return E.right(duplicatedTeamEnvironment);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAllTeamEnvironments(teamID: string) {
|
/**
|
||||||
return pipe(
|
* Fetch all TeamEnvironments of a team.
|
||||||
() =>
|
*
|
||||||
this.prisma.teamEnvironment.findMany({
|
* @param teamID teamID of new TeamEnvironment
|
||||||
|
* @returns List of TeamEnvironments
|
||||||
|
*/
|
||||||
|
async fetchAllTeamEnvironments(teamID: string) {
|
||||||
|
const result = await this.prisma.teamEnvironment.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
T.map(
|
const teamEnvironments = result.map((item) => {
|
||||||
A.map(
|
return this.cast(item);
|
||||||
(environment) =>
|
});
|
||||||
<TeamEnvironment>{
|
|
||||||
id: environment.id,
|
return teamEnvironments;
|
||||||
name: environment.name,
|
|
||||||
teamID: environment.teamID,
|
|
||||||
variables: JSON.stringify(environment.variables),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
|
|||||||
description: 'Returns all Team Environments for the given Team',
|
description: 'Returns all Team Environments for the given Team',
|
||||||
})
|
})
|
||||||
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
||||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
|
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||||
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class CreateTeamInvitationArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'teamID',
|
||||||
|
description: 'ID of the Team ID to invite from',
|
||||||
|
})
|
||||||
|
teamID: string;
|
||||||
|
|
||||||
|
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
|
||||||
|
inviteeEmail: string;
|
||||||
|
|
||||||
|
@Field(() => TeamMemberRole, {
|
||||||
|
name: 'inviteeRole',
|
||||||
|
description: 'Role to be given to the user',
|
||||||
|
})
|
||||||
|
inviteeRole: TeamMemberRole;
|
||||||
|
}
|
||||||
@@ -12,15 +12,10 @@ import { TeamInvitation } from './team-invitation.model';
|
|||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe } from 'fp-ts/function';
|
import { pipe } from 'fp-ts/function';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
import * as TE from 'fp-ts/TaskEither';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { EmailCodec } from 'src/types/Email';
|
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||||
import {
|
|
||||||
INVALID_EMAIL,
|
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
|
||||||
USER_NOT_FOUND,
|
|
||||||
} from 'src/errors';
|
|
||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { User } from 'src/user/user.model';
|
import { User } from 'src/user/user.model';
|
||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
@@ -36,6 +31,8 @@ import { UserService } from 'src/user/user.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { CreateTeamInvitationArgs } from './input-type.args';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => TeamInvitation)
|
@Resolver(() => TeamInvitation)
|
||||||
@@ -79,8 +76,8 @@ export class TeamInvitationResolver {
|
|||||||
'Gets the Team Invitation with the given ID, or null if not exists',
|
'Gets the Team Invitation with the given ID, or null if not exists',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
||||||
teamInvitation(
|
async teamInvitation(
|
||||||
@GqlUser() user: User,
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
description: 'ID of the Team Invitation to lookup',
|
description: 'ID of the Team Invitation to lookup',
|
||||||
@@ -88,17 +85,11 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
return pipe(
|
const teamInvitation = await this.teamInvitationService.getInvitation(
|
||||||
this.teamInvitationService.getInvitation(inviteID),
|
inviteID,
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
);
|
||||||
TE.chainW(
|
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
TE.fromPredicate(
|
return teamInvitation.value;
|
||||||
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.getOrElse(throwErr),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamInvitation, {
|
@Mutation(() => TeamInvitation, {
|
||||||
@@ -106,56 +97,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
createTeamInvitation(
|
async createTeamInvitation(
|
||||||
@GqlUser()
|
@GqlUser() user: AuthUser,
|
||||||
user: User,
|
@Args() args: CreateTeamInvitationArgs,
|
||||||
|
|
||||||
@Args({
|
|
||||||
name: 'teamID',
|
|
||||||
description: 'ID of the Team ID to invite from',
|
|
||||||
type: () => ID,
|
|
||||||
})
|
|
||||||
teamID: string,
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeEmail',
|
|
||||||
description: 'Email of the user to invite',
|
|
||||||
})
|
|
||||||
inviteeEmail: string,
|
|
||||||
@Args({
|
|
||||||
name: 'inviteeRole',
|
|
||||||
type: () => TeamMemberRole,
|
|
||||||
description: 'Role to be given to the user',
|
|
||||||
})
|
|
||||||
inviteeRole: TeamMemberRole,
|
|
||||||
): Promise<TeamInvitation> {
|
): Promise<TeamInvitation> {
|
||||||
return pipe(
|
const teamInvitation = await this.teamInvitationService.createInvitation(
|
||||||
TE.Do,
|
|
||||||
|
|
||||||
// Validate email
|
|
||||||
TE.bindW('email', () =>
|
|
||||||
pipe(
|
|
||||||
EmailCodec.decode(inviteeEmail),
|
|
||||||
TE.fromEither,
|
|
||||||
TE.mapLeft(() => INVALID_EMAIL),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validate and get Team
|
|
||||||
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
|
|
||||||
|
|
||||||
// Create team
|
|
||||||
TE.chainW(({ email, team }) =>
|
|
||||||
this.teamInvitationService.createInvitation(
|
|
||||||
user,
|
user,
|
||||||
team,
|
args.teamID,
|
||||||
email,
|
args.inviteeEmail,
|
||||||
inviteeRole,
|
args.inviteeRole,
|
||||||
),
|
);
|
||||||
),
|
|
||||||
|
|
||||||
// If failed, throw err (so the message is passed) else return value
|
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
|
||||||
TE.getOrElse(throwErr),
|
return teamInvitation.right;
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => Boolean, {
|
@Mutation(() => Boolean, {
|
||||||
@@ -163,7 +117,7 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||||
revokeTeamInvitation(
|
async revokeTeamInvitation(
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -171,19 +125,19 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<true> {
|
): Promise<true> {
|
||||||
return pipe(
|
const isRevoked = await this.teamInvitationService.revokeInvitation(
|
||||||
this.teamInvitationService.revokeInvitation(inviteID),
|
inviteID,
|
||||||
TE.map(() => true as const),
|
);
|
||||||
TE.getOrElse(throwErr),
|
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
|
||||||
)();
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => TeamMember, {
|
@Mutation(() => TeamMember, {
|
||||||
description: 'Accept an Invitation',
|
description: 'Accept an Invitation',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
||||||
acceptTeamInvitation(
|
async acceptTeamInvitation(
|
||||||
@GqlUser() user: User,
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'inviteID',
|
name: 'inviteID',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
@@ -191,10 +145,12 @@ export class TeamInvitationResolver {
|
|||||||
})
|
})
|
||||||
inviteID: string,
|
inviteID: string,
|
||||||
): Promise<TeamMember> {
|
): Promise<TeamMember> {
|
||||||
return pipe(
|
const teamMember = await this.teamInvitationService.acceptInvitation(
|
||||||
this.teamInvitationService.acceptInvitation(inviteID, user),
|
inviteID,
|
||||||
TE.getOrElse(throwErr),
|
user,
|
||||||
)();
|
);
|
||||||
|
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||||
|
return teamMember.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as TO from 'fp-ts/TaskOption';
|
import * as E from 'fp-ts/Either';
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { pipe, flow, constVoid } from 'fp-ts/function';
|
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { Team, TeamMemberRole } from 'src/team/team.model';
|
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
|
||||||
import { Email } from 'src/types/Email';
|
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import {
|
import {
|
||||||
|
INVALID_EMAIL,
|
||||||
|
TEAM_INVALID_ID,
|
||||||
TEAM_INVITE_ALREADY_MEMBER,
|
TEAM_INVITE_ALREADY_MEMBER,
|
||||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { TeamInvitation } from './team-invitation.model';
|
import { TeamInvitation } from './team-invitation.model';
|
||||||
import { MailerService } from 'src/mailer/mailer.service';
|
import { MailerService } from 'src/mailer/mailer.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
|
import { validateEmail } from '../utils';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInvitationService {
|
export class TeamInvitationService {
|
||||||
@@ -29,245 +30,221 @@ export class TeamInvitationService {
|
|||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
) {
|
) {}
|
||||||
this.getInvitation = this.getInvitation.bind(this);
|
|
||||||
|
/**
|
||||||
|
* Cast a DBTeamInvitation to a TeamInvitation
|
||||||
|
* @param dbTeamInvitation database TeamInvitation
|
||||||
|
* @returns TeamInvitation model
|
||||||
|
*/
|
||||||
|
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
|
||||||
|
return {
|
||||||
|
...dbTeamInvitation,
|
||||||
|
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
|
/**
|
||||||
return pipe(
|
* Get the team invite
|
||||||
() =>
|
* @param inviteID invite id
|
||||||
this.prisma.teamInvitation.findUnique({
|
* @returns an Option of team invitation or none
|
||||||
|
*/
|
||||||
|
async getInvitation(inviteID: string) {
|
||||||
|
try {
|
||||||
|
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: inviteID,
|
id: inviteID,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
TO.fromTask,
|
|
||||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
return O.some(this.cast(dbInvitation));
|
||||||
TO.map((x) => x as TeamInvitation),
|
} catch (e) {
|
||||||
);
|
return O.none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getInvitationWithEmail(email: Email, team: Team) {
|
/**
|
||||||
return pipe(
|
* Get the team invite for an invitee with email and teamID.
|
||||||
() =>
|
* @param inviteeEmail invitee email
|
||||||
this.prisma.teamInvitation.findUnique({
|
* @param teamID team id
|
||||||
|
* @returns an Either of team invitation for the invitee or error
|
||||||
|
*/
|
||||||
|
async getTeamInviteByEmailAndTeamID(inviteeEmail: string, teamID: string) {
|
||||||
|
const isEmailValid = validateEmail(inviteeEmail);
|
||||||
|
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
teamID_inviteeEmail: {
|
teamID_inviteeEmail: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: inviteeEmail,
|
||||||
teamID: team.id,
|
teamID: teamID,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
TO.fromTask,
|
|
||||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
return E.right(teamInvite);
|
||||||
);
|
} catch (e) {
|
||||||
|
return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createInvitation(
|
/**
|
||||||
creator: User,
|
* Create a team invitation
|
||||||
team: Team,
|
* @param creator creator of the invitation
|
||||||
inviteeEmail: Email,
|
* @param teamID team id
|
||||||
|
* @param inviteeEmail invitee email
|
||||||
|
* @param inviteeRole invitee role
|
||||||
|
* @returns an Either of team invitation or error message
|
||||||
|
*/
|
||||||
|
async createInvitation(
|
||||||
|
creator: AuthUser,
|
||||||
|
teamID: string,
|
||||||
|
inviteeEmail: string,
|
||||||
inviteeRole: TeamMemberRole,
|
inviteeRole: TeamMemberRole,
|
||||||
) {
|
) {
|
||||||
return pipe(
|
// validate email
|
||||||
// Perform all validation checks
|
const isEmailValid = validateEmail(inviteeEmail);
|
||||||
TE.sequenceArray([
|
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||||
// creator should be a TeamMember
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(team.id, creator.uid),
|
|
||||||
TE.map(constVoid),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Invitee should not be a team member
|
// team ID should valid
|
||||||
pipe(
|
const team = await this.teamService.getTeamWithID(teamID);
|
||||||
async () => await this.userService.findUserByEmail(inviteeEmail),
|
if (!team) return E.left(TEAM_INVALID_ID);
|
||||||
TO.foldW(
|
|
||||||
() => TE.right(undefined), // If no user, short circuit to completion
|
|
||||||
(user) =>
|
|
||||||
pipe(
|
|
||||||
// If user is found, check if team member
|
|
||||||
this.teamService.getTeamMemberTE(team.id, user.uid),
|
|
||||||
TE.foldW(
|
|
||||||
() => TE.right(undefined), // Not team-member, this is good
|
|
||||||
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TE.map(constVoid),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Should not have an existing invite
|
// invitation creator should be a TeamMember
|
||||||
pipe(
|
const isTeamMember = await this.teamService.getTeamMember(
|
||||||
this.getInvitationWithEmail(inviteeEmail, team),
|
team.id,
|
||||||
TE.fromTaskOption(() => null),
|
creator.uid,
|
||||||
TE.swap,
|
);
|
||||||
TE.map(constVoid),
|
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||||
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// Create the invitation
|
// Checking to see if the invitee is already part of the team or not
|
||||||
TE.chainTaskK(
|
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
|
||||||
() => () =>
|
if (O.isSome(inviteeUser)) {
|
||||||
this.prisma.teamInvitation.create({
|
// invitee should not already a member
|
||||||
|
const isTeamMember = await this.teamService.getTeamMember(
|
||||||
|
team.id,
|
||||||
|
inviteeUser.value.uid,
|
||||||
|
);
|
||||||
|
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check invitee already invited earlier or not
|
||||||
|
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
|
||||||
|
inviteeEmail,
|
||||||
|
team.id,
|
||||||
|
);
|
||||||
|
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
|
||||||
|
|
||||||
|
// create the invitation
|
||||||
|
const dbInvitation = await this.prisma.teamInvitation.create({
|
||||||
data: {
|
data: {
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
inviteeEmail,
|
inviteeEmail,
|
||||||
inviteeRole,
|
inviteeRole,
|
||||||
creatorUid: creator.uid,
|
creatorUid: creator.uid,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
),
|
|
||||||
|
|
||||||
// Send email, this is a side effect
|
await this.mailerService.sendEmail(inviteeEmail, {
|
||||||
TE.chainFirstTaskK((invitation) =>
|
|
||||||
pipe(
|
|
||||||
this.mailerService.sendMail(inviteeEmail, {
|
|
||||||
template: 'team-invitation',
|
template: 'team-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
|
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||||
invite_team_name: team.name,
|
invite_team_name: team.name,
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
|
|
||||||
TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
|
const invitation = this.cast(dbInvitation);
|
||||||
),
|
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
|
||||||
),
|
|
||||||
|
|
||||||
// Send PubSub topic
|
return E.right(invitation);
|
||||||
TE.chainFirstTaskK((invitation) =>
|
|
||||||
TE.fromTask(async () => {
|
|
||||||
const inv: TeamInvitation = {
|
|
||||||
id: invitation.id,
|
|
||||||
teamID: invitation.teamID,
|
|
||||||
creatorUid: invitation.creatorUid,
|
|
||||||
inviteeEmail: invitation.inviteeEmail,
|
|
||||||
inviteeRole: TeamMemberRole[invitation.inviteeRole],
|
|
||||||
};
|
|
||||||
|
|
||||||
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Map to model type
|
|
||||||
TE.map((x) => x as TeamInvitation),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
revokeInvitation(inviteID: string) {
|
|
||||||
return pipe(
|
|
||||||
// Make sure invite exists
|
|
||||||
this.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
|
|
||||||
// Delete team invitation
|
|
||||||
TE.chainTaskK(
|
|
||||||
() => () =>
|
|
||||||
this.prisma.teamInvitation.delete({
|
|
||||||
where: {
|
|
||||||
id: inviteID,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Emit Pubsub Event
|
|
||||||
TE.chainFirst((invitation) =>
|
|
||||||
TE.fromTask(() =>
|
|
||||||
this.pubsub.publish(
|
|
||||||
`team/${invitation.teamID}/invite_removed`,
|
|
||||||
invitation.id,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// We are not returning anything
|
|
||||||
TE.map(constVoid),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllInvitationsInTeam(team: Team) {
|
|
||||||
return pipe(
|
|
||||||
() =>
|
|
||||||
this.prisma.teamInvitation.findMany({
|
|
||||||
where: {
|
|
||||||
teamID: team.id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
T.map((x) => x as TeamInvitation[]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptInvitation(inviteID: string, acceptedBy: User) {
|
|
||||||
return pipe(
|
|
||||||
TE.Do,
|
|
||||||
|
|
||||||
// First get the invitation
|
|
||||||
TE.bindW('invitation', () =>
|
|
||||||
pipe(
|
|
||||||
this.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Validation checks
|
|
||||||
TE.chainFirstW(({ invitation }) =>
|
|
||||||
TE.sequenceArray([
|
|
||||||
// Make sure the invited user is not part of the team
|
|
||||||
pipe(
|
|
||||||
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
|
|
||||||
TE.swap,
|
|
||||||
TE.bimap(
|
|
||||||
() => TEAM_INVITE_ALREADY_MEMBER,
|
|
||||||
constVoid, // The return type is ignored
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Make sure the invited user and accepting user has the same email
|
|
||||||
pipe(
|
|
||||||
undefined,
|
|
||||||
TE.fromPredicate(
|
|
||||||
(a) => acceptedBy.email === invitation.inviteeEmail,
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Add the team member
|
|
||||||
// TODO: Somehow bring subscriptions to this ?
|
|
||||||
TE.bindW('teamMember', ({ invitation }) =>
|
|
||||||
pipe(
|
|
||||||
TE.tryCatch(
|
|
||||||
() =>
|
|
||||||
this.teamService.addMemberToTeam(
|
|
||||||
invitation.teamID,
|
|
||||||
acceptedBy.uid,
|
|
||||||
invitation.inviteeRole,
|
|
||||||
),
|
|
||||||
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
|
|
||||||
|
|
||||||
TE.map(({ teamMember }) => teamMember),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the count invitations for a given team.
|
* Revoke a team invitation
|
||||||
* @param teamID team id
|
* @param inviteID invite id
|
||||||
* @returns a count team invitations for a team
|
* @returns an Either of true or error message
|
||||||
*/
|
*/
|
||||||
async getAllTeamInvitations(teamID: string) {
|
async revokeInvitation(inviteID: string) {
|
||||||
const invitations = await this.prisma.teamInvitation.findMany({
|
// check if the invite exists
|
||||||
|
const invitation = await this.getInvitation(inviteID);
|
||||||
|
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
|
||||||
|
// delete the invite
|
||||||
|
await this.prisma.teamInvitation.delete({
|
||||||
|
where: {
|
||||||
|
id: inviteID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team/${invitation.value.teamID}/invite_removed`,
|
||||||
|
invitation.value.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Accept a team invitation
|
||||||
|
* @param inviteID invite id
|
||||||
|
* @param acceptedBy user who accepted the invitation
|
||||||
|
* @returns an Either of team member or error message
|
||||||
|
*/
|
||||||
|
async acceptInvitation(inviteID: string, acceptedBy: AuthUser) {
|
||||||
|
// check if the invite exists
|
||||||
|
const invitation = await this.getInvitation(inviteID);
|
||||||
|
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
|
|
||||||
|
// make sure the user is not already a member of the team
|
||||||
|
const teamMemberInvitee = await this.teamService.getTeamMember(
|
||||||
|
invitation.value.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
);
|
||||||
|
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
|
||||||
|
// make sure the user is the same as the invitee
|
||||||
|
if (
|
||||||
|
acceptedBy.email.toLowerCase() !==
|
||||||
|
invitation.value.inviteeEmail.toLowerCase()
|
||||||
|
)
|
||||||
|
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||||
|
|
||||||
|
// add the user to the team
|
||||||
|
let teamMember: TeamMember;
|
||||||
|
try {
|
||||||
|
teamMember = await this.teamService.addMemberToTeam(
|
||||||
|
invitation.value.teamID,
|
||||||
|
acceptedBy.uid,
|
||||||
|
invitation.value.inviteeRole,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the invite
|
||||||
|
await this.revokeInvitation(inviteID);
|
||||||
|
|
||||||
|
return E.right(teamMember);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all team invitations for a given team.
|
||||||
|
* @param teamID team id
|
||||||
|
* @returns array of team invitations for a team
|
||||||
|
*/
|
||||||
|
async getTeamInvitations(teamID: string) {
|
||||||
|
const dbInvitations = await this.prisma.teamInvitation.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID: teamID,
|
teamID: teamID,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
|
||||||
|
this.cast(dbInvitation),
|
||||||
|
);
|
||||||
|
|
||||||
return invitations;
|
return invitations;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { pipe } from 'fp-ts/function';
|
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
TEAM_NOT_REQUIRED_ROLE,
|
TEAM_NOT_REQUIRED_ROLE,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { TeamMemberRole } from 'src/team/team.model';
|
import { TeamMemberRole } from 'src/team/team.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This guard only allows team owner to execute the resolver
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -24,48 +24,30 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
// Get user
|
||||||
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
|
|
||||||
// Get the invite
|
// Get the invite
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW((inviteID) =>
|
|
||||||
pipe(
|
|
||||||
this.teamInviteService.getInvitation(inviteID),
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
pipe(
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
gqlCtx.getContext().req.user,
|
|
||||||
O.fromNullable,
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.bindW('userMember', ({ invite, user }) =>
|
// Fetch team member details of this user
|
||||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
),
|
invitation.value.teamID,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
TE.chainW(
|
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||||
TE.fromPredicate(
|
if (teamMember.role !== TeamMemberRole.OWNER)
|
||||||
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
|
throwErr(TEAM_NOT_REQUIRED_ROLE);
|
||||||
() => TEAM_NOT_REQUIRED_ROLE,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.fold(
|
return true;
|
||||||
(err) => throwErr(err),
|
|
||||||
() => T.of(true),
|
|
||||||
),
|
|
||||||
)();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe, flow } from 'fp-ts/function';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
TEAM_INVITE_NOT_VALID_VIEWER,
|
|
||||||
TEAM_INVITE_NO_INVITE_FOUND,
|
TEAM_INVITE_NO_INVITE_FOUND,
|
||||||
|
TEAM_MEMBER_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { TeamService } from 'src/team/team.service';
|
import { TeamService } from 'src/team/team.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This guard only allows user to execute the resolver
|
||||||
|
* 1. If user is invitee, allow
|
||||||
|
* 2. Or else, if user is team member, allow
|
||||||
|
*
|
||||||
|
* TLDR: Allow if user is invitee or team member
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamInviteViewerGuard implements CanActivate {
|
export class TeamInviteViewerGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -23,50 +26,32 @@ export class TeamInviteViewerGuard implements CanActivate {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
// Get GQL Context
|
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
pipe(
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Get the invite
|
// Get the invite
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW(
|
|
||||||
flow(
|
|
||||||
this.teamInviteService.getInvitation,
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Check if the user and the invite email match, else if we can resolver the user as a team member
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
// any better solution ?
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
TE.chainW(({ user, invite }) =>
|
|
||||||
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
|
|
||||||
? TE.of(true)
|
|
||||||
: pipe(
|
|
||||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
|
||||||
TE.map(() => true),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
TE.mapLeft((e) =>
|
// Check if the user and the invite email match, else if user is a team member
|
||||||
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
|
if (
|
||||||
),
|
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||||
|
) {
|
||||||
|
const teamMember = await this.teamService.getTeamMember(
|
||||||
|
invitation.value.teamID,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
TE.fold(throwErr, () => T.of(true)),
|
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||||
)();
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { TeamInvitationService } from './team-invitation.service';
|
import { TeamInvitationService } from './team-invitation.service';
|
||||||
import { pipe, flow } from 'fp-ts/function';
|
|
||||||
import * as O from 'fp-ts/Option';
|
import * as O from 'fp-ts/Option';
|
||||||
import * as T from 'fp-ts/Task';
|
|
||||||
import * as TE from 'fp-ts/TaskEither';
|
|
||||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||||
import { User } from 'src/user/user.model';
|
|
||||||
import {
|
import {
|
||||||
BUG_AUTH_NO_USER_CTX,
|
BUG_AUTH_NO_USER_CTX,
|
||||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||||
@@ -24,44 +20,26 @@ export class TeamInviteeGuard implements CanActivate {
|
|||||||
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
return pipe(
|
// Get GQL Context
|
||||||
TE.Do,
|
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||||
|
|
||||||
// Get execution context
|
|
||||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
TE.bindW('user', ({ gqlCtx }) =>
|
const { user } = gqlExecCtx.getContext().req;
|
||||||
pipe(
|
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
|
||||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Get invite
|
// Get the invite
|
||||||
TE.bindW('invite', ({ gqlCtx }) =>
|
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||||
pipe(
|
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
|
||||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
|
||||||
TE.chainW(
|
|
||||||
flow(
|
|
||||||
this.teamInviteService.getInvitation,
|
|
||||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Check if the emails match
|
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||||
TE.chainW(
|
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||||
TE.fromPredicate(
|
|
||||||
({ user, invite }) => user.email === invite.inviteeEmail,
|
|
||||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Fold it to a promise
|
if (
|
||||||
TE.fold(throwErr, () => T.of(true)),
|
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||||
)();
|
) {
|
||||||
|
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
|
|||||||
complexity: 10,
|
complexity: 10,
|
||||||
})
|
})
|
||||||
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
||||||
return this.teamInviteService.getAllInvitationsInTeam(team)();
|
return this.teamInviteService.getTeamInvitations(team.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
** Custom interface to handle errors specific to Auth module
|
** Custom interface to handle errors specific to Auth module
|
||||||
** Since its REST we need to return HTTP status code along with error message
|
** Since its REST we need to return the HTTP status code along with the error message
|
||||||
*/
|
*/
|
||||||
export type AuthError = {
|
export type AuthError = {
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ beforeEach(() => {
|
|||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
describe('UserHistoryService', () => {
|
describe('UserHistoryService', () => {
|
||||||
describe('fetchUserHistory', () => {
|
describe('fetchUserHistory', () => {
|
||||||
test('Should return a list of users REST history if exists', async () => {
|
test('Should return a list of users REST history if exists', async () => {
|
||||||
@@ -400,7 +402,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn: date,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -410,7 +412,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn: date,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,13 @@ import * as E from 'fp-ts/Either';
|
|||||||
import * as A from 'fp-ts/Array';
|
import * as A from 'fp-ts/Array';
|
||||||
import { TeamMemberRole } from './team/team.model';
|
import { TeamMemberRole } from './team/team.model';
|
||||||
import { User } from './user/user.model';
|
import { User } from './user/user.model';
|
||||||
import { JSON_INVALID } from './errors';
|
import {
|
||||||
|
ENV_EMPTY_AUTH_PROVIDERS,
|
||||||
|
ENV_NOT_FOUND_KEY_AUTH_PROVIDERS,
|
||||||
|
ENV_NOT_SUPPORT_AUTH_PROVIDERS,
|
||||||
|
JSON_INVALID,
|
||||||
|
} from './errors';
|
||||||
|
import { AuthProvider } from './auth/helper';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A workaround to throw an exception in an expression.
|
* A workaround to throw an exception in an expression.
|
||||||
@@ -152,3 +158,31 @@ export function isValidLength(title: string, length: number) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called by bootstrap() in main.ts
|
||||||
|
* It checks if the "VITE_ALLOWED_AUTH_PROVIDERS" environment variable is properly set or not.
|
||||||
|
* If not, it throws an error.
|
||||||
|
*/
|
||||||
|
export function checkEnvironmentAuthProvider() {
|
||||||
|
if (!process.env.hasOwnProperty('VITE_ALLOWED_AUTH_PROVIDERS')) {
|
||||||
|
throw new Error(ENV_NOT_FOUND_KEY_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.VITE_ALLOWED_AUTH_PROVIDERS === '') {
|
||||||
|
throw new Error(ENV_EMPTY_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const givenAuthProviders = process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(
|
||||||
|
',',
|
||||||
|
).map((provider) => provider.toLocaleUpperCase());
|
||||||
|
const supportedAuthProviders = Object.values(AuthProvider).map(
|
||||||
|
(provider: string) => provider.toLocaleUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const givenAuthProvider of givenAuthProviders) {
|
||||||
|
if (!supportedAuthProviders.includes(givenAuthProvider)) {
|
||||||
|
throw new Error(ENV_NOT_SUPPORT_AUTH_PROVIDERS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
# 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
|
|
||||||
support@hoppscotch.io.
|
|
||||||
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.
|
|
||||||
|
|
||||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
||||||
enforcement ladder](https://github.com/mozilla/diversity).
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
|
|
||||||
For answers to common questions about this code of conduct, see the FAQ at
|
|
||||||
https://www.contributor-covenant.org/faq. Translations are available at
|
|
||||||
https://www.contributor-covenant.org/translations.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -1,29 +1,19 @@
|
|||||||
<div align="center">
|
|
||||||
<a href="https://hoppscotch.io">
|
|
||||||
<img
|
|
||||||
src="https://avatars.githubusercontent.com/u/56705483"
|
|
||||||
alt="Hoppscotch Logo"
|
|
||||||
height="64"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
# Hoppscotch CLI <font size=2><sup>ALPHA</sup></font>
|
||||||
|
|
||||||
</div>
|
A CLI to run Hoppscotch Test Scripts in CI environments.
|
||||||
|
|
||||||
A CLI to run Hoppscotch test scripts in CI environments.
|
|
||||||
|
|
||||||
### **Commands:**
|
### **Commands:**
|
||||||
|
|
||||||
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
- `hopp test [options] [file]`: testing hoppscotch collection.json file
|
||||||
|
|
||||||
### **Usage:**
|
### **Usage:**
|
||||||
```
|
|
||||||
|
```bash
|
||||||
hopp [options or commands] arguments
|
hopp [options or commands] arguments
|
||||||
```
|
```
|
||||||
|
|
||||||
### **Options:**
|
### **Options:**
|
||||||
|
|
||||||
- `-v`, `--ver`: see the current version of the CLI
|
- `-v`, `--ver`: see the current version of the CLI
|
||||||
- `-h`, `--help`: display help for command
|
- `-h`, `--help`: display help for command
|
||||||
|
|
||||||
@@ -45,14 +35,18 @@ hopp [options or commands] arguments
|
|||||||
- Executes and outputs test-script response.
|
- Executes and outputs test-script response.
|
||||||
|
|
||||||
#### Options:
|
#### Options:
|
||||||
|
|
||||||
##### `-e <file_path>` / `--env <file_path>`
|
##### `-e <file_path>` / `--env <file_path>`
|
||||||
|
|
||||||
- Accepts path to env.json with contents in below format:
|
- Accepts path to env.json with contents in below format:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"ENV1":"value1",
|
"ENV1":"value1",
|
||||||
"ENV2":"value2"
|
"ENV2":"value2"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- You can now access those variables using `pw.env.get('<var_name>')`
|
- You can now access those variables using `pw.env.get('<var_name>')`
|
||||||
|
|
||||||
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
Taking the above example, `pw.env.get("ENV1")` will return `"value1"`
|
||||||
@@ -75,4 +69,59 @@ npm i -g @hoppscotch/cli
|
|||||||
|
|
||||||
## **Contributing:**
|
## **Contributing:**
|
||||||
|
|
||||||
To get started contributing to the repository, please read **[CONTRIBUTING.md](./CONTRIBUTING.md)**
|
When contributing to this repository, please first discuss the change you wish to make via issue,
|
||||||
|
email, or any other method with the owners of this repository before making a change.
|
||||||
|
|
||||||
|
Please note we have a code of conduct, please follow it in all your interactions with the project.
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. Ensure any install or build dependencies are removed before the end of the layer when doing a
|
||||||
|
build.
|
||||||
|
2. Update the README.md with details of changes to the interface, this includes new environment
|
||||||
|
variables, exposed ports, useful file locations and container parameters.
|
||||||
|
3. Increase the version numbers in any examples files and the README.md to the new version that this
|
||||||
|
Pull Request would represent. The versioning scheme we use is [SemVer](https://semver.org).
|
||||||
|
4. You may merge the Pull Request once you have the sign-off of two other developers, or if you
|
||||||
|
do not have permission to do that, you may request the second reviewer merge it for you.
|
||||||
|
|
||||||
|
## Set Up The Development Environment
|
||||||
|
|
||||||
|
1. After cloning the repository, execute the following commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In order to test locally, you can use two types of package linking:
|
||||||
|
|
||||||
|
1. The 'pnpm exec' way (preferred since it does not hamper your original installation of the CLI):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm link @hoppscotch/cli
|
||||||
|
|
||||||
|
// Then to use or test the CLI:
|
||||||
|
pnpm exec hopp
|
||||||
|
|
||||||
|
// After testing, to remove the package linking:
|
||||||
|
pnpm rm @hoppscotch/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
2. The 'global' way (warning: this might override the globally installed CLI, if exists):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pnpm link --global
|
||||||
|
|
||||||
|
// Then to use or test the CLI:
|
||||||
|
hopp
|
||||||
|
|
||||||
|
// After testing, to remove the package linking:
|
||||||
|
sudo pnpm rm --global @hoppscotch/cli
|
||||||
|
```
|
||||||
|
|
||||||
|
3. To use the Typescript watch scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
jest: true,
|
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
@@ -30,8 +29,18 @@ module.exports = {
|
|||||||
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
"import/named": "off", // because, named import issue with typescript see: https://github.com/typescript-eslint/typescript-eslint/issues/154
|
||||||
"no-console": "off",
|
"no-console": "off",
|
||||||
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
"no-debugger": process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||||
"prettier/prettier":
|
"prettier/prettier": [
|
||||||
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
process.env.HOPP_LINT_FOR_PROD === "true" ? "error" : "warn",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
semi: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 80,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
"vue/multi-word-component-names": "off",
|
"vue/multi-word-component-names": "off",
|
||||||
"vue/no-side-effects-in-computed-properties": "off",
|
"vue/no-side-effects-in-computed-properties": "off",
|
||||||
"import/no-named-as-default": "off",
|
"import/no-named-as-default": "off",
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
semi: false
|
semi: false,
|
||||||
|
trailingComma: "es5",
|
||||||
|
singleQuote: false,
|
||||||
|
printWidth: 80,
|
||||||
|
useTabs: false,
|
||||||
|
tabWidth: 2
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/hoppscotch-common/assets/icons/star-off.svg
Normal file
1
packages/hoppscotch-common/assets/icons/star-off.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||||
|
After Width: | Height: | Size: 337 B |
@@ -4,6 +4,7 @@
|
|||||||
@apply after:backface-hidden;
|
@apply after:backface-hidden;
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
|
@apply overscroll-none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@@ -165,12 +166,6 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.env-icon {
|
|
||||||
@apply transition;
|
|
||||||
@apply inline-flex;
|
|
||||||
@apply items-center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -189,10 +184,11 @@ a {
|
|||||||
@apply border-solid border-dividerDark;
|
@apply border-solid border-dividerDark;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply shadow-lg;
|
@apply shadow-lg;
|
||||||
|
@apply max-w-[45vw] #{!important};
|
||||||
|
|
||||||
.tippy-content {
|
.tippy-content {
|
||||||
@apply flex flex-col;
|
@apply flex flex-col;
|
||||||
@apply max-h-56;
|
@apply max-h-[45vh];
|
||||||
@apply items-stretch;
|
@apply items-stretch;
|
||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply text-secondary text-body;
|
@apply text-secondary text-body;
|
||||||
@@ -200,6 +196,10 @@ a {
|
|||||||
@apply leading-normal;
|
@apply leading-normal;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
@apply block #{!important};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -215,6 +215,7 @@ a {
|
|||||||
|
|
||||||
[data-v-tippy] {
|
[data-v-tippy] {
|
||||||
@apply flex flex-1;
|
@apply flex flex-1;
|
||||||
|
@apply truncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
[interactive] > div {
|
[interactive] > div {
|
||||||
@@ -325,7 +326,7 @@ pre.ace_editor {
|
|||||||
@apply after:font-icon;
|
@apply after:font-icon;
|
||||||
@apply after:text-current;
|
@apply after:text-current;
|
||||||
@apply after:right-3;
|
@apply after:right-3;
|
||||||
@apply after:content-["\e313"];
|
@apply after:content-["\e5cf"];
|
||||||
@apply after:text-lg;
|
@apply after:text-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,6 +481,10 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-scroller {
|
||||||
|
@apply overscroll-y-auto;
|
||||||
|
}
|
||||||
|
|
||||||
.cm-editor {
|
.cm-editor {
|
||||||
.cm-line::selection {
|
.cm-line::selection {
|
||||||
@apply bg-accentDark #{!important};
|
@apply bg-accentDark #{!important};
|
||||||
@@ -567,3 +572,11 @@ details[open] summary .indicator {
|
|||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply border-0;
|
@apply border-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gql-operation-not-highlight {
|
||||||
|
@apply opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gql-operation-highlight {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@mixin base-theme {
|
@mixin base-theme {
|
||||||
--font-sans: "Inter", sans-serif;
|
--font-sans: "Inter Variable", sans-serif;
|
||||||
--font-mono: "Roboto Mono", monospace;
|
--font-icon: "Material Symbols Rounded Variable";
|
||||||
--font-icon: "Material Icons";
|
--font-mono: "Roboto Mono Variable", monospace;
|
||||||
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
--font-size-tiny: calc(var(--font-size-body) - 0.062rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Reaksie liggaam",
|
"body": "Reaksie liggaam",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Opskrifte",
|
"headers": "Opskrifte",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Tyd",
|
"time": "Tyd",
|
||||||
"title": "Reaksie",
|
"title": "Reaksie",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "wag vir verbinding",
|
"waiting_for_connection": "wag vir verbinding",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "هيئة الاستجابة",
|
"body": "هيئة الاستجابة",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "الرؤوس",
|
"headers": "الرؤوس",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "حالة",
|
"status": "حالة",
|
||||||
"time": "وقت",
|
"time": "وقت",
|
||||||
"title": "إجابة",
|
"title": "إجابة",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "في انتظار الاتصال",
|
"waiting_for_connection": "في انتظار الاتصال",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "Visualitzar els meus enllaços"
|
"view_my_links": "Visualitzar els meus enllaços"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Cos de resposta",
|
"body": "Cos de resposta",
|
||||||
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
|
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
|
||||||
"headers": "Capçaleres",
|
"headers": "Capçaleres",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Estat",
|
"status": "Estat",
|
||||||
"time": "Temps",
|
"time": "Temps",
|
||||||
"title": "Resposta",
|
"title": "Resposta",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "esperant la connexió",
|
"waiting_for_connection": "esperant la connexió",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "查看我的链接"
|
"view_my_links": "查看我的链接"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "响应体",
|
"body": "响应体",
|
||||||
"filter_response_body": "筛选JSON响应本体(使用JSONPath语法)",
|
"filter_response_body": "筛选JSON响应本体(使用JSONPath语法)",
|
||||||
"headers": "响应头",
|
"headers": "响应头",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "状态",
|
"status": "状态",
|
||||||
"time": "时间",
|
"time": "时间",
|
||||||
"title": "响应",
|
"title": "响应",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "等待连接",
|
"waiting_for_connection": "等待连接",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Odpovědní orgán",
|
"body": "Odpovědní orgán",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Záhlaví",
|
"headers": "Záhlaví",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Postavení",
|
"status": "Postavení",
|
||||||
"time": "Čas",
|
"time": "Čas",
|
||||||
"title": "Odezva",
|
"title": "Odezva",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "čekání na připojení",
|
"waiting_for_connection": "čekání na připojení",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Svarorgan",
|
"body": "Svarorgan",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Overskrifter",
|
"headers": "Overskrifter",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Tid",
|
"time": "Tid",
|
||||||
"title": "Respons",
|
"title": "Respons",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "venter på forbindelse",
|
"waiting_for_connection": "venter på forbindelse",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Antworttext",
|
"body": "Antworttext",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Header",
|
"headers": "Header",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Zeit",
|
"time": "Zeit",
|
||||||
"title": "Antwort",
|
"title": "Antwort",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "auf Verbindung warten",
|
"waiting_for_connection": "auf Verbindung warten",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "Προβολή των links μου"
|
"view_my_links": "Προβολή των links μου"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Σώμα απόκρισης",
|
"body": "Σώμα απόκρισης",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Κεφαλίδες",
|
"headers": "Κεφαλίδες",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Κατάσταση",
|
"status": "Κατάσταση",
|
||||||
"time": "χρόνος",
|
"time": "χρόνος",
|
||||||
"title": "Απάντηση",
|
"title": "Απάντηση",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "περιμένοντας τη σύνδεση",
|
"waiting_for_connection": "περιμένοντας τη σύνδεση",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"choose_file": "Choose a file",
|
"choose_file": "Choose a file",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
|
"clear_history": "Clear All History",
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Open workspace",
|
||||||
"paste": "Paste",
|
"paste": "Paste",
|
||||||
"prettify": "Prettify",
|
"prettify": "Prettify",
|
||||||
|
"rename": "Rename",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
@@ -67,6 +69,8 @@
|
|||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
"invite_description": "Hoppscotch is an open source API development ecosystem. We designed a simple and intuitive interface for creating and managing your APIs. Hoppscotch is a tool that helps you build, test, document and share your APIs.",
|
||||||
"invite_your_friends": "Invite your friends",
|
"invite_your_friends": "Invite your friends",
|
||||||
|
"social_links": "Social links",
|
||||||
|
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||||
"join_discord_community": "Join our Discord community",
|
"join_discord_community": "Join our Discord community",
|
||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
@@ -131,6 +135,7 @@
|
|||||||
"renamed": "Collection renamed",
|
"renamed": "Collection renamed",
|
||||||
"request_in_use": "Request in use",
|
"request_in_use": "Request in use",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
|
"save_to_collection": "Save to Collection",
|
||||||
"select": "Select a Collection",
|
"select": "Select a Collection",
|
||||||
"select_location": "Select location",
|
"select_location": "Select location",
|
||||||
"select_team": "Select a team",
|
"select_team": "Select a team",
|
||||||
@@ -148,8 +153,15 @@
|
|||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||||
|
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||||
|
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||||
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
"sync": "Would you like to restore your workspace from cloud? This will discard your local progress."
|
||||||
},
|
},
|
||||||
|
"context_menu": {
|
||||||
|
"set_environment_variable": "Set as variable",
|
||||||
|
"add_parameters": "Add to parameters",
|
||||||
|
"open_request_in_new_tab": "Open request in new tab"
|
||||||
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"header": "Header {count}",
|
"header": "Header {count}",
|
||||||
"message": "Message {count}",
|
"message": "Message {count}",
|
||||||
@@ -192,17 +204,31 @@
|
|||||||
"create_new": "Create new environment",
|
"create_new": "Create new environment",
|
||||||
"created": "Environment created",
|
"created": "Environment created",
|
||||||
"deleted": "Environment deletion",
|
"deleted": "Environment deletion",
|
||||||
|
"duplicated": "Environment duplicated",
|
||||||
"edit": "Edit Environment",
|
"edit": "Edit Environment",
|
||||||
|
"global": "Global",
|
||||||
|
"empty_variables": "No variables",
|
||||||
|
"global_variables": "Global variables",
|
||||||
"invalid_name": "Please provide a name for the environment",
|
"invalid_name": "Please provide a name for the environment",
|
||||||
|
"list": "Environment variables",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "My Environments",
|
||||||
|
"name": "Name",
|
||||||
"nested_overflow": "nested environment variables are limited to 10 levels",
|
"nested_overflow": "nested environment variables are limited to 10 levels",
|
||||||
"new": "New Environment",
|
"new": "New Environment",
|
||||||
|
"no_active_environment": "No active environment",
|
||||||
"no_environment": "No environment",
|
"no_environment": "No environment",
|
||||||
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
"no_environment_description": "No environments were selected. Choose what to do with the following variables.",
|
||||||
|
"quick_peek": "Environment Quick Peek",
|
||||||
|
"replace_with_variable": "Replace with variable",
|
||||||
|
"scope": "Scope",
|
||||||
"select": "Select environment",
|
"select": "Select environment",
|
||||||
|
"set": "Set environment",
|
||||||
|
"set_as_environment": "Set as environment",
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Team Environments",
|
||||||
"title": "Environments",
|
"title": "Environments",
|
||||||
"updated": "Environment updated",
|
"updated": "Environment updated",
|
||||||
|
"value": "Value",
|
||||||
|
"variable": "Variable",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -226,6 +252,7 @@
|
|||||||
"no_duration": "No duration",
|
"no_duration": "No duration",
|
||||||
"no_results_found": "No matches found",
|
"no_results_found": "No matches found",
|
||||||
"page_not_found": "This page could not be found",
|
"page_not_found": "This page could not be found",
|
||||||
|
"proxy_error": "Proxy error",
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
"test_script_fail": "Could not execute post-request script"
|
"test_script_fail": "Could not execute post-request script"
|
||||||
@@ -253,6 +280,10 @@
|
|||||||
"graphql": {
|
"graphql": {
|
||||||
"mutations": "Mutations",
|
"mutations": "Mutations",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
|
"switch_connection": "Switch connection",
|
||||||
|
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||||
|
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||||
|
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||||
"subscriptions": "Subscriptions"
|
"subscriptions": "Subscriptions"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
@@ -282,6 +313,30 @@
|
|||||||
"preview": "Hide Preview",
|
"preview": "Hide Preview",
|
||||||
"sidebar": "Collapse sidebar"
|
"sidebar": "Collapse sidebar"
|
||||||
},
|
},
|
||||||
|
"inspections": {
|
||||||
|
"title": "Inspector",
|
||||||
|
"description": "Inspect possible errors",
|
||||||
|
"environment": {
|
||||||
|
"add_environment": "Add to Environment",
|
||||||
|
"not_found": "Environment variable “{environment}” not found."
|
||||||
|
},
|
||||||
|
"header": {
|
||||||
|
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"401_error": "Please check your authentication credentials.",
|
||||||
|
"404_error": "Please check your request URL and method type.",
|
||||||
|
"network_error": "Please check your network connection.",
|
||||||
|
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||||
|
"default_error": "Please check your request."
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"extension_not_installed": "Extension not installed.",
|
||||||
|
"extention_not_enabled": "Extension not enabled.",
|
||||||
|
"extention_enable_action": "Enable Browser Extension",
|
||||||
|
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list."
|
||||||
|
}
|
||||||
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "Import collections",
|
"collections": "Import collections",
|
||||||
"curl": "Import cURL",
|
"curl": "Import cURL",
|
||||||
@@ -418,8 +473,10 @@
|
|||||||
"payload": "Payload",
|
"payload": "Payload",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"raw_body": "Raw Request Body",
|
"raw_body": "Raw Request Body",
|
||||||
|
"rename": "Rename Request",
|
||||||
"renamed": "Request renamed",
|
"renamed": "Request renamed",
|
||||||
"run": "Run",
|
"run": "Run",
|
||||||
|
"stop": "Stop",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_as": "Save as",
|
"save_as": "Save as",
|
||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
@@ -459,9 +516,9 @@
|
|||||||
"account_name_description": "This is your display name.",
|
"account_name_description": "This is your display name.",
|
||||||
"background": "Background",
|
"background": "Background",
|
||||||
"black_mode": "Black",
|
"black_mode": "Black",
|
||||||
|
"dark_mode": "Dark",
|
||||||
"change_font_size": "Change font size",
|
"change_font_size": "Change font size",
|
||||||
"choose_language": "Choose language",
|
"choose_language": "Choose language",
|
||||||
"dark_mode": "Dark",
|
|
||||||
"delete_account": "Delete account",
|
"delete_account": "Delete account",
|
||||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||||
"expand_navigation": "Expand navigation",
|
"expand_navigation": "Expand navigation",
|
||||||
@@ -525,6 +582,10 @@
|
|||||||
"show_all": "Keyboard shortcuts",
|
"show_all": "Keyboard shortcuts",
|
||||||
"title": "General"
|
"title": "General"
|
||||||
},
|
},
|
||||||
|
"others": {
|
||||||
|
"title": "Others",
|
||||||
|
"prettify": "Prettify Editor's Content"
|
||||||
|
},
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Invite people to Hoppscotch",
|
"invite": "Invite people to Hoppscotch",
|
||||||
"title": "Miscellaneous"
|
"title": "Miscellaneous"
|
||||||
@@ -545,6 +606,9 @@
|
|||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
|
"rename": "Rename Request",
|
||||||
|
"import_curl": "Import cURL",
|
||||||
|
"show_code": "Generate code snippet",
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
"next_method": "Select Next method",
|
"next_method": "Select Next method",
|
||||||
"post_method": "Select POST method",
|
"post_method": "Select POST method",
|
||||||
@@ -553,6 +617,7 @@
|
|||||||
"reset_request": "Reset Request",
|
"reset_request": "Reset Request",
|
||||||
"save_to_collections": "Save to Collections",
|
"save_to_collections": "Save to Collections",
|
||||||
"send_request": "Send Request",
|
"send_request": "Send Request",
|
||||||
|
"save_request": "Save Request",
|
||||||
"title": "Request"
|
"title": "Request"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -561,10 +626,10 @@
|
|||||||
"title": "Response"
|
"title": "Response"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Switch theme to black mode",
|
"black": "Switch theme to Black Mode",
|
||||||
"dark": "Switch theme to dark mode",
|
"dark": "Switch theme to Dark Mode",
|
||||||
"light": "Switch theme to light mode",
|
"light": "Switch theme to Light Mode",
|
||||||
"system": "Switch theme to system mode",
|
"system": "Switch theme to System Mode",
|
||||||
"title": "Theme"
|
"title": "Theme"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -582,6 +647,90 @@
|
|||||||
"log": "Log",
|
"log": "Log",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
|
"spotlight": {
|
||||||
|
"general": {
|
||||||
|
"help_menu": "Help and support",
|
||||||
|
"chat": "Chat with support",
|
||||||
|
"open_docs": "Read Documentation",
|
||||||
|
"open_keybindings": "Keyboard shortcuts",
|
||||||
|
"open_github": "Open GitHub repository",
|
||||||
|
"social": "Social",
|
||||||
|
"title": "General"
|
||||||
|
},
|
||||||
|
"miscellaneous": {
|
||||||
|
"invite": "Invite your friends to Hoppscotch",
|
||||||
|
"title": "Miscellaneous"
|
||||||
|
},
|
||||||
|
"request": {
|
||||||
|
"switch_to": "Switch to",
|
||||||
|
"select_method": "Select method",
|
||||||
|
"save_as_new": "Save as new request",
|
||||||
|
"tab_parameters": "Parameters tab",
|
||||||
|
"tab_body": "Body tab",
|
||||||
|
"tab_headers": "Headers tab",
|
||||||
|
"tab_authorization": "Authorization tab",
|
||||||
|
"tab_pre_request_script": "Pre-request script tab",
|
||||||
|
"tab_tests": "Tests tab",
|
||||||
|
"tab_query": "Query tab",
|
||||||
|
"tab_variables": "Variables tab"
|
||||||
|
},
|
||||||
|
"graphql": {
|
||||||
|
"connect": "Connect to server",
|
||||||
|
"disconnect": "Disconnect from server"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"copy": "Copy response",
|
||||||
|
"download": "Download response as file",
|
||||||
|
"title": "Response"
|
||||||
|
},
|
||||||
|
"environments": {
|
||||||
|
"new": "Create new environment",
|
||||||
|
"new_variable": "Create a new environment variable",
|
||||||
|
"edit": "Edit current environment",
|
||||||
|
"delete": "Delete current environment",
|
||||||
|
"duplicate": "Duplicate current environment",
|
||||||
|
"edit_global": "Edit global environment",
|
||||||
|
"duplicate_global": "Duplicate global environment",
|
||||||
|
"title": "Environments"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"new": "Create new team",
|
||||||
|
"edit": "Edit current team",
|
||||||
|
"delete": "Delete current team",
|
||||||
|
"invite": "Invite people to team",
|
||||||
|
"switch_to_personal": "Switch to your personal workspace",
|
||||||
|
"title": "Teams"
|
||||||
|
},
|
||||||
|
"tab": {
|
||||||
|
"duplicate": "Duplicate current tab",
|
||||||
|
"close_current": "Close current tab",
|
||||||
|
"close_others": "Close all other tabs",
|
||||||
|
"new_tab": "Open a new tab",
|
||||||
|
"title": "Tabs"
|
||||||
|
},
|
||||||
|
"section": {
|
||||||
|
"user": "User",
|
||||||
|
"theme": "Theme",
|
||||||
|
"interface": "Interface",
|
||||||
|
"interceptor": "Interceptor"
|
||||||
|
},
|
||||||
|
"change_language": "Change Language",
|
||||||
|
"settings": {
|
||||||
|
"theme": {
|
||||||
|
"black": "Black",
|
||||||
|
"dark": "Dark",
|
||||||
|
"light": "Light",
|
||||||
|
"system": "System preference"
|
||||||
|
},
|
||||||
|
"font": {
|
||||||
|
"size_sm": "Small",
|
||||||
|
"size_md": "Medium",
|
||||||
|
"size_lg": "Large"
|
||||||
|
},
|
||||||
|
"change_interceptor": "Change Interceptor",
|
||||||
|
"change_language": "Change Language"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sse": {
|
"sse": {
|
||||||
"event_type": "Event type",
|
"event_type": "Event type",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
@@ -639,8 +788,11 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "Authorization",
|
"authorization": "Authorization",
|
||||||
"body": "Body",
|
"body": "Body",
|
||||||
|
"close": "Close Tab",
|
||||||
|
"close_others": "Close other Tabs",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
|
"duplicate": "Duplicate Tab",
|
||||||
"environments": "Environments",
|
"environments": "Environments",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "Ver mis enlaces"
|
"view_my_links": "Ver mis enlaces"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Cuerpo de respuesta",
|
"body": "Cuerpo de respuesta",
|
||||||
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
|
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
|
||||||
"headers": "Encabezados",
|
"headers": "Encabezados",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Estado",
|
"status": "Estado",
|
||||||
"time": "Tiempo",
|
"time": "Tiempo",
|
||||||
"title": "Respuesta",
|
"title": "Respuesta",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "esperando la conexión",
|
"waiting_for_connection": "esperando la conexión",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Vastauselin",
|
"body": "Vastauselin",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Otsikot",
|
"headers": "Otsikot",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Tila",
|
"status": "Tila",
|
||||||
"time": "Aika",
|
"time": "Aika",
|
||||||
"title": "Vastaus",
|
"title": "Vastaus",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "yhteyttä odotellessa",
|
"waiting_for_connection": "yhteyttä odotellessa",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "Voir mes liens"
|
"view_my_links": "Voir mes liens"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Corps de réponse",
|
"body": "Corps de réponse",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "En-têtes",
|
"headers": "En-têtes",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Statut",
|
"status": "Statut",
|
||||||
"time": "Temps",
|
"time": "Temps",
|
||||||
"title": "Réponse",
|
"title": "Réponse",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "En attente de connexion",
|
"waiting_for_connection": "En attente de connexion",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "גוף תגובה",
|
"body": "גוף תגובה",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "כותרות",
|
"headers": "כותרות",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "סטָטוּס",
|
"status": "סטָטוּס",
|
||||||
"time": "זְמַן",
|
"time": "זְמַן",
|
||||||
"title": "תְגוּבָה",
|
"title": "תְגוּבָה",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "מחכה לחיבור",
|
"waiting_for_connection": "מחכה לחיבור",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -433,6 +433,7 @@
|
|||||||
"view_my_links": "मेरे लिंक देखें"
|
"view_my_links": "मेरे लिंक देखें"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "प्रतिक्रिया निकाय",
|
"body": "प्रतिक्रिया निकाय",
|
||||||
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
|
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
|
||||||
"headers": "हेडर",
|
"headers": "हेडर",
|
||||||
@@ -446,6 +447,7 @@
|
|||||||
"status": "दर्जा",
|
"status": "दर्जा",
|
||||||
"time": "समय",
|
"time": "समय",
|
||||||
"title": "जवाब",
|
"title": "जवाब",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "जुडने के लिए इंतजार",
|
"waiting_for_connection": "जुडने के लिए इंतजार",
|
||||||
"xml": "एक्सएमएल"
|
"xml": "एक्सएमएल"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,29 +5,29 @@
|
|||||||
"choose_file": "Válasszon egy fájlt",
|
"choose_file": "Válasszon egy fájlt",
|
||||||
"clear": "Törlés",
|
"clear": "Törlés",
|
||||||
"clear_all": "Összes törlése",
|
"clear_all": "Összes törlése",
|
||||||
"close": "Close",
|
"close": "Bezárás",
|
||||||
"connect": "Kapcsolódás",
|
"connect": "Kapcsolódás",
|
||||||
"connecting": "Connecting",
|
"connecting": "Kapcsolódás",
|
||||||
"copy": "Másolás",
|
"copy": "Másolás",
|
||||||
"delete": "Törlés",
|
"delete": "Törlés",
|
||||||
"disconnect": "Leválasztás",
|
"disconnect": "Leválasztás",
|
||||||
"dismiss": "Eltüntetés",
|
"dismiss": "Eltüntetés",
|
||||||
"dont_save": "Ne mentse",
|
"dont_save": "Ne mentse",
|
||||||
"download_file": "Fájl letöltése",
|
"download_file": "Fájl letöltése",
|
||||||
"drag_to_reorder": "Drag to reorder",
|
"drag_to_reorder": "Húzza az átrendezéshez",
|
||||||
"duplicate": "Kettőzés",
|
"duplicate": "Kettőzés",
|
||||||
"edit": "Szerkesztés",
|
"edit": "Szerkesztés",
|
||||||
"filter": "Filter",
|
"filter": "Szűrő",
|
||||||
"go_back": "Vissza",
|
"go_back": "Vissza",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "Előre",
|
||||||
"group_by": "Group by",
|
"group_by": "Csoportosítás",
|
||||||
"label": "Címke",
|
"label": "Címke",
|
||||||
"learn_more": "Tudjon meg többet",
|
"learn_more": "Tudjon meg többet",
|
||||||
"less": "Kevesebb",
|
"less": "Kevesebb",
|
||||||
"more": "Több",
|
"more": "Több",
|
||||||
"new": "Új",
|
"new": "Új",
|
||||||
"no": "Nem",
|
"no": "Nem",
|
||||||
"open_workspace": "Open workspace",
|
"open_workspace": "Munkaterület megnyitása",
|
||||||
"paste": "Beillesztés",
|
"paste": "Beillesztés",
|
||||||
"prettify": "Csinosítás",
|
"prettify": "Csinosítás",
|
||||||
"remove": "Eltávolítás",
|
"remove": "Eltávolítás",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"search": "Keresés",
|
"search": "Keresés",
|
||||||
"send": "Küldés",
|
"send": "Küldés",
|
||||||
"start": "Indítás",
|
"start": "Indítás",
|
||||||
"starting": "Starting",
|
"starting": "Indítás",
|
||||||
"stop": "Leállítás",
|
"stop": "Leállítás",
|
||||||
"to_close": "a bezáráshoz",
|
"to_close": "a bezáráshoz",
|
||||||
"to_navigate": "a navigáláshoz",
|
"to_navigate": "a navigáláshoz",
|
||||||
@@ -118,16 +118,16 @@
|
|||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "Gyűjtemény létrehozva",
|
"created": "Gyűjtemény létrehozva",
|
||||||
"different_parent": "Cannot reorder collection with different parent",
|
"different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt",
|
||||||
"edit": "Gyűjtemény szerkesztése",
|
"edit": "Gyűjtemény szerkesztése",
|
||||||
"invalid_name": "Adjon nevet a gyűjteménynek",
|
"invalid_name": "Adjon nevet a gyűjteménynek",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "A gyűjtemény már a gyökérben van",
|
||||||
"moved": "Moved Successfully",
|
"moved": "Sikeresen áthelyezve",
|
||||||
"my_collections": "Saját gyűjtemények",
|
"my_collections": "Saját gyűjtemények",
|
||||||
"name": "Saját új gyűjtemény",
|
"name": "Saját új gyűjtemény",
|
||||||
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
|
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
|
||||||
"new": "Új gyűjtemény",
|
"new": "Új gyűjtemény",
|
||||||
"order_changed": "Collection Order Updated",
|
"order_changed": "Gyűjtemény sorrendje frissítve",
|
||||||
"renamed": "Gyűjtemény átnevezve",
|
"renamed": "Gyűjtemény átnevezve",
|
||||||
"request_in_use": "A kérés használatban",
|
"request_in_use": "A kérés használatban",
|
||||||
"save_as": "Mentés másként",
|
"save_as": "Mentés másként",
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
|
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
|
||||||
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
|
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
|
||||||
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
|
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "Szeretné menteni az ezen a lapon elvégzett változtatásokat?",
|
||||||
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
|
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
@@ -180,8 +180,8 @@
|
|||||||
"profile": "Jelentkezzen be a profilja megtekintéséhez",
|
"profile": "Jelentkezzen be a profilja megtekintéséhez",
|
||||||
"protocols": "A protokollok üresek",
|
"protocols": "A protokollok üresek",
|
||||||
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
|
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shortcodes": "A rövid kódok üresek",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "A feliratkozások üresek",
|
||||||
"team_name": "A csapat neve üres",
|
"team_name": "A csapat neve üres",
|
||||||
"teams": "Ön nem tartozik semmilyen csapathoz",
|
"teams": "Ön nem tartozik semmilyen csapathoz",
|
||||||
"tests": "Nincsenek tesztek ehhez a kéréshez"
|
"tests": "Nincsenek tesztek ehhez a kéréshez"
|
||||||
@@ -194,13 +194,13 @@
|
|||||||
"deleted": "Környezet törlése",
|
"deleted": "Környezet törlése",
|
||||||
"edit": "Környezet szerkesztése",
|
"edit": "Környezet szerkesztése",
|
||||||
"invalid_name": "Adjon nevet a környezetnek",
|
"invalid_name": "Adjon nevet a környezetnek",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "Saját környezetek",
|
||||||
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
|
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
|
||||||
"new": "Új környezet",
|
"new": "Új környezet",
|
||||||
"no_environment": "Nincs környezet",
|
"no_environment": "Nincs környezet",
|
||||||
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
|
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
|
||||||
"select": "Környezet kiválasztása",
|
"select": "Környezet kiválasztása",
|
||||||
"team_environments": "Team Environments",
|
"team_environments": "Csapatkörnyezetek",
|
||||||
"title": "Környezetek",
|
"title": "Környezetek",
|
||||||
"updated": "Környezet frissítve",
|
"updated": "Környezet frissítve",
|
||||||
"variable_list": "Változólista"
|
"variable_list": "Változólista"
|
||||||
@@ -209,9 +209,9 @@
|
|||||||
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
|
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
|
||||||
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
|
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
|
||||||
"curl_invalid_format": "A cURL nincs megfelelően formázva",
|
"curl_invalid_format": "A cURL nincs megfelelően formázva",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "Veszélyes zóna",
|
||||||
"delete_account": "Your account is currently an owner in these teams:",
|
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
|
||||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
"delete_account_description": "El kell távolítani magát, át kell adnia a tulajdonjogot vagy törölnie kell ezeket a csapatokat, mielőtt törölhetné a fiókját.",
|
||||||
"empty_req_name": "Üres kérésnév",
|
"empty_req_name": "Üres kérésnév",
|
||||||
"f12_details": "(F12 a részletekért)",
|
"f12_details": "(F12 a részletekért)",
|
||||||
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
|
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
|
||||||
@@ -219,13 +219,13 @@
|
|||||||
"incorrect_email": "Hibás e-mail",
|
"incorrect_email": "Hibás e-mail",
|
||||||
"invalid_link": "Érvénytelen hivatkozás",
|
"invalid_link": "Érvénytelen hivatkozás",
|
||||||
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
|
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
|
||||||
"json_parsing_failed": "Invalid JSON",
|
"json_parsing_failed": "Érvénytelen JSON",
|
||||||
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
|
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
|
||||||
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
|
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
|
||||||
"network_fail": "Nem sikerült elküldeni a kérést",
|
"network_fail": "Nem sikerült elküldeni a kérést",
|
||||||
"no_duration": "Nincs időtartam",
|
"no_duration": "Nincs időtartam",
|
||||||
"no_results_found": "No matches found",
|
"no_results_found": "Nincs találat",
|
||||||
"page_not_found": "This page could not be found",
|
"page_not_found": "Ez az oldal nem található",
|
||||||
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
|
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
|
||||||
"something_went_wrong": "Valami elromlott",
|
"something_went_wrong": "Valami elromlott",
|
||||||
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
|
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
|
||||||
@@ -238,9 +238,9 @@
|
|||||||
"title": "Exportálás"
|
"title": "Exportálás"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"all": "All",
|
"all": "Összes",
|
||||||
"none": "None",
|
"none": "Nincs",
|
||||||
"starred": "Starred"
|
"starred": "Csillagozott"
|
||||||
},
|
},
|
||||||
"folder": {
|
"folder": {
|
||||||
"created": "Mappa létrehozva",
|
"created": "Mappa létrehozva",
|
||||||
@@ -256,7 +256,7 @@
|
|||||||
"subscriptions": "Feliratkozások"
|
"subscriptions": "Feliratkozások"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"time": "Time",
|
"time": "Idő",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
@@ -316,32 +316,32 @@
|
|||||||
"zen_mode": "Zen mód"
|
"zen_mode": "Zen mód"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "Elmentetlen változtatásai vannak",
|
||||||
"collections": "Gyűjtemények",
|
"collections": "Gyűjtemények",
|
||||||
"confirm": "Megerősítés",
|
"confirm": "Megerősítés",
|
||||||
"edit_request": "Kérés szerkesztése",
|
"edit_request": "Kérés szerkesztése",
|
||||||
"import_export": "Importálás és exportálás"
|
"import_export": "Importálás és exportálás"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
"already_subscribed": "You are already subscribed to this topic.",
|
"already_subscribed": "Ön már feliratkozott erre a témára.",
|
||||||
"clean_session": "Clean Session",
|
"clean_session": "Munkamenet törlése",
|
||||||
"clear_input": "Clear input",
|
"clear_input": "Bevitel törlése",
|
||||||
"clear_input_on_send": "Clear input on send",
|
"clear_input_on_send": "Bevitel törlése küldéskor",
|
||||||
"client_id": "Client ID",
|
"client_id": "Ügyfél-azonosító",
|
||||||
"color": "Pick a color",
|
"color": "Válasszon színt",
|
||||||
"communication": "Kommunikáció",
|
"communication": "Kommunikáció",
|
||||||
"connection_config": "Connection Config",
|
"connection_config": "Kapcsolat beállításai",
|
||||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
"connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.",
|
||||||
"invalid_topic": "Please provide a topic for the subscription",
|
"invalid_topic": "Adjon témát a feliratkozáshoz",
|
||||||
"keep_alive": "Keep Alive",
|
"keep_alive": "Életben tartás",
|
||||||
"log": "Napló",
|
"log": "Napló",
|
||||||
"lw_message": "Last-Will Message",
|
"lw_message": "Utolsó kívánság üzenet",
|
||||||
"lw_qos": "Last-Will QoS",
|
"lw_qos": "Utolsó kívánság QoS",
|
||||||
"lw_retain": "Last-Will Retain",
|
"lw_retain": "Utolsó kívánság megtartás",
|
||||||
"lw_topic": "Last-Will Topic",
|
"lw_topic": "Utolsó kívánság téma",
|
||||||
"message": "Üzenet",
|
"message": "Üzenet",
|
||||||
"new": "New Subscription",
|
"new": "Új feliratkozás",
|
||||||
"not_connected": "Please start a MQTT connection first.",
|
"not_connected": "Először indítson egy MQTT-kapcsolatot.",
|
||||||
"publish": "Közzététel",
|
"publish": "Közzététel",
|
||||||
"qos": "QoS",
|
"qos": "QoS",
|
||||||
"ssl": "SSL",
|
"ssl": "SSL",
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"app_settings": "Alkalmazás beállításai",
|
"app_settings": "Alkalmazás beállításai",
|
||||||
"default_hopp_displayname": "Unnamed User",
|
"default_hopp_displayname": "Névtelen felhasználó",
|
||||||
"editor": "Szerkesztő",
|
"editor": "Szerkesztő",
|
||||||
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
|
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
|
||||||
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
|
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
|
||||||
@@ -391,26 +391,26 @@
|
|||||||
"choose_language": "Nyelv kiválasztása",
|
"choose_language": "Nyelv kiválasztása",
|
||||||
"content_type": "Tartalom típusa",
|
"content_type": "Tartalom típusa",
|
||||||
"content_type_titles": {
|
"content_type_titles": {
|
||||||
"others": "Others",
|
"others": "Egyebek",
|
||||||
"structured": "Structured",
|
"structured": "Szerkesztett",
|
||||||
"text": "Text"
|
"text": "Szöveg"
|
||||||
},
|
},
|
||||||
"copy_link": "Hivatkozás másolása",
|
"copy_link": "Hivatkozás másolása",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Kérés megkettőzve",
|
||||||
"duration": "Időtartam",
|
"duration": "Időtartam",
|
||||||
"enter_curl": "cURL megadása",
|
"enter_curl": "cURL-parancs megadása",
|
||||||
"generate_code": "Kód előállítása",
|
"generate_code": "Kód előállítása",
|
||||||
"generated_code": "Előállított kód",
|
"generated_code": "Előállított kód",
|
||||||
"header_list": "Fejléclista",
|
"header_list": "Fejléclista",
|
||||||
"invalid_name": "Adjon nevet a kérésnek",
|
"invalid_name": "Adjon nevet a kérésnek",
|
||||||
"method": "Módszer",
|
"method": "Módszer",
|
||||||
"moved": "Request moved",
|
"moved": "Kérés áthelyezve",
|
||||||
"name": "Kérés neve",
|
"name": "Kérés neve",
|
||||||
"new": "Új kérés",
|
"new": "Új kérés",
|
||||||
"order_changed": "Request Order Updated",
|
"order_changed": "Kérés sorrendje frissítve",
|
||||||
"override": "Felülbírálás",
|
"override": "Felülbírálás",
|
||||||
"override_help": "A <kbd>Content-Type</kbd> beállítása a fejlécekben",
|
"override_help": "<kbd>Content-Type</kbd> beállítása a fejlécekben",
|
||||||
"overriden": "Felülbírálva",
|
"overriden": "Felülbírálva",
|
||||||
"parameter_list": "Lekérdezési paraméterek",
|
"parameter_list": "Lekérdezési paraméterek",
|
||||||
"parameters": "Paraméterek",
|
"parameters": "Paraméterek",
|
||||||
@@ -429,11 +429,12 @@
|
|||||||
"type": "Kérés típusa",
|
"type": "Kérés típusa",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"variables": "Változók",
|
"variables": "Változók",
|
||||||
"view_my_links": "View my links"
|
"view_my_links": "Saját hivatkozások megtekintése"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Hang",
|
||||||
"body": "Válasz törzse",
|
"body": "Válasz törzse",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "JSON-válasz törzsének szűrése (JSONPath szintaxist használ)",
|
||||||
"headers": "Fejlécek",
|
"headers": "Fejlécek",
|
||||||
"html": "HTML",
|
"html": "HTML",
|
||||||
"image": "Kép",
|
"image": "Kép",
|
||||||
@@ -445,13 +446,14 @@
|
|||||||
"status": "Állapot",
|
"status": "Állapot",
|
||||||
"time": "Idő",
|
"time": "Idő",
|
||||||
"title": "Válasz",
|
"title": "Válasz",
|
||||||
|
"video": "Videó",
|
||||||
"waiting_for_connection": "várakozás kapcsolódásra",
|
"waiting_for_connection": "várakozás kapcsolódásra",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"accent_color": "Kiemelőszín",
|
"accent_color": "Kiemelőszín",
|
||||||
"account": "Fiók",
|
"account": "Fiók",
|
||||||
"account_deleted": "Your account has been deleted",
|
"account_deleted": "A fiókja törölve lett",
|
||||||
"account_description": "A fiókbeállítások személyre szabása.",
|
"account_description": "A fiókbeállítások személyre szabása.",
|
||||||
"account_email_description": "Az Ön elsődleges e-mail-címe.",
|
"account_email_description": "Az Ön elsődleges e-mail-címe.",
|
||||||
"account_name_description": "Ez a megjelenített neve.",
|
"account_name_description": "Ez a megjelenített neve.",
|
||||||
@@ -460,8 +462,8 @@
|
|||||||
"change_font_size": "Betűméret megváltoztatása",
|
"change_font_size": "Betűméret megváltoztatása",
|
||||||
"choose_language": "Nyelv kiválasztása",
|
"choose_language": "Nyelv kiválasztása",
|
||||||
"dark_mode": "Sötét",
|
"dark_mode": "Sötét",
|
||||||
"delete_account": "Delete account",
|
"delete_account": "Fiók törlése",
|
||||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
"delete_account_description": "Ha törli a fiókját, akkor az összes adata véglegesen törlésre kerül. Ezt a műveletet nem lehet visszavonni.",
|
||||||
"expand_navigation": "Navigáció kinyitása",
|
"expand_navigation": "Navigáció kinyitása",
|
||||||
"experiments": "Kísérletek",
|
"experiments": "Kísérletek",
|
||||||
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
|
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
|
||||||
@@ -488,8 +490,8 @@
|
|||||||
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
|
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
|
||||||
"read_the": "Olvassa el:",
|
"read_the": "Olvassa el:",
|
||||||
"reset_default": "Visszaállítás az alapértelmezettre",
|
"reset_default": "Visszaállítás az alapértelmezettre",
|
||||||
"short_codes": "Short codes",
|
"short_codes": "Rövid kódok",
|
||||||
"short_codes_description": "Short codes which were created by you.",
|
"short_codes_description": "Az Ön által létrehozott rövid kódok.",
|
||||||
"sidebar_on_left": "Oldalsáv a bal oldalon",
|
"sidebar_on_left": "Oldalsáv a bal oldalon",
|
||||||
"sync": "Szinkronizálás",
|
"sync": "Szinkronizálás",
|
||||||
"sync_collections": "Gyűjtemények",
|
"sync_collections": "Gyűjtemények",
|
||||||
@@ -503,16 +505,16 @@
|
|||||||
"theme_description": "Az alkalmazás témájának személyre szabása.",
|
"theme_description": "Az alkalmazás témájának személyre szabása.",
|
||||||
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
|
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
|
||||||
"user": "Felhasználó",
|
"user": "Felhasználó",
|
||||||
"verified_email": "Verified email",
|
"verified_email": "Ellenőrzött e-mail-cím",
|
||||||
"verify_email": "E-mail-cím ellenőrzése"
|
"verify_email": "E-mail-cím ellenőrzése"
|
||||||
},
|
},
|
||||||
"shortcodes": {
|
"shortcodes": {
|
||||||
"actions": "Actions",
|
"actions": "Műveletek",
|
||||||
"created_on": "Created on",
|
"created_on": "Létrehozva",
|
||||||
"deleted": "Shortcode deleted",
|
"deleted": "Rövid kód törölve",
|
||||||
"method": "Method",
|
"method": "Módszer",
|
||||||
"not_found": "Shortcode not found",
|
"not_found": "A rövid kód nem található",
|
||||||
"short_code": "Short code",
|
"short_code": "Rövid kód",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"shortcut": {
|
"shortcut": {
|
||||||
@@ -554,9 +556,9 @@
|
|||||||
"title": "Kérés"
|
"title": "Kérés"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"copy": "Copy response to clipboard",
|
"copy": "Válasz másolása a vágólapra",
|
||||||
"download": "Download response as file",
|
"download": "Válasz letöltés fájlként",
|
||||||
"title": "Response"
|
"title": "Válasz"
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Téma átváltása fekete módra",
|
"black": "Téma átváltása fekete módra",
|
||||||
@@ -574,8 +576,8 @@
|
|||||||
},
|
},
|
||||||
"socketio": {
|
"socketio": {
|
||||||
"communication": "Kommunikáció",
|
"communication": "Kommunikáció",
|
||||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
"connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.",
|
||||||
"event_name": "Esemény neve",
|
"event_name": "Esemény vagy téma neve",
|
||||||
"events": "Események",
|
"events": "Események",
|
||||||
"log": "Napló",
|
"log": "Napló",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
@@ -592,9 +594,9 @@
|
|||||||
"connected": "Kapcsolódva",
|
"connected": "Kapcsolódva",
|
||||||
"connected_to": "Kapcsolódva ehhez: {name}",
|
"connected_to": "Kapcsolódva ehhez: {name}",
|
||||||
"connecting_to": "Kapcsolódás ehhez: {name}…",
|
"connecting_to": "Kapcsolódás ehhez: {name}…",
|
||||||
"connection_error": "Failed to connect",
|
"connection_error": "Nem sikerült kapcsolódni",
|
||||||
"connection_failed": "Connection failed",
|
"connection_failed": "A kapcsolódás sikertelen",
|
||||||
"connection_lost": "Connection lost",
|
"connection_lost": "A kapcsolat elveszett",
|
||||||
"copied_to_clipboard": "Vágólapra másolva",
|
"copied_to_clipboard": "Vágólapra másolva",
|
||||||
"deleted": "Törölve",
|
"deleted": "Törölve",
|
||||||
"deprecated": "ELAVULT",
|
"deprecated": "ELAVULT",
|
||||||
@@ -609,17 +611,17 @@
|
|||||||
"history_deleted": "Előzmények törölve",
|
"history_deleted": "Előzmények törölve",
|
||||||
"linewrap": "Sorok tördelése",
|
"linewrap": "Sorok tördelése",
|
||||||
"loading": "Betöltés…",
|
"loading": "Betöltés…",
|
||||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
"message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}",
|
||||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
"mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}",
|
||||||
"none": "Nincs",
|
"none": "Nincs",
|
||||||
"nothing_found": "Semmi sem található ehhez:",
|
"nothing_found": "Semmi sem található ehhez:",
|
||||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
"published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}",
|
||||||
"published_message": "Published message: {message} to topic: {topic}",
|
"published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}",
|
||||||
"reconnection_error": "Failed to reconnect",
|
"reconnection_error": "Nem sikerült újrakapcsolódni",
|
||||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
"subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}",
|
||||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
"subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}",
|
||||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
"unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {topic}",
|
||||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
"unsubscribed_success": "Sikeresen leiratkozott erről a témáról: {topic}",
|
||||||
"waiting_send_request": "Várakozás a kérés elküldésére"
|
"waiting_send_request": "Várakozás a kérés elküldésére"
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
@@ -639,7 +641,7 @@
|
|||||||
"body": "Törzs",
|
"body": "Törzs",
|
||||||
"collections": "Gyűjtemények",
|
"collections": "Gyűjtemények",
|
||||||
"documentation": "Dokumentáció",
|
"documentation": "Dokumentáció",
|
||||||
"environments": "Environments",
|
"environments": "Környezetek",
|
||||||
"headers": "Fejlécek",
|
"headers": "Fejlécek",
|
||||||
"history": "Előzmények",
|
"history": "Előzmények",
|
||||||
"mqtt": "MQTT",
|
"mqtt": "MQTT",
|
||||||
@@ -664,7 +666,7 @@
|
|||||||
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||||
"exit": "Kilépés a csapatból",
|
"exit": "Kilépés a csapatból",
|
||||||
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
|
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
|
||||||
"invalid_coll_id": "Invalid collection ID",
|
"invalid_coll_id": "Érvénytelen gyűjteményazonosító",
|
||||||
"invalid_email_format": "Az e-mail formátuma érvénytelen",
|
"invalid_email_format": "Az e-mail formátuma érvénytelen",
|
||||||
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||||
"invalid_invite_link": "Érvénytelen meghívási hivatkozás",
|
"invalid_invite_link": "Érvénytelen meghívási hivatkozás",
|
||||||
@@ -688,7 +690,7 @@
|
|||||||
"member_removed": "Felhasználó eltávolítva",
|
"member_removed": "Felhasználó eltávolítva",
|
||||||
"member_role_updated": "Felhasználói szerepek frissítve",
|
"member_role_updated": "Felhasználói szerepek frissítve",
|
||||||
"members": "Tagok",
|
"members": "Tagok",
|
||||||
"more_members": "+{count} more",
|
"more_members": "+{count} további",
|
||||||
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
|
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
|
||||||
"name_updated": "Csapatnév frissítve",
|
"name_updated": "Csapatnév frissítve",
|
||||||
"new": "Új csapat",
|
"new": "Új csapat",
|
||||||
@@ -696,13 +698,13 @@
|
|||||||
"new_name": "Saját új csapat",
|
"new_name": "Saját új csapat",
|
||||||
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
|
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
|
||||||
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "A kérés nem található.",
|
||||||
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||||
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||||
"parent_coll_move": "Cannot move collection to a child collection",
|
"parent_coll_move": "Nem lehet áthelyezni a gyűjteményt egy gyermekgyűjteménybe",
|
||||||
"pending_invites": "Függőben lévő meghívások",
|
"pending_invites": "Függőben lévő meghívások",
|
||||||
"permissions": "Jogosultságok",
|
"permissions": "Jogosultságok",
|
||||||
"same_target_destination": "Same target and destination",
|
"same_target_destination": "Ugyanaz a cél és célhely",
|
||||||
"saved": "Csapat elmentve",
|
"saved": "Csapat elmentve",
|
||||||
"select_a_team": "Csapat kiválasztása",
|
"select_a_team": "Csapat kiválasztása",
|
||||||
"title": "Csapatok",
|
"title": "Csapatok",
|
||||||
@@ -710,9 +712,9 @@
|
|||||||
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
|
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
|
||||||
},
|
},
|
||||||
"team_environment": {
|
"team_environment": {
|
||||||
"deleted": "Environment Deleted",
|
"deleted": "Környezet törölve",
|
||||||
"duplicate": "Environment Duplicated",
|
"duplicate": "Környezet megkettőzve",
|
||||||
"not_found": "Environment not found."
|
"not_found": "A környezet nem található."
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"failed": "teszt sikertelen",
|
"failed": "teszt sikertelen",
|
||||||
@@ -732,9 +734,9 @@
|
|||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "Change workspace",
|
"change": "Munkaterület váltása",
|
||||||
"personal": "My Workspace",
|
"personal": "Saját munkaterület",
|
||||||
"team": "Team Workspace",
|
"team": "Csapat-munkaterület",
|
||||||
"title": "Workspaces"
|
"title": "Munkaterületek"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "Lihat tautan saya"
|
"view_my_links": "Lihat tautan saya"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Response Body",
|
"body": "Response Body",
|
||||||
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
|
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Waktu",
|
"time": "Waktu",
|
||||||
"title": "Response",
|
"title": "Response",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "Menunggu koneksi",
|
"waiting_for_connection": "Menunggu koneksi",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Corpo della risposta",
|
"body": "Corpo della risposta",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Intestazioni",
|
"headers": "Intestazioni",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Stato",
|
"status": "Stato",
|
||||||
"time": "Tempo impiegato",
|
"time": "Tempo impiegato",
|
||||||
"title": "Risposta",
|
"title": "Risposta",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "In attesa di connessione",
|
"waiting_for_connection": "In attesa di connessione",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "自分のリンクを見る"
|
"view_my_links": "自分のリンクを見る"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "レスポンスボディ",
|
"body": "レスポンスボディ",
|
||||||
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
|
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
|
||||||
"headers": "ヘッダー",
|
"headers": "ヘッダー",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "ステータス",
|
"status": "ステータス",
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
"title": "レスポンス",
|
"title": "レスポンス",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "接続を待っています",
|
"waiting_for_connection": "接続を待っています",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "내 링크 보기"
|
"view_my_links": "내 링크 보기"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "응답 본문",
|
"body": "응답 본문",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "헤더",
|
"headers": "헤더",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "상태",
|
"status": "상태",
|
||||||
"time": "시간",
|
"time": "시간",
|
||||||
"title": "제목",
|
"title": "제목",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "연결 대기 중",
|
"waiting_for_connection": "연결 대기 중",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Reactie inhoud",
|
"body": "Reactie inhoud",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Headers",
|
"headers": "Headers",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Tijd",
|
"time": "Tijd",
|
||||||
"title": "Antwoord",
|
"title": "Antwoord",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "wachten op verbinding",
|
"waiting_for_connection": "wachten op verbinding",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Svarkropp",
|
"body": "Svarkropp",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Overskrifter",
|
"headers": "Overskrifter",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Tid",
|
"time": "Tid",
|
||||||
"title": "Respons",
|
"title": "Respons",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "venter på tilkobling",
|
"waiting_for_connection": "venter på tilkobling",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Ciało odpowiedzi",
|
"body": "Ciało odpowiedzi",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Nagłówki",
|
"headers": "Nagłówki",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Czas",
|
"time": "Czas",
|
||||||
"title": "Odpowiedź",
|
"title": "Odpowiedź",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "oczekiwanie na połączenie",
|
"waiting_for_connection": "oczekiwanie na połączenie",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Corpo de Resposta",
|
"body": "Corpo de Resposta",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Cabeçalhos",
|
"headers": "Cabeçalhos",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Tempo",
|
"time": "Tempo",
|
||||||
"title": "Resposta",
|
"title": "Resposta",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "aguardando conexão",
|
"waiting_for_connection": "aguardando conexão",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "View my links"
|
"view_my_links": "View my links"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Corpo de Resposta",
|
"body": "Corpo de Resposta",
|
||||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||||
"headers": "Cabeçalhos",
|
"headers": "Cabeçalhos",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"time": "Tempo",
|
"time": "Tempo",
|
||||||
"title": "Resposta",
|
"title": "Resposta",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "aguardando conexão",
|
"waiting_for_connection": "aguardando conexão",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"view_my_links": "Vizualizare link-uri"
|
"view_my_links": "Vizualizare link-uri"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
|
"audio": "Audio",
|
||||||
"body": "Corpul de răspuns",
|
"body": "Corpul de răspuns",
|
||||||
"filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)",
|
"filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)",
|
||||||
"headers": "Anteturi",
|
"headers": "Anteturi",
|
||||||
@@ -445,6 +446,7 @@
|
|||||||
"status": "Stare",
|
"status": "Stare",
|
||||||
"time": "Timp",
|
"time": "Timp",
|
||||||
"title": "Raspuns",
|
"title": "Raspuns",
|
||||||
|
"video": "Video",
|
||||||
"waiting_for_connection": "Așteptând conexiunea",
|
"waiting_for_connection": "Așteptând conexiunea",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user