Compare commits

...

86 Commits

Author SHA1 Message Date
nivedin
fa42fc1538 fix: update spelling 2024-03-20 15:25:34 +05:30
nivedin
0f4168d12c feat: add github enterprise auth option 2024-03-20 14:44:44 +05:30
Akash K
6b58915caa feat: oauth revamp + support for multiple grant types in oauth (#3885)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-20 00:18:03 +05:30
Akash K
457857a711 feat: team search in workspace search and spotlight (#3896)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-19 18:50:35 +05:30
Balu Babu
a3f3e3e62d refactor: collection search query (#3908) 2024-03-19 17:12:35 +05:30
Andrew Bastin
66f20d10e1 chore: enable subpath based access in test deploy docker compose 2024-03-19 16:26:04 +05:30
Andrew Bastin
32e9366609 chore: update test deploy docker compose aio port 2024-03-19 15:53:08 +05:30
Andrew Bastin
e41e956273 chore: add test deployment docker compose file 2024-03-19 14:39:57 +05:30
Nivedin
a14870f3f0 fix: collection auth headers active tab update bug and type fix (#3899) 2024-03-15 21:17:34 +05:30
Andrew Bastin
0e96665254 refactor: use trigram search index instead of full text search (#3900)
Co-authored-by: Balu Babu <balub997@gmail.com>
2024-03-15 20:10:12 +05:30
kaifulee
efdc1c2f5d chore: fix some typos (#3895)
Signed-off-by: kaifulee <cuishuang@outlook.com>
2024-03-15 20:06:34 +05:30
Andrew Bastin
c5334d4c06 chore(sh-admin): bump @hoppscotch/ui version to 0.1.3 2024-03-15 12:43:05 +05:30
Balu Babu
4f549974ed fix: reset infra-config bug (#3898) 2024-03-14 21:46:34 +05:30
Nivedin
41d617b507 fix: secret env bug in firebase due to undefined value (#3881)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-13 17:11:51 +05:30
Joel Jacob Stephen
be7387ed19 refactor(sh-admin): updated data sharing doc links + remove disabled property from all inputs in configurations (#3894) 2024-03-13 16:18:27 +05:30
Joel Jacob Stephen
acfb0189df feat(sh-admin): enhanced user management in admin dashboard (#3814)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-13 14:45:13 +05:30
Nivedin
8fdba760a2 refactor: personal workspace nomenclature update (#3893)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-13 14:21:23 +05:30
Nivedin
bf98009abb fix: request variable version syncing bug (#3889)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-12 11:42:05 +05:30
James George
dce396c164 chore: bump codemirror dependencies (#3888) 2024-03-11 14:21:39 +05:30
Nivedin
07e8af7947 refactor: update team nomenclature (#3880)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-08 23:54:32 +05:30
Joel Jacob Stephen
e69d5a6253 feat(sh-admin): introducing additional SSO related server configurations to dashboard (#3737)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-08 15:18:53 +05:30
Akash K
6d66d12a9e feat: common changes for site protection (#3878) 2024-03-07 23:43:20 +05:30
James George
439cd82c88 chore: pin dependencies across packages (#3876) 2024-03-07 23:37:48 +05:30
Akash K
6dbaf524ce feat: use tags as folders when importing from openapi (#3846) 2024-03-07 19:55:46 +05:30
Andrew Bastin
68e439d1a4 chore: bump version to 2024.3.0 2024-03-07 19:22:46 +05:30
Nivedin
8deba7a28e fix: context menu bug and incorrect position (#3874) 2024-03-07 17:59:06 +05:30
Nivedin
7ec8659381 feat: request variables (#3825)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-07 12:50:44 +05:30
Mir Arif Hasan
3611cac241 feat(backend): sso callback url and scope added in infra-config (#3718) 2024-03-07 12:07:51 +05:30
Joel Jacob Stephen
919579b1da feat(sh-admin): introducing data analytics and newsletter configurations (#3845)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
2024-03-06 20:06:48 +05:30
Nivedin
4798d7bbbd refactor: remove restore tab popup and its functionalities (#3867) 2024-03-05 18:14:41 +05:30
Balu Babu
a0c6b22641 feat: full text search for TeamCollections and TeamRequests (#3857)
Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
2024-03-05 18:05:58 +05:30
James George
de8929ab18 feat(common): support simultaneous imports of collections and environment files (#3719) 2024-03-05 17:49:01 +05:30
Andrew Bastin
55a94bdccc chore: merge hoppscotch/release/2023.12.6 into hoppscotch/release/2024.3.0 2024-02-27 13:35:20 +05:30
Andrew Bastin
faab1d20fd chore: bump version to 2023.12.6 2024-02-26 22:31:58 +05:30
Anwarul Islam
bd406616ec fix: collection level authorization inheritance issue (#3852) 2024-02-23 19:39:55 +05:30
Andrew Bastin
6827e97ec5 refactor: possible links in email templates do not highlight (#3851) 2024-02-23 01:05:20 +05:30
amk-dev
10d2048975 fix: use x-www-form-urlencoded for token exchange requests 2024-02-22 00:43:50 +05:30
Nivedin
291f18591e fix: perfomance in safari (#3848) 2024-02-22 00:41:30 +05:30
James George
342532c9b1 fix(common): prevent exceptions with open shared requests in new tab action (#3835) 2024-02-22 00:36:45 +05:30
Balu Babu
cf039c482a feat: SH instance analytics data collection (#3838) 2024-02-22 00:35:12 +05:30
Mir Arif Hasan
ded2725116 feat: admin user management (backend) (#3786) 2024-02-21 21:35:08 +05:30
Balu Babu
9c6754c70f feat: inital setup info route (#3847) 2024-02-21 21:15:47 +05:30
James George
4bd54b12cd fix(persistence-service): add fallbacks for environments related schemas (#3832) 2024-02-15 23:38:56 +05:30
Andrew Bastin
ed6e9b6954 chore: bump version to 2023.12.5 2024-02-15 21:47:58 +05:30
James George
dfdd44b4ed fix(persistence-service): update global environment variables schema (#3829) 2024-02-15 21:40:31 +05:30
Akash K
fc34871dae fix: accessing undefined property variables (#3831) 2024-02-15 21:32:50 +05:30
Nivedin
45b532747e fix: environment tooltip update bug (#3819) 2024-02-13 17:42:02 +05:30
Akash K
de4635df23 chore: add workspace type property in request run analytics event (#3820)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2024-02-13 17:38:11 +05:30
Nivedin
41bad1f3dc fix: secret environment flow bugs (#3817) 2024-02-10 20:22:10 +05:30
Andrew Bastin
ecca3d2032 chore: correct linting errors 2024-02-09 14:42:12 +05:30
Muhammed Ajmal M
47226be6d0 feat: persist line wrap setting (#3647)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-09 14:05:09 +05:30
Andrew Bastin
6a0e73fdec chore: bump versions 2024-02-08 22:41:59 +05:30
Andrew Bastin
672ee69b2c chore: correct linting errors 2024-02-08 22:33:03 +05:30
Joel Jacob Stephen
b359650d96 refactor: updated teams nomenclature in admin dashboard to workspaces (#3770) 2024-02-08 22:17:42 +05:30
James George
c0fae79678 fix(sh-admin): persist active selection in the sidebar (#3812) 2024-02-08 22:16:33 +05:30
James George
5bcc38e36b feat: support secret environment variables in CLI (#3815) 2024-02-08 22:08:18 +05:30
Nivedin
00862eb192 feat: secret variables in environments (#3779)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-08 21:58:42 +05:30
Akash K
16803acb26 chore: Oauth temporary ux improvements (#3792) 2024-02-06 20:35:29 +05:30
Nivedin
3911c9cd1f refactor: update share request flow (#3805) 2024-02-05 23:50:15 +05:30
Florian Metz
0028f6e878 feat(js-sandbox): expose atob & btoa functions for Node.js (#3724)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-05 23:12:55 +05:30
Anwarul Islam
0ba33ec187 fix: request endpoint heading (#3804) 2024-02-05 23:08:16 +05:30
James George
3482743782 chore(cli): emit bundle in ESM format (#3777) 2024-02-05 22:55:05 +05:30
James George
d7cdeb796a chore(common): analytics on spotlight (#3727)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
2024-02-02 15:32:06 +05:30
Joel Jacob Stephen
3d6adcc39d refactor: consolidated admin dashboard improvements (#3790)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-02-02 15:17:25 +05:30
Andrew Bastin
aab76f1358 chore: bump version to 2023.12.3 2024-01-30 20:27:25 +05:30
Anwarul Islam
a28a576c41 feat: team environment search and switch (#3700)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-30 19:49:04 +05:30
Liyas Thomas
0d0ad7a2f8 chore: added micro interactions (#3783)
Co-authored-by: Dmitry <mukovkin@yandex.ru>
2024-01-30 18:42:42 +05:30
Balu Babu
1df9de44b7 chore: upgraded prisma version to v5.8.0 (#3787) 2024-01-30 18:16:28 +05:30
Muhammed Ajmal M
4cba03e53f feat(js-sandbox): add pw.env.unset method (#3677)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 22:31:27 +05:30
Nivedin
9e1466a877 fix: bugs in shared request (#3704) 2024-01-23 22:24:18 +05:30
Anwarul Islam
b81ccb4ee3 fix: tab on current input field to focus the next input field (#3754) 2024-01-23 22:21:23 +05:30
Nivedin
27d0a7c437 refactor: persist running requests while switching tabs (#3742) 2024-01-23 22:13:57 +05:30
Nivedin
aca96dd5f2 refactor: add option to disable context menu (#3717) 2024-01-23 22:05:05 +05:30
Anwarul Islam
c0dbcc901f fix: documentation is not being generated on GQL (#3730)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2024-01-23 22:00:41 +05:30
Joel Jacob Stephen
ba52c8cc37 refactor: improvements to the dashboard sidebar (#3709)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:55:42 +05:30
Dmitry
d1f6f40ef8 fix: perform logout if the silent refresh attempt fails (#3705)
Co-authored-by: Dmitry Mukovkin <d.mukovkin@cft.ru>
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-01-23 21:53:59 +05:30
Akash K
99f5070f71 fix: unwanted double slashes when importing from openapi (#3745) 2024-01-23 21:49:34 +05:30
Andrew Bastin
cd371fc9d4 chore: bump versions to 2023.12.2 2024-01-03 16:58:51 +05:30
Jordi Been
59fef248c0 build: update node alpine version (#3660) 2024-01-03 16:49:51 +05:30
Andrew Bastin
286fcd2bb0 chore: bump selfhost-desktop version to 23.12.1-1 2023-12-24 13:45:41 +05:30
Andrew Bastin
b2d98f7b66 chore: remove windi from selfhost-desktop 2023-12-24 13:21:12 +05:30
James George
c6c220091a feat(common): support importing environments individually (#3691) 2023-12-24 12:12:02 +05:30
Andrew Bastin
8f503479b6 chore: bump package version to 2023.12.1 2023-12-22 20:49:21 +05:30
Mir Arif Hasan
54d8378ccf fix: improve smtp email validation and fix enableAndDisableSSO mutation (#3689)
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-12-22 20:37:15 +05:30
James George
0df194f9c5 fix(cli): environment resolution in the single-entry export format (#3687) 2023-12-22 19:21:33 +05:30
Liyas Thomas
ddf7eb6ad6 chore: minor ui improvements 2023-12-20 18:30:16 +05:30
377 changed files with 18988 additions and 10825 deletions

1
.npmrc
View File

@@ -1 +1,2 @@
shamefully-hoist=false
save-prefix=''

View File

@@ -239,7 +239,7 @@ Help us to translate Hoppscotch. Please read [`TRANSLATIONS`](TRANSLATIONS.md) f
📦 **Add-ons:** Official add-ons for hoppscotch.
- **[Hoppscotch CLI](https://github.com/hoppscotch/hopp-cli)** - Command-line interface for Hoppscotch.
- **[Hoppscotch CLI](https://github.com/hoppscotch/hoppscotch/tree/main/packages/hoppscotch-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.

48
docker-compose.deploy.yml Normal file
View File

@@ -0,0 +1,48 @@
# Docker Compose config used for internal test and QA deployments
# This just spins up the AIO container along with an attached DB to the standard HTTP ports with subpath access mode
# TODO: Add Healthcheck for the AIO container
version: "3.7"
services:
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
container_name: hoppscotch-aio
restart: unless-stopped
build:
dockerfile: prod.Dockerfile
context: .
target: aio
environment:
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
- ENABLE_SUBPATH_BASED_ACCESS=true
depends_on:
hoppscotch-db:
condition: service_healthy
ports:
- "3080:80"
# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance
# This will be exposed at port 5432
hoppscotch-db:
image: postgres:15
ports:
- "5432:5432"
user: postgres
environment:
# The default user defined by the docker image
POSTGRES_USER: postgres
# NOTE: Please UPDATE THIS PASSWORD!
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s
retries: 10

View File

@@ -23,13 +23,13 @@
"./packages/*"
],
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@hoppscotch/ui": "^0.1.0",
"@commitlint/cli": "16.3.0",
"@commitlint/config-conventional": "16.2.4",
"@hoppscotch/ui": "0.1.0",
"@types/node": "17.0.27",
"cross-env": "^7.0.3",
"http-server": "^14.1.1",
"husky": "^7.0.4",
"cross-env": "7.0.3",
"http-server": "14.1.1",
"husky": "7.0.4",
"lint-staged": "12.4.0"
},
"pnpm": {
@@ -37,7 +37,7 @@
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"httpsnippet@3.0.1": {
"peerDependencies": {
"ajv": "6.12.3"
}

View File

@@ -17,16 +17,16 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "6.9.3",
"@codemirror/language": "6.10.1",
"@lezer/highlight": "1.2.0",
"@lezer/lr": "^1.3.14"
"@lezer/lr": "1.3.14"
},
"devDependencies": {
"@lezer/generator": "^1.5.1",
"mocha": "^9.2.2",
"rollup": "^3.29.3",
"rollup-plugin-dts": "^6.0.2",
"rollup-plugin-ts": "^3.4.5",
"typescript": "^5.2.2"
"@lezer/generator": "1.5.1",
"mocha": "9.2.2",
"rollup": "3.29.4",
"rollup-plugin-dts": "6.0.2",
"rollup-plugin-ts": "3.4.5",
"typescript": "5.2.2"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.12.0-1",
"version": "2024.3.0",
"description": "",
"author": "",
"private": true,
@@ -24,80 +24,83 @@
"do-test": "pnpm run test"
},
"dependencies": {
"@apollo/server": "^4.9.4",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.6",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.6",
"@nestjs/graphql": "^12.0.9",
"@nestjs/jwt": "^10.1.1",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.2.6",
"@nestjs/throttler": "^5.0.0",
"@prisma/client": "^4.16.2",
"argon2": "^0.30.3",
"bcrypt": "^5.1.0",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"express": "^4.17.1",
"express-session": "^1.17.3",
"fp-ts": "^2.13.1",
"graphql": "^16.8.1",
"graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.6.0",
"graphql-subscriptions": "^2.0.0",
"handlebars": "^4.7.7",
"io-ts": "^2.2.16",
"luxon": "^3.2.1",
"nodemailer": "^6.9.1",
"passport": "^0.6.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0",
"prisma": "^4.16.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"
"@apollo/server": "4.9.5",
"@nestjs-modules/mailer": "1.9.1",
"@nestjs/apollo": "12.0.9",
"@nestjs/common": "10.2.7",
"@nestjs/config": "3.1.1",
"@nestjs/core": "10.2.7",
"@nestjs/graphql": "12.0.9",
"@nestjs/jwt": "10.1.1",
"@nestjs/passport": "10.0.2",
"@nestjs/platform-express": "10.2.7",
"@nestjs/schedule": "4.0.1",
"@nestjs/throttler": "5.0.1",
"@prisma/client": "5.8.1",
"argon2": "0.30.3",
"bcrypt": "5.1.0",
"cookie": "0.5.0",
"cookie-parser": "1.4.6",
"cron": "3.1.6",
"express": "4.18.2",
"express-session": "1.17.3",
"fp-ts": "2.13.1",
"graphql": "16.8.1",
"graphql-query-complexity": "0.12.0",
"graphql-redis-subscriptions": "2.6.0",
"graphql-subscriptions": "2.0.0",
"handlebars": "4.7.7",
"io-ts": "2.2.16",
"luxon": "3.2.1",
"nodemailer": "6.9.1",
"passport": "0.6.0",
"passport-github2": "0.1.12",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",
"passport-microsoft": "1.0.0",
"posthog-node": "3.6.3",
"prisma": "5.8.1",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "7.6.0"
},
"devDependencies": {
"@nestjs/cli": "^10.1.18",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.6",
"@relmify/jest-fp-ts": "^2.0.2",
"@types/argon2": "^0.15.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie": "^0.5.1",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.14",
"@types/jest": "^29.4.0",
"@types/luxon": "^3.2.0",
"@types/node": "^18.11.10",
"@types/nodemailer": "^6.4.7",
"@types/passport-github2": "^1.2.5",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^0.0.0",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"cross-env": "^7.0.3",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.4.1",
"jest-mock-extended": "^3.0.1",
"@nestjs/cli": "10.2.1",
"@nestjs/schematics": "10.0.3",
"@nestjs/testing": "10.2.7",
"@relmify/jest-fp-ts": "2.0.2",
"@types/argon2": "0.15.0",
"@types/bcrypt": "5.0.0",
"@types/cookie": "0.5.1",
"@types/cookie-parser": "1.4.3",
"@types/express": "4.17.14",
"@types/jest": "29.4.0",
"@types/luxon": "3.2.0",
"@types/node": "18.11.10",
"@types/nodemailer": "6.4.7",
"@types/passport-github2": "1.2.5",
"@types/passport-google-oauth20": "2.0.11",
"@types/passport-jwt": "3.0.8",
"@types/passport-microsoft": "0.0.0",
"@types/supertest": "2.0.12",
"@typescript-eslint/eslint-plugin": "5.45.0",
"@typescript-eslint/parser": "5.45.0",
"cross-env": "7.0.3",
"eslint": "8.29.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"jest": "29.4.1",
"jest-mock-extended": "3.0.1",
"jwt": "link:@types/nestjs/jwt",
"prettier": "^2.8.4",
"source-map-support": "^0.5.21",
"supertest": "^6.3.2",
"prettier": "2.8.4",
"source-map-support": "0.5.21",
"supertest": "6.3.2",
"ts-jest": "29.0.5",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"ts-loader": "9.4.2",
"ts-node": "10.9.1",
"tsconfig-paths": "4.1.1",
"typescript": "^4.9.3"
"typescript": "4.9.3"
},
"jest": {
"moduleFileExtensions": [
@@ -118,4 +121,4 @@
"^src/(.*)$": "<rootDir>/$1"
}
}
}
}

View File

@@ -0,0 +1,22 @@
-- This is a custom migration file which is not generated by Prisma.
-- The aim of this migration is to add text search indices to the TeamCollection and TeamRequest tables.
-- Create Extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Create GIN Trigram Index for Team Collection title
CREATE INDEX
"TeamCollection_title_trgm_idx"
ON
"TeamCollection"
USING
GIN (title gin_trgm_ops);
-- Create GIN Trigram Index for Team Collection title
CREATE INDEX
"TeamRequest_title_trgm_idx"
ON
"TeamRequest"
USING
GIN (title gin_trgm_ops);

View File

@@ -41,31 +41,31 @@ model TeamInvitation {
}
model TeamCollection {
id String @id @default(cuid())
id String @id @default(cuid())
parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model TeamRequest {
id String @id @default(cuid())
id String @id @default(cuid())
collectionID String
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
teamID String
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
title String
request Json
orderIndex Int
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
createdOn DateTime @default(now()) @db.Timestamp(3)
updatedOn DateTime @updatedAt @db.Timestamp(3)
}
model Shortcode {

View File

@@ -27,9 +27,7 @@ import {
} from './input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { User } from 'src/user/user.model';
import { PaginationArgs } from 'src/types/input-types.args';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { UserDeletionResult } from 'src/user/user.model';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Admin)
@@ -49,203 +47,6 @@ export class AdminResolver {
return admin;
}
@ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra',
deprecationReason: 'Use `infra` query instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() {
const admins = await this.adminService.fetchAdmins();
return admins;
}
@ResolveField(() => User, {
description: 'Returns a user info by UID',
deprecationReason: 'Use `infra` query instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async userInfo(
@Args({
name: 'userUid',
type: () => ID,
description: 'The user UID',
})
userUid: string,
): Promise<AuthUser> {
const user = await this.adminService.fetchUserInfo(userUid);
if (E.isLeft(user)) throwErr(user.left);
return user.right;
}
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
deprecationReason: 'Use `infra` query instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(
@Parent() admin: Admin,
@Args() args: PaginationArgs,
): Promise<AuthUser[]> {
const users = await this.adminService.fetchUsers(args.cursor, args.take);
return users;
}
@ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users',
deprecationReason: 'Use `infra` query instead',
})
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
return users;
}
@ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra',
deprecationReason: 'Use `infra` query instead',
})
async allTeams(
@Parent() admin: Admin,
@Args() args: PaginationArgs,
): Promise<Team[]> {
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
return teams;
}
@ResolveField(() => Team, {
description: 'Returns a team info by ID when requested by Admin',
deprecationReason: 'Use `infra` query instead',
})
async teamInfo(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which info to fetch',
})
teamID: string,
): Promise<Team> {
const team = await this.adminService.getTeamInfo(teamID);
if (E.isLeft(team)) throwErr(team.left);
return team.right;
}
@ResolveField(() => Number, {
description: 'Return count of all the members in a team',
deprecationReason: 'Use `infra` query instead',
})
async membersCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
nullable: false,
})
teamID: string,
): Promise<number> {
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
return teamMembersCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored collections in a team',
deprecationReason: 'Use `infra` query instead',
})
async collectionCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
return teamCollCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored requests in a team',
deprecationReason: 'Use `infra` query instead',
})
async requestCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
return teamReqCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored environments in a team',
deprecationReason: 'Use `infra` query instead',
})
async environmentCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const envsCount = await this.adminService.environmentCountInTeam(teamID);
return envsCount;
}
@ResolveField(() => [TeamInvitation], {
description: 'Return all the pending invitations in a team',
deprecationReason: 'Use `infra` query instead',
})
async pendingInvitationCountInTeam(
@Parent() admin: Admin,
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
) {
const invitations = await this.adminService.pendingInvitationCountInTeam(
teamID,
);
return invitations;
}
@ResolveField(() => Number, {
description: 'Return total number of Users in organization',
deprecationReason: 'Use `infra` query instead',
})
async usersCount() {
return this.adminService.getUsersCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Teams in organization',
deprecationReason: 'Use `infra` query instead',
})
async teamsCount() {
return this.adminService.getTeamsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization',
deprecationReason: 'Use `infra` query instead',
})
async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization',
deprecationReason: 'Use `infra` query instead',
})
async teamRequestsCount() {
return this.adminService.getTeamRequestsCount();
}
/* Mutations */
@Mutation(() => InvitedUser, {
@@ -269,8 +70,26 @@ export class AdminResolver {
return invitedUser.right;
}
@Mutation(() => Boolean, {
description: 'Revoke a user invites by invitee emails',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async revokeUserInvitationsByAdmin(
@Args({
name: 'inviteeEmails',
description: 'Invitee Emails',
type: () => [String],
})
inviteeEmails: string[],
): Promise<boolean> {
const invite = await this.adminService.revokeUserInvitations(inviteeEmails);
if (E.isLeft(invite)) throwErr(invite.left);
return invite.right;
}
@Mutation(() => Boolean, {
description: 'Delete an user account from infra',
deprecationReason: 'Use removeUsersByAdmin instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async removeUserByAdmin(
@@ -281,12 +100,33 @@ export class AdminResolver {
})
userUID: string,
): Promise<boolean> {
const invitedUser = await this.adminService.removeUserAccount(userUID);
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
return invitedUser.right;
const removedUser = await this.adminService.removeUserAccount(userUID);
if (E.isLeft(removedUser)) throwErr(removedUser.left);
return removedUser.right;
}
@Mutation(() => [UserDeletionResult], {
description: 'Delete user accounts from infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async removeUsersByAdmin(
@Args({
name: 'userUIDs',
description: 'users UID',
type: () => [ID],
})
userUIDs: string[],
): Promise<UserDeletionResult[]> {
const deletionResults = await this.adminService.removeUserAccounts(
userUIDs,
);
if (E.isLeft(deletionResults)) throwErr(deletionResults.left);
return deletionResults.right;
}
@Mutation(() => Boolean, {
description: 'Make user an admin',
deprecationReason: 'Use makeUsersAdmin instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async makeUserAdmin(
@@ -302,8 +142,51 @@ export class AdminResolver {
return admin.right;
}
@Mutation(() => Boolean, {
description: 'Make users an admin',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async makeUsersAdmin(
@Args({
name: 'userUIDs',
description: 'users UID',
type: () => [ID],
})
userUIDs: string[],
): Promise<boolean> {
const isUpdated = await this.adminService.makeUsersAdmin(userUIDs);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return isUpdated.right;
}
@Mutation(() => Boolean, {
description: 'Update user display name',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async updateUserDisplayNameByAdmin(
@Args({
name: 'userUID',
description: 'users UID',
type: () => ID,
})
userUID: string,
@Args({
name: 'displayName',
description: 'users display name',
})
displayName: string,
): Promise<boolean> {
const isUpdated = await this.adminService.updateUserDisplayName(
userUID,
displayName,
);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return isUpdated.right;
}
@Mutation(() => Boolean, {
description: 'Remove user as admin',
deprecationReason: 'Use demoteUsersByAdmin instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async removeUserAsAdmin(
@@ -319,6 +202,23 @@ export class AdminResolver {
return admin.right;
}
@Mutation(() => Boolean, {
description: 'Remove users as admin',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async demoteUsersByAdmin(
@Args({
name: 'userUIDs',
description: 'users UID',
type: () => [ID],
})
userUIDs: string[],
): Promise<boolean> {
const isUpdated = await this.adminService.demoteUsersByAdmin(userUIDs);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return isUpdated.right;
}
@Mutation(() => Team, {
description:
'Create a new team by providing the user uid to nominate as Team owner',

View File

@@ -1,7 +1,7 @@
import { AdminService } from './admin.service';
import { PubSubService } from '../pubsub/pubsub.service';
import { mockDeep } from 'jest-mock-extended';
import { InvitedUsers } from '@prisma/client';
import { InvitedUsers, User as DbUser } from '@prisma/client';
import { UserService } from '../user/user.service';
import { TeamService } from '../team/team.service';
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
@@ -13,10 +13,15 @@ import { PrismaService } from 'src/prisma/prisma.service';
import {
DUPLICATE_EMAIL,
INVALID_EMAIL,
ONLY_ONE_ADMIN_ACCOUNT,
USER_ALREADY_INVITED,
USER_INVITATION_DELETION_FAILED,
USER_NOT_FOUND,
} from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
import * as E from 'fp-ts/Either';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -58,20 +63,87 @@ const invitedUsers: InvitedUsers[] = [
invitedOn: new Date(),
},
];
const dbAdminUsers: DbUser[] = [
{
uid: 'uid 1',
displayName: 'displayName',
email: 'email@email.com',
photoURL: 'photoURL',
isAdmin: true,
refreshToken: 'refreshToken',
currentRESTSession: '',
currentGQLSession: '',
createdOn: new Date(),
},
{
uid: 'uid 2',
displayName: 'displayName',
email: 'email@email.com',
photoURL: 'photoURL',
isAdmin: true,
refreshToken: 'refreshToken',
currentRESTSession: '',
currentGQLSession: '',
createdOn: new Date(),
},
];
const dbNonAminUser: DbUser = {
uid: 'uid 3',
displayName: 'displayName',
email: 'email@email.com',
photoURL: 'photoURL',
isAdmin: false,
refreshToken: 'refreshToken',
currentRESTSession: '',
currentGQLSession: '',
createdOn: new Date(),
};
describe('AdminService', () => {
describe('fetchInvitedUsers', () => {
test('should resolve right and return an array of invited users', async () => {
test('should resolve right and apply pagination correctly', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
// @ts-ignore
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
const results = await adminService.fetchInvitedUsers();
const paginationArgs: OffsetPaginationArgs = { take: 5, skip: 2 };
const results = await adminService.fetchInvitedUsers(paginationArgs);
expect(mockPrisma.invitedUsers.findMany).toHaveBeenCalledWith({
...paginationArgs,
orderBy: {
invitedOn: 'desc',
},
where: {
NOT: {
inviteeEmail: {
in: [dbAdminUsers[0].email],
},
},
},
});
});
test('should resolve right and return an array of invited users', async () => {
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
// @ts-ignore
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
const results = await adminService.fetchInvitedUsers(paginationArgs);
expect(results).toEqual(invitedUsers);
});
test('should resolve left and return an empty array if invited users not found', async () => {
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
const results = await adminService.fetchInvitedUsers();
const results = await adminService.fetchInvitedUsers(paginationArgs);
expect(results).toEqual([]);
});
});
@@ -134,6 +206,58 @@ describe('AdminService', () => {
});
});
describe('revokeUserInvitations', () => {
test('should resolve left and return error if email not invited', async () => {
mockPrisma.invitedUsers.deleteMany.mockRejectedValueOnce(
'RecordNotFound',
);
const result = await adminService.revokeUserInvitations([
'test@gmail.com',
]);
expect(result).toEqualLeft(USER_INVITATION_DELETION_FAILED);
});
test('should resolve right and return deleted invitee email', async () => {
const adminUid = 'adminUid';
mockPrisma.invitedUsers.deleteMany.mockResolvedValueOnce({ count: 1 });
const result = await adminService.revokeUserInvitations([
invitedUsers[0].inviteeEmail,
]);
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
where: {
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
},
});
expect(result).toEqualRight(true);
});
});
describe('removeUsersAsAdmin', () => {
test('should resolve right and make admins to users', async () => {
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
return expect(
await adminService.demoteUsersByAdmin([dbAdminUsers[0].uid]),
).toEqualRight(true);
});
test('should resolve left and return error if only one admin in the infra', async () => {
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
return expect(
await adminService.demoteUsersByAdmin(
dbAdminUsers.map((user) => user.uid),
),
).toEqualLeft(ONLY_ONE_ADMIN_ACCOUNT);
});
});
describe('getUsersCount', () => {
test('should return count of all users in the organization', async () => {
mockUserService.getUsersCount.mockResolvedValueOnce(10);

View File

@@ -6,13 +6,16 @@ import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { validateEmail } from '../utils';
import {
ADMIN_CAN_NOT_BE_DELETED,
DUPLICATE_EMAIL,
EMAIL_FAILED,
INVALID_EMAIL,
ONLY_ONE_ADMIN_ACCOUNT,
TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_NO_INVITE_FOUND,
USERS_NOT_FOUND,
USER_ALREADY_INVITED,
USER_INVITATION_DELETION_FAILED,
USER_IS_ADMIN,
USER_NOT_FOUND,
} from '../errors';
@@ -26,6 +29,8 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
import { UserDeletionResult } from 'src/user/user.model';
@Injectable()
export class AdminService {
@@ -48,12 +53,30 @@ export class AdminService {
* @param cursorID Users uid
* @param take number of users to fetch
* @returns an Either of array of user or error
* @deprecated use fetchUsersV2 instead
*/
async fetchUsers(cursorID: string, take: number) {
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
return allUsers;
}
/**
* Fetch all the users in the infra.
* @param searchString search on users displayName or email
* @param paginationOption pagination options
* @returns an Either of array of user or error
*/
async fetchUsersV2(
searchString: string,
paginationOption: OffsetPaginationArgs,
) {
const allUsers = await this.userService.fetchAllUsersV2(
searchString,
paginationOption,
);
return allUsers;
}
/**
* Invite a user to join the infra.
* @param adminUID Admin's UID
@@ -110,14 +133,68 @@ export class AdminService {
return E.right(invitedUser);
}
/**
* Update the display name of a user
* @param userUid Who's display name is being updated
* @param displayName New display name of the user
* @returns an Either of boolean or error
*/
async updateUserDisplayName(userUid: string, displayName: string) {
const updatedUser = await this.userService.updateUserDisplayName(
userUid,
displayName,
);
if (E.isLeft(updatedUser)) return E.left(updatedUser.left);
return E.right(true);
}
/**
* Revoke infra level user invitations
* @param inviteeEmails Invitee's emails
* @param adminUid Admin Uid
* @returns an Either of boolean or error string
*/
async revokeUserInvitations(inviteeEmails: string[]) {
try {
await this.prisma.invitedUsers.deleteMany({
where: {
inviteeEmail: { in: inviteeEmails },
},
});
return E.right(true);
} catch (error) {
return E.left(USER_INVITATION_DELETION_FAILED);
}
}
/**
* Fetch the list of invited users by the admin.
* @returns an Either of array of `InvitedUser` object or error
*/
async fetchInvitedUsers() {
const invitedUsers = await this.prisma.invitedUsers.findMany();
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
const userEmailObjs = await this.prisma.user.findMany({
select: {
email: true,
},
});
const users: InvitedUser[] = invitedUsers.map(
const pendingInvitedUsers = await this.prisma.invitedUsers.findMany({
take: paginationOption.take,
skip: paginationOption.skip,
orderBy: {
invitedOn: 'desc',
},
where: {
NOT: {
inviteeEmail: {
in: userEmailObjs.map((user) => user.email),
},
},
},
});
const users: InvitedUser[] = pendingInvitedUsers.map(
(user) => <InvitedUser>{ ...user },
);
@@ -337,6 +414,7 @@ export class AdminService {
* Remove a user account by UID
* @param userUid User UID
* @returns an Either of boolean or error
* @deprecated use removeUserAccounts instead
*/
async removeUserAccount(userUid: string) {
const user = await this.userService.findUserById(userUid);
@@ -349,10 +427,73 @@ export class AdminService {
return E.right(delUser.right);
}
/**
* Remove user (not Admin) accounts by UIDs
* @param userUIDs User UIDs
* @returns an Either of boolean or error
*/
async removeUserAccounts(userUIDs: string[]) {
const userDeleteResult: UserDeletionResult[] = [];
// step 1: fetch all users
const allUsersList = await this.userService.findUsersByIds(userUIDs);
if (allUsersList.length === 0) return E.left(USERS_NOT_FOUND);
// step 2: admin user can not be deleted without removing admin status/role
allUsersList.forEach((user) => {
if (user.isAdmin) {
userDeleteResult.push({
userUID: user.uid,
isDeleted: false,
errorMessage: ADMIN_CAN_NOT_BE_DELETED,
});
}
});
const nonAdminUsers = allUsersList.filter((user) => !user.isAdmin);
let deletedUserEmails: string[] = [];
// step 3: delete non-admin users
const deletionPromises = nonAdminUsers.map((user) => {
return this.userService
.deleteUserByUID(user)()
.then((res) => {
if (E.isLeft(res)) {
return {
userUID: user.uid,
isDeleted: false,
errorMessage: res.left,
} as UserDeletionResult;
}
deletedUserEmails.push(user.email);
return {
userUID: user.uid,
isDeleted: true,
errorMessage: null,
} as UserDeletionResult;
});
});
const promiseResult = await Promise.allSettled(deletionPromises);
// step 4: revoke all the invites sent to the deleted users
await this.revokeUserInvitations(deletedUserEmails);
// step 5: return the result
promiseResult.forEach((result) => {
if (result.status === 'fulfilled') {
userDeleteResult.push(result.value);
}
});
return E.right(userDeleteResult);
}
/**
* Make a user an admin
* @param userUid User UID
* @returns an Either of boolean or error
* @deprecated use makeUsersAdmin instead
*/
async makeUserAdmin(userUID: string) {
const admin = await this.userService.makeAdmin(userUID);
@@ -360,10 +501,22 @@ export class AdminService {
return E.right(true);
}
/**
* Make users to admin
* @param userUid User UIDs
* @returns an Either of boolean or error
*/
async makeUsersAdmin(userUIDs: string[]) {
const isUpdated = await this.userService.makeAdmins(userUIDs);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(true);
}
/**
* Remove user as admin
* @param userUid User UID
* @returns an Either of boolean or error
* @deprecated use demoteUsersByAdmin instead
*/
async removeUserAsAdmin(userUID: string) {
const adminUsers = await this.userService.fetchAdminUsers();
@@ -374,6 +527,26 @@ export class AdminService {
return E.right(true);
}
/**
* Remove users as admin
* @param userUIDs User UIDs
* @returns an Either of boolean or error
*/
async demoteUsersByAdmin(userUIDs: string[]) {
const adminUsers = await this.userService.fetchAdminUsers();
const remainingAdmins = adminUsers.filter(
(adminUser) => !userUIDs.includes(adminUser.uid),
);
if (remainingAdmins.length < 1) {
return E.left(ONLY_ONE_ADMIN_ACCOUNT);
}
const isUpdated = await this.userService.removeUsersAsAdmin(userUIDs);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(isUpdated.right);
}
/**
* Fetch list of all the Users in org
* @returns number of users in the org

View File

@@ -0,0 +1,11 @@
import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common';
@Injectable()
export class RESTAdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const user = request.user;
return user.isAdmin;
}
}

View File

@@ -17,7 +17,10 @@ import { AuthUser } from 'src/types/AuthUser';
import { throwErr } from 'src/utils';
import * as E from 'fp-ts/Either';
import { Admin } from './admin.model';
import { PaginationArgs } from 'src/types/input-types.args';
import {
OffsetPaginationArgs,
PaginationArgs,
} from 'src/types/input-types.args';
import { InvitedUser } from './invited-user.model';
import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
@@ -29,7 +32,8 @@ import {
EnableAndDisableSSOArgs,
InfraConfigArgs,
} from 'src/infra-config/input-args';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import { ServiceStatus } from 'src/infra-config/helper';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra)
@@ -76,6 +80,7 @@ export class InfraResolver {
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
deprecationReason: 'Use allUsersV2 instead',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
@@ -83,11 +88,33 @@ export class InfraResolver {
return users;
}
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsersV2(
@Args({
name: 'searchString',
nullable: true,
description: 'Search on users displayName or email',
})
searchString: string,
@Args() paginationOption: OffsetPaginationArgs,
): Promise<AuthUser[]> {
const users = await this.adminService.fetchUsersV2(
searchString,
paginationOption,
);
return users;
}
@ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users',
})
async invitedUsers(): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
async invitedUsers(
@Args() args: OffsetPaginationArgs,
): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers(args);
return users;
}
@@ -247,10 +274,10 @@ export class InfraResolver {
async infraConfigs(
@Args({
name: 'configNames',
type: () => [InfraConfigEnumForClient],
type: () => [InfraConfigEnum],
description: 'Configs to fetch',
})
names: InfraConfigEnumForClient[],
names: InfraConfigEnum[],
) {
const infraConfigs = await this.infraConfigService.getMany(names);
if (E.isLeft(infraConfigs)) throwErr(infraConfigs.left);
@@ -284,6 +311,25 @@ export class InfraResolver {
return updatedRes.right;
}
@Mutation(() => Boolean, {
description: 'Enable or disable analytics collection',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async toggleAnalyticsCollection(
@Args({
name: 'status',
type: () => ServiceStatus,
description: 'Toggle analytics collection',
})
analyticsCollectionStatus: ServiceStatus,
) {
const res = await this.infraConfigService.toggleAnalyticsCollection(
analyticsCollectionStatus,
);
if (E.isLeft(res)) throwErr(res.left);
return res.right;
}
@Mutation(() => Boolean, {
description: 'Reset Infra Configs with default values (.env)',
})
@@ -306,7 +352,9 @@ export class InfraResolver {
})
providerInfo: EnableAndDisableSSOArgs[],
) {
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
const isUpdated = await this.infraConfigService.enableAndDisableSSO(
providerInfo,
);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return true;

View File

@@ -24,6 +24,8 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { InfraConfigModule } from './infra-config/infra-config.module';
import { loadInfraConfiguration } from './infra-config/helper';
import { MailerModule } from './mailer/mailer.module';
import { PosthogModule } from './posthog/posthog.module';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [
@@ -96,6 +98,8 @@ import { MailerModule } from './mailer/mailer.module';
UserCollectionModule,
ShortcodeModule,
InfraConfigModule,
PosthogModule,
ScheduleModule.forRoot(),
],
providers: [GQLComplexityPlugin],
controllers: [AppController],

View File

@@ -18,12 +18,7 @@ import { JwtAuthGuard } from './guards/jwt-auth.guard';
import { GqlUser } from 'src/decorators/gql-user.decorator';
import { AuthUser } from 'src/types/AuthUser';
import { RTCookie } from 'src/decorators/rt-cookie.decorator';
import {
AuthProvider,
authCookieHandler,
authProviderCheck,
throwHTTPErr,
} from './helper';
import { AuthProvider, authCookieHandler, authProviderCheck } from './helper';
import { GoogleSSOGuard } from './guards/google-sso.guard';
import { GithubSSOGuard } from './guards/github-sso.guard';
import { MicrosoftSSOGuard } from './guards/microsoft-sso-.guard';
@@ -31,6 +26,7 @@ import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.gua
import { SkipThrottle } from '@nestjs/throttler';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { throwHTTPErr } from 'src/utils';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'auth', version: '1' })

View File

@@ -12,7 +12,10 @@ import { GithubStrategy } from './strategies/github.strategy';
import { MicrosoftStrategy } from './strategies/microsoft.strategy';
import { AuthProvider, authProviderCheck } from './helper';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
import {
isInfraConfigTablePopulated,
loadInfraConfiguration,
} from 'src/infra-config/helper';
import { InfraConfigModule } from 'src/infra-config/infra-config.module';
@Module({
@@ -34,6 +37,11 @@ import { InfraConfigModule } from 'src/infra-config/infra-config.module';
})
export class AuthModule {
static async register() {
const isInfraConfigPopulated = await isInfraConfigTablePopulated();
if (!isInfraConfigPopulated) {
return { module: AuthModule };
}
const env = await loadInfraConfiguration();
const allowedAuthProviders = env.INFRA.VITE_ALLOWED_AUTH_PROVIDERS;

View File

@@ -24,7 +24,7 @@ import {
RefreshTokenPayload,
} from 'src/types/AuthTokens';
import { JwtService } from '@nestjs/jwt';
import { AuthError } from 'src/types/AuthError';
import { RESTError } from 'src/types/RESTError';
import { AuthUser, IsAdmin } from 'src/types/AuthUser';
import { VerificationToken } from '@prisma/client';
import { Origin } from './helper';
@@ -117,7 +117,7 @@ export class AuthService {
userUid,
);
if (E.isLeft(updatedUser))
return E.left(<AuthError>{
return E.left(<RESTError>{
message: updatedUser.left,
statusCode: HttpStatus.NOT_FOUND,
});
@@ -255,7 +255,7 @@ export class AuthService {
*/
async verifyMagicLinkTokens(
magicLinkIDTokens: VerifyMagicDto,
): Promise<E.Right<AuthTokens> | E.Left<AuthError>> {
): Promise<E.Right<AuthTokens> | E.Left<RESTError>> {
const passwordlessTokens = await this.validatePasswordlessTokens(
magicLinkIDTokens,
);
@@ -373,7 +373,7 @@ export class AuthService {
if (usersCount === 1) {
const elevatedUser = await this.usersService.makeAdmin(user.uid);
if (E.isLeft(elevatedUser))
return E.left(<AuthError>{
return E.left(<RESTError>{
message: elevatedUser.left,
statusCode: HttpStatus.NOT_FOUND,
});

View File

@@ -1,9 +1,10 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { AuthProvider, authProviderCheck } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { throwHTTPErr } from 'src/utils';
@Injectable()
export class GithubSSOGuard extends AuthGuard('github') implements CanActivate {

View File

@@ -1,9 +1,10 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { AuthProvider, authProviderCheck } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { throwHTTPErr } from 'src/utils';
@Injectable()
export class GoogleSSOGuard extends AuthGuard('google') implements CanActivate {

View File

@@ -1,9 +1,10 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthProvider, authProviderCheck, throwHTTPErr } from '../helper';
import { AuthProvider, authProviderCheck } from '../helper';
import { Observable } from 'rxjs';
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { throwHTTPErr } from 'src/utils';
@Injectable()
export class MicrosoftSSOGuard

View File

@@ -1,6 +1,5 @@
import { HttpException, HttpStatus } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AuthError } from 'src/types/AuthError';
import { AuthTokens } from 'src/types/AuthTokens';
import { Response } from 'express';
import * as cookie from 'cookie';
@@ -25,15 +24,6 @@ export enum AuthProvider {
EMAIL = 'EMAIL',
}
/**
* This function allows throw to be used as an expression
* @param errMessage Message present in the error message
*/
export function throwHTTPErr(errorData: AuthError): never {
const { message, statusCode } = errorData;
throw new HttpException(message, statusCode);
}
/**
* Sets and returns the cookies in the response object on successful authentication
* @param res Express Response Object

View File

@@ -17,8 +17,8 @@ export class GithubStrategy extends PassportStrategy(Strategy) {
super({
clientID: configService.get('INFRA.GITHUB_CLIENT_ID'),
clientSecret: configService.get('INFRA.GITHUB_CLIENT_SECRET'),
callbackURL: configService.get('GITHUB_CALLBACK_URL'),
scope: [configService.get('GITHUB_SCOPE')],
callbackURL: configService.get('INFRA.GITHUB_CALLBACK_URL'),
scope: [configService.get('INFRA.GITHUB_SCOPE')],
store: true,
});
}

View File

@@ -17,8 +17,8 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
super({
clientID: configService.get('INFRA.GOOGLE_CLIENT_ID'),
clientSecret: configService.get('INFRA.GOOGLE_CLIENT_SECRET'),
callbackURL: configService.get('GOOGLE_CALLBACK_URL'),
scope: configService.get('GOOGLE_SCOPE').split(','),
callbackURL: configService.get('INFRA.GOOGLE_CALLBACK_URL'),
scope: configService.get('INFRA.GOOGLE_SCOPE').split(','),
passReqToCallback: true,
store: true,
});

View File

@@ -17,9 +17,9 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
super({
clientID: configService.get('INFRA.MICROSOFT_CLIENT_ID'),
clientSecret: configService.get('INFRA.MICROSOFT_CLIENT_SECRET'),
callbackURL: configService.get('MICROSOFT_CALLBACK_URL'),
scope: [configService.get('MICROSOFT_SCOPE')],
tenant: configService.get('MICROSOFT_TENANT'),
callbackURL: configService.get('INFRA.MICROSOFT_CALLBACK_URL'),
scope: [configService.get('INFRA.MICROSOFT_SCOPE')],
tenant: configService.get('INFRA.MICROSOFT_TENANT'),
store: true,
});
}

View File

@@ -10,6 +10,14 @@ export const DUPLICATE_EMAIL = 'email/both_emails_cannot_be_same' as const;
export const ONLY_ONE_ADMIN_ACCOUNT =
'admin/only_one_admin_account_found' as const;
/**
* Admin user can not be deleted
* To delete the admin user, first make the Admin user a normal user
* (AdminService)
*/
export const ADMIN_CAN_NOT_BE_DELETED =
'admin/admin_can_not_be_deleted' as const;
/**
* Token Authorization failed (Check 'Authorization' Header)
* (GqlAuthGuard)
@@ -28,6 +36,13 @@ export const JSON_INVALID = 'json_invalid';
*/
export const AUTH_PROVIDER_NOT_SPECIFIED = 'auth/provider_not_specified';
/**
* Auth Provider not specified
* (Auth)
*/
export const AUTH_PROVIDER_NOT_CONFIGURED =
'auth/provider_not_configured_correctly';
/**
* Environment variable "VITE_ALLOWED_AUTH_PROVIDERS" is not present in .env file
*/
@@ -92,6 +107,13 @@ export const USER_IS_OWNER = 'user/is_owner' as const;
*/
export const USER_IS_ADMIN = 'user/is_admin' as const;
/**
* User invite deletion failure error due to invitation not found
* (AdminService)
*/
export const USER_INVITATION_DELETION_FAILED =
'user/invitation_deletion_failed' as const;
/**
* Teams not found
* (TeamsService)
@@ -206,6 +228,12 @@ export const TEAM_COL_NOT_SAME_PARENT =
export const TEAM_COL_SAME_NEXT_COLL =
'team_coll/collection_and_next_collection_are_same';
/**
* Team Collection search failed
* (TeamCollectionService)
*/
export const TEAM_COL_SEARCH_FAILED = 'team_coll/team_collection_search_failed';
/**
* Team Collection Re-Ordering Failed
* (TeamCollectionService)
@@ -261,6 +289,13 @@ export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
export const TEAM_COLL_DATA_INVALID =
'team_coll/team_coll_data_invalid' as const;
/**
* Team Collection parent tree generation failed
* (TeamCollectionService)
*/
export const TEAM_COLL_PARENT_TREE_GEN_FAILED =
'team_coll/team_coll_parent_tree_generation_failed';
/**
* Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard)
@@ -286,6 +321,19 @@ export const TEAM_REQ_INVALID_TARGET_COLL_ID =
*/
export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
/**
* Team Request search failed
* (TeamRequestService)
*/
export const TEAM_REQ_SEARCH_FAILED = 'team_req/team_request_search_failed';
/**
* Team Request parent tree generation failed
* (TeamRequestService)
*/
export const TEAM_REQ_PARENT_TREE_GEN_FAILED =
'team_req/team_req_parent_tree_generation_failed';
/**
* No Postmark Sender Email defined
* (AuthService)
@@ -683,9 +731,22 @@ export const INFRA_CONFIG_INVALID_INPUT = 'infra_config/invalid_input' as const;
export const INFRA_CONFIG_SERVICE_NOT_CONFIGURED =
'infra_config/service_not_configured' as const;
/**
* Infra Config update/fetch operation not allowed
* (InfraConfigService)
*/
export const INFRA_CONFIG_OPERATION_NOT_ALLOWED =
'infra_config/operation_not_allowed';
/**
* Error message for when the database table does not exist
* (InfraConfigService)
*/
export const DATABASE_TABLE_NOT_EXIST =
'Database migration not found. Please check the documentation for assistance: https://docs.hoppscotch.io/documentation/self-host/community-edition/install-and-build#running-migrations';
/**
* PostHog client is not initialized
* (InfraConfigService)
*/
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';

View File

@@ -1,10 +1,44 @@
import { AuthProvider } from 'src/auth/helper';
import {
AUTH_PROVIDER_NOT_CONFIGURED,
DATABASE_TABLE_NOT_EXIST,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import { throwErr } from 'src/utils';
import { randomBytes } from 'crypto';
export enum ServiceStatus {
ENABLE = 'ENABLE',
DISABLE = 'DISABLE',
}
const AuthProviderConfigurations = {
[AuthProvider.GOOGLE]: [
InfraConfigEnum.GOOGLE_CLIENT_ID,
InfraConfigEnum.GOOGLE_CLIENT_SECRET,
InfraConfigEnum.GOOGLE_CALLBACK_URL,
InfraConfigEnum.GOOGLE_SCOPE,
],
[AuthProvider.GITHUB]: [
InfraConfigEnum.GITHUB_CLIENT_ID,
InfraConfigEnum.GITHUB_CLIENT_SECRET,
InfraConfigEnum.GITHUB_CALLBACK_URL,
InfraConfigEnum.GITHUB_SCOPE,
],
[AuthProvider.MICROSOFT]: [
InfraConfigEnum.MICROSOFT_CLIENT_ID,
InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
InfraConfigEnum.MICROSOFT_CALLBACK_URL,
InfraConfigEnum.MICROSOFT_SCOPE,
InfraConfigEnum.MICROSOFT_TENANT,
],
[AuthProvider.EMAIL]: [
InfraConfigEnum.MAILER_SMTP_URL,
InfraConfigEnum.MAILER_ADDRESS_FROM,
],
};
/**
* Load environment variables from the database and set them in the process
*
@@ -30,6 +64,125 @@ export async function loadInfraConfiguration() {
}
}
/**
* Read the default values from .env file and return them as an array
* @returns Array of default infra configs
*/
export async function getDefaultInfraConfigs(): Promise<
{ name: InfraConfigEnum; value: string }[]
> {
const prisma = new PrismaService();
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
{
name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL,
},
{
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
value: process.env.GOOGLE_CLIENT_SECRET,
},
{
name: InfraConfigEnum.GOOGLE_CALLBACK_URL,
value: process.env.GOOGLE_CALLBACK_URL,
},
{
name: InfraConfigEnum.GOOGLE_SCOPE,
value: process.env.GOOGLE_SCOPE,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_ID,
value: process.env.GITHUB_CLIENT_ID,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
value: process.env.GITHUB_CLIENT_SECRET,
},
{
name: InfraConfigEnum.GITHUB_CALLBACK_URL,
value: process.env.GITHUB_CALLBACK_URL,
},
{
name: InfraConfigEnum.GITHUB_SCOPE,
value: process.env.GITHUB_SCOPE,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
value: process.env.MICROSOFT_CLIENT_ID,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
value: process.env.MICROSOFT_CLIENT_SECRET,
},
{
name: InfraConfigEnum.MICROSOFT_CALLBACK_URL,
value: process.env.MICROSOFT_CALLBACK_URL,
},
{
name: InfraConfigEnum.MICROSOFT_SCOPE,
value: process.env.MICROSOFT_SCOPE,
},
{
name: InfraConfigEnum.MICROSOFT_TENANT,
value: process.env.MICROSOFT_TENANT,
},
{
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: getConfiguredSSOProviders(),
},
{
name: InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
value: false.toString(),
},
{
name: InfraConfigEnum.ANALYTICS_USER_ID,
value: generateAnalyticsUserId(),
},
{
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
value: (await prisma.infraConfig.count()) === 0 ? 'true' : 'false',
},
];
return infraConfigDefaultObjs;
}
/**
* Verify if 'infra_config' table is loaded with all entries
* @returns boolean
*/
export async function isInfraConfigTablePopulated(): Promise<boolean> {
const prisma = new PrismaService();
try {
const dbInfraConfigs = await prisma.infraConfig.findMany();
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
const propsRemainingToInsert = infraConfigDefaultObjs.filter(
(p) => !dbInfraConfigs.find((e) => e.name === p.name),
);
if (propsRemainingToInsert.length > 0) {
console.log(
'Infra Config table is not populated with all entries. Populating now...',
);
return false;
}
return true;
} catch (error) {
return false;
}
}
/**
* Stop the app after 5 seconds
* (Docker will re-start the app)
@@ -42,3 +195,51 @@ export function stopApp() {
process.kill(process.pid, 'SIGTERM');
}, 5000);
}
/**
* Get the configured SSO providers
* @returns Array of configured SSO providers
*/
export function getConfiguredSSOProviders() {
const allowedAuthProviders: string[] =
process.env.VITE_ALLOWED_AUTH_PROVIDERS.split(',');
let configuredAuthProviders: string[] = [];
const addProviderIfConfigured = (provider) => {
const configParameters: string[] = AuthProviderConfigurations[provider];
const isConfigured = configParameters.every((configParameter) => {
return process.env[configParameter];
});
if (isConfigured) configuredAuthProviders.push(provider);
};
allowedAuthProviders.forEach((provider) => addProviderIfConfigured(provider));
if (configuredAuthProviders.length === 0) {
throwErr(AUTH_PROVIDER_NOT_CONFIGURED);
} else if (allowedAuthProviders.length !== configuredAuthProviders.length) {
const unConfiguredAuthProviders = allowedAuthProviders.filter(
(provider) => {
return !configuredAuthProviders.includes(provider);
},
);
console.log(
`${unConfiguredAuthProviders.join(
',',
)} SSO auth provider(s) are not configured properly. Do configure them from Admin Dashboard.`,
);
}
return configuredAuthProviders.join(',');
}
/**
* Generate a hashed valued for analytics
* @returns Generated hashed value
*/
export function generateAnalyticsUserId() {
const hashedUserID = randomBytes(20).toString('hex');
return hashedUserID;
}

View File

@@ -0,0 +1,47 @@
import { Controller, Get, HttpStatus, Put, UseGuards } from '@nestjs/common';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { InfraConfigService } from './infra-config.service';
import * as E from 'fp-ts/Either';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
import { RESTError } from 'src/types/RESTError';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import { throwHTTPErr } from 'src/utils';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'site', version: '1' })
export class SiteController {
constructor(private infraConfigService: InfraConfigService) {}
@Get('setup')
@UseGuards(JwtAuthGuard, RESTAdminGuard)
async fetchSetupInfo() {
const status = await this.infraConfigService.get(
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
);
if (E.isLeft(status))
throwHTTPErr(<RESTError>{
message: status.left,
statusCode: HttpStatus.NOT_FOUND,
});
return status.right;
}
@Put('setup')
@UseGuards(JwtAuthGuard, RESTAdminGuard)
async setSetupAsComplete() {
const res = await this.infraConfigService.update(
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
false.toString(),
false,
);
if (E.isLeft(res))
throwHTTPErr(<RESTError>{
message: res.left,
statusCode: HttpStatus.FORBIDDEN,
});
return res.right;
}
}

View File

@@ -1,6 +1,6 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { AuthProvider } from 'src/auth/helper';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper';
@ObjectType()
@@ -8,7 +8,7 @@ export class InfraConfig {
@Field({
description: 'Infra Config Name',
})
name: InfraConfigEnumForClient;
name: InfraConfigEnum;
@Field({
description: 'Infra Config Value',
@@ -16,7 +16,7 @@ export class InfraConfig {
value: string;
}
registerEnumType(InfraConfigEnumForClient, {
registerEnumType(InfraConfigEnum, {
name: 'InfraConfigEnum',
});

View File

@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common';
import { InfraConfigService } from './infra-config.service';
import { PrismaModule } from 'src/prisma/prisma.module';
import { SiteController } from './infra-config.controller';
@Module({
imports: [PrismaModule],
providers: [InfraConfigService],
exports: [InfraConfigService],
controllers: [SiteController],
})
export class InfraConfigModule {}

View File

@@ -1,13 +1,16 @@
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfigService } from './infra-config.service';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import {
InfraConfigEnum,
InfraConfigEnumForClient,
} from 'src/types/InfraConfig';
import { INFRA_CONFIG_NOT_FOUND, INFRA_CONFIG_UPDATE_FAILED } from 'src/errors';
INFRA_CONFIG_NOT_FOUND,
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
INFRA_CONFIG_UPDATE_FAILED,
} from 'src/errors';
import { ConfigService } from '@nestjs/config';
import * as helper from './helper';
import { InfraConfig as dbInfraConfig } from '@prisma/client';
import { InfraConfig } from './infra-config.model';
const mockPrisma = mockDeep<PrismaService>();
const mockConfigService = mockDeep<ConfigService>();
@@ -19,12 +22,82 @@ const infraConfigService = new InfraConfigService(
mockConfigService,
);
const INITIALIZED_DATE_CONST = new Date();
const dbInfraConfigs: dbInfraConfig[] = [
{
id: '3',
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: 'abcdefghijkl',
active: true,
createdOn: INITIALIZED_DATE_CONST,
updatedOn: INITIALIZED_DATE_CONST,
},
{
id: '4',
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: 'google',
active: true,
createdOn: INITIALIZED_DATE_CONST,
updatedOn: INITIALIZED_DATE_CONST,
},
];
const infraConfigs: InfraConfig[] = [
{
name: dbInfraConfigs[0].name as InfraConfigEnum,
value: dbInfraConfigs[0].value,
},
{
name: dbInfraConfigs[1].name as InfraConfigEnum,
value: dbInfraConfigs[1].value,
},
];
beforeEach(() => {
mockReset(mockPrisma);
});
describe('InfraConfigService', () => {
describe('update', () => {
it('should update the infra config without backend server restart', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.update.mockResolvedValueOnce({
id: '',
name,
value,
active: true,
createdOn: new Date(),
updatedOn: new Date(),
});
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
const result = await infraConfigService.update(name, value);
expect(helper.stopApp).not.toHaveBeenCalled();
expect(result).toEqualRight({ name, value });
});
it('should update the infra config with backend server restart', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.update.mockResolvedValueOnce({
id: '',
name,
value,
active: true,
createdOn: new Date(),
updatedOn: new Date(),
});
jest.spyOn(helper, 'stopApp').mockReturnValueOnce();
const result = await infraConfigService.update(name, value, true);
expect(helper.stopApp).toHaveBeenCalledTimes(1);
expect(result).toEqualRight({ name, value });
});
it('should update the infra config', async () => {
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
@@ -71,7 +144,7 @@ describe('InfraConfigService', () => {
describe('get', () => {
it('should get the infra config', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
const value = 'true';
mockPrisma.infraConfig.findUniqueOrThrow.mockResolvedValueOnce({
@@ -87,7 +160,7 @@ describe('InfraConfigService', () => {
});
it('should pass correct params to prisma findUnique', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
await infraConfigService.get(name);
@@ -98,7 +171,7 @@ describe('InfraConfigService', () => {
});
it('should throw an error if the infra config does not exist', async () => {
const name = InfraConfigEnumForClient.GOOGLE_CLIENT_ID;
const name = InfraConfigEnum.GOOGLE_CLIENT_ID;
mockPrisma.infraConfig.findUniqueOrThrow.mockRejectedValueOnce('null');
@@ -106,4 +179,45 @@ describe('InfraConfigService', () => {
expect(result).toEqualLeft(INFRA_CONFIG_NOT_FOUND);
});
});
describe('getMany', () => {
it('should throw error if any disallowed names are provided', async () => {
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
const result = await infraConfigService.getMany(disallowedNames);
expect(result).toEqualLeft(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
});
it('should resolve right with disallowed names if `checkDisallowed` parameter passed', async () => {
const disallowedNames = [InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS];
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
disallowedNames.includes(dbConfig.name as InfraConfigEnum),
);
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
dbInfraConfigResponses,
);
const result = await infraConfigService.getMany(disallowedNames, false);
expect(result).toEqualRight(
infraConfigs.filter((i) => disallowedNames.includes(i.name)),
);
});
it('should return right with infraConfigs if Prisma query succeeds', async () => {
const allowedNames = [InfraConfigEnum.GOOGLE_CLIENT_ID];
const dbInfraConfigResponses = dbInfraConfigs.filter((dbConfig) =>
allowedNames.includes(dbConfig.name as InfraConfigEnum),
);
mockPrisma.infraConfig.findMany.mockResolvedValueOnce(
dbInfraConfigResponses,
);
const result = await infraConfigService.getMany(allowedNames);
expect(result).toEqualRight(
infraConfigs.filter((i) => allowedNames.includes(i.name)),
);
});
});
});

View File

@@ -3,23 +3,25 @@ import { InfraConfig } from './infra-config.model';
import { PrismaService } from 'src/prisma/prisma.service';
import { InfraConfig as DBInfraConfig } from '@prisma/client';
import * as E from 'fp-ts/Either';
import {
InfraConfigEnum,
InfraConfigEnumForClient,
} from 'src/types/InfraConfig';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import {
AUTH_PROVIDER_NOT_SPECIFIED,
DATABASE_TABLE_NOT_EXIST,
INFRA_CONFIG_INVALID_INPUT,
INFRA_CONFIG_NOT_FOUND,
INFRA_CONFIG_NOT_LISTED,
INFRA_CONFIG_RESET_FAILED,
INFRA_CONFIG_UPDATE_FAILED,
INFRA_CONFIG_SERVICE_NOT_CONFIGURED,
INFRA_CONFIG_OPERATION_NOT_ALLOWED,
} from 'src/errors';
import { throwErr, validateEmail, validateSMTPUrl } from 'src/utils';
import {
throwErr,
validateSMTPEmail,
validateSMTPUrl,
validateUrl,
} from 'src/utils';
import { ConfigService } from '@nestjs/config';
import { ServiceStatus, stopApp } from './helper';
import { ServiceStatus, getDefaultInfraConfigs, stopApp } from './helper';
import { EnableAndDisableSSOArgs, InfraConfigArgs } from './input-args';
import { AuthProvider } from 'src/auth/helper';
@@ -30,70 +32,32 @@ export class InfraConfigService implements OnModuleInit {
private readonly configService: ConfigService,
) {}
// Following fields are not updatable by `infraConfigs` Mutation. Use dedicated mutations for these fields instead.
EXCLUDE_FROM_UPDATE_CONFIGS = [
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
];
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
EXCLUDE_FROM_FETCH_CONFIGS = [
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
];
async onModuleInit() {
await this.initializeInfraConfigTable();
}
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
{
name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL,
},
{
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_SECRET,
value: process.env.GOOGLE_CLIENT_SECRET,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_ID,
value: process.env.GITHUB_CLIENT_ID,
},
{
name: InfraConfigEnum.GITHUB_CLIENT_SECRET,
value: process.env.GITHUB_CLIENT_SECRET,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_ID,
value: process.env.MICROSOFT_CLIENT_ID,
},
{
name: InfraConfigEnum.MICROSOFT_CLIENT_SECRET,
value: process.env.MICROSOFT_CLIENT_SECRET,
},
{
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
value: process.env.VITE_ALLOWED_AUTH_PROVIDERS.toLocaleUpperCase(),
},
];
return infraConfigDefaultObjs;
}
/**
* Initialize the 'infra_config' table with values from .env
* @description This function create rows 'infra_config' in very first time (only once)
*/
async initializeInfraConfigTable() {
try {
// Get all the 'names' of the properties to be saved in the 'infra_config' table
const enumValues = Object.values(InfraConfigEnum);
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
// Check if all the 'names' are listed in the default values
if (enumValues.length !== infraConfigDefaultObjs.length) {
throw new Error(INFRA_CONFIG_NOT_LISTED);
}
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
// Eliminate the rows (from 'infraConfigDefaultObjs') that are already present in the database table
const dbInfraConfigs = await this.prisma.infraConfig.findMany();
@@ -130,16 +94,27 @@ export class InfraConfigService implements OnModuleInit {
};
}
/**
* Get all the InfraConfigs as map
* @returns InfraConfig map
*/
async getInfraConfigsMap() {
const infraConfigs = await this.prisma.infraConfig.findMany();
const infraConfigMap: Record<string, string> = {};
infraConfigs.forEach((config) => {
infraConfigMap[config.name] = config.value;
});
return infraConfigMap;
}
/**
* Update InfraConfig by name
* @param name Name of the InfraConfig
* @param value Value of the InfraConfig
* @param restartEnabled If true, restart the app after updating the InfraConfig
* @returns InfraConfig model
*/
async update(
name: InfraConfigEnumForClient | InfraConfigEnum,
value: string,
) {
async update(name: InfraConfigEnum, value: string, restartEnabled = false) {
const isValidate = this.validateEnvValues([{ name, value }]);
if (E.isLeft(isValidate)) return E.left(isValidate.left);
@@ -149,7 +124,7 @@ export class InfraConfigService implements OnModuleInit {
data: { value },
});
stopApp();
if (restartEnabled) stopApp();
return E.right(this.cast(infraConfig));
} catch (e) {
@@ -163,6 +138,11 @@ export class InfraConfigService implements OnModuleInit {
* @returns InfraConfig model
*/
async updateMany(infraConfigs: InfraConfigArgs[]) {
for (let i = 0; i < infraConfigs.length; i++) {
if (this.EXCLUDE_FROM_UPDATE_CONFIGS.includes(infraConfigs[i].name))
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
}
const isValidate = this.validateEnvValues(infraConfigs);
if (E.isLeft(isValidate)) return E.left(isValidate.left);
@@ -187,35 +167,59 @@ export class InfraConfigService implements OnModuleInit {
/**
* Check if the service is configured or not
* @param service Service can be Auth Provider, Mailer, Audit Log etc.
* @param configMap Map of all the infra configs
* @returns Either true or false
*/
isServiceConfigured(service: AuthProvider) {
isServiceConfigured(
service: AuthProvider,
configMap: Record<string, string>,
) {
switch (service) {
case AuthProvider.GOOGLE:
return (
this.configService.get<string>('INFRA.GOOGLE_CLIENT_ID') &&
this.configService.get<string>('INFRA.GOOGLE_CLIENT_SECRET')
configMap.GOOGLE_CLIENT_ID &&
configMap.GOOGLE_CLIENT_SECRET &&
configMap.GOOGLE_CALLBACK_URL &&
configMap.GOOGLE_SCOPE
);
case AuthProvider.GITHUB:
return (
this.configService.get<string>('INFRA.GITHUB_CLIENT_ID') &&
!this.configService.get<string>('INFRA.GITHUB_CLIENT_SECRET')
configMap.GITHUB_CLIENT_ID &&
configMap.GITHUB_CLIENT_SECRET &&
configMap.GITHUB_CALLBACK_URL &&
configMap.GITHUB_SCOPE
);
case AuthProvider.MICROSOFT:
return (
this.configService.get<string>('INFRA.MICROSOFT_CLIENT_ID') &&
!this.configService.get<string>('INFRA.MICROSOFT_CLIENT_SECRET')
configMap.MICROSOFT_CLIENT_ID &&
configMap.MICROSOFT_CLIENT_SECRET &&
configMap.MICROSOFT_CALLBACK_URL &&
configMap.MICROSOFT_SCOPE &&
configMap.MICROSOFT_TENANT
);
case AuthProvider.EMAIL:
return (
this.configService.get<string>('INFRA.MAILER_SMTP_URL') &&
this.configService.get<string>('INFRA.MAILER_ADDRESS_FROM')
);
return configMap.MAILER_SMTP_URL && configMap.MAILER_ADDRESS_FROM;
default:
return false;
}
}
/**
* Enable or Disable Analytics Collection
*
* @param status Status to enable or disable
* @returns Boolean of status of analytics collection
*/
async toggleAnalyticsCollection(status: ServiceStatus) {
const isUpdated = await this.update(
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
status === ServiceStatus.ENABLE ? 'true' : 'false',
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(isUpdated.right.value === 'true');
}
/**
* Enable or Disable SSO for login/signup
* @param provider Auth Provider to enable or disable
@@ -229,11 +233,11 @@ export class InfraConfigService implements OnModuleInit {
let updatedAuthProviders = allowedAuthProviders;
for (let i = 0; i < providerInfo.length; i++) {
const { provider, status } = providerInfo[i];
const infraConfigMap = await this.getInfraConfigsMap();
providerInfo.forEach(({ provider, status }) => {
if (status === ServiceStatus.ENABLE) {
const isConfigured = this.isServiceConfigured(provider);
const isConfigured = this.isServiceConfigured(provider, infraConfigMap);
if (!isConfigured) {
throwErr(INFRA_CONFIG_SERVICE_NOT_CONFIGURED);
}
@@ -243,7 +247,7 @@ export class InfraConfigService implements OnModuleInit {
(p) => p !== provider,
);
}
}
});
updatedAuthProviders = [...new Set(updatedAuthProviders)];
@@ -254,6 +258,7 @@ export class InfraConfigService implements OnModuleInit {
const isUpdated = await this.update(
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
updatedAuthProviders.join(','),
true,
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
@@ -265,7 +270,7 @@ export class InfraConfigService implements OnModuleInit {
* @param name Name of the InfraConfig
* @returns InfraConfig model
*/
async get(name: InfraConfigEnumForClient) {
async get(name: InfraConfigEnum) {
try {
const infraConfig = await this.prisma.infraConfig.findUniqueOrThrow({
where: { name },
@@ -282,7 +287,15 @@ export class InfraConfigService implements OnModuleInit {
* @param names Names of the InfraConfigs
* @returns InfraConfig model
*/
async getMany(names: InfraConfigEnumForClient[]) {
async getMany(names: InfraConfigEnum[], checkDisallowedKeys: boolean = true) {
if (checkDisallowedKeys) {
// Check if the names are allowed to fetch by client
for (let i = 0; i < names.length; i++) {
if (this.EXCLUDE_FROM_FETCH_CONFIGS.includes(names[i]))
return E.left(INFRA_CONFIG_OPERATION_NOT_ALLOWED);
}
}
try {
const infraConfigs = await this.prisma.infraConfig.findMany({
where: { name: { in: names } },
@@ -308,14 +321,28 @@ export class InfraConfigService implements OnModuleInit {
* Reset all the InfraConfigs to their default values (from .env)
*/
async reset() {
// These are all the infra-configs that should not be reset
const RESET_EXCLUSION_LIST = [
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
];
try {
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(p) => RESET_EXCLUSION_LIST.includes(p.name) === false,
);
await this.prisma.infraConfig.deleteMany({
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
where: {
name: {
in: updatedInfraConfigDefaultObjs.map((p) => p.name),
},
},
});
await this.prisma.infraConfig.createMany({
data: infraConfigDefaultObjs,
data: updatedInfraConfigDefaultObjs,
});
stopApp();
@@ -331,36 +358,60 @@ export class InfraConfigService implements OnModuleInit {
*/
validateEnvValues(
infraConfigs: {
name: InfraConfigEnumForClient | InfraConfigEnum;
name: InfraConfigEnum;
value: string;
}[],
) {
for (let i = 0; i < infraConfigs.length; i++) {
switch (infraConfigs[i].name) {
case InfraConfigEnumForClient.MAILER_SMTP_URL:
case InfraConfigEnum.MAILER_SMTP_URL:
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MAILER_ADDRESS_FROM:
const isValidEmail = validateEmail(infraConfigs[i].value);
case InfraConfigEnum.MAILER_ADDRESS_FROM:
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GOOGLE_CLIENT_ID:
case InfraConfigEnum.GOOGLE_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GOOGLE_CLIENT_SECRET:
case InfraConfigEnum.GOOGLE_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GITHUB_CLIENT_ID:
case InfraConfigEnum.GOOGLE_CALLBACK_URL:
if (!validateUrl(infraConfigs[i].value))
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.GOOGLE_SCOPE:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.GITHUB_CLIENT_SECRET:
case InfraConfigEnum.GITHUB_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MICROSOFT_CLIENT_ID:
case InfraConfigEnum.GITHUB_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnumForClient.MICROSOFT_CLIENT_SECRET:
case InfraConfigEnum.GITHUB_CALLBACK_URL:
if (!validateUrl(infraConfigs[i].value))
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.GITHUB_SCOPE:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MICROSOFT_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MICROSOFT_CLIENT_SECRET:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MICROSOFT_CALLBACK_URL:
if (!validateUrl(infraConfigs[i].value))
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MICROSOFT_SCOPE:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MICROSOFT_TENANT:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
default:

View File

@@ -1,14 +1,14 @@
import { Field, InputType } from '@nestjs/graphql';
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
import { InfraConfigEnum } from 'src/types/InfraConfig';
import { ServiceStatus } from './helper';
import { AuthProvider } from 'src/auth/helper';
@InputType()
export class InfraConfigArgs {
@Field(() => InfraConfigEnumForClient, {
@Field(() => InfraConfigEnum, {
description: 'Infra Config Name',
})
name: InfraConfigEnumForClient;
name: InfraConfigEnum;
@Field({
description: 'Infra Config Value',

View File

@@ -25,7 +25,7 @@ export class MailerService {
): string {
switch (mailDesc.template) {
case 'team-invitation':
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
return `A user has invited you to join a team workspace in Hoppscotch`;
case 'user-invitation':
return 'Sign in to Hoppscotch';

View File

@@ -27,6 +27,12 @@
color: #3869D4;
}
a.nohighlight {
color: inherit !important;
text-decoration: none !important;
cursor: default !important;
}
a img {
border: none;
}
@@ -458,7 +464,7 @@
<td class="content-cell">
<div class="f-fallback">
<h1>Hi there,</h1>
<p>{{invitee}} with {{invite_team_name}} has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
<p><a class="nohighlight" name="invitee" href="#">{{invitee}}</a> with <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a> has invited you to use Hoppscotch to collaborate with them. Click the button below to set up your account and get started:</p>
<!-- Action -->
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
@@ -484,7 +490,7 @@
Welcome aboard, <br />
Your friends at Hoppscotch
</p>
<p><strong>P.S.</strong> If you don't associate with {{invitee}} or {{invite_team_name}}, just ignore this email.</p>
<p><strong>P.S.</strong> If you don't associate with <a class="nohighlight" name="invitee" href="#">{{invitee}}</a> or <a class="nohighlight" name="invite_team_name" href="#">{{invite_team_name}}</a>, just ignore this email.</p>
<!-- Sub copy -->
<table class="body-sub">
<tr>

View File

@@ -14,7 +14,7 @@
-->
<style type="text/css" rel="stylesheet" media="all">
/* Base ------------------------------ */
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
body {
width: 100% !important;
@@ -22,19 +22,25 @@
margin: 0;
-webkit-text-size-adjust: none;
}
a {
color: #3869D4;
}
a.nohighlight {
color: inherit !important;
text-decoration: none !important;
cursor: default !important;
}
a img {
border: none;
}
td {
word-break: break-word;
}
.preheader {
display: none !important;
visibility: hidden;
@@ -47,13 +53,13 @@
overflow: hidden;
}
/* Type ------------------------------ */
body,
td,
th {
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
}
h1 {
margin-top: 0;
color: #333333;
@@ -61,7 +67,7 @@
font-weight: bold;
text-align: left;
}
h2 {
margin-top: 0;
color: #333333;
@@ -69,7 +75,7 @@
font-weight: bold;
text-align: left;
}
h3 {
margin-top: 0;
color: #333333;
@@ -77,12 +83,12 @@
font-weight: bold;
text-align: left;
}
td,
th {
font-size: 16px;
}
p,
ul,
ol,
@@ -91,25 +97,25 @@
font-size: 16px;
line-height: 1.625;
}
p.sub {
font-size: 13px;
}
/* Utilities ------------------------------ */
.align-right {
text-align: right;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
/* Buttons ------------------------------ */
.button {
background-color: #3869D4;
border-top: 10px solid #3869D4;
@@ -124,7 +130,7 @@
-webkit-text-size-adjust: none;
box-sizing: border-box;
}
.button--green {
background-color: #22BC66;
border-top: 10px solid #22BC66;
@@ -132,7 +138,7 @@
border-bottom: 10px solid #22BC66;
border-left: 18px solid #22BC66;
}
.button--red {
background-color: #FF6136;
border-top: 10px solid #FF6136;
@@ -140,7 +146,7 @@
border-bottom: 10px solid #FF6136;
border-left: 18px solid #FF6136;
}
@media only screen and (max-width: 500px) {
.button {
width: 100% !important;
@@ -148,21 +154,21 @@
}
}
/* Attribute list ------------------------------ */
.attributes {
margin: 0 0 21px;
}
.attributes_content {
background-color: #F4F4F7;
padding: 16px;
}
.attributes_item {
padding: 0;
}
/* Related Items ------------------------------ */
.related {
width: 100%;
margin: 0;
@@ -171,31 +177,31 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.related_item {
padding: 10px 0;
color: #CBCCCF;
font-size: 15px;
line-height: 18px;
}
.related_item-title {
display: block;
margin: .5em 0 0;
}
.related_item-thumb {
display: block;
padding-bottom: 10px;
}
.related_heading {
border-top: 1px solid #CBCCCF;
text-align: center;
padding: 25px 0 10px;
}
/* Discount Code ------------------------------ */
.discount {
width: 100%;
margin: 0;
@@ -206,33 +212,33 @@
background-color: #F4F4F7;
border: 2px dashed #CBCCCF;
}
.discount_heading {
text-align: center;
}
.discount_body {
text-align: center;
font-size: 15px;
}
/* Social Icons ------------------------------ */
.social {
width: auto;
}
.social td {
padding: 0;
width: auto;
}
.social_icon {
height: 20px;
margin: 0 8px 10px 8px;
padding: 0;
}
/* Data table ------------------------------ */
.purchase {
width: 100%;
margin: 0;
@@ -241,7 +247,7 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_content {
width: 100%;
margin: 0;
@@ -250,50 +256,50 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.purchase_item {
padding: 10px 0;
color: #51545E;
font-size: 15px;
line-height: 18px;
}
.purchase_heading {
padding-bottom: 8px;
border-bottom: 1px solid #EAEAEC;
}
.purchase_heading p {
margin: 0;
color: #85878E;
font-size: 12px;
}
.purchase_footer {
padding-top: 15px;
border-top: 1px solid #EAEAEC;
}
.purchase_total {
margin: 0;
text-align: right;
font-weight: bold;
color: #333333;
}
.purchase_total--label {
padding: 0 15px 0 0;
}
body {
background-color: #F2F4F6;
color: #51545E;
}
p {
color: #51545E;
}
.email-wrapper {
width: 100%;
margin: 0;
@@ -303,7 +309,7 @@
-premailer-cellspacing: 0;
background-color: #F2F4F6;
}
.email-content {
width: 100%;
margin: 0;
@@ -313,16 +319,16 @@
-premailer-cellspacing: 0;
}
/* Masthead ----------------------- */
.email-masthead {
padding: 25px 0;
text-align: center;
}
.email-masthead_logo {
width: 94px;
}
.email-masthead_name {
font-size: 16px;
font-weight: bold;
@@ -331,7 +337,7 @@
text-shadow: 0 1px 0 white;
}
/* Body ------------------------------ */
.email-body {
width: 100%;
margin: 0;
@@ -340,7 +346,7 @@
-premailer-cellpadding: 0;
-premailer-cellspacing: 0;
}
.email-body_inner {
width: 570px;
margin: 0 auto;
@@ -350,7 +356,7 @@
-premailer-cellspacing: 0;
background-color: #FFFFFF;
}
.email-footer {
width: 570px;
margin: 0 auto;
@@ -360,11 +366,11 @@
-premailer-cellspacing: 0;
text-align: center;
}
.email-footer p {
color: #A8AAAF;
}
.body-action {
width: 100%;
margin: 30px auto;
@@ -374,25 +380,25 @@
-premailer-cellspacing: 0;
text-align: center;
}
.body-sub {
margin-top: 25px;
padding-top: 25px;
border-top: 1px solid #EAEAEC;
}
.content-cell {
padding: 45px;
}
/*Media Queries ------------------------------ */
@media only screen and (max-width: 600px) {
.email-body_inner,
.email-footer {
width: 100% !important;
}
}
@media (prefers-color-scheme: dark) {
body,
.email-body,

View File

@@ -17,7 +17,8 @@ async function bootstrap() {
console.log(`Port: ${configService.get('PORT')}`);
checkEnvironmentAuthProvider(
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
configService.get('INFRA.VITE_ALLOWED_AUTH_PROVIDERS') ??
configService.get('VITE_ALLOWED_AUTH_PROVIDERS'),
);
app.use(

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { PosthogService } from './posthog.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [PosthogService],
})
export class PosthogModule {}

View File

@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common';
import { PostHog } from 'posthog-node';
import { Cron, CronExpression, SchedulerRegistry } from '@nestjs/schedule';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from 'src/prisma/prisma.service';
import { CronJob } from 'cron';
import { POSTHOG_CLIENT_NOT_INITIALIZED } from 'src/errors';
import { throwErr } from 'src/utils';
@Injectable()
export class PosthogService {
private postHogClient: PostHog;
private POSTHOG_API_KEY = 'phc_9CipPajQC22mSkk2wxe2TXsUA0Ysyupe8dt5KQQELqx';
constructor(
private readonly configService: ConfigService,
private readonly prismaService: PrismaService,
private schedulerRegistry: SchedulerRegistry,
) {}
async onModuleInit() {
if (this.configService.get('INFRA.ALLOW_ANALYTICS_COLLECTION') === 'true') {
console.log('Initializing PostHog');
this.postHogClient = new PostHog(this.POSTHOG_API_KEY, {
host: 'https://eu.posthog.com',
});
// Schedule the cron job only if analytics collection is allowed
this.scheduleCronJob();
}
}
private scheduleCronJob() {
const job = new CronJob(CronExpression.EVERY_WEEK, async () => {
await this.capture();
});
this.schedulerRegistry.addCronJob('captureAnalytics', job);
job.start();
}
async capture() {
if (!this.postHogClient) {
throwErr(POSTHOG_CLIENT_NOT_INITIALIZED);
}
this.postHogClient.capture({
distinctId: this.configService.get('INFRA.ANALYTICS_USER_ID'),
event: 'sh_instance',
properties: {
type: 'COMMUNITY',
total_user_count: await this.prismaService.user.count(),
total_workspace_count: await this.prismaService.team.count(),
version: this.configService.get('npm_package_version'),
},
});
console.log('Sent event to PostHog');
}
}

View File

@@ -0,0 +1,14 @@
// Type of data returned from the query to obtain all search results
export type SearchQueryReturnType = {
id: string;
title: string;
type: 'collection' | 'request';
method?: string;
};
// Type of data returned from the query to obtain all parents
export type ParentTreeQueryReturnType = {
id: string;
parentID: string;
title: string;
};

View File

@@ -0,0 +1,38 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { TeamCollectionService } from './team-collection.service';
import * as E from 'fp-ts/Either';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorator';
import { TeamMemberRole } from '@prisma/client';
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
import { throwHTTPErr } from 'src/utils';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'team-collection', version: '1' })
export class TeamCollectionController {
constructor(private readonly teamCollectionService: TeamCollectionService) {}
@Get('search/:teamID')
@RequiresTeamRole(
TeamMemberRole.VIEWER,
TeamMemberRole.EDITOR,
TeamMemberRole.OWNER,
)
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
async searchByTitle(
@Query('searchQuery') searchQuery: string,
@Param('teamID') teamID: string,
@Query('take') take: string,
@Query('skip') skip: string,
) {
const res = await this.teamCollectionService.searchByTitle(
searchQuery,
teamID,
parseInt(take),
parseInt(skip),
);
if (E.isLeft(res)) throwHTTPErr(res.left);
return res.right;
}
}

View File

@@ -6,6 +6,7 @@ import { GqlCollectionTeamMemberGuard } from './guards/gql-collection-team-membe
import { TeamModule } from '../team/team.module';
import { UserModule } from '../user/user.module';
import { PubSubModule } from '../pubsub/pubsub.module';
import { TeamCollectionController } from './team-collection.controller';
@Module({
imports: [PrismaModule, TeamModule, UserModule, PubSubModule],
@@ -15,5 +16,6 @@ import { PubSubModule } from '../pubsub/pubsub.module';
GqlCollectionTeamMemberGuard,
],
exports: [TeamCollectionService, GqlCollectionTeamMemberGuard],
controllers: [TeamCollectionController],
})
export class TeamCollectionModule {}

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { HttpStatus, Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { TeamCollection } from './team-collection.model';
import {
@@ -14,14 +14,21 @@ import {
TEAM_COL_SAME_NEXT_COLL,
TEAM_COL_REORDERING_FAILED,
TEAM_COLL_DATA_INVALID,
TEAM_REQ_SEARCH_FAILED,
TEAM_COL_SEARCH_FAILED,
TEAM_REQ_PARENT_TREE_GEN_FAILED,
TEAM_COLL_PARENT_TREE_GEN_FAILED,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils';
import { escapeSqlLikeString, isValidLength } from 'src/utils';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
import { CollectionFolder } from 'src/types/CollectionFolder';
import { stringToJson } from 'src/utils';
import { CollectionSearchNode } from 'src/types/CollectionSearchNode';
import { ParentTreeQueryReturnType, SearchQueryReturnType } from './helper';
import { RESTError } from 'src/types/RESTError';
@Injectable()
export class TeamCollectionService {
@@ -1056,4 +1063,285 @@ export class TeamCollectionService {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
/**
* Search for TeamCollections and TeamRequests by title
*
* @param searchQuery The search query
* @param teamID The Team ID
* @param take Number of items we want returned
* @param skip Number of items we want to skip
* @returns An Either of the search results
*/
async searchByTitle(
searchQuery: string,
teamID: string,
take = 10,
skip = 0,
) {
// Fetch all collections and requests that match the search query
const searchResults: SearchQueryReturnType[] = [];
const matchedCollections = await this.searchCollections(
searchQuery,
teamID,
take,
skip,
);
if (E.isLeft(matchedCollections))
return E.left(<RESTError>{
message: matchedCollections.left,
statusCode: HttpStatus.NOT_FOUND,
});
searchResults.push(...matchedCollections.right);
const matchedRequests = await this.searchRequests(
searchQuery,
teamID,
take,
skip,
);
if (E.isLeft(matchedRequests))
return E.left(<RESTError>{
message: matchedRequests.left,
statusCode: HttpStatus.NOT_FOUND,
});
searchResults.push(...matchedRequests.right);
// Generate the parent tree for searchResults
const searchResultsWithTree: CollectionSearchNode[] = [];
for (let i = 0; i < searchResults.length; i++) {
const fetchedParentTree = await this.fetchParentTree(searchResults[i]);
if (E.isLeft(fetchedParentTree))
return E.left(<RESTError>{
message: fetchedParentTree.left,
statusCode: HttpStatus.NOT_FOUND,
});
searchResultsWithTree.push({
type: searchResults[i].type,
title: searchResults[i].title,
method: searchResults[i].method,
id: searchResults[i].id,
path: !fetchedParentTree
? []
: (fetchedParentTree.right as CollectionSearchNode[]),
});
}
return E.right({ data: searchResultsWithTree });
}
/**
* Search for TeamCollections by title
*
* @param searchQuery The search query
* @param teamID The Team ID
* @param take Number of items we want returned
* @param skip Number of items we want to skip
* @returns An Either of the search results
*/
private async searchCollections(
searchQuery: string,
teamID: string,
take: number,
skip: number,
) {
const query = Prisma.sql`
SELECT
id,title,'collection' AS type
FROM
"TeamCollection"
WHERE
"TeamCollection"."teamID"=${teamID}
AND
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
ORDER BY
similarity(title, ${searchQuery})
LIMIT ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`;
try {
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
return E.right(res);
} catch (error) {
return E.left(TEAM_COL_SEARCH_FAILED);
}
}
/**
* Search for TeamRequests by title
*
* @param searchQuery The search query
* @param teamID The Team ID
* @param take Number of items we want returned
* @param skip Number of items we want to skip
* @returns An Either of the search results
*/
private async searchRequests(
searchQuery: string,
teamID: string,
take: number,
skip: number,
) {
const query = Prisma.sql`
SELECT
id,title,request->>'method' as method,'request' AS type
FROM
"TeamRequest"
WHERE
"TeamRequest"."teamID"=${teamID}
AND
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
ORDER BY
similarity(title, ${searchQuery})
LIMIT ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`;
try {
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
return E.right(res);
} catch (error) {
return E.left(TEAM_REQ_SEARCH_FAILED);
}
}
/**
* Generate the parent tree of a search result
*
* @param searchResult The search result for which we want to generate the parent tree
* @returns The parent tree of the search result
*/
private async fetchParentTree(searchResult: SearchQueryReturnType) {
return searchResult.type === 'collection'
? await this.fetchCollectionParentTree(searchResult.id)
: await this.fetchRequestParentTree(searchResult.id);
}
/**
* Generate the parent tree of a collection
*
* @param id The ID of the collection
* @returns The parent tree of the collection
*/
private async fetchCollectionParentTree(id: string) {
try {
const query = Prisma.sql`
WITH RECURSIVE collection_tree AS (
SELECT tc.id, tc."parentID", tc.title
FROM "TeamCollection" AS tc
JOIN "TeamCollection" AS tr ON tc.id = tr."parentID"
WHERE tr.id = ${id}
UNION ALL
SELECT parent.id, parent."parentID", parent.title
FROM "TeamCollection" AS parent
JOIN collection_tree AS ct ON parent.id = ct."parentID"
)
SELECT * FROM collection_tree;
`;
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
query,
);
const collectionParentTree = this.generateParentTree(res);
return E.right(collectionParentTree);
} catch (error) {
E.left(TEAM_COLL_PARENT_TREE_GEN_FAILED);
}
}
/**
* Generate the parent tree from the collections
*
* @param parentCollections The parent collections
* @returns The parent tree of the parent collections
*/
private generateParentTree(parentCollections: ParentTreeQueryReturnType[]) {
function findChildren(id: string): CollectionSearchNode[] {
const collection = parentCollections.filter((item) => item.id === id)[0];
if (collection.parentID == null) {
return <CollectionSearchNode[]>[
{
id: collection.id,
title: collection.title,
type: 'collection' as const,
path: [],
},
];
}
const res = <CollectionSearchNode[]>[
{
id: collection.id,
title: collection.title,
type: 'collection' as const,
path: findChildren(collection.parentID),
},
];
return res;
}
if (parentCollections.length > 0) {
if (parentCollections[0].parentID == null) {
return <CollectionSearchNode[]>[
{
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: [],
},
];
}
return <CollectionSearchNode[]>[
{
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: findChildren(parentCollections[0].parentID),
},
];
}
return <CollectionSearchNode[]>[];
}
/**
* Generate the parent tree of a request
*
* @param id The ID of the request
* @returns The parent tree of the request
*/
private async fetchRequestParentTree(id: string) {
try {
const query = Prisma.sql`
WITH RECURSIVE request_collection_tree AS (
SELECT tc.id, tc."parentID", tc.title
FROM "TeamCollection" AS tc
JOIN "TeamRequest" AS tr ON tc.id = tr."collectionID"
WHERE tr.id = ${id}
UNION ALL
SELECT parent.id, parent."parentID", parent.title
FROM "TeamCollection" AS parent
JOIN request_collection_tree AS ct ON parent.id = ct."parentID"
)
SELECT * FROM request_collection_tree;
`;
const res = await this.prisma.$queryRaw<ParentTreeQueryReturnType[]>(
query,
);
const requestParentTree = this.generateParentTree(res);
return E.right(requestParentTree);
} catch (error) {
return E.left(TEAM_REQ_PARENT_TREE_GEN_FAILED);
}
}
}

View File

@@ -0,0 +1,47 @@
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { TeamService } from '../../team/team.service';
import { TeamMemberRole } from '../../team/team.model';
import {
BUG_TEAM_NO_REQUIRE_TEAM_ROLE,
BUG_AUTH_NO_USER_CTX,
BUG_TEAM_NO_TEAM_ID,
TEAM_MEMBER_NOT_FOUND,
TEAM_NOT_REQUIRED_ROLE,
} from 'src/errors';
import { throwHTTPErr } from 'src/utils';
@Injectable()
export class RESTTeamMemberGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly teamService: TeamService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requireRoles = this.reflector.get<TeamMemberRole[]>(
'requiresTeamRole',
context.getHandler(),
);
if (!requireRoles)
throwHTTPErr({ message: BUG_TEAM_NO_REQUIRE_TEAM_ROLE, statusCode: 400 });
const request = context.switchToHttp().getRequest();
const { user } = request;
if (user == undefined)
throwHTTPErr({ message: BUG_AUTH_NO_USER_CTX, statusCode: 400 });
const teamID = request.params.teamID;
if (!teamID)
throwHTTPErr({ message: BUG_TEAM_NO_TEAM_ID, statusCode: 400 });
const teamMember = await this.teamService.getTeamMember(teamID, user.uid);
if (!teamMember)
throwHTTPErr({ message: TEAM_MEMBER_NOT_FOUND, statusCode: 404 });
if (requireRoles.includes(teamMember.role)) return true;
throwHTTPErr({ message: TEAM_NOT_REQUIRED_ROLE, statusCode: 403 });
}
}

View File

@@ -0,0 +1,17 @@
// Response type of results from the search query
export type CollectionSearchNode = {
/** Encodes the hierarchy of where the node is **/
path: CollectionSearchNode[];
} & (
| {
type: 'request';
title: string;
method: string;
id: string;
}
| {
type: 'collection';
title: string;
id: string;
}
);

View File

@@ -4,26 +4,23 @@ export enum InfraConfigEnum {
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',
GOOGLE_SCOPE = 'GOOGLE_SCOPE',
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
GITHUB_CALLBACK_URL = 'GITHUB_CALLBACK_URL',
GITHUB_SCOPE = 'GITHUB_SCOPE',
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
MICROSOFT_CALLBACK_URL = 'MICROSOFT_CALLBACK_URL',
MICROSOFT_SCOPE = 'MICROSOFT_SCOPE',
MICROSOFT_TENANT = 'MICROSOFT_TENANT',
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
}
export enum InfraConfigEnumForClient {
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GITHUB_CLIENT_ID = 'GITHUB_CLIENT_ID',
GITHUB_CLIENT_SECRET = 'GITHUB_CLIENT_SECRET',
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
ALLOW_ANALYTICS_COLLECTION = 'ALLOW_ANALYTICS_COLLECTION',
ANALYTICS_USER_ID = 'ANALYTICS_USER_ID',
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
}

View File

@@ -1,10 +1,10 @@
import { HttpStatus } from '@nestjs/common';
/**
** Custom interface to handle errors specific to Auth module
** Custom interface to handle errors for REST modules such as Auth, Admin modules
** Since its REST we need to return the HTTP status code along with the error message
*/
export type AuthError = {
export type RESTError = {
message: string;
statusCode: HttpStatus;
};

View File

@@ -17,3 +17,21 @@ export class PaginationArgs {
})
take: number;
}
@ArgsType()
@InputType()
export class OffsetPaginationArgs {
@Field({
nullable: true,
defaultValue: 0,
description: 'Number of items to skip',
})
skip: number;
@Field({
nullable: true,
defaultValue: 10,
description: 'Number of items to fetch',
})
take: number;
}

View File

@@ -56,3 +56,22 @@ export enum SessionType {
registerEnumType(SessionType, {
name: 'SessionType',
});
@ObjectType()
export class UserDeletionResult {
@Field(() => ID, {
description: 'UID of the user',
})
userUID: string;
@Field(() => Boolean, {
description: 'Flag to determine if user deletion was successful or not',
})
isDeleted: Boolean;
@Field({
nullable: true,
description: 'Error message if user deletion was not successful',
})
errorMessage: String;
}

View File

@@ -1,4 +1,4 @@
import { JSON_INVALID, USER_NOT_FOUND } from 'src/errors';
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser';
@@ -176,6 +176,26 @@ describe('UserService', () => {
});
});
describe('findUsersByIds', () => {
test('should successfully return users given valid user UIDs', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce(users);
const result = await userService.findUsersByIds([
'123344',
'5555',
'6666',
]);
expect(result).toEqual(users);
});
test('should return empty array of users given a invalid user UIDs', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce([]);
const result = await userService.findUsersByIds(['sdcvbdbr']);
expect(result).toEqual([]);
});
});
describe('createUserViaMagicLink', () => {
test('should successfully create user and account for magic-link given valid inputs', async () => {
mockPrisma.user.create.mockResolvedValueOnce(user);
@@ -414,6 +434,54 @@ describe('UserService', () => {
});
});
describe('updateUserDisplayName', () => {
test('should resolve right and update user display name', async () => {
const newDisplayName = 'New Name';
mockPrisma.user.update.mockResolvedValueOnce({
...user,
displayName: newDisplayName,
});
const result = await userService.updateUserDisplayName(
user.uid,
newDisplayName,
);
expect(result).toEqualRight({
...user,
displayName: newDisplayName,
currentGQLSession: JSON.stringify(user.currentGQLSession),
currentRESTSession: JSON.stringify(user.currentRESTSession),
});
});
test('should resolve right and publish user updated subscription', async () => {
const newDisplayName = 'New Name';
mockPrisma.user.update.mockResolvedValueOnce({
...user,
displayName: newDisplayName,
});
await userService.updateUserDisplayName(user.uid, user.displayName);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`user/${user.uid}/updated`,
{
...user,
displayName: newDisplayName,
currentGQLSession: JSON.stringify(user.currentGQLSession),
currentRESTSession: JSON.stringify(user.currentRESTSession),
},
);
});
test('should resolve left and error when invalid user uid is passed', async () => {
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
const result = await userService.updateUserDisplayName(
'invalidUserUid',
user.displayName,
);
expect(result).toEqualLeft(USER_NOT_FOUND);
});
});
describe('fetchAllUsers', () => {
test('should resolve right and return 20 users when cursor is null', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce(users);
@@ -435,6 +503,36 @@ describe('UserService', () => {
});
});
describe('fetchAllUsersV2', () => {
test('should resolve right and return first 20 users when searchString is null', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce(users);
const result = await userService.fetchAllUsersV2(null, {
take: 20,
skip: 0,
});
expect(result).toEqual(users);
});
test('should resolve right and return next 20 users when searchString is provided', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce(users);
const result = await userService.fetchAllUsersV2('.com', {
take: 20,
skip: 0,
});
expect(result).toEqual(users);
});
test('should resolve left and return an empty array when users not found', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce([]);
const result = await userService.fetchAllUsersV2('Unknown entry', {
take: 20,
skip: 0,
});
expect(result).toEqual([]);
});
});
describe('fetchAdminUsers', () => {
test('should return a list of admin users', async () => {
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
@@ -556,4 +654,17 @@ describe('UserService', () => {
expect(result).toEqual(10);
});
});
describe('removeUsersAsAdmin', () => {
test('should resolve right and return true for valid user UIDs', async () => {
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 1 });
const result = await userService.removeUsersAsAdmin(['123344']);
expect(result).toEqualRight(true);
});
test('should resolve right and return false for invalid user UIDs', async () => {
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 0 });
const result = await userService.removeUsersAsAdmin(['123344']);
expect(result).toEqualLeft(USERS_NOT_FOUND);
});
});
});

View File

@@ -8,13 +8,14 @@ import * as T from 'fp-ts/Task';
import * as A from 'fp-ts/Array';
import { pipe, constVoid } from 'fp-ts/function';
import { AuthUser } from 'src/types/AuthUser';
import { USER_NOT_FOUND } from 'src/errors';
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
import { SessionType, User } from './user.model';
import { USER_UPDATE_FAILED } from 'src/errors';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
import { UserDataHandler } from './user.data.handler';
import { User as DbUser } from '@prisma/client';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
@Injectable()
export class UserService {
@@ -88,6 +89,20 @@ export class UserService {
}
}
/**
* Find users with given IDs
* @param userUIDs User IDs
* @returns Array of found Users
*/
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
const users = await this.prisma.user.findMany({
where: {
uid: { in: userUIDs },
},
});
return users;
}
/**
* Update User with new generated hashed refresh token
*
@@ -269,6 +284,30 @@ export class UserService {
}
}
/**
* Update a user's data
* @param userUID User UID
* @param displayName User's displayName
* @returns a Either of User or error
*/
async updateUserDisplayName(userUID: string, displayName: string) {
try {
const dbUpdatedUser = await this.prisma.user.update({
where: { uid: userUID },
data: { displayName },
});
const updatedUser = this.convertDbUserToUser(dbUpdatedUser);
// Publish subscription for user updates
await this.pubsub.publish(`user/${updatedUser.uid}/updated`, updatedUser);
return E.right(updatedUser);
} catch (error) {
return E.left(USER_NOT_FOUND);
}
}
/**
* Validate and parse currentRESTSession and currentGQLSession
* @param sessionData string of the session
@@ -286,6 +325,7 @@ export class UserService {
* @param cursorID string of userUID or null
* @param take number of users to query
* @returns an array of `User` object
* @deprecated use fetchAllUsersV2 instead
*/
async fetchAllUsers(cursorID: string, take: number) {
const fetchedUsers = await this.prisma.user.findMany({
@@ -296,6 +336,43 @@ export class UserService {
return fetchedUsers;
}
/**
* Fetch all the users in the `User` table based on cursor
* @param searchString search on user's displayName or email
* @param paginationOption pagination options
* @returns an array of `User` object
*/
async fetchAllUsersV2(
searchString: string,
paginationOption: OffsetPaginationArgs,
) {
const fetchedUsers = await this.prisma.user.findMany({
skip: paginationOption.skip,
take: paginationOption.take,
where: searchString
? {
OR: [
{
displayName: {
contains: searchString,
mode: 'insensitive',
},
},
{
email: {
contains: searchString,
mode: 'insensitive',
},
},
],
}
: undefined,
orderBy: [{ isAdmin: 'desc' }, { displayName: 'asc' }],
});
return fetchedUsers;
}
/**
* Fetch the number of users in db
* @returns a count (Int) of user records in DB
@@ -326,6 +403,23 @@ export class UserService {
}
}
/**
* Change users to admins by toggling isAdmin param to true
* @param userUID user UIDs
* @returns a Either of true or error
*/
async makeAdmins(userUIDs: string[]) {
try {
await this.prisma.user.updateMany({
where: { uid: { in: userUIDs } },
data: { isAdmin: true },
});
return E.right(true);
} catch (error) {
return E.left(USER_UPDATE_FAILED);
}
}
/**
* Fetch all the admin users
* @returns an array of admin users
@@ -444,4 +538,22 @@ export class UserService {
return E.left(USER_NOT_FOUND);
}
}
/**
* Change users from an admin by toggling isAdmin param to false
* @param userUIDs user UIDs
* @returns a Either of true or error
*/
async removeUsersAsAdmin(userUIDs: string[]) {
const data = await this.prisma.user.updateMany({
where: { uid: { in: userUIDs } },
data: { isAdmin: false },
});
if (data.count === 0) {
return E.left(USERS_NOT_FOUND);
}
return E.right(true);
}
}

View File

@@ -1,4 +1,4 @@
import { ExecutionContext } from '@nestjs/common';
import { ExecutionContext, HttpException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
import { pipe } from 'fp-ts/lib/function';
@@ -16,6 +16,7 @@ import {
JSON_INVALID,
} from './errors';
import { AuthProvider } from './auth/helper';
import { RESTError } from './types/RESTError';
/**
* A workaround to throw an exception in an expression.
@@ -27,6 +28,15 @@ export function throwErr(errMessage: string): never {
throw new Error(errMessage);
}
/**
* This function allows throw to be used as an expression
* @param errMessage Message present in the error message
*/
export function throwHTTPErr(errorData: RESTError): never {
const { message, statusCode } = errorData;
throw new HttpException(message, statusCode);
}
/**
* Prints the given value to log and returns the same value.
* Used for debugging functional pipelines.
@@ -131,6 +141,28 @@ export const validateEmail = (email: string) => {
).test(email);
};
// Regular expressions for supported address object formats by nodemailer
// check out for more info https://nodemailer.com/message/addresses
const emailRegex1 = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const emailRegex2 =
/^[\w\s]* <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/;
const emailRegex3 =
/^"[\w\s]+" <([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>$/;
/**
* Checks to see if the SMTP email is valid or not
* @param email
* @returns A Boolean depending on the format of the email
*/
export const validateSMTPEmail = (email: string) => {
// Check if the input matches any of the formats
return (
emailRegex1.test(email) ||
emailRegex2.test(email) ||
emailRegex3.test(email)
);
};
/**
* Checks to see if the URL is valid or not
* @param url The URL to validate
@@ -151,6 +183,16 @@ export const validateSMTPUrl = (url: string) => {
return false;
};
/**
* Checks to see if the URL is valid or not
* @param url The URL to validate
* @returns boolean
*/
export const validateUrl = (url: string) => {
const urlRegex = /^(http|https):\/\/[^ "]+$/;
return urlRegex.test(url);
};
/**
* String to JSON parser
* @param {str} str The string to parse
@@ -208,3 +250,39 @@ export function checkEnvironmentAuthProvider(
}
}
}
/**
* Adds escape backslashes to the input so that it can be used inside
* SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
* function.
*
* Eg. "100%" -> "100\\%"
*
* Source: https://stackoverflow.com/a/32648526
*/
export function escapeSqlLikeString(str: string) {
if (typeof str != 'string') return str;
return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
switch (char) {
case '\0':
return '\\0';
case '\x08':
return '\\b';
case '\x09':
return '\\t';
case '\x1a':
return '\\z';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '"':
case "'":
case '\\':
case '%':
return '\\' + char; // prepends a backslash to backslash, percent,
// and double/single quotes
}
});
}

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env node
// * The entry point of the CLI
require("../dist").cli(process.argv);

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env node
// * The entry point of the CLI
import { cli } from "../dist/index.js";
cli(process.argv);

View File

@@ -1,11 +1,12 @@
{
"name": "@hoppscotch/cli",
"version": "0.5.0",
"version": "0.7.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"type": "module",
"main": "dist/index.js",
"bin": {
"hopp": "bin/hopp"
"hopp": "bin/hopp.js"
},
"publishConfig": {
"access": "public"
@@ -39,27 +40,29 @@
},
"license": "MIT",
"private": false,
"dependencies": {
"axios": "1.6.7",
"chalk": "5.3.0",
"commander": "11.1.0",
"lodash-es": "4.17.21",
"qs": "6.11.2",
"verzod": "0.2.2",
"zod": "3.22.4"
},
"devDependencies": {
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.1.1",
"@swc/core": "^1.3.92",
"@types/jest": "^29.5.5",
"@types/lodash": "^4.14.199",
"@types/qs": "^6.9.8",
"axios": "^0.21.4",
"chalk": "^4.1.2",
"commander": "^11.0.0",
"esm": "^3.2.25",
"fp-ts": "^2.16.1",
"io-ts": "^2.2.20",
"jest": "^29.7.0",
"lodash": "^4.17.21",
"prettier": "^3.0.3",
"qs": "^6.11.2",
"ts-jest": "^29.1.1",
"tsup": "^7.2.0",
"typescript": "^5.2.2",
"zod": "^3.22.4"
"@relmify/jest-fp-ts": "2.1.1",
"@swc/core": "1.4.2",
"@types/jest": "29.5.12",
"@types/lodash-es": "4.17.12",
"@types/qs": "6.9.12",
"fp-ts": "2.16.2",
"jest": "29.7.0",
"prettier": "3.2.5",
"qs": "6.11.2",
"ts-jest": "29.1.2",
"tsup": "8.0.2",
"typescript": "5.3.3"
}
}
}

View File

@@ -3,129 +3,247 @@ import { ExecException } from "child_process";
import { HoppErrorCode } from "../../types/errors";
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
describe("Test 'hopp test <file>' command:", () => {
test("No collection file path provided.", async () => {
const args = "test";
const { stderr } = await runCLI(args);
describe("Test `hopp test <file>` command:", () => {
describe("Argument parsing", () => {
test("Errors with the code `INVALID_ARGUMENT` for not supplying enough arguments", async () => {
const args = "test";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Collection file not found.", async () => {
const args = "test notfound.json";
const { stderr } = await runCLI(args);
test("Errors with the code `INVALID_ARGUMENT` for an invalid command", async () => {
const args = "invalid-arg";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
})
test("Collection file is invalid JSON.", async () => {
const args = `test ${getTestJsonFilePath(
"malformed-collection.json"
)}`;
const { stderr } = await runCLI(args);
describe("Supplied collection export file validations", () => {
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
const args = "test notfound.json";
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Malformed collection file.", async () => {
const args = `test ${getTestJsonFilePath(
"malformed-collection2.json"
)}`;
const { stderr } = await runCLI(args);
test("Errors with the code UNKNOWN_ERROR if the supplied collection export file content isn't valid JSON", async () => {
const args = `test ${getTestJsonFilePath("malformed-coll.json", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Invalid arguement.", async () => {
const args = "invalid-arg";
const { stderr } = await runCLI(args);
test("Errors with the code `MALFORMED_COLLECTION` if the supplied collection export file content is malformed", async () => {
const args = `test ${getTestJsonFilePath("malformed-coll-2.json", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
});
test("Collection file not JSON type.", async () => {
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
const { stderr } = await runCLI(args);
test("Errors with the code `INVALID_FILE_TYPE` if the supplied collection export file doesn't end with the `.json` extension", async () => {
const args = `test ${getTestJsonFilePath("notjson-coll.txt", "collection")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Some errors occured (exit code 1).", async () => {
const args = `test ${getTestJsonFilePath("fails.json")}`;
const { error } = await runCLI(args);
test("Fails if the collection file includes scripts with incorrect API usage and failed assertions", async () => {
const args = `test ${getTestJsonFilePath("fails-coll.json", "collection")}`;
const { error } = await runCLI(args);
expect(error).not.toBeNull();
expect(error).toMatchObject(<ExecException>{
code: 1,
expect(error).not.toBeNull();
expect(error).toMatchObject(<ExecException>{
code: 1,
});
});
});
test("No errors occured (exit code 0).", async () => {
const args = `test ${getTestJsonFilePath("passes.json")}`;
test("Successfully processes a supplied collection export file of the expected format", async () => {
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Supports inheriting headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
test("Successfully inherits headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath(
"collection-level-headers-auth-coll.json", "collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
})
});
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
const args = `test ${getTestJsonFilePath(
"pre-req-script-env-var-persistence-coll.json", "collection"
)}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("Test 'hopp test <file> --env <file>' command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
"passes.json"
)}`;
describe("Test `hopp test <file> --env <file>` command:", () => {
describe("Supplied environment export file validations", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
test("No env file path provided.", async () => {
const args = `${VALID_TEST_ARGS} --env`;
const { stderr } = await runCLI(args);
test("Errors with the code `INVALID_ARGUMENT` if no file is supplied", async () => {
const args = `${VALID_TEST_ARGS} --env`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
"notjson-coll.txt", "collection"
)}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Errors with the code `FILE_NOT_FOUND` if the supplied environment export file doesn't exist", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_ENV_FILE");
});
test("Errors with the code `BULK_ENV_FILE` on supplying an environment export file based on the bulk environment export format", async () => {
const ENV_PATH = getTestJsonFilePath("bulk-envs.json", "environment");
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("BULK_ENV_FILE");
});
});
test("ENV file not JSON type.", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("ENV file not found.", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("No errors occured (exit code 0).", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
test("Successfully resolves values from the supplied environment export file", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Successfully resolves environment variables referenced in the request body", async () => {
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Works with shorth `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
describe("Secret environment variables", () => {
jest.setTimeout(10000);
// Reads secret environment values from system environment
test("Successfully picks the values for secret environment variables from `process.env` and persists the variables set from the pre-request script", async () => {
const env = {
...process.env,
secretBearerToken: "test-token",
secretBasicAuthUsername: "test-user",
secretBasicAuthPassword: "test-pass",
secretQueryParamValue: "secret-query-param-value",
secretBodyValue: "secret-body-value",
secretHeaderValue: "secret-header-value",
};
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args, { env });
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Prefers values specified in the environment export file over values set in the system environment
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
// Values set from the scripting context takes the highest precedence
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-coll.json", "collection"
);
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
expect(stdout).toContain(
"https://httpbin.org/basic-auth/*********/*********"
);
expect(error).toBeNull();
});
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-coll.json", "collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json", "environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
"passes.json"
)}`;
describe("Test `hopp test <file> --delay <delay_in_ms>` command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
test("No value passed to delay flag.", async () => {
test("Errors with the code `INVALID_ARGUMENT` on not supplying a delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay`;
const { stderr } = await runCLI(args);
@@ -133,7 +251,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Invalid value passed to delay flag.", async () => {
test("Errors with the code `INVALID_ARGUMENT` on supplying an invalid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
const { stderr } = await runCLI(args);
@@ -141,10 +259,17 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Valid value passed to delay flag.", async () => {
test("Successfully performs delayed request execution for a valid delay value", async () => {
const args = `${VALID_TEST_ARGS} --delay 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
test("Works with the short `-d` flag", async () => {
const args = `${VALID_TEST_ARGS} -d 1`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});

View File

@@ -17,7 +17,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestD",
"params": [],
@@ -40,7 +40,8 @@
"body": {
"contentType": null,
"body": null
}
},
"requestVariables": []
}
],
"auth": {
@@ -52,7 +53,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestC",
"params": [],
@@ -67,7 +68,8 @@
"body": {
"contentType": null,
"body": null
}
},
"requestVariables": []
}
],
"auth": {
@@ -88,7 +90,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
@@ -104,6 +106,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -116,7 +119,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
@@ -132,6 +135,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -158,7 +162,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
@@ -174,6 +178,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -186,7 +191,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
@@ -202,6 +207,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -218,4 +224,4 @@
"token": "BearerToken"
}
}
]
]

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "<<URL>>",
"name": "test1",
"params": [],
@@ -16,7 +16,8 @@
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -5,7 +5,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "",
"params": [],
@@ -13,20 +13,18 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
},
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -34,17 +32,15 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -2,9 +2,9 @@
{
"v": 1,
"folders": [],
"requests":
"requests":
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail",
"params": [],
@@ -12,20 +12,18 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": [],
},
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -33,17 +31,15 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -2,9 +2,9 @@
{
"v": 1,
"folders": [],
"requests":
"requests":
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail",
"params": [],
@@ -22,7 +22,8 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -5,7 +5,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "",
"params": [],
@@ -13,20 +13,18 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
},
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -34,17 +32,15 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -0,0 +1,22 @@
{
"v": 2,
"name": "pre-req-script-env-var-persistence-coll",
"folders": [],
"requests": [
{
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "sample-req",
"method": "GET",
"params": [],
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
"requestVariables": []
}
],
"auth": { "authType": "inherit", "authActive": true },
"headers": []
}

View File

@@ -0,0 +1,31 @@
{
"v": 2,
"name": "Test environment variables in request body",
"folders": [],
"requests": [
{
"v": "3",
"name": "test-request",
"endpoint": "https://echo.hoppscotch.io",
"method": "POST",
"headers": [],
"params": [],
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"contentType": "application/json",
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
},
"preRequestScript": "",
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
"requestVariables": []
}
],
"auth": {
"authType": "none",
"authActive": true
},
"headers": []
}

View File

@@ -0,0 +1,113 @@
{
"v": 2,
"name": "secret-envs-coll",
"folders": [],
"requests": [
{
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-headers",
"method": "GET",
"params": [],
"headers": [
{
"key": "Secret-Header-Key",
"value": "<<secretHeaderValue>>",
"active": true
}
],
"requestVariables": [],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
"contentType": "application/json"
},
"name": "test-secret-body",
"method": "POST",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-query-params",
"method": "GET",
"params": [
{
"key": "secretQueryParamKey",
"value": "<<secretQueryParamValue>>",
"active": true
}
],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "3",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
"username": "<<secretBasicAuthUsername>>",
"authActive": true
},
"body": { "body": null, "contentType": null },
"name": "test-secret-basic-auth",
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": ""
},
{
"v": "3",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
"password": "testpassword",
"username": "testuser",
"authActive": true
},
"body": { "body": null, "contentType": null },
"name": "test-secret-bearer-auth",
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
},
{
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-fallback",
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>",
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
"preRequestScript": ""
}
],
"auth": { "authType": "inherit", "authActive": false },
"headers": []
}

View File

@@ -0,0 +1,149 @@
{
"v": 2,
"name": "secret-envs-setters-coll",
"folders": [],
"requests": [
{
"v": "3",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-headers",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [
{
"key": "Secret-Header-Key",
"value": "<<secretHeaderValue>>",
"active": true
}
],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "3",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-headers-overrides",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [
{
"key": "Secret-Header-Key",
"value": "<<secretHeaderValue>>",
"active": true
}
],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Value set at the pre-request script takes precedence\", () => {\n const secretHeaderValue = pw.env.getResolve(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value-overriden\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.getResolve(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value-overriden\")\n})",
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "3",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
"contentType": "application/json"
},
"name": "test-secret-body",
"method": "POST",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "3",
"auth": {
"authType": "none",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-query-params",
"method": "GET",
"params": [
{
"key": "secretQueryParamKey",
"value": "<<secretQueryParamValue>>",
"active": true
}
],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "3",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
"username": "<<secretBasicAuthUsername>>",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-basic-auth",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
},
{
"v": "3",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
"password": "testpassword",
"username": "testuser",
"authActive": true
},
"body": {
"body": null,
"contentType": null
},
"name": "test-secret-bearer-auth",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
"preRequestScript": "let secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n\nif (!secretBearerToken) {\n pw.env.set(\"secretBearerToken\", \"test-token\")\n secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n}\n\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
}
],
"auth": {
"authType": "inherit",
"authActive": false
},
"headers": []
}

View File

@@ -0,0 +1,31 @@
{
"v": 2,
"name": "secret-envs-persistence-scripting-req",
"folders": [],
"requests": [
{
"v": "3",
"endpoint": "https://httpbin.org/post",
"name": "req",
"params": [],
"headers": [
{
"active": true,
"key": "Custom-Header",
"value": "<<customHeaderValueFromSecretVar>>"
}
],
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "pw.env.set(\"preReqVarOne\", \"pre-req-value-one\")\n\npw.env.set(\"preReqVarTwo\", \"pre-req-value-two\")\n\npw.env.set(\"customHeaderValueFromSecretVar\", \"custom-header-secret-value\")\n\npw.env.set(\"customBodyValue\", \"custom-body-value\")",
"testScript": "pw.test(\"Secret environment value set from the pre-request script takes precedence\", () => {\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(\"pre-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the pre-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request headers that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.headers[\"Custom-Header\"]).toBe(\"custom-header-secret-value\")\n})\n\npw.test(\"Successfully resolves secret variable values referred in request body that are set in pre-request sccript\", () => {\n pw.expect(pw.response.body.json.key).toBe(\"custom-body-value\")\n})\n\npw.test(\"Secret environment variable set from the post-request script takes precedence\", () => {\n pw.env.set(\"postReqVarOne\", \"post-req-value-one\")\n pw.expect(pw.env.get(\"postReqVarOne\")).toBe(\"post-req-value-one\")\n})\n\npw.test(\"Successfully sets initial value for the secret variable from the post-request script\", () => {\n pw.env.set(\"postReqVarTwo\", \"post-req-value-two\")\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(\"post-req-value-two\")\n})\n\npw.test(\"Successfully removes environment variables via the pw.env.unset method\", () => {\n pw.env.unset(\"preReqVarOne\")\n pw.env.unset(\"postReqVarTwo\")\n\n pw.expect(pw.env.get(\"preReqVarOne\")).toBe(undefined)\n pw.expect(pw.env.get(\"postReqVarTwo\")).toBe(undefined)\n})",
"body": {
"contentType": "application/json",
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
},
"requestVariables": []
}
],
"auth": { "authType": "inherit", "authActive": false },
"headers": []
}

View File

@@ -0,0 +1,32 @@
[
{
"v": 0,
"name": "Env-I",
"variables": [
{
"key": "firstName",
"value": "John"
},
{
"key": "lastName",
"value": "Doe"
}
]
},
{
"v": 1,
"id": "2",
"name": "Env-II",
"variables": [
{
"key": "baseUrl",
"value": "https://echo.hoppscotch.io",
"secret": false
},
{
"key": "secretVar",
"secret": true
}
]
}
]

View File

@@ -0,0 +1,16 @@
{
"id": 123,
"v": "1",
"name": "secret-envs",
"values": [
{
"key": "secretVar",
"secret": true
},
{
"key": "regularVar",
"secret": false,
"value": "regular-variable"
}
]
}

View File

@@ -0,0 +1,38 @@
{
"v": 0,
"name": "Response body sample",
"variables": [
{
"key": "firstName",
"value": "John"
},
{
"key": "lastName",
"value": "Doe"
},
{
"key": "id",
"value": "7"
},
{
"key": "fullName",
"value": "<<firstName>> <<lastName>>"
},
{
"key": "recursiveVarX",
"value": "<<recursiveVarY>>"
},
{
"key": "recursiveVarY",
"value": "<<salutation>>"
},
{
"key": "salutation",
"value": "Hello"
},
{
"key": "greetText",
"value": "<<salutation>> <<fullName>>"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"v": 1,
"id": "2",
"name": "secret-envs-persistence-scripting-envs",
"variables": [
{
"key": "preReqVarOne",
"secret": true
},
{
"key": "preReqVarTwo",
"secret": true
},
{
"key": "postReqVarOne",
"secret": true
},
{
"key": "preReqVarTwo",
"secret": true
},
{
"key": "customHeaderValueFromSecretVar",
"secret": true
}
]
}

View File

@@ -0,0 +1,40 @@
{
"id": "2",
"v": 1,
"name": "secret-envs",
"variables": [
{
"key": "secretBearerToken",
"secret": true
},
{
"key": "secretBasicAuthUsername",
"secret": true
},
{
"key": "secretBasicAuthPassword",
"secret": true
},
{
"key": "secretQueryParamValue",
"secret": true
},
{
"key": "secretBodyValue",
"secret": true
},
{
"key": "secretHeaderValue",
"secret": true
},
{
"key": "nonExistentValueInSystemEnv",
"secret": true
},
{
"key": "baseURL",
"value": "https://httpbin.org",
"secret": false
}
]
}

View File

@@ -0,0 +1,46 @@
{
"v": 1,
"id": "2",
"name": "secret-values-envs",
"variables": [
{
"key": "secretBearerToken",
"value": "test-token",
"secret": true
},
{
"key": "secretBasicAuthUsername",
"value": "test-user",
"secret": true
},
{
"key": "secretBasicAuthPassword",
"value": "test-pass",
"secret": true
},
{
"key": "secretQueryParamValue",
"value": "secret-query-param-value",
"secret": true
},
{
"key": "secretBodyValue",
"value": "secret-body-value",
"secret": true
},
{
"key": "secretHeaderValue",
"value": "secret-header-value",
"secret": true
},
{
"key": "nonExistentValueInSystemEnv",
"secret": true
},
{
"key": "baseURL",
"value": "https://httpbin.org",
"secret": false
}
]
}

View File

@@ -3,13 +3,13 @@ import { resolve } from "path";
import { ExecResponse } from "./types";
export const runCLI = (args: string): Promise<ExecResponse> =>
export const runCLI = (args: string, options = {}): Promise<ExecResponse> =>
{
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
const command = `node ${CLI_PATH} ${args}`
return new Promise((resolve) =>
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
exec(command, options, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
);
}
@@ -22,12 +22,15 @@ export const trimAnsi = (target: string) => {
export const getErrorCode = (out: string) => {
const ansiTrimmedStr = trimAnsi(out);
return ansiTrimmedStr.split(" ")[0];
};
export const getTestJsonFilePath = (file: string) => {
const filePath = `${process.cwd()}/src/__tests__/samples/${file}`;
export const getTestJsonFilePath = (file: string, kind: "collection" | "environment") => {
const kindDir = {
collection: "collections",
environment: "environments",
}[kind];
const filePath = resolve(__dirname, `../../src/__tests__/samples/${kindDir}/${file}`);
return filePath;
};

View File

@@ -1,5 +1,5 @@
import chalk from "chalk";
import { program } from "commander";
import { Command } from "commander";
import * as E from "fp-ts/Either";
import { version } from "../package.json";
import { test } from "./commands/test";
@@ -20,6 +20,8 @@ const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
"https://docs.hoppscotch.io/documentation/clients/cli"
)}`;
const program = new Command()
program
.name("hopp")
.version(version, "-v, --ver", "see the current version of hopp-cli")

View File

@@ -21,6 +21,7 @@ export interface RequestStack {
*/
export interface RequestConfig extends AxiosRequestConfig {
supported: boolean;
displayUrl?: string
}
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
@@ -30,6 +31,7 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
* This contains path, params and environment variables all applied to it
*/
effectiveFinalURL: string;
effectiveFinalDisplayURL?: string;
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
effectiveFinalParams: { key: string; value: string; active: boolean }[];
effectiveFinalBody: FormData | string | null;

View File

@@ -1,34 +1,47 @@
import { Environment } from "@hoppscotch/data";
import { entityReference } from "verzod";
import { z } from "zod";
import { error } from "../../types/errors";
import {
HoppEnvs,
HoppEnvPair,
HoppEnvKeyPairObject,
HoppEnvExportObject,
HoppBulkEnvExportObject,
HoppEnvPair,
HoppEnvs,
} from "../../types/request";
import { readJsonFile } from "../../utils/mutators";
/**
* Parses env json file for given path and validates the parsed env json object.
* @param path Path of env.json file to be parsed.
* @returns For successful parsing we get HoppEnvs object.
* Parses env json file for given path and validates the parsed env json object
* @param path Path of env.json file to be parsed
* @returns For successful parsing we get HoppEnvs object
*/
export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path);
const envPairs: Array<HoppEnvPair> = [];
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
const HoppBulkEnvExportObjectResult =
HoppBulkEnvExportObject.safeParse(contents);
const envPairs: Array<HoppEnvPair | Record<string, string>> = [];
// CLI doesnt support bulk environments export.
// Hence we check for this case and throw an error if it matches the format.
// The legacy key-value pair format that is still supported
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
// Shape of the single environment export object that is exported from the app
const HoppEnvExportObjectResult = Environment.safeParse(contents);
// Shape of the bulk environment export object that is exported from the app
const HoppBulkEnvExportObjectResult = z
.array(entityReference(Environment))
.safeParse(contents);
// CLI doesnt support bulk environments export
// Hence we check for this case and throw an error if it matches the format
if (HoppBulkEnvExportObjectResult.success) {
throw error({ code: "BULK_ENV_FILE", path, data: error });
}
// Checks if the environment file is of the correct format.
// If it doesnt match either of them, we throw an error.
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
// Checks if the environment file is of the correct format
// If it doesnt match either of them, we throw an error
if (
!HoppEnvKeyPairResult.success &&
HoppEnvExportObjectResult.type === "err"
) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
}
@@ -36,9 +49,8 @@ export async function parseEnvsData(path: string) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value });
}
} else if (HoppEnvExportObjectResult.success) {
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
envPairs.push({ key, value });
} else if (HoppEnvExportObjectResult.type === "ok") {
envPairs.push(...HoppEnvExportObjectResult.value.variables);
}
return <HoppEnvs>{ global: [], selected: envPairs };

View File

@@ -1,31 +1,18 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { z } from "zod";
import { TestReport } from "../interfaces/response";
import { HoppCLIError } from "./errors";
import { z } from "zod";
export type FormDataEntry = {
key: string;
value: string | Blob;
};
export type HoppEnvPair = { key: string; value: string };
export type HoppEnvPair = Environment["variables"][number];
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
// Shape of the single environment export object that is exported from the app.
export const HoppEnvExportObject = z.object({
name: z.string(),
variables: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
});
// Shape of the bulk environment export object that is exported from the app.
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
export type HoppEnvs = {
global: HoppEnvPair[];
selected: HoppEnvPair[];

View File

@@ -1,9 +1,9 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { bold } from "chalk";
import chalk from "chalk";
import { log } from "console";
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";
import round from "lodash/round";
import { round } from "lodash-es";
import { CollectionRunnerParam } from "../types/collections";
import {
@@ -68,7 +68,7 @@ export const collectionsRunner = async (
};
// Request processing initiated message.
log(WARN(`\nRunning: ${bold(requestPath)}`));
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`));
// Processing current request.
const result = await processRequest(processRequestParams)();
@@ -131,7 +131,7 @@ const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
* path of each request within collection-json file, failed-tests-report, errors,
* total execution duration for requests, pre-request-scripts, test-scripts.
* @returns True, if collection runner executed without any errors or failed test-cases.
* False, if errors occured or test-cases failed.
* False, if errors occurred or test-cases failed.
*/
export const collectionsRunnerResult = (
requestsReport: RequestReport[]

View File

@@ -1,4 +1,4 @@
import { bold } from "chalk";
import chalk from "chalk";
import { groupEnd, group, log } from "console";
import { handleError } from "../handlers/error";
import { RequestConfig } from "../interfaces/request";
@@ -112,7 +112,7 @@ export const printTestsMetrics = (testsMetrics: TestMetrics) => {
/**
* Prints details of each reported error for a request with error code.
* @param path Request's path in collection for which errors occured.
* @param path Request's path in collection for which errors occurred.
* @param errorsReport List of errors reported.
*/
export const printErrorsReport = (
@@ -120,7 +120,7 @@ export const printErrorsReport = (
errorsReport: HoppCLIError[]
) => {
if (errorsReport.length > 0) {
const REPORTED_ERRORS_TITLE = FAIL(`\n${bold(path)} reported errors:`);
const REPORTED_ERRORS_TITLE = FAIL(`\n${chalk.bold(path)} reported errors:`);
group(REPORTED_ERRORS_TITLE);
for (const errorReport of errorsReport) {
@@ -143,7 +143,7 @@ export const printFailedTestsReport = (
// Only printing test-reports with failed test-cases.
if (failedTestsReport.length > 0) {
const FAILED_TESTS_PATH = FAIL(`\n${bold(path)} failed tests:`);
const FAILED_TESTS_PATH = FAIL(`\n${chalk.bold(path)} failed tests:`);
group(FAILED_TESTS_PATH);
for (const failedTestReport of failedTestsReport) {
@@ -176,7 +176,7 @@ export const printRequestRunner = {
*/
start: (requestConfig: RequestConfig) => {
const METHOD = BG_INFO(` ${requestConfig.method} `);
const ENDPOINT = requestConfig.url;
const ENDPOINT = requestConfig.displayUrl || requestConfig.url;
process.stdout.write(`${METHOD} ${ENDPOINT}`);
},

View File

@@ -1,4 +1,4 @@
import { clone } from "lodash";
import { clone } from "lodash-es";
/**
* Sorts the array based on the sort func.

View File

@@ -11,7 +11,7 @@ import * as E from "fp-ts/Either";
import * as S from "fp-ts/string";
import * as O from "fp-ts/Option";
import { error } from "../types/errors";
import round from "lodash/round";
import { round } from "lodash-es";
import { DEFAULT_DURATION_PRECISION } from "./constants";
/**

View File

@@ -36,7 +36,10 @@ import { toFormData } from "./mutators";
export const preRequestScriptRunner = (
request: HoppRESTRequest,
envs: HoppEnvs
): TE.TaskEither<HoppCLIError, EffectiveHoppRESTRequest> =>
): TE.TaskEither<
HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
> =>
pipe(
TE.of(request),
TE.chain(({ preRequestScript }) =>
@@ -68,7 +71,10 @@ export const preRequestScriptRunner = (
export function getEffectiveRESTRequest(
request: HoppRESTRequest,
environment: Environment
): E.Either<HoppCLIError, EffectiveHoppRESTRequest> {
): E.Either<
HoppCLIError,
{ effectiveRequest: EffectiveHoppRESTRequest } & { updatedEnvs: HoppEnvs }
> {
const envVariables = environment.variables;
// Parsing final headers with applied ENVs.
@@ -103,18 +109,31 @@ export function getEffectiveRESTRequest(
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
});
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
} else if (request.auth.authType === "bearer") {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(
request.auth.token,
envVariables
)}`,
value: `Bearer ${parseTemplateString(request.auth.token, envVariables)}`,
});
} else if (request.auth.authType === "oauth-2") {
const { addTo } = request.auth;
if (addTo === "HEADERS") {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, envVariables)}`,
});
} else if (addTo === "QUERY_PARAMS") {
effectiveFinalParams.push({
active: true,
key: "access_token",
value: parseTemplateString(
request.auth.grantTypeInfo.token,
envVariables
),
});
}
} else if (request.auth.authType === "api-key") {
const { key, value, addTo } = request.auth;
if (addTo === "Headers") {
@@ -162,12 +181,30 @@ export function getEffectiveRESTRequest(
}
const effectiveFinalURL = _effectiveFinalURL.right;
// Secret environment variables referenced in the request endpoint should be masked
let effectiveFinalDisplayURL;
if (envVariables.some(({ secret }) => secret)) {
const _effectiveFinalDisplayURL = parseTemplateStringE(
request.endpoint,
envVariables,
true
);
if (E.isRight(_effectiveFinalDisplayURL)) {
effectiveFinalDisplayURL = _effectiveFinalDisplayURL.right;
}
}
return E.right({
...request,
effectiveFinalURL,
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
effectiveRequest: {
...request,
effectiveFinalURL,
effectiveFinalDisplayURL,
effectiveFinalHeaders,
effectiveFinalParams,
effectiveFinalBody,
},
updatedEnvs: { global: [], selected: envVariables },
});
}

View File

@@ -1,4 +1,4 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { Environment, HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import axios, { Method } from "axios";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
@@ -29,6 +29,38 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
/**
* Processes given variable, which includes checking for secret variables
* and getting value from system environment
* @param variable Variable to be processed
* @returns Updated variable with value from system environment
*/
const processVariables = (variable: Environment["variables"][number]) => {
if (variable.secret) {
return {
...variable,
value:
"value" in variable ? variable.value : process.env[variable.key] || "",
};
}
return variable;
};
/**
* Processes given envs, which includes processing each variable in global
* and selected envs
* @param envs Global + selected envs used by requests with in collection
* @returns Processed envs with each variable processed
*/
const processEnvs = (envs: HoppEnvs) => {
const processedEnvs = {
global: envs.global.map(processVariables),
selected: envs.selected.map(processVariables),
};
return processedEnvs;
};
/**
* Transforms given request data to request-config used by request-runner to
* perform HTTP request.
@@ -38,6 +70,7 @@ import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
const config: RequestConfig = {
supported: true,
displayUrl: req.effectiveFinalDisplayURL,
};
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
const reqParams = finalParams(req);
@@ -98,6 +131,7 @@ export const requestRunner =
let status: number;
const baseResponse = await axios(requestConfig);
const { config } = baseResponse;
// PR-COMMENT: type error
const runnerResponse: RequestRunnerResponse = {
...baseResponse,
endpoint: getRequest.endpoint(config.url),
@@ -221,9 +255,16 @@ export const processRequest =
effectiveFinalParams: [],
effectiveFinalURL: "",
};
let updatedEnvs = <HoppEnvs>{};
// Fetch values for secret environment variables from system environment
const processedEnvs = processEnvs(envs);
// Executing pre-request-script
const preRequestRes = await preRequestScriptRunner(request, envs)();
const preRequestRes = await preRequestScriptRunner(
request,
processedEnvs
)();
if (E.isLeft(preRequestRes)) {
printPreRequestRunner.fail();
@@ -231,8 +272,8 @@ export const processRequest =
report.errors.push(preRequestRes.left);
report.result = report.result && false;
} else {
// Updating effective-request
effectiveRequest = preRequestRes.right;
// Updating effective-request and consuming updated envs after pre-request script execution
({ effectiveRequest, updatedEnvs } = preRequestRes.right);
}
// Creating request-config for request-runner.
@@ -270,7 +311,7 @@ export const processRequest =
const testScriptParams = getTestScriptParams(
_requestRunnerRes,
request,
envs
updatedEnvs
);
// Executing test-runner.
@@ -310,7 +351,7 @@ export const processRequest =
*/
export const preProcessRequest = (
request: HoppRESTRequest,
collection: HoppCollection,
collection: HoppCollection
): HoppRESTRequest => {
const tempRequest = Object.assign({}, request);
const { headers: parentHeaders, auth: parentAuth } = collection;
@@ -335,8 +376,10 @@ export const preProcessRequest = (
// Filter out header entries present in the parent (folder/collection) under the same name
// This ensures the child headers take precedence over the parent headers
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
})
return !tempRequest.headers.some(
(reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key
);
});
tempRequest.headers.push(...filteredEntries);
} else if (!tempRequest.headers) {
tempRequest.headers = [];

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"target": "ESNext",
"module": "ESNext",
"outDir": ".",
"rootDir": ".",
"strict": true,

View File

@@ -3,17 +3,14 @@ import { defineConfig } from "tsup";
export default defineConfig({
entry: [ "./src/index.ts" ],
outDir: "./dist/",
format: ["cjs"],
format: ["esm"],
platform: "node",
sourcemap: true,
bundle: true,
target: "node12",
target: "esnext",
skipNodeModulesBundle: false,
esbuildOptions(options) {
options.bundle = true
},
noExternal: [
/\w+/
],
clean: true,
});

View File

@@ -429,6 +429,11 @@ pre.ace_editor {
}
}
.splitpanes__pane {
@apply will-change-auto;
transform: translateZ(0);
}
.smart-splitter .splitpanes__splitter {
@apply relative;
@apply before:absolute;
@@ -558,12 +563,22 @@ details[open] summary .indicator {
.env-highlight {
@apply text-accentContrast;
&.env-found {
@apply bg-accentDark;
@apply hover:bg-accent;
&.request-variable-highlight {
@apply bg-amber-500;
@apply hover:bg-amber-600;
}
&.env-not-found {
&.environment-variable-highlight {
@apply bg-green-500;
@apply hover:bg-green-600;
}
&.global-variable-highlight {
@apply bg-blue-500;
@apply hover:bg-blue-600;
}
&.environment-not-found-highlight {
@apply bg-red-500;
@apply hover:bg-red-600;
}

View File

@@ -6,7 +6,7 @@
"choose_file": "选择文件",
"clear": "清除",
"clear_all": "全部清除",
"clear_history": "Clear all History",
"clear_history": "清除全部历史记录",
"close": "关闭",
"connect": "连接",
"connecting": "连接中",
@@ -35,7 +35,7 @@
"prettify": "美化",
"properties": "Properties",
"remove": "移除",
"rename": "Rename",
"rename": "重命名",
"restore": "恢复",
"save": "保存",
"scroll_to_bottom": "滚动至底部",
@@ -86,8 +86,8 @@
"search": "搜索",
"share": "分享",
"shortcuts": "快捷方式",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
"social_links": "Social links",
"social_description": "在社交媒体上关注我们,了解最新新闻、更新和发布。",
"social_links": "社交媒体链接",
"spotlight": "聚光灯",
"status": "状态",
"status_description": "检查网站状态",
@@ -121,7 +121,7 @@
"generate_token": "生成令牌",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "包含在 URL 内",
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
"inherited_from": "Inherited {auth} from parent collection {collection} ",
"learn": "了解更多",
"oauth": {
"redirect_auth_server_returned_error": "Auth Server returned an error state",
@@ -162,15 +162,15 @@
"renamed": "集合已更名",
"request_in_use": "请求正在使用中",
"save_as": "另存为",
"save_to_collection": "Save to Collection",
"save_to_collection": "保存至集合",
"select": "选择一个集合",
"select_location": "选择位置",
"select_team": "选择一个团队",
"team_collections": "团队集合"
},
"confirm": {
"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.",
"close_unsaved_tab": "你确定要关闭此标签页吗?",
"close_unsaved_tabs": "你确定要关闭所有标签页吗? {count} 个未保存的标签页将被丢失。",
"exit_team": "你确定要离开此团队吗?",
"logout": "你确定要登出吗?",
"remove_collection": "你确定要永久删除该集合吗?",
@@ -186,9 +186,9 @@
"sync": "您确定要同步该工作区吗?"
},
"context_menu": {
"add_parameters": "Add to parameters",
"open_request_in_new_tab": "Open request in new tab",
"set_environment_variable": "Set as variable"
"add_parameters": "添加至参数",
"open_request_in_new_tab": "在新标签页中打开请求",
"set_environment_variable": "设置为变量"
},
"cookies": {
"modal": {
@@ -244,7 +244,7 @@
"team_name": "团队名称为空",
"teams": "团队为空",
"tests": "没有针对该请求的测试",
"shortcodes": "Shortcodes 为空"
"shortcodes": "短链接为空"
},
"environment": {
"add_to_global": "添加到全局环境",
@@ -252,32 +252,32 @@
"create_new": "创建新环境",
"created": "环境已创建",
"deleted": "环境已删除",
"duplicated": "Environment duplicated",
"duplicated": "环境已复制",
"edit": "编辑环境",
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Global variables",
"empty_variables": "没有变量",
"global": "全局",
"global_variables": "全局变量",
"import_or_create": "Import or create a environment",
"invalid_name": "请提供有效的环境名称",
"list": "Environment variables",
"list": "环境变量",
"my_environments": "我的环境",
"name": "Name",
"name": "名称",
"nested_overflow": "环境嵌套深度超过限制10层",
"new": "新建环境",
"no_active_environment": "No active environment",
"no_active_environment": "没有激活的环境",
"no_environment": "无环境",
"no_environment_description": "没有选择环境。选择如何处理以下变量。",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"quick_peek": "快速浏览环境",
"replace_with_variable": "替换为变量",
"scope": "范围",
"select": "选择环境",
"set": "Set environment",
"set_as_environment": "Set as environment",
"set": "设置环境",
"set_as_environment": "设置为环境",
"team_environments": "团队环境",
"title": "环境",
"updated": "环境已更新",
"value": "Value",
"variable": "Variable",
"value": "",
"variable": "变量",
"variable_list": "变量列表"
},
"error": {
@@ -296,6 +296,7 @@
"incorrect_email": "电子邮箱错误",
"invalid_link": "无效链接",
"invalid_link_description": "你点击的链接无效或已过期。",
"invalid_embed_link": "The embed does not exist or is invalid.",
"json_parsing_failed": "不合法的 JSON",
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
"network_error": "好像发生了网络错误,请重试。",
@@ -306,7 +307,7 @@
"no_results_found": "找不到结果",
"page_not_found": "找不到此頁面",
"please_install_extension": "Please install the extension and add origin to the extension.",
"proxy_error": "Proxy error",
"proxy_error": "代理错误",
"script_fail": "无法执行预请求脚本",
"something_went_wrong": "发生了一些错误",
"test_script_fail": "无法执行请求脚本"
@@ -314,10 +315,13 @@
"export": {
"as_json": "导出为 JSON",
"create_secret_gist": "创建私密 Gist",
"create_secret_gist_tooltip_text": "Export as secret Gist",
"failed": "Something went wrong while exporting",
"gist_created": "已创建 Gist",
"secret_gist_success": "Successfully exported as secret Gist",
"require_github": "使用 GitHub 登录以创建私密 Gist",
"title": "导出"
"title": "导出",
"success": "Successfully exported",
"gist_created": "已创建 Gist"
},
"filter": {
"all": "全部",
@@ -333,13 +337,13 @@
"renamed": "文件夹已更名"
},
"graphql": {
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
"connection_switch_confirm": "您想连接最新的 GraphQL 端点吗?",
"connection_switch_new_url": "切换到标签页将使您与活动的 GraphQL 连接断开。新的连接 URL ",
"connection_switch_url": "您已连接到 GraphQL 端点,连接 URL ",
"mutations": "变更",
"schema": "模式",
"subscriptions": "订阅",
"switch_connection": "Switch connection"
"switch_connection": "切换连接"
},
"graphql_collections": {
"title": "GraphQL Collections"
@@ -408,27 +412,27 @@
"title": "导入"
},
"inspections": {
"description": "Inspect possible errors",
"description": "查可能的错误",
"environment": {
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
"add_environment": "添加到环境",
"not_found": "环境变量“{environment}”未找到。"
},
"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."
"cookie": "浏览器不允许 Hoppscotch 设置 Cookie 标头。当前我们正在开发 Hoppscotch 桌面应用程序(即将推出),与此同时请改用授权标头。"
},
"response": {
"401_error": "Please check your authentication credentials.",
"404_error": "Please check your request URL and method type.",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
"default_error": "Please check your request.",
"network_error": "Please check your network connection."
"401_error": "请检查您的身份验证凭据。",
"404_error": "请检查您的请求 URL 和方法类型。",
"cors_error": "请检查您的跨源资源共享配置。",
"default_error": "请检查您的请求。",
"network_error": "请检查您的网络连接。"
},
"title": "Inspector",
"url": {
"extension_not_installed": "Extension not installed.",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
"extention_enable_action": "Enable Browser Extension",
"extention_not_enabled": "Extension not enabled."
"extension_not_installed": "未安装扩展。",
"extension_unknown_origin": "确保您已将 API 端点的源添加到 Hoppscotch 浏览器扩展列表中。",
"extention_enable_action": "启用浏览器扩展",
"extention_not_enabled": "扩展未启用。"
}
},
"layout": {
@@ -460,10 +464,10 @@
"invalid_topic": "请提供该订阅的主题",
"keep_alive": "Keep Alive",
"log": "日志",
"lw_message": "Last-Will Message",
"lw_qos": "Last-Will QoS",
"lw_retain": "Last-Will Retain",
"lw_topic": "Last-Will Topic",
"lw_message": "遗嘱消息",
"lw_qos": "遗嘱消息QoS",
"lw_retain": "遗嘱消息保留",
"lw_topic": "遗嘱消息主题",
"message": "消息",
"new": "新订阅",
"not_connected": "请先启动MQTT连接。",
@@ -544,7 +548,7 @@
"payload": "负载",
"query": "查询",
"raw_body": "原始请求体",
"rename": "Rename Request",
"rename": "重命名请求",
"renamed": "请求重命名",
"run": "运行",
"save": "保存",
@@ -553,7 +557,7 @@
"share": "分享",
"share_description": "分享 Hoppscotch 给你的朋友",
"share_request": "Share Request",
"stop": "Stop",
"stop": "停止",
"title": "请求",
"type": "请求类型",
"url": "URL",
@@ -684,26 +688,26 @@
"title": "导航"
},
"others": {
"prettify": "Prettify Editor's Content",
"title": "Others"
"prettify": "美化内容",
"title": "其他"
},
"request": {
"delete_method": "选择 DELETE 方法",
"get_method": "选择 GET 方法",
"head_method": "选择 HEAD 方法",
"import_curl": "Import cURL",
"import_curl": "导入cURL",
"method": "方法",
"next_method": "选择下一个方法",
"post_method": "选择 POST 方法",
"previous_method": "选择上一个方法",
"put_method": "选择 PUT 方法",
"rename": "Rename Request",
"rename": "重命名请求",
"reset_request": "重置请求",
"save_request": "Save Request",
"save_request": "保存请求",
"save_to_collections": "保存到集合",
"send_request": "发送请求",
"share_request": "Share Request",
"show_code": "Generate code snippet",
"show_code": "生成代码片段",
"title": "请求",
"copy_request_link": "复制请求链接"
},
@@ -735,82 +739,82 @@
"url": "URL"
},
"spotlight": {
"change_language": "Change Language",
"change_language": "更改语言",
"environments": {
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"duplicate_global": "Duplicate global environment",
"edit": "Edit current environment",
"edit_global": "Edit global environment",
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"title": "Environments"
"delete": "删除当前环境",
"duplicate": "复制当前环境",
"duplicate_global": "复制全局环境",
"edit": "编辑当前环境",
"edit_global": "编辑全局环境",
"new": "创建新环境",
"new_variable": "创建新的环境变量",
"title": "环境"
},
"general": {
"chat": "Chat with support",
"help_menu": "Help and support",
"open_docs": "Read Documentation",
"open_github": "Open GitHub repository",
"open_keybindings": "Keyboard shortcuts",
"social": "Social",
"title": "General"
"chat": "与支持人员聊天",
"help_menu": "帮助和支持",
"open_docs": "阅读文档",
"open_github": "打开 GitHub 存储库",
"open_keybindings": "键盘快捷键",
"social": "社交媒体",
"title": "一般"
},
"graphql": {
"connect": "Connect to server",
"disconnect": "Disconnect from server"
"connect": "连接到服务器",
"disconnect": "与服务器断开连接"
},
"miscellaneous": {
"invite": "Invite your friends to Hoppscotch",
"title": "Miscellaneous"
"invite": "邀请你的朋友来 Hoppscotch",
"title": "杂项"
},
"request": {
"save_as_new": "Save as new request",
"select_method": "Select method",
"switch_to": "Switch to",
"tab_authorization": "Authorization tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_parameters": "Parameters tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_query": "Query tab",
"tab_tests": "Tests tab",
"tab_variables": "Variables tab"
"save_as_new": "另存为新请求",
"select_method": "选择方法",
"switch_to": "切换到",
"tab_authorization": "授权标签页",
"tab_body": "请求体标签页",
"tab_headers": "请求头标签页",
"tab_parameters": "参数标签页",
"tab_pre_request_script": "预请求脚本标签页",
"tab_query": "查询标签页",
"tab_tests": "测试标签页b",
"tab_variables": "变量标签页"
},
"response": {
"copy": "Copy response",
"download": "Download response as file",
"title": "Response"
"copy": "复制响应",
"download": "将响应下载为文件",
"title": "响应"
},
"section": {
"interceptor": "Interceptor",
"interface": "Interface",
"theme": "Theme",
"user": "User"
"interceptor": "拦截器",
"interface": "界面",
"theme": "主题",
"user": "用户"
},
"settings": {
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
"change_interceptor": "更改拦截器",
"change_language": "更改语言",
"theme": {
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
"black": "黑色",
"dark": "暗色",
"light": "亮色",
"system": "系统"
}
},
"tab": {
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"duplicate": "Duplicate current tab",
"new_tab": "Open a new tab",
"title": "Tabs"
"close_current": "关闭当前标签页",
"close_others": "关闭所有其他标签页",
"duplicate": "复制当前标签页",
"new_tab": "打开新的标签页",
"title": "标签页"
},
"workspace": {
"delete": "Delete current team",
"edit": "Edit current team",
"invite": "Invite people to team",
"new": "Create new team",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
"delete": "删除当前团队",
"edit": "编辑当前团队",
"invite": "邀请人员加入团队",
"new": "创建新团队",
"switch_to_personal": "切换到您的个人工作空间",
"title": "团队"
}
},
"sse": {
@@ -983,10 +987,10 @@
"shortcodes": {
"actions": "操作",
"created_on": "创建于",
"deleted": "已刪除快捷键",
"deleted": "已刪除短链接",
"method": "方法",
"not_found": "找不到快捷键",
"short_code": "快捷键",
"not_found": "找不到短链接",
"short_code": "短链接",
"url": "URL"
}
}

View File

@@ -24,6 +24,7 @@
"go_back": "Go back",
"go_forward": "Go forward",
"group_by": "Group by",
"hide_secret": "Hide secret",
"label": "Label",
"learn_more": "Learn more",
"less": "Less",
@@ -43,6 +44,7 @@
"search": "Search",
"send": "Send",
"share": "Share",
"show_secret": "Show secret",
"start": "Start",
"starting": "Starting",
"stop": "Stop",
@@ -101,8 +103,10 @@
"auth": {
"account_exists": "Account exists with different credential - Login to link both accounts",
"all_sign_in_options": "All sign in options",
"continue_with_auth_provider": "Continue with {provider}",
"continue_with_email": "Continue with Email",
"continue_with_github": "Continue with GitHub",
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
"continue_with_google": "Continue with Google",
"continue_with_microsoft": "Continue with Microsoft",
"email": "Email",
@@ -135,7 +139,26 @@
"redirect_no_token_endpoint": "No Token Endpoint Defined",
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
"grant_type": "Grant Type",
"grant_type_auth_code": "Authorization Code",
"token_fetched_successfully": "Token fetched successfully",
"token_fetch_failed": "Failed to fetch token",
"validation_failed": "Validation Failed, please check the form fields",
"label_authorization_endpoint": "Authorization Endpoint",
"label_client_id": "Client ID",
"label_client_secret": "Client Secret",
"label_code_challenge": "Code Challenge",
"label_code_challenge_method": "Code Challenge Method",
"label_code_verifier": "Code Verifier",
"label_scopes": "Scopes",
"label_token_endpoint": "Token Endpoint",
"label_use_pkce": "Use PKCE",
"label_implicit": "Implicit",
"label_password": "Password",
"label_username": "Username",
"label_auth_code": "Authorization Code",
"label_client_credentials": "Client Credentials"
},
"pass_key_by": "Pass by",
"password": "Password",
@@ -152,7 +175,7 @@
"invalid_name": "Please provide a name for the collection",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "My Collections",
"my_collections": "Personal Collections",
"name": "My New Collection",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "New Collection",
@@ -164,14 +187,12 @@
"save_as": "Save as",
"save_to_collection": "Save to Collection",
"select": "Select a Collection",
"select_location": "Select location",
"select_team": "Select a team",
"team_collections": "Team Collections"
"select_location": "Select location"
},
"confirm": {
"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.",
"exit_team": "Are you sure you want to leave this team?",
"exit_team": "Are you sure you want to leave this workspace?",
"logout": "Are you sure you want to logout?",
"remove_collection": "Are you sure you want to permanently delete this collection?",
"remove_environment": "Are you sure you want to permanently delete this environment?",
@@ -179,7 +200,7 @@
"remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this team?",
"remove_team": "Are you sure you want to delete this workspace?",
"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.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
@@ -232,17 +253,19 @@
"headers": "This request does not have any headers",
"history": "History is empty",
"invites": "Invite list is empty",
"members": "Team is empty",
"members": "Workspace is empty",
"parameters": "This request does not have any parameters",
"pending_invites": "There are no pending invites for this team",
"pending_invites": "There are no pending invites for this workspace",
"profile": "Login to view your profile",
"protocols": "Protocols are empty",
"request_variables": "This request does not have any request variables",
"schema": "Connect to a GraphQL endpoint to view schema",
"secret_environments": "Secrets are not synced to Hoppscotch",
"shared_requests": "Shared requests are empty",
"shared_requests_logout": "Login to view your shared requests or create a new one",
"subscription": "Subscriptions are empty",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
"team_name": "Workspace name empty",
"teams": "You don't belong to any workspaces",
"tests": "There are no tests for this request"
},
"environment": {
@@ -259,7 +282,7 @@
"import_or_create": "Import or create a environment",
"invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments",
"my_environments": "Personal Environments",
"name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
@@ -269,14 +292,17 @@
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"secrets": "Secrets",
"secret_value": "Secret value",
"select": "Select environment",
"set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "Team Environments",
"team_environments": "Workspace Environments",
"title": "Environments",
"updated": "Environment updated",
"value": "Value",
"variable": "Variable",
"variables": "Variables",
"variable_list": "Variable List"
},
"error": {
@@ -286,8 +312,8 @@
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL is not formatted properly",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
"delete_account": "Your account is currently an owner in these workspaces:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.",
"empty_req_name": "Empty Request Name",
"f12_details": "(F12 for details)",
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
@@ -309,7 +335,8 @@
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script"
"test_script_fail": "Could not execute post-request script",
"reading_files": "Error while reading one or more files."
},
"export": {
"as_json": "Export as JSON",
@@ -388,8 +415,8 @@
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_my_collections": "Import from My Collections",
"from_my_collections_description": "Import from My Collections file",
"from_my_collections": "Import from Personal Collections",
"from_my_collections_description": "Import from Personal Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
@@ -407,12 +434,17 @@
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment from a JSON file",
"title": "Import"
"title": "Import",
"file_size_limit_exceeded_warning_multiple_files": "Chosen files exceed the recommended limit of 10MB. Only the first {files} selected will be imported",
"file_size_limit_exceeded_warning_single_file": "The currently chosen file exceeds the recommended limit of 10MB. Please select another file.",
"success": "Successfully imported"
},
"inspections": {
"description": "Inspect possible errors",
"environment": {
"add_environment": "Add to Environment",
"add_environment_value": "Add value",
"empty_value": "Environment value is empty for the variable '{variable}' ",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
@@ -501,7 +533,7 @@
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
"no_permission": "You do not have permission to perform this action.",
"owner": "Owner",
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
"owner_description": "Owners can add, edit, and delete requests, collections and workspace members.",
"roles": "Roles",
"roles_description": "Roles are used to control access to the shared collections.",
"updated": "Profile updated",
@@ -548,6 +580,7 @@
"raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed",
"request_variables": "Request variables",
"run": "Run",
"save": "Save",
"save_as": "Save as",
@@ -805,12 +838,12 @@
"title": "Tabs"
},
"workspace": {
"delete": "Delete current team",
"edit": "Edit current team",
"invite": "Invite people to team",
"new": "Create new team",
"delete": "Delete current workspace",
"edit": "Edit current workspace",
"invite": "Invite people to workspace",
"new": "Create new workspace",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
"title": "Workspaces"
}
},
"sse": {
@@ -867,7 +900,6 @@
"forum": "Ask questions and get answers",
"github": "Follow us on Github",
"shortcuts": "Browse app faster",
"team": "Get in touch with the team",
"title": "Support",
"twitter": "Follow us on Twitter"
},
@@ -889,6 +921,7 @@
"query": "Query",
"schema": "Schema",
"shared_requests": "Shared Requests",
"share_tab_request": "Share tab request",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Tests",
@@ -897,60 +930,60 @@
"websocket": "WebSocket"
},
"team": {
"already_member": "You are already a member of this team. Contact your team owner.",
"create_new": "Create new team",
"deleted": "Team deleted",
"edit": "Edit Team",
"already_member": "You are already a member of this workspace. Contact your workspace owner.",
"create_new": "Create new workspace",
"deleted": "Workspace deleted",
"edit": "Edit Workspace",
"email": "E-mail",
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Exit Team",
"exit_disabled": "Only owner cannot exit the team",
"email_do_not_match": "Email doesn't match with your account details. Contact your workspace owner.",
"exit": "Exit Workspace",
"exit_disabled": "Only owner cannot exit the workspace",
"failed_invites": "Failed invites",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Email format is invalid",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_id": "Invalid workspace ID. Contact your workspace owner.",
"invalid_invite_link": "Invalid invite link",
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
"invalid_member_permission": "Please provide a valid permission to the team member",
"invalid_invite_link_description": "The link you followed is invalid. Contact your workspace owner.",
"invalid_member_permission": "Please provide a valid permission to the workspace member",
"invite": "Invite",
"invite_more": "Invite more",
"invite_tooltip": "Invite people to this workspace",
"invited_to_team": "{owner} invited you to join {team}",
"invited_to_team": "{owner} invited you to join {workspace}",
"join": "Invitation accepted",
"join_beta": "Join the beta program to access teams.",
"join_team": "Join {team}",
"joined_team": "You have joined {team}",
"joined_team_description": "You are now a member of this team",
"left": "You left the team",
"join_team": "Join {workspace}",
"joined_team": "You have joined {workspace}",
"joined_team_description": "You are now a member of this workspace",
"left": "You left the workspace",
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to join a team.",
"login_to_continue_description": "You need to be logged in to join a workspace.",
"logout_and_try_again": "Logout and sign in with another account",
"member_has_invite": "This email ID already has an invite. Contact your team owner.",
"member_not_found": "Member not found. Contact your team owner.",
"member_has_invite": "This email ID already has an invite. Contact your workspace owner.",
"member_not_found": "Member not found. Contact your workspace owner.",
"member_removed": "User removed",
"member_role_updated": "User roles updated",
"members": "Members",
"more_members": "+{count} more",
"name_length_insufficient": "Team name should be at least 6 characters long",
"name_updated": "Team name updated",
"new": "New Team",
"new_created": "New team created",
"new_name": "My New Team",
"no_access": "You do not have edit access to this team",
"no_invite_found": "Invitation not found. Contact your team owner.",
"name_length_insufficient": "Workspace name should be at least 6 characters long",
"name_updated": "Workspace name updated",
"new": "New Workspace",
"new_created": "New workspace created",
"new_name": "My New Workspace",
"no_access": "You do not have edit access to this workspace",
"no_invite_found": "Invitation not found. Contact your workspace owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"not_found": "Workspace not found. Contact your workspace owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your workspace owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Permissions",
"same_target_destination": "Same target and destination",
"saved": "Team saved",
"select_a_team": "Select a team",
"saved": "Workspace saved",
"select_a_team": "Select a workspace",
"success_invites": "Success invites",
"title": "Teams",
"title": "Workspaces",
"we_sent_invite_link": "We sent an invite link to all invitees!",
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the workspace.",
"search_title": "Team Requests"
},
"team_environment": {
"deleted": "Environment Deleted",
@@ -976,8 +1009,14 @@
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"personal": "Personal Workspace",
"other_workspaces": "My Workspaces",
"team": "Workspace",
"title": "Workspaces"
},
"site_protection": {
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance.",
"error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status"
}
}

View File

@@ -1,6 +1,6 @@
{
"action": {
"add": "Add",
"add": "Добавить",
"autoscroll": "Автоскрол",
"cancel": "Отменить",
"choose_file": "Выберите файл",
@@ -121,7 +121,7 @@
"generate_token": "Сгенерировать токен",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Добавить в URL",
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
"inherited_from": "Inherited {auth} from parent collection {collection} ",
"learn": "Узнать больше",
"oauth": {
"redirect_auth_server_returned_error": "Auth Server returned an error state",
@@ -148,7 +148,7 @@
"created": "Коллекция создана",
"different_parent": "Нельзя сортировать коллекцию с разной родительской коллекцией",
"edit": "Редактировать коллекцию",
"import_or_create": "Import or create a collection",
"import_or_create": "Вы можете импортировать существующую или создать новую коллекцию",
"invalid_name": "Укажите допустимое название коллекции",
"invalid_root_move": "Коллекция уже в корне",
"moved": "Перемещено успешно",
@@ -170,7 +170,7 @@
},
"confirm": {
"close_unsaved_tab": "Вы уверены что хотите закрыть эту вкладку?",
"close_unsaved_tabs": "ВЫ уверены что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
"close_unsaved_tabs": "Вы уверены что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
"exit_team": "Вы точно хотите покинуть эту команду?",
"logout": "Вы действительно хотите выйти?",
"remove_collection": "Вы уверены, что хотите навсегда удалить эту коллекцию?",
@@ -192,20 +192,20 @@
},
"cookies": {
"modal": {
"cookie_expires": "Expires",
"cookie_name": "Name",
"cookie_path": "Path",
"cookie_string": "Cookie string",
"cookie_value": "Value",
"empty_domain": "Domain is empty",
"empty_domains": "Domain list is empty",
"enter_cookie_string": "Enter cookie string",
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
"cookie_expires": "Истекает",
"cookie_name": "Имя",
"cookie_path": "Путь",
"cookie_string": "Cookie параметры",
"cookie_value": "Значение",
"empty_domain": "Нужно заполнить домен",
"empty_domains": "Список доменов пуст",
"enter_cookie_string": "Введите cookie параметры сюда",
"interceptor_no_support": "Ваш текущий Перехватчик не поддерживает работу с cookie. Выберите другой Перехватчик и попробуйте еще раз.",
"managed_tab": "Managed",
"new_domain_name": "New domain name",
"no_cookies_in_domain": "No cookies set for this domain",
"new_domain_name": "Добавить новый домен",
"no_cookies_in_domain": "Никаких cookie не установлено для этого домена",
"raw_tab": "Raw",
"set": "Set a cookie"
"set": "Установить cookie"
}
},
"count": {
@@ -257,14 +257,14 @@
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Global variables",
"import_or_create": "Import or create a environment",
"import_or_create": "Импортировать или создать новое окружение",
"invalid_name": "Укажите допустимое имя для окружения",
"list": "Environment variables",
"list": "Переменные окружения",
"my_environments": "Мои окружения",
"name": "Name",
"nested_overflow": "максимальный уровень вложения переменных окружения - 10",
"new": "Новая среда",
"no_active_environment": "No active environment",
"no_active_environment": "Нет активных окружений",
"no_environment": "Нет окружения",
"no_environment_description": "Не выбрано окружение, выберите что делать с переменными.",
"quick_peek": "Environment Quick Peek",
@@ -284,7 +284,7 @@
"authproviders_load_error": "Unable to load auth providers",
"browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.",
"check_console_details": "Подробности смотрите в журнале консоли.",
"check_how_to_add_origin": "Check how you can add an origin",
"check_how_to_add_origin": "Инструкция как добавить origin в настройки расширения",
"curl_invalid_format": "cURL неправильно отформатирован",
"danger_zone": "Опасная зона",
"delete_account": "Вы являетесь владельцем этой команды:",
@@ -296,16 +296,17 @@
"incorrect_email": "Не корректный Email",
"invalid_link": "Не корректная ссылка",
"invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.",
"invalid_embed_link": "The embed does not exist or is invalid.",
"json_parsing_failed": "Не корректный JSON",
"json_prettify_invalid_body": "Не удалось определить недопустимое тело, устранить синтаксические ошибки json и повторить попытку.",
"network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.",
"network_fail": "Не удалось отправить запрос",
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
"no_collections_to_export": "Нечего экспортировать. Для начала нужно создать коллекцию.",
"no_duration": "Без продолжительности",
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
"no_environments_to_export": "Нечего экспортировать. Для начала нужно создать переменные окружения.",
"no_results_found": "Совпадения не найдены",
"page_not_found": "Эта страница не найдена",
"please_install_extension": "Please install the extension and add origin to the extension.",
"please_install_extension": "Нужно установить специальное расширение и добавить этот домен как новый origin в настройках расширения.",
"proxy_error": "Proxy error",
"script_fail": "Не удалось выполнить сценарий предварительного запроса",
"something_went_wrong": "Что-то пошло не так",
@@ -314,10 +315,13 @@
"export": {
"as_json": "Экспорт как JSON",
"create_secret_gist": "Создать секретный Gist",
"create_secret_gist_tooltip_text": "Export as secret Gist",
"failed": "Something went wrong while exporting",
"gist_created": "Gist создан",
"secret_gist_success": "Successfully exported as secret Gist",
"require_github": "Войдите через GitHub, чтобы создать секретную суть",
"title": "Экспорт"
"title": "Экспорт",
"success": "Successfully exported",
"gist_created": "Gist создан"
},
"filter": {
"all": "Все",
@@ -375,7 +379,7 @@
},
"import": {
"collections": "Импортировать коллекции",
"curl": "Импортировать cURL",
"curl": "Импортировать из cURL",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"failed": "Ошибка импорта",
@@ -523,10 +527,10 @@
"different_collection": "Нельзя изменять порядок запросов из разных коллекций",
"duplicated": "Запрос скопирован",
"duration": "Продолжительность",
"enter_curl": "Введите cURL",
"enter_curl": "Введите сюда команду cURL",
"generate_code": "Сгенерировать код",
"generated_code": "Сгенерированный код",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_authorization_tab": "Go to Authorization",
"go_to_body_tab": "Go to Body tab",
"header_list": "Список заголовков",
"invalid_name": "Укажите имя для запроса",
@@ -544,7 +548,7 @@
"payload": "Полезная нагрузка",
"query": "Запрос",
"raw_body": "Необработанное тело запроса",
"rename": "Rename Request",
"rename": "Переименость запрос",
"renamed": "Запрос переименован",
"run": "Запустить",
"save": "Сохранить",
@@ -691,13 +695,13 @@
"delete_method": "Выберите метод DELETE",
"get_method": "Выберите метод GET",
"head_method": "Выберите метод HEAD",
"import_curl": "Import cURL",
"import_curl": "Импортировать из cURL",
"method": "Методика",
"next_method": "Выберите следующий метод",
"post_method": "Выберите метод POST",
"previous_method": "Выбрать предыдущий метод",
"put_method": "Выберите метод PUT",
"rename": "Rename Request",
"rename": "Переименовать запрос",
"reset_request": "Сбросить запрос",
"save_request": "Сохарнить запрос",
"save_to_collections": "Сохранить в коллекции",
@@ -874,8 +878,8 @@
"tab": {
"authorization": "Авторизация",
"body": "Тело",
"close": "Close Tab",
"close_others": "Close other Tabs",
"close": "Закрыть вкладку",
"close_others": "Закрыть остальные вкладки",
"collections": "Коллекции",
"documentation": "Документация",
"duplicate": "Duplicate Tab",
@@ -905,7 +909,7 @@
"email_do_not_match": "Электронная почта, которой Вы воспользовались не соответсвует указанной в данных Вашей учетной записи.",
"exit": "Выйти из команды",
"exit_disabled": "Только владелец не может выйти из команды",
"failed_invites": "Failed invites",
"failed_invites": "Непринятые приглашения",
"invalid_coll_id": "Не верный идентификатор коллекции",
"invalid_email_format": "Формат электронной почты недействителен",
"invalid_id": "Некорректный ID команды. Свяжитесь с руководителем команды.",
@@ -947,7 +951,7 @@
"same_target_destination": "Таже цель и конечная точка",
"saved": "Команда сохранена",
"select_a_team": "Выбрать команду",
"success_invites": "Success invites",
"success_invites": "Принятые приглашения",
"title": "Команды",
"we_sent_invite_link": "Мы отправили все приглашения!",
"we_sent_invite_link_description": "Попросите тех, кого Вы пригласили, проверить их почтовые ящики. Им нужно перейди по ссылке, чтобы подтвердить вступление в эту команду."

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.12.0-1",
"version": "2024.3.0",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -21,147 +21,147 @@
"do-lintfix": "pnpm run lintfix"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "6.9.3",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.3",
"@apidevtools/swagger-parser": "10.1.0",
"@codemirror/autocomplete": "6.13.0",
"@codemirror/commands": "6.3.3",
"@codemirror/lang-javascript": "6.2.2",
"@codemirror/lang-json": "6.0.1",
"@codemirror/lang-xml": "6.1.0",
"@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/lint": "6.5.0",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.25.1",
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "^0.1.0",
"@hoppscotch/vue-toasted": "^0.1.0",
"@hoppscotch/ui": "0.1.0",
"@hoppscotch/vue-toasted": "0.1.0",
"@lezer/highlight": "1.2.0",
"@unhead/vue": "^1.8.8",
"@urql/core": "^4.2.0",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.3",
"@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.6.1",
"acorn-walk": "^8.3.0",
"axios": "^1.6.2",
"buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "^1.0.1",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.16.1",
"globalthis": "^1.0.3",
"graphql": "^16.8.1",
"graphql-language-service-interface": "^2.10.2",
"graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1",
"insomnia-importers": "^3.6.0",
"io-ts": "^2.2.20",
"js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21",
"lossless-json": "^3.0.2",
"minisearch": "^6.3.0",
"nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0",
"path": "^0.12.7",
"postman-collection": "^4.3.0",
"process": "^0.11.10",
"qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
"socketio-wildcard": "^2.0.0",
"splitpanes": "^3.1.5",
"stream-browserify": "^3.0.0",
"subscriptions-transport-ws": "^0.11.0",
"tern": "^0.24.3",
"timers": "^0.1.1",
"tippy.js": "^6.3.7",
"url": "^0.11.3",
"util": "^0.12.5",
"uuid": "^9.0.1",
"verzod": "^0.2.0",
"vue": "^3.3.8",
"vue-i18n": "^9.7.1",
"vue-pdf-embed": "^1.2.1",
"vue-router": "^4.2.5",
"@unhead/vue": "1.8.8",
"@urql/core": "4.2.0",
"@urql/devtools": "2.0.3",
"@urql/exchange-auth": "2.1.6",
"@urql/exchange-graphcache": "6.4.0",
"@vitejs/plugin-legacy": "4.1.1",
"@vueuse/core": "10.7.0",
"acorn-walk": "8.3.0",
"axios": "1.6.2",
"buffer": "6.0.3",
"cookie-es": "1.0.0",
"dioc": "1.0.1",
"esprima": "4.0.1",
"events": "3.3.0",
"fp-ts": "2.16.1",
"globalthis": "1.0.3",
"graphql": "16.8.1",
"graphql-language-service-interface": "2.10.2",
"graphql-tag": "2.12.6",
"httpsnippet": "3.0.1",
"insomnia-importers": "3.6.0",
"io-ts": "2.2.20",
"js-yaml": "4.1.0",
"jsonpath-plus": "7.2.0",
"lodash-es": "4.17.21",
"lossless-json": "3.0.2",
"minisearch": "6.3.0",
"nprogress": "0.2.0",
"paho-mqtt": "1.1.0",
"path": "0.12.7",
"postman-collection": "4.3.0",
"process": "0.11.10",
"qs": "6.11.2",
"quicktype-core": "23.0.79",
"rxjs": "7.8.1",
"set-cookie-parser": "2.6.0",
"set-cookie-parser-es": "1.0.5",
"socket.io-client-v2": "npm:socket.io-client@2.4.0",
"socket.io-client-v3": "npm:socket.io-client@3.1.3",
"socket.io-client-v4": "npm:socket.io-client@4.4.1",
"socketio-wildcard": "2.0.0",
"splitpanes": "3.1.5",
"stream-browserify": "3.0.0",
"subscriptions-transport-ws": "0.11.0",
"tern": "0.24.3",
"timers": "0.1.1",
"tippy.js": "6.3.7",
"url": "0.11.3",
"util": "0.12.5",
"uuid": "9.0.1",
"verzod": "0.2.2",
"vue": "3.3.9",
"vue-i18n": "9.8.0",
"vue-pdf-embed": "1.2.1",
"vue-router": "4.2.5",
"vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.6.0",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
"vuedraggable-es": "4.1.1",
"wonka": "6.3.4",
"workbox-window": "7.0.0",
"xml-formatter": "3.6.0",
"yargs-parser": "21.1.1",
"zod": "3.22.4"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
"@graphql-codegen/urql-introspection": "^3.0.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.141",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.6.0",
"@types/har-format": "^1.2.15",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/lossless-json": "^1.0.4",
"@types/nprogress": "^0.2.3",
"@types/paho-mqtt": "^1.0.10",
"@types/postman-collection": "^3.5.10",
"@types/splitpanes": "^2.2.6",
"@types/uuid": "^9.0.7",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/compiler-sfc": "^3.3.8",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/runtime-core": "^3.3.8",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.54.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.18.1",
"glob": "^10.3.10",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3",
"postcss": "^8.4.23",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"rollup-plugin-polyfill-node": "^0.13.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.2",
"typescript": "^5.3.2",
"unplugin-fonts": "^1.1.1",
"unplugin-icons": "^0.17.4",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-fonts": "^0.7.0",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.42",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.17.0",
"vite-plugin-vue-layouts": "^0.8.0",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@esbuild-plugins/node-modules-polyfill": "0.2.2",
"@graphql-codegen/add": "5.0.0",
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "3.0.0",
"@graphql-codegen/urql-introspection": "3.0.0",
"@graphql-typed-document-node/core": "3.2.0",
"@iconify-json/lucide": "1.1.144",
"@intlify/vite-plugin-vue-i18n": "7.0.0",
"@relmify/jest-fp-ts": "2.1.1",
"@rushstack/eslint-patch": "1.6.0",
"@types/har-format": "1.2.15",
"@types/js-yaml": "4.0.9",
"@types/lodash-es": "4.17.12",
"@types/lossless-json": "1.0.4",
"@types/nprogress": "0.2.3",
"@types/paho-mqtt": "1.0.10",
"@types/postman-collection": "3.5.10",
"@types/splitpanes": "2.2.6",
"@types/uuid": "9.0.7",
"@types/yargs-parser": "21.0.3",
"@typescript-eslint/eslint-plugin": "6.13.2",
"@typescript-eslint/parser": "6.13.2",
"@vitejs/plugin-vue": "4.5.1",
"@vue/compiler-sfc": "3.3.10",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/runtime-core": "3.3.10",
"autoprefixer": "10.4.16",
"cross-env": "7.0.3",
"dotenv": "16.3.1",
"eslint": "8.55.0",
"eslint-plugin-prettier": "5.0.1",
"eslint-plugin-vue": "9.19.2",
"glob": "10.3.10",
"npm-run-all": "4.1.5",
"openapi-types": "12.1.3",
"postcss": "8.4.31",
"prettier": "3.1.0",
"prettier-plugin-tailwindcss": "0.5.7",
"rollup-plugin-polyfill-node": "0.13.0",
"sass": "1.69.5",
"tailwindcss": "3.3.5",
"typescript": "5.3.2",
"unplugin-fonts": "1.1.1",
"unplugin-icons": "0.17.4",
"unplugin-vue-components": "0.25.2",
"vite": "4.5.0",
"vite-plugin-checker": "0.6.2",
"vite-plugin-fonts": "0.7.0",
"vite-plugin-html-config": "1.0.11",
"vite-plugin-inspect": "0.7.42",
"vite-plugin-pages": "0.31.0",
"vite-plugin-pages-sitemap": "1.6.1",
"vite-plugin-pwa": "0.17.3",
"vite-plugin-vue-layouts": "0.8.0",
"vitest": "0.34.6",
"vue-tsc": "1.8.24"
}
}
}

View File

@@ -1,12 +1,12 @@
<svg width="119" height="117" viewBox="0 0 119 117" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M52.0114 115.804C80.1842 115.804 103.023 92.8871 103.023 64.6183C103.023 36.3495 80.1842 13.4331 52.0114 13.4331C23.8386 13.4331 1 36.3495 1 64.6183C1 92.8871 23.8386 115.804 52.0114 115.804Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10"/>
<path d="M110.666 21.9438C111.335 21.3298 111.382 20.2875 110.77 19.6157C110.158 18.9439 109.12 18.897 108.45 19.5109C107.781 20.1249 107.734 21.1672 108.346 21.839C108.957 22.5108 109.996 22.5577 110.666 21.9438Z" fill="#525252"/>
<path opacity="0.3" d="M83.4192 41.2397C85.2331 41.2397 86.7036 39.7642 86.7036 37.9441C86.7036 36.1239 85.2331 34.6484 83.4192 34.6484C81.6053 34.6484 80.1348 36.1239 80.1348 37.9441C80.1348 39.7642 81.6053 41.2397 83.4192 41.2397Z" fill="#525252"/>
<path d="M61.2816 45.569C60.8718 47.7548 59.0958 49.804 56.7733 50.3505C54.9973 50.7604 53.2213 50.3505 51.9918 49.3942V65.2415H37.2374C37.7839 64.1486 37.9205 63.0557 37.7839 61.8261C37.5106 59.0939 35.1882 56.908 32.5925 56.6348C29.0405 56.2249 25.8984 58.9572 25.8984 62.5092C25.8984 63.4655 26.035 64.4218 26.4448 65.1049H9.77783V34.6398C9.77783 28.3555 14.8326 23.3008 21.2535 23.3008H51.8552V39.1481C52.9481 38.465 54.041 38.0552 55.2706 38.0552C58.9592 38.6016 61.9647 41.7438 61.2816 45.569Z" fill="#181818" stroke="#525252" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M42.019 85.187C41.4726 88.8756 44.3415 92.0177 48.0301 92.0177C49.5328 92.0177 50.899 91.4713 51.9919 90.6516V107.045H21.3902C15.1059 107.045 9.91455 101.854 9.91455 95.7063V65.1046H26.4449C26.0351 64.2849 25.8985 63.4653 25.8985 62.3723C26.0351 59.5034 28.2209 57.0444 30.9532 56.6345C34.6418 56.2247 37.784 58.957 37.784 62.5089C37.784 63.4652 37.6474 64.2849 37.2375 65.1046H51.7187V81.4984C50.4891 80.4055 48.8498 79.859 47.0738 80.1323C44.4781 80.4055 42.4289 82.5913 42.019 85.187Z" fill="#181818" stroke="#525252" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M94.0691 65.1047V95.4331C94.0691 101.717 88.8777 106.772 82.5934 106.772H51.9917V90.3784C50.8988 91.3347 49.5327 91.7446 48.0299 91.7446C44.4779 91.7446 41.7456 88.6024 42.0188 84.9138C42.4287 82.1815 44.6145 80.1323 47.0736 79.7224C48.8496 79.5858 50.6256 80.1323 51.7185 81.0886V64.6948H69.3418C68.7953 65.5145 68.6587 66.4708 68.6587 67.4271C68.6587 70.8425 71.6642 73.7114 75.3528 73.3016C78.0851 73.0283 80.4076 70.7059 80.6808 68.1102C80.8174 66.8807 80.5442 65.7877 80.1344 64.6948L94.0691 65.1047Z" fill="#181818" stroke="#525252" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M106.911 52.6728L93.3859 50.3503C93.6591 51.4432 93.7958 52.5362 93.3859 53.6291C92.5662 56.2247 90.1072 58.0007 87.3749 57.8641C83.8229 57.5909 81.2272 54.4488 81.7737 51.0334C81.9103 50.0771 82.3201 49.2574 82.8666 48.5743L65.3799 45.4322L68.1122 29.7215C69.2051 30.8144 70.8445 31.4975 72.7571 31.4975C75.2161 31.3609 77.2654 29.8581 77.9484 27.6723C79.3146 24.1203 76.9921 20.2951 73.3035 19.7486C72.074 19.4754 70.8445 19.7486 69.7516 20.1585L72.4839 4.44775L102.539 9.63912C108.823 10.732 112.922 16.6065 111.829 22.7541L106.911 52.6728Z" fill="#181818" stroke="#737373" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M113.473 64.1927L118.015 62.8237C118.344 62.7282 118.344 62.2506 118.015 62.1551L113.473 60.7861C113.356 60.7543 113.271 60.6694 113.24 60.5526L111.871 56.0212C111.775 55.6922 111.297 55.6922 111.202 56.0212L109.833 60.5526C109.801 60.6694 109.716 60.7543 109.6 60.7861L105.068 62.1551C104.739 62.2506 104.739 62.7282 105.068 62.8237L109.61 64.1927C109.727 64.2245 109.812 64.3094 109.844 64.4261L111.213 68.9682C111.308 69.2972 111.786 69.2972 111.881 68.9682L113.25 64.4261C113.271 64.3094 113.356 64.2245 113.473 64.1927Z" fill="#525252"/>
<path d="M103.424 24.4523L109.073 22.7496C109.482 22.6308 109.482 22.0369 109.073 21.9181L103.424 20.2153C103.279 20.1757 103.173 20.0701 103.133 19.925L101.431 14.2888C101.312 13.8796 100.718 13.8796 100.599 14.2888L98.8965 19.925C98.8569 20.0701 98.7513 20.1757 98.6061 20.2153L92.97 21.9181C92.5608 22.0369 92.5608 22.6308 92.97 22.7496L98.6193 24.4523C98.7645 24.4919 98.8701 24.5975 98.9097 24.7427L100.612 30.3921C100.731 30.8012 101.325 30.8012 101.444 30.3921L103.147 24.7427C103.173 24.5975 103.279 24.4919 103.424 24.4523Z" fill="#525252"/>
<path d="M62.357 5.26099L65.1958 4.40543C65.4015 4.34575 65.4015 4.0473 65.1958 3.98761L62.357 3.13205C62.2841 3.11216 62.231 3.0591 62.2111 2.98615L61.3555 0.154199C61.2958 -0.0513995 60.9973 -0.0513995 60.9376 0.154199L60.082 2.98615C60.0621 3.0591 60.009 3.11216 59.9361 3.13205L57.1039 3.98761C56.8983 4.0473 56.8983 4.34575 57.1039 4.40543L59.9427 5.26099C60.0157 5.28088 60.0687 5.33394 60.0886 5.4069L60.9443 8.24548C61.004 8.45107 61.3024 8.45107 61.3621 8.24548L62.2177 5.4069C62.231 5.33394 62.2841 5.28088 62.357 5.26099Z" fill="#525252"/>
<path d="M52.0114 115.804C80.1842 115.804 103.023 92.8871 103.023 64.6183C103.023 36.3495 80.1842 13.4331 52.0114 13.4331C23.8386 13.4331 1 36.3495 1 64.6183C1 92.8871 23.8386 115.804 52.0114 115.804Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M110.666 21.9438C111.335 21.3298 111.382 20.2875 110.77 19.6157C110.158 18.9439 109.12 18.897 108.45 19.5109C107.781 20.1249 107.734 21.1672 108.346 21.839C108.957 22.5108 109.996 22.5577 110.666 21.9438Z" fill="#575757"/>
<path opacity="0.3" d="M83.419 41.2397C85.2329 41.2397 86.7034 39.7642 86.7034 37.9441C86.7034 36.1239 85.2329 34.6484 83.419 34.6484C81.605 34.6484 80.1345 36.1239 80.1345 37.9441C80.1345 39.7642 81.605 41.2397 83.419 41.2397Z" fill="#575757"/>
<path d="M61.2815 45.569C60.8716 47.7548 59.0956 49.804 56.7732 50.3505C54.9972 50.7603 53.2212 50.3505 51.9917 49.3942V65.2415H37.2373C37.7837 64.1486 37.9204 63.0557 37.7837 61.8261C37.5105 59.0939 35.1881 56.908 32.5924 56.6348C29.0404 56.2249 25.8983 58.9572 25.8983 62.5092C25.8983 63.4655 26.0349 64.4218 26.4447 65.1049H9.77771V34.6398C9.77771 28.3555 14.8325 23.3008 21.2534 23.3008H51.8551V39.1481C52.948 38.465 54.0409 38.0552 55.2704 38.0552C58.959 38.6016 61.9646 41.7438 61.2815 45.569Z" fill="#0E0E0E" stroke="#575757" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M42.0188 85.187C41.4723 88.8756 44.3412 92.0177 48.0298 92.0177C49.5326 92.0177 50.8987 91.4713 51.9917 90.6516V107.045H21.3899C15.1057 107.045 9.91431 101.854 9.91431 95.7063V65.1046H26.4447C26.0348 64.2849 25.8982 63.4653 25.8982 62.3723C26.0348 59.5034 28.2207 57.0444 30.953 56.6345C34.6416 56.2247 37.7837 58.957 37.7837 62.5089C37.7837 63.4652 37.6471 64.2849 37.2373 65.1046H51.7184V81.4984C50.4889 80.4055 48.8495 79.859 47.0735 80.1323C44.4778 80.4055 42.4286 82.5913 42.0188 85.187Z" fill="#0E0E0E" stroke="#575757" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M94.069 65.1047V95.4331C94.069 101.717 88.8776 106.772 82.5933 106.772H51.9916V90.3784C50.8987 91.3347 49.5325 91.7446 48.0298 91.7446C44.4778 91.7446 41.7455 88.6024 42.0187 84.9138C42.4286 82.1815 44.6144 80.1323 47.0735 79.7224C48.8495 79.5858 50.6254 80.1323 51.7184 81.0886V64.6948H69.3417C68.7952 65.5145 68.6586 66.4708 68.6586 67.4271C68.6586 70.8425 71.6641 73.7114 75.3527 73.3016C78.085 73.0283 80.4075 70.7059 80.6807 68.1102C80.8173 66.8807 80.5441 65.7877 80.1342 64.6948L94.069 65.1047Z" fill="#0E0E0E" stroke="#575757" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M106.911 52.6728L93.3859 50.3503C93.6591 51.4432 93.7958 52.5362 93.3859 53.6291C92.5662 56.2248 90.1072 58.0007 87.3749 57.8641C83.8229 57.5909 81.2272 54.4488 81.7737 51.0334C81.9103 50.0771 82.3201 49.2574 82.8666 48.5743L65.3799 45.4322L68.1122 29.7215C69.2051 30.8144 70.8445 31.4975 72.7571 31.4975C75.2161 31.3609 77.2654 29.8581 77.9484 27.6723C79.3146 24.1203 76.9921 20.2951 73.3035 19.7486C72.074 19.4754 70.8445 19.7486 69.7516 20.1585L72.4839 4.44775L102.539 9.63912C108.823 10.732 112.922 16.6065 111.829 22.7541L106.911 52.6728Z" fill="#1E1E1E" stroke="#545454" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M113.473 64.1927L118.015 62.8237C118.344 62.7282 118.344 62.2506 118.015 62.1551L113.473 60.7861C113.356 60.7543 113.271 60.6694 113.239 60.5526L111.87 56.0212C111.775 55.6922 111.297 55.6922 111.202 56.0212L109.833 60.5526C109.801 60.6694 109.716 60.7543 109.599 60.7861L105.068 62.1551C104.739 62.2506 104.739 62.7282 105.068 62.8237L109.61 64.1927C109.727 64.2245 109.812 64.3094 109.843 64.4261L111.212 68.9682C111.308 69.2972 111.786 69.2972 111.881 68.9682L113.25 64.4261C113.271 64.3094 113.356 64.2245 113.473 64.1927Z" fill="#575757"/>
<path d="M103.424 24.4523L109.073 22.7496C109.483 22.6308 109.483 22.0369 109.073 21.9181L103.424 20.2153C103.279 20.1757 103.173 20.0701 103.134 19.925L101.431 14.2888C101.312 13.8796 100.718 13.8796 100.599 14.2888L98.8967 19.925C98.8571 20.0701 98.7515 20.1757 98.6064 20.2153L92.9702 21.9181C92.561 22.0369 92.561 22.6308 92.9702 22.7496L98.6196 24.4523C98.7647 24.4919 98.8703 24.5975 98.9099 24.7427L100.613 30.3921C100.731 30.8012 101.325 30.8012 101.444 30.3921L103.147 24.7427C103.173 24.5975 103.279 24.4919 103.424 24.4523Z" fill="#575757"/>
<path d="M62.357 5.26099L65.1958 4.40544C65.4015 4.34575 65.4015 4.0473 65.1958 3.98761L62.357 3.13205C62.2841 3.11216 62.231 3.0591 62.2111 2.98615L61.3555 0.154199C61.2958 -0.0513995 60.9973 -0.0513995 60.9376 0.154199L60.082 2.98615C60.0621 3.0591 60.009 3.11216 59.9361 3.13205L57.1039 3.98761C56.8983 4.0473 56.8983 4.34575 57.1039 4.40544L59.9427 5.26099C60.0157 5.28088 60.0687 5.33394 60.0886 5.4069L60.9443 8.24547C61.004 8.45107 61.3024 8.45107 61.3621 8.24547L62.2177 5.4069C62.231 5.33394 62.2841 5.28088 62.357 5.26099Z" fill="#575757"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -1,19 +1,19 @@
<svg width="150" height="126" viewBox="0 0 150 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M84.2755 124.383C117.256 124.383 143.993 97.6454 143.993 64.5473C143.993 31.4493 117.138 4.71167 84.2755 4.71167C51.2952 4.71167 24.5576 31.4493 24.5576 64.5473C24.5576 97.6454 51.2952 124.383 84.2755 124.383Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M139.635 25.4417C142.302 25.4417 144.464 23.2796 144.464 20.6125C144.464 17.9453 142.302 15.7832 139.635 15.7832C136.968 15.7832 134.806 17.9453 134.806 20.6125C134.806 23.2796 136.968 25.4417 139.635 25.4417Z" fill="#181818"/>
<path d="M146.702 6.59605C148.523 6.59605 150 5.11948 150 3.29803C150 1.47658 148.523 0 146.702 0C144.88 0 143.404 1.47658 143.404 3.29803C143.404 5.11948 144.88 6.59605 146.702 6.59605Z" fill="#181818"/>
<path d="M27.2663 25.3241C29.0877 25.3241 30.5643 23.8475 30.5643 22.0261C30.5643 20.2046 29.0877 18.728 27.2663 18.728C25.4448 18.728 23.9683 20.2046 23.9683 22.0261C23.9683 23.8475 25.4448 25.3241 27.2663 25.3241Z" fill="#181818"/>
<path d="M7.12491 88.9293C10.5076 88.9293 13.2498 86.187 13.2498 82.8044C13.2498 79.4217 10.5076 76.6794 7.12491 76.6794C3.74221 76.6794 1 79.4217 1 82.8044C1 86.187 3.74221 88.9293 7.12491 88.9293Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M128.187 105.12C117.275 116.97 101.64 124.383 84.275 124.383C70.4162 124.383 57.6597 119.662 47.5264 111.731V14.0746C47.5264 10.8443 50.1275 8.20825 53.3724 8.20825H111.37L128.187 25.0838V105.12Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M71.5926 104.186H63.1468C62.8317 104.186 62.5796 103.369 62.5796 102.39C62.5796 101.41 62.8317 100.593 63.1468 100.593H71.5926C71.9078 100.593 72.1599 101.41 72.1599 102.39C72.1599 103.533 71.9078 104.186 71.5926 104.186Z" fill="#525252"/>
<path d="M90.1779 28.7408H63.7222C63.1069 28.7408 62.5796 27.9117 62.5796 26.9445C62.5796 25.9773 63.1069 25.1482 63.7222 25.1482H90.1779C90.7932 25.1482 91.3205 25.9773 91.3205 26.9445C91.3205 27.9117 90.7932 28.7408 90.1779 28.7408Z" fill="#525252"/>
<path d="M70.2489 37.1239H63.3525C62.9363 37.1239 62.5796 36.2948 62.5796 35.3276C62.5796 34.3603 62.9363 33.5312 63.3525 33.5312H70.1895C70.6056 33.5312 70.9624 34.3603 70.9624 35.3276C70.9624 36.2948 70.6056 37.1239 70.2489 37.1239Z" fill="#525252"/>
<path d="M111.994 83.8276H79.203H75.5888H64.6583C64.2176 83.8276 63.7769 84.5761 63.7769 85.6239C63.7769 86.5221 64.1295 87.4203 64.6583 87.4203H75.5888H79.203H111.994C112.435 87.4203 112.876 86.6718 112.876 85.6239C112.788 84.5761 112.435 83.8276 111.994 83.8276Z" fill="#525252"/>
<path d="M111.993 74.2476H99.8064H95.6559H64.6599C64.2184 74.2476 63.7769 74.996 63.7769 76.0439C63.7769 76.942 64.1301 77.8402 64.6599 77.8402H95.6559H99.8064H111.993C112.434 77.8402 112.876 77.0917 112.876 76.0439C112.788 74.996 112.434 74.2476 111.993 74.2476Z" fill="#525252"/>
<path d="M113.251 65.8647H110.007H106.762H64.678C64.2274 65.8647 63.7769 66.6132 63.7769 67.6611C63.7769 68.5592 64.1373 69.4574 64.678 69.4574H106.762H110.367H113.161C113.611 69.4574 114.062 68.7089 114.062 67.6611C114.152 66.7629 113.701 65.8647 113.251 65.8647Z" fill="#525252"/>
<path d="M113.18 56.2844H102.728H100.137H64.6702C64.2235 56.2844 63.7769 57.0329 63.7769 58.0807C63.7769 58.9789 64.1342 59.877 64.6702 59.877H100.137H102.728H113.091C113.716 59.877 114.073 59.1286 114.073 58.0807C114.073 57.1826 113.716 56.2844 113.18 56.2844Z" fill="#525252"/>
<path d="M111.37 8.91162V19.7872C111.37 23.1009 114.056 25.7872 117.37 25.7872H128.187" fill="#181818"/>
<path d="M111.37 8.91162V19.7872C111.37 23.1009 114.056 25.7872 117.37 25.7872H128.187" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M70.8988 42.4288V79.5311C70.8988 83.8729 67.3447 87.0305 63.3956 87.0305H61.421V96.8981C61.421 99.2664 58.6567 100.056 57.077 98.477L47.2043 87.0305H8.50327C4.15928 87.0305 1 83.4782 1 79.5311V42.4288C1 38.0871 4.55418 34.9294 8.50327 34.9294H63.3956C67.3447 34.9294 70.8988 38.4818 70.8988 42.4288Z" fill="#181818" stroke="#737373" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M19.5605 61.7695V53.8754C19.5605 52.6913 20.3504 51.9019 21.5351 51.9019H30.2231C31.4078 51.9019 32.1976 52.6913 32.1976 53.8754V62.9536C32.1976 65.3218 32.1976 67.2953 31.8027 68.8742C31.4157 70.4216 30.6493 71.2107 29.5035 72.3563C29.457 72.4028 29.4061 72.4451 29.3504 72.4803C28.3636 73.1039 26.8665 73.4756 25.0679 73.8037C24.46 73.9146 23.9046 73.4429 23.9046 72.825V71.3204C23.9046 70.8227 24.274 70.4097 24.7555 70.2839C26.9907 69.7001 28.2485 68.1396 28.2485 65.3218C28.2485 65.3218 28.2485 65.3218 28.2485 65.3218V65.3218C28.2485 65.3218 27.8951 64.5324 27.4591 64.5324H22.2327C22.0353 64.5324 21.8422 64.4743 21.6783 64.3643C20.5832 63.629 19.5605 62.8696 19.5605 61.7695ZM38.9111 61.7695V53.8754C38.9111 52.6913 39.7009 51.9019 40.8856 51.9019H49.5736C50.7583 51.9019 51.5481 52.6913 51.5481 53.8754V62.9536C51.5481 65.3218 51.5481 67.2953 51.1532 68.8742C50.7662 70.4216 49.9998 71.2107 48.8541 72.3563C48.8075 72.4028 48.7566 72.4451 48.7009 72.4803C47.7141 73.1039 46.217 73.4756 44.4184 73.8037C43.8105 73.9146 43.255 73.4429 43.255 72.825V71.3204C43.255 70.8227 43.6244 70.4097 44.106 70.2839C46.3411 69.7001 47.5991 68.1396 47.5991 65.3218C47.5991 65.3218 47.5991 65.3218 47.5991 65.3218V65.3218C47.5991 65.3218 47.2456 64.5324 46.8097 64.5324H41.5832C41.3858 64.5324 41.1927 64.4743 41.0288 64.3643C39.9337 63.629 38.9111 62.8696 38.9111 61.7695Z" fill="#525252"/>
<path d="M84.2752 124.383C117.256 124.383 143.993 97.6454 143.993 64.5473C143.993 31.4493 117.138 4.71167 84.2752 4.71167C51.295 4.71167 24.5574 31.4493 24.5574 64.5473C24.5574 97.6454 51.295 124.383 84.2752 124.383Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M139.635 25.4417C142.302 25.4417 144.464 23.2796 144.464 20.6125C144.464 17.9453 142.302 15.7832 139.635 15.7832C136.968 15.7832 134.806 17.9453 134.806 20.6125C134.806 23.2796 136.968 25.4417 139.635 25.4417Z" fill="#0E0E0E"/>
<path d="M146.702 6.59605C148.523 6.59605 150 5.11948 150 3.29803C150 1.47658 148.523 0 146.702 0C144.881 0 143.404 1.47658 143.404 3.29803C143.404 5.11948 144.881 6.59605 146.702 6.59605Z" fill="#0E0E0E"/>
<path d="M27.2665 25.3241C29.088 25.3241 30.5646 23.8475 30.5646 22.0261C30.5646 20.2046 29.088 18.728 27.2665 18.728C25.4451 18.728 23.9685 20.2046 23.9685 22.0261C23.9685 23.8475 25.4451 25.3241 27.2665 25.3241Z" fill="#0E0E0E"/>
<path d="M7.12491 88.9293C10.5076 88.9293 13.2498 86.187 13.2498 82.8044C13.2498 79.4217 10.5076 76.6794 7.12491 76.6794C3.74221 76.6794 1 79.4217 1 82.8044C1 86.187 3.74221 88.9293 7.12491 88.9293Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M128.187 105.12C117.275 116.97 101.64 124.383 84.275 124.383C70.4162 124.383 57.6597 119.662 47.5264 111.731V14.0746C47.5264 10.8443 50.1275 8.20825 53.3724 8.20825H111.37L128.187 25.0838V105.12Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M71.5926 104.186H63.1468C62.8317 104.186 62.5796 103.369 62.5796 102.39C62.5796 101.41 62.8317 100.593 63.1468 100.593H71.5926C71.9078 100.593 72.1599 101.41 72.1599 102.39C72.1599 103.533 71.9078 104.186 71.5926 104.186Z" fill="#575757"/>
<path d="M90.1779 28.7408H63.7222C63.1069 28.7408 62.5796 27.9117 62.5796 26.9445C62.5796 25.9773 63.1069 25.1482 63.7222 25.1482H90.1779C90.7932 25.1482 91.3205 25.9773 91.3205 26.9445C91.3205 27.9117 90.7932 28.7408 90.1779 28.7408Z" fill="#575757"/>
<path d="M70.2489 37.1239H63.3525C62.9363 37.1239 62.5796 36.2948 62.5796 35.3276C62.5796 34.3603 62.9363 33.5312 63.3525 33.5312H70.1895C70.6056 33.5312 70.9624 34.3603 70.9624 35.3276C70.9624 36.2948 70.6056 37.1239 70.2489 37.1239Z" fill="#575757"/>
<path d="M111.995 83.8276H79.2032H75.5891H64.6586C64.2178 83.8276 63.7771 84.5761 63.7771 85.6239C63.7771 86.5221 64.1297 87.4203 64.6586 87.4203H75.5891H79.2032H111.995C112.435 87.4203 112.876 86.6718 112.876 85.6239C112.788 84.5761 112.435 83.8276 111.995 83.8276Z" fill="#575757"/>
<path d="M111.993 74.2476H99.8067H95.6562H64.6602C64.2186 74.2476 63.7771 74.996 63.7771 76.0439C63.7771 76.942 64.1303 77.8402 64.6602 77.8402H95.6562H99.8067H111.993C112.435 77.8402 112.876 77.0917 112.876 76.0439C112.788 74.996 112.435 74.2476 111.993 74.2476Z" fill="#575757"/>
<path d="M113.251 65.8647H110.007H106.763H64.6783C64.2277 65.8647 63.7771 66.6132 63.7771 67.6611C63.7771 68.5592 64.1376 69.4574 64.6783 69.4574H106.763H110.367H113.161C113.612 69.4574 114.062 68.7089 114.062 67.6611C114.152 66.7629 113.702 65.8647 113.251 65.8647Z" fill="#575757"/>
<path d="M113.18 56.2844H102.728H100.137H64.6705C64.2238 56.2844 63.7771 57.0329 63.7771 58.0807C63.7771 58.9789 64.1344 59.877 64.6705 59.877H100.137H102.728H113.091C113.716 59.877 114.074 59.1286 114.074 58.0807C114.074 57.1826 113.716 56.2844 113.18 56.2844Z" fill="#575757"/>
<path d="M111.37 8.91162V19.7872C111.37 23.1009 114.056 25.7872 117.37 25.7872H128.187" fill="#0E0E0E"/>
<path d="M111.37 8.91162V19.7872C111.37 23.1009 114.056 25.7872 117.37 25.7872H128.187" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M70.8988 42.4288V79.5311C70.8988 83.8729 67.3447 87.0305 63.3956 87.0305H61.421V96.8981C61.421 99.2664 58.6567 100.056 57.077 98.477L47.2043 87.0305H8.50327C4.15928 87.0305 1 83.4782 1 79.5311V42.4288C1 38.0871 4.55418 34.9294 8.50327 34.9294H63.3956C67.3447 34.9294 70.8988 38.4818 70.8988 42.4288Z" fill="#1E1E1E" stroke="#545454" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M19.5605 61.7695V53.8754C19.5605 52.6913 20.3504 51.9019 21.5351 51.9019H30.2231C31.4078 51.9019 32.1976 52.6913 32.1976 53.8754V62.9536C32.1976 65.3218 32.1976 67.2954 31.8027 68.8742C31.4157 70.4216 30.6493 71.2107 29.5035 72.3563C29.457 72.4028 29.4061 72.4451 29.3504 72.4803C28.3636 73.1039 26.8665 73.4756 25.0679 73.8037C24.46 73.9146 23.9046 73.4429 23.9046 72.825V71.3204C23.9046 70.8227 24.274 70.4097 24.7555 70.2839C26.9907 69.7001 28.2485 68.1396 28.2485 65.3218C28.2485 65.3218 27.8951 64.5324 27.4591 64.5324H22.2327C22.0353 64.5324 21.8422 64.4743 21.6783 64.3643C20.5832 63.629 19.5605 62.8696 19.5605 61.7695ZM38.9111 61.7695V53.8754C38.9111 52.6913 39.7009 51.9019 40.8856 51.9019H49.5736C50.7583 51.9019 51.5481 52.6913 51.5481 53.8754V62.9536C51.5481 65.3218 51.5481 67.2954 51.1532 68.8742C50.7662 70.4216 49.9998 71.2107 48.8541 72.3563C48.8075 72.4028 48.7566 72.4451 48.7009 72.4803C47.7141 73.1039 46.217 73.4756 44.4184 73.8037C43.8105 73.9146 43.255 73.4429 43.255 72.825V71.3204C43.255 70.8227 43.6244 70.4097 44.106 70.2839C46.3411 69.7001 47.5991 68.1396 47.5991 65.3218C47.5991 65.3218 47.2456 64.5324 46.8097 64.5324H41.5832C41.3858 64.5324 41.1927 64.4743 41.0288 64.3643C39.9337 63.629 38.9111 62.8696 38.9111 61.7695Z" fill="#575757"/>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,26 +1,28 @@
<svg width="165" height="142" viewBox="0 0 165 142" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M85.3227 123.613C119.114 123.613 146.509 96.2184 146.509 62.3067C146.509 28.3949 118.993 1 85.3227 1C51.5316 1 24.1367 28.3949 24.1367 62.3067C24.1367 96.2184 51.5316 123.613 85.3227 123.613Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M154.276 42.8823C157.009 42.8823 159.224 40.667 159.224 37.9343C159.224 35.2016 157.009 32.9863 154.276 32.9863C151.543 32.9863 149.328 35.2016 149.328 37.9343C149.328 40.667 151.543 42.8823 154.276 42.8823Z" fill="#181818"/>
<path d="M161.516 23.5734C163.383 23.5734 164.895 22.0605 164.895 20.1943C164.895 18.3281 163.383 16.8152 161.516 16.8152C159.65 16.8152 158.137 18.3281 158.137 20.1943C158.137 22.0605 159.65 23.5734 161.516 23.5734Z" fill="#181818"/>
<path d="M26.9123 22.1193C28.7785 22.1193 30.2914 20.6064 30.2914 18.7402C30.2914 16.874 28.7785 15.3611 26.9123 15.3611C25.0461 15.3611 23.5332 16.874 23.5332 18.7402C23.5332 20.6064 25.0461 22.1193 26.9123 22.1193Z" fill="#181818"/>
<path d="M6.27549 87.288C9.74134 87.288 12.551 84.4784 12.551 81.0126C12.551 77.5467 9.74134 74.7371 6.27549 74.7371C2.80963 74.7371 0 77.5467 0 81.0126C0 84.4784 2.80963 87.288 6.27549 87.288Z" fill="#181818"/>
<path d="M121.099 107.854H138.619C140.82 107.854 142.706 105.968 142.706 103.769V22.0724C142.706 19.8729 140.82 17.9875 138.619 17.9875H121.643" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M51.5361 107.854H34.5596C32.3589 107.854 30.4727 105.968 30.4727 103.769V22.0724C30.4727 19.8729 32.3589 17.9875 34.5596 17.9875H51.1864" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 34.3269C41.2832 33.7746 41.7309 33.3269 42.2832 33.3269H50.7714C51.3237 33.3269 51.7714 33.7746 51.7714 34.3269C51.7714 34.8792 51.3237 35.3269 50.7714 35.3269H42.2832C41.7309 35.3269 41.2832 34.8792 41.2832 34.3269Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 45.3245C41.2832 44.7722 41.7309 44.3245 42.2832 44.3245H50.7714C51.3237 44.3245 51.7714 44.7722 51.7714 45.3245C51.7714 45.8767 51.3237 46.3245 50.7714 46.3245H42.2832C41.7309 46.3245 41.2832 45.8767 41.2832 45.3245Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 56.6365C41.2832 56.0842 41.7309 55.6365 42.2832 55.6365H50.7714C51.3237 55.6365 51.7714 56.0842 51.7714 56.6365C51.7714 57.1888 51.3237 57.6365 50.7714 57.6365H42.2832C41.7309 57.6365 41.2832 57.1888 41.2832 56.6365Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 67.9485C41.2832 67.3962 41.7309 66.9485 42.2832 66.9485H50.7714C51.3237 66.9485 51.7714 67.3962 51.7714 67.9485C51.7714 68.5008 51.3237 68.9485 50.7714 68.9485H42.2832C41.7309 68.9485 41.2832 68.5008 41.2832 67.9485Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 89.9434C41.2832 89.3911 41.7309 88.9434 42.2832 88.9434H50.7714C51.3237 88.9434 51.7714 89.3911 51.7714 89.9434C51.7714 90.4956 51.3237 90.9434 50.7714 90.9434H42.2832C41.7309 90.9434 41.2832 90.4956 41.2832 89.9434Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 34.3269C120.643 33.7746 121.09 33.3269 121.643 33.3269H129.816C130.369 33.3269 130.816 33.7746 130.816 34.3269C130.816 34.8792 130.369 35.3269 129.816 35.3269H121.643C121.09 35.3269 120.643 34.8792 120.643 34.3269Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 45.3245C120.643 44.7722 121.09 44.3245 121.643 44.3245H129.816C130.369 44.3245 130.816 44.7722 130.816 45.3245C130.816 45.8767 130.369 46.3245 129.816 46.3245H121.643C121.09 46.3245 120.643 45.8767 120.643 45.3245Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 56.6365C120.643 56.0842 121.09 55.6365 121.643 55.6365H129.816C130.369 55.6365 130.816 56.0842 130.816 56.6365C130.816 57.1888 130.369 57.6365 129.816 57.6365H121.643C121.09 57.6365 120.643 57.1888 120.643 56.6365Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 67.9485C120.643 67.3962 121.09 66.9485 121.643 66.9485H129.816C130.369 66.9485 130.816 67.3962 130.816 67.9485C130.816 68.5008 130.369 68.9485 129.816 68.9485H121.643C121.09 68.9485 120.643 68.5008 120.643 67.9485Z" fill="#525252"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 89.9434C120.643 89.3911 121.09 88.9434 121.643 88.9434H129.816C130.369 88.9434 130.816 89.3911 130.816 89.9434C130.816 90.4956 130.369 90.9434 129.816 90.9434H121.643C121.09 90.9434 120.643 90.4956 120.643 89.9434Z" fill="#525252"/>
<path d="M117.556 114.767H55.3086C53.108 114.767 51.2217 112.881 51.2217 110.682V15.7882C51.2217 13.5887 53.108 11.7034 55.3086 11.7034H117.556C119.756 11.7034 121.643 13.5887 121.643 15.7882V110.682C121.643 112.881 119.756 114.767 117.556 114.767Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M110.01 42.4467H66.6261C65.683 42.4467 65.0542 41.8773 65.0542 41.0231V37.3216C65.0542 36.4674 65.683 35.8979 66.6261 35.8979H110.01C110.954 35.8979 111.582 36.4674 111.582 37.3216V41.0231C111.582 41.5925 110.954 42.4467 110.01 42.4467Z" fill="#525252"/>
<path d="M110.01 59.4145H66.6261C65.683 59.4145 65.0542 58.845 65.0542 57.9909V54.2894C65.0542 53.4352 65.683 52.8657 66.6261 52.8657H110.01C110.954 52.8657 111.582 53.4352 111.582 54.2894V57.9909C111.582 58.845 110.954 59.4145 110.01 59.4145Z" fill="#525252"/>
<path d="M110.01 76.6962H66.6261C65.683 76.6962 65.0542 76.1268 65.0542 75.2726V71.5711C65.0542 70.7169 65.683 70.1475 66.6261 70.1475H110.01C110.954 70.1475 111.582 70.7169 111.582 71.5711V75.2726C111.582 76.1268 110.954 76.6962 110.01 76.6962Z" fill="#525252"/>
<path d="M110.011 94.2924H92.7198C91.7767 94.2924 91.1479 93.723 91.1479 92.8688V89.1673C91.1479 88.3131 91.7767 87.7437 92.7198 87.7437H110.011C110.954 87.7437 111.583 88.3131 111.583 89.1673V92.8688C111.583 93.723 110.954 94.2924 110.011 94.2924Z" fill="#525252"/>
<path d="M105.548 125.569C105.155 127.237 104.566 129.003 103.879 130.475C102.015 134.106 99.0712 136.952 95.4405 138.816C91.7116 140.681 87.2959 141.466 82.8801 140.484C72.4786 138.326 65.8059 128.12 67.9647 117.719C70.1235 107.317 80.2307 100.546 90.6322 102.803C94.3611 103.588 97.5993 105.453 100.347 108.004C104.959 112.616 106.921 119.289 105.548 125.569Z" fill="#181818" stroke="#737373" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M92.8892 119.976H88.4735V115.56C88.4735 114.677 87.7866 113.892 86.8053 113.892C85.9222 113.892 85.1371 114.579 85.1371 115.56V119.976H80.7214C79.8382 119.976 79.0532 120.662 79.0532 121.644C79.0532 122.625 79.7401 123.312 80.7214 123.312H85.1371V127.728C85.1371 128.611 85.824 129.396 86.8053 129.396C87.6885 129.396 88.4735 128.709 88.4735 127.728V123.312H92.8892C93.7724 123.312 94.5574 122.625 94.5574 121.644C94.5574 120.662 93.7724 119.976 92.8892 119.976Z" fill="#525252"/>
<path d="M85.3227 123.613C119.114 123.613 146.509 96.2184 146.509 62.3067C146.509 28.3949 118.993 1 85.3227 1C51.5316 1 24.1367 28.3949 24.1367 62.3067C24.1367 96.2184 51.5316 123.613 85.3227 123.613Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M154.276 42.8823C157.009 42.8823 159.224 40.667 159.224 37.9343C159.224 35.2016 157.009 32.9863 154.276 32.9863C151.543 32.9863 149.328 35.2016 149.328 37.9343C149.328 40.667 151.543 42.8823 154.276 42.8823Z" fill="#0E0E0E"/>
<path d="M161.517 23.5734C163.383 23.5734 164.896 22.0605 164.896 20.1943C164.896 18.3281 163.383 16.8152 161.517 16.8152C159.65 16.8152 158.137 18.3281 158.137 20.1943C158.137 22.0605 159.65 23.5734 161.517 23.5734Z" fill="#0E0E0E"/>
<path d="M26.9123 22.1193C28.7785 22.1193 30.2914 20.6064 30.2914 18.7402C30.2914 16.874 28.7785 15.3611 26.9123 15.3611C25.0461 15.3611 23.5332 16.874 23.5332 18.7402C23.5332 20.6064 25.0461 22.1193 26.9123 22.1193Z" fill="#0E0E0E"/>
<path d="M6.27549 87.288C9.74134 87.288 12.551 84.4784 12.551 81.0126C12.551 77.5467 9.74134 74.7371 6.27549 74.7371C2.80963 74.7371 0 77.5467 0 81.0126C0 84.4784 2.80963 87.288 6.27549 87.288Z" fill="#0E0E0E"/>
<path d="M121.099 107.854H138.619C140.82 107.854 142.706 105.968 142.706 103.769V22.0724C142.706 19.8729 140.82 17.9875 138.619 17.9875H121.643" fill="#0E0E0E"/>
<path d="M121.099 107.854H138.619C140.82 107.854 142.706 105.968 142.706 103.769V22.0724C142.706 19.8729 140.82 17.9875 138.619 17.9875H121.643L121.099 107.854Z" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M51.5361 107.854H34.5596C32.3589 107.854 30.4727 105.968 30.4727 103.769V22.0724C30.4727 19.8729 32.3589 17.9875 34.5596 17.9875H51.1864" fill="#0E0E0E"/>
<path d="M51.5361 107.854H34.5596C32.3589 107.854 30.4727 105.968 30.4727 103.769V22.0724C30.4727 19.8729 32.3589 17.9875 34.5596 17.9875H51.1864L51.5361 107.854Z" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 34.3269C41.2832 33.7746 41.7309 33.3269 42.2832 33.3269H50.7714C51.3237 33.3269 51.7714 33.7746 51.7714 34.3269C51.7714 34.8792 51.3237 35.3269 50.7714 35.3269H42.2832C41.7309 35.3269 41.2832 34.8792 41.2832 34.3269Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 45.3245C41.2832 44.7722 41.7309 44.3245 42.2832 44.3245H50.7714C51.3237 44.3245 51.7714 44.7722 51.7714 45.3245C51.7714 45.8767 51.3237 46.3245 50.7714 46.3245H42.2832C41.7309 46.3245 41.2832 45.8767 41.2832 45.3245Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 56.6365C41.2832 56.0842 41.7309 55.6365 42.2832 55.6365H50.7714C51.3237 55.6365 51.7714 56.0842 51.7714 56.6365C51.7714 57.1888 51.3237 57.6365 50.7714 57.6365H42.2832C41.7309 57.6365 41.2832 57.1888 41.2832 56.6365Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 67.9485C41.2832 67.3962 41.7309 66.9485 42.2832 66.9485H50.7714C51.3237 66.9485 51.7714 67.3962 51.7714 67.9485C51.7714 68.5008 51.3237 68.9485 50.7714 68.9485H42.2832C41.7309 68.9485 41.2832 68.5008 41.2832 67.9485Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M41.2832 89.9434C41.2832 89.3911 41.7309 88.9434 42.2832 88.9434H50.7714C51.3237 88.9434 51.7714 89.3911 51.7714 89.9434C51.7714 90.4956 51.3237 90.9434 50.7714 90.9434H42.2832C41.7309 90.9434 41.2832 90.4956 41.2832 89.9434Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 34.3269C120.643 33.7746 121.09 33.3269 121.643 33.3269H129.816C130.369 33.3269 130.816 33.7746 130.816 34.3269C130.816 34.8792 130.369 35.3269 129.816 35.3269H121.643C121.09 35.3269 120.643 34.8792 120.643 34.3269Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 45.3245C120.643 44.7722 121.09 44.3245 121.643 44.3245H129.816C130.369 44.3245 130.816 44.7722 130.816 45.3245C130.816 45.8767 130.369 46.3245 129.816 46.3245H121.643C121.09 46.3245 120.643 45.8767 120.643 45.3245Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 56.6365C120.643 56.0842 121.09 55.6365 121.643 55.6365H129.816C130.369 55.6365 130.816 56.0842 130.816 56.6365C130.816 57.1888 130.369 57.6365 129.816 57.6365H121.643C121.09 57.6365 120.643 57.1888 120.643 56.6365Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 67.9485C120.643 67.3962 121.09 66.9485 121.643 66.9485H129.816C130.369 66.9485 130.816 67.3962 130.816 67.9485C130.816 68.5008 130.369 68.9485 129.816 68.9485H121.643C121.09 68.9485 120.643 68.5008 120.643 67.9485Z" fill="#575757"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.643 89.9434C120.643 89.3911 121.09 88.9434 121.643 88.9434H129.816C130.369 88.9434 130.816 89.3911 130.816 89.9434C130.816 90.4956 130.369 90.9434 129.816 90.9434H121.643C121.09 90.9434 120.643 90.4956 120.643 89.9434Z" fill="#575757"/>
<path d="M117.556 114.767H55.3086C53.108 114.767 51.2217 112.881 51.2217 110.682V15.7882C51.2217 13.5887 53.108 11.7034 55.3086 11.7034H117.556C119.756 11.7034 121.643 13.5887 121.643 15.7882V110.682C121.643 112.881 119.756 114.767 117.556 114.767Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M110.01 42.4467H66.6261C65.683 42.4467 65.0542 41.8773 65.0542 41.0231V37.3216C65.0542 36.4674 65.683 35.8979 66.6261 35.8979H110.01C110.954 35.8979 111.582 36.4674 111.582 37.3216V41.0231C111.582 41.5925 110.954 42.4467 110.01 42.4467Z" fill="#575757"/>
<path d="M110.01 59.4145H66.6261C65.683 59.4145 65.0542 58.845 65.0542 57.9909V54.2894C65.0542 53.4352 65.683 52.8657 66.6261 52.8657H110.01C110.954 52.8657 111.582 53.4352 111.582 54.2894V57.9909C111.582 58.845 110.954 59.4145 110.01 59.4145Z" fill="#575757"/>
<path d="M110.01 76.6962H66.6261C65.683 76.6962 65.0542 76.1268 65.0542 75.2726V71.5711C65.0542 70.7169 65.683 70.1475 66.6261 70.1475H110.01C110.954 70.1475 111.582 70.7169 111.582 71.5711V75.2726C111.582 76.1268 110.954 76.6962 110.01 76.6962Z" fill="#575757"/>
<path d="M110.011 94.2924H92.7201C91.777 94.2924 91.1482 93.723 91.1482 92.8688V89.1673C91.1482 88.3131 91.777 87.7437 92.7201 87.7437H110.011C110.954 87.7437 111.583 88.3131 111.583 89.1673V92.8688C111.583 93.723 110.954 94.2924 110.011 94.2924Z" fill="#575757"/>
<path d="M105.547 125.569C105.155 127.237 104.566 129.003 103.879 130.475C102.015 134.106 99.071 136.952 95.4402 138.816C91.7114 140.681 87.2956 141.466 82.8799 140.484C72.4783 138.326 65.8057 128.12 67.9645 117.719C70.1233 107.317 80.2304 100.546 90.632 102.803C94.3608 103.588 97.5991 105.453 100.347 108.004C104.959 112.616 106.921 119.289 105.547 125.569Z" fill="#1E1E1E" stroke="#545454" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M92.889 119.976H88.4732V115.56C88.4732 114.677 87.7864 113.892 86.8051 113.892C85.9219 113.892 85.1369 114.579 85.1369 115.56V119.976H80.7212C79.838 119.976 79.053 120.662 79.053 121.644C79.053 122.625 79.7399 123.312 80.7212 123.312H85.1369V127.728C85.1369 128.611 85.8238 129.396 86.8051 129.396C87.6882 129.396 88.4732 128.709 88.4732 127.728V123.312H92.889C93.7721 123.312 94.5572 122.625 94.5572 121.644C94.5572 120.662 93.7721 119.976 92.889 119.976Z" fill="#575757"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -1,37 +1,37 @@
<svg width="117" height="120" viewBox="0 0 117 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M104.634 68.6831C104.634 80.1972 100.53 90.6702 93.6709 98.8771C85.0353 109.228 72.0513 115.781 57.4749 115.781C43.511 115.781 31.0169 109.717 22.3813 100.102C14.9706 91.7726 10.4385 80.7484 10.4385 68.6831C10.4385 42.6538 31.5069 21.5854 57.5361 21.5854C83.5654 21.5854 104.634 42.6538 104.634 68.6831Z" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 4"/>
<path d="M74.8781 71.7725C74.5226 73.2834 73.9893 74.8833 73.3671 76.2164C71.6785 79.5049 69.0121 82.0824 65.7236 83.7711C62.3462 85.4598 58.3467 86.1708 54.3471 85.282C44.926 83.3267 38.8823 74.0833 40.8376 64.6622C42.7929 55.2411 51.9474 49.1085 61.3685 51.1527C64.7459 51.8637 67.6789 53.5524 70.1675 55.8632C74.3448 60.0405 76.1224 66.0843 74.8781 71.7725Z" fill="#525252" stroke="#737373" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.4127 66.7063H60.4132C59.8609 66.7063 59.4132 66.2586 59.4132 65.7063V62.7067C59.4132 61.9068 58.791 61.1958 57.9023 61.1958C57.1024 61.1958 56.3913 61.818 56.3913 62.7067V65.7063C56.3913 66.2586 55.9436 66.7063 55.3913 66.7063H52.3918C51.5919 66.7063 50.8809 67.3284 50.8809 68.2172C50.8809 69.106 51.503 69.7281 52.3918 69.7281H55.3913C55.9436 69.7281 56.3913 70.1759 56.3913 70.7281V73.7277C56.3913 74.5276 57.0135 75.2386 57.9023 75.2386C58.7022 75.2386 59.4132 74.6165 59.4132 73.7277V70.7281C59.4132 70.1759 59.8609 69.7281 60.4132 69.7281H63.4127C64.2126 69.7281 64.9237 69.106 64.9237 68.2172C64.9237 67.3284 64.2126 66.7063 63.4127 66.7063Z" fill="#525252"/>
<path d="M104.634 68.6831C104.634 80.1972 100.53 90.6702 93.6709 98.8771C85.0353 109.228 72.0513 115.781 57.4749 115.781C43.511 115.781 31.0169 109.717 22.3813 100.102C14.9706 91.7726 10.4385 80.7484 10.4385 68.6831C10.4385 42.6538 31.5069 21.5854 57.5361 21.5854C83.5654 21.5854 104.634 42.6538 104.634 68.6831Z" stroke="#575757" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 4"/>
<path d="M74.8781 71.7725C74.5226 73.2834 73.9893 74.8833 73.3671 76.2164C71.6785 79.5049 69.0121 82.0824 65.7236 83.7711C62.3462 85.4598 58.3467 86.1708 54.3471 85.282C44.926 83.3267 38.8823 74.0833 40.8376 64.6622C42.7929 55.2411 51.9474 49.1085 61.3685 51.1527C64.7459 51.8637 67.6789 53.5524 70.1675 55.8632C74.3448 60.0405 76.1224 66.0843 74.8781 71.7725Z" fill="#1E1E1E" stroke="#545454" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.4127 66.7063H60.4132C59.8609 66.7063 59.4132 66.2586 59.4132 65.7063V62.7067C59.4132 61.9068 58.791 61.1958 57.9023 61.1958C57.1024 61.1958 56.3913 61.818 56.3913 62.7067V65.7063C56.3913 66.2586 55.9436 66.7063 55.3913 66.7063H52.3918C51.5919 66.7063 50.8809 67.3284 50.8809 68.2172C50.8809 69.106 51.503 69.7281 52.3918 69.7281H55.3913C55.9436 69.7281 56.3913 70.1759 56.3913 70.7281V73.7277C56.3913 74.5276 57.0135 75.2386 57.9023 75.2386C58.7022 75.2386 59.4132 74.6165 59.4132 73.7277V70.7281C59.4132 70.1759 59.8609 69.7281 60.4132 69.7281H63.4127C64.2126 69.7281 64.9237 69.106 64.9237 68.2172C64.9237 67.3284 64.2126 66.7063 63.4127 66.7063Z" fill="#575757"/>
<path d="M78.3516 21.6057C78.3516 27.1603 76.1318 32.0976 72.6787 35.8007C72.4321 36.171 72.0621 36.4178 71.6921 36.6647C67.9924 40.1209 63.0594 42.2192 57.6332 42.2192C53.3169 42.2192 49.2472 40.8615 45.9174 38.5162C44.9308 37.899 44.0675 37.035 43.2043 36.2944C39.3812 32.5914 37.0381 27.4071 37.0381 21.6057C37.0381 10.2498 46.2874 0.992188 57.6332 0.992188C69.1023 0.992188 78.3516 10.2498 78.3516 21.6057Z" fill="black"/>
<path d="M78.3516 21.6043C78.3516 27.6526 75.7618 32.9603 71.6921 36.7868C67.9924 40.2429 63.0594 42.3413 57.6332 42.3413C53.3169 42.3413 49.2472 40.9835 45.9174 38.6383C40.4912 34.9353 37.0381 28.7635 37.0381 21.7278C37.0381 10.3718 46.2874 1.11426 57.6332 1.11426C68.979 1.11426 78.3516 10.2484 78.3516 21.6043Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M72.679 35.8005C72.4324 36.1708 72.0624 36.4176 71.6924 36.6645C67.9927 40.1207 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8613 45.9177 38.516C44.9311 37.8988 44.0679 37.0348 43.2046 36.2942C43.6979 35.677 44.4378 35.3067 46.041 34.6895L46.6577 34.4427C47.8909 33.9489 49.6174 33.3318 51.8373 32.3443C52.2072 32.2209 52.4539 31.974 52.7005 31.7271C52.8239 31.6037 52.9472 31.4803 52.9472 31.2334C53.0705 30.9865 53.1938 30.6162 53.1938 30.3693V26.1726C53.0705 26.0492 53.0705 26.0492 52.9472 25.9257C52.5772 25.432 52.3306 24.8148 52.3306 24.0742L52.0839 23.9508C50.974 24.1976 51.0973 23.0867 50.8507 20.8649C50.7274 20.0009 50.8507 19.754 51.344 19.6306L51.7139 19.1368C50.974 17.4088 50.604 15.8041 50.604 14.5698C50.604 12.4714 51.4673 11.1136 52.7005 10.4964C51.9606 9.01522 51.9606 8.52148 51.9606 8.52148C51.9606 8.52148 56.2769 9.26209 57.7568 9.01522C59.6067 8.64492 62.5664 9.13866 63.6764 11.6073C65.5262 12.3479 66.1428 13.4589 66.3895 14.6932C66.6361 16.6681 65.5262 18.7665 65.2796 19.6306V19.754C65.5262 19.8774 65.6495 20.1243 65.5262 20.9883C65.2796 23.0867 65.2796 24.3211 64.293 24.0742L63.3064 25.8023C63.3064 26.0492 63.3064 26.0492 63.1831 26.1726C63.1831 26.5429 63.1831 27.1601 63.1831 30.4928C63.1831 30.8631 63.3064 31.3568 63.553 31.6037C63.6764 31.7271 63.6764 31.8506 63.7997 31.8506C64.0463 32.0974 64.293 32.3443 64.5396 32.3443C67.0061 33.3318 68.7326 34.0724 70.0892 34.5661C71.3224 35.0599 72.1857 35.4302 72.679 35.8005Z" fill="#181818"/>
<path d="M72.679 35.8004C72.4324 36.1707 72.0624 36.4176 71.6924 36.6644C67.9927 40.1206 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8612 45.9177 38.5159C44.9311 37.8988 44.0679 37.0347 43.2046 36.2941C43.6979 35.6769 44.4378 35.3066 46.041 34.6895L46.6577 34.4426C47.8909 33.9489 49.6174 33.3317 51.8373 32.3442C52.2072 32.2208 52.4539 31.9739 52.7005 31.7271C53.9338 33.4551 55.907 34.566 58.2501 34.566C60.4699 34.566 62.4431 33.4551 63.6764 31.8505C63.923 32.0974 64.1697 32.3442 64.4163 32.3442C66.8828 33.3317 68.6093 34.0723 69.9659 34.566C71.3224 35.0598 72.1857 35.4301 72.679 35.8004Z" fill="#525252"/>
<path d="M65.1564 19.5071C65.2797 19.0134 65.0331 18.2728 64.7864 17.9025C64.7864 17.7791 64.6631 17.7791 64.6631 17.6556C63.7999 15.9275 61.95 15.3104 60.2235 15.1869C55.6605 14.9401 55.2905 15.8041 53.9339 14.5698C54.4272 15.1869 54.4272 16.2978 53.6873 17.5322C53.194 18.3962 52.3307 18.89 51.4675 19.1368C49.371 14.4463 50.4809 11.4839 52.454 10.4964C51.7141 9.01522 51.7141 8.52148 51.7141 8.52148C51.7141 8.52148 56.0304 9.26209 57.5103 9.01522C59.3602 8.64492 62.32 9.13866 63.4299 11.6073C65.2797 12.3479 65.8964 13.4589 66.143 14.6932C66.513 16.5447 65.4031 18.6431 65.1564 19.5071Z" fill="#737373"/>
<path d="M53.317 30.3692V26.1724C53.1936 26.049 53.1936 26.049 53.0703 25.9256V25.6787C53.317 26.049 53.5636 26.4193 53.9336 26.6662L57.2633 29.0114C58.0033 29.6286 59.1132 29.6286 59.8531 29.0114L62.9362 26.2959C63.0595 26.1724 63.1829 26.1724 63.3062 26.049C63.3062 26.4193 63.3062 27.0365 63.3062 30.3692C63.3062 30.6161 63.3062 30.7395 63.4295 30.9864H53.317C53.1936 30.7395 53.317 30.6161 53.317 30.3692Z" fill="url(#paint0_linear)"/>
<path d="M78.3516 21.6043C78.3516 27.6526 75.7618 32.9603 71.6921 36.7868C67.9924 40.2429 63.0594 42.3413 57.6332 42.3413C53.3169 42.3413 49.2472 40.9835 45.9174 38.6383C40.4912 34.9353 37.0381 28.7635 37.0381 21.7278C37.0381 10.3718 46.2874 1.11426 57.6332 1.11426C68.979 1.11426 78.3516 10.2484 78.3516 21.6043Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M72.679 35.8005C72.4324 36.1708 72.0624 36.4176 71.6924 36.6645C67.9927 40.1207 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8613 45.9177 38.516C44.9311 37.8988 44.0679 37.0348 43.2046 36.2942C43.6979 35.677 44.4378 35.3067 46.041 34.6895L46.6577 34.4427C47.8909 33.9489 49.6174 33.3318 51.8373 32.3443C52.2072 32.2209 52.4539 31.974 52.7005 31.7271C52.8239 31.6037 52.9472 31.4803 52.9472 31.2334C53.0705 30.9865 53.1938 30.6162 53.1938 30.3693V26.1726C53.0705 26.0492 53.0705 26.0492 52.9472 25.9257C52.5772 25.432 52.3306 24.8148 52.3306 24.0742L52.0839 23.9508C50.974 24.1976 51.0973 23.0867 50.8507 20.8649C50.7274 20.0009 50.8507 19.754 51.344 19.6306L51.7139 19.1368C50.974 17.4088 50.604 15.8041 50.604 14.5698C50.604 12.4714 51.4673 11.1136 52.7005 10.4964C51.9606 9.01522 51.9606 8.52148 51.9606 8.52148C51.9606 8.52148 56.2769 9.26209 57.7568 9.01522C59.6067 8.64492 62.5664 9.13866 63.6764 11.6073C65.5262 12.3479 66.1428 13.4589 66.3895 14.6932C66.6361 16.6681 65.5262 18.7665 65.2796 19.6306V19.754C65.5262 19.8774 65.6495 20.1243 65.5262 20.9883C65.2796 23.0867 65.2796 24.3211 64.293 24.0742L63.3064 25.8023C63.3064 26.0492 63.3064 26.0492 63.1831 26.1726C63.1831 26.5429 63.1831 27.1601 63.1831 30.4928C63.1831 30.8631 63.3064 31.3568 63.553 31.6037C63.6764 31.7271 63.6764 31.8506 63.7997 31.8506C64.0463 32.0974 64.293 32.3443 64.5396 32.3443C67.0061 33.3318 68.7326 34.0724 70.0892 34.5661C71.3224 35.0599 72.1857 35.4302 72.679 35.8005Z" fill="#0E0E0E"/>
<path d="M72.679 35.8004C72.4324 36.1707 72.0624 36.4176 71.6924 36.6644C67.9927 40.1206 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8612 45.9177 38.5159C44.9311 37.8988 44.0679 37.0347 43.2046 36.2941C43.6979 35.6769 44.4378 35.3066 46.041 34.6895L46.6577 34.4426C47.8909 33.9489 49.6174 33.3317 51.8373 32.3442C52.2072 32.2208 52.4539 31.9739 52.7005 31.7271C53.9338 33.4551 55.907 34.566 58.2501 34.566C60.4699 34.566 62.4431 33.4551 63.6764 31.8505C63.923 32.0974 64.1697 32.3442 64.4163 32.3442C66.8828 33.3317 68.6093 34.0723 69.9659 34.566C71.3224 35.0598 72.1857 35.4301 72.679 35.8004Z" fill="#575757"/>
<path d="M65.1562 19.5071C65.2795 19.0134 65.0328 18.2728 64.7862 17.9025C64.7862 17.7791 64.6629 17.7791 64.6629 17.6556C63.7996 15.9275 61.9497 15.3104 60.2232 15.1869C55.6602 14.9401 55.2903 15.8041 53.9337 14.5698C54.427 15.1869 54.427 16.2978 53.687 17.5322C53.1937 18.3962 52.3305 18.89 51.4672 19.1368C49.3707 14.4463 50.4806 11.4839 52.4538 10.4964C51.7139 9.01522 51.7139 8.52148 51.7139 8.52148C51.7139 8.52148 56.0302 9.26209 57.5101 9.01522C59.3599 8.64492 62.3197 9.13866 63.4296 11.6073C65.2795 12.3479 65.8961 13.4589 66.1428 14.6932C66.5127 16.5447 65.4028 18.6431 65.1562 19.5071Z" fill="#545454"/>
<path d="M53.3172 30.3692V26.1724C53.1939 26.049 53.1939 26.049 53.0706 25.9256V25.6787C53.3172 26.049 53.5639 26.4193 53.9338 26.6662L57.2636 29.0114C58.0035 29.6286 59.1134 29.6286 59.8534 29.0114L62.9365 26.2959C63.0598 26.1724 63.1831 26.1724 63.3064 26.049C63.3064 26.4193 63.3064 27.0365 63.3064 30.3692C63.3064 30.6161 63.3064 30.7395 63.4298 30.9864H53.3172C53.1939 30.7395 53.3172 30.6161 53.3172 30.3692Z" fill="url(#paint0_linear_592_1141)"/>
<path d="M115.285 97.8074C115.285 103.362 113.065 108.299 109.612 112.002C109.365 112.373 108.995 112.619 108.625 112.866C104.925 116.323 99.9925 118.421 94.5663 118.421C90.25 118.421 86.1803 117.063 82.8505 114.718C81.8639 114.101 81.0007 113.237 80.1374 112.496C76.3143 108.793 73.9712 103.609 73.9712 97.8074C73.9712 86.4514 83.2205 77.1938 94.5663 77.1938C106.035 77.1938 115.285 86.4514 115.285 97.8074Z" fill="black"/>
<path d="M115.285 97.8065C115.285 103.855 112.695 109.162 108.625 112.989C104.925 116.445 99.9925 118.543 94.5663 118.543C90.25 118.543 86.1803 117.186 82.8505 114.84C77.4243 111.137 73.9712 104.966 73.9712 97.9299C73.9712 86.574 83.2205 77.3164 94.5663 77.3164C105.912 77.3164 115.285 86.4505 115.285 97.8065Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.613 112.003C109.366 112.373 108.996 112.62 108.626 112.867C104.927 116.323 99.9938 118.421 94.5676 118.421C90.2512 118.421 86.1815 117.063 82.8518 114.718C81.8652 114.101 81.0019 113.237 80.1387 112.496C80.632 111.879 81.3719 111.509 82.9751 110.892L83.5917 110.645C84.825 110.151 86.5515 109.534 88.7713 108.546C89.1413 108.423 89.388 108.176 89.6346 107.929C89.7579 107.806 89.8813 107.682 89.8813 107.436C90.0046 107.189 90.1279 106.818 90.1279 106.571V102.375C90.0046 102.251 90.0046 102.251 89.8813 102.128C89.5113 101.634 89.2646 101.017 89.2646 100.276L89.018 100.153C87.9081 100.4 88.0314 99.2889 87.7848 97.0671C87.6614 96.203 87.7848 95.9562 88.278 95.8327L88.648 95.339C87.9081 93.6109 87.5381 92.0063 87.5381 90.7719C87.5381 88.6735 88.4014 87.3158 89.6346 86.6986C88.8947 85.2174 88.8947 84.7236 88.8947 84.7236C88.8947 84.7236 93.211 85.4642 94.6909 85.2174C96.5408 84.8471 99.5005 85.3408 100.61 87.8095C102.46 88.5501 103.077 89.661 103.324 90.8953C103.57 92.8703 102.46 94.9687 102.214 95.8327V95.9562C102.46 96.0796 102.584 96.3265 102.46 97.1905C102.214 99.2889 102.214 100.523 101.227 100.276L100.24 102.004C100.24 102.251 100.24 102.251 100.117 102.375C100.117 102.745 100.117 103.362 100.117 106.695C100.117 107.065 100.24 107.559 100.487 107.806C100.61 107.929 100.61 108.053 100.734 108.053C100.98 108.3 101.227 108.546 101.474 108.546C103.94 109.534 105.667 110.275 107.023 110.768C108.257 111.262 109.12 111.632 109.613 112.003Z" fill="#181818"/>
<path d="M109.612 112.003C109.365 112.373 108.995 112.62 108.626 112.867C104.926 116.323 99.9928 118.421 94.5666 118.421C90.2503 118.421 86.1806 117.063 82.8508 114.718C81.8642 114.101 81.001 113.237 80.1377 112.496C80.631 111.879 81.3709 111.509 82.9741 110.892L83.5908 110.645C84.824 110.151 86.5505 109.534 88.7704 108.546C89.1403 108.423 89.387 108.176 89.6336 107.929C90.8669 109.657 92.8401 110.768 95.1832 110.768C97.403 110.768 99.3762 109.657 100.609 108.053C100.856 108.3 101.103 108.546 101.349 108.546C103.816 109.534 105.542 110.274 106.899 110.768C108.256 111.262 109.119 111.632 109.612 112.003Z" fill="#525252"/>
<path d="M102.09 95.7093C102.213 95.2155 101.966 94.4749 101.72 94.1046C101.72 93.9812 101.596 93.9812 101.596 93.8578C100.733 92.1297 98.8831 91.5125 97.1566 91.3891C92.5936 91.1422 92.2236 92.0063 90.867 90.7719C91.3603 91.3891 91.3603 92.5 90.6204 93.7343C90.1271 94.5984 89.2638 95.0921 88.4006 95.339C86.3041 90.6485 87.414 87.6861 89.3872 86.6986C88.6472 85.2174 88.6472 84.7236 88.6472 84.7236C88.6472 84.7236 92.9635 85.4642 94.4434 85.2174C96.2933 84.8471 99.2531 85.3408 100.363 87.8095C102.213 88.5501 102.829 89.661 103.076 90.8953C103.446 92.7469 102.336 94.8452 102.09 95.7093Z" fill="#737373"/>
<path d="M90.2501 106.571V102.375C90.1267 102.251 90.1267 102.251 90.0034 102.128V101.881C90.2501 102.251 90.4967 102.621 90.8667 102.868L94.1964 105.214C94.9364 105.831 96.0463 105.831 96.7862 105.214L99.8693 102.498C99.9927 102.375 100.116 102.375 100.239 102.251C100.239 102.621 100.239 103.239 100.239 106.571C100.239 106.818 100.239 106.942 100.363 107.189H90.2501C90.1267 106.942 90.2501 106.818 90.2501 106.571Z" fill="url(#paint1_linear)"/>
<path d="M41.2036 98.1168C41.2036 103.918 38.7371 109.102 34.7908 112.805C33.6808 113.793 32.5709 114.657 31.2144 115.398C28.2546 117.126 24.8015 118.113 21.1018 118.113C17.4021 118.113 13.949 117.126 10.9892 115.398C10.4959 115.151 10.126 114.904 9.63268 114.534C4.45307 110.954 1 104.906 1 98.1168C1 87.0077 10.0026 78.1204 20.9785 78.1204C32.201 77.997 41.2036 87.0077 41.2036 98.1168Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M17.0323 102.56C17.279 102.806 17.5256 103.177 17.8956 103.424C18.1422 103.67 18.3889 103.794 18.6355 104.041C18.7589 104.164 19.0055 104.288 19.1288 104.411C19.1288 104.411 19.2522 104.411 19.2522 104.534L19.3755 104.658V106.386C19.3755 106.386 19.3755 106.386 19.2522 106.263C19.1288 106.139 18.8822 106.016 18.7589 105.892C18.5122 105.769 18.2656 105.522 18.0189 105.399C17.8956 105.399 17.8956 105.275 17.7723 105.275C16.909 104.781 16.1691 104.288 16.1691 103.67C16.2924 103.424 16.539 103.053 17.0323 102.56ZM34.1878 112.107C33.4478 110.502 32.4478 108.978 30.7213 108.114C29.858 107.744 28.8714 107.373 27.8848 107.373C27.6382 107.373 27.2682 107.373 27.0216 107.373C26.8982 107.373 26.7749 107.373 26.6516 107.373C25.295 107.25 25.1717 107.003 25.1717 107.003V104.164C26.035 103.424 26.8982 102.56 27.6382 101.695C28.2548 100.831 28.7481 99.844 28.9947 98.6096C30.1047 98.3628 30.8446 97.3753 30.7213 96.1409C30.7213 95.6472 30.3513 95.1535 30.3513 94.6597C30.3513 94.4129 30.3513 94.166 30.3513 93.9191C30.3513 93.7957 30.3513 93.5488 30.3513 93.4254C30.3513 93.3019 30.3513 93.0551 30.3513 92.9316C30.228 92.0676 29.9813 91.2036 29.488 90.2161C28.0082 87.5005 25.295 85.7725 22.0886 85.7725C21.472 85.7725 20.8554 85.8959 20.2387 86.0193C19.1288 86.2662 18.0189 86.7599 17.1556 87.5005C17.0323 87.624 16.7857 87.7474 16.6624 87.9943L16.539 88.1177C15.5524 89.1052 14.6892 90.2161 14.3192 91.5739C13.8259 92.9316 13.8259 94.2894 13.9492 95.6472C13.9492 95.6472 13.9492 95.6472 13.9492 95.7706V95.8941C13.9492 96.1409 14.0726 96.1409 13.9492 96.2644C13.9492 96.3878 13.8259 96.3878 13.8259 96.5112C13.5793 96.8815 13.4559 97.3753 13.7026 98.1159C14.1959 99.3502 14.9358 99.2268 15.7991 99.844C15.7991 99.844 15.6758 99.844 15.6758 99.9674L14.8125 100.214C10.8661 101.449 9.50957 104.781 11.2361 106.88C11.8527 107.62 12.8393 108.238 14.3192 108.608C13.9492 108.608 13.5793 108.855 13.3326 109.102C11.6061 110.459 10.4962 112.558 10.2495 114.533C10.2495 114.656 10.2495 114.78 10.2495 114.903C10.7428 115.15 11.1128 115.52 11.6061 115.767L30.9175 115.315C32.1507 114.575 32.6963 114.003 33.8062 113.016C33.6829 112.399 34.3111 112.23 34.1878 112.107Z" fill="#181818"/>
<path d="M34.7909 112.805C33.681 113.792 32.5711 114.656 31.2145 115.397C28.2547 117.125 24.8017 118.113 21.1019 118.113C17.4022 118.113 13.9491 117.125 10.9894 115.397C10.4961 115.15 10.1261 114.903 9.63281 114.533C9.63281 114.41 9.63281 114.286 9.63281 114.163C9.87946 112.188 10.9894 110.089 12.7159 108.732C12.9626 108.485 13.3325 108.361 13.7025 108.238C12.2226 107.991 11.236 107.374 10.6194 106.51H15.3057C16.6623 108.361 18.7588 109.472 21.2253 109.472C23.3218 109.472 25.1716 108.608 26.5282 107.25C26.6515 107.25 26.7748 107.25 26.8982 107.25C27.1448 107.25 27.3915 107.25 27.7614 107.25C28.748 107.25 29.7346 107.497 30.5979 107.991C32.3244 108.855 33.5577 110.336 34.4209 112.064C34.6676 112.311 34.6676 112.558 34.7909 112.805Z" fill="#525252"/>
<path d="M25.2953 104.165V106.757L17.5259 107.004L17.8958 105.275C18.0192 105.275 18.0192 105.399 18.1425 105.399C18.3891 105.522 18.6358 105.769 18.8824 105.893C19.0058 106.016 19.1291 106.139 19.3757 106.263C19.3757 106.263 19.4991 106.263 19.4991 106.386V104.658L19.3757 104.535C20.7323 105.275 22.5822 105.769 25.2953 104.165Z" fill="url(#paint2_linear)"/>
<path d="M30.351 93.4261C28.8711 93.9198 27.1446 94.1667 25.5414 94.0432C22.9516 93.7964 20.4851 92.8089 18.5119 91.0808C17.8953 92.9323 16.2921 94.2901 14.4422 95.1541C14.1956 95.2776 13.949 95.401 13.7023 95.401C13.7023 95.401 13.7023 95.401 13.7023 95.2776C13.579 93.9198 13.579 92.562 14.0723 91.2042C14.4422 89.8465 15.3055 88.7356 16.2921 87.7481L16.4154 87.6247C16.5388 87.5012 16.7854 87.3778 16.9087 87.1309C17.772 86.3903 18.8819 85.8966 19.9918 85.6497C20.6084 85.5263 21.2251 85.4028 21.8417 85.4028C25.0481 85.4028 27.8846 87.1309 29.2411 89.8465C29.7344 90.8339 29.9811 91.8214 30.1044 92.562C30.351 93.0558 30.351 93.3026 30.351 93.4261Z" fill="#737373"/>
<path d="M20.4853 111.694C19.7453 112.558 18.5121 112.558 17.4022 112.558C18.5121 111.447 17.8955 107.868 13.9491 108.238C8.52286 107.251 9.01616 101.573 14.4424 99.8445L15.3057 99.5977L15.429 99.7211C15.799 100.832 16.4156 101.819 17.0322 102.56C14.8124 104.412 17.8955 104.905 19.3754 106.387C20.6086 107.127 21.7185 110.213 20.4853 111.694Z" fill="#737373"/>
<path d="M115.285 97.8065C115.285 103.855 112.695 109.162 108.625 112.989C104.925 116.445 99.9925 118.543 94.5663 118.543C90.25 118.543 86.1803 117.186 82.8505 114.84C77.4243 111.137 73.9712 104.966 73.9712 97.9299C73.9712 86.574 83.2205 77.3164 94.5663 77.3164C105.912 77.3164 115.285 86.4505 115.285 97.8065Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.613 112.003C109.366 112.373 108.996 112.62 108.626 112.867C104.927 116.323 99.9936 118.421 94.5673 118.421C90.251 118.421 86.1813 117.063 82.8516 114.718C81.865 114.101 81.0017 113.237 80.1384 112.496C80.6317 111.879 81.3717 111.509 82.9749 110.892L83.5915 110.645C84.8247 110.151 86.5513 109.534 88.7711 108.546C89.1411 108.423 89.3877 108.176 89.6344 107.929C89.7577 107.806 89.881 107.682 89.881 107.436C90.0043 107.189 90.1277 106.818 90.1277 106.571V102.375C90.0043 102.251 90.0043 102.251 89.881 102.128C89.511 101.634 89.2644 101.017 89.2644 100.276L89.0178 100.153C87.9078 100.4 88.0312 99.2889 87.7845 97.0671C87.6612 96.203 87.7845 95.9562 88.2778 95.8327L88.6478 95.339C87.9078 93.6109 87.5379 92.0063 87.5379 90.7719C87.5379 88.6735 88.4011 87.3158 89.6344 86.6986C88.8944 85.2174 88.8944 84.7236 88.8944 84.7236C88.8944 84.7236 93.2108 85.4642 94.6907 85.2174C96.5405 84.8471 99.5003 85.3408 100.61 87.8095C102.46 88.5501 103.077 89.661 103.323 90.8953C103.57 92.8703 102.46 94.9687 102.213 95.8327V95.9562C102.46 96.0796 102.583 96.3265 102.46 97.1905C102.213 99.2889 102.213 100.523 101.227 100.276L100.24 102.004C100.24 102.251 100.24 102.251 100.117 102.375C100.117 102.745 100.117 103.362 100.117 106.695C100.117 107.065 100.24 107.559 100.487 107.806C100.61 107.929 100.61 108.053 100.734 108.053C100.98 108.3 101.227 108.546 101.473 108.546C103.94 109.534 105.666 110.275 107.023 110.768C108.256 111.262 109.12 111.632 109.613 112.003Z" fill="#0E0E0E"/>
<path d="M109.612 112.003C109.365 112.373 108.995 112.62 108.626 112.867C104.926 116.323 99.9928 118.421 94.5666 118.421C90.2503 118.421 86.1806 117.063 82.8508 114.718C81.8642 114.101 81.001 113.237 80.1377 112.496C80.631 111.879 81.3709 111.509 82.9741 110.892L83.5908 110.645C84.824 110.151 86.5505 109.534 88.7704 108.546C89.1403 108.423 89.387 108.176 89.6336 107.929C90.8669 109.657 92.8401 110.768 95.1832 110.768C97.403 110.768 99.3762 109.657 100.609 108.053C100.856 108.3 101.103 108.546 101.349 108.546C103.816 109.534 105.542 110.274 106.899 110.768C108.256 111.262 109.119 111.632 109.612 112.003Z" fill="#575757"/>
<path d="M102.089 95.7093C102.213 95.2155 101.966 94.4749 101.719 94.1046C101.719 93.9812 101.596 93.9812 101.596 93.8578C100.733 92.1297 98.8829 91.5125 97.1563 91.3891C92.5933 91.1422 92.2234 92.0063 90.8668 90.7719C91.3601 91.3891 91.3601 92.5 90.6201 93.7343C90.1269 94.5984 89.2636 95.0921 88.4003 95.339C86.3038 90.6485 87.4137 87.6861 89.3869 86.6986C88.647 85.2174 88.647 84.7236 88.647 84.7236C88.647 84.7236 92.9633 85.4642 94.4432 85.2174C96.293 84.8471 99.2528 85.3408 100.363 87.8095C102.213 88.5501 102.829 89.661 103.076 90.8953C103.446 92.7469 102.336 94.8452 102.089 95.7093Z" fill="#545454"/>
<path d="M90.2501 106.571V102.375C90.1267 102.251 90.1267 102.251 90.0034 102.128V101.881C90.2501 102.251 90.4967 102.621 90.8667 102.868L94.1964 105.214C94.9364 105.831 96.0463 105.831 96.7862 105.214L99.8693 102.498C99.9927 102.375 100.116 102.375 100.239 102.251C100.239 102.621 100.239 103.239 100.239 106.571C100.239 106.818 100.239 106.942 100.363 107.189H90.2501C90.1267 106.942 90.2501 106.818 90.2501 106.571Z" fill="url(#paint1_linear_592_1141)"/>
<path d="M41.2036 98.1168C41.2036 103.918 38.7371 109.102 34.7908 112.805C33.6808 113.793 32.5709 114.657 31.2144 115.398C28.2546 117.126 24.8015 118.113 21.1018 118.113C17.4021 118.113 13.949 117.126 10.9892 115.398C10.4959 115.151 10.126 114.904 9.63268 114.534C4.45307 110.954 1 104.906 1 98.1168C1 87.0077 10.0026 78.1204 20.9785 78.1204C32.201 77.997 41.2036 87.0077 41.2036 98.1168Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M17.0321 102.56C17.2787 102.806 17.5254 103.177 17.8954 103.424C18.142 103.67 18.3886 103.794 18.6353 104.041C18.7586 104.164 19.0053 104.288 19.1286 104.411C19.1286 104.411 19.2519 104.411 19.2519 104.534L19.3752 104.658V106.386C19.3752 106.386 19.3752 106.386 19.2519 106.263C19.1286 106.139 18.8819 106.016 18.7586 105.892C18.512 105.769 18.2653 105.522 18.0187 105.399C17.8953 105.399 17.8953 105.275 17.772 105.275C16.9088 104.781 16.1688 104.288 16.1688 103.67C16.2921 103.424 16.5388 103.053 17.0321 102.56ZM34.1875 112.107C33.4476 110.502 32.4476 108.978 30.721 108.114C29.8578 107.744 28.8712 107.373 27.8846 107.373C27.6379 107.373 27.268 107.373 27.0213 107.373C26.898 107.373 26.7747 107.373 26.6513 107.373C25.2948 107.25 25.1715 107.003 25.1715 107.003V104.164C26.0347 103.424 26.898 102.56 27.6379 101.695C28.2546 100.831 28.7479 99.844 28.9945 98.6096C30.1044 98.3628 30.8444 97.3753 30.721 96.1409C30.721 95.6472 30.3511 95.1535 30.3511 94.6597C30.3511 94.4129 30.3511 94.166 30.3511 93.9191C30.3511 93.7957 30.3511 93.5488 30.3511 93.4254C30.3511 93.3019 30.3511 93.0551 30.3511 92.9316C30.2277 92.0676 29.9811 91.2036 29.4878 90.2161C28.0079 87.5005 25.2948 85.7725 22.0884 85.7725C21.4717 85.7725 20.8551 85.8959 20.2385 86.0193C19.1286 86.2662 18.0187 86.7599 17.1554 87.5005C17.0321 87.624 16.7854 87.7474 16.6621 87.9943L16.5388 88.1177C15.5522 89.1052 14.6889 90.2161 14.319 91.5739C13.8257 92.9316 13.8257 94.2894 13.949 95.6472C13.949 95.6472 13.949 95.6472 13.949 95.7706V95.8941C13.949 96.1409 14.0723 96.1409 13.949 96.2644C13.949 96.3878 13.8257 96.3878 13.8257 96.5112C13.579 96.8815 13.4557 97.3753 13.7023 98.1159C14.1956 99.3502 14.9356 99.2268 15.7988 99.844C15.7988 99.844 15.6755 99.844 15.6755 99.9674L14.8123 100.214C10.8659 101.449 9.50932 104.781 11.2359 106.88C11.8525 107.62 12.8391 108.238 14.319 108.608C13.949 108.608 13.579 108.855 13.3324 109.102C11.6058 110.459 10.4959 112.558 10.2493 114.533C10.2493 114.656 10.2493 114.78 10.2493 114.903C10.7426 115.15 11.1125 115.52 11.6058 115.767L30.9172 115.315C32.1505 114.575 32.6961 114.003 33.806 113.016C33.6827 112.399 34.3108 112.23 34.1875 112.107Z" fill="#0E0E0E"/>
<path d="M34.7909 112.805C33.681 113.792 32.5711 114.656 31.2145 115.397C28.2547 117.125 24.8017 118.113 21.1019 118.113C17.4022 118.113 13.9491 117.125 10.9894 115.397C10.4961 115.15 10.1261 114.903 9.63281 114.533C9.63281 114.41 9.63281 114.286 9.63281 114.163C9.87946 112.188 10.9894 110.089 12.7159 108.732C12.9626 108.485 13.3325 108.361 13.7025 108.238C12.2226 107.991 11.236 107.374 10.6194 106.51H15.3057C16.6623 108.361 18.7588 109.472 21.2253 109.472C23.3218 109.472 25.1716 108.608 26.5282 107.25C26.6515 107.25 26.7748 107.25 26.8982 107.25C27.1448 107.25 27.3915 107.25 27.7614 107.25C28.748 107.25 29.7346 107.497 30.5979 107.991C32.3244 108.855 33.5577 110.336 34.4209 112.064C34.6676 112.311 34.6676 112.558 34.7909 112.805Z" fill="#575757"/>
<path d="M25.2953 104.165V106.757L17.5259 107.004L17.8958 105.275C18.0192 105.275 18.0192 105.399 18.1425 105.399C18.3891 105.522 18.6358 105.769 18.8824 105.893C19.0058 106.016 19.1291 106.139 19.3757 106.263C19.3757 106.263 19.4991 106.263 19.4991 106.386V104.658L19.3757 104.535C20.7323 105.275 22.5822 105.769 25.2953 104.165Z" fill="url(#paint2_linear_592_1141)"/>
<path d="M30.351 93.4261C28.8711 93.9198 27.1446 94.1667 25.5414 94.0432C22.9516 93.7964 20.4851 92.8089 18.5119 91.0808C17.8953 92.9323 16.2921 94.2901 14.4422 95.1541C14.1956 95.2776 13.949 95.401 13.7023 95.401C13.7023 95.401 13.7023 95.401 13.7023 95.2776C13.579 93.9198 13.579 92.562 14.0723 91.2042C14.4422 89.8465 15.3055 88.7356 16.2921 87.7481L16.4154 87.6247C16.5388 87.5012 16.7854 87.3778 16.9087 87.1309C17.772 86.3903 18.8819 85.8966 19.9918 85.6497C20.6084 85.5263 21.2251 85.4028 21.8417 85.4028C25.0481 85.4028 27.8846 87.1309 29.2411 89.8465C29.7344 90.8339 29.9811 91.8214 30.1044 92.562C30.351 93.0558 30.351 93.3026 30.351 93.4261Z" fill="#545454"/>
<path d="M20.4853 111.694C19.7453 112.558 18.5121 112.558 17.4022 112.558C18.5121 111.447 17.8955 107.868 13.9491 108.238C8.52286 107.251 9.01616 101.573 14.4424 99.8445L15.3057 99.5977L15.429 99.7211C15.799 100.832 16.4156 101.819 17.0322 102.56C14.8124 104.412 17.8955 104.905 19.3754 106.387C20.6086 107.127 21.7185 110.213 20.4853 111.694Z" fill="#545454"/>
<defs>
<linearGradient id="paint0_linear" x1="58.2299" y1="30.8211" x2="58.2299" y2="28.0409" gradientUnits="userSpaceOnUse">
<stop stop-color="#181818"/>
<stop offset="0.9913" stop-color="#222427"/>
<linearGradient id="paint0_linear_592_1141" x1="58.2301" y1="30.8211" x2="58.2301" y2="28.0409" gradientUnits="userSpaceOnUse">
<stop stop-color="#151515"/>
<stop offset="0.9913" stop-color="#0B0B0B"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="95.163" y1="107.023" x2="95.163" y2="104.243" gradientUnits="userSpaceOnUse">
<stop stop-color="#181818"/>
<stop offset="0.9913" stop-color="#222427"/>
<linearGradient id="paint1_linear_592_1141" x1="95.163" y1="107.023" x2="95.163" y2="104.243" gradientUnits="userSpaceOnUse">
<stop stop-color="#151515"/>
<stop offset="0.9913" stop-color="#0B0B0B"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="21.3956" y1="106.915" x2="21.3956" y2="105.428" gradientUnits="userSpaceOnUse">
<stop stop-color="#181818"/>
<stop offset="0.9913" stop-color="#222427"/>
<linearGradient id="paint2_linear_592_1141" x1="21.3956" y1="106.915" x2="21.3956" y2="105.428" gradientUnits="userSpaceOnUse">
<stop stop-color="#151515"/>
<stop offset="0.9913" stop-color="#0B0B0B"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,24 +1,24 @@
<svg width="145" height="110" viewBox="0 0 145 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M74.6528 108.28C104.218 108.28 128.187 84.3113 128.187 54.6402C128.187 24.9692 104.113 1 74.6528 1C45.0873 1 21.1182 24.9692 21.1182 54.6402C21.1182 84.3113 45.0873 108.28 74.6528 108.28Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M134.984 37.645C137.374 37.645 139.313 35.7068 139.313 33.3158C139.313 30.9248 137.374 28.9866 134.984 28.9866C132.593 28.9866 130.654 30.9248 130.654 33.3158C130.654 35.7068 132.593 37.645 134.984 37.645Z" fill="#525252"/>
<path d="M141.319 20.7505C142.952 20.7505 144.275 19.4268 144.275 17.7939C144.275 16.1611 142.952 14.8374 141.319 14.8374C139.686 14.8374 138.362 16.1611 138.362 17.7939C138.362 19.4268 139.686 20.7505 141.319 20.7505Z" fill="#525252"/>
<path d="M23.5469 19.4783C25.1797 19.4783 26.5034 18.1546 26.5034 16.5217C26.5034 14.8889 25.1797 13.5652 23.5469 13.5652C21.914 13.5652 20.5903 14.8889 20.5903 16.5217C20.5903 18.1546 21.914 19.4783 23.5469 19.4783Z" fill="#181818"/>
<path d="M5.49073 76.4976C8.52318 76.4976 10.9815 74.0393 10.9815 71.0068C10.9815 67.9744 8.52318 65.5161 5.49073 65.5161C2.45828 65.5161 0 67.9744 0 71.0068C0 74.0393 2.45828 76.4976 5.49073 76.4976Z" fill="#181818"/>
<path d="M85.5262 69.1928V89.5045C85.5262 91.2707 84.4985 93.037 83.0304 93.9201L67.4679 102.898C66.587 103.487 65.4125 103.782 64.3848 103.782V78.4656L84.7921 66.6907C85.2326 67.4266 85.5262 68.3097 85.5262 69.1928Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M64.3846 78.4656V103.782C63.3569 103.782 62.3292 103.487 61.3015 102.898L45.739 93.9201C44.1241 93.037 43.2432 91.4179 43.2432 89.5045V69.1928C43.2432 68.3097 43.5368 67.4266 43.9772 66.6907L64.3846 78.4656Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M84.7921 66.6908L64.3848 78.4657L43.8306 66.5437C44.271 65.8077 44.8583 65.0718 45.7392 64.6302L61.7421 55.3575C63.2102 54.4744 65.1188 54.4744 66.7338 55.2103L83.1772 64.7774C83.1772 64.7774 83.1772 64.7774 83.324 64.7774C83.9112 65.3662 84.4985 65.9549 84.7921 66.6908Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M127.809 69.1928V89.5045C127.809 91.2707 126.782 93.037 125.314 93.9201L109.751 102.898C108.87 103.487 107.696 103.782 106.668 103.782V78.4656L127.075 66.6907C127.516 67.4266 127.809 68.3097 127.809 69.1928Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M106.668 78.4656V103.782C105.64 103.782 104.612 103.487 103.585 102.898L88.0222 93.9201C86.4073 93.037 85.5264 91.4179 85.5264 89.5045V69.1928C85.5264 68.3097 85.82 67.4266 86.2604 66.6907L106.668 78.4656Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M127.075 66.6908L106.668 78.4657L86.1138 66.5437C86.5542 65.8077 87.1415 65.0718 88.0224 64.6302L104.025 55.3575C105.493 54.4744 107.402 54.4744 109.017 55.2103L125.46 64.7774C125.46 64.7774 125.46 64.7774 125.607 64.7774C126.194 65.3662 126.782 65.9549 127.075 66.6908Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M64.2381 30.7776V51.0892C64.2381 52.8554 63.2104 54.6217 61.7423 55.5048L46.1798 64.4831C45.2989 65.0719 44.1244 65.3662 43.0967 65.3662V40.0503L63.5041 28.2754C64.0913 29.0113 64.2381 29.8944 64.2381 30.7776Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M43.0965 40.0503V65.3662C42.0688 65.3662 41.0411 65.0719 40.0134 64.4831L24.4509 55.5048C22.836 54.6217 21.9551 53.0026 21.9551 51.0892V30.7776C21.9551 29.8944 22.2487 29.0113 22.6892 28.2754L43.0965 40.0503Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.6505 28.2751L43.2432 40.0499L22.689 28.1279C23.1294 27.392 23.7167 26.656 24.5976 26.2145L40.6005 16.9418C42.0686 16.0586 43.9772 16.0586 45.5922 16.7946L61.8887 26.5088C61.8887 26.5088 61.8887 26.5088 62.0356 26.5088C62.6228 26.9504 63.2101 27.5391 63.6505 28.2751Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.311 30.3358V53.0024C109.311 55.063 108.283 56.9765 106.374 58.0068L88.9031 68.1626C87.8754 68.7513 86.7009 69.0457 85.5264 69.0457V40.6388L108.576 27.3921C109.017 28.2752 109.311 29.3055 109.311 30.3358Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M85.5262 40.6388V58.5955V69.0457C84.3516 69.0457 83.1771 68.7513 82.1494 68.1626L74.515 63.747L64.5315 58.0067C62.7697 56.9764 61.742 55.063 61.5952 53.1496V30.1886C61.5952 29.1583 61.8888 28.128 62.3293 27.2449L85.5262 40.6388Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M108.429 27.3921L85.3792 40.6388L62.3291 27.2449C62.7695 26.3618 63.5036 25.6258 64.3845 25.1843L82.296 14.7341C84.0578 13.7038 86.1132 13.7038 87.875 14.5869L106.374 25.3315C106.374 25.3315 106.521 25.3315 106.521 25.4787C107.402 26.0674 107.989 26.6561 108.429 27.3921Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.206 12.7656C108.856 12.7656 109.383 13.2922 109.383 13.9418V20.9987C109.383 21.6483 108.856 22.1749 108.206 22.1749C107.557 22.1749 107.03 21.6483 107.03 20.9987V13.9418C107.03 13.2922 107.557 12.7656 108.206 12.7656Z" fill="#737373"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M123.936 13.1101C124.396 13.5694 124.396 14.3141 123.936 14.7735L115.311 23.3986C114.852 23.8579 114.107 23.8579 113.648 23.3986C113.188 22.9393 113.188 22.1946 113.648 21.7353L122.273 13.1101C122.732 12.6508 123.477 12.6508 123.936 13.1101Z" fill="#737373"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.087 29.6239C114.087 28.9743 114.614 28.4478 115.264 28.4478H123.105C123.754 28.4478 124.281 28.9743 124.281 29.6239C124.281 30.2735 123.754 30.8001 123.105 30.8001H115.264C114.614 30.8001 114.087 30.2735 114.087 29.6239Z" fill="#737373"/>
<path d="M96.9656 43.7842C96.7253 44.8023 96.3648 45.8804 95.9443 46.7788C94.8027 48.9948 93.0003 50.7317 90.7772 51.8696C88.4942 53.0076 85.7905 53.4867 83.0868 52.8878C76.7182 51.5702 72.6326 45.3414 73.9544 38.9928C75.2762 32.6442 81.4646 28.5116 87.8333 29.8891C90.1164 30.3683 92.099 31.5062 93.7813 33.0634C96.6052 35.8784 97.8068 39.9511 96.9656 43.7842Z" fill="#181818" stroke="#737373" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M89.1596 40.3732H86.4797V37.6933C86.4797 37.1573 86.0628 36.6809 85.4673 36.6809C84.9314 36.6809 84.4549 37.0978 84.4549 37.6933V40.3732H81.7751C81.2391 40.3732 80.7627 40.79 80.7627 41.3855C80.7627 41.9811 81.1796 42.3979 81.7751 42.3979H84.4549V45.0778C84.4549 45.6137 84.8718 46.0902 85.4673 46.0902C86.0033 46.0902 86.4797 45.6733 86.4797 45.0778V42.3979H89.1596C89.6955 42.3979 90.172 41.9811 90.172 41.3855C90.172 40.79 89.6955 40.3732 89.1596 40.3732Z" fill="#525252"/>
<path d="M74.6528 108.28C104.218 108.28 128.187 84.3113 128.187 54.6402C128.187 24.9692 104.113 1 74.6528 1C45.0873 1 21.1182 24.9692 21.1182 54.6402C21.1182 84.3113 45.0873 108.28 74.6528 108.28Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M134.984 37.645C137.374 37.645 139.313 35.7068 139.313 33.3158C139.313 30.9248 137.374 28.9866 134.984 28.9866C132.593 28.9866 130.654 30.9248 130.654 33.3158C130.654 35.7068 132.593 37.645 134.984 37.645Z" fill="#575757"/>
<path d="M141.319 20.7505C142.952 20.7505 144.275 19.4268 144.275 17.7939C144.275 16.1611 142.952 14.8374 141.319 14.8374C139.686 14.8374 138.362 16.1611 138.362 17.7939C138.362 19.4268 139.686 20.7505 141.319 20.7505Z" fill="#575757"/>
<path d="M23.5466 19.4783C25.1795 19.4783 26.5032 18.1546 26.5032 16.5217C26.5032 14.8889 25.1795 13.5652 23.5466 13.5652C21.9138 13.5652 20.5901 14.8889 20.5901 16.5217C20.5901 18.1546 21.9138 19.4783 23.5466 19.4783Z" fill="#0E0E0E"/>
<path d="M5.49073 76.4976C8.52318 76.4976 10.9815 74.0393 10.9815 71.0068C10.9815 67.9744 8.52318 65.5161 5.49073 65.5161C2.45828 65.5161 0 67.9744 0 71.0068C0 74.0393 2.45828 76.4976 5.49073 76.4976Z" fill="#0E0E0E"/>
<path d="M85.5262 69.1928V89.5045C85.5262 91.2707 84.4985 93.037 83.0304 93.9201L67.4679 102.898C66.587 103.487 65.4125 103.782 64.3848 103.782V78.4656L84.7921 66.6907C85.2326 67.4266 85.5262 68.3097 85.5262 69.1928Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M64.3846 78.4656V103.782C63.3569 103.782 62.3292 103.487 61.3015 102.898L45.739 93.9201C44.1241 93.037 43.2432 91.4179 43.2432 89.5045V69.1928C43.2432 68.3097 43.5368 67.4266 43.9772 66.6907L64.3846 78.4656Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M84.7921 66.6908L64.3848 78.4657L43.8306 66.5437C44.271 65.8077 44.8583 65.0718 45.7392 64.6302L61.7421 55.3575C63.2102 54.4744 65.1188 54.4744 66.7338 55.2103L83.1772 64.7774C83.1772 64.7774 83.1772 64.7774 83.324 64.7774C83.9112 65.3662 84.4985 65.9549 84.7921 66.6908Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M127.809 69.1928V89.5045C127.809 91.2707 126.781 93.037 125.313 93.9201L109.751 102.898C108.87 103.487 107.695 103.782 106.668 103.782V78.4656L127.075 66.6907C127.515 67.4266 127.809 68.3097 127.809 69.1928Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M106.668 78.4656V103.782C105.64 103.782 104.612 103.487 103.584 102.898L88.022 93.9201C86.407 93.037 85.5261 91.4179 85.5261 89.5045V69.1928C85.5261 68.3097 85.8198 67.4266 86.2602 66.6907L106.668 78.4656Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M127.075 66.6908L106.667 78.4657L86.1133 66.5437C86.5537 65.8077 87.141 65.0718 88.0219 64.6302L104.025 55.3575C105.493 54.4744 107.402 54.4744 109.017 55.2103L125.46 64.7774C125.46 64.7774 125.46 64.7774 125.607 64.7774C126.194 65.3662 126.781 65.9549 127.075 66.6908Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M64.2381 30.7776V51.0892C64.2381 52.8554 63.2104 54.6217 61.7423 55.5048L46.1798 64.4831C45.2989 65.0719 44.1244 65.3662 43.0967 65.3662V40.0503L63.5041 28.2754C64.0913 29.0113 64.2381 29.8944 64.2381 30.7776Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M43.0965 40.0503V65.3662C42.0688 65.3662 41.0411 65.0719 40.0134 64.4831L24.4509 55.5048C22.836 54.6217 21.9551 53.0026 21.9551 51.0892V30.7776C21.9551 29.8944 22.2487 29.0113 22.6892 28.2754L43.0965 40.0503Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M63.6505 28.2751L43.2432 40.0499L22.689 28.1279C23.1294 27.392 23.7167 26.656 24.5976 26.2145L40.6005 16.9418C42.0686 16.0586 43.9772 16.0586 45.5922 16.7946L61.8887 26.5088C61.8887 26.5088 61.8887 26.5088 62.0356 26.5088C62.6228 26.9504 63.2101 27.5391 63.6505 28.2751Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M109.31 30.3358V53.0024C109.31 55.063 108.283 56.9765 106.374 58.0068L88.9029 68.1626C87.8752 68.7513 86.7006 69.0457 85.5261 69.0457V40.6388L108.576 27.3921C109.017 28.2752 109.31 29.3055 109.31 30.3358Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M85.5262 40.6388V58.5955V69.0457C84.3516 69.0457 83.1771 68.7513 82.1494 68.1626L74.515 63.747L64.5315 58.0067C62.7697 56.9764 61.742 55.063 61.5952 53.1496V30.1886C61.5952 29.1583 61.8888 28.128 62.3293 27.2449L85.5262 40.6388Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M108.429 27.3921L85.3792 40.6388L62.3291 27.2449C62.7695 26.3618 63.5036 25.6258 64.3845 25.1843L82.296 14.7341C84.0578 13.7038 86.1132 13.7038 87.875 14.5869L106.374 25.3315C106.374 25.3315 106.521 25.3315 106.521 25.4787C107.402 26.0674 107.989 26.6561 108.429 27.3921Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M108.207 12.7656C108.856 12.7656 109.383 13.2922 109.383 13.9418V20.9987C109.383 21.6483 108.856 22.1749 108.207 22.1749C107.557 22.1749 107.03 21.6483 107.03 20.9987V13.9418C107.03 13.2922 107.557 12.7656 108.207 12.7656Z" fill="#545454"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M123.936 13.1101C124.396 13.5694 124.396 14.3141 123.936 14.7735L115.311 23.3986C114.852 23.8579 114.107 23.8579 113.648 23.3986C113.188 22.9393 113.188 22.1946 113.648 21.7353L122.273 13.1101C122.732 12.6508 123.477 12.6508 123.936 13.1101Z" fill="#545454"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M114.087 29.6239C114.087 28.9743 114.614 28.4478 115.264 28.4478H123.105C123.754 28.4478 124.281 28.9743 124.281 29.6239C124.281 30.2735 123.754 30.8001 123.105 30.8001H115.264C114.614 30.8001 114.087 30.2735 114.087 29.6239Z" fill="#545454"/>
<path d="M96.9654 43.7842C96.7251 44.8023 96.3646 45.8804 95.944 46.7788C94.8025 48.9948 93 50.7317 90.777 51.8696C88.4939 53.0076 85.7902 53.4867 83.0866 52.8878C76.7179 51.5702 72.6324 45.3414 73.9542 38.9928C75.276 32.6442 81.4644 28.5116 87.833 29.8891C90.1161 30.3683 92.0988 31.5062 93.7811 33.0634C96.6049 35.8784 97.8065 39.9511 96.9654 43.7842Z" fill="#1E1E1E" stroke="#545454" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M89.1597 40.3732H86.4798V37.6933C86.4798 37.1573 86.063 36.6809 85.4674 36.6809C84.9315 36.6809 84.4551 37.0978 84.4551 37.6933V40.3732H81.7752C81.2392 40.3732 80.7628 40.79 80.7628 41.3855C80.7628 41.9811 81.1797 42.3979 81.7752 42.3979H84.4551V45.0778C84.4551 45.6137 84.8719 46.0902 85.4674 46.0902C86.0034 46.0902 86.4798 45.6733 86.4798 45.0778V42.3979H89.1597C89.6957 42.3979 90.1721 41.9811 90.1721 41.3855C90.1721 40.79 89.6957 40.3732 89.1597 40.3732Z" fill="#575757"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@@ -1,38 +0,0 @@
<svg width="123" height="126" viewBox="0 0 123 126" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M64.4237 111.867C91.7974 111.867 113.99 89.6745 113.99 62.2031C113.99 34.7317 91.6996 12.5396 64.4237 12.5396C37.0501 12.5396 14.8579 34.7317 14.8579 62.2031C14.8579 89.6745 37.0501 111.867 64.4237 111.867Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10"/>
<path d="M93.6338 12.5396C83.2422 33.022 67.9889 41.1703 54.0085 46.0363C50.764 47.1812 47.2331 48.183 43.8931 47.3243C40.5531 46.4656 37.6425 43.0785 38.5491 39.7392C39.3602 36.6383 43.3682 35.1117 46.2788 36.3521C49.2371 37.5924 50.8594 41.0272 50.5731 44.2235C50.2868 47.4197 48.3305 50.2343 45.8494 52.1903C43.3205 54.1462 40.3145 55.3388 37.2608 56.2452C30.533 58.2488 23.519 59.2029 16.9821 61.7313C10.3976 64.2597 4.14697 68.7917 1.9044 75.4227C0.282106 80.1456 0.950108 85.4886 3.04954 89.9729C5.14898 94.4572 8.72756 98.2259 12.7356 101.231C18.8907 105.811 26.4773 108.673 34.1116 108.483C41.7459 108.292 49.4757 104.952 54.1994 98.9415C58.9231 92.9306 60.3068 84.2005 56.9668 77.2833C54.6765 72.5127 49.4757 68.6009 44.2748 69.7458C40.1714 70.6999 37.1653 74.7072 36.7359 78.9529C36.3065 83.1987 38.1673 87.3968 40.9348 90.5453C43.7022 93.7416 47.3762 95.9837 51.1934 97.9397C56.776 100.754 63.3606 102.901 69.134 100.516C74.6212 98.2736 77.818 92.5967 79.9175 87.1106C82.0169 81.6244 83.5915 75.6613 87.4086 71.1293C91.9892 65.6909 99.7189 63.1625 106.638 64.7845C113.556 66.4064 119.377 72.1311 121.095 79.0006C122.431 84.3436 120.809 91.0224 115.799 93.36C111.886 95.2205 107.067 93.8847 103.679 91.2132C100.292 88.5417 98.0012 84.7253 95.8064 81.0043" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 4"/>
<path d="M55.1784 35.3718C63.5321 38.1997 72.5966 33.7202 75.4245 25.3665C78.2524 17.0128 73.7729 7.94831 65.4192 5.1204C57.0655 2.29248 48.001 6.77201 45.1731 15.1257C42.3451 23.4794 46.8247 32.5439 55.1784 35.3718Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M56.1375 32.5372C62.9259 34.8352 70.2919 31.1951 72.5899 24.4067C74.8879 17.6184 71.2478 10.2524 64.4594 7.95438C57.671 5.65637 50.3051 9.29651 48.007 16.0849C45.709 22.8732 49.3492 30.2392 56.1375 32.5372Z" fill="#181818"/>
<path d="M59.7561 21.8493C60.6415 22.149 61.6023 21.6742 61.902 20.7888C62.2018 19.9033 61.7269 18.9426 60.8415 18.6428C59.9561 18.3431 58.9953 18.8179 58.6956 19.7033C58.3958 20.5888 58.8706 21.5495 59.7561 21.8493Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M63.0122 12.23L60.6604 19.1773" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M62.083 20.2544L66.1857 20.4519" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M64.0744 10.7699C64.3349 10.6457 64.433 10.3214 64.2934 10.0454C64.1538 9.76951 63.8294 9.64651 63.5689 9.77072C63.3084 9.89492 63.2103 10.2193 63.3499 10.4952C63.4895 10.7711 63.8139 10.8941 64.0744 10.7699Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M57.3166 30.7045C57.5771 30.5803 57.6752 30.2559 57.5356 29.98C57.396 29.7041 57.0716 29.5811 56.8111 29.7053C56.5506 29.8295 56.4525 30.1539 56.5921 30.4298C56.7317 30.7057 57.0561 30.8287 57.3166 30.7045Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M50.7199 17.3822C50.9804 17.258 51.0785 16.9337 50.9389 16.6577C50.7993 16.3818 50.4749 16.2588 50.2144 16.383C49.9539 16.5072 49.8558 16.8316 49.9954 17.1075C50.135 17.3834 50.4594 17.5064 50.7199 17.3822Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M70.6545 24.14C70.915 24.0158 71.0131 23.6915 70.8735 23.4155C70.7339 23.1396 70.4095 23.0166 70.149 23.1408C69.8884 23.265 69.7904 23.5894 69.93 23.8653C70.0696 24.1412 70.394 24.2642 70.6545 24.14Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M12.3005 40.0978L14.9425 39.3015C15.1339 39.246 15.1339 38.9682 14.9425 38.9126L12.3005 38.1163C12.2326 38.0978 12.1832 38.0484 12.1647 37.9805L11.3684 35.3447C11.3128 35.1533 11.0351 35.1533 10.9795 35.3447L10.1832 37.9805C10.1647 38.0484 10.1153 38.0978 10.0474 38.1163L7.41159 38.9126C7.22023 38.9682 7.22023 39.246 7.41159 39.3015L10.0536 40.0978C10.1215 40.1163 10.1709 40.1657 10.1894 40.2336L10.9857 42.8756C11.0412 43.067 11.319 43.067 11.3746 42.8756L12.1709 40.2336C12.1832 40.1657 12.2326 40.1163 12.3005 40.0978Z" fill="#3c3c3c"/>
<path d="M96.5557 9.17336L99.2576 9.73368C99.4523 9.77613 99.5848 9.53199 99.4431 9.39188L97.5008 7.4318C97.45 7.38314 97.4301 7.31618 97.4463 7.24767L98.0036 4.55119C98.0461 4.35651 97.8019 4.22401 97.6618 4.3657L95.7047 6.30252C95.656 6.35336 95.5891 6.37321 95.5206 6.3571L92.8241 5.79972C92.6294 5.75727 92.4969 6.00142 92.6386 6.14152L94.5808 8.1016C94.6317 8.15026 94.6515 8.21722 94.6354 8.28573L94.0751 10.9876C94.0326 11.1823 94.2768 11.3148 94.4169 11.1731L96.377 9.23089C96.4202 9.17709 96.4872 9.15725 96.5557 9.17336Z" fill="#3c3c3c"/>
<path d="M94.8952 76.0129L96.9595 74.1819C97.1099 74.0512 96.9933 73.799 96.7963 73.8289L94.064 74.2143C93.9946 74.226 93.9291 74.2019 93.8838 74.148L92.0553 72.0895C91.9246 71.9391 91.6724 72.0556 91.7023 72.2526L92.085 74.9792C92.0967 75.0486 92.0726 75.1142 92.0187 75.1594L89.96 76.9879C89.8096 77.1186 89.9262 77.3708 90.1232 77.3409L92.8555 76.9555C92.9249 76.9438 92.9904 76.9679 93.0357 77.0218L94.8668 79.086C94.9975 79.2363 95.2497 79.1198 95.2198 78.9228L94.8345 76.1906C94.8172 76.1238 94.8413 76.0582 94.8952 76.0129Z" fill="#3c3c3c"/>
<path d="M73.8716 57.3258L76.1834 56.6291C76.3508 56.5805 76.3508 56.3374 76.1834 56.2888L73.8716 55.5921C73.8122 55.5759 73.769 55.5327 73.7528 55.4733L73.0561 53.1671C73.0074 52.9996 72.7644 52.9996 72.7158 53.1671L72.019 55.4733C72.0028 55.5327 71.9596 55.5759 71.9002 55.5921L69.5938 56.2888C69.4264 56.3374 69.4264 56.5805 69.5938 56.6291L71.9056 57.3258C71.965 57.342 72.0082 57.3852 72.0244 57.4446L72.7212 59.7563C72.7698 59.9237 73.0128 59.9237 73.0615 59.7563L73.7582 57.4446C73.769 57.3852 73.8122 57.342 73.8716 57.3258Z" fill="#3c3c3c"/>
<path d="M29.5118 89.7614L31.8235 88.9651C31.991 88.9095 31.991 88.6317 31.8235 88.5762L29.5118 87.7799C29.4524 87.7614 29.4092 87.712 29.393 87.6441L28.6962 85.0083C28.6476 84.8169 28.4045 84.8169 28.3559 85.0083L27.6591 87.6441C27.6429 87.712 27.5997 87.7614 27.5403 87.7799L25.234 88.5762C25.0665 88.6317 25.0665 88.9095 25.234 88.9651L27.5457 89.7614C27.6051 89.7799 27.6483 89.8293 27.6646 89.8972L28.3613 92.5392C28.4099 92.7305 28.653 92.7305 28.7016 92.5392L29.3984 89.8972C29.4092 89.8293 29.4524 89.7799 29.5118 89.7614Z" fill="#3c3c3c"/>
<path d="M25.6922 50.6715L27.3434 50.1738C27.463 50.1391 27.463 49.9655 27.3434 49.9308L25.6922 49.4331C25.6497 49.4216 25.6188 49.3907 25.6073 49.3483L25.1096 47.701C25.0749 47.5814 24.9013 47.5814 24.8665 47.701L24.3688 49.3483C24.3573 49.3907 24.3264 49.4216 24.284 49.4331L22.6366 49.9308C22.517 49.9655 22.517 50.1391 22.6366 50.1738L24.2878 50.6715C24.3303 50.6831 24.3611 50.7139 24.3727 50.7564L24.8704 52.4075C24.9051 52.5271 25.0787 52.5271 25.1134 52.4075L25.6111 50.7564C25.6188 50.7139 25.6497 50.6831 25.6922 50.6715Z" fill="#3c3c3c"/>
<path d="M120.831 51.7623L122.482 51.2647C122.602 51.2299 122.602 51.0563 122.482 51.0216L120.831 50.524C120.788 50.5124 120.758 50.4815 120.746 50.4391L120.248 48.7918C120.214 48.6723 120.04 48.6723 120.005 48.7918L119.508 50.4391C119.496 50.4815 119.465 50.5124 119.423 50.524L117.775 51.0216C117.656 51.0563 117.656 51.2299 117.775 51.2647L119.426 51.7623C119.469 51.7739 119.5 51.8047 119.511 51.8472L120.009 53.4983C120.044 53.6179 120.217 53.6179 120.252 53.4983L120.75 51.8472C120.758 51.8047 120.788 51.7739 120.831 51.7623Z" fill="#3c3c3c"/>
<path d="M37.8972 27.6539L39.5485 27.1563C39.6681 27.1215 39.6681 26.9479 39.5485 26.9132L37.8972 26.4156C37.8548 26.404 37.8239 26.3731 37.8124 26.3307L37.3147 24.6834C37.2799 24.5639 37.1063 24.5639 37.0716 24.6834L36.5739 26.3307C36.5623 26.3731 36.5315 26.404 36.489 26.4156L34.8417 26.9132C34.7221 26.9479 34.7221 27.1215 34.8417 27.1563L36.4929 27.6539C36.5353 27.6655 36.5662 27.6963 36.5778 27.7388L37.0755 29.3899C37.1102 29.5095 37.2838 29.5095 37.3185 29.3899L37.8162 27.7388C37.8239 27.6963 37.8548 27.6655 37.8972 27.6539Z" fill="#3c3c3c"/>
<path d="M13.3069 83.1031L14.9581 82.6055C15.0777 82.5708 15.0777 82.3972 14.9581 82.3624L13.3069 81.8648C13.2645 81.8532 13.2336 81.8224 13.222 81.7799L12.7243 80.1327C12.6896 80.0131 12.516 80.0131 12.4813 80.1327L11.9836 81.7799C11.972 81.8224 11.9411 81.8532 11.8987 81.8648L10.2513 82.3624C10.1317 82.3972 10.1317 82.5708 10.2513 82.6055L11.9026 83.1031C11.945 83.1147 11.9759 83.1456 11.9874 83.188L12.4851 84.8391C12.5199 84.9587 12.6935 84.9587 12.7282 84.8391L13.2259 83.188C13.2336 83.1456 13.2645 83.1147 13.3069 83.1031Z" fill="#3c3c3c"/>
<path d="M58.6922 57.5907L61.979 62.276C62.9181 63.6146 63.5603 64.7335 64.0498 66.2448C65.8824 71.0942 65.1988 76.4246 62.6771 80.7618C62.209 81.5182 61.9707 82.3987 62.0863 83.1736C62.1063 84.0157 62.356 84.9818 62.8927 85.7468C63.9659 87.2767 65.7655 88.0116 67.5737 87.7419C73.3808 86.6643 79.3534 88.4667 83.4436 92.8737C83.9417 93.3803 84.3442 93.954 84.8137 94.6233L88.1005 99.3086C88.5029 99.8823 88.8098 100.523 89.1452 101.001L61.0335 120.722C60.6025 120.311 60.133 119.641 59.7976 119.163L56.5108 114.478C56.1754 114 55.9357 113.455 55.6003 112.977C55.4661 112.786 55.3605 112.432 55.1593 112.145C52.5704 106.827 52.88 100.76 55.8413 95.8296C56.7775 94.3168 56.6319 92.2789 55.5586 90.749C55.0891 90.0797 54.3612 89.4489 53.5763 89.1435C53.5092 89.0479 53.4136 89.1149 53.4136 89.1149C52.6287 88.8095 51.7867 88.8295 50.9447 88.8495C47.0414 89.5903 43.0411 88.9724 39.4319 87.0814C38.5128 86.5848 37.5937 86.0882 36.7988 85.3618C36.569 85.2376 36.3678 84.9507 36.2336 84.7595C35.3716 83.9375 34.4425 83.0198 33.7047 81.968L30.4179 77.2828C30.3508 77.1872 30.2167 76.9959 30.0825 76.8047L58.3854 56.9499C58.491 57.3038 58.5581 57.3995 58.6922 57.5907Z" fill="#181818" stroke="#3c3c3c" stroke-width="1.9781" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M74.5356 102.547C74.5356 102.547 64.4216 111.925 63.7422 111.974C63.2927 112.146 59.3138 113.796 56.7792 114.861L56.1085 113.905C55.7731 113.426 55.4377 112.948 55.1309 112.308C55.255 112.078 55.3121 111.752 55.3692 111.427C56.6664 107.378 60.698 101.126 69.1952 101.443L74.5356 102.547Z" fill="#181818" stroke="#3c3c3c" stroke-width="1.9781" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M58.6922 57.5907L61.979 62.276C62.9181 63.6146 63.5603 64.7335 64.0498 66.2448C65.8824 71.0942 65.1988 76.4246 62.6771 80.7618C62.209 81.5182 61.9707 82.3987 62.0863 83.1736C62.1063 84.0157 62.356 84.9818 62.8927 85.7468C63.9659 87.2767 65.7655 88.0116 67.5737 87.7419C73.3808 86.6643 79.3534 88.4667 83.4436 92.8737C83.9417 93.3803 84.3442 93.954 84.8137 94.6233L88.1005 99.3086C88.5029 99.8823 88.8098 100.523 89.1452 101.001L61.0335 120.722C60.6025 120.311 60.133 119.641 59.7976 119.163L56.5108 114.478C56.1754 114 55.9357 113.455 55.6003 112.977C55.4661 112.786 55.3605 112.432 55.1593 112.145C52.5704 106.827 52.88 100.76 55.8413 95.8296C56.7775 94.3168 56.6319 92.2789 55.5586 90.749C55.0891 90.0797 54.3612 89.4489 53.5763 89.1435C53.5092 89.0479 53.4136 89.1149 53.4136 89.1149C52.6287 88.8095 51.7867 88.8295 50.9447 88.8495C47.0414 89.5903 43.0411 88.9724 39.4319 87.0814C38.5128 86.5848 37.5937 86.0882 36.7988 85.3618C36.569 85.2376 36.3678 84.9507 36.2336 84.7595C35.3716 83.9375 34.4425 83.0198 33.7047 81.968L30.4179 77.2828C30.3508 77.1872 30.2167 76.9959 30.0825 76.8047L58.3854 56.9499C58.491 57.3038 58.5581 57.3995 58.6922 57.5907Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M56.5304 77.084L45.3631 85.774L39.5946 87.1099C38.6755 86.6132 37.7565 86.1166 36.9615 85.3902C36.7318 85.266 36.5305 84.9792 36.3964 84.788C39.4818 79.6274 42.3203 76.352 50.5763 74.6979C51.9635 74.4381 53.2551 74.2454 54.805 74.0142C68.6011 72.3258 56.5304 77.084 56.5304 77.084Z" fill="#181818" stroke="#525252" stroke-width="1.9781" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M64.1171 66.3406C65.9496 71.19 65.266 76.5204 62.7443 80.8576C62.2762 81.6139 62.0379 82.4945 62.1535 83.2694L62.0579 83.3365C60.528 84.4097 60.1284 86.6875 61.2017 88.2174L71.1961 102.464L68.4232 104.41L58.3617 90.067C57.2885 88.5371 55.1063 88.0704 53.4808 89.2107C52.6959 88.9053 51.8539 88.9253 51.0119 88.9453C47.1086 89.6861 43.1084 89.0682 39.4991 87.1772C38.58 86.6806 37.6609 86.184 36.866 85.4576C45.3803 83.7649 48.2459 78.9011 50.4523 74.9279C51.1687 73.712 51.7895 72.5631 52.4773 71.5099C55.2003 67.4596 59.2177 66.0681 64.1171 66.3406Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M84.7178 94.6907L88.0046 99.376C88.407 99.9497 88.7139 100.59 89.0493 101.069L63.9974 118.643L59.3949 118.59L56.4435 114.383C56.1081 113.905 55.7727 113.427 55.5329 112.881C55.3988 112.69 55.2932 112.336 55.0919 112.049C55.1876 111.982 55.2546 112.078 55.3503 112.011C55.513 112.039 55.6086 111.972 55.7042 111.905C65.022 109.934 67.5137 104.334 69.8727 99.9684C70.2452 99.2791 70.7133 98.5227 71.1528 97.929C74.0385 93.9073 78.2186 92.5443 83.2807 92.8454C83.7502 93.5147 84.2483 94.0214 84.7178 94.6907Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M60.9889 57.4062L29.6263 79.4074C28.957 79.8769 27.9808 79.7057 27.5783 79.132L25.4319 76.0722C24.9623 75.4029 25.2007 74.5223 25.87 74.0528L57.3282 51.9845C57.9976 51.515 58.9067 51.5906 59.3762 52.2599L61.4556 55.2241C61.7339 56.0275 61.6583 56.9366 60.9889 57.4062Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M92.6489 102.538L61.2863 124.539C60.6169 125.009 59.7078 124.933 59.2383 124.264L57.1589 121.3C56.6894 120.63 56.9277 119.75 57.597 119.28L89.0553 97.212C89.7246 96.7425 90.6337 96.8181 91.1032 97.4875L93.1826 100.452C93.4609 101.255 93.3182 102.069 92.6489 102.538Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M111.308 35.6053L109.875 38.2699C109.466 39.0312 109.09 39.6002 108.492 40.1902C106.636 42.2069 103.956 43.1493 101.309 42.9878C100.841 42.9462 100.368 43.0427 100.03 43.2813C99.6367 43.4906 99.2396 43.8379 99.0057 44.2729C98.5378 45.143 98.6179 46.1675 99.1745 46.9576C101.062 49.4447 101.628 52.6936 100.518 55.6717C100.397 56.0272 100.221 56.3535 100.017 56.7342L98.5837 59.3988C98.4083 59.7251 98.1785 60.0221 98.0323 60.294L82.0444 51.6971C82.1362 51.396 82.3409 51.0153 82.4871 50.7434L83.9199 48.0788C84.0661 47.8069 84.2667 47.5642 84.4129 47.2923C84.4714 47.1836 84.6135 47.0497 84.7012 46.8865C86.5983 44.4014 89.5374 43.1075 92.5693 43.3358C93.5061 43.4189 94.4341 42.8663 94.902 41.9962C95.1067 41.6156 95.2319 41.122 95.1898 40.6788C95.219 40.6244 95.1646 40.5952 95.1646 40.5952C95.1226 40.1519 94.9132 39.759 94.7039 39.366C93.4275 37.6982 92.7698 35.662 92.8062 33.5084C92.8226 32.9563 92.839 32.4043 92.9934 31.8564C92.9975 31.7184 93.0852 31.5553 93.1437 31.4465C93.3274 30.8442 93.5402 30.1876 93.8619 29.5894L95.2947 26.9247C95.324 26.8704 95.3824 26.7616 95.4409 26.6528L111.538 35.3082C111.395 35.4421 111.366 35.4965 111.308 35.6053Z" fill="#181818" stroke="#3c3c3c" stroke-width="1.9781" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M93.8341 53.7603C93.8341 53.7603 87.0037 51.2092 86.8196 50.8998C86.6313 50.7285 84.9075 49.2407 83.803 48.2963L84.0954 47.7525C84.2416 47.4806 84.3878 47.2087 84.6176 46.9116C84.7556 46.9157 84.9229 46.8655 85.0901 46.8152C87.3104 46.4668 91.2207 46.8869 93.0881 50.9756L93.8341 53.7603Z" fill="#181818" stroke="#3c3c3c" stroke-width="1.9781" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M111.308 35.6053L109.875 38.2699C109.466 39.0312 109.09 39.6002 108.492 40.1902C106.636 42.2069 103.956 43.1493 101.309 42.9878C100.841 42.9462 100.368 43.0427 100.03 43.2813C99.6367 43.4906 99.2396 43.8379 99.0057 44.2729C98.5378 45.143 98.6179 46.1675 99.1745 46.9576C101.062 49.4447 101.628 52.6936 100.518 55.6717C100.397 56.0272 100.221 56.3535 100.017 56.7342L98.5837 59.3988C98.4083 59.7251 98.1785 60.0221 98.0323 60.294L82.0444 51.6971C82.1362 51.396 82.3409 51.0153 82.4871 50.7434L83.9199 48.0788C84.0661 47.8069 84.2667 47.5642 84.4129 47.2923C84.4714 47.1836 84.6135 47.0497 84.7012 46.8865C86.5983 44.4014 89.5374 43.1075 92.5693 43.3358C93.5061 43.4189 94.4341 42.8663 94.902 41.9962C95.1067 41.6156 95.2319 41.122 95.1898 40.6788C95.219 40.6244 95.1646 40.5952 95.1646 40.5952C95.1226 40.1519 94.9132 39.759 94.7039 39.366C93.4275 37.6982 92.7698 35.662 92.8062 33.5084C92.8226 32.9563 92.839 32.4043 92.9934 31.8564C92.9975 31.7184 93.0852 31.5553 93.1437 31.4465C93.3274 30.8442 93.5402 30.1876 93.8619 29.5894L95.2947 26.9247C95.324 26.8704 95.3824 26.7616 95.4409 26.6528L111.538 35.3082C111.395 35.4421 111.366 35.4965 111.308 35.6053Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M101.587 39.2113L94.8318 35.9994L92.8315 33.5918C92.8479 33.0398 92.8643 32.4878 93.0187 31.9399C93.0228 31.8019 93.1105 31.6388 93.169 31.53C96.339 31.7623 98.5598 32.3256 101.301 35.8325C101.753 36.4261 102.151 36.9904 102.628 37.6676C106.7 43.7832 101.587 39.2113 101.587 39.2113Z" fill="#181818" stroke="#525252" stroke-width="1.9781" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M108.462 40.2444C106.607 42.2611 103.927 43.2036 101.28 43.042C100.811 43.0005 100.339 43.0969 100 43.3355L99.9459 43.3063C99.0758 42.8384 97.9051 43.1904 97.4372 44.0605L93.0803 52.1632L91.5033 51.3152L95.8894 43.1581C96.3573 42.288 96.0597 41.1465 95.1353 40.6494C95.0932 40.2062 94.8839 39.8132 94.6745 39.4202C93.3981 37.7524 92.7404 35.7163 92.7768 33.5626C92.7932 33.0106 92.8096 32.4586 92.964 31.9107C95.7847 35.5305 98.7622 35.7295 101.163 35.8285C101.907 35.8782 102.597 35.8987 103.258 35.9736C105.817 36.2983 107.428 37.8656 108.462 40.2444Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M99.9622 56.7048L98.5294 59.3694C98.354 59.6957 98.1241 59.9928 97.9779 60.2647L83.7302 52.6035L82.6626 50.417L83.9492 48.0243C84.0954 47.7524 84.2416 47.4805 84.4422 47.2378C84.5007 47.129 84.6428 46.9951 84.7305 46.832C84.7849 46.8612 84.7556 46.9156 84.81 46.9449C84.8352 47.0285 84.8895 47.0577 84.9439 47.087C88.0867 51.0202 91.3234 50.8678 93.9454 50.9457C94.3594 50.958 94.8278 50.9996 95.2126 51.0663C97.7972 51.4746 99.4332 53.1255 100.493 55.5879C100.288 55.9686 100.167 56.3241 99.9622 56.7048Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M111.939 36.6459L94.1026 27.0548C93.722 26.8502 93.5711 26.3484 93.7466 26.0221L94.6823 24.282C94.887 23.9013 95.3595 23.8048 95.7402 24.0095L113.631 33.6299C114.012 33.8346 114.192 34.2819 113.987 34.6626L113.081 36.3484C112.767 36.6706 112.32 36.8506 111.939 36.6459Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M98.1382 62.3137L80.3013 52.7226C79.9207 52.5179 79.7406 52.0706 79.9453 51.6899L80.8517 50.0041C81.0564 49.6235 81.5289 49.527 81.9096 49.7317L99.8008 59.352C100.181 59.5567 100.362 60.0041 100.157 60.3847L99.2504 62.0705C98.9369 62.3927 98.5188 62.5184 98.1382 62.3137Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,22 +1,22 @@
<svg width="144" height="118" viewBox="0 0 144 118" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M71.915 116.003C103.558 116.003 129.168 90.2405 129.168 58.455C129.168 26.6694 103.558 1 71.915 1C40.3653 1 14.6616 26.7621 14.6616 58.5476C14.6616 90.3332 40.3653 116.003 71.915 116.003Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M6.40267 71.9221C8.53691 71.9221 10.2072 70.2541 10.2072 68.1227C10.2072 65.9913 8.53691 64.3232 6.40267 64.3232C4.26842 64.3232 2.59814 65.9913 2.59814 68.1227C2.59814 70.2541 4.36122 71.9221 6.40267 71.9221Z" fill="#181818"/>
<path d="M2.59821 59.5972C3.99011 59.5972 5.19642 58.3925 5.19642 57.0025C5.19642 55.6124 3.99011 54.4077 2.59821 54.4077C1.20631 54.4077 0 55.6124 0 57.0025C0 58.4852 1.20631 59.5972 2.59821 59.5972Z" fill="#181818"/>
<path d="M138.726 81.467C141.417 81.467 143.551 79.3356 143.551 76.6481C143.551 73.9607 141.417 71.8293 138.726 71.8293C136.035 71.8293 133.9 73.9607 133.9 76.6481C133.9 79.3356 136.035 81.467 138.726 81.467Z" fill="#181818"/>
<circle cx="70.6176" cy="28.6049" r="2.23823" fill="#4D4D4D"/>
<path d="M113.372 106.197H29.3551C26.224 106.197 23.6147 103.61 23.6147 100.505V15.6447C23.6147 12.5401 26.224 9.95288 29.3551 9.95288H113.372C116.503 9.95288 119.113 12.5401 119.113 15.6447V100.505C119.113 103.61 116.503 106.197 113.372 106.197Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M70.8772 42.0341C77.4699 42.0341 82.8144 36.6896 82.8144 30.0969C82.8144 23.5041 77.4699 18.1597 70.8772 18.1597C64.2844 18.1597 58.9399 23.5041 58.9399 30.0969C58.9399 36.6896 64.2844 42.0341 70.8772 42.0341Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M70.8773 30.8431C73.3496 30.8431 75.3538 28.8389 75.3538 26.3666C75.3538 23.8943 73.3496 21.8901 70.8773 21.8901C68.4051 21.8901 66.4009 23.8943 66.4009 26.3666C66.4009 28.8389 68.4051 30.8431 70.8773 30.8431Z" fill="#525252"/>
<path d="M79.8302 38.3074C77.6656 40.6044 74.6218 42.0342 71.2503 42.0342C67.8788 42.0342 64.8349 40.6044 62.6704 38.3074C63.8656 34.8375 67.2553 32.3352 71.2503 32.3352C75.2452 32.3352 78.635 34.8375 79.8302 38.3074Z" fill="#525252"/>
<path d="M103.388 60.686H38.5928C37.8184 60.686 37.0439 59.8252 37.0439 58.9643V51.2166C37.0439 50.3557 37.8184 49.4949 38.5928 49.4949H103.388C104.162 49.4949 104.937 50.3557 104.937 51.2166V58.9643C104.937 59.8252 104.162 60.686 103.388 60.686Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M103.388 77.0998H38.5928C37.8184 77.0998 37.0439 76.239 37.0439 75.3781V67.6304C37.0439 66.7695 37.8184 65.9087 38.5928 65.9087H103.388C104.162 65.9087 104.937 66.7695 104.937 67.6304V75.3781C104.937 76.239 104.162 77.0998 103.388 77.0998Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M103.325 83.8145H79.6896C78.8839 83.8145 78.0781 84.4877 78.0781 85.8342V93.2398C78.0781 94.2497 78.6153 95.2595 79.6896 95.2595H103.325C104.131 95.2595 104.937 94.5863 104.937 93.2398V85.8342C104.937 84.7682 104.131 83.8145 103.325 83.8145Z" fill="#181818" stroke="#525252" stroke-width="2" stroke-miterlimit="10"/>
<path d="M97.9375 88.291H85.8239C85.3084 88.291 84.793 88.9141 84.793 89.5371C84.793 90.1601 85.3084 90.7832 85.8239 90.7832H97.9375C98.453 90.7832 98.9684 90.1601 98.9684 89.5371C98.9684 88.6025 98.453 88.291 97.9375 88.291Z" fill="#737373"/>
<rect x="41.5205" y="53.9714" width="29.097" height="2.23823" rx="1.11911" fill="#737373"/>
<rect x="41.5205" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#737373"/>
<rect x="45.9971" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#737373"/>
<rect x="50.4736" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#737373"/>
<rect x="54.9497" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#737373"/>
<rect x="58.6802" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#737373"/>
<rect x="63.1567" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#737373"/>
<svg width="144" height="117" viewBox="0 0 144 117" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M71.9152 116.003C103.558 116.003 129.169 90.2405 129.169 58.455C129.169 26.6694 103.558 1 71.9152 1C40.3655 1 14.6617 26.7621 14.6617 58.5476C14.6617 90.3332 40.3655 116.003 71.9152 116.003Z" fill="#0E0E0E" stroke="#3D3D3D" stroke-width="2" stroke-miterlimit="10"/>
<path d="M6.40267 71.9221C8.53691 71.9221 10.2072 70.2541 10.2072 68.1227C10.2072 65.9913 8.53691 64.3232 6.40267 64.3232C4.26842 64.3232 2.59814 65.9913 2.59814 68.1227C2.59814 70.2541 4.36122 71.9221 6.40267 71.9221Z" fill="#0E0E0E"/>
<path d="M2.59821 59.5972C3.99011 59.5972 5.19642 58.3925 5.19642 57.0025C5.19642 55.6124 3.99011 54.4077 2.59821 54.4077C1.20631 54.4077 0 55.6124 0 57.0025C0 58.4852 1.20631 59.5972 2.59821 59.5972Z" fill="#0E0E0E"/>
<path d="M138.726 81.467C141.417 81.467 143.551 79.3356 143.551 76.6481C143.551 73.9607 141.417 71.8293 138.726 71.8293C136.035 71.8293 133.901 73.9607 133.901 76.6481C133.901 79.3356 136.035 81.467 138.726 81.467Z" fill="#0E0E0E"/>
<circle cx="70.6175" cy="28.6049" r="2.23823" fill="#4D4D4D"/>
<path d="M113.372 106.197H29.3551C26.224 106.197 23.6147 103.61 23.6147 100.505V15.6447C23.6147 12.5401 26.224 9.95288 29.3551 9.95288H113.372C116.503 9.95288 119.113 12.5401 119.113 15.6447V100.505C119.113 103.61 116.503 106.197 113.372 106.197Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M70.8772 42.0341C77.4699 42.0341 82.8144 36.6896 82.8144 30.0969C82.8144 23.5041 77.4699 18.1597 70.8772 18.1597C64.2844 18.1597 58.9399 23.5041 58.9399 30.0969C58.9399 36.6896 64.2844 42.0341 70.8772 42.0341Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M70.8772 30.8431C73.3495 30.8431 75.3537 28.8389 75.3537 26.3666C75.3537 23.8943 73.3495 21.8901 70.8772 21.8901C68.4049 21.8901 66.4008 23.8943 66.4008 26.3666C66.4008 28.8389 68.4049 30.8431 70.8772 30.8431Z" fill="#575757"/>
<path d="M79.8302 38.3074C77.6656 40.6044 74.6218 42.0342 71.2503 42.0342C67.8788 42.0342 64.8349 40.6044 62.6704 38.3074C63.8656 34.8375 67.2553 32.3352 71.2503 32.3352C75.2452 32.3352 78.635 34.8375 79.8302 38.3074Z" fill="#575757"/>
<path d="M103.388 60.686H38.593C37.8185 60.686 37.0441 59.8252 37.0441 58.9643V51.2166C37.0441 50.3557 37.8185 49.4949 38.593 49.4949H103.388C104.163 49.4949 104.937 50.3557 104.937 51.2166V58.9643C104.937 59.8252 104.163 60.686 103.388 60.686Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M103.388 77.0998H38.593C37.8185 77.0998 37.0441 76.239 37.0441 75.3781V67.6304C37.0441 66.7695 37.8185 65.9087 38.593 65.9087H103.388C104.163 65.9087 104.937 66.7695 104.937 67.6304V75.3781C104.937 76.239 104.163 77.0998 103.388 77.0998Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M103.325 83.8145H79.6898C78.884 83.8145 78.0782 84.4877 78.0782 85.8342V93.2398C78.0782 94.2497 78.6154 95.2595 79.6898 95.2595H103.325C104.131 95.2595 104.937 94.5863 104.937 93.2398V85.8342C104.937 84.7682 104.131 83.8145 103.325 83.8145Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M97.9375 88.291H85.8239C85.3084 88.291 84.793 88.9141 84.793 89.5371C84.793 90.1601 85.3084 90.7832 85.8239 90.7832H97.9375C98.453 90.7832 98.9684 90.1601 98.9684 89.5371C98.9684 88.6025 98.453 88.291 97.9375 88.291Z" fill="#545454"/>
<rect x="41.5205" y="53.9714" width="29.097" height="2.23823" rx="1.11911" fill="#545454"/>
<rect x="41.5205" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#545454"/>
<rect x="45.9971" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#545454"/>
<rect x="50.4734" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#545454"/>
<rect x="54.9498" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#545454"/>
<rect x="58.6803" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#545454"/>
<rect x="63.1567" y="70.385" width="2.23823" height="2.23823" rx="1.11911" fill="#545454"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,37 +1,38 @@
<svg width="145" height="120" viewBox="0 0 145 120" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.648 59.7265C130.648 72.9542 126.219 85.3837 118.837 95.1904C114.748 100.55 109.865 105.225 104.187 108.874C95.1015 114.918 84.0856 118.339 72.3883 118.339C40.2491 118.453 14.1289 92.2256 14.1289 59.7265C14.1289 27.3414 40.1355 1 72.3883 1C84.0856 1 94.9879 4.42096 104.187 10.4647C109.865 14.1137 114.748 18.789 118.837 24.1485C126.219 34.0693 130.648 46.3847 130.648 59.7265Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10"/>
<path d="M28.0195 51.8743H27.3162C26.7887 51.8743 26.437 51.7102 26.437 51.464C26.437 51.2178 26.7887 51.0537 27.3162 51.0537H28.0195C28.547 51.0537 28.8987 51.2178 28.8987 51.464C28.8987 51.7102 28.547 51.8743 28.0195 51.8743Z" fill="#2D5887"/>
<path d="M28.0195 55.1565H27.3162C26.7887 55.1565 26.437 54.9924 26.437 54.7462C26.437 54.5 26.7887 54.3359 27.3162 54.3359H28.0195C28.547 54.3359 28.8987 54.5 28.8987 54.7462C28.8987 54.9924 28.547 55.1565 28.0195 55.1565Z" fill="#2D5887"/>
<path d="M28.0195 58.4387H27.3162C26.7887 58.4387 26.437 58.2746 26.437 58.0284C26.437 57.7823 26.7887 57.6182 27.3162 57.6182H28.0195C28.547 57.6182 28.8987 57.7823 28.8987 58.0284C28.8987 58.2746 28.547 58.4387 28.0195 58.4387Z" fill="#2D5887"/>
<path d="M28.0195 61.7209H27.3162C26.7887 61.7209 26.437 61.5568 26.437 61.3107C26.437 61.0645 26.7887 60.9004 27.3162 60.9004H28.0195C28.547 60.9004 28.8987 61.0645 28.8987 61.3107C28.8987 61.5568 28.547 61.7209 28.0195 61.7209Z" fill="#2D5887"/>
<path d="M60.1189 51.8743H48.553C48.0841 51.8743 47.7715 51.7102 47.7715 51.464C47.7715 51.2178 48.0841 51.0537 48.553 51.0537H60.1189C60.5878 51.0537 60.9004 51.2178 60.9004 51.464C60.9004 51.7102 60.5878 51.8743 60.1189 51.8743Z" fill="white"/>
<path d="M84.7621 61.7209H48.5264C48.0734 61.7209 47.7715 61.5568 47.7715 61.3107C47.7715 61.0645 48.0734 60.9004 48.5264 60.9004H84.7621C85.215 60.9004 85.517 61.0645 85.517 61.3107C85.517 61.5568 85.215 61.7209 84.7621 61.7209Z" fill="url(#paint0_linear)"/>
<path d="M60.1642 55.1565H44.4053C43.9635 55.1565 43.6689 54.9924 43.6689 54.7462C43.6689 54.5 43.9635 54.3359 44.4053 54.3359H60.1642C60.606 54.3359 60.9006 54.5 60.9006 54.7462C60.9006 54.9924 60.606 55.1565 60.1642 55.1565Z" fill="#2D5887"/>
<path d="M72.4741 58.4387H44.4038C43.9629 58.4387 43.6689 58.2746 43.6689 58.0284C43.6689 57.7823 43.9629 57.6182 44.4038 57.6182H72.4741C72.915 57.6182 73.2089 57.7823 73.2089 58.0284C73.0619 58.2746 72.768 58.4387 72.4741 58.4387Z" fill="#2D5887"/>
<path d="M84.7181 58.4387H74.0082C73.5287 58.4387 73.209 58.2746 73.209 58.0284C73.209 57.7823 73.5287 57.6182 74.0082 57.6182H84.7181C85.1976 57.6182 85.5173 57.7823 85.5173 58.0284C85.5173 58.2746 85.1976 58.4387 84.7181 58.4387Z" fill="white"/>
<path d="M91.3056 58.4387H87.1141C86.6484 58.4387 86.3379 58.2746 86.3379 58.0284C86.3379 57.7823 86.6484 57.6182 87.1141 57.6182H91.3056C91.7713 57.6182 92.0818 57.7823 92.0818 58.0284C92.0818 58.2746 91.7713 58.4387 91.3056 58.4387Z" fill="url(#paint1_linear)"/>
<path d="M1 103.569H138.853" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M97.8252 51.0537V103.569H23.3713C20.4342 103.569 18.2314 101.209 18.2314 98.4062V51.0537H97.8252Z" fill="#181818" stroke="#525252" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M127.366 51.0537V98.4062C127.366 101.356 124.917 103.569 122.009 103.569H97.8257V51.0537H127.366Z" fill="#DAE1ED"/>
<path d="M127.366 51.0537V98.4062C127.366 101.356 124.917 103.569 122.009 103.569H97.8257V51.0537H127.366Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10"/>
<path d="M46.9512 51.0537L62.6074 26.437H142.956L126.857 51.0537H46.9512Z" fill="#C5CDDB"/>
<path d="M46.9512 51.0537L62.6074 26.437H142.956L126.857 51.0537H46.9512Z" fill="#181818" stroke="#3c3c3c" stroke-width="2" stroke-miterlimit="10"/>
<path opacity="0.3" d="M127.366 51.1975V78.9526H105.826C103.826 78.9526 102.441 77.6583 102.134 75.7888L97.8257 51.0537L127.366 51.1975Z" fill="url(#paint2_linear)"/>
<path d="M126.747 51.0537H97.8257L112.511 73.3119C113.56 74.7859 115.208 75.6704 116.857 75.6704H139.334C140.833 75.6704 141.882 73.9015 140.982 72.7223L126.747 51.0537Z" fill="#181818" stroke="#525252" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M97.8254 51.0537L82.0597 26.437H1L17.3607 51.0537H97.8254Z" fill="#181818" stroke="#525252" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M57.9543 81.4143H26.9218C26.1967 81.4143 25.6167 80.8673 25.6167 80.1835C25.6167 79.4997 26.1967 78.9526 26.9218 78.9526H57.9543C58.6794 78.9526 59.2594 79.4997 59.2594 80.1835C59.1144 80.8673 58.6794 81.4143 57.9543 81.4143Z" fill="#3c3c3c"/>
<path d="M57.9543 87.1582H26.9218C26.1967 87.1582 25.6167 86.7935 25.6167 86.3376C25.6167 85.8818 26.1967 85.5171 26.9218 85.5171H57.9543C58.6794 85.5171 59.2594 85.8818 59.2594 86.3376C59.1144 86.7935 58.6794 87.1582 57.9543 87.1582Z" fill="#3c3c3c"/>
<path d="M41.5228 93.7227H26.9422C26.2058 93.7227 25.6167 93.358 25.6167 92.9021C25.6167 92.4462 26.2058 92.0815 26.9422 92.0815H41.5228C42.2592 92.0815 42.8483 92.4462 42.8483 92.9021C42.7011 93.358 42.1119 93.7227 41.5228 93.7227Z" fill="#3c3c3c"/>
<path d="M130.648 59.7265C130.648 72.9542 126.219 85.3837 118.837 95.1904C114.748 100.55 109.865 105.225 104.187 108.874C95.1015 114.918 84.0856 118.339 72.3883 118.339C40.2491 118.453 14.1289 92.2256 14.1289 59.7265C14.1289 27.3414 40.1355 1 72.3883 1C84.0856 1 94.9879 4.42096 104.187 10.4647C109.865 14.1137 114.748 18.789 118.837 24.1485C126.219 34.0693 130.648 46.3847 130.648 59.7265Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M28.0198 51.8743H27.3164C26.7889 51.8743 26.4373 51.7102 26.4373 51.464C26.4373 51.2178 26.7889 51.0537 27.3164 51.0537H28.0198C28.5473 51.0537 28.8989 51.2178 28.8989 51.464C28.8989 51.7102 28.5473 51.8743 28.0198 51.8743Z" fill="#7F7F7F"/>
<path d="M28.0198 55.1565H27.3164C26.7889 55.1565 26.4373 54.9924 26.4373 54.7462C26.4373 54.5 26.7889 54.3359 27.3164 54.3359H28.0198C28.5473 54.3359 28.8989 54.5 28.8989 54.7462C28.8989 54.9924 28.5473 55.1565 28.0198 55.1565Z" fill="#7F7F7F"/>
<path d="M28.0198 58.4387H27.3164C26.7889 58.4387 26.4373 58.2746 26.4373 58.0284C26.4373 57.7823 26.7889 57.6182 27.3164 57.6182H28.0198C28.5473 57.6182 28.8989 57.7823 28.8989 58.0284C28.8989 58.2746 28.5473 58.4387 28.0198 58.4387Z" fill="#7F7F7F"/>
<path d="M28.0198 61.7209H27.3164C26.7889 61.7209 26.4373 61.5568 26.4373 61.3107C26.4373 61.0645 26.7889 60.9004 27.3164 60.9004H28.0198C28.5473 60.9004 28.8989 61.0645 28.8989 61.3107C28.8989 61.5568 28.5473 61.7209 28.0198 61.7209Z" fill="#7F7F7F"/>
<path d="M60.1191 51.8743H48.5532C48.0843 51.8743 47.7717 51.7102 47.7717 51.464C47.7717 51.2178 48.0843 51.0537 48.5532 51.0537H60.1191C60.588 51.0537 60.9006 51.2178 60.9006 51.464C60.9006 51.7102 60.588 51.8743 60.1191 51.8743Z" fill="white"/>
<path d="M84.7623 61.7209H48.5266C48.0737 61.7209 47.7717 61.5568 47.7717 61.3107C47.7717 61.0645 48.0737 60.9004 48.5266 60.9004H84.7623C85.2153 60.9004 85.5172 61.0645 85.5172 61.3107C85.5172 61.5568 85.2153 61.7209 84.7623 61.7209Z" fill="url(#paint0_linear_592_1292)"/>
<path d="M60.1642 55.1565H44.4053C43.9635 55.1565 43.6689 54.9924 43.6689 54.7462C43.6689 54.5 43.9635 54.3359 44.4053 54.3359H60.1642C60.606 54.3359 60.9006 54.5 60.9006 54.7462C60.9006 54.9924 60.606 55.1565 60.1642 55.1565Z" fill="#7F7F7F"/>
<path d="M72.4741 58.4387H44.4038C43.9629 58.4387 43.6689 58.2746 43.6689 58.0284C43.6689 57.7823 43.9629 57.6182 44.4038 57.6182H72.4741C72.915 57.6182 73.2089 57.7823 73.2089 58.0284C73.0619 58.2746 72.768 58.4387 72.4741 58.4387Z" fill="#7F7F7F"/>
<path d="M84.7178 58.4387H74.008C73.5284 58.4387 73.2087 58.2746 73.2087 58.0284C73.2087 57.7823 73.5284 57.6182 74.008 57.6182H84.7178C85.1974 57.6182 85.5171 57.7823 85.5171 58.0284C85.5171 58.2746 85.1974 58.4387 84.7178 58.4387Z" fill="white"/>
<path d="M91.3053 58.4387H87.1138C86.6481 58.4387 86.3376 58.2746 86.3376 58.0284C86.3376 57.7823 86.6481 57.6182 87.1138 57.6182H91.3053C91.771 57.6182 92.0815 57.7823 92.0815 58.0284C92.0815 58.2746 91.771 58.4387 91.3053 58.4387Z" fill="url(#paint1_linear_592_1292)"/>
<path d="M1 103.569H138.853" stroke="#575757" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M97.8255 51.0537V103.569H23.3715C20.4345 103.569 18.2317 101.209 18.2317 98.4062V51.0537H97.8255Z" fill="#0E0E0E" stroke="#575757" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M127.365 51.0537V98.4062C127.365 101.356 124.916 103.569 122.008 103.569H97.8254V51.0537H127.365Z" fill="#EAEAEA"/>
<path d="M127.365 51.0537V98.4062C127.365 101.356 124.916 103.569 122.008 103.569H97.8254V51.0537H127.365Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path d="M46.9512 51.0537L62.6074 26.437H142.956L126.857 51.0537H46.9512Z" fill="#D6D6D6"/>
<path d="M46.9512 51.0537L62.6074 26.437H142.956L126.857 51.0537H46.9512Z" fill="#0E0E0E" stroke="#575757" stroke-width="2" stroke-miterlimit="10"/>
<path opacity="0.3" d="M127.365 51.1975V78.9526H105.826C103.826 78.9526 102.441 77.6583 102.133 75.7888L97.8254 51.0537L127.365 51.1975Z" fill="url(#paint2_linear_592_1292)"/>
<path d="M126.746 51.0537H97.8254L112.511 73.3119C113.56 74.7859 115.208 75.6704 116.856 75.6704H139.334C140.832 75.6704 141.881 73.9015 140.982 72.7223L126.746 51.0537Z" fill="#0E0E0E" stroke="#575757" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M97.8254 51.0537L82.0597 26.437H1L17.3607 51.0537H97.8254Z" fill="#0E0E0E" stroke="#575757" stroke-width="1.5905" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M57.9543 81.4143H26.9218C26.1967 81.4143 25.6167 80.8673 25.6167 80.1835C25.6167 79.4997 26.1967 78.9526 26.9218 78.9526H57.9543C58.6794 78.9526 59.2594 79.4997 59.2594 80.1835C59.1144 80.8673 58.6794 81.4143 57.9543 81.4143Z" fill="#575757"/>
<path d="M57.9543 87.1582H26.9218C26.1967 87.1582 25.6167 86.7935 25.6167 86.3376C25.6167 85.8818 26.1967 85.5171 26.9218 85.5171H57.9543C58.6794 85.5171 59.2594 85.8818 59.2594 86.3376C59.1144 86.7935 58.6794 87.1582 57.9543 87.1582Z" fill="#575757"/>
<path d="M41.5228 93.7227H26.9422C26.2058 93.7227 25.6167 93.358 25.6167 92.9021C25.6167 92.4462 26.2058 92.0815 26.9422 92.0815H41.5228C42.2592 92.0815 42.8483 92.4462 42.8483 92.9021C42.7011 93.358 42.1119 93.7227 41.5228 93.7227Z" fill="#575757"/>
<defs>
<linearGradient id="paint0_linear" x1="47.941" y1="61.3273" x2="85.5608" y2="61.3273" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_592_1292" x1="47.9413" y1="61.3273" x2="85.561" y2="61.3273" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF66A9"/>
<stop offset="1" stop-color="#F53689"/>
<stop offset="0.0001" stop-color="#AAAAAA"/>
<stop offset="1" stop-color="#3B3B3B"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="86.3762" y1="58.0033" x2="92.0807" y2="58.0033" gradientUnits="userSpaceOnUse">
<stop stop-color="#83A6FF"/>
<stop offset="1" stop-color="#5A78FF"/>
<linearGradient id="paint1_linear_592_1292" x1="86.376" y1="58.0033" x2="92.0804" y2="58.0033" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="#8D8D8D"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="112.602" y1="79.5252" x2="112.602" y2="54.1127" gradientUnits="userSpaceOnUse">
<linearGradient id="paint2_linear_592_1292" x1="112.602" y1="79.5252" x2="112.602" y2="54.1127" gradientUnits="userSpaceOnUse">
<stop offset="0.00289017" stop-opacity="0"/>
<stop offset="1"/>
</linearGradient>

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

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