Compare commits

...

110 Commits

Author SHA1 Message Date
Eli Bosley
57587b9175 chore(release): 3.11.0 2024-09-11 13:25:19 -04:00
ljm42
5ee7cb2647 feat: reduce how often rc.flashbackup checks for changes
Instead of checking once per minute, check once every 30 minutes
2024-09-10 12:48:40 -04:00
ljm42
911a3f8f1a feat: send api_version to flash/activate endpoint
also use _var() function in a few more places for consistency
2024-09-10 09:30:17 -07:00
ljm42
d426001372 feat: update ProvisionCert.php to clean hosts file when it runs 2024-09-09 12:49:15 -07:00
ljm42
2d0c65aaf4 fix: remove local flash backup ratelimit file on uninstall/update 2024-09-06 16:38:10 -07:00
ljm42
fd4605b956 chore: prevent corner case issue and fix php warning
* Update remoteerror in flashback.ini if it gets out of sync with gitratelimit (can happen during testing if you delete flashbackup.ini)
* Fix php warning for retry_after
2024-09-06 11:27:17 -07:00
Eli Bosley
3f84b6bbfd chore(release): 3.10.1 2024-09-03 14:43:08 -04:00
github-actions[bot]
5ad10af303 chore: release main 2024-09-03 14:36:05 -04:00
Eli Bosley
9aa11faaaa fix: don't release to github 2024-09-03 14:32:04 -04:00
Eli Bosley
bfa98574f1 fix: single tag for both components 2024-09-03 14:30:03 -04:00
github-actions[bot]
dd2dc40ff1 chore: release main 2024-09-03 14:24:36 -04:00
ljm42
8a3265d7b1 Feat: flash backup supports keyserver rate limits 2024-09-03 11:14:56 -07:00
ljm42
a240a031a8 feat: set OS minver to 6.12.0 2024-08-30 12:50:44 -07:00
ljm42
979e41fe41 fix: remove hard-coded entry for keyserver from hosts file 2024-08-30 10:52:50 -04:00
ljm42
03dc404aa7 Use "go links" when linking to Docs 2024-08-29 16:35:02 -07:00
Eli Bosley
364320ffc9 fix: unify pull requests for release-please 2024-08-28 16:51:26 -04:00
Eli Bosley
2492f4cec9 feat: web version set 2024-08-28 16:37:15 -04:00
Eli Bosley
1a643b3eef feat: set API version 2024-08-28 16:36:34 -04:00
Eli Bosley
58ee3b958b feat: remove plugin as part of release please 2024-08-28 16:15:00 -04:00
Eli Bosley
2928cf5821 fix: add manifest 2024-08-28 15:58:02 -04:00
Eli Bosley
b21b276151 fix: infer release type 2024-08-28 15:55:58 -04:00
Eli Bosley
d80f25dc96 feat: begin release-please setup 2024-08-28 15:53:24 -04:00
Eli Bosley
f5f5a081e6 fix: unused import 2024-08-28 15:44:03 -04:00
Eli Bosley
f60474b4d7 fix: lint 2024-08-28 15:44:03 -04:00
Eli Bosley
364373df0c fix: revert myservers.cfg to fix test 2024-08-28 15:44:03 -04:00
Eli Bosley
bb38533bb2 fix: update snapshots 2024-08-28 15:44:03 -04:00
Eli Bosley
836801c524 feat: dynamic remote access using remote queries 2024-08-28 15:44:03 -04:00
Eli Bosley
b54cf5ede9 feat: move dynamic remote access to be fully api controlled 2024-08-28 15:44:03 -04:00
Eli Bosley
1a20c66c02 fix: permission for dashboard payload 2024-08-28 15:44:03 -04:00
Eli Bosley
587bbb3b4d feat: create stable hash based on apikey rather than hostname 2024-08-28 15:44:03 -04:00
Eli Bosley
e95f7a1a03 feat: remove dashboard types 2024-08-28 15:44:03 -04:00
Eli Bosley
9c75f6e2ca feat: update tests and snapshots 2024-08-28 15:44:03 -04:00
Eli Bosley
822042ab9c feat: remove dashboard resolver completely in favor of direct field resolvers 2024-08-28 15:44:03 -04:00
Eli Bosley
3a843b6e16 feat: disable all legacy dashboard and network logic 2024-08-28 15:44:03 -04:00
renovate[bot]
6072387c37 chore(deps): update dependency @types/node to v18.19.46 (#795)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-27 14:27:11 -04:00
renovate[bot]
313162dbf2 fix(deps): update dependency wtfnode to v0.9.3 (#901)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-27 14:26:50 -04:00
Eli Bosley
495515abac feat: move FQDN urls to a generic parser (#899)
* feat: move FQDN urls to a generic parser

* feat: update myservers.cfg

* feat: update parser to begin changing ID fields

* fix: ID parser issues resolved

* fix: remove console log

* fix: update snapshots
2024-08-21 10:38:16 -04:00
Zack Spear
09087040e9 fix: flash backup activated detection in account payload (#898)
fix: flash backup activated detection in account payload to warn users to download back before unregistering with connect
2024-08-16 10:08:42 -04:00
Eli Bosley
4423829911 feat: add global agent (#897)
* feat: add global agent

* feat: add proxy setup to rc.unraid-api

* feat: update myservers.cfg to latest version
2024-08-14 14:52:36 -04:00
Eli Bosley
c8f469c4fb chore(release): 3.8.1 2024-08-13 14:11:02 -04:00
Zack Spear
bc61b45f9f refactor: registration component remove contact support 2024-08-13 14:04:54 -04:00
Eli Bosley
f530d9ea82 chore(release): 3.8.0 2024-08-13 13:50:07 -04:00
ljm42
2046fa5310 refactor: change flag that skips delete on uninstall (#892) 2024-08-13 13:40:18 -04:00
Zack Spear
9ea2327fa0 refactor: registration transfer check ineligible copy 2024-08-13 10:31:50 -07:00
Zack Spear
ff67b54a1b refactor: doc urls use /go links 2024-08-13 10:31:31 -07:00
ljm42
e6bd7a54be feat: always force push 2024-08-13 10:30:49 -07:00
Eli Bosley
5827b5ffa3 feat: swap to docker compose from docker-compose 2024-08-07 11:04:54 -04:00
ljm42
572a1310e0 Use "go links" when linking to Docs (#891) 2024-08-07 10:41:57 -04:00
ljm42
c1403d3826 feat: don't allow flash backup repos larger than 500MB (#890)
* feat: don't allow flash backup repos larger than 500MB

* fix: don't backup dynamix.file.integrity/logs

* feat: max file size for backup limited to 10mb

* feat: limit max repo size to 100MB

* feat: delete large repo again after 90 days
2024-08-07 10:41:36 -04:00
Eli Bosley
29afe9b9e8 feat: settings through the API (#867)
* feat: api settings fully working
* refactor: nuxt config ConnectSettings

---------

Co-authored-by: Zack Spear <hi@zackspear.com>
2024-07-03 13:38:09 -04:00
Zack Spear
e9ff33d263 feat: downgradeOs callback for non stable osCurrentBranch 2024-05-28 11:57:05 -07:00
Zack Spear
a62f60a436 fix: update status button alignment 2024-05-28 11:57:05 -07:00
Eli Bosley
838964c6ef chore: update package.json with new dependencies (#886)
* chore: update package.json with new dependencies

* feat: run codegen

* fix: got and reflect metadata revert version

* fix: pino version mismatch

* feat: update package-lock.json
2024-05-17 11:21:55 -04:00
Zack Spear
800fc12c15 refactor: server state refresh and response mutations 2024-05-16 14:13:01 -07:00
Zack Spear
80175241e3 fix: lint error for web components 2024-05-16 14:13:01 -07:00
Zack Spear
5d801f22f5 chore: ts-expect-error description for webgui troubleshoot form 2024-05-16 14:13:01 -07:00
Zack Spear
ba772add54 refactor: instantiation of web components 2024-05-16 14:13:01 -07:00
Zack Spear
ff24f12cae refactor: optional chaining for click props 2024-05-16 14:13:01 -07:00
Eli Bosley
487f5c1865 fix: tailwind config types 2024-05-16 14:13:01 -07:00
Eli Bosley
e0c90037fb fix: swap undefined to null 2024-05-16 14:13:01 -07:00
Eli Bosley
aa5f603cba fix: apolloClient types 2024-05-16 14:13:01 -07:00
Eli Bosley
409db43973 fix: ts-expect-error unneeded 2024-05-16 14:13:01 -07:00
Zack Spear
cef1b29355 fix: type check 2024-05-16 14:13:01 -07:00
Zack Spear
045750c87e fix: lint issues 2024-05-16 14:13:01 -07:00
Zack Spear
85802e7af7 fix: formattedRegTm type 2024-05-16 14:13:01 -07:00
Zack Spear
4bfdb66d46 fix: i18n t prop type 2024-05-16 14:13:01 -07:00
Zack Spear
81a6a52d9f fix: type errors round 1 2024-05-16 14:13:01 -07:00
Zack Spear
7759fe1dc3 chore(web): update deps + eslint (#887)
* chore: update deps + eslint

* fix: lint + type errors
2024-05-16 09:26:39 -04:00
renovate[bot]
3b2acb29b5 chore(deps): update dependency @vueuse/nuxt to v10.9.0 (#797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 15:23:11 -04:00
renovate[bot]
5f2b949ecf chore(deps): update dependency @nuxtjs/tailwindcss to v6.12.0 (#794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 15:22:43 -04:00
renovate[bot]
1b956d563e fix(deps): update dependency @vue/apollo-composable to v4.0.2 (#787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 15:22:30 -04:00
Eli Bosley
c6a97f5082 chore(release): 3.7.1 2024-05-15 14:33:43 -04:00
Zack Spear
7f512e47e9 fix: reboot required and available edge case (#885)
* fix: reboot required and available edge case

* chore: add missing web component translations

* chore: translations sort unique ascending, case insensitive

* fix: translation json
2024-05-15 12:24:06 -04:00
Eli Bosley
5d725b0e76 chore(release): 3.7.0 2024-05-14 15:18:07 -04:00
renovate[bot]
fe63607260 chore(deps): update dependency @types/dockerode to v3.3.29 (#768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-13 16:03:35 -04:00
Zack Spear
0a1d4daf6e refactor: update os status updateAvailable 2024-05-13 10:52:30 -07:00
Zack Spear
9e9e385bef chore: UnraidUpdateCancel reorganize 2024-05-13 10:52:30 -07:00
Zack Spear
6fed39e05b chore: update cancel comment 2024-05-13 10:52:30 -07:00
Zack Spear
3dec53d13d chore: UpdateOS Status unused import 2024-05-13 10:52:30 -07:00
Zack Spear
f0ded9f5be fix: update os cancel refresh on update page 2024-05-13 10:52:30 -07:00
Zack Spear
7d55a1c2cd chore: UpdateOS Status unused import 2024-05-13 10:52:30 -07:00
Zack Spear
f3dc9663b8 feat: UI Update OS Cancel 2024-05-13 10:52:30 -07:00
Zack Spear
05c7c481a9 chore: update cancel script 2024-05-13 10:52:30 -07:00
Zack Spear
adcc1543f0 feat: UnraidUpdateCancel script 2024-05-13 10:52:30 -07:00
Zack Spear
95f873c752 refactor: UnraidCheck use current unRAIDServer.plg 2024-05-13 10:52:30 -07:00
Zack Spear
ec90f8b295 fix: plugin file deployment script 2024-05-13 10:52:30 -07:00
Zack Spear
f84195a98d fix(web): lint unused rebootVersion 2024-05-13 10:52:30 -07:00
Zack Spear
5e98a68e2e feat: ui to allow second update without reboot 2024-05-13 10:52:30 -07:00
Zack Spear
b91dbca144 refactor: btnStyle prop for CallbackButton component 2024-05-13 10:52:30 -07:00
Zack Spear
79a01da18d refactor: ButtonStyle type 2024-05-13 10:52:30 -07:00
Zack Spear
14951d3004 refactor: reboot details added to server payload to account 2024-05-13 10:52:30 -07:00
Zack Spear
64c2061bea chore: dev deployment script improvements 2024-05-13 10:52:30 -07:00
Zack Spear
e3adc9a29a chore: dev deployment script improvements 2024-05-13 10:52:30 -07:00
ljm42
6b689ffcce Chore: sync http_get_contents() with webgui (#883) 2024-05-10 12:57:57 -07:00
ljm42
c995a4c5c8 Fix: rc.flashbackup needs to check both signed in and connected (#882)
because /var/local/emhttp/myservers.cfg does not clear the connected status when the user signs out
2024-05-10 10:44:21 -07:00
Zack Spear
8d1e0f67d1 refactor: simplify version_compare in reboot-details 2024-05-08 12:47:29 -07:00
Zack Spear
7877a5dca2 refactor: reboot type detection for downgrade via callback 2024-05-08 12:47:29 -07:00
Zack Spear
16db278ffd feat: downgradeOs callback 2024-05-08 12:47:29 -07:00
ljm42
521b4381f2 Fix bug in flash backup rate limiter (#880)
Don't try to read from an empty file
2024-05-07 17:11:36 -07:00
ljm42
9ae9d40f94 fix: keep minor enhancements from #872 (#878) 2024-05-07 08:39:46 -07:00
Zack Spear
1d562d404c fix(web): registration component remove unused ref 2024-05-06 10:44:16 -07:00
Zack Spear
7ac1b268d9 refactor(web): registration linked learn more callback to my keys 2024-05-06 10:44:16 -07:00
Zack Spear
4833e9dccf chore: translations 2024-05-06 10:44:16 -07:00
Zack Spear
f28b7510fa feat(web): Registration key linked to account status 2024-05-06 10:44:16 -07:00
Zack Spear
37b717b142 refactor(web): button component no style option 2024-05-06 10:44:16 -07:00
Zack Spear
fd8b40d9aa feat(web): callback types myKeys & linkKey 2024-05-06 10:44:16 -07:00
Eli Bosley
1d944781cf feat: add a timestamp to flash backup (#877)
* feat: add a timestamp to flash backup

* feat: update gitignore

* feat: random interval is now 30 minutes
2024-05-06 13:40:42 -04:00
Zack Spear
1f4c64d022 feat(plg): install prevent downgrade of shared page & php files (#873)
* feat(plg): install prevent downgrade of shared page & php files

* chore(plg): remove debug echo

* fix(plg): remove extra char
2024-05-02 14:08:38 -07:00
Zack Spear
f69b5130a3 refactor(web): copy Ineligible for feature updates (#875)
* refactor(web): copy Ineligible for feature updates

* refactor(web): Eligible for free feature updates
2024-05-02 14:04:16 -07:00
Zack Spear
f8b143904b fix: prevent corrupt case model in state.php (#874)
fix: prevent corruprt case model in state.php
2024-05-02 14:00:29 -07:00
195 changed files with 13440 additions and 9971 deletions

View File

@@ -12,6 +12,17 @@ concurrency:
cancel-in-progress: true
jobs:
release-please:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- id: release
uses: googleapis/release-please-action@v4
outputs:
releases_created: ${{ steps.release.outputs.releases_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
start:
# This prevents a tag running twice as it'll have a "tag" and a "commit" event
# We only want the tag to run the action as it'll be able to create the release notes
@@ -81,10 +92,10 @@ jobs:
- name: Build Docker Compose
run: |
docker network create mothership_default
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build builder
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose build builder
- name: Run Docker Compose
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run builder npm run coverage
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose run builder npm run coverage
lint-web:
defaults:

4
.gitignore vendored
View File

@@ -83,4 +83,6 @@ deploy/*
.cache
.output
.env*
!.env.example
!.env.example
fb_keepalive

View File

@@ -0,0 +1 @@
{"api":"3.10.0","web":"3.10.0"}

View File

@@ -29,5 +29,6 @@
"i18n-ally.localesPaths": [
"locales"
],
"i18n-ally.keystyle": "flat"
"i18n-ally.keystyle": "flat",
"eslint.experimental.useFlatConfig": true,
}

View File

@@ -13,6 +13,6 @@ PLAYGROUND=true
INTROSPECTION=true
MOTHERSHIP_GRAPHQL_LINK="http://authenticator:3000/graphql"
NODE_TLS_REJECT_UNAUTHORIZED=0
BYPASS_PERMISSION_CHECKS=true
BYPASS_PERMISSION_CHECKS=false
BYPASS_CORS_CHECKS=false
CHOKIDAR_USEPOLLING=true

11
api/.env.test Normal file
View File

@@ -0,0 +1,11 @@
VERSION="THIS_WILL_BE_REPLACED_WHEN_BUILT"
PATHS_UNRAID_DATA=./dev/data # Where we store plugin data (e.g. permissions.json)
PATHS_STATES=./dev/states # Where .ini files live (e.g. vars.ini)
PATHS_DYNAMIX_BASE=./dev/dynamix # Dynamix's data directory
PATHS_DYNAMIX_CONFIG=./dev/dynamix/dynamix.cfg # Dynamix's config file
PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file
PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
PORT=5000
NODE_ENV=test

View File

@@ -2,6 +2,226 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
## [3.11.0](https://github.com/unraid/api/compare/v3.10.1...v3.11.0) (2024-09-11)
### Features
* reduce how often rc.flashbackup checks for changes ([793d368](https://github.com/unraid/api/commit/793d3681404018e0ae933df0ad111809220ad138))
* send api_version to flash/activate endpoint ([d8ec20e](https://github.com/unraid/api/commit/d8ec20ea6aa35aa241abd8424c4d884bcbb8f590))
* update ProvisionCert.php to clean hosts file when it runs ([fbe20c9](https://github.com/unraid/api/commit/fbe20c97b327849c15a4b34f5f53476edaefbeb6))
### Bug Fixes
* remove local flash backup ratelimit file on uninstall/update ([abf207b](https://github.com/unraid/api/commit/abf207b077861798c53739b1965207f87d5633b3))
### [3.10.1](https://github.com/unraid/api/compare/v3.10.0...v3.10.1) (2024-09-03)
## [3.10.0](https://github.com/unraid/api/compare/v3.9.0...v3.10.0) (2024-09-03)
### Features
* add a timestamp to flash backup ([#877](https://github.com/unraid/api/issues/877)) ([b868fd4](https://github.com/unraid/api/commit/b868fd46c3886b2182245a61f20be6df65e46abe))
* add environment to docker-compose ([2ee4683](https://github.com/unraid/api/commit/2ee46839095e3b8ee287cfe10f29ae9a39dcff68))
* add global agent ([#897](https://github.com/unraid/api/issues/897)) ([8b0dc69](https://github.com/unraid/api/commit/8b0dc69f65bd3e280a21c50aab221334f7341b1c))
* add logrotate to cron in nestjs ([#839](https://github.com/unraid/api/issues/839)) ([5c91524](https://github.com/unraid/api/commit/5c91524d849147c0ac7925f3a2f1cce67ffe75de))
* add new staging url for connect website ([#841](https://github.com/unraid/api/issues/841)) ([4cfc07b](https://github.com/unraid/api/commit/4cfc07b6763dbb79b68cf01f7eaf7cf33370d4db))
* add support for expiration in var.ini ([#833](https://github.com/unraid/api/issues/833)) ([0474c2e](https://github.com/unraid/api/commit/0474c2e14fa462d2e1ec6d9a7f974660385d073e))
* always show DRA even if disabled ([ab708c0](https://github.com/unraid/api/commit/ab708c0df634e21bf81595412d7de0be3ff7c392))
* close log on exit ([d6ede86](https://github.com/unraid/api/commit/d6ede86eca6301342cdf35bf1f9365896b5e5009))
* create stable hash based on apikey rather than hostname ([ecf5554](https://github.com/unraid/api/commit/ecf5554e304cc7dee78cb1f206ef4e80222c3e64))
* disable all legacy dashboard and network logic ([6784f4b](https://github.com/unraid/api/commit/6784f4b6e1a12b2f30bfa9ab4fe6310994bd18ae))
* dynamic remote access using remote queries ([f7fc0c4](https://github.com/unraid/api/commit/f7fc0c431561978054d2ff37d1aa644865e846ec))
* extraOrigins public, remove origin listener ([91f96ba](https://github.com/unraid/api/commit/91f96ba818773d6e71dde1ff52a4c8ec21ba6b5d))
* fix codegen ([d0bf5bb](https://github.com/unraid/api/commit/d0bf5bb8197b11f7a250ca5392890184a1dbeff7))
* fix exit hook and cleanup docker scripts ([#758](https://github.com/unraid/api/issues/758)) ([a9ff73e](https://github.com/unraid/api/commit/a9ff73e0a04c67e9ec9d5551cf0b1f124be6f381))
* fix logging format on start and stop ([c6720c3](https://github.com/unraid/api/commit/c6720c331df055480d2d65b37290f4978fe429da))
* local start command ([99b6007](https://github.com/unraid/api/commit/99b6007ba30353084a8bea54cc0e782fcc1bfea4))
* log config recreation reason ([f36c72f](https://github.com/unraid/api/commit/f36c72f5ad44b7e41d1726fa181dc2b9f594c72c))
* move dynamic remote access to be fully api controlled ([206eb6b](https://github.com/unraid/api/commit/206eb6b74aa83047237e5f6c94c46b08c6507168))
* move FQDN urls to a generic parser ([#899](https://github.com/unraid/api/issues/899)) ([246595e](https://github.com/unraid/api/commit/246595ee7acd8370906a759cbe618def4f52c173))
* nestjs initial query implementation ([#748](https://github.com/unraid/api/issues/748)) ([075d7f2](https://github.com/unraid/api/commit/075d7f25785bf686779b7fee1d5ea39f09ff3ea8))
* new key types in API ([e42f9dc](https://github.com/unraid/api/commit/e42f9dc95be03e8389aac443f2147c07a316d48d))
* regTy swapped ([564b25c](https://github.com/unraid/api/commit/564b25cf5ce0a62d40f8d63d44c81e9c8560e0be))
* remove dashboard resolver completely in favor of direct field resolvers ([1cd1ee5](https://github.com/unraid/api/commit/1cd1ee534825ccf775208c438ae0bd777bbe4d39))
* remove dashboard types ([2f0167d](https://github.com/unraid/api/commit/2f0167dc89835bcf8aa946425c5c6683221fd763))
* run codegen and update build script ([07512ad](https://github.com/unraid/api/commit/07512adc13ee0d819db45ff6c5c5f58a0ba31141))
* settings through the API ([#867](https://github.com/unraid/api/issues/867)) ([e73624b](https://github.com/unraid/api/commit/e73624be6be8bc2c70d898b8601a88cc8d20a3e4))
* swap to docker compose from docker-compose ([ec16a6a](https://github.com/unraid/api/commit/ec16a6aab1a2d5c836387da438fbeade07d23425))
* swap to fragement usage on webcomponent ([42733ab](https://github.com/unraid/api/commit/42733abf6e443516ff715569333422ce80d3b1d2))
* update tests and snapshots ([c39aa17](https://github.com/unraid/api/commit/c39aa17e4302ed56b3097ab3244d840f11eb686b))
* upgrade a ton of dependencies ([#842](https://github.com/unraid/api/issues/842)) ([94c1746](https://github.com/unraid/api/commit/94c174620c2347a3cf3d100404635f99a5b47287))
### Bug Fixes
* add serverName / description to dashboard payload ([9677aff](https://github.com/unraid/api/commit/9677aff1cd0942f36a2845f3f105601c494efd9e))
* allow failure for log deletion ([eff3142](https://github.com/unraid/api/commit/eff31423927644be436a831126678719c2eb0621))
* allowed origins check not working without spaces ([#838](https://github.com/unraid/api/issues/838)) ([b998b38](https://github.com/unraid/api/commit/b998b38355fab77ecc2f62bc64896766218db3d4))
* **api:** readme discord url ([ffd5c6a](https://github.com/unraid/api/commit/ffd5c6afb64956e76df22c77104a21bc22798008))
* build docker command updated to use dc.sh script ([0b40886](https://github.com/unraid/api/commit/0b40886e84f27a94dbf67ef4ca0cd8539ef3913e))
* codegen on web run ([e2e67c2](https://github.com/unraid/api/commit/e2e67c21067a138d963f5f10760b84cf6a533542))
* **deps:** update dependency @apollo/client to v3.9.5 ([#785](https://github.com/unraid/api/issues/785)) ([75b98bc](https://github.com/unraid/api/commit/75b98bc1cbca5b66ae72f52a0b6f5f58230a2473))
* **deps:** update dependency graphql to v16.8.1 ([bff1b19](https://github.com/unraid/api/commit/bff1b19706bee1e3103e3a0a1d2fceb3154f9bba))
* **deps:** update dependency graphql-ws to v5.15.0 ([#790](https://github.com/unraid/api/issues/790)) ([4773b13](https://github.com/unraid/api/commit/4773b132167d740d4c996efe22e0f1b99576fb9b))
* **deps:** update dependency ws to v8.16.0 ([#815](https://github.com/unraid/api/issues/815)) ([212020e](https://github.com/unraid/api/commit/212020e78d4de0576137058a3374837b4a43e02d))
* **deps:** update dependency wtfnode to v0.9.3 ([#901](https://github.com/unraid/api/issues/901)) ([a88482b](https://github.com/unraid/api/commit/a88482bfcbf134f55330f8728bc5c7f67c521773))
* **deps:** update graphql-tools monorepo ([3447eb0](https://github.com/unraid/api/commit/3447eb047a1dcd575b88a96bbcef9946aca366a1))
* **deps:** update graphql-tools monorepo (major) ([#693](https://github.com/unraid/api/issues/693)) ([3447eb0](https://github.com/unraid/api/commit/3447eb047a1dcd575b88a96bbcef9946aca366a1))
* **deps:** update nest monorepo ([#816](https://github.com/unraid/api/issues/816)) ([4af3699](https://github.com/unraid/api/commit/4af36991b8b376f816ed51fd503a66e99675a3e7))
* excessive logging ([89cb254](https://github.com/unraid/api/commit/89cb2544ed0e0edd33b59f15d487487e22c0ae32))
* exit with process.exit not process.exitcode ([dcb6def](https://github.com/unraid/api/commit/dcb6def1cf3365dca819feed101160c8ad0125dc))
* lint ([919873d](https://github.com/unraid/api/commit/919873d9edee304d99036a4a810db3789c734fbf))
* local container startup commands cleaned up ([6c0ccb2](https://github.com/unraid/api/commit/6c0ccb2b24f98282be4db2e0b2e6362f4a187def))
* logrotate not working due to invalid ownership of unraid-api folder ([ec0581a](https://github.com/unraid/api/commit/ec0581abf58a217f698d52d5337f2b312e5a645b))
* optional check on api.version to allow fallback to save value ([0ac4455](https://github.com/unraid/api/commit/0ac4455f78407eca7aa1d6ee360830067a1c5c3e))
* permission for dashboard payload ([704a530](https://github.com/unraid/api/commit/704a530653dac415766bded5e96f6060f931e591))
* rearrange exit hook to try to fix closing ([843d3f4](https://github.com/unraid/api/commit/843d3f41162c5dbcfd7803912b1879d7a182231a))
* revert myservers.cfg to fix test ([a7705be](https://github.com/unraid/api/commit/a7705beb0a5b32660367ad8de9b46b06f7a3bec7))
* run hourly ([0425794](https://github.com/unraid/api/commit/0425794356a01262222e7dff2645d3629e00d0f6))
* unused import ([065fe57](https://github.com/unraid/api/commit/065fe575f578a74d593805c3121dd7fbdfc3e5ae))
* update snapshots ([c8a0a8e](https://github.com/unraid/api/commit/c8a0a8ec007abc0372464c7e2b44bd47b6babd94))
## [3.9.0](https://github.com/unraid/api/compare/api-v3.8.1...api-v3.9.0) (2024-09-03)
### Features
* add a timestamp to flash backup ([#877](https://github.com/unraid/api/issues/877)) ([b868fd4](https://github.com/unraid/api/commit/b868fd46c3886b2182245a61f20be6df65e46abe))
* add environment to docker-compose ([2ee4683](https://github.com/unraid/api/commit/2ee46839095e3b8ee287cfe10f29ae9a39dcff68))
* add global agent ([#897](https://github.com/unraid/api/issues/897)) ([8b0dc69](https://github.com/unraid/api/commit/8b0dc69f65bd3e280a21c50aab221334f7341b1c))
* add logrotate to cron in nestjs ([#839](https://github.com/unraid/api/issues/839)) ([5c91524](https://github.com/unraid/api/commit/5c91524d849147c0ac7925f3a2f1cce67ffe75de))
* add new staging url for connect website ([#841](https://github.com/unraid/api/issues/841)) ([4cfc07b](https://github.com/unraid/api/commit/4cfc07b6763dbb79b68cf01f7eaf7cf33370d4db))
* add support for expiration in var.ini ([#833](https://github.com/unraid/api/issues/833)) ([0474c2e](https://github.com/unraid/api/commit/0474c2e14fa462d2e1ec6d9a7f974660385d073e))
* always show DRA even if disabled ([ab708c0](https://github.com/unraid/api/commit/ab708c0df634e21bf81595412d7de0be3ff7c392))
* close log on exit ([d6ede86](https://github.com/unraid/api/commit/d6ede86eca6301342cdf35bf1f9365896b5e5009))
* create stable hash based on apikey rather than hostname ([ecf5554](https://github.com/unraid/api/commit/ecf5554e304cc7dee78cb1f206ef4e80222c3e64))
* disable all legacy dashboard and network logic ([6784f4b](https://github.com/unraid/api/commit/6784f4b6e1a12b2f30bfa9ab4fe6310994bd18ae))
* dynamic remote access using remote queries ([f7fc0c4](https://github.com/unraid/api/commit/f7fc0c431561978054d2ff37d1aa644865e846ec))
* extraOrigins public, remove origin listener ([91f96ba](https://github.com/unraid/api/commit/91f96ba818773d6e71dde1ff52a4c8ec21ba6b5d))
* fix codegen ([d0bf5bb](https://github.com/unraid/api/commit/d0bf5bb8197b11f7a250ca5392890184a1dbeff7))
* fix exit hook and cleanup docker scripts ([#758](https://github.com/unraid/api/issues/758)) ([a9ff73e](https://github.com/unraid/api/commit/a9ff73e0a04c67e9ec9d5551cf0b1f124be6f381))
* fix logging format on start and stop ([c6720c3](https://github.com/unraid/api/commit/c6720c331df055480d2d65b37290f4978fe429da))
* local start command ([99b6007](https://github.com/unraid/api/commit/99b6007ba30353084a8bea54cc0e782fcc1bfea4))
* log config recreation reason ([f36c72f](https://github.com/unraid/api/commit/f36c72f5ad44b7e41d1726fa181dc2b9f594c72c))
* move dynamic remote access to be fully api controlled ([206eb6b](https://github.com/unraid/api/commit/206eb6b74aa83047237e5f6c94c46b08c6507168))
* move FQDN urls to a generic parser ([#899](https://github.com/unraid/api/issues/899)) ([246595e](https://github.com/unraid/api/commit/246595ee7acd8370906a759cbe618def4f52c173))
* nestjs initial query implementation ([#748](https://github.com/unraid/api/issues/748)) ([075d7f2](https://github.com/unraid/api/commit/075d7f25785bf686779b7fee1d5ea39f09ff3ea8))
* new key types in API ([e42f9dc](https://github.com/unraid/api/commit/e42f9dc95be03e8389aac443f2147c07a316d48d))
* regTy swapped ([564b25c](https://github.com/unraid/api/commit/564b25cf5ce0a62d40f8d63d44c81e9c8560e0be))
* remove dashboard resolver completely in favor of direct field resolvers ([1cd1ee5](https://github.com/unraid/api/commit/1cd1ee534825ccf775208c438ae0bd777bbe4d39))
* remove dashboard types ([2f0167d](https://github.com/unraid/api/commit/2f0167dc89835bcf8aa946425c5c6683221fd763))
* run codegen and update build script ([07512ad](https://github.com/unraid/api/commit/07512adc13ee0d819db45ff6c5c5f58a0ba31141))
* settings through the API ([#867](https://github.com/unraid/api/issues/867)) ([e73624b](https://github.com/unraid/api/commit/e73624be6be8bc2c70d898b8601a88cc8d20a3e4))
* swap to docker compose from docker-compose ([ec16a6a](https://github.com/unraid/api/commit/ec16a6aab1a2d5c836387da438fbeade07d23425))
* swap to fragement usage on webcomponent ([42733ab](https://github.com/unraid/api/commit/42733abf6e443516ff715569333422ce80d3b1d2))
* update tests and snapshots ([c39aa17](https://github.com/unraid/api/commit/c39aa17e4302ed56b3097ab3244d840f11eb686b))
* upgrade a ton of dependencies ([#842](https://github.com/unraid/api/issues/842)) ([94c1746](https://github.com/unraid/api/commit/94c174620c2347a3cf3d100404635f99a5b47287))
### Bug Fixes
* add serverName / description to dashboard payload ([9677aff](https://github.com/unraid/api/commit/9677aff1cd0942f36a2845f3f105601c494efd9e))
* allow failure for log deletion ([eff3142](https://github.com/unraid/api/commit/eff31423927644be436a831126678719c2eb0621))
* allowed origins check not working without spaces ([#838](https://github.com/unraid/api/issues/838)) ([b998b38](https://github.com/unraid/api/commit/b998b38355fab77ecc2f62bc64896766218db3d4))
* **api:** readme discord url ([ffd5c6a](https://github.com/unraid/api/commit/ffd5c6afb64956e76df22c77104a21bc22798008))
* build docker command updated to use dc.sh script ([0b40886](https://github.com/unraid/api/commit/0b40886e84f27a94dbf67ef4ca0cd8539ef3913e))
* codegen on web run ([e2e67c2](https://github.com/unraid/api/commit/e2e67c21067a138d963f5f10760b84cf6a533542))
* **deps:** update dependency @apollo/client to v3.9.5 ([#785](https://github.com/unraid/api/issues/785)) ([75b98bc](https://github.com/unraid/api/commit/75b98bc1cbca5b66ae72f52a0b6f5f58230a2473))
* **deps:** update dependency graphql to v16.8.1 ([bff1b19](https://github.com/unraid/api/commit/bff1b19706bee1e3103e3a0a1d2fceb3154f9bba))
* **deps:** update dependency graphql-ws to v5.15.0 ([#790](https://github.com/unraid/api/issues/790)) ([4773b13](https://github.com/unraid/api/commit/4773b132167d740d4c996efe22e0f1b99576fb9b))
* **deps:** update dependency ws to v8.16.0 ([#815](https://github.com/unraid/api/issues/815)) ([212020e](https://github.com/unraid/api/commit/212020e78d4de0576137058a3374837b4a43e02d))
* **deps:** update dependency wtfnode to v0.9.3 ([#901](https://github.com/unraid/api/issues/901)) ([a88482b](https://github.com/unraid/api/commit/a88482bfcbf134f55330f8728bc5c7f67c521773))
* **deps:** update graphql-tools monorepo ([3447eb0](https://github.com/unraid/api/commit/3447eb047a1dcd575b88a96bbcef9946aca366a1))
* **deps:** update graphql-tools monorepo (major) ([#693](https://github.com/unraid/api/issues/693)) ([3447eb0](https://github.com/unraid/api/commit/3447eb047a1dcd575b88a96bbcef9946aca366a1))
* **deps:** update nest monorepo ([#816](https://github.com/unraid/api/issues/816)) ([4af3699](https://github.com/unraid/api/commit/4af36991b8b376f816ed51fd503a66e99675a3e7))
* excessive logging ([89cb254](https://github.com/unraid/api/commit/89cb2544ed0e0edd33b59f15d487487e22c0ae32))
* exit with process.exit not process.exitcode ([dcb6def](https://github.com/unraid/api/commit/dcb6def1cf3365dca819feed101160c8ad0125dc))
* lint ([919873d](https://github.com/unraid/api/commit/919873d9edee304d99036a4a810db3789c734fbf))
* local container startup commands cleaned up ([6c0ccb2](https://github.com/unraid/api/commit/6c0ccb2b24f98282be4db2e0b2e6362f4a187def))
* logrotate not working due to invalid ownership of unraid-api folder ([ec0581a](https://github.com/unraid/api/commit/ec0581abf58a217f698d52d5337f2b312e5a645b))
* optional check on api.version to allow fallback to save value ([0ac4455](https://github.com/unraid/api/commit/0ac4455f78407eca7aa1d6ee360830067a1c5c3e))
* permission for dashboard payload ([704a530](https://github.com/unraid/api/commit/704a530653dac415766bded5e96f6060f931e591))
* rearrange exit hook to try to fix closing ([843d3f4](https://github.com/unraid/api/commit/843d3f41162c5dbcfd7803912b1879d7a182231a))
* revert myservers.cfg to fix test ([a7705be](https://github.com/unraid/api/commit/a7705beb0a5b32660367ad8de9b46b06f7a3bec7))
* run hourly ([0425794](https://github.com/unraid/api/commit/0425794356a01262222e7dff2645d3629e00d0f6))
* unused import ([065fe57](https://github.com/unraid/api/commit/065fe575f578a74d593805c3121dd7fbdfc3e5ae))
* update snapshots ([c8a0a8e](https://github.com/unraid/api/commit/c8a0a8ec007abc0372464c7e2b44bd47b6babd94))
### [3.8.1](https://github.com/unraid/api/compare/v3.8.0...v3.8.1) (2024-08-13)
## [3.8.0](https://github.com/unraid/api/compare/v3.7.1...v3.8.0) (2024-08-13)
### Features
* always force push ([662f3ce](https://github.com/unraid/api/commit/662f3ce440593e609c64364726f7da16dda0972b))
* don't allow flash backup repos larger than 500MB ([#890](https://github.com/unraid/api/issues/890)) ([30a32f5](https://github.com/unraid/api/commit/30a32f5fe684bb32c084c4125aade5e63ffd788b))
* downgradeOs callback for non stable osCurrentBranch ([17c4489](https://github.com/unraid/api/commit/17c4489e97bda504ca45e360591655ded166c355))
* settings through the API ([#867](https://github.com/unraid/api/issues/867)) ([e73624b](https://github.com/unraid/api/commit/e73624be6be8bc2c70d898b8601a88cc8d20a3e4))
* swap to docker compose from docker-compose ([ec16a6a](https://github.com/unraid/api/commit/ec16a6aab1a2d5c836387da438fbeade07d23425))
### Bug Fixes
* apolloClient types ([f14c767](https://github.com/unraid/api/commit/f14c7673735b92aa167e9e8dcb14a045bcfea994))
* **deps:** update dependency @vue/apollo-composable to v4.0.2 ([#787](https://github.com/unraid/api/issues/787)) ([edfc846](https://github.com/unraid/api/commit/edfc8464b0e0c2f38003ae8420e81532fd18351f))
* formattedRegTm type ([748906e](https://github.com/unraid/api/commit/748906e15d30c6162e2f08f28724c9104c81d123))
* i18n t prop type ([96d519f](https://github.com/unraid/api/commit/96d519f3e6b96ea7c4dc60616522216de20ee140))
* lint error for web components ([bc27b20](https://github.com/unraid/api/commit/bc27b20524934cf896efb84a131cd270431c508c))
* lint issues ([853dc19](https://github.com/unraid/api/commit/853dc195b13fff29160afb44f9ff11d4dd6a3232))
* swap undefined to null ([ebba976](https://github.com/unraid/api/commit/ebba9769873a6536e3fce65978e6475d93280560))
* tailwind config types ([0f77e55](https://github.com/unraid/api/commit/0f77e5596db3356b5dc05129b3ce215a8809e1dc))
* ts-expect-error unneeded ([ee4d4e9](https://github.com/unraid/api/commit/ee4d4e9f12b4488ff39445bc72c1b83a9d93e993))
* type check ([606aad7](https://github.com/unraid/api/commit/606aad703d91b72a14e15da3100dfa355052ed58))
* type errors round 1 ([977d5da](https://github.com/unraid/api/commit/977d5daf04012f16e7b6602167338f0bc363735a))
* update status button alignment ([4f2deaf](https://github.com/unraid/api/commit/4f2deaf70e5caa9f29fc5b2974b278f80b7b3a8a))
### [3.7.1](https://github.com/unraid/api/compare/v3.7.0...v3.7.1) (2024-05-15)
### Bug Fixes
* reboot required and available edge case ([#885](https://github.com/unraid/api/issues/885)) ([76e9cdf](https://github.com/unraid/api/commit/76e9cdf81f06a19c2e4c9a40a4d8e062bad2a607))
## [3.7.0](https://github.com/unraid/api/compare/v3.6.0...v3.7.0) (2024-05-14)
### Features
* add a timestamp to flash backup ([#877](https://github.com/unraid/api/issues/877)) ([b868fd4](https://github.com/unraid/api/commit/b868fd46c3886b2182245a61f20be6df65e46abe))
* add support for outgoing proxies ([#863](https://github.com/unraid/api/issues/863)) ([223693e](https://github.com/unraid/api/commit/223693e0981d5f2884a1f8b8baf03d4dc58e8cb2))
* array state on registration page ([d36fef0](https://github.com/unraid/api/commit/d36fef0545ddb820e67e8bc6cb42ea3644021d66))
* downgradeOs callback ([154a976](https://github.com/unraid/api/commit/154a976109f0a32653a2851988420707631327ca))
* Flash Backup requires connection to mothership ([#868](https://github.com/unraid/api/issues/868)) ([d127208](https://github.com/unraid/api/commit/d127208b5e0f7f9991f515f95b0e266d38cf3287))
* **plg:** install prevent downgrade of shared page & php files ([#873](https://github.com/unraid/api/issues/873)) ([4ac72b1](https://github.com/unraid/api/commit/4ac72b16692c4246c9d2c0b53b23d8b2d95f5de6))
* **plg:** plg install prevent web component downgrade ([8703bd4](https://github.com/unraid/api/commit/8703bd498108f5c05562584a708bd2306e53f7a6))
* postbuild script to add timestamp to web component manifest ([47f08ea](https://github.com/unraid/api/commit/47f08ea3594a91098f67718c0123110c7b5f86f7))
* registration page server error heading + subheading ([6038ebd](https://github.com/unraid/api/commit/6038ebdf39bf47f2cb5c0b1de84764795374f018))
* remove cron to download JS daily ([#864](https://github.com/unraid/api/issues/864)) ([33f6d6b](https://github.com/unraid/api/commit/33f6d6b343de07dbe70de863926906736d42f371)), closes [#529](https://github.com/unraid/api/issues/529)
* ui to allow second update without reboot ([b0f2d10](https://github.com/unraid/api/commit/b0f2d102917f54ab33f0ad10863522b8ff8e3ce5))
* UI Update OS Cancel ([7c02308](https://github.com/unraid/api/commit/7c02308964d5e21990427a2c626c9db2d9e1fed0))
* UnraidUpdateCancel script ([b73bdc0](https://github.com/unraid/api/commit/b73bdc021764762ed12dca494e1345412a45c677))
* **web:** callback types myKeys & linkKey ([c88ee01](https://github.com/unraid/api/commit/c88ee01827396c3fa8a30bb88c4be712c80b1f4f))
* **web:** Registration key linked to account status ([8f6182d](https://github.com/unraid/api/commit/8f6182d426453b73aa19c5f0f59469fa07571694))
* **web:** registration page array status messaging ([23ef5a9](https://github.com/unraid/api/commit/23ef5a975e0d5ff0c246c2df5e6c2cb38979d12a))
### Bug Fixes
* **api:** readme discord url ([ffd5c6a](https://github.com/unraid/api/commit/ffd5c6afb64956e76df22c77104a21bc22798008))
* keep minor enhancements from [#872](https://github.com/unraid/api/issues/872) ([#878](https://github.com/unraid/api/issues/878)) ([94a5aa8](https://github.com/unraid/api/commit/94a5aa87b9979fe0f02f884ac61298473bb3271a))
* plugin file deployment script ([780d87d](https://github.com/unraid/api/commit/780d87d6589a5469f47ac3fdfd50610ecfc394c8))
* prevent corrupt case model in state.php ([#874](https://github.com/unraid/api/issues/874)) ([4ad31df](https://github.com/unraid/api/commit/4ad31dfea9192146dbd2c90bc64a913c696ab0b7))
* prevent local dev from throwing ssl error ([051f647](https://github.com/unraid/api/commit/051f6474becf3b25b242cdc6ceee67247b79f8ba))
* rc.flashbackup needs to check both signed in and connected ([#882](https://github.com/unraid/api/issues/882)) ([ac8068c](https://github.com/unraid/api/commit/ac8068c9b084622d46fe2c9cb320b793c9ea8c52))
* update os cancel refresh on update page ([213c16b](https://github.com/unraid/api/commit/213c16ba3d5a84ebf4965f9d2f4024c66605a613))
* **web:** discord url ([1a6f4c6](https://github.com/unraid/api/commit/1a6f4c6db4ef0e5eefac467ec6583b14cb3546c4))
* **web:** lint unused rebootVersion ([e198ec9](https://github.com/unraid/api/commit/e198ec9d458e262c412c2dcb5a9d279238de1730))
* **web:** registration component remove unused ref ([76f556b](https://github.com/unraid/api/commit/76f556bd64b95ba96af795c9edfa045ebdff4444))
## [3.6.0](https://github.com/unraid/api/compare/v3.5.3...v3.6.0) (2024-03-26)
@@ -3401,4 +3621,4 @@ All notable changes to this project will be documented in this file. See [standa
# Changelog
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

View File

@@ -1,5 +1,5 @@
[api]
version="3.4.0"
version="3.8.1+d06e215a"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]

View File

@@ -1,11 +1,11 @@
[api]
version="3.4.0"
version="3.8.1+d06e215a"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
[remote]
wanaccess="yes"
wanaccess="no"
wanport="8443"
upnpEnabled="no"
apikey="_______________________BIG_API_KEY_HERE_________________________"
@@ -16,9 +16,9 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken=""
accesstoken=""
refreshtoken=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
dynamicRemoteAccessType="DISABLED"
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
dynamicRemoteAccessType="STATIC"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus]
minigraph="PRE_INIT"
minigraph="CONNECTED"

26
api/dev/states/nginx.ini Normal file
View File

@@ -0,0 +1,26 @@
NGINX_LANIP="192.168.1.150"
NGINX_LANIP6=""
NGINX_LANNAME="Tower"
NGINX_LANMDNS="Tower.local"
NGINX_CERTPATH="/boot/config/ssl/certs/certificate_bundle.pem"
NGINX_USESSL="yes"
NGINX_PORT="8080"
NGINX_PORTSSL="4443"
NGINX_DEFAULTURL="https://Tower.local:4443"
NGINX_CERTNAME="*.thisisfourtyrandomcharacters012345678900.myunraid.net"
NGINX_LANFQDN="192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net"
NGINX_LANFQDN6=""
NGINX_WANACCESS=""
NGINX_WANIP=""
NGINX_WANIP6=""
NGINX_WANFQDN="85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net"
NGINX_WANFQDN6=""
NGINX_WG0FQDN="10-252-0-1.hash.myunraid.net"
NGINX_WG1FQDN="10-252-1-1.hash.myunraid.net"
NGINX_WG3FQDN="10-253-3-1.hash.myunraid.net"
NGINX_WG4FQDN="10-253-4-1.hash.myunraid.net"
NGINX_WG55FQDN="10-253-5-1.hash.myunraid.net"
NGINX_TAILSCALEFQDN="10-100-0-1.hash.myunraid.net"
NGINX_TAILSCALE0FQDN="10-100-0-2.hash.myunraid.net"
NGINX_CUSTOMFQDN="10-123-1-2.hash.myunraid.net"
NGINX_CUSTOMFQDN6="221-123-121-112.hash.myunraid.net"

View File

@@ -24,14 +24,9 @@ x-volumes: &volumes
- ./fix-array-type.cjs:/app/fix-array-type.cjs
- /var/run/docker.sock:/var/run/docker.sock
networks:
mothership_default:
services:
dev:
networks:
- mothership_default
image: unraid-api:development
ports:
- "3001:3001"
@@ -51,8 +46,6 @@ services:
- builder
local:
networks:
- mothership_default
image: unraid-api:development
ports:
- "3001:3001"

229
api/docs/development.md Normal file
View File

@@ -0,0 +1,229 @@
# Development
## Installation
Install the [production](https://unraid-dl.sfo2.digitaloceanspaces.com/unraid-api/dynamix.unraid.net.plg) or [staging](https://unraid-dl.sfo2.digitaloceanspaces.com/unraid-api/dynamix.unraid.net.staging.plg) plugin on Unraid 6.9.0-rc1 or later (6.9.2 or higher recommended).
## Connecting to the API
### HTTP
This can be accessed by default via `http://tower.local/graphql`.
See <https://graphql.org/learn/serving-over-http/#http-methods-headers-and-body>
### WS
If you're using the ApolloClient please see <https://github.com/apollographql/subscriptions-transport-ws#full-websocket-transport> otherwise see <https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md>
<br>
<hr>
<br>
## Building in Docker
To get a development environment for testing start by running this docker command:
``docker compose run build-interactive``
which will give you an interactive shell inside of the newly build linux container.
To automatically build the plugin run the command below:
``docker compose run builder``
The builder command will build the plugin into deploy/release, and the interactive plugin lets you build the plugin or install node modules how you like.
## Logs
Logging can be configured via environment variables.
Log levels can be set when the api starts via `LOG_LEVEL=all/trace/debug/info/warn/error/fatal/mark/off`.
Additional detail for the log entry can be added with `LOG_CONTEXT=true` (warning, generates a lot of data).
By default, logs will be sent to syslog. Or you can set `LOG_TRANSPORT=file` to have logs saved in `/var/log/unraid-api/stdout.log`. Or enable debug mode to view logs inline.
Examples:
* `unraid-api start`
* `LOG_LEVEL=debug unraid-api start --debug`
* `LOG_LEVEL=trace LOG_CONTEXT=true LOG_TRANSPORT=file unraid-api start`
Log levels can be increased without restarting the api by issuing this command:
```
kill -s SIGUSR2 `pidof unraid-api`
```
and decreased via:
```
kill -s SIGUSR1 `pidof unraid-api`
```
<br>
<hr>
<br>
## Viewing data sent to mothership
If the environment variable `LOG_MOTHERSHIP_MESSAGES=true` exists, any data the unraid-api sends to mothership will be saved in clear text here: `/var/log/unraid-api/relay-messages.log`
Examples:
* `LOG_MOTHERSHIP_MESSAGES=true unraid-api start`
* `LOG_MOTHERSHIP_MESSAGES=true LOG_LEVEL=debug unraid-api start --debug`
<br>
<hr>
<br>
## Debug mode
Debug mode can be enabled with the `-d` or `--debug` flag.
This will enable the graphql playground and prevent the application starting as a daemon. Logs will be shown inline rather than saved to a file.
Examples:
* `unraid-api start --debug`
* `LOG_LEVEL=debug unraid-api start --debug`
<br>
<hr>
<br>
## Crash API On Demand
The `PLEASE_SEGFAULT_FOR_ME` env var can be to used to make the api crash after 30 seconds:
Examples:
* `PLEASE_SEGFAULT_FOR_ME=true LOG_LEVEL=debug unraid-api start --debug`
* `PLEASE_SEGFAULT_FOR_ME=true unraid-api start`
The crash log will be stored here:
* `/var/log/unraid-api/crash.log`
* `/var/log/unraid-api/crash.json`
`crash.json` just includes the most recent crash, while the reports get appended to `crash.log`.
<br>
<hr>
<br>
## Switching between staging and production environments
1. Stop the api: `unraid-api stop`
2. Switch environments: `unraid-api switch-env`
3. Start the api: `unraid-api start`
4. Confirm the environment: `unraid-api report`
<br>
<hr>
<br>
## Playground
The playground can be access via `http://tower.local/graphql` while in debug mode.
To get your API key open a terminal on your server and run `cat /boot/config/plugins/dynamix.my.servers/myservers.cfg | grep apikey=\"unraid | cut -d '"' -f2`. Add that API key in the "HTTP headers" panel of the playground.
```json
{
"x-api-key":"__REPLACE_ME_WITH_API_KEY__"
}
```
Next add the query you want to run and hit the play icon.
```gql
query welcome {
welcome {
message
}
}
```
You should get something like this back.
```json
{
"data": {
"welcome": {
"message": "Welcome root to this Unraid 6.10.0 server"
}
}
}
```
Click the "Schema" and "Docs" button on the right side of the playground to learn more.
For exploring the schema visually I'd suggest using [Voyager](https://apis.guru/graphql-voyager/) (click Change Schema -> Introspection, then copy/paste the introspection query into the local Graph Playground, and copy/paste the results back into Voyager).
<br>
<hr>
<br>
## Running this locally
```bash
MOTHERSHIP_RELAY_WS_LINK=ws://localhost:8000 \ # Switch to local copy of mothership
PATHS_UNRAID_DATA=$(pwd)/dev/data \ # Where we store plugin data (e.g. permissions.json)
PATHS_STATES=$(pwd)/dev/states \ # Where .ini files live (e.g. vars.ini)
PATHS_DYNAMIX_BASE=$(pwd)/dev/dynamix \ # Dynamix's data directory
PATHS_DYNAMIX_CONFIG=$(pwd)/dev/dynamix/dynamix.cfg \ # Dynamix's config file
PATHS_MY_SERVERS_CONFIG=$(pwd)/dev/Unraid.net/myservers.cfg \ # My servers config file
PORT=8500 \ # What port unraid-api should start on (e.g. /var/run/unraid-api.sock or 8000)
node dist/cli.js --debug # Enable debug logging
```
<br>
<hr>
<br>
## Create a new release
To create a new version run `npm run release` and then run **ONLY** the `git push` section of the commands it returns.
To create a new prerelease run `npm run release -- --prerelease alpha`.
Pushing to this repo will cause an automatic "rolling" release to be built which can be accessed via the page for the associated Github action run.
<br>
<hr>
<br>
## Using a custom version (e.g. testing a new release)
1. Install the staging or production plugin (links in the Installation section at the top of this file)
2. Download or build the api tgz file you want
* Download from [the releases page](https://github.com/unraid/api/releases)
* Build it on your local machine (``docker compose run builder``) and copy from the `deploy/release` folder
3. Copy the file to `/boot/config/plugins/dynamix.my.servers/unraid-api.tgz`.
4. Install the new api: `/etc/rc.d/rc.unraid-api (install / _install)`
* `_install` will no start the plugin for you after running, so you can make sure you launch in dev mode
* `install` will start the plugin after install
5. Start the api: `unraid-api start`
6. Confirm the version: `unraid-api report`
## Cloning Secrets from AWS
1. Go here to create security credentials for your user [S3 Creds](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1&skipRegion=true#/security_credentials)
2. Export your AWS secrets OR run `aws configure` to setup your environment
```sh
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=us-east-1
```
3. Set variables for staging and production to the ARN of the secret you would like to clone:
* `STAGING_SECRET_ID`
* `PRODUCTION_SECRET_ID`
4. Run `scripts/copy-env-from-aws.sh` to pull down the secrets into their respective files

4803
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "3.6.0",
"version": "3.11.0",
"main": "dist/index.js",
"bin": "dist/unraid-api.cjs",
"type": "module",
@@ -29,7 +29,7 @@
"build:docker": "./scripts/dc.sh run --rm builder",
"build-pkg": "./scripts/build.mjs",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen --config codegen.yml --watch -r dotenv/config",
"codegen:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 MOTHERSHIP_GRAPHQL_LINK='https://mothership.localhost/ws' graphql-codegen-esm --config codegen.yml --watch",
"tsc": "tsc --noEmit",
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
@@ -50,7 +50,8 @@
"build:dev": "./scripts/dc.sh build dev",
"start:local": "./scripts/dc.sh run --rm --service-ports local",
"start:ddev": "./scripts/dc.sh run --rm --service-ports dev",
"start:dtest": "./scripts/dc.sh run --rm builder npm run test"
"start:dtest": "./scripts/dc.sh run --rm builder npm run test",
"enter:ddev": "./scripts/dc.sh exec dev /bin/sh"
},
"files": [
".env.staging",
@@ -59,21 +60,21 @@
"unraid-api"
],
"dependencies": {
"@apollo/client": "^3.8.9",
"@apollo/server": "^4.10.0",
"@apollo/client": "^3.10.4",
"@apollo/server": "^4.10.4",
"@as-integrations/fastify": "^2.1.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/client-preset": "^4.2.5",
"@graphql-tools/load-files": "^7.0.0",
"@graphql-tools/merge": "^9.0.1",
"@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12",
"@nestjs/apollo": "^12.0.11",
"@nestjs/core": "^10.3.0",
"@nestjs/graphql": "^12.0.11",
"@graphql-tools/merge": "^9.0.4",
"@graphql-tools/schema": "^10.0.3",
"@graphql-tools/utils": "^10.2.0",
"@nestjs/apollo": "^12.1.0",
"@nestjs/core": "^10.3.8",
"@nestjs/graphql": "^12.1.1",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.0",
"@nestjs/schedule": "^4.0.0",
"@reduxjs/toolkit": "^2.0.1",
"@nestjs/platform-fastify": "^10.3.8",
"@nestjs/schedule": "^4.0.2",
"@reduxjs/toolkit": "^2.2.4",
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
"accesscontrol": "^2.2.1",
@@ -84,7 +85,7 @@
"bytes": "^3.1.2",
"cacheable-lookup": "^6.1.0",
"catch-exit": "^1.2.2",
"chokidar": "^3.5.3",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cli-table": "^0.3.11",
@@ -94,21 +95,22 @@
"cross-fetch": "^4.0.0",
"docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"find-process": "^1.4.7",
"global-agent": "^3.0.0",
"graphql": "^16.8.1",
"graphql-fields": "^2.0.3",
"graphql-scalars": "^1.22.4",
"graphql-scalars": "^1.23.0",
"graphql-subscriptions": "^2.0.0",
"graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2",
"graphql-type-uuid": "^0.2.0",
"graphql-ws": "^5.14.3",
"graphql-ws": "^5.16.0",
"htpasswd-js": "^1.0.2",
"ini": "^4.1.1",
"ip": "^1.1.8",
"jose": "^4.14.2",
"ini": "^4.1.2",
"ip": "^2.0.1",
"jose": "^5.3.0",
"lodash": "^4.17.21",
"multi-ini": "^2.2.0",
"mustache": "^4.2.0",
@@ -117,79 +119,79 @@
"nestjs-pino": "^4.0.0",
"node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2",
"openid-client": "^5.6.4",
"openid-client": "^5.6.5",
"p-iteration": "^1.1.8",
"p-retry": "^4.6.2",
"passport-http-header-strategy": "^1.1.0",
"pidusage": "^3.0.2",
"pino": "^8.17.2",
"pino": "^9.1.0",
"pino-http": "^9.0.0",
"pino-pretty": "^10.3.1",
"pino-pretty": "^11.0.0",
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.5.4",
"semver": "^7.6.2",
"stoppable": "^1.1.0",
"systeminformation": "^5.21.22",
"systeminformation": "^5.22.9",
"ts-command-line-args": "^2.5.1",
"uuid": "^9.0.1",
"ws": "^8.13.0",
"wtfnode": "^0.9.1",
"ws": "^8.17.0",
"wtfnode": "^0.9.2",
"xhr2": "^0.2.1",
"zod": "^3.22.4"
"zod": "^3.23.8"
},
"devDependencies": {
"@babel/runtime": "^7.23.8",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@babel/runtime": "^7.24.5",
"@graphql-codegen/add": "^5.0.2",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/fragment-matcher": "^5.0.2",
"@graphql-codegen/import-types-preset": "^3.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-resolvers": "4.0.1",
"@graphql-codegen/typed-document-node": "^5.0.6",
"@graphql-codegen/typescript": "^4.0.6",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@graphql-codegen/typescript-resolvers": "4.0.6",
"@graphql-typed-document-node/core": "^3.2.0",
"@nestjs/testing": "^10.3.0",
"@swc/core": "^1.3.102",
"@nestjs/testing": "^10.3.8",
"@swc/core": "^1.5.7",
"@types/async-exit-hook": "^2.0.2",
"@types/btoa": "^1.2.5",
"@types/bytes": "^3.1.4",
"@types/cli-table": "^0.3.4",
"@types/command-exists": "^1.2.3",
"@types/dockerode": "^3.3.16",
"@types/dockerode": "^3.3.29",
"@types/express": "^4.17.21",
"@types/graphql-fields": "^1.3.9",
"@types/graphql-type-uuid": "^0.2.6",
"@types/ini": "^4.1.0",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.1",
"@types/mustache": "^4.2.5",
"@types/node": "^20.11.0",
"@types/node": "^20.12.12",
"@types/pidusage": "^2.0.5",
"@types/pify": "^5.0.4",
"@types/semver": "^7.5.6",
"@types/semver": "^7.5.8",
"@types/sendmail": "^1.4.7",
"@types/stoppable": "^1.1.3",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.4",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"@types/wtfnode": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@unraid/eslint-config": "github:unraid/eslint-config",
"@vitest/coverage-v8": "^1.2.0",
"@vitest/ui": "^1.2.0",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"camelcase-keys": "^8.0.2",
"cz-conventional-changelog": "3.3.0",
"eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^50.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unused-imports": "^3.2.0",
"execa": "^7.1.1",
"filter-obj": "^5.1.0",
"got": "^13.0.0",
"graphql-codegen-typescript-validation-schema": "^0.12.1",
"got": "^13",
"graphql-codegen-typescript-validation-schema": "^0.14.1",
"ip-regex": "^5.0.0",
"json-difference": "^1.16.0",
"json-difference": "^1.16.1",
"map-obj": "^5.0.2",
"p-props": "^5.0.0",
"path-exists": "^5.0.0",
@@ -198,11 +200,11 @@
"pretty-bytes": "^6.1.1",
"pretty-ms": "^8.0.0",
"standard-version": "^9.5.0",
"tsup": "^8.0.1",
"typescript": "^5.3.3",
"tsup": "^8.0.2",
"typescript": "^5.4.5",
"typesync": "^0.12.1",
"vite-tsconfig-paths": "^4.2.3",
"vitest": "^1.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"zx": "^7.2.3"
},
"optionalDependencies": {

View File

@@ -1,4 +1,4 @@
#!/bin/sh
# Pass all entered params after the docker-compose call
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose -f docker-compose.yml "$@"
# Pass all entered params after the docker compose call
COMPOSE_PROJECT_NAME="connect" GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose -f docker-compose.yml "$@"

View File

@@ -1,3 +1,4 @@
import 'reflect-metadata';
import { expect, test } from 'vitest';
// Preloading imports for faster tests
@@ -6,17 +7,17 @@ import '@app/store/modules/emhttp';
import '@app/store';
test('Returns allowed origins', async () => {
const { store } = await import('@app/store');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
const { getAllowedOrigins } = await import('@app/common/allowed-origins');
const { loadConfigFile } = await import('@app/store/modules/config');
const { store } = await import('@app/store');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
const { getAllowedOrigins } = await import('@app/common/allowed-origins');
const { loadConfigFile } = await import('@app/store/modules/config');
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
// Get allowed origins
expect(getAllowedOrigins()).toMatchInlineSnapshot(`
// Get allowed origins
expect(getAllowedOrigins()).toMatchInlineSnapshot(`
[
"/var/run/unraid-notifications.sock",
"/var/run/unraid-php.sock",
@@ -33,6 +34,10 @@ test('Returns allowed origins', async () => {
"https://10-253-3-1.hash.myunraid.net:4443",
"https://10-253-4-1.hash.myunraid.net:4443",
"https://10-253-5-1.hash.myunraid.net:4443",
"https://10-100-0-1.hash.myunraid.net:4443",
"https://10-100-0-2.hash.myunraid.net:4443",
"https://10-123-1-2.hash.myunraid.net:4443",
"https://221-123-121-112.hash.myunraid.net:4443",
"https://google.com",
"https://test.com",
"https://connect.myunraid.net",

View File

@@ -1,109 +0,0 @@
import { expect, test, vi } from 'vitest';
import { store } from '@app/store';
import { loadStateFiles } from '@app/store/modules/emhttp';
vi.mock('@vmngr/libvirt', () => ({
ConnectListAllDomainsFlags: {
ACTIVE: 0,
INACTIVE: 1,
},
}));
vi.mock('@app/core/log', () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
},
dashboardLogger: {
info: vi.fn(),
error: vi.fn((...input) => console.log(input)),
debug: vi.fn(),
trace: vi.fn(),
},
emhttpLogger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
},
}));
vi.mock('@app/common/dashboard/boot-timestamp', () => ({
bootTimestamp: new Date('2022-06-10T04:35:58.276Z'),
}));
test('Returns generated data', async () => {
await store.dispatch(loadStateFiles()).unwrap();
const { generateData } = await import('@app/common/dashboard/generate-data');
const result = await generateData();
expect(result).toMatchInlineSnapshot(`
{
"apps": {
"installed": 0,
"started": 0,
},
"array": {
"capacity": {
"bytes": {
"free": 19495825571000,
"total": 41994745901000,
"used": 22498920330000,
},
},
"state": "STOPPED",
},
"config": {
"valid": true,
},
"display": {
"case": {
"base64": "",
"error": "",
"icon": "",
"url": "",
},
},
"os": {
"hostname": "Tower",
"uptime": "2022-06-10T04:35:58.276Z",
},
"services": [
{
"name": "unraid-api",
"online": true,
"uptime": {
"timestamp": "2022-06-10T04:35:58.276Z",
},
"version": "THIS_WILL_BE_REPLACED_WHEN_BUILT",
},
{
"name": "dynamic-remote-access",
"online": false,
"uptime": {
"timestamp": "2022-06-10T04:35:58.276Z",
},
"version": "DISABLED",
},
],
"vars": {
"flashGuid": "0000-0000-0000-000000000000",
"regState": "PRO",
"regTy": "PRO",
"serverDescription": "Dev Server",
"serverName": "Tower",
},
"versions": {
"unraid": "6.11.2",
},
"vms": {
"installed": 0,
"started": 0,
},
}
`);
}, 10_000);

View File

@@ -245,6 +245,24 @@ RolesBuilder {
"*",
],
},
"config": {
"read:any": [
"*",
],
},
"connect": {
"read:any": [
"*",
],
},
"connect/dynamic-remote-access": {
"read:any": [
"*",
],
"update:own": [
"*",
],
},
"customizations": {
"read:any": [
"*",
@@ -255,12 +273,22 @@ RolesBuilder {
"*",
],
},
"display": {
"read:any": [
"*",
],
},
"docker": {
"read:any": [
"*",
],
},
"docker/container": {
"read:any": [
"*",
],
},
"docker/network": {
"info": {
"read:any": [
"*",
],
@@ -270,16 +298,31 @@ RolesBuilder {
"*",
],
},
"network": {
"read:any": [
"*",
],
},
"notifications": {
"read:any": [
"*",
],
},
"services": {
"read:any": [
"*",
],
},
"unraid-version": {
"read:any": [
"*",
],
},
"vars": {
"read:any": [
"*",
],
},
"vms": {
"read:any": [
"*",

View File

@@ -10,10 +10,12 @@ test('Creates an array event', async () => {
);
const { store } = await import('@app/store');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
const { loadConfigFile } = await import('@app/store/modules/config');
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
const arrayEvent = getArrayData(store.getState);
expect(arrayEvent).toMatchInlineSnapshot(`
{
@@ -177,6 +179,7 @@ test('Creates an array event', async () => {
"warning": null,
},
],
"id": "97bbe87602982688216c367801f7aa24ea57350b44b7523160d01a9d48d6fcb9",
"parities": [
{
"comment": null,

View File

@@ -1,6 +1,8 @@
import 'reflect-metadata';
import { expect, test } from 'vitest';
import { setupPermissions } from '@app/core/permissions';
test('Returns default permissions', () => {
expect(setupPermissions()).toMatchSnapshot();
expect(setupPermissions()).toMatchSnapshot();
});

View File

@@ -1,12 +1,14 @@
import 'reflect-metadata';
import { test, expect } from 'vitest';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer';
import { initialState } from '@app/store/modules/config';
import { cloneDeep } from 'lodash';
test('it creates a FLASH config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
@@ -37,9 +39,9 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
});
test('it creates a MEMORY config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
@@ -74,15 +76,15 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
});
test('it creates a FLASH config with OPTIONAL values', () => {
const basicConfig = cloneDeep(initialState);
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
const basicConfig = cloneDeep(initialState);
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "myextra.origins",
@@ -118,15 +120,15 @@ test('it creates a FLASH config with OPTIONAL values', () => {
});
test('it creates a MEMORY config with OPTIONAL values', () => {
const basicConfig = cloneDeep(initialState);
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
const basicConfig = cloneDeep(initialState);
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
basicConfig.api.extraOrigins = 'myextra.origins';
basicConfig.remote.upnpEnabled = 'yes';
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "myextra.origins",

View File

@@ -1,3 +1,4 @@
import 'reflect-metadata';
import { checkMothershipAuthentication } from "@app/graphql/resolvers/query/cloud/check-mothership-authentication";
import { expect, test } from "vitest";
import packageJson from '@app/../package.json'

View File

@@ -143,37 +143,57 @@ test('integration test, loading nginx ini and generating all URLs', async () =>
},
{
"ipv4": "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443/",
"name": "LAN FQDN",
"name": "FQDN LAN",
"type": "LAN",
},
{
"ipv4": "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443/",
"name": "WAN FQDN",
"name": "FQDN WAN",
"type": "WAN",
},
{
"ipv4": "https://10-252-0-1.hash.myunraid.net:4443/",
"name": "WG FQDN 0",
"name": "FQDN WG 0",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-252-1-1.hash.myunraid.net:4443/",
"name": "WG FQDN 1",
"name": "FQDN WG 1",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-253-3-1.hash.myunraid.net:4443/",
"name": "WG FQDN 3",
"name": "FQDN WG 2",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-253-4-1.hash.myunraid.net:4443/",
"name": "WG FQDN 4",
"name": "FQDN WG 3",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-253-5-1.hash.myunraid.net:4443/",
"name": "WG FQDN 55",
"name": "FQDN WG 4",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-100-0-1.hash.myunraid.net:4443/",
"name": "FQDN TAILSCALE 0",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-100-0-2.hash.myunraid.net:4443/",
"name": "FQDN TAILSCALE 1",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-123-1-2.hash.myunraid.net:4443/",
"name": "FQDN CUSTOM 0",
"type": "WIREGUARD",
},
{
"ipv4": "https://221-123-121-112.hash.myunraid.net:4443/",
"name": "FQDN CUSTOM 1",
"type": "WIREGUARD",
},
]
@@ -181,8 +201,6 @@ test('integration test, loading nginx ini and generating all URLs', async () =>
expect(urls.errors).toMatchInlineSnapshot(`
[
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanIp6", is FQDN?: false],
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanFqdn6", is FQDN?: true],
[Error: No URL Provided],
]
`);
});

View File

@@ -104,10 +104,76 @@ test('After init returns values from cfg file for all fields', async () => {
"certificateName": "*.thisisfourtyrandomcharacters012345678900.myunraid.net",
"certificatePath": "/boot/config/ssl/certs/certificate_bundle.pem",
"defaultUrl": "https://Tower.local:4443",
"fqdnUrls": [
{
"fqdn": "192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net",
"id": null,
"interface": "LAN",
"isIpv6": false,
},
{
"fqdn": "85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net",
"id": null,
"interface": "WAN",
"isIpv6": false,
},
{
"fqdn": "10-252-0-1.hash.myunraid.net",
"id": 0,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-252-1-1.hash.myunraid.net",
"id": 1,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-253-3-1.hash.myunraid.net",
"id": 2,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-253-4-1.hash.myunraid.net",
"id": 3,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-253-5-1.hash.myunraid.net",
"id": 4,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-100-0-1.hash.myunraid.net",
"id": 0,
"interface": "TAILSCALE",
"isIpv6": false,
},
{
"fqdn": "10-100-0-2.hash.myunraid.net",
"id": 1,
"interface": "TAILSCALE",
"isIpv6": false,
},
{
"fqdn": "10-123-1-2.hash.myunraid.net",
"id": 0,
"interface": "CUSTOM",
"isIpv6": false,
},
{
"fqdn": "221-123-121-112.hash.myunraid.net",
"id": 1,
"interface": "CUSTOM",
"isIpv6": true,
},
],
"httpPort": 8080,
"httpsPort": 4443,
"lanFqdn": "192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net",
"lanFqdn6": "",
"lanIp": "192.168.1.150",
"lanIp6": "",
"lanMdns": "Tower.local",
@@ -115,31 +181,7 @@ test('After init returns values from cfg file for all fields', async () => {
"sslEnabled": true,
"sslMode": "yes",
"wanAccessEnabled": false,
"wanFqdn": "85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net",
"wanFqdn6": "",
"wanIp": "",
"wgFqdns": [
{
"fqdn": "10-252-0-1.hash.myunraid.net",
"id": 0,
},
{
"fqdn": "10-252-1-1.hash.myunraid.net",
"id": 1,
},
{
"fqdn": "10-253-3-1.hash.myunraid.net",
"id": 3,
},
{
"fqdn": "10-253-4-1.hash.myunraid.net",
"id": 4,
},
{
"fqdn": "10-253-5-1.hash.myunraid.net",
"id": 55,
},
],
}
`);
expect(disks).toMatchInlineSnapshot(`

View File

@@ -20,6 +20,7 @@ test('Returns paths', async () => {
"myservers-config",
"myservers-config-states",
"myservers-env",
"myservers-keepalive",
"keyfile-base",
"machine-id",
"log-base",

View File

@@ -0,0 +1,87 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Returns parsed state file 1`] = `
{
"certificateName": "*.thisisfourtyrandomcharacters012345678900.myunraid.net",
"certificatePath": "/boot/config/ssl/certs/certificate_bundle.pem",
"defaultUrl": "https://Tower.local:4443",
"fqdnUrls": [
{
"fqdn": "192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net",
"id": null,
"interface": "LAN",
"isIpv6": false,
},
{
"fqdn": "85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net",
"id": null,
"interface": "WAN",
"isIpv6": false,
},
{
"fqdn": "10-252-0-1.hash.myunraid.net",
"id": 0,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-252-1-1.hash.myunraid.net",
"id": 1,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-253-3-1.hash.myunraid.net",
"id": 2,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-253-4-1.hash.myunraid.net",
"id": 3,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-253-5-1.hash.myunraid.net",
"id": 4,
"interface": "WG",
"isIpv6": false,
},
{
"fqdn": "10-100-0-1.hash.myunraid.net",
"id": 0,
"interface": "TAILSCALE",
"isIpv6": false,
},
{
"fqdn": "10-100-0-2.hash.myunraid.net",
"id": 1,
"interface": "TAILSCALE",
"isIpv6": false,
},
{
"fqdn": "10-123-1-2.hash.myunraid.net",
"id": 0,
"interface": "CUSTOM",
"isIpv6": false,
},
{
"fqdn": "221-123-121-112.hash.myunraid.net",
"id": 1,
"interface": "CUSTOM",
"isIpv6": true,
},
],
"httpPort": 8080,
"httpsPort": 4443,
"lanIp": "192.168.1.150",
"lanIp6": "",
"lanMdns": "Tower.local",
"lanName": "Tower",
"sslEnabled": true,
"sslMode": "yes",
"wanAccessEnabled": false,
"wanIp": "",
}
`;

View File

@@ -0,0 +1,16 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { NginxIni } from '@app/store/state-parsers/nginx';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/nginx');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'nginx.ini');
const stateFile = parseConfig<NginxIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchSnapshot();
});

View File

@@ -68,7 +68,7 @@ const getRemoteAccessUrlsForAllowedOrigins = (
return [];
};
const getExtraOrigins = (): string[] => {
export const getExtraOrigins = (): string[] => {
const { extraOrigins } = getters.config().api;
if (extraOrigins) {
return extraOrigins

View File

@@ -1,152 +0,0 @@
import { ConnectListAllDomainsFlags } from '@vmngr/libvirt';
import { getHypervisor } from '@app/core/utils/vms/get-hypervisor';
import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version';
import { getArray } from '@app/common/dashboard/get-array';
import { bootTimestamp } from '@app/common/dashboard/boot-timestamp';
import { dashboardLogger } from '@app/core/log';
import { getters, store } from '@app/store';
import {
type DashboardServiceInput,
type DashboardInput,
} from '@app/graphql/generated/client/graphql';
import { API_VERSION } from '@app/environment';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { DashboardInputSchema } from '@app/graphql/generated/client/validators';
import { ZodError } from 'zod';
const getVmSummary = async (): Promise<DashboardInput['vms']> => {
try {
const hypervisor = await getHypervisor();
if (!hypervisor) {
return {
installed: 0,
started: 0,
};
}
const activeDomains = (await hypervisor.connectListAllDomains(
ConnectListAllDomainsFlags.ACTIVE
)) as unknown[];
const inactiveDomains = (await hypervisor.connectListAllDomains(
ConnectListAllDomainsFlags.INACTIVE
)) as unknown[];
return {
installed: activeDomains.length + inactiveDomains.length,
started: activeDomains.length,
};
} catch {
return {
installed: 0,
started: 0,
};
}
};
const getDynamicRemoteAccessService = (): DashboardServiceInput | null => {
const { config, dynamicRemoteAccess } = store.getState();
const enabledStatus = config.remote.dynamicRemoteAccessType;
return {
name: 'dynamic-remote-access',
online: enabledStatus !== DynamicRemoteAccessType.DISABLED,
version: dynamicRemoteAccess.runningType,
uptime: {
timestamp: bootTimestamp.toISOString(),
},
};
};
const services = (): DashboardInput['services'] => {
const dynamicRemoteAccess = getDynamicRemoteAccessService();
return [
{
name: 'unraid-api',
online: true,
uptime: {
timestamp: bootTimestamp.toISOString(),
},
version: API_VERSION,
},
...(dynamicRemoteAccess ? [dynamicRemoteAccess] : []),
];
};
const getData = async (): Promise<DashboardInput> => {
const emhttp = getters.emhttp();
const docker = getters.docker();
return {
vars: {
regState: emhttp.var.regState,
regTy: emhttp.var.regTy,
flashGuid: emhttp.var.flashGuid,
serverName: emhttp.var.name,
serverDescription: emhttp.var.comment,
},
apps: {
installed: docker.installed ?? 0,
started: docker.running ?? 0,
},
versions: {
unraid: await getUnraidVersion(),
},
os: {
hostname: emhttp.var.name,
uptime: bootTimestamp.toISOString(),
},
vms: await getVmSummary(),
array: getArray(),
services: services(),
display: {
case: {
url: '',
icon: '',
error: '',
base64: '',
},
},
config: emhttp.var.configValid
? { valid: true }
: {
valid: false,
error:
{
error: 'UNKNOWN_ERROR',
invalid: 'INVALID',
nokeyserver: 'NO_KEY_SERVER',
withdrawn: 'WITHDRAWN',
}[emhttp.var.configState] ?? 'UNKNOWN_ERROR',
},
};
};
export const generateData = async (): Promise<DashboardInput | null> => {
const data = await getData();
try {
// Validate generated data
// @TODO: Fix this runtype to use generated types from the Zod validators (as seen in mothership Codegen)
const result = DashboardInputSchema().parse(data);
return result;
} catch (error: unknown) {
// Log error for user
if (error instanceof ZodError) {
dashboardLogger.error(
'Failed validation with issues: ',
error.issues.map((issue) => ({
message: issue.message,
path: issue.path.join(','),
}))
);
} else {
dashboardLogger.error(
'Failed validating dashboard object: ',
error,
data
);
}
}
return null;
};

View File

@@ -34,6 +34,7 @@ export const FIVE_MINUTES_MS = 5 * ONE_MINUTE;
export const TEN_MINUTES_MS = 10 * ONE_MINUTE;
export const THIRTY_MINUTES_MS = 30 * ONE_MINUTE;
export const ONE_HOUR_MS = 60 * ONE_MINUTE;
export const ONE_DAY_MS = ONE_HOUR_MS * 24;
// Seconds
export const ONE_HOUR_SECS = 60 * 60;

View File

@@ -1,3 +1,4 @@
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import {
ArrayDiskType,
type ArrayCapacity,
@@ -56,6 +57,7 @@ export const getArrayData = (getState = store.getState): ArrayType => {
};
return {
id: getServerIdentifier('array'),
state: emhttp.var.mdState,
capacity,
boot,

View File

@@ -1,30 +0,0 @@
import type { CoreResult, CoreContext } from '@app/core/types';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { getShares } from '@app/core/utils/shares/get-shares';
/**
* Get all shares.
*/
export const getAllShares = async (context: CoreContext): Promise<CoreResult> => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'share',
action: 'read',
possession: 'any',
});
const userShares = getShares('users');
const diskShares = getShares('disks');
const shares = [
...userShares,
...diskShares,
];
return {
text: `Shares: ${JSON.stringify(shares, null, 2)}`,
json: shares,
};
};

View File

@@ -11,7 +11,6 @@ export * from './users';
export * from './vms';
export * from './add-share';
export * from './add-user';
export * from './get-all-shares';
export * from './get-apps';
export * from './get-devices';
export * from './get-disks';

View File

@@ -123,16 +123,38 @@ const roles: Record<string, Role> = {
extends: 'guest',
permissions: [
{ resource: 'array', action: 'read:any', attributes: '*' },
{ resource: 'config', action: 'read:any', attributes: '*' },
{ resource: 'connect', action: 'read:any', attributes: '*' },
{
resource: 'connect/dynamic-remote-access',
action: 'read:any',
attributes: '*',
},
{
resource: 'connect/dynamic-remote-access',
action: 'update:own',
attributes: '*',
},
{ resource: 'customizations', action: 'read:any', attributes: '*' },
{ resource: 'dashboard', action: 'read:any', attributes: '*' },
{ resource: 'display', action: 'read:any', attributes: '*' },
{
resource: 'docker/container',
action: 'read:any',
attributes: '*',
},
{ resource: 'docker', action: 'read:any', attributes: '*' },
{
resource: 'docker/container',
action: 'read:any',
attributes: '*',
},
{ resource: 'info', action: 'read:any', attributes: '*' },
{ resource: 'logs', action: 'read:any', attributes: '*' },
{ resource: 'docker/network', action: 'read:any', attributes: '*' },
{ resource: 'network', action: 'read:any', attributes: '*' },
{ resource: 'notifications', action: 'read:any', attributes: '*' },
{ resource: 'services', action: 'read:any', attributes: '*' },
{ resource: 'vars', action: 'read:any', attributes: '*' },
{ resource: 'vms', action: 'read:any', attributes: '*' },
{ resource: 'vms/domain', action: 'read:any', attributes: '*' },
{ resource: 'unraid-version', action: 'read:any', attributes: '*' },

View File

@@ -0,0 +1,23 @@
export interface FqdnEntry {
interface: string;
id: number | null;
fqdn: string;
isIpv6: boolean;
}
export interface Nginx {
certificateName: string;
certificatePath: string;
defaultUrl: string;
httpPort: number;
httpsPort: number;
lanIp: string;
lanIp6: string;
lanMdns: string;
lanName: string;
sslEnabled: boolean;
sslMode: 'yes' | 'no' | 'auto';
wanAccessEnabled: boolean;
wanIp: string;
fqdnUrls: FqdnEntry[];
}

View File

@@ -1,5 +1,5 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types';
import {
type SliceState as ConfigSliceState,
initialState,

View File

@@ -0,0 +1,6 @@
import { getters } from '@app/store/index';
import crypto from 'crypto';
export const getServerIdentifier = (domain: string | null = null): string => {
const config = getters.config();
return crypto.createHash('sha256').update(`${domain ? domain : ''}-${config.api.version}-${config.remote.apikey ?? config.upc.apikey}`).digest('hex');
};

View File

@@ -2,7 +2,7 @@
import * as Types from '@app/graphql/generated/api/types';
import { z } from 'zod'
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { AccessUrl, AccessUrlInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
type Properties<T> = Required<{
@@ -37,6 +37,8 @@ export const DiskInterfaceTypeSchema = z.nativeEnum(DiskInterfaceType);
export const DiskSmartStatusSchema = z.nativeEnum(DiskSmartStatus);
export const DynamicRemoteAccessTypeSchema = z.nativeEnum(DynamicRemoteAccessType);
export const ImportanceSchema = z.nativeEnum(Importance);
export const MemoryFormFactorSchema = z.nativeEnum(MemoryFormFactor);
@@ -55,6 +57,8 @@ export const TemperatureSchema = z.nativeEnum(Temperature);
export const ThemeSchema = z.nativeEnum(Theme);
export const URL_TYPESchema = z.nativeEnum(URL_TYPE);
export const VmStateSchema = z.nativeEnum(VmState);
export const WAN_ACCESS_TYPESchema = z.nativeEnum(WAN_ACCESS_TYPE);
@@ -65,6 +69,25 @@ export const mdStateSchema = z.nativeEnum(mdState);
export const registrationTypeSchema = z.nativeEnum(registrationType);
export function AccessUrlSchema(): z.ZodObject<Properties<AccessUrl>> {
return z.object({
__typename: z.literal('AccessUrl').optional(),
ipv4: definedNonNullAnySchema.nullish(),
ipv6: definedNonNullAnySchema.nullish(),
name: z.string().nullish(),
type: URL_TYPESchema
})
}
export function AccessUrlInputSchema(): z.ZodObject<Properties<AccessUrlInput>> {
return z.object({
ipv4: definedNonNullAnySchema.nullish(),
ipv6: definedNonNullAnySchema.nullish(),
name: z.string().nullish(),
type: URL_TYPESchema
})
}
export function AllowedOriginInputSchema(): z.ZodObject<Properties<AllowedOriginInput>> {
return z.object({
origins: z.array(z.string())
@@ -97,6 +120,7 @@ export function ArrayTypeSchema(): z.ZodObject<Properties<ArrayType>> {
caches: z.array(ArrayDiskSchema()),
capacity: ArrayCapacitySchema(),
disks: z.array(ArrayDiskSchema()),
id: z.string(),
parities: z.array(ArrayDiskSchema()),
pendingState: ArrayPendingStateSchema.nullish(),
previousState: ArrayStateSchema.nullish(),
@@ -195,10 +219,19 @@ export function ConfigSchema(): z.ZodObject<Properties<Config>> {
return z.object({
__typename: z.literal('Config').optional(),
error: ConfigErrorStateSchema.nullish(),
id: z.string(),
valid: z.boolean().nullish()
})
}
export function ConnectSchema(): z.ZodObject<Properties<Connect>> {
return z.object({
__typename: z.literal('Connect').optional(),
dynamicRemoteAccess: DynamicRemoteAccessStatusSchema(),
id: z.string()
})
}
export function ConnectSignInInputSchema(): z.ZodObject<Properties<ConnectSignInInput>> {
return z.object({
accessToken: z.string().nullish(),
@@ -300,6 +333,7 @@ export function DisplaySchema(): z.ZodObject<Properties<Display>> {
dashapps: z.string().nullish(),
date: z.string().nullish(),
hot: z.number().nullish(),
id: z.string(),
locale: z.string().nullish(),
max: z.number().nullish(),
number: z.string().nullish(),
@@ -317,6 +351,15 @@ export function DisplaySchema(): z.ZodObject<Properties<Display>> {
})
}
export function DockerSchema(): z.ZodObject<Properties<Docker>> {
return z.object({
__typename: z.literal('Docker').optional(),
containers: z.array(DockerContainerSchema()).nullish(),
id: z.string(),
networks: z.array(DockerNetworkSchema()).nullish()
})
}
export function DockerContainerSchema(): z.ZodObject<Properties<DockerContainer>> {
return z.object({
__typename: z.literal('DockerContainer').optional(),
@@ -359,6 +402,22 @@ export function DockerNetworkSchema(): z.ZodObject<Properties<DockerNetwork>> {
})
}
export function DynamicRemoteAccessStatusSchema(): z.ZodObject<Properties<DynamicRemoteAccessStatus>> {
return z.object({
__typename: z.literal('DynamicRemoteAccessStatus').optional(),
enabledType: DynamicRemoteAccessTypeSchema,
error: z.string().nullish(),
runningType: DynamicRemoteAccessTypeSchema
})
}
export function EnableDynamicRemoteAccessInputSchema(): z.ZodObject<Properties<EnableDynamicRemoteAccessInput>> {
return z.object({
enabled: z.boolean(),
url: z.lazy(() => AccessUrlInputSchema())
})
}
export function FlashSchema(): z.ZodObject<Properties<Flash>> {
return z.object({
__typename: z.literal('Flash').optional(),
@@ -389,10 +448,12 @@ export function InfoSchema(): z.ZodObject<Properties<Info>> {
cpu: InfoCpuSchema().nullish(),
devices: DevicesSchema().nullish(),
display: DisplaySchema().nullish(),
id: z.string(),
machineId: z.string().nullish(),
memory: InfoMemorySchema().nullish(),
os: OsSchema().nullish(),
system: SystemSchema().nullish(),
time: z.string(),
versions: VersionsSchema().nullish()
})
}
@@ -503,8 +564,10 @@ export function MountSchema(): z.ZodObject<Properties<Mount>> {
export function NetworkSchema(): z.ZodObject<Properties<Network>> {
return z.object({
__typename: z.literal('Network').optional(),
accessUrls: z.array(AccessUrlSchema()).nullish(),
carrierChanges: z.string().nullish(),
duplex: z.string().nullish(),
id: z.string(),
iface: z.string().nullish(),
ifaceName: z.string().nullish(),
internal: z.string().nullish(),
@@ -518,6 +581,12 @@ export function NetworkSchema(): z.ZodObject<Properties<Network>> {
})
}
export function NodeSchema(): z.ZodObject<Properties<Node>> {
return z.object({
id: z.string()
})
}
export function NotificationSchema(): z.ZodObject<Properties<Notification>> {
return z.object({
__typename: z.literal('Notification').optional(),
@@ -702,6 +771,15 @@ export function RelayResponseSchema(): z.ZodObject<Properties<RelayResponse>> {
})
}
export function RemoteAccessSchema(): z.ZodObject<Properties<RemoteAccess>> {
return z.object({
__typename: z.literal('RemoteAccess').optional(),
accessType: WAN_ACCESS_TYPESchema,
forwardType: WAN_FORWARD_TYPESchema.nullish(),
port: z.number().nullish()
})
}
export function ServerSchema(): z.ZodObject<Properties<Server>> {
return z.object({
__typename: z.literal('Server').optional(),
@@ -720,6 +798,7 @@ export function ServerSchema(): z.ZodObject<Properties<Server>> {
export function ServiceSchema(): z.ZodObject<Properties<Service>> {
return z.object({
__typename: z.literal('Service').optional(),
id: z.string(),
name: z.string().nullish(),
online: z.boolean().nullish(),
uptime: UptimeSchema().nullish(),
@@ -852,6 +931,15 @@ export function UserSchema(): z.ZodObject<Properties<User>> {
})
}
export function UserAccountSchema(): z.ZodObject<Properties<UserAccount>> {
return z.object({
description: z.string(),
id: z.string(),
name: z.string(),
roles: z.string()
})
}
export function VarsSchema(): z.ZodObject<Properties<Vars>> {
return z.object({
__typename: z.literal('Vars').optional(),
@@ -885,6 +973,7 @@ export function VarsSchema(): z.ZodObject<Properties<Vars>> {
fuseRememberDefault: z.string().nullish(),
fuseRememberStatus: z.string().nullish(),
hideDotFiles: z.boolean().nullish(),
id: z.string(),
joinStatus: z.string().nullish(),
localMaster: z.boolean().nullish(),
localTld: z.string().nullish(),

View File

@@ -20,9 +20,25 @@ export type Scalars = {
JSON: { input: { [key: string]: any }; output: { [key: string]: any }; }
Long: { input: number; output: number; }
Port: { input: number; output: number; }
URL: { input: URL; output: URL; }
UUID: { input: string; output: string; }
};
export type AccessUrl = {
__typename?: 'AccessUrl';
ipv4?: Maybe<Scalars['URL']['output']>;
ipv6?: Maybe<Scalars['URL']['output']>;
name?: Maybe<Scalars['String']['output']>;
type: URL_TYPE;
};
export type AccessUrlInput = {
ipv4?: InputMaybe<Scalars['URL']['input']>;
ipv6?: InputMaybe<Scalars['URL']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
type: URL_TYPE;
};
export type AllowedOriginInput = {
origins: Array<Scalars['String']['input']>;
};
@@ -42,7 +58,7 @@ export type ApiKeyResponse = {
valid: Scalars['Boolean']['output'];
};
export type ArrayType = {
export type ArrayType = Node & {
__typename?: 'Array';
/** Current boot disk */
boot?: Maybe<ArrayDisk>;
@@ -52,6 +68,7 @@ export type ArrayType = {
capacity: ArrayCapacity;
/** Data disks in the current array */
disks: Array<ArrayDisk>;
id: Scalars['ID']['output'];
/** Parity disks in the current array */
parities: Array<ArrayDisk>;
/** Array state after this query/mutation */
@@ -232,9 +249,10 @@ export type CloudResponse = {
status: Scalars['String']['output'];
};
export type Config = {
export type Config = Node & {
__typename?: 'Config';
error?: Maybe<ConfigErrorState>;
id: Scalars['ID']['output'];
valid?: Maybe<Scalars['Boolean']['output']>;
};
@@ -245,6 +263,12 @@ export enum ConfigErrorState {
WITHDRAWN = 'WITHDRAWN'
}
export type Connect = Node & {
__typename?: 'Connect';
dynamicRemoteAccess: DynamicRemoteAccessStatus;
id: Scalars['ID']['output'];
};
export type ConnectSignInInput = {
accessToken?: InputMaybe<Scalars['String']['input']>;
apiKey: Scalars['String']['input'];
@@ -359,6 +383,7 @@ export type Display = {
dashapps?: Maybe<Scalars['String']['output']>;
date?: Maybe<Scalars['String']['output']>;
hot?: Maybe<Scalars['Int']['output']>;
id: Scalars['ID']['output'];
locale?: Maybe<Scalars['String']['output']>;
max?: Maybe<Scalars['Int']['output']>;
number?: Maybe<Scalars['String']['output']>;
@@ -375,6 +400,13 @@ export type Display = {
wwn?: Maybe<Scalars['Boolean']['output']>;
};
export type Docker = Node & {
__typename?: 'Docker';
containers?: Maybe<Array<DockerContainer>>;
id: Scalars['ID']['output'];
networks?: Maybe<Array<DockerNetwork>>;
};
export type DockerContainer = {
__typename?: 'DockerContainer';
autoStart: Scalars['Boolean']['output'];
@@ -414,6 +446,24 @@ export type DockerNetwork = {
scope?: Maybe<Scalars['String']['output']>;
};
export type DynamicRemoteAccessStatus = {
__typename?: 'DynamicRemoteAccessStatus';
enabledType: DynamicRemoteAccessType;
error?: Maybe<Scalars['String']['output']>;
runningType: DynamicRemoteAccessType;
};
export enum DynamicRemoteAccessType {
DISABLED = 'DISABLED',
STATIC = 'STATIC',
UPNP = 'UPNP'
}
export type EnableDynamicRemoteAccessInput = {
enabled: Scalars['Boolean']['input'];
url: AccessUrlInput;
};
export type Flash = {
__typename?: 'Flash';
guid?: Maybe<Scalars['String']['output']>;
@@ -438,7 +488,7 @@ export enum Importance {
WARNING = 'WARNING'
}
export type Info = {
export type Info = Node & {
__typename?: 'Info';
/** Count of docker containers */
apps?: Maybe<InfoApps>;
@@ -446,11 +496,13 @@ export type Info = {
cpu?: Maybe<InfoCpu>;
devices?: Maybe<Devices>;
display?: Maybe<Display>;
id: Scalars['ID']['output'];
/** Machine ID */
machineId?: Maybe<Scalars['ID']['output']>;
memory?: Maybe<InfoMemory>;
os?: Maybe<Os>;
system?: Maybe<System>;
time: Scalars['DateTime']['output'];
versions?: Maybe<Versions>;
};
@@ -577,6 +629,7 @@ export type Mutation = {
connectSignOut: Scalars['Boolean']['output'];
/** Delete a user */
deleteUser?: Maybe<User>;
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
/** Get an existing API key */
getApiKey?: Maybe<ApiKey>;
login?: Maybe<Scalars['String']['output']>;
@@ -588,7 +641,6 @@ export type Mutation = {
removeDiskFromArray?: Maybe<ArrayType>;
/** Resume parity check */
resumeParityCheck?: Maybe<Scalars['JSON']['output']>;
sendNotification?: Maybe<Notification>;
setAdditionalAllowedOrigins: Array<Scalars['String']['output']>;
setupRemoteAccess: Scalars['Boolean']['output'];
shutdown?: Maybe<Scalars['String']['output']>;
@@ -635,6 +687,11 @@ export type MutationdeleteUserArgs = {
};
export type MutationenableDynamicRemoteAccessArgs = {
input: EnableDynamicRemoteAccessInput;
};
export type MutationgetApiKeyArgs = {
input?: InputMaybe<authenticateInput>;
name: Scalars['String']['input'];
@@ -657,11 +714,6 @@ export type MutationremoveDiskFromArrayArgs = {
};
export type MutationsendNotificationArgs = {
notification: NotificationInput;
};
export type MutationsetAdditionalAllowedOriginsArgs = {
input: AllowedOriginInput;
};
@@ -687,10 +739,12 @@ export type MutationupdateApikeyArgs = {
name: Scalars['String']['input'];
};
export type Network = {
export type Network = Node & {
__typename?: 'Network';
accessUrls?: Maybe<Array<AccessUrl>>;
carrierChanges?: Maybe<Scalars['String']['output']>;
duplex?: Maybe<Scalars['String']['output']>;
id: Scalars['ID']['output'];
iface?: Maybe<Scalars['String']['output']>;
ifaceName?: Maybe<Scalars['String']['output']>;
internal?: Maybe<Scalars['String']['output']>;
@@ -703,6 +757,10 @@ export type Network = {
type?: Maybe<Scalars['String']['output']>;
};
export type Node = {
id: Scalars['ID']['output'];
};
export type Notification = {
__typename?: 'Notification';
description: Scalars['String']['output'];
@@ -863,28 +921,34 @@ export type Query = {
array: ArrayType;
cloud?: Maybe<Cloud>;
config: Config;
connect: Connect;
/** Single disk */
disk?: Maybe<Disk>;
/** Mulitiple disks */
disks: Array<Maybe<Disk>>;
display?: Maybe<Display>;
docker: Docker;
/** All Docker containers */
dockerContainers: Array<DockerContainer>;
/** Docker network */
dockerNetwork: DockerNetwork;
/** All Docker networks */
dockerNetworks: Array<Maybe<DockerNetwork>>;
extraAllowedOrigins: Array<Scalars['String']['output']>;
flash?: Maybe<Flash>;
info?: Maybe<Info>;
/** Current user account */
me?: Maybe<Me>;
network?: Maybe<Network>;
notifications: Array<Notification>;
online?: Maybe<Scalars['Boolean']['output']>;
owner?: Maybe<Owner>;
parityHistory?: Maybe<Array<Maybe<ParityCheck>>>;
registration?: Maybe<Registration>;
remoteAccess: RemoteAccess;
server?: Maybe<Server>;
servers: Array<Server>;
services: Array<Service>;
/** Network Shares */
shares?: Maybe<Array<Maybe<Share>>>;
unassignedDevices?: Maybe<Array<Maybe<UnassignedDevice>>>;
@@ -990,6 +1054,13 @@ export type RelayResponse = {
timeout?: Maybe<Scalars['String']['output']>;
};
export type RemoteAccess = {
__typename?: 'RemoteAccess';
accessType: WAN_ACCESS_TYPE;
forwardType?: Maybe<WAN_FORWARD_TYPE>;
port?: Maybe<Scalars['Port']['output']>;
};
export type Server = {
__typename?: 'Server';
apikey: Scalars['String']['output'];
@@ -1009,8 +1080,9 @@ export enum ServerStatus {
ONLINE = 'online'
}
export type Service = {
export type Service = Node & {
__typename?: 'Service';
id: Scalars['ID']['output'];
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Uptime>;
@@ -1124,6 +1196,15 @@ export enum Theme {
WHITE = 'white'
}
export enum URL_TYPE {
DEFAULT = 'DEFAULT',
LAN = 'LAN',
MDNS = 'MDNS',
OTHER = 'OTHER',
WAN = 'WAN',
WIREGUARD = 'WIREGUARD'
}
export type UnassignedDevice = {
__typename?: 'UnassignedDevice';
devlinks?: Maybe<Scalars['String']['output']>;
@@ -1210,7 +1291,7 @@ export type UserAccount = {
roles: Scalars['String']['output'];
};
export type Vars = {
export type Vars = Node & {
__typename?: 'Vars';
bindMgt?: Maybe<Scalars['Boolean']['output']>;
cacheNumDevices?: Maybe<Scalars['Int']['output']>;
@@ -1244,6 +1325,7 @@ export type Vars = {
fuseRememberDefault?: Maybe<Scalars['String']['output']>;
fuseRememberStatus?: Maybe<Scalars['String']['output']>;
hideDotFiles?: Maybe<Scalars['Boolean']['output']>;
id: Scalars['ID']['output'];
joinStatus?: Maybe<Scalars['String']['output']>;
localMaster?: Maybe<Scalars['Boolean']['output']>;
localTld?: Maybe<Scalars['String']['output']>;
@@ -1568,11 +1650,14 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
/** Mapping of interface types */
export type ResolversInterfaceTypes<RefType extends Record<string, unknown>> = ResolversObject<{
Node: ( ArrayType ) | ( Config ) | ( Connect ) | ( Docker ) | ( Info ) | ( Network ) | ( Service ) | ( Vars );
UserAccount: ( Me ) | ( User );
}>;
/** Mapping between all available schema types and the resolvers types */
export type ResolversTypes = ResolversObject<{
AccessUrl: ResolverTypeWrapper<AccessUrl>;
AccessUrlInput: AccessUrlInput;
AllowedOriginInput: AllowedOriginInput;
ApiKey: ResolverTypeWrapper<ApiKey>;
ApiKeyResponse: ResolverTypeWrapper<ApiKeyResponse>;
@@ -1592,6 +1677,7 @@ export type ResolversTypes = ResolversObject<{
CloudResponse: ResolverTypeWrapper<CloudResponse>;
Config: ResolverTypeWrapper<Config>;
ConfigErrorState: ConfigErrorState;
Connect: ResolverTypeWrapper<Connect>;
ConnectSignInInput: ConnectSignInInput;
ConnectUserInfoInput: ConnectUserInfoInput;
ContainerHostConfig: ResolverTypeWrapper<ContainerHostConfig>;
@@ -1607,8 +1693,12 @@ export type ResolversTypes = ResolversObject<{
DiskPartition: ResolverTypeWrapper<DiskPartition>;
DiskSmartStatus: DiskSmartStatus;
Display: ResolverTypeWrapper<Display>;
Docker: ResolverTypeWrapper<Docker>;
DockerContainer: ResolverTypeWrapper<DockerContainer>;
DockerNetwork: ResolverTypeWrapper<DockerNetwork>;
DynamicRemoteAccessStatus: ResolverTypeWrapper<DynamicRemoteAccessStatus>;
DynamicRemoteAccessType: DynamicRemoteAccessType;
EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput;
Flash: ResolverTypeWrapper<Flash>;
Float: ResolverTypeWrapper<Scalars['Float']['output']>;
Gpu: ResolverTypeWrapper<Gpu>;
@@ -1631,6 +1721,7 @@ export type ResolversTypes = ResolversObject<{
Mount: ResolverTypeWrapper<Mount>;
Mutation: ResolverTypeWrapper<{}>;
Network: ResolverTypeWrapper<Network>;
Node: ResolverTypeWrapper<ResolversInterfaceTypes<ResolversTypes>['Node']>;
Notification: ResolverTypeWrapper<Notification>;
NotificationFilter: NotificationFilter;
NotificationInput: NotificationInput;
@@ -1646,6 +1737,7 @@ export type ResolversTypes = ResolversObject<{
Registration: ResolverTypeWrapper<Registration>;
RegistrationState: RegistrationState;
RelayResponse: ResolverTypeWrapper<RelayResponse>;
RemoteAccess: ResolverTypeWrapper<RemoteAccess>;
Server: ResolverTypeWrapper<Server>;
ServerStatus: ServerStatus;
Service: ResolverTypeWrapper<Service>;
@@ -1656,6 +1748,8 @@ export type ResolversTypes = ResolversObject<{
System: ResolverTypeWrapper<System>;
Temperature: Temperature;
Theme: Theme;
URL: ResolverTypeWrapper<Scalars['URL']['output']>;
URL_TYPE: URL_TYPE;
UUID: ResolverTypeWrapper<Scalars['UUID']['output']>;
UnassignedDevice: ResolverTypeWrapper<UnassignedDevice>;
Uptime: ResolverTypeWrapper<Uptime>;
@@ -1683,6 +1777,8 @@ export type ResolversTypes = ResolversObject<{
/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = ResolversObject<{
AccessUrl: AccessUrl;
AccessUrlInput: AccessUrlInput;
AllowedOriginInput: AllowedOriginInput;
ApiKey: ApiKey;
ApiKeyResponse: ApiKeyResponse;
@@ -1696,6 +1792,7 @@ export type ResolversParentTypes = ResolversObject<{
Cloud: Cloud;
CloudResponse: CloudResponse;
Config: Config;
Connect: Connect;
ConnectSignInInput: ConnectSignInInput;
ConnectUserInfoInput: ConnectUserInfoInput;
ContainerHostConfig: ContainerHostConfig;
@@ -1706,8 +1803,11 @@ export type ResolversParentTypes = ResolversObject<{
Disk: Disk;
DiskPartition: DiskPartition;
Display: Display;
Docker: Docker;
DockerContainer: DockerContainer;
DockerNetwork: DockerNetwork;
DynamicRemoteAccessStatus: DynamicRemoteAccessStatus;
EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput;
Flash: Flash;
Float: Scalars['Float']['output'];
Gpu: Gpu;
@@ -1726,6 +1826,7 @@ export type ResolversParentTypes = ResolversObject<{
Mount: Mount;
Mutation: {};
Network: Network;
Node: ResolversInterfaceTypes<ResolversParentTypes>['Node'];
Notification: Notification;
NotificationFilter: NotificationFilter;
NotificationInput: NotificationInput;
@@ -1739,6 +1840,7 @@ export type ResolversParentTypes = ResolversObject<{
Query: {};
Registration: Registration;
RelayResponse: RelayResponse;
RemoteAccess: RemoteAccess;
Server: Server;
Service: Service;
SetupRemoteAccessInput: SetupRemoteAccessInput;
@@ -1746,6 +1848,7 @@ export type ResolversParentTypes = ResolversObject<{
String: Scalars['String']['output'];
Subscription: {};
System: System;
URL: Scalars['URL']['output'];
UUID: Scalars['UUID']['output'];
UnassignedDevice: UnassignedDevice;
Uptime: Uptime;
@@ -1766,6 +1869,14 @@ export type ResolversParentTypes = ResolversObject<{
usersInput: usersInput;
}>;
export type AccessUrlResolvers<ContextType = Context, ParentType extends ResolversParentTypes['AccessUrl'] = ResolversParentTypes['AccessUrl']> = ResolversObject<{
ipv4?: Resolver<Maybe<ResolversTypes['URL']>, ParentType, ContextType>;
ipv6?: Resolver<Maybe<ResolversTypes['URL']>, ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
type?: Resolver<ResolversTypes['URL_TYPE'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ApiKeyResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ApiKey'] = ResolversParentTypes['ApiKey']> = ResolversObject<{
description?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
expiresAt?: Resolver<ResolversTypes['Long'], ParentType, ContextType>;
@@ -1786,6 +1897,7 @@ export type ArrayResolvers<ContextType = Context, ParentType extends ResolversPa
caches?: Resolver<Array<ResolversTypes['ArrayDisk']>, ParentType, ContextType>;
capacity?: Resolver<ResolversTypes['ArrayCapacity'], ParentType, ContextType>;
disks?: Resolver<Array<ResolversTypes['ArrayDisk']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
parities?: Resolver<Array<ResolversTypes['ArrayDisk']>, ParentType, ContextType>;
pendingState?: Resolver<Maybe<ResolversTypes['ArrayPendingState']>, ParentType, ContextType>;
previousState?: Resolver<Maybe<ResolversTypes['ArrayState']>, ParentType, ContextType>;
@@ -1868,10 +1980,17 @@ export type CloudResponseResolvers<ContextType = Context, ParentType extends Res
export type ConfigResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Config'] = ResolversParentTypes['Config']> = ResolversObject<{
error?: Resolver<Maybe<ResolversTypes['ConfigErrorState']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
valid?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ConnectResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Connect'] = ResolversParentTypes['Connect']> = ResolversObject<{
dynamicRemoteAccess?: Resolver<ResolversTypes['DynamicRemoteAccessStatus'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ContainerHostConfigResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ContainerHostConfig'] = ResolversParentTypes['ContainerHostConfig']> = ResolversObject<{
networkMode?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
@@ -1945,6 +2064,7 @@ export type DisplayResolvers<ContextType = Context, ParentType extends Resolvers
dashapps?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
date?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
hot?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
locale?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
max?: Resolver<Maybe<ResolversTypes['Int']>, ParentType, ContextType>;
number?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -1962,6 +2082,13 @@ export type DisplayResolvers<ContextType = Context, ParentType extends Resolvers
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DockerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Docker'] = ResolversParentTypes['Docker']> = ResolversObject<{
containers?: Resolver<Maybe<Array<ResolversTypes['DockerContainer']>>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
networks?: Resolver<Maybe<Array<ResolversTypes['DockerNetwork']>>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DockerContainerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerContainer'] = ResolversParentTypes['DockerContainer']> = ResolversObject<{
autoStart?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
command?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2000,6 +2127,13 @@ export type DockerNetworkResolvers<ContextType = Context, ParentType extends Res
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type DynamicRemoteAccessStatusResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DynamicRemoteAccessStatus'] = ResolversParentTypes['DynamicRemoteAccessStatus']> = ResolversObject<{
enabledType?: Resolver<ResolversTypes['DynamicRemoteAccessType'], ParentType, ContextType>;
error?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
runningType?: Resolver<ResolversTypes['DynamicRemoteAccessType'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type FlashResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Flash'] = ResolversParentTypes['Flash']> = ResolversObject<{
guid?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
product?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2024,10 +2158,12 @@ export type InfoResolvers<ContextType = Context, ParentType extends ResolversPar
cpu?: Resolver<Maybe<ResolversTypes['InfoCpu']>, ParentType, ContextType>;
devices?: Resolver<Maybe<ResolversTypes['Devices']>, ParentType, ContextType>;
display?: Resolver<Maybe<ResolversTypes['Display']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
machineId?: Resolver<Maybe<ResolversTypes['ID']>, ParentType, ContextType>;
memory?: Resolver<Maybe<ResolversTypes['InfoMemory']>, ParentType, ContextType>;
os?: Resolver<Maybe<ResolversTypes['Os']>, ParentType, ContextType>;
system?: Resolver<Maybe<ResolversTypes['System']>, ParentType, ContextType>;
time?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
versions?: Resolver<Maybe<ResolversTypes['Versions']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
@@ -2136,6 +2272,7 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
connectSignIn?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationconnectSignInArgs, 'input'>>;
connectSignOut?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
deleteUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationdeleteUserArgs, 'input'>>;
enableDynamicRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationenableDynamicRemoteAccessArgs, 'input'>>;
getApiKey?: Resolver<Maybe<ResolversTypes['ApiKey']>, ParentType, ContextType, RequireFields<MutationgetApiKeyArgs, 'name'>>;
login?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationloginArgs, 'password' | 'username'>>;
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationmountArrayDiskArgs, 'id'>>;
@@ -2143,7 +2280,6 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
reboot?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationremoveDiskFromArrayArgs>>;
resumeParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
sendNotification?: Resolver<Maybe<ResolversTypes['Notification']>, ParentType, ContextType, RequireFields<MutationsendNotificationArgs, 'notification'>>;
setAdditionalAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationsetAdditionalAllowedOriginsArgs, 'input'>>;
setupRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationsetupRemoteAccessArgs, 'input'>>;
shutdown?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2155,8 +2291,10 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
}>;
export type NetworkResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Network'] = ResolversParentTypes['Network']> = ResolversObject<{
accessUrls?: Resolver<Maybe<Array<ResolversTypes['AccessUrl']>>, ParentType, ContextType>;
carrierChanges?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
duplex?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
iface?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
ifaceName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
internal?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2170,6 +2308,11 @@ export type NetworkResolvers<ContextType = Context, ParentType extends Resolvers
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type NodeResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Node'] = ResolversParentTypes['Node']> = ResolversObject<{
__resolveType: TypeResolveFn<'Array' | 'Config' | 'Connect' | 'Docker' | 'Info' | 'Network' | 'Service' | 'Vars', ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
}>;
export type NotificationResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Notification'] = ResolversParentTypes['Notification']> = ResolversObject<{
description?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
@@ -2306,22 +2449,28 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
array?: Resolver<ResolversTypes['Array'], ParentType, ContextType>;
cloud?: Resolver<Maybe<ResolversTypes['Cloud']>, ParentType, ContextType>;
config?: Resolver<ResolversTypes['Config'], ParentType, ContextType>;
connect?: Resolver<ResolversTypes['Connect'], ParentType, ContextType>;
disk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<QuerydiskArgs, 'id'>>;
disks?: Resolver<Array<Maybe<ResolversTypes['Disk']>>, ParentType, ContextType>;
display?: Resolver<Maybe<ResolversTypes['Display']>, ParentType, ContextType>;
docker?: Resolver<ResolversTypes['Docker'], ParentType, ContextType>;
dockerContainers?: Resolver<Array<ResolversTypes['DockerContainer']>, ParentType, ContextType, Partial<QuerydockerContainersArgs>>;
dockerNetwork?: Resolver<ResolversTypes['DockerNetwork'], ParentType, ContextType, RequireFields<QuerydockerNetworkArgs, 'id'>>;
dockerNetworks?: Resolver<Array<Maybe<ResolversTypes['DockerNetwork']>>, ParentType, ContextType, Partial<QuerydockerNetworksArgs>>;
extraAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
flash?: Resolver<Maybe<ResolversTypes['Flash']>, ParentType, ContextType>;
info?: Resolver<Maybe<ResolversTypes['Info']>, ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['Me']>, ParentType, ContextType>;
network?: Resolver<Maybe<ResolversTypes['Network']>, ParentType, ContextType>;
notifications?: Resolver<Array<ResolversTypes['Notification']>, ParentType, ContextType, RequireFields<QuerynotificationsArgs, 'filter'>>;
online?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
owner?: Resolver<Maybe<ResolversTypes['Owner']>, ParentType, ContextType>;
parityHistory?: Resolver<Maybe<Array<Maybe<ResolversTypes['ParityCheck']>>>, ParentType, ContextType>;
registration?: Resolver<Maybe<ResolversTypes['Registration']>, ParentType, ContextType>;
remoteAccess?: Resolver<ResolversTypes['RemoteAccess'], ParentType, ContextType>;
server?: Resolver<Maybe<ResolversTypes['Server']>, ParentType, ContextType>;
servers?: Resolver<Array<ResolversTypes['Server']>, ParentType, ContextType>;
services?: Resolver<Array<ResolversTypes['Service']>, ParentType, ContextType>;
shares?: Resolver<Maybe<Array<Maybe<ResolversTypes['Share']>>>, ParentType, ContextType>;
unassignedDevices?: Resolver<Maybe<Array<Maybe<ResolversTypes['UnassignedDevice']>>>, ParentType, ContextType>;
user?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<QueryuserArgs, 'id'>>;
@@ -2347,6 +2496,13 @@ export type RelayResponseResolvers<ContextType = Context, ParentType extends Res
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type RemoteAccessResolvers<ContextType = Context, ParentType extends ResolversParentTypes['RemoteAccess'] = ResolversParentTypes['RemoteAccess']> = ResolversObject<{
accessType?: Resolver<ResolversTypes['WAN_ACCESS_TYPE'], ParentType, ContextType>;
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ServerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Server'] = ResolversParentTypes['Server']> = ResolversObject<{
apikey?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
guid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2361,6 +2517,7 @@ export type ServerResolvers<ContextType = Context, ParentType extends ResolversP
}>;
export type ServiceResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Service'] = ResolversParentTypes['Service']> = ResolversObject<{
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
online?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
uptime?: Resolver<Maybe<ResolversTypes['Uptime']>, ParentType, ContextType>;
@@ -2426,6 +2583,10 @@ export type SystemResolvers<ContextType = Context, ParentType extends ResolversP
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export interface URLScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['URL'], any> {
name: 'URL';
}
export interface UUIDScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['UUID'], any> {
name: 'UUID';
}
@@ -2545,6 +2706,7 @@ export type VarsResolvers<ContextType = Context, ParentType extends ResolversPar
fuseRememberDefault?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
fuseRememberStatus?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
hideDotFiles?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
joinStatus?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
localMaster?: Resolver<Maybe<ResolversTypes['Boolean']>, ParentType, ContextType>;
localTld?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
@@ -2708,6 +2870,7 @@ export type WelcomeResolvers<ContextType = Context, ParentType extends Resolvers
}>;
export type Resolvers<ContextType = Context> = ResolversObject<{
AccessUrl?: AccessUrlResolvers<ContextType>;
ApiKey?: ApiKeyResolvers<ContextType>;
ApiKeyResponse?: ApiKeyResponseResolvers<ContextType>;
Array?: ArrayResolvers<ContextType>;
@@ -2719,6 +2882,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Cloud?: CloudResolvers<ContextType>;
CloudResponse?: CloudResponseResolvers<ContextType>;
Config?: ConfigResolvers<ContextType>;
Connect?: ConnectResolvers<ContextType>;
ContainerHostConfig?: ContainerHostConfigResolvers<ContextType>;
ContainerMount?: ContainerMountResolvers<ContextType>;
ContainerPort?: ContainerPortResolvers<ContextType>;
@@ -2727,8 +2891,10 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Disk?: DiskResolvers<ContextType>;
DiskPartition?: DiskPartitionResolvers<ContextType>;
Display?: DisplayResolvers<ContextType>;
Docker?: DockerResolvers<ContextType>;
DockerContainer?: DockerContainerResolvers<ContextType>;
DockerNetwork?: DockerNetworkResolvers<ContextType>;
DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers<ContextType>;
Flash?: FlashResolvers<ContextType>;
Gpu?: GpuResolvers<ContextType>;
Info?: InfoResolvers<ContextType>;
@@ -2744,6 +2910,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Mount?: MountResolvers<ContextType>;
Mutation?: MutationResolvers<ContextType>;
Network?: NetworkResolvers<ContextType>;
Node?: NodeResolvers<ContextType>;
Notification?: NotificationResolvers<ContextType>;
Os?: OsResolvers<ContextType>;
Owner?: OwnerResolvers<ContextType>;
@@ -2755,11 +2922,13 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Query?: QueryResolvers<ContextType>;
Registration?: RegistrationResolvers<ContextType>;
RelayResponse?: RelayResponseResolvers<ContextType>;
RemoteAccess?: RemoteAccessResolvers<ContextType>;
Server?: ServerResolvers<ContextType>;
Service?: ServiceResolvers<ContextType>;
Share?: ShareResolvers<ContextType>;
Subscription?: SubscriptionResolvers<ContextType>;
System?: SystemResolvers<ContextType>;
URL?: GraphQLScalarType;
UUID?: GraphQLScalarType;
UnassignedDevice?: UnassignedDeviceResolvers<ContextType>;
Uptime?: UptimeResolvers<ContextType>;

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql.js';

View File

@@ -0,0 +1,52 @@
/* eslint-disable */
import * as types from './graphql.js';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n": types.sendRemoteGraphQLResponseDocument,
"\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n": types.RemoteGraphQLEventFragmentFragmentDoc,
"\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n": types.eventsDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n"): (typeof documents)["\n mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {\n remoteGraphQLResponse(input: $input)\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n"): (typeof documents)["\n fragment RemoteGraphQLEventFragment on RemoteGraphQLEvent {\n remoteGraphQLEventData: data {\n type\n body\n sha256\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n"): (typeof documents)["\n subscription events {\n events {\n __typename\n ... on ClientConnectedEvent {\n connectedData: data {\n type\n version\n apiKey\n }\n connectedEvent: type\n }\n ... on ClientDisconnectedEvent {\n disconnectedData: data {\n type\n version\n apiKey\n }\n disconnectedEvent: type\n }\n ...RemoteGraphQLEventFragment\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;

View File

@@ -468,6 +468,7 @@ export type Query = {
ksServers: Array<KsServerDetails>;
online?: Maybe<Scalars['Boolean']['output']>;
remoteQuery: Scalars['String']['output'];
serverStatus: ServerStatusResponse;
servers: Array<Maybe<Server>>;
status?: Maybe<ServerStatus>;
};
@@ -482,6 +483,11 @@ export type QueryremoteQueryArgs = {
input: RemoteGraphQLClientInput;
};
export type QueryserverStatusArgs = {
apiKey: Scalars['String']['input'];
};
export enum RegistrationState {
/** Basic */
BASIC = 'BASIC',
@@ -554,6 +560,10 @@ export type RemoteAccessInput = {
export type RemoteGraphQLClientInput = {
apiKey: Scalars['String']['input'];
body: Scalars['String']['input'];
/** Time in milliseconds to wait for a response from the remote server (defaults to 15000) */
timeout?: InputMaybe<Scalars['Int']['input']>;
/** How long mothership should cache the result of this query in seconds, only valid on queries */
ttl?: InputMaybe<Scalars['Int']['input']>;
};
export type RemoteGraphQLEvent = {
@@ -621,6 +631,13 @@ export enum ServerStatus {
ONLINE = 'online'
}
export type ServerStatusResponse = {
__typename?: 'ServerStatusResponse';
id: Scalars['ID']['output'];
lastPublish?: Maybe<Scalars['String']['output']>;
online: Scalars['Boolean']['output'];
};
export type Service = {
__typename?: 'Service';
name?: Maybe<Scalars['String']['output']>;
@@ -709,37 +726,6 @@ export type Vars = {
regTy?: Maybe<Scalars['String']['output']>;
};
export type updateDashboardMutationVariables = Exact<{
data: DashboardInput;
apiKey: Scalars['String']['input'];
}>;
export type updateDashboardMutation = { __typename?: 'Mutation', updateDashboard: { __typename?: 'Dashboard', apps?: { __typename?: 'DashboardApps', installed?: number | null } | null } };
export type sendNotificationMutationVariables = Exact<{
notification: NotificationInput;
apiKey: Scalars['String']['input'];
}>;
export type sendNotificationMutation = { __typename?: 'Mutation', sendNotification?: { __typename?: 'Notification', title?: string | null, subject?: string | null, description?: string | null, importance?: Importance | null, link?: string | null, status: NotificationStatus } | null };
export type updateNetworkMutationVariables = Exact<{
data: NetworkInput;
apiKey: Scalars['String']['input'];
}>;
export type updateNetworkMutation = { __typename?: 'Mutation', updateNetwork: { __typename?: 'Network', accessUrls?: Array<{ __typename?: 'AccessUrl', name?: string | null, type: URL_TYPE, ipv4?: URL | null, ipv6?: URL | null }> | null } };
export type sendRemoteAccessMutationMutationVariables = Exact<{
remoteAccess: RemoteAccessInput;
}>;
export type sendRemoteAccessMutationMutation = { __typename?: 'Mutation', remoteSession?: boolean | null };
export type sendRemoteGraphQLResponseMutationVariables = Exact<{
input: RemoteGraphQLServerInput;
}>;
@@ -749,24 +735,14 @@ export type sendRemoteGraphQLResponseMutation = { __typename?: 'Mutation', remot
export type RemoteGraphQLEventFragmentFragment = { __typename?: 'RemoteGraphQLEvent', remoteGraphQLEventData: { __typename?: 'RemoteGraphQLEventData', type: RemoteGraphQLEventType, body: string, sha256: string } } & { ' $fragmentName'?: 'RemoteGraphQLEventFragmentFragment' };
export type RemoteAccessEventFragmentFragment = { __typename?: 'RemoteAccessEvent', type: EventType, data: { __typename?: 'RemoteAccessEventData', type: RemoteAccessEventActionType, apiKey: string, url?: { __typename?: 'AccessUrl', type: URL_TYPE, name?: string | null, ipv4?: URL | null, ipv6?: URL | null } | null } } & { ' $fragmentName'?: 'RemoteAccessEventFragmentFragment' };
export type eventsSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type eventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | (
{ __typename: 'RemoteAccessEvent' }
& { ' $fragmentRefs'?: { 'RemoteAccessEventFragmentFragment': RemoteAccessEventFragmentFragment } }
) | (
export type eventsSubscription = { __typename?: 'Subscription', events?: Array<{ __typename: 'ClientConnectedEvent', connectedEvent: EventType, connectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientDisconnectedEvent', disconnectedEvent: EventType, disconnectedData: { __typename?: 'ClientConnectionEventData', type: ClientType, version: string, apiKey: string } } | { __typename: 'ClientPingEvent' } | { __typename: 'RemoteAccessEvent' } | (
{ __typename: 'RemoteGraphQLEvent' }
& { ' $fragmentRefs'?: { 'RemoteGraphQLEventFragmentFragment': RemoteGraphQLEventFragmentFragment } }
) | { __typename: 'UpdateEvent' }> | null };
export const RemoteGraphQLEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<RemoteGraphQLEventFragmentFragment, unknown>;
export const RemoteAccessEventFragmentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteAccessEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteAccessEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"ipv4"}},{"kind":"Field","name":{"kind":"Name","value":"ipv6"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}}]}}]} as unknown as DocumentNode<RemoteAccessEventFragmentFragment, unknown>;
export const updateDashboardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateDashboard"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DashboardInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateDashboard"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"auth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"apiKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"installed"}}]}}]}}]}}]} as unknown as DocumentNode<updateDashboardMutation, updateDashboardMutationVariables>;
export const sendNotificationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendNotification"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"notification"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NotificationInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sendNotification"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"notification"},"value":{"kind":"Variable","name":{"kind":"Name","value":"notification"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"auth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"apiKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"subject"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"importance"}},{"kind":"Field","name":{"kind":"Name","value":"link"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]} as unknown as DocumentNode<sendNotificationMutation, sendNotificationMutationVariables>;
export const updateNetworkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"updateNetwork"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"data"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"NetworkInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateNetwork"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"data"},"value":{"kind":"Variable","name":{"kind":"Name","value":"data"}}}],"directives":[{"kind":"Directive","name":{"kind":"Name","value":"auth"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"apiKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"apiKey"}}}]}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessUrls"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"ipv4"}},{"kind":"Field","name":{"kind":"Name","value":"ipv6"}}]}}]}}]}}]} as unknown as DocumentNode<updateNetworkMutation, updateNetworkMutationVariables>;
export const sendRemoteAccessMutationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteAccessMutation"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"remoteAccess"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteAccessInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"remoteAccess"},"value":{"kind":"Variable","name":{"kind":"Name","value":"remoteAccess"}}}]}]}}]} as unknown as DocumentNode<sendRemoteAccessMutationMutation, sendRemoteAccessMutationMutationVariables>;
export const sendRemoteGraphQLResponseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"sendRemoteGraphQLResponse"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteGraphQLResponse"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<sendRemoteGraphQLResponseMutation, sendRemoteGraphQLResponseMutationVariables>;
export const eventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteAccessEventFragment"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteAccessEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteAccessEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"url"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"ipv4"}},{"kind":"Field","name":{"kind":"Name","value":"ipv6"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<eventsSubscription, eventsSubscriptionVariables>;
export const eventsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientConnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"connectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"connectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ClientDisconnectedEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"disconnectedData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"disconnectedEvent"},"name":{"kind":"Name","value":"type"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteGraphQLEventFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteGraphQLEvent"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"remoteGraphQLEventData"},"name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"sha256"}}]}}]}}]} as unknown as DocumentNode<eventsSubscription, eventsSubscriptionVariables>;

View File

@@ -0,0 +1,7 @@
import { graphql } from '@app/graphql/generated/client/gql';
export const SEND_REMOTE_QUERY_RESPONSE = graphql(/* GraphQL */ `
mutation sendRemoteGraphQLResponse($input: RemoteGraphQLServerInput!) {
remoteGraphQLResponse(input: $input)
}
`);

View File

@@ -10,22 +10,6 @@ export const RemoteGraphQL_Fragment = graphql(/* GraphQL */ `
}
`);
export const RemoteAccess_Fragment = graphql(/* GraphQL */ `
fragment RemoteAccessEventFragment on RemoteAccessEvent {
type
data {
type
url {
type
name
ipv4
ipv6
}
apiKey
}
}
`);
export const EVENTS_SUBSCRIPTION = graphql(/* GraphQL */ `
subscription events {
events {
@@ -46,7 +30,6 @@ export const EVENTS_SUBSCRIPTION = graphql(/* GraphQL */ `
}
disconnectedEvent: type
}
...RemoteAccessEventFragment
...RemoteGraphQLEventFragment
}
}

View File

@@ -1,91 +0,0 @@
import { dashboardLogger } from '@app/core/log';
import { generateData } from '@app/common/dashboard/generate-data';
import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub';
import { getters, store } from '@app/store';
import { saveDataPacket } from '@app/store/modules/dashboard';
import { isEqual } from 'lodash';
import { GraphQLClient } from '@app/mothership/graphql-client';
import { SEND_DASHBOARD_PAYLOAD_MUTATION } from '../../mothership/mutations';
import { type DashboardInput } from '../../generated/client/graphql';
import { getDiff } from 'json-difference';
import { DEBUG } from '@app/environment';
import { isApolloError } from '@apollo/client/core';
const isNumberBetween = (min: number, max: number) => (num: number) => num > min && num < max;
const logAndReturn = <T>(returnValue: T, logLevel: 'info' | 'debug' | 'trace' | 'error', logLine: string, ...logParams: unknown[]): T => {
dashboardLogger[logLevel](logLine, ...logParams);
return returnValue;
};
const ONE_MB = 1_024 * 1_024;
const ONE_HUNDRED_MB = 100 * ONE_MB;
const canSendDataPacket = (dataPacket: DashboardInput | null) => {
const { lastDataPacketTimestamp, lastDataPacket } = getters.dashboard();
// Const { lastDataPacketTimestamp, lastDataPacketString, lastDataPacket } = dashboardStore;
if (!dataPacket) return logAndReturn(false, 'error', 'Not sending update to dashboard becuase the data packet is empty');
// UPDATE - No data packet has been sent since boot
if (!lastDataPacketTimestamp) return logAndReturn(true, 'debug', 'Sending update as none have been sent since the API started');
// NO_UPDATE - This is an exact copy of the last data packet
if (isEqual(dataPacket, lastDataPacket)) return logAndReturn(false, 'trace', '[NETWORK] Skipping Update');
if (!lastDataPacket) return logAndReturn(true, 'debug', 'Sending update as no data packets have been stored in state yet');
const difference = getDiff(lastDataPacket, dataPacket);
const oldBytesFree = lastDataPacket.array?.capacity.bytes?.free;
const newBytesFree = dataPacket.array?.capacity.bytes?.free;
if (oldBytesFree && newBytesFree && difference.added.length === 0 && difference.removed.length === 0 && difference.edited.length === 2) {
// If size has changed less than 100 MB (and nothing else has changed), don't send an update
const numberBetweenCheck = isNumberBetween((Number(oldBytesFree) * ONE_MB) - ONE_HUNDRED_MB, (Number(oldBytesFree) * ONE_MB) + ONE_HUNDRED_MB);
if (numberBetweenCheck(Number(newBytesFree) * ONE_MB)) {
logAndReturn(false, 'info', 'Size has not changed enough to send a new dashboard payload');
}
}
return logAndReturn(true, 'trace', 'Sending update because the packets are not equal');
};
export const publishToDashboard = async () => {
try {
const dataPacket = await generateData();
// Only update data on change
if (!canSendDataPacket(dataPacket)) return;
dashboardLogger.debug('New Data Packet Is: %o', dataPacket);
// Save new data packet
store.dispatch(saveDataPacket({ lastDataPacket: dataPacket }));
// Publish the updated data
dashboardLogger.trace({ dataPacket } , 'Publishing update');
// Update local clients
await pubsub.publish(PUBSUB_CHANNEL.DASHBOARD, {
dashboard: dataPacket,
});
if (dataPacket) {
const client = GraphQLClient.getInstance();
if (!client) {
throw new Error('Invalid Client');
}
// Update mothership
await client.mutate({ mutation: SEND_DASHBOARD_PAYLOAD_MUTATION, variables: { apiKey: getters.config().remote.apikey, data: dataPacket } });
} else {
dashboardLogger.error('DataPacket Was Empty');
}
} catch (error: unknown) {
if (error instanceof Error && isApolloError(error)) {
dashboardLogger.error('Failed publishing with GQL Errors: %s, \nClient Errors: %s', error.graphQLErrors.map(error => error.message).join(','), error.clientErrors.join(', '));
}
if (DEBUG) dashboardLogger.error(error);
}
};

View File

@@ -1,21 +1,14 @@
import { GraphQLClient } from '@app/mothership/graphql-client';
import { type Nginx } from '@app/core/types/states/nginx';
import { type RootState, store, getters } from '@app/store';
import { type RootState, store } from '@app/store';
import {
type NetworkInput,
URL_TYPE,
type AccessUrlInput,
} from '@app/graphql/generated/client/graphql';
import { dashboardLogger, logger } from '@app/core';
import { isEqual } from 'lodash';
import { SEND_NETWORK_MUTATION } from '@app/graphql/mothership/mutations';
import { saveNetworkPacket } from '@app/store/modules/dashboard';
import { ApolloError } from '@apollo/client/core/core.cjs';
import { logger } from '@app/core';
import {
AccessUrlInputSchema,
NetworkInputSchema,
} from '@app/graphql/generated/client/validators';
import { ZodError } from 'zod';
import { type AccessUrl } from '@app/graphql/generated/api/types';
interface UrlForFieldInput {
url: string;
@@ -72,10 +65,6 @@ export type NginxUrlFields = Extract<
| 'lanIp6'
| 'lanName'
| 'lanMdns'
| 'lanFqdn'
| 'lanFqdn6'
| 'wanFqdn'
| 'wanFqdn6'
>;
/**
@@ -126,10 +115,24 @@ export const getUrlForServer = ({
);
};
const getUrlTypeFromFqdn = (fqdnType: string): URL_TYPE => {
switch (fqdnType) {
case 'LAN':
return URL_TYPE.LAN;
case 'WAN':
return URL_TYPE.WAN;
case 'WG':
return URL_TYPE.WIREGUARD;
default:
// HACK: This should be added as a new type (e.g. OTHER or CUSTOM)
return URL_TYPE.WIREGUARD;
}
};
// eslint-disable-next-line complexity
export const getServerIps = (
state: RootState = store.getState()
): { urls: AccessUrlInput[]; errors: Error[] } => {
): { urls: AccessUrl[]; errors: Error[] } => {
const { nginx } = state.emhttp;
const {
remote: { wanport },
@@ -222,87 +225,22 @@ export const getServerIps = (
}
}
try {
// Lan FQDN URL
const lanFqdnUrl = getUrlForServer({ nginx, field: 'lanFqdn' });
urls.push({
name: 'LAN FQDN',
type: URL_TYPE.LAN,
ipv4: lanFqdnUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan FQDN6 URL
const lanFqdn6Url = getUrlForServer({ nginx, field: 'lanFqdn6' });
urls.push({
name: 'LAN FQDNv6',
type: URL_TYPE.LAN,
ipv6: lanFqdn6Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// WAN FQDN URL
const wanFqdnUrl = getUrlForField({
url: nginx.wanFqdn,
portSsl: Number(wanport || 443),
});
urls.push({
name: 'WAN FQDN',
type: URL_TYPE.WAN,
ipv4: wanFqdnUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// WAN FQDN6 URL
const wanFqdn6Url = getUrlForField({
url: nginx.wanFqdn6,
portSsl: Number(wanport),
});
urls.push({
name: 'WAN FQDNv6',
type: URL_TYPE.WAN,
ipv6: wanFqdn6Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
for (const wgFqdn of nginx.wgFqdns) {
// Now Process the FQDN Urls
nginx.fqdnUrls.forEach((fqdnUrl) => {
try {
// WG FQDN URL
const wgFqdnUrl = getUrlForField({
url: wgFqdn.fqdn,
portSsl: nginx.httpsPort,
const urlType = getUrlTypeFromFqdn(fqdnUrl.interface);
const fqdnUrlToUse = getUrlForField({
url: fqdnUrl.fqdn,
portSsl:
urlType === URL_TYPE.WAN
? Number(wanport)
: nginx.httpsPort,
});
urls.push({
name: `WG FQDN ${wgFqdn.id}`,
type: URL_TYPE.WIREGUARD,
ipv4: wgFqdnUrl,
name: `FQDN ${fqdnUrl.interface}${fqdnUrl.id !== null ? ` ${fqdnUrl.id}` : ''}`,
type: getUrlTypeFromFqdn(fqdnUrl.interface),
ipv4: fqdnUrlToUse,
});
} catch (error: unknown) {
if (error instanceof Error) {
@@ -311,7 +249,7 @@ export const getServerIps = (
logger.warn('Uncaught error in network resolver', error);
}
}
}
});
const safeUrls = urls
.map((url) => AccessUrlInputSchema().safeParse(url))
@@ -326,66 +264,3 @@ export const getServerIps = (
return { urls: safeUrls, errors };
};
export const publishNetwork = async () => {
try {
const client = GraphQLClient.getInstance();
const datapacket = getServerIps();
if (datapacket.errors) {
const zodErrors = datapacket.errors.filter(
(error) => error instanceof ZodError
);
if (zodErrors.length) {
dashboardLogger.warn(
'Validation Errors Encountered with Network Payload: %s',
zodErrors.map((error) => error.message).join(',')
);
}
}
const networkPacket: NetworkInput = { accessUrls: datapacket.urls };
const validatedNetwork = NetworkInputSchema().parse(networkPacket);
const { lastNetworkPacket } = getters.dashboard();
const { apikey: apiKey } = getters.config().remote;
if (
isEqual(
JSON.stringify(lastNetworkPacket),
JSON.stringify(validatedNetwork)
)
) {
dashboardLogger.trace('[DASHBOARD] Skipping Update');
} else if (client) {
dashboardLogger.info(
{ validatedNetwork },
'Sending data packet for network'
);
const result = await client.mutate({
mutation: SEND_NETWORK_MUTATION,
variables: {
apiKey,
data: validatedNetwork,
},
});
dashboardLogger.debug(
{ result },
'Sent network mutation with %s urls',
datapacket.urls.length
);
store.dispatch(
saveNetworkPacket({ lastNetworkPacket: validatedNetwork })
);
}
} catch (error: unknown) {
dashboardLogger.trace('ERROR', error);
if (error instanceof ApolloError) {
dashboardLogger.error(
'Failed publishing with GQL Errors: %s, \nClient Errors: %s',
error.graphQLErrors.map((error) => error.message).join(','),
error.clientErrors.join(', ')
);
} else {
dashboardLogger.error(error);
}
}
};

View File

@@ -31,7 +31,8 @@ input arrayDiskInput {
slot: Int
}
type Array {
type Array implements Node {
id: ID!
"""Array state before this query/mutation"""
previousState: ArrayState
"""Array state after this query/mutation"""

View File

@@ -3,6 +3,7 @@ scalar Long
scalar UUID
scalar DateTime
scalar Port
scalar URL
type Welcome {
message: String!
@@ -16,7 +17,6 @@ type Query {
type Mutation {
login(username: String!, password: String!): String
sendNotification(notification: NotificationInput!): Notification
shutdown: String
reboot: String
}
@@ -25,4 +25,9 @@ type Subscription {
ping: String!
info: Info!
online: Boolean!
}
# An object with a Globally Unique ID: see https://graphql.org/learn/global-object-identification/
interface Node {
id: ID!
}

View File

@@ -0,0 +1,20 @@
enum ConfigErrorState {
UNKNOWN_ERROR
INVALID
NO_KEY_SERVER
WITHDRAWN
}
type Config implements Node {
id: ID!
valid: Boolean
error: ConfigErrorState
}
type Query {
config: Config!
}
type Subscription {
config: Config!
}

View File

@@ -27,15 +27,52 @@ enum WAN_FORWARD_TYPE {
STATIC
}
type RemoteAccess {
accessType: WAN_ACCESS_TYPE!
forwardType: WAN_FORWARD_TYPE
port: Port
}
input SetupRemoteAccessInput {
accessType: WAN_ACCESS_TYPE!
forwardType: WAN_FORWARD_TYPE
port: Port
}
input EnableDynamicRemoteAccessInput {
url: AccessUrlInput!
enabled: Boolean!
}
enum DynamicRemoteAccessType {
STATIC
UPNP
DISABLED
}
type DynamicRemoteAccessStatus {
enabledType: DynamicRemoteAccessType!
runningType: DynamicRemoteAccessType!
error: String
}
type Connect implements Node {
id: ID!
dynamicRemoteAccess: DynamicRemoteAccessStatus!
}
type Query {
remoteAccess: RemoteAccess!
extraAllowedOrigins: [String!]!
connect: Connect!
}
type Mutation {
connectSignIn(input: ConnectSignInInput!): Boolean!
connectSignOut: Boolean!
enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean!
setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]!
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
}

View File

@@ -0,0 +1,19 @@
type Query {
display: Display
}
type Subscription {
display: Display
}
type Display {
id: ID!
case: Case
}
type Case {
icon: String
url: String
error: String
base64: String
}

View File

@@ -0,0 +1,9 @@
type Docker implements Node {
id: ID!
containers: [DockerContainer!]
networks: [DockerNetwork!]
}
type Query {
docker: Docker!
}

View File

@@ -0,0 +1,3 @@
type Info implements Node {
id: ID!
}

View File

@@ -0,0 +1,3 @@
type Info {
time: DateTime!
}

View File

@@ -0,0 +1,32 @@
enum URL_TYPE {
LAN
WIREGUARD
WAN
MDNS
OTHER
DEFAULT
}
input AccessUrlInput {
type: URL_TYPE!
name: String
ipv4: URL
ipv6: URL
}
type AccessUrl {
type: URL_TYPE!
name: String
ipv4: URL
ipv6: URL
}
type Query {
network: Network
}
type Network implements Node {
id: ID!
accessUrls: [AccessUrl!]
}

View File

@@ -22,10 +22,6 @@ input NotificationFilter {
limit: Int!
}
type Mutation {
sendNotification(notification: NotificationInput!): Notification
}
type Query {
notifications(filter: NotificationFilter!): [Notification!]!
}

View File

@@ -0,0 +1,19 @@
type Subscription {
service(name: String!): [Service!]
}
type Uptime {
timestamp: String
}
type Service implements Node {
id: ID!
name: String
online: Boolean
uptime: Uptime
version: String
}
type Query {
services: [Service!]!
}

View File

@@ -13,7 +13,8 @@ enum ConfigErrorState {
WITHDRAWN
}
type Vars {
type Vars implements Node {
id: ID!
"""
Unraid version
"""

View File

@@ -1,4 +1,6 @@
import 'reflect-metadata';
import 'global-agent/bootstrap';
import { am } from 'am';
import http from 'http';
import https from 'https';

View File

@@ -0,0 +1,88 @@
import { KEEP_ALIVE_INTERVAL_MS, ONE_MINUTE_MS } from '@app/consts';
import { minigraphLogger, mothershipLogger, remoteAccessLogger } from '@app/core/log';
import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types';
import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status';
import { store } from '@app/store/index';
import { setRemoteAccessRunningType } from '@app/store/modules/dynamic-remote-access';
import { clearSubscription } from '@app/store/modules/remote-graphql';
import { Cron, Expression, Initializer } from '@reflet/cron';
export class PingTimeoutJobs extends Initializer<typeof PingTimeoutJobs> {
@Cron.PreventOverlap
@Cron(Expression.EVERY_MINUTE)
@Cron.Start
async checkForPingTimeouts() {
const state = store.getState()
if (!isAPIStateDataFullyLoaded(state)) {
mothershipLogger.warn(
'State data not fully loaded, but job has been started'
);
return;
}
// Check for ping timeouts in remote graphql events
const subscriptionsToClear = state.remoteGraphQL.subscriptions.filter(
(subscription) =>
Date.now() - subscription.lastPing > KEEP_ALIVE_INTERVAL_MS
);
if (subscriptionsToClear.length > 0) {
mothershipLogger.debug(
`Clearing %s / %s subscriptions that are older than ${
KEEP_ALIVE_INTERVAL_MS / 1_000 / 60
} minutes`,
subscriptionsToClear.length,
state.remoteGraphQL.subscriptions.length
);
}
subscriptionsToClear.forEach((sub) =>
store.dispatch(clearSubscription(sub.sha256))
);
// Check for ping timeouts in mothership
if (
state.minigraph.lastPing &&
Date.now() - state.minigraph.lastPing > KEEP_ALIVE_INTERVAL_MS &&
state.minigraph.status === MinigraphStatus.CONNECTED
) {
minigraphLogger.error(
`NO PINGS RECEIVED IN ${
KEEP_ALIVE_INTERVAL_MS / 1_000 / 60
} MINUTES, SOCKET MUST BE RECONNECTED`
);
store.dispatch(
setGraphqlConnectionStatus({
status: MinigraphStatus.PING_FAILURE,
error: 'Ping Receive Exceeded Timeout',
})
);
}
// Check for ping timeouts from mothership events
if (
state.minigraph.selfDisconnectedSince &&
Date.now() - state.minigraph.selfDisconnectedSince >
KEEP_ALIVE_INTERVAL_MS &&
state.minigraph.status === MinigraphStatus.CONNECTED
) {
minigraphLogger.error(
`SELF DISCONNECTION EVENT NEVER CLEARED, SOCKET MUST BE RECONNECTED`
);
store.dispatch(
setGraphqlConnectionStatus({
status: MinigraphStatus.PING_FAILURE,
error: 'Received disconnect event for own server',
})
);
}
// Check for ping timeouts in remote access
if (state.dynamicRemoteAccess.lastPing && Date.now() - state.dynamicRemoteAccess.lastPing > ONE_MINUTE_MS) {
remoteAccessLogger.error(
`NO PINGS RECEIVED IN 1 MINUTE, REMOTE ACCESS MUST BE DISABLED`
);
store.dispatch(setRemoteAccessRunningType(DynamicRemoteAccessType.DISABLED));
}
}
}

View File

@@ -2,20 +2,14 @@
import { minigraphLogger, mothershipLogger } from '@app/core/log';
import { GraphQLClient } from './graphql-client';
import { store } from '@app/store';
import {
startDashboardProducer,
stopDashboardProducer,
} from '@app/store/modules/dashboard';
import {
EVENTS_SUBSCRIPTION,
RemoteAccess_Fragment,
RemoteGraphQL_Fragment,
} from '@app/graphql/mothership/subscriptions';
import { ClientType } from '@app/graphql/generated/client/graphql';
import { notNull } from '@app/utils';
import { handleRemoteAccessEvent } from '@app/store/actions/handle-remote-access-event';
import { useFragment } from '@app/graphql/generated/client/fragment-masking';
import { handleRemoteGraphQLEvent } from '@app/store/actions/handle-remote-graphql-event';
import {
@@ -41,7 +35,10 @@ export const subscribeToEvents = async (apiKey: string) => {
errors.join(',')
);
} else if (data) {
mothershipLogger.trace({ events: data.events }, 'Got events from mothership');
mothershipLogger.trace(
{ events: data.events },
'Got events from mothership'
);
for (const event of data.events?.filter(notNull) ?? []) {
switch (event.__typename) {
@@ -57,15 +54,6 @@ export const subscribeToEvents = async (apiKey: string) => {
}
}
// Dashboard Connected to Mothership
if (
type === ClientType.DASHBOARD &&
apiKey === eventApiKey
) {
store.dispatch(startDashboardProducer());
}
break;
}
@@ -80,32 +68,6 @@ export const subscribeToEvents = async (apiKey: string) => {
}
}
// The dashboard was closed or went idle
if (
type === ClientType.DASHBOARD &&
apiKey === eventApiKey
) {
store.dispatch(stopDashboardProducer());
}
break;
}
case 'RemoteAccessEvent': {
const eventAsRemoteAccessEvent = useFragment(
RemoteAccess_Fragment,
event
);
if (eventAsRemoteAccessEvent.data.apiKey === apiKey) {
void store.dispatch(
handleRemoteAccessEvent(
eventAsRemoteAccessEvent
)
);
}
break;
}
@@ -122,10 +84,6 @@ export const subscribeToEvents = async (apiKey: string) => {
break;
}
case 'UpdateEvent': {
break;
}
default:
break;
}

View File

@@ -0,0 +1,34 @@
import { type AccessUrl } from '@app/graphql/generated/api/types';
import { type AppDispatch, type RootState } from '@app/store/index';
export interface GenericRemoteAccess {
beginRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}): Promise<AccessUrl | null>;
stopRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}): Promise<void>;
getRemoteAccessUrl({
getState,
}: {
getState: () => RootState;
}): AccessUrl | null;
}
export interface IRemoteAccessController extends GenericRemoteAccess {
extendRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}): void;
}

View File

@@ -0,0 +1,55 @@
import { remoteAccessLogger } from '@app/core/log';
import { type AccessUrl, DynamicRemoteAccessType, URL_TYPE } from '@app/graphql/generated/api/types';
import { getServerIps } from '@app/graphql/resolvers/subscription/network';
import { type GenericRemoteAccess } from '@app/remoteAccess/handlers/remote-access-interface';
import { setWanAccessAndReloadNginx } from '@app/store/actions/set-wan-access-with-reload';
import { type AppDispatch, type RootState } from '@app/store/index';
export class StaticRemoteAccess implements GenericRemoteAccess {
public getRemoteAccessUrl({
getState,
}: {
getState: () => RootState;
}): AccessUrl | null {
const url = getServerIps(getState()).urls.find(
(url) => url.type === URL_TYPE.WAN
);
return url ?? null;
}
async beginRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}): Promise<AccessUrl | null> {
const {
config: {
remote: { dynamicRemoteAccessType },
},
} = getState();
if (dynamicRemoteAccessType === DynamicRemoteAccessType.STATIC) {
remoteAccessLogger.debug(
'Enabling remote access for Static Client'
);
await dispatch(setWanAccessAndReloadNginx('yes'));
return this.getRemoteAccessUrl({ getState });
}
throw new Error(
'Invalid Parameters Passed to Static Remote Access Enabler'
);
}
async stopRemoteAccess({
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}): Promise<void> {
await dispatch(setWanAccessAndReloadNginx('no'));
}
}

View File

@@ -0,0 +1,77 @@
import { remoteAccessLogger } from '@app/core/log';
import { type AccessUrl, DynamicRemoteAccessType, URL_TYPE } from '@app/graphql/generated/api/types';
import { getServerIps } from '@app/graphql/resolvers/subscription/network';
import { type GenericRemoteAccess } from '@app/remoteAccess/handlers/remote-access-interface';
import { setWanAccessAndReloadNginx } from '@app/store/actions/set-wan-access-with-reload';
import { type AppDispatch, type RootState } from '@app/store/index';
import { disableUpnp, enableUpnp } from '@app/store/modules/upnp';
export class UpnpRemoteAccess implements GenericRemoteAccess {
async stopRemoteAccess({
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}) {
// Stop
await dispatch(disableUpnp());
await dispatch(setWanAccessAndReloadNginx('no'));
}
public getRemoteAccessUrl({ getState }: { getState: () => RootState }): AccessUrl | null {
const urlsForServer = getServerIps(getState());
const url = urlsForServer.urls.find((url) => url.type === URL_TYPE.WAN);
return url ?? null;
}
async beginRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}) {
// Stop Close Event
const state = getState();
const { dynamicRemoteAccessType } = state.config.remote;
if (
dynamicRemoteAccessType === DynamicRemoteAccessType.UPNP &&
!state.upnp.upnpEnabled
) {
const { portssl } = state.emhttp.var;
try {
const upnpEnableResult = await dispatch(
enableUpnp({ portssl })
).unwrap();
await dispatch(setWanAccessAndReloadNginx('yes'));
remoteAccessLogger.debug(
'UPNP Enable Result',
upnpEnableResult
);
if (!upnpEnableResult.wanPortForUpnp) {
throw new Error('Failed to get a WAN Port from UPNP');
}
return this.getRemoteAccessUrl({ getState });
} catch (error: unknown) {
remoteAccessLogger.warn(
'Caught error, disabling UPNP and re-throwing'
);
await this.stopRemoteAccess({ dispatch, getState });
throw new Error(
`UPNP Dynamic Remote Access Error: ${
error instanceof Error ? error.message : 'Unknown Error'
}`
);
}
}
throw new Error(
'Invalid Parameters Passed to UPNP Remote Access Enabler'
);
}
}

View File

@@ -0,0 +1,150 @@
import { remoteAccessLogger } from '@app/core/log';
import { UnraidLocalNotifier } from '@app/core/notifiers/unraid-local';
import {
type AccessUrl,
DynamicRemoteAccessType,
} from '@app/graphql/generated/api/types';
import { type IRemoteAccessController } from '@app/remoteAccess/handlers/remote-access-interface';
import { StaticRemoteAccess } from '@app/remoteAccess/handlers/static-remote-access';
import { UpnpRemoteAccess } from '@app/remoteAccess/handlers/upnp-remote-access';
import { getters, type AppDispatch, type RootState } from '@app/store/index';
import {
clearPing,
receivedPing,
setDynamicRemoteAccessError,
setRemoteAccessRunningType,
} from '@app/store/modules/dynamic-remote-access';
export class RemoteAccessController implements IRemoteAccessController {
static _instance: RemoteAccessController | null = null;
activeRemoteAccess: UpnpRemoteAccess | StaticRemoteAccess | null = null;
notifier: UnraidLocalNotifier = new UnraidLocalNotifier({ level: 'info' });
// eslint-disable-next-line @typescript-eslint/no-useless-constructor, @typescript-eslint/no-empty-function
constructor() {}
public static get instance(): RemoteAccessController {
if (!RemoteAccessController._instance) {
RemoteAccessController._instance = new RemoteAccessController();
}
return RemoteAccessController._instance;
}
getRunningRemoteAccessType() {
return getters.dynamicRemoteAccess().runningType;
}
public getRemoteAccessUrl({
getState,
}: {
getState: () => RootState;
}): AccessUrl | null {
if (!this.activeRemoteAccess) {
return null;
}
return this.activeRemoteAccess.getRemoteAccessUrl({ getState });
}
async beginRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}) {
const state = getState();
const {
config: {
remote: { dynamicRemoteAccessType },
},
dynamicRemoteAccess: { runningType },
} = state;
if (!dynamicRemoteAccessType) {
// Should never get here
return null;
}
remoteAccessLogger.debug(
'Beginning remote access',
runningType,
dynamicRemoteAccessType
);
if (runningType !== dynamicRemoteAccessType) {
await this.activeRemoteAccess?.stopRemoteAccess({
getState,
dispatch,
});
}
switch (dynamicRemoteAccessType) {
case DynamicRemoteAccessType.DISABLED:
this.activeRemoteAccess = null;
remoteAccessLogger.debug(
'Received begin event, but DRA is disabled.'
);
break;
case DynamicRemoteAccessType.UPNP:
remoteAccessLogger.debug('UPNP DRA Begin');
this.activeRemoteAccess = new UpnpRemoteAccess();
break;
case DynamicRemoteAccessType.STATIC:
remoteAccessLogger.debug('Static DRA Begin');
this.activeRemoteAccess = new StaticRemoteAccess();
break;
default:
break;
}
// Essentially a super call to the active type
try {
await this.activeRemoteAccess?.beginRemoteAccess({
getState,
dispatch,
});
dispatch(setRemoteAccessRunningType(dynamicRemoteAccessType));
this.extendRemoteAccess({ getState, dispatch });
await this.notifier.send({
title: 'Remote Access Started',
data: { message: 'Remote access has been started' },
});
} catch (error: unknown) {
dispatch(
setDynamicRemoteAccessError(
error instanceof Error ? error.message : 'Unknown Error'
)
);
}
return null;
}
public extendRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}) {
dispatch(receivedPing());
return this.getRemoteAccessUrl({ getState });
}
async stopRemoteAccess({
getState,
dispatch,
}: {
getState: () => RootState;
dispatch: AppDispatch;
}) {
remoteAccessLogger.debug('Stopping remote access');
dispatch(clearPing());
await this.activeRemoteAccess?.stopRemoteAccess({ getState, dispatch });
dispatch(setRemoteAccessRunningType(DynamicRemoteAccessType.DISABLED));
await this.notifier.send({
title: 'Remote Access Stopped',
data: { message: 'Remote access has been stopped' },
});
}
}

View File

@@ -1,5 +0,0 @@
export enum DynamicRemoteAccessType {
UPNP = 'UPNP',
STATIC = 'STATIC',
DISABLED = 'DISABLED',
}

View File

@@ -1,50 +0,0 @@
import { remoteAccessLogger } from '@app/core/log';
import { RemoteAccessEventActionType, type RemoteAccessEventFragmentFragment } from '@app/graphql/generated/client/graphql';
import { RemoteAccessController } from '@app/remoteAccess/remote-access-controller';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { type AppDispatch, type RootState } from '@app/store/index';
import { setAllowedRemoteAccessUrls } from '@app/store/modules/dynamic-remote-access';
import { createAsyncThunk } from '@reduxjs/toolkit';
export const handleRemoteAccessEvent = createAsyncThunk<void, RemoteAccessEventFragmentFragment, { state: RootState; dispatch: AppDispatch }>('dynamicRemoteAccess/handleRemoteAccessEvent', async (event, { getState, dispatch }) => {
const state = getState();
const pluginApiKey = state.config.remote.apikey;
if (pluginApiKey !== event.data.apiKey) {
remoteAccessLogger.error('Remote Access Event Not For This Client');
return;
}
const { dynamicRemoteAccessType } = state.config.remote;
if (!dynamicRemoteAccessType || dynamicRemoteAccessType === DynamicRemoteAccessType.DISABLED) {
remoteAccessLogger.error('Received Remote Access Event, but Dynamic Remote Access is not enabled.');
return;
}
switch (event.data.type) {
case RemoteAccessEventActionType.INIT:
remoteAccessLogger.debug('Init Event');
// Init - Begin listening, transmit an ACK event back from the client.
if (event.data.url) {
// @todo use this URL to set the only allowed access url
dispatch(setAllowedRemoteAccessUrls(event.data.url));
}
await RemoteAccessController.instance.beginRemoteAccess({ getState, dispatch });
// @TODO Move this logic into the remote access manager class
break;
case RemoteAccessEventActionType.ACK:
// Ack - these events come from the API (this client), so we don't need to respond
break;
case RemoteAccessEventActionType.PING:
// Ping - would continue remote access if necessary;
RemoteAccessController.instance.extendRemoteAccess({ dispatch });
break;
case RemoteAccessEventActionType.END:
// End
await RemoteAccessController.instance.stopRemoteAccess({ getState, dispatch });
break;
default:
break;
}
});

View File

@@ -1,9 +1,9 @@
import {
DynamicRemoteAccessType,
type SetupRemoteAccessInput,
WAN_ACCESS_TYPE,
WAN_FORWARD_TYPE,
} from '@app/graphql/generated/api/types';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { type AppDispatch, type RootState } from '@app/store/index';
import { type MyServersConfig } from '@app/types/my-servers-config';
import { createAsyncThunk } from '@reduxjs/toolkit';

View File

@@ -1,6 +1,5 @@
import { logDestination, logger } from '@app/core/log';
import { MinigraphStatus } from '@app/graphql/generated/api/types';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status';
import { store } from '@app/store/index';
import { stopListeners } from '@app/store/listeners/stop-listeners';

View File

@@ -5,7 +5,6 @@ import { configReducer } from '@app/store/modules/config';
import { emhttp } from '@app/store/modules/emhttp';
import { registration } from '@app/store/modules/registration';
import { cache } from '@app/store/modules/cache';
import { dashboard } from '@app/store/modules/dashboard';
import { docker } from '@app/store/modules/docker';
import { upnp } from '@app/store/modules/upnp';
import { listenerMiddleware } from '@app/store/listeners/listener-middleware';
@@ -27,7 +26,6 @@ export const store = configureStore({
remoteGraphQL: remoteGraphQLReducer,
notifications: notificationReducer,
cache: cache.reducer,
dashboard: dashboard.reducer,
docker: docker.reducer,
upnp: upnp.reducer,
dynamix: dynamix.reducer,
@@ -45,8 +43,8 @@ export const getters = {
apiKey: () => store.getState().apiKey,
cache: () => store.getState().cache,
config: () => store.getState().config,
dashboard: () => store.getState().dashboard,
docker: () => store.getState().docker,
dynamicRemoteAccess: () => store.getState().dynamicRemoteAccess,
dynamix: () => store.getState().dynamix,
emhttp: () => store.getState().emhttp,
minigraph: () => store.getState().minigraph,

View File

@@ -0,0 +1,45 @@
import { startAppListening } from '@app/store/listeners/listener-middleware';
import { type RootState } from '@app/store';
import { remoteAccessLogger } from '@app/core/log';
import { loadConfigFile } from '@app/store/modules/config';
import { FileLoadStatus } from '@app/store/types';
import { isAnyOf } from '@reduxjs/toolkit';
import { RemoteAccessController } from '@app/remoteAccess/remote-access-controller';
import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types';
const shouldDynamicRemoteAccessBeEnabled = (state: RootState | null): boolean => {
if (state?.config.status !== FileLoadStatus.LOADED || state?.emhttp.status !== FileLoadStatus.LOADED) {
return false;
}
if (state.config.remote.dynamicRemoteAccessType && state.config.remote.dynamicRemoteAccessType !== DynamicRemoteAccessType.DISABLED) {
return true;
}
return false;
};
const isStateOrConfigUpdate = isAnyOf(loadConfigFile.fulfilled);
export const enableDynamicRemoteAccessListener = () => startAppListening({
predicate(action, currentState, previousState) {
if ((isStateOrConfigUpdate(action) || !action?.type)
&& (shouldDynamicRemoteAccessBeEnabled(currentState) !== shouldDynamicRemoteAccessBeEnabled(previousState))) {
return true;
}
return false;
}, async effect(_, { getState, dispatch }) {
const state = getState();
const remoteAccessType = state.config.remote?.dynamicRemoteAccessType;
if (!remoteAccessType) {
return;
}
if (remoteAccessType === DynamicRemoteAccessType.DISABLED) {
remoteAccessLogger.info('[Listener] Disabling Dynamic Remote Access Feature');
await RemoteAccessController.instance.stopRemoteAccess({ getState, dispatch });
}
},
});

View File

@@ -13,7 +13,7 @@ import merge from 'lodash/merge';
import { FileLoadStatus } from '@app/store/types';
import { F_OK } from 'constants';
import { type RecursivePartial } from '@app/types';
import { MinigraphStatus, type Owner } from '@app/graphql/generated/api/types';
import { DynamicRemoteAccessType, MinigraphStatus, type Owner } from '@app/graphql/generated/api/types';
import { type RootState } from '@app/store';
import { randomBytes } from 'crypto';
import { logger } from '@app/core/log';
@@ -22,7 +22,6 @@ import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer
import { writeFileSync } from 'fs';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { isEqual } from 'lodash';
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access';

View File

@@ -0,0 +1,72 @@
import { remoteAccessLogger } from '@app/core/log';
import { type AccessUrlInput, type AccessUrl, DynamicRemoteAccessType, URL_TYPE } from '@app/graphql/generated/api/types';
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
interface DynamicRemoteAccessState {
runningType: DynamicRemoteAccessType; // Is Dynamic Remote Access actively running - shows type of access currently running
error: string | null;
lastPing: number | null;
allowedUrl: AccessUrl | null; // Not used yet, will be used to facilitate allowlisting clients
}
const initialState: DynamicRemoteAccessState = {
runningType: DynamicRemoteAccessType.DISABLED,
error: null,
lastPing: null,
allowedUrl: null
};
const dynamicRemoteAccess = createSlice({
name: 'dynamicRemoteAccess',
initialState,
reducers: {
receivedPing(state) {
remoteAccessLogger.info('ping');
state.lastPing = Date.now();
},
clearPing(state) {
remoteAccessLogger.info('clearing ping');
state.lastPing = null;
},
setRemoteAccessRunningType(
state,
action: PayloadAction<DynamicRemoteAccessType>
) {
state.error = null;
state.runningType = action.payload;
if (action.payload === DynamicRemoteAccessType.DISABLED) {
state.lastPing = null;
} else {
state.lastPing = Date.now();
}
},
setDynamicRemoteAccessError(state, action: PayloadAction<string>) {
state.error = action.payload;
},
setAllowedRemoteAccessUrl(
state,
action: PayloadAction<AccessUrlInput | null>
) {
if (action.payload) {
console.log(action.payload);
state.allowedUrl = {
ipv4: action.payload.ipv4,
ipv6: action.payload.ipv6,
type: action.payload.type ?? URL_TYPE.WAN,
name: action.payload.name,
}
}
},
},
});
const { actions, reducer } = dynamicRemoteAccess;
export const {
receivedPing,
clearPing,
setAllowedRemoteAccessUrl,
setRemoteAccessRunningType,
setDynamicRemoteAccessError,
} = actions;
export const dynamicRemoteAccessReducer = reducer;

View File

@@ -2,29 +2,54 @@ import { createSlice } from '@reduxjs/toolkit';
import { join, resolve as resolvePath } from 'path';
const initialState = {
core: __dirname,
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
'unraid-data': resolvePath(process.env.PATHS_UNRAID_DATA ?? '/boot/config/plugins/dynamix.my.servers/data/' as const),
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
'docker-socket': '/var/run/docker.sock' as const,
'parity-checks': '/boot/config/parity-checks.log' as const,
htpasswd: '/etc/nginx/htpasswd' as const,
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
states: resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const),
'dynamix-base': resolvePath(process.env.PATHS_DYNAMIX_BASE ?? '/boot/config/plugins/dynamix/' as const),
'dynamix-config': resolvePath(process.env.PATHS_DYNAMIX_CONFIG ?? '/boot/config/plugins/dynamix/dynamix.cfg' as const),
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
'myservers-config': resolvePath(process.env.PATHS_MY_SERVERS_CONFIG ?? '/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const),
'myservers-config-states': join(resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const), 'myservers.cfg' as const),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
'keyfile-base': resolvePath(process.env.PATHS_KEYFILE_BASE ?? '/boot/config' as const),
'machine-id': resolvePath(process.env.PATHS_MACHINE_ID ?? '/var/lib/dbus/machine-id' as const),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'var-run': '/var/run' as const,
core: __dirname,
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
'unraid-data': resolvePath(
process.env.PATHS_UNRAID_DATA ??
('/boot/config/plugins/dynamix.my.servers/data/' as const)
),
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
'docker-socket': '/var/run/docker.sock' as const,
'parity-checks': '/boot/config/parity-checks.log' as const,
htpasswd: '/etc/nginx/htpasswd' as const,
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
states: resolvePath(
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
),
'dynamix-base': resolvePath(
process.env.PATHS_DYNAMIX_BASE ??
('/boot/config/plugins/dynamix/' as const)
),
'dynamix-config': resolvePath(
process.env.PATHS_DYNAMIX_CONFIG ??
('/boot/config/plugins/dynamix/dynamix.cfg' as const)
),
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
'myservers-config': resolvePath(
process.env.PATHS_MY_SERVERS_CONFIG ??
('/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const)
),
'myservers-config-states': join(
resolvePath(
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
),
'myservers.cfg' as const
),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
'myservers-keepalive':
process.env.PATHS_MY_SERVERS_FB ?? ('/boot/config/plugins/dynamix.my.servers/fb_keepalive' as const),
'keyfile-base': resolvePath(
process.env.PATHS_KEYFILE_BASE ?? ('/boot/config' as const)
),
'machine-id': resolvePath(
process.env.PATHS_MACHINE_ID ?? ('/var/lib/dbus/machine-id' as const)
),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'var-run': '/var/run' as const,
};
export const paths = createSlice({
name: 'paths',
initialState,
reducers: {},
name: 'paths',
initialState,
reducers: {},
});

View File

@@ -0,0 +1,70 @@
import type { IniStringBooleanOrAuto } from '@app/core/types/ini';
import { type FqdnEntry } from '@app/core/types/states/nginx';
import type { StateFileToIniParserMap } from '@app/store/types';
// Allow upper or lowercase FQDN6
const fqdnRegex = /^nginx(.*?)fqdn6?$/i;
export type NginxIni = {
nginxCertname: string;
nginxCertpath: string;
nginxDefaulturl: string;
nginxLanip: string;
nginxLanip6: string;
nginxLanmdns: string;
nginxLanname: string;
nginxPort: string;
nginxPortssl: string;
nginxUsessl: IniStringBooleanOrAuto;
nginxWanip: string;
nginxWanaccess: string;
[nginxInterfaceFqdn: string]: string;
};
export const parse: StateFileToIniParserMap['nginx'] = (state) => {
const fqdnKeys = Object.keys(state).filter((key) => fqdnRegex.test(key));
const interfaceId = new Map<string, number>();
const fqdnUrls: FqdnEntry[] = fqdnKeys.reduce<FqdnEntry[]>((acc, key) => {
const match = fqdnRegex.exec(key);
if (match && state[key]) {
// We need to pull the number from the interface to get it by itself
const interfaceType = match[1].replace(/[0-9]/g, '').toUpperCase();
// Count the number of interfaces we've already added to the list
const isIPv6 = key.endsWith('6');
const currInterfaceId = interfaceId.get(interfaceType) || 0;
interfaceId.set(interfaceType, currInterfaceId + 1);
acc.push({
interface: interfaceType,
id: currInterfaceId,
fqdn: state[key],
isIpv6: isIPv6,
});
}
return acc;
}, []);
fqdnUrls.forEach((fqdn) => {
if ((interfaceId.get(fqdn.interface) || 0) <= 1 ) {
fqdn.id = null;
}
});
return {
certificateName: state.nginxCertname,
certificatePath: state.nginxCertpath,
defaultUrl: state.nginxDefaulturl,
httpPort: Number(state.nginxPort),
httpsPort: Number(state.nginxPortssl),
lanIp: state.nginxLanip,
lanIp6: state.nginxLanip6,
lanMdns: state.nginxLanmdns,
lanName: state.nginxLanname,
sslEnabled: state.nginxUsessl !== 'no',
sslMode: state.nginxUsessl,
wanAccessEnabled: state.nginxWanaccess === 'yes',
wanIp: state.nginxWanip,
fqdnUrls: fqdnUrls as FqdnEntry[],
};
};

View File

@@ -1,6 +1,5 @@
import { type MinigraphStatus } from '@app/graphql/generated/api/types';
import { type DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { type DynamicRemoteAccessType } from '@app/graphql/generated/api/types';
interface MyServersConfig extends Record<string, unknown> {
api: {
version: string;

View File

@@ -1,9 +1,10 @@
import { LogCleanupService } from '@app/unraid-api/cron/log-cleanup.service';
import { WriteFlashFileService } from '@app/unraid-api/cron/write-flash-file.service';
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
providers: [LogCleanupService],
providers: [LogCleanupService, WriteFlashFileService],
})
export class CronModule {}

View File

@@ -0,0 +1,55 @@
import { ONE_DAY_MS, THIRTY_MINUTES_MS } from '@app/consts';
import { sleep } from '@app/core/utils/misc/sleep';
import { convertToFuzzyTime } from '@app/mothership/utils/convert-to-fuzzy-time';
import { getters } from '@app/store/index';
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { readFile, writeFile } from 'fs/promises';
@Injectable()
export class WriteFlashFileService {
constructor() {}
private readonly logger = new Logger(WriteFlashFileService.name);
private fileLocation = getters.paths()['myservers-keepalive'];
public randomizeWriteTime = true;
public writeNewTimestamp = async (): Promise<number> => {
const wait = this.randomizeWriteTime
? convertToFuzzyTime(0, THIRTY_MINUTES_MS)
: 0;
await sleep(wait);
const newDate = new Date();
try {
await writeFile(this.fileLocation, newDate.toISOString());
} catch (error) {
this.logger.error(error);
}
return newDate.getTime();
};
public getOrCreateTimestamp = async (): Promise<number> => {
try {
const file = (
await readFile(this.fileLocation, 'utf-8')
).toString();
return Date.parse(file);
} catch (error) {
return await this.writeNewTimestamp();
}
};
@Cron('0 * * * *')
async handleCron() {
try {
const currentDate = new Date().getTime();
const prevDate = await this.getOrCreateTimestamp();
if (currentDate - prevDate > ONE_DAY_MS * 7) {
// Write new timestamp
await this.writeNewTimestamp();
}
} catch (error) {
// File does not exist, write it
await this.writeNewTimestamp();
this.logger.error(error);
}
}
}

View File

@@ -0,0 +1,43 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { WriteFlashFileService } from './write-flash-file.service';
import { readFileSync, writeFileSync } from 'fs';
import { getters } from '@app/store/index';
describe('WriteFlashFileService', () => {
let service: WriteFlashFileService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [WriteFlashFileService],
}).compile();
service = module.get<WriteFlashFileService>(WriteFlashFileService);
service.randomizeWriteTime = false;
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should write and update the file when called', async () => {
const timestamp = await service.writeNewTimestamp();
expect(timestamp).toBeGreaterThan(0);
const file = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
expect(file).toBe(new Date(timestamp).toISOString(), 'file contents match the returned timestamp');
// Now make the file very old
writeFileSync(getters.paths()['myservers-keepalive'], '2021-01-01T00:00:00.000Z');
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).toBe('2021-01-01T00:00:00.000Z', 'file was updated');
await service.handleCron();
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).not.toBe('2021-01-01T00:00:00.000Z', 'file was updated');
// Now make the file kind of old (one day )
writeFileSync(getters.paths()['myservers-keepalive'], new Date(Date.now() - (1_000 * 60 * 60 * 24)).toISOString());
const now = Date.now();
await service.handleCron();
const contents = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
expect(new Date(contents).getTime() + (1_000 * 60 * 60 * 12)).toBeLessThan(new Date(now).getTime(), 'file was updated but is still older than today');
});
});

View File

@@ -0,0 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectResolver } from './connect.resolver';
describe('ConnectResolver', () => {
let resolver: ConnectResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ConnectResolver],
}).compile();
resolver = module.get<ConnectResolver>(ConnectResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@@ -0,0 +1,100 @@
import { store } from '@app/store/index';
import { Logger } from '@nestjs/common';
import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GraphQLError } from 'graphql';
import { UseRoles } from 'nest-access-control';
import { RemoteAccessController } from '@app/remoteAccess/remote-access-controller';
import {
ConnectResolvers,
type DynamicRemoteAccessStatus,
DynamicRemoteAccessType,
type EnableDynamicRemoteAccessInput,
} from '@app/graphql/generated/api/types';
import {
setAllowedRemoteAccessUrl,
} from '@app/store/modules/dynamic-remote-access';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
@Resolver('Connect')
export class ConnectResolver implements ConnectResolvers {
protected logger = new Logger(ConnectResolver.name);
@Query('connect')
@UseRoles({
resource: 'connect/dynamic-remote-access',
action: 'read',
possession: 'own',
})
public connect() {
return {};
}
@ResolveField()
public id() {
return getServerIdentifier('connect');
}
@ResolveField()
public dynamicRemoteAccess(): DynamicRemoteAccessStatus {
return {
runningType: store.getState().dynamicRemoteAccess.runningType,
enabledType:
store.getState().config.remote.dynamicRemoteAccessType ??
DynamicRemoteAccessType.DISABLED,
error: store.getState().dynamicRemoteAccess.error,
};
}
@Mutation()
@UseRoles({
resource: 'connect/dynamic-remote-access',
action: 'update',
possession: 'own',
})
public async enableDynamicRemoteAccess(
@Args('input') dynamicRemoteAccessInput: EnableDynamicRemoteAccessInput
): Promise<boolean> {
// Start or extend dynamic remote access
const state = store.getState();
const { dynamicRemoteAccessType } = state.config.remote;
if (
!dynamicRemoteAccessType ||
dynamicRemoteAccessType === DynamicRemoteAccessType.DISABLED
) {
throw new GraphQLError('Dynamic Remote Access is not enabled.', {
extensions: { code: 'FORBIDDEN' },
});
}
const controller = RemoteAccessController.instance;
if (dynamicRemoteAccessInput.enabled === false) {
controller.stopRemoteAccess({
getState: store.getState,
dispatch: store.dispatch,
});
return true;
} else if (
controller.getRunningRemoteAccessType() ===
DynamicRemoteAccessType.DISABLED
) {
if (dynamicRemoteAccessInput.url) {
store.dispatch(
setAllowedRemoteAccessUrl(dynamicRemoteAccessInput.url)
);
}
controller.beginRemoteAccess({
getState: store.getState,
dispatch: store.dispatch,
});
} else {
controller.extendRemoteAccess({
getState: store.getState,
dispatch: store.dispatch,
});
}
return true;
}
}

View File

@@ -0,0 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectService } from './connect.service';
describe('ConnectService', () => {
let service: ConnectService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ConnectService],
}).compile();
service = module.get<ConnectService>(ConnectService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ConnectService {}

View File

@@ -2,6 +2,7 @@ import {
DateTimeResolver,
JSONResolver,
PortResolver,
URLResolver,
UUIDResolver,
} from 'graphql-scalars';
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long';
@@ -12,7 +13,12 @@ import { GraphQLModule } from '@nestjs/graphql';
import { ResolversModule } from './resolvers/resolvers.module';
import { GRAPHQL_INTROSPECTION } from '@app/environment';
import { typeDefs } from '@app/graphql/schema/index';
import { print } from 'graphql';
import { NoUnusedVariablesRule, print } from 'graphql';
import { NetworkResolver } from './network/network.resolver';
import { ServicesResolver } from './services/services.resolver';
import { SharesResolver } from './shares/shares.resolver';
import { ConnectResolver } from './connect/connect.resolver';
import { ConnectService } from './connect/connect.service';
@Module({
imports: [
@@ -32,7 +38,7 @@ import { print } from 'graphql';
subscriptions: {
'graphql-ws': {
path: '/graphql',
}
},
},
path: '/graphql',
typeDefs: print(typeDefs),
@@ -42,10 +48,20 @@ import { print } from 'graphql';
UUID: UUIDResolver,
DateTime: DateTimeResolver,
Port: PortResolver,
URL: URLResolver,
},
validationRules: [
NoUnusedVariablesRule
]
// schema: schema
}),
],
providers: [],
providers: [
NetworkResolver,
ServicesResolver,
SharesResolver,
ConnectResolver,
ConnectService,
],
})
export class GraphModule {}

View File

@@ -0,0 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { NetworkResolver } from './network.resolver';
describe('NetworkResolver', () => {
let resolver: NetworkResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [NetworkResolver],
}).compile();
resolver = module.get<NetworkResolver>(NetworkResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@@ -0,0 +1,28 @@
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { AccessUrl, Network } from '@app/graphql/generated/api/types';
import { getServerIps } from '@app/graphql/resolvers/subscription/network';
import { Query, ResolveField, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@Resolver('Network')
export class NetworkResolver {
constructor() {}
@UseRoles({
resource: 'network',
action: 'read',
possession: 'any',
})
@Query('network')
public async network(): Promise<Network> {
return {
id: getServerIdentifier('network'),
};
}
@ResolveField()
public async accessUrls(): Promise<AccessUrl[]> {
const ips = await getServerIps();
return ips.urls;
}
}

View File

@@ -1,15 +1,22 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import {
getAllowedOrigins,
getExtraOrigins,
} from '@app/common/allowed-origins';
import {
DynamicRemoteAccessType,
WAN_ACCESS_TYPE,
WAN_FORWARD_TYPE,
type ConnectSignInInput,
type SetupRemoteAccessInput,
} from '@app/graphql/generated/api/types';
import type { Cloud } from '@app/graphql/generated/api/types';
import type { Cloud, RemoteAccess } from '@app/graphql/generated/api/types';
import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in';
import { checkApi } from '@app/graphql/resolvers/query/cloud/check-api';
import { checkCloud } from '@app/graphql/resolvers/query/cloud/check-cloud';
import { checkMinigraphql } from '@app/graphql/resolvers/query/cloud/check-minigraphql';
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access';
import { store } from '@app/store/index';
import { getters, store } from '@app/store/index';
import { logoutUser } from '@app/store/modules/config';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@@ -45,6 +52,44 @@ export class CloudResolver {
};
}
@Query()
@UseRoles({
resource: 'connect',
action: 'read',
possession: 'own',
})
public async remoteAccess(): Promise<RemoteAccess> {
const hasWanAccess = getters.config().remote.wanaccess === 'yes';
const dynamicRemoteAccessSettings: RemoteAccess = {
accessType: hasWanAccess
? getters.config().remote.dynamicRemoteAccessType !==
DynamicRemoteAccessType.DISABLED
? WAN_ACCESS_TYPE.DYNAMIC
: WAN_ACCESS_TYPE.ALWAYS
: WAN_ACCESS_TYPE.DISABLED,
forwardType: getters.config().remote.upnpEnabled
? WAN_FORWARD_TYPE.UPNP
: WAN_FORWARD_TYPE.STATIC,
port: getters.config().remote.wanport
? Number(getters.config().remote.wanport)
: null,
};
return dynamicRemoteAccessSettings;
}
@Query()
@UseRoles({
resource: 'connect',
action: 'read',
possession: 'own',
})
public async extraAllowedOrigins(): Promise<Array<string>> {
const extraOrigins = getExtraOrigins();
return extraOrigins;
}
@Mutation()
@UseRoles({
resource: 'connect',
@@ -85,4 +130,5 @@ export class CloudResolver {
await store.dispatch(setupRemoteAccessThunk(input)).unwrap();
return true;
}
}

View File

@@ -1,5 +1,6 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { type AllowedOriginInput, ConfigErrorState } from '@app/graphql/generated/api/types';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { type AllowedOriginInput, Config, ConfigErrorState } from '@app/graphql/generated/api/types';
import { getters, store } from '@app/store/index';
import { updateAllowedOrigins } from '@app/store/modules/config';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
@@ -13,9 +14,10 @@ export class ConfigResolver {
action: 'read',
possession: 'any',
})
public async config() {
public async config(): Promise<Config> {
const emhttp = getters.emhttp();
return {
id: getServerIdentifier('config'),
valid: emhttp.var.configValid,
error: emhttp.var.configValid
? null

View File

@@ -1,4 +1,5 @@
import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { type Display } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index';
import { Query, Resolver, Subscription } from '@nestjs/graphql';
@@ -69,11 +70,14 @@ export class DisplayResolver {
*/
const dynamixBasePath = getters.paths()['dynamix-base'];
const configFilePath = join(dynamixBasePath, 'case-model.cfg');
const result = {
id: getServerIdentifier('display'),
}
// If the config file doesn't exist then it's a new OS install
// Default to "default"
if (!existsSync(configFilePath)) {
return { case: states.default };
return { case: states.default, ...result };
}
// Attempt to get case from file
@@ -83,13 +87,14 @@ export class DisplayResolver {
// Config file can't be read, maybe a permissions issue?
if (serverCase === 'error_reading_config_file') {
return { case: states.couldNotReadConfigFile };
return { case: states.couldNotReadConfigFile, ...result };
}
// Blank cfg file?
if (serverCase.trim().length === 0) {
return {
case: states.default,
...result
};
}
@@ -99,6 +104,7 @@ export class DisplayResolver {
...states.default,
icon: serverCase,
},
...result
};
}

View File

@@ -1,18 +0,0 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { DockerContainersResolver } from './docker-containers.resolver';
describe('DockerContainersResolver', () => {
let resolver: DockerContainersResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerContainersResolver],
}).compile();
resolver = module.get<DockerContainersResolver>(DockerContainersResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@@ -1,16 +0,0 @@
import { getDockerContainers } from '@app/core/modules/index';
import { Query, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@Resolver('DockerContainers')
export class DockerContainersResolver {
@Query()
@UseRoles({
resource: 'docker/container',
action: 'read',
possession: 'any',
})
public async dockerContainers() {
return getDockerContainers();
}
}

View File

@@ -0,0 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { DockerResolver } from './docker.resolver';
describe('DockerResolver', () => {
let resolver: DockerResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DockerResolver],
}).compile();
resolver = module.get<DockerResolver>(DockerResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@@ -0,0 +1,29 @@
import { getDockerContainers } from '@app/core/modules/index';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { Query, ResolveField, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@Resolver('Docker')
export class DockerResolver {
@UseRoles({
resource: 'docker',
action: 'read',
possession: 'any',
})
@Query()
public docker() {
return {
id: getServerIdentifier('docker'),
};
}
@UseRoles({
resource: 'docker/container',
action: 'read',
possession: 'any',
})
@ResolveField()
public async containers() {
return getDockerContainers({ useCache: false });
}
}

View File

@@ -1,5 +1,6 @@
import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub';
import { getMachineId } from '@app/core/utils/misc/get-machine-id';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import {
generateApps,
generateCpu,
@@ -22,7 +23,14 @@ export class InfoResolver {
possession: 'any',
})
public async info() {
return {};
return {
id: getServerIdentifier('info')
};
}
@ResolveField('time')
public async now() {
return new Date().toISOString();
}
@ResolveField('apps')

View File

@@ -1,17 +1,12 @@
import { type NotificationFilter } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index';
import { Query, Resolver, Args, Mutation, Subscription } from '@nestjs/graphql';
import { Query, Resolver, Args, Subscription } from '@nestjs/graphql';
import { GraphQLError } from 'graphql';
import { UseRoles } from 'nest-access-control';
import { Logger } from '@nestjs/common';
import { type NotificationInput } from '@app/graphql/generated/client/graphql';
import { GraphQLClient } from '@app/mothership/graphql-client';
import { SEND_NOTIFICATION_MUTATION } from '@app/graphql/mothership/mutations';
import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub';
@Resolver()
export class NotificationsResolver {
private logger = new Logger(NotificationsResolver.name);
@Query()
@UseRoles({
resource: 'notifications',
@@ -44,49 +39,6 @@ export class NotificationsResolver {
.slice(offset, limit + offset);
}
@Mutation('sendNotification')
@UseRoles({
resource: 'notifications',
action: 'create',
possession: 'own',
})
public async sendNotification(
@Args('notification') notification: NotificationInput
) {
this.logger.log('Sending notification', JSON.stringify(notification));
const promise = new Promise((res, rej) => {
setTimeout(async () => {
rej(new GraphQLError('Sending Notification Timeout'));
}, 5_000);
const client = GraphQLClient.getInstance();
// If there's no mothership connection then bail
if (!client) {
this.logger.error('Mothership is not working');
throw new GraphQLError('Mothership is down');
}
client
.query({
query: SEND_NOTIFICATION_MUTATION,
variables: {
notification: notification,
apiKey: getters.config().remote.apikey,
},
})
.then((result) => {
this.logger.debug(
'Query Result from Notifications.ts',
result
);
res(notification);
})
.catch((err) => {
rej(err);
});
});
return promise;
}
@Subscription('notificationAdded')
@UseRoles({
resource: 'notifications',

View File

@@ -3,7 +3,6 @@ import { Module } from '@nestjs/common';
import { CloudResolver } from './cloud/cloud.resolver';
import { ConfigResolver } from './config/config.resolver';
import { DisksResolver } from './disks/disks.resolver';
import { DockerContainersResolver } from './docker-containers/docker-containers.resolver';
import { DisplayResolver } from './display/display.resolver';
import { NotificationsResolver } from './notifications/notifications.resolver';
import { OnlineResolver } from './online/online.resolver';
@@ -14,6 +13,7 @@ import { OwnerResolver } from './owner/owner.resolver';
import { RegistrationResolver } from './registration/registration.resolver';
import { ServerResolver } from './servers/server.resolver';
import { VarsResolver } from './vars/vars.resolver';
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver';
@Module({
providers: [
@@ -21,17 +21,17 @@ import { VarsResolver } from './vars/vars.resolver';
CloudResolver,
ConfigResolver,
DisksResolver,
DockerContainersResolver,
DisplayResolver,
DockerResolver,
FlashResolver,
InfoResolver,
NotificationsResolver,
OnlineResolver,
InfoResolver,
VmsResolver,
FlashResolver,
OwnerResolver,
RegistrationResolver,
ServerResolver,
VarsResolver,
VmsResolver,
],
})
export class ResolversModule {}

View File

@@ -1,3 +1,4 @@
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { getters } from '@app/store/index';
import { Query, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@@ -11,6 +12,9 @@ export class VarsResolver {
possession: 'any',
})
public async vars() {
return getters.emhttp().var ?? {};
return {
id: getServerIdentifier('vars'),
...getters.emhttp().var ?? {},
}
}
}

View File

@@ -11,9 +11,6 @@ export class VmsResolver {
possession: 'any',
})
public async vms() {
/**
* @todo Method implemntation
*/
return {};
}

View File

@@ -0,0 +1,18 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { ServicesResolver } from './services.resolver';
describe('ServicesResolver', () => {
let resolver: ServicesResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ServicesResolver],
}).compile();
resolver = module.get<ServicesResolver>(ServicesResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@@ -0,0 +1,53 @@
import { bootTimestamp } from '@app/common/dashboard/boot-timestamp';
import { getServerIdentifier } from '@app/core/utils/server-identifier';
import { API_VERSION } from '@app/environment';
import { DynamicRemoteAccessType, Service } from '@app/graphql/generated/api/types';
import { store } from '@app/store/index';
import { Query, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@Resolver('Services')
export class ServicesResolver {
constructor() {}
private getDynamicRemoteAccessService = (): Service | null => {
const { config, dynamicRemoteAccess } = store.getState();
const enabledStatus = config.remote.dynamicRemoteAccessType;
return {
id: getServerIdentifier('service/dynamic-remote-access'),
name: 'dynamic-remote-access',
online: enabledStatus !== DynamicRemoteAccessType.DISABLED,
version: dynamicRemoteAccess.runningType,
uptime: {
timestamp: bootTimestamp.toISOString(),
},
};
};
private getApiService = (): Service => {
return {
id: getServerIdentifier('service/unraid-api'),
name: 'unraid-api',
online: true,
uptime: {
timestamp: bootTimestamp.toISOString(),
},
version: API_VERSION,
};
}
@Query('services')
@UseRoles({
resource: 'services',
action: 'read',
possession: 'own',
})
public services(): Service[] {
const dynamicRemoteAccess = this.getDynamicRemoteAccessService();
return [
this.getApiService(),
...(dynamicRemoteAccess ? [dynamicRemoteAccess] : []),
];
}
}

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