Compare commits

...

478 Commits

Author SHA1 Message Date
Eli Bosley
cefda7c42b chore(release): 3.5.1 2024-02-29 12:43:48 -05:00
Eli Bosley
0393b2382c fix: build docker command updated to use dc.sh script 2024-02-29 12:43:41 -05:00
renovate[bot]
23e900f7fd chore(deps): update docker/setup-buildx-action action to v3 (#827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:36:32 -05:00
renovate[bot]
2d6aafc257 chore(deps): update dependency eslint to v8.57.0 (#798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:55 -05:00
renovate[bot]
b191efece1 chore(deps): update vitest monorepo to v1.3.1 (#784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:40 -05:00
renovate[bot]
2a7f0043f5 fix(deps): update dependency @heroicons/vue to v2.1.1 (#804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:17 -05:00
renovate[bot]
607c7e3704 fix(deps): update dependency @apollo/client to v3.9.5 (#785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:01 -05:00
renovate[bot]
c246a443c5 fix(deps): update dependency graphql-ws to v5.15.0 (#790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:25:35 -05:00
renovate[bot]
fd495e1f5c fix(deps): update dependency focus-trap to v7.5.4 (#788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:24:58 -05:00
Zack Spear
621a06cafa fix: unraid-api.php $param1 fallback 2024-02-28 21:11:21 -08:00
Zack Spear
f890b05151 fix: unraid-api missing start command + var defaults 2024-02-28 21:11:21 -08:00
Zack Spear
567d8fdd6d fix: state php special chars for html attributes (#853)
* fix: state php special chars for html attributes

* refactor: upc description as v-html to latest state php change
2024-02-28 13:42:11 -05:00
ljm42
7d996906ad fix: date format in UnraidCheck.php (#852) 2024-02-27 13:43:01 -05:00
Zack Spear
b5ec076279 fix: os updates rc to stable 2024-02-23 13:47:35 -08:00
Zack Spear
de8dfe3dba fix: state php breaking with double quotes in server description 2024-02-23 13:46:09 -08:00
Zack Spear
7249956d40 fix: state connect values without connect installed 2024-02-23 13:46:09 -08:00
Zack Spear
e6eb56466e fix: display dropdown for pro key no connect installed (#848) 2024-02-14 11:10:28 -05:00
Zack Spear
8954700bcb fix: dropdown reboot link text (#849)
fix: dropdown reboot link
2024-02-14 10:50:03 -05:00
Eli Bosley
eb595cea9e chore(release): 3.5.0 2024-02-07 12:51:01 -05:00
Eli Bosley
9a1a0a54e6 feat: ship production to different bucket (#846) 2024-02-07 12:47:35 -05:00
Zack Spear
134396b602 refactor: state class webgui global fallback 2024-02-05 12:58:15 -08:00
Zack Spear
2aa65fdb68 fix: state php usage from cli 2024-02-05 12:58:15 -08:00
Zack Spear
a9c4d7d5dd chore: comment to handle error on sha256 fetch 2024-02-05 12:58:15 -08:00
Zack Spear
2cbbd5ee40 refactor: remove client side auto renewal 2024-02-05 12:58:15 -08:00
Zack Spear
c84c55761c fix: State Class usage in other files 2024-02-05 12:58:15 -08:00
Zack Spear
77eed36990 refactor: replaceRenewCheck only fire on Tools > Registration 2024-02-05 12:58:15 -08:00
Eli Bosley
5c2d84d8b4 feat: ship preview to different bucket (#845) 2024-02-02 12:01:18 -05:00
Eli Bosley
9883f0f82f feat: also ship to cloudflare (#844) 2024-02-02 10:53:24 -05:00
Zack Spear
e62b05b6f6 fix: extraLinks when no updates available 2024-02-01 12:38:10 -08:00
Zack Spear
8e6ee8b770 refactor: copy Extend Key to Extend License 2024-02-01 12:11:38 -08:00
Zack Spear
666b51a28a refactor: update os response modal button ordering 2024-02-01 11:48:14 -08:00
Zack Spear
1962097a66 refactor: callback modal button icons 2024-01-31 18:45:42 -08:00
Zack Spear
7f010854b5 refactor: availableWithRenewal determined by updateOsResponse isEligible
refactor: updateOsResponse changelogPretty key renamed
2024-01-31 14:52:04 -08:00
Zack Spear
17288a4c02 refactor: removing ignored release uncheck ignoreThisRelease 2024-01-31 14:52:04 -08:00
ljm42
ea48def9fc fix: backport _var() PHP function to older versions of Unraid 2024-01-31 14:06:15 -08:00
Zack Spear
a1d5c29ffb fix: state data humanReadable switch fallthrus 2024-01-31 13:33:34 -08:00
Zack Spear
bf99eb25c8 feat: update os notifications enabled usage + link to enable & more options to account app 2024-01-31 12:39:14 -08:00
Zack Spear
b35a440792 refactor: callback modal spacing 2024-01-31 11:02:37 -08:00
Zack Spear
58f9eec8b1 refactor: targeting keyType strings for Starter / Unleashed 2024-01-31 11:02:37 -08:00
Zack Spear
26841aa10d refactor: Registration component onBeforeMount 2024-01-31 11:02:37 -08:00
Zack Spear
e18a8d670e refactor: UnraidCheck writeJsonFile JSON_UNESCAPED_SLASHES 2024-01-31 11:02:37 -08:00
Zack Spear
49d077db97 refactor: UnraidCheck removeAllIgnored fail silently 2024-01-31 11:02:37 -08:00
Zack Spear
9dafe165b0 chore: UnraidCheck.checkForUpdate todo comment 2024-01-31 11:02:37 -08:00
Zack Spear
cce1953cb8 fix: ServerUpdateOsResponse type 2024-01-31 11:02:37 -08:00
Zack Spear
7e33b25593 refactor: header os version update status only when no state error 2024-01-31 11:02:37 -08:00
Zack Spear
78fb49a6fc refactor: only display UPC update links when no stateDataError 2024-01-31 11:02:37 -08:00
Zack Spear
f1e0d93bc5 refactor: header os version reboot type status simplify 2024-01-31 11:02:37 -08:00
Zack Spear
195a178d15 refactor: unraidcheck to use UnraidCheck class 2024-01-31 11:02:37 -08:00
Zack Spear
b9257fce28 refactor: state php to use UnraidCheck class 2024-01-31 11:02:37 -08:00
Zack Spear
41eaf4ef1b fix: lint 2024-01-31 11:02:37 -08:00
Zack Spear
93d0c08955 fix: check update response modal expired key button styles 2024-01-31 11:02:37 -08:00
Zack Spear
c5bc3454ff refactor: UnraidCheck clean up 2024-01-31 11:02:37 -08:00
Zack Spear
c33b4ef709 refactor: consolidate UpdateOS php files into a single class 2024-01-31 11:02:37 -08:00
Zack Spear
ce3ba7d070 refactor: update os check conditional altUrl param 2024-01-31 11:02:37 -08:00
Zack Spear
639eb08291 refactor: modal close button spacing 2024-01-31 11:02:37 -08:00
Zack Spear
6d109b4c4c refactor: upc dropdown conditional update os buttons 2024-01-31 11:02:37 -08:00
Zack Spear
6f3971dc47 refactor: check update response modal copy + alignment 2024-01-31 11:02:37 -08:00
Zack Spear
2ccb503dc8 refactor: lint clean up 2024-01-31 11:02:37 -08:00
Zack Spear
3cb9fdf102 refactor: modal spacing 2024-01-31 11:02:37 -08:00
Zack Spear
40d81a4081 refactor: translations 2024-01-31 11:02:37 -08:00
Zack Spear
5a85f55be8 refactor: header os version update os status pills 2024-01-31 11:02:37 -08:00
Zack Spear
5455e211bc chore: @todo for changelog_pretty 2024-01-31 11:02:37 -08:00
Zack Spear
cb4cc989c7 fix: missing translations 2024-01-31 11:02:37 -08:00
Zack Spear
037aa479bf refactor: improve responsive modal 2024-01-31 11:02:37 -08:00
Zack Spear
08567f287a fix: marked-base-url install 2024-01-31 11:02:37 -08:00
Zack Spear
a57f1d890d fix: changlog relative links and external links 2024-01-31 11:02:37 -08:00
Zack Spear
3ab406e012 refactor: modal styles & content scrollable 2024-01-31 11:02:37 -08:00
Zack Spear
f36f4702a2 fix: lint unused value 2024-01-31 11:02:37 -08:00
Zack Spear
62697f7972 feat: updateOs check response determines if update auth is required 2024-01-31 11:02:37 -08:00
Zack Spear
ec8d2bc0e8 feat: getOsReleaseBySha256 cached endpoint with keyfile header 2024-01-31 11:02:37 -08:00
Zack Spear
d83664b6a3 refactor: update os change modal continue button 2024-01-31 11:02:37 -08:00
Zack Spear
6910a020d2 chore: clean up console log 2024-01-31 11:02:37 -08:00
Zack Spear
60e5c6e3e8 fix: regTm format when already set 2024-01-31 11:02:37 -08:00
Zack Spear
90b1432875 fix: regTm format after key install without page refresh 2024-01-31 11:02:37 -08:00
Zack Spear
f1059aa381 refactor: header os update available badge open update modal 2024-01-31 11:02:37 -08:00
Zack Spear
01b4937f35 refactor: update os ignore release text 2024-01-31 11:02:37 -08:00
Zack Spear
3e051815c5 feat: update os ignore release 2024-01-31 11:02:37 -08:00
Zack Spear
e976daf8b0 refactor: ignore release switch colors 2024-01-31 11:02:37 -08:00
Zack Spear
422046dc03 refactor: registration item label text-right 2024-01-31 11:02:37 -08:00
Zack Spear
9a270971d1 refactor: center registration item without label 2024-01-31 11:02:37 -08:00
Zack Spear
0742382ae1 refactor: registration page conditionals 2024-01-31 11:02:37 -08:00
Zack Spear
763c38430e feat: add manage account link to all versions of upc dropdown 2024-01-31 11:02:37 -08:00
Zack Spear
4d926bba8e refactor: remove update os callback link from upc dropdown 2024-01-31 11:02:37 -08:00
Zack Spear
4acc4ea9a9 fix: ignore release localStorage 2024-01-31 11:02:37 -08:00
Zack Spear
565bf47818 fix: translations 2024-01-31 11:02:37 -08:00
Zack Spear
176a0f30be refactor: check update modal styles 2024-01-31 11:02:37 -08:00
Zack Spear
6f4d983d89 chore: uninstall pinia-plugin-persistedstate 2024-01-31 11:02:37 -08:00
Zack Spear
6a0e258cf2 test: component viewer 2024-01-31 11:02:37 -08:00
Zack Spear
8d82064888 refactor: translations 2024-01-31 11:02:37 -08:00
Zack Spear
4300179b67 refactor: check update response modal styling 2024-01-31 11:02:37 -08:00
Zack Spear
7e31ae2ebf refactor: changelog modal improvements 2024-01-31 11:02:37 -08:00
Zack Spear
7a27560b0d fix: type issue with changlelog modal visibility 2024-01-31 11:02:37 -08:00
Zack Spear
93655fef62 refactor: tailwind prose styles 2024-01-31 11:02:37 -08:00
Zack Spear
a581a95cb4 chore: formatting 2024-01-31 11:02:37 -08:00
Zack Spear
261fdda47c test: update data 2024-01-31 11:02:37 -08:00
Zack Spear
7a2a243a21 refactor: translations for new check update modals 2024-01-31 11:02:37 -08:00
Zack Spear
bead4256af feat: new check update buttons in dropdown 2024-01-31 11:02:37 -08:00
Zack Spear
e8dfd7e3b3 feat: update modals 2024-01-31 11:02:37 -08:00
Zack Spear
e456b7fcac feat: changelog modal 2024-01-31 11:02:37 -08:00
Zack Spear
fbe5e417ef feat: check update response modal 2024-01-31 11:02:37 -08:00
Zack Spear
5f80053a33 refactor: test page 2024-01-31 11:02:37 -08:00
Zack Spear
fa520a2d3e feat: button add underline-hover-red style option 2024-01-31 11:02:37 -08:00
Zack Spear
cf54f01945 chore: install marked 2024-01-31 11:02:37 -08:00
Zack Spear
44d2d58f12 refactor: modal spacing 2024-01-31 11:02:37 -08:00
Zack Spear
daba2a352f feat: updateOs store call local server-side endpoint & add modal support 2024-01-31 11:02:37 -08:00
Zack Spear
d1ff2b1fad refactor: button props type usage 2024-01-31 11:02:37 -08:00
Zack Spear
b1bd71f2e2 refactor: updateOs callback button action 2024-01-31 11:02:37 -08:00
Zack Spear
7f49816275 refactor: use account store updateOs callback 2024-01-31 11:02:37 -08:00
Zack Spear
d73d460e88 feat: create WebguiCheckForUpdate endpoint 2024-01-31 11:02:37 -08:00
Zack Spear
ab1e852b6c refactor: abstract button compnoent props type 2024-01-31 11:02:37 -08:00
Zack Spear
117b7430db chore: organize npm scripts & install pinia-plugin-persistedstate 2024-01-31 11:02:37 -08:00
Zack Spear
2e73f9e37a refactor: account updateOs callback 2024-01-31 11:02:37 -08:00
Zack Spear
d3158983b4 refactor: ServerUpdateOsResponse type 2024-01-31 11:02:37 -08:00
Zack Spear
dae7baa6ad refactor: server state parsedRegExp & set updateOsResponse 2024-01-31 11:02:37 -08:00
Zack Spear
e29f5e1adf feat: WebguiCheckForUpdate using server-side check 2024-01-31 11:02:37 -08:00
Zack Spear
e8d15c7dbb refactor: nuxt auto import components 2024-01-31 11:02:37 -08:00
Zack Spear
58be009da4 feat: unraidcheck callable from webgui with altUrl & json output 2024-01-31 11:02:37 -08:00
Eli Bosley
d4eb0ce3f2 feat: add new staging url for connect website (#841)
* feat: add new staging url for connect website

* feat: add url to plg
2024-01-12 13:42:15 -05:00
Eli Bosley
d73324a141 feat: upgrade a ton of dependencies (#842)
* feat: upgrade a ton of dependencies
2024-01-12 13:05:51 -05:00
renovate[bot]
7061be60f4 chore(deps): update docker/build-push-action action to v5 (#826)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-12 11:03:19 -05:00
renovate[bot]
2a65f64ac1 fix(deps): update dependency ws to v8.16.0 (#815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-12 11:03:02 -05:00
Eli Bosley
120ba3e447 chore(release): 3.4.0 2024-01-11 17:28:39 -05:00
Eli Bosley
a527c7183a fix: run hourly 2024-01-11 17:25:57 -05:00
Eli Bosley
a0dfbb4e15 feat: add logrotate to cron in nestjs (#839)
* feat: add logrotate to cron in nestjs

* fix: set max size to 5m and remove old logs

* fix: logrotate command invalid
2024-01-11 17:17:23 -05:00
Eli Bosley
764f65ff61 fix: allow failure for log deletion 2024-01-11 16:44:25 -05:00
Eli Bosley
1615e8623c fix: excessive logging 2024-01-11 16:34:10 -05:00
Eli Bosley
d7bb9ff073 fix: allowed origins check not working without spaces (#838)
* fix: allowed origins check not working without spaces

* fix: broken test
2024-01-11 10:52:42 -05:00
Eli Bosley
d896581e12 chore(release): 3.3.0 2024-01-09 17:07:42 -05:00
ljm42
f833fa1fab fix: patch ShowChanges.php in 6.10 2024-01-09 13:51:00 -08:00
Zack Spear
2823517b26 fix: 6.10 view release notes js 2024-01-09 13:51:00 -08:00
Zack Spear
3e0a8d0070 fix: unraid-api server state refresh after key extension use regExp 2024-01-09 12:41:59 -08:00
Eli Bosley
b3768d65aa fix: codegen on web run 2024-01-09 12:15:38 -08:00
Eli Bosley
d2e17c0051 fix: local container startup commands cleaned up 2024-01-09 11:29:23 -08:00
Eli Bosley
d0354c2ef2 feat: local start command 2024-01-09 11:29:23 -08:00
Eli Bosley
17fc1181c2 feat: add support for expiration in var.ini (#833)
* feat: add support for expiration in var.ini

* tests: update snapshots
2024-01-09 12:19:59 -05:00
renovate[bot]
dbe7c5fb93 chore(deps): update actions/checkout action to v4 (#817)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-09 09:59:26 -05:00
renovate[bot]
54b421d01f chore(deps): update actions/download-artifact action to v4 (#818)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-09 09:59:14 -05:00
renovate[bot]
0e9611f802 chore(deps): update actions/upload-artifact action to v4 (#820)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-09 09:59:02 -05:00
renovate[bot]
4743a2439d chore(deps): update actions/setup-node action to v4 (#819)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-09 09:58:34 -05:00
Zack Spear
242c167f82 fix: refreshServerState check regExp 2024-01-08 13:12:33 -06:00
Zack Spear
e071b994cf refactor: installKey.install failure handling 2024-01-08 13:12:14 -06:00
Zack Spear
60bb8aa0fa chore: translations 2024-01-08 13:11:47 -06:00
Zack Spear
9f56f34ea4 fix: renew callback messaging in modal 2024-01-08 13:11:23 -06:00
Zack Spear
d66b33e600 fix: replaceRenew response cache use & purge 2024-01-08 12:31:20 -06:00
Zack Spear
63b7c0361e feat: npm scripts to prevent webgui builds with wrong urls 2024-01-08 11:13:22 -06:00
renovate[bot]
dea66ff49d fix(deps): update nest monorepo (#816)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 13:41:48 -05:00
renovate[bot]
c29621741e chore(deps): update dependency @swc/core to v1.3.102 (#700)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 09:52:22 -05:00
renovate[bot]
6d7d013f7a fix(deps): update graphql-tools monorepo (major) (#693)
fix(deps): update graphql-tools monorepo

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-05 09:51:46 -05:00
Eli Bosley
ed8e2420a5 fix: logrotate not working due to invalid ownership of unraid-api folder 2024-01-05 09:47:27 -05:00
Zack Spear
92ad66dd59 refactor: removed new key type translations 2024-01-04 12:04:00 -08:00
Zack Spear
5699e34ae9 refactor: sort translations alpha order case insensitive 2024-01-04 12:04:00 -08:00
Zack Spear
5f9dc26173 refactor: Registration page link order (#760) 2024-01-04 11:19:36 -07:00
Zack Spear
6d29ec2b90 fix: azure and gray theme custom colors 2024-01-03 13:50:36 -08:00
Eli Bosley
f91ae5c7a0 fix: rearrange exit hook to try to fix closing 2024-01-03 15:12:17 -05:00
Eli Bosley
33c69bf76f feat: fix exit hook and cleanup docker scripts (#758)
* feat: cleanup docker scripts

* feat: make logging directory if non-existent to fix stop behavior
2024-01-03 12:12:37 -05:00
ljm42
3e95bb259f fix: plugin install should suppress output from unraid-api stop (#757) 2024-01-03 10:10:37 -07:00
Eli Bosley
64b6bee559 fix: exit with process.exit not process.exitcode 2024-01-03 11:23:38 -05:00
Eli Bosley
61cb029780 feat: stretch downgrade component buttons 2024-01-03 08:13:56 -08:00
Eli Bosley
c02d823618 feat: fix logging format on start and stop 2024-01-03 10:43:00 -05:00
ljm42
fec36919c2 feat: change sort order of Update/Downgrade (#754) 2024-01-03 10:39:36 -05:00
Zack Spear
072242704d chore: translations note 2023-12-29 12:22:26 -05:00
ljm42
9199ffdeee feat: improve check for OS updates via PHP
* use http_build_query, and include query parms in result.json
* capture warnings and errors from file_get_contents in result.json
* track json decoding errors in result.json
2023-12-21 10:21:49 -08:00
ljm42
b92307eef5 feat: check for OS updates via PHP (#752) 2023-12-19 21:49:04 -07:00
Zack Spear
6445d1647a refactor: HeaderOsVersion update available badge 2023-12-19 16:25:26 -08:00
Zack Spear
3046fb9eed test: temp comment out serverState imports 2023-12-19 16:25:26 -08:00
Zack Spear
ad2c8b451a test: temp comment out mimicWebguiUnraidCheck in static serverStatic 2023-12-19 16:25:26 -08:00
Zack Spear
b39c5203fd chore: lint fix 2023-12-19 16:25:26 -08:00
Zack Spear
6e11ca209b refactor: clean up URLs 2023-12-19 16:25:26 -08:00
Zack Spear
eeb3598255 chore: lint 2023-12-19 16:25:26 -08:00
Zack Spear
c2063c28af refactor: components with refactored updateOsStore 2023-12-19 16:25:26 -08:00
Zack Spear
23b63de91f chore: remove unused component 2023-12-19 16:25:26 -08:00
Zack Spear
05a9340fc3 refactor: updateOsActions store with new updateOs store 2023-12-19 16:25:26 -08:00
Zack Spear
16f0ac5771 refactor: simplified updateOs store to use updateOsResponse from server store 2023-12-19 16:25:26 -08:00
Zack Spear
ebebf76933 refactor: clean up URLs 2023-12-19 16:25:26 -08:00
Zack Spear
8c956d45c7 fix: add missing translation keys 2023-12-19 16:25:26 -08:00
Zack Spear
4040933fad test: local dev serverState updateOsResponse 2023-12-19 16:25:26 -08:00
Zack Spear
63899f94fc refactor: state updateOs key to updateOsResponse 2023-12-19 16:25:26 -08:00
Zack Spear
7630ae87d4 refactor: state get updateOs details from /tmp/unraidcheck/response.json 2023-12-19 16:25:26 -08:00
Eli Bosley
127e2c3be6 feat: log config recreation reason 2023-12-13 12:47:40 -05:00
Eli Bosley
2aacbc1f1a feat: close log on exit 2023-12-13 12:27:47 -05:00
Eli Bosley
6f0673f428 feat: nestjs initial query implementation (#748)
* feat: nestjs initial query implementation
* feat: more permissions and resolver cleanup
* fix: back to ubuntu to remain compatible with pkg docker building
* feat: listen on socket as well as ports
* feat: swap to bookworm instead of ubuntu
2023-12-12 13:59:59 -05:00
Zack Spear
1315dc6099 chore: dateTime comment 2023-12-12 13:55:22 -05:00
Zack Spear
48bc19543e fix: dateTime system settings 2023-12-12 13:54:48 -05:00
Eli Bosley
08f7f95ea0 feat: always show DRA even if disabled 2023-12-12 13:02:22 -05:00
Zack Spear
6b72f188ef refactor: host in known origin check 2023-12-12 08:22:47 -08:00
Zack Spear
79ff9bedb9 refactor: add title to BrandButton usage 2023-12-12 08:22:47 -08:00
Zack Spear
d1c0f46325 refactor: Connect page web component button sizes 2023-12-12 08:22:47 -08:00
Zack Spear
909c2c6798 feat: disable account & key actions when unraid-api CORS error 2023-12-12 08:22:47 -08:00
Zack Spear
56dcd85aa1 fix: graphQL CORS error detection 2023-12-12 08:22:47 -08:00
Zack Spear
7918f5754f refactor: Connect extra origins add current host button 2023-12-12 08:22:47 -08:00
Zack Spear
519c24983a fix: combinedKnownOrigins in state.php for UPC 2023-12-12 08:22:47 -08:00
Zack Spear
735db3d5f5 refactor: connect page and state php data sharing 2023-12-12 08:22:47 -08:00
Zack Spear
53627f20c7 refactor: upc sign in text dropdown button 2023-12-12 08:22:47 -08:00
Zack Spear
181026567e refactor: include extraOrigins in list of allowedOrigins when checking for warning 2023-12-12 08:22:47 -08:00
Zack Spear
db6ca23533 feat: add button to add current origin to extra origins setting 2023-12-12 08:22:47 -08:00
Zack Spear
e0560afb6d chore: vscode settings 2023-12-12 08:22:47 -08:00
Eli Bosley
7e081e6661 fix: optional check on api.version to allow fallback to save value 2023-12-08 10:47:14 -05:00
Zack Spear
213caea5b6 fix: missing translation 2023-12-07 11:51:18 -05:00
Zack Spear
abd439f131 test: serverState osVersionBranch 2023-12-07 11:51:07 -05:00
Zack Spear
c681848d60 fix(web): azure & gray theme header font colors 2023-11-27 17:36:49 -05:00
Zack Spear
46a0567881 fix: lint unused param var prefixed 2023-11-21 10:59:48 -08:00
Zack Spear
0c80ef88b5 refactor(plg): state read case model from flash 2023-11-21 10:42:30 -08:00
Zack Spear
71252ddbea fix: state php version checking 2023-11-20 19:24:29 -08:00
Zack Spear
1ef2522089 refactor: updateOs lint 2023-11-20 19:24:14 -08:00
Zack Spear
f9652d7c06 refactor: updateOs store to match auth repo 2023-11-20 18:15:35 -08:00
Zack Spear
1de59150bc chore: rename web component deploy script 2023-11-20 17:43:19 -08:00
Zack Spear
2dd8cbb779 feat(web): caseModel 2023-11-20 17:42:40 -08:00
Zack Spear
f2b9cb0478 fix(plg): third party reboot detection 2023-11-15 16:02:38 -08:00
Zack Spear
e79ac7122a refactor(web): change callback url replace /Tools/Update to /Tolls 2023-11-15 15:21:02 -08:00
Zack Spear
c1c4baf476 test: remove standard-version 2023-11-15 13:55:10 -08:00
Zack Spear
e023ba6a19 chore(release): 1.0.8 2023-11-15 13:54:05 -08:00
Zack Spear
2ffeabe2a6 chore(release): 1.0.6 2023-11-15 13:49:25 -08:00
Zack Spear
36c5bbc3fd chore(release): 1.0.4 2023-11-15 13:49:07 -08:00
Zack Spear
da1ee3d631 test: package.json 2023-11-15 13:43:42 -08:00
Zack Spear
86b54dbe9a chore(release): 1.0.2 2023-11-15 13:42:59 -08:00
Zack Spear
296906758d chore(web): setup .versionrc to update version.txt on release 2023-11-15 13:42:43 -08:00
Zack Spear
cc2ea1244d chore(release): 1.0.1 2023-11-15 13:41:16 -08:00
Zack Spear
4aaf223007 chore(release): 1.0.0 2023-11-15 13:41:06 -08:00
Zack Spear
d283f1f321 refactor(web): remove unused onBeforeMount with console out 2023-11-15 13:35:31 -08:00
Zack Spear
f1731d732b refactor(web): remove console output 2023-11-15 13:32:09 -08:00
Zack Spear
33e4ba261c chore(web): add release script 2023-11-15 13:20:23 -08:00
Zack Spear
00f73bd3b8 chore(release): 1.0.0 2023-11-15 13:18:14 -08:00
Zack Spear
5ebce0ebfc refactor(plg): include Translations wrapper in translation class 2023-11-15 13:01:44 -08:00
Zack Spear
81f7f41b3b fix(web): use dateTime format from server 2023-11-15 13:01:13 -08:00
Zack Spear
00182ebb3c chore: deps updated 2023-11-14 17:22:48 -08:00
Zack Spear
58b2b2f130 fix: plg remove reboot-details path 2023-11-14 16:02:05 -08:00
Zack Spear
d132ad481b fix: header version thirdPartyDriversDownloading pill 2023-11-14 15:39:00 -08:00
Zack Spear
dd1ec82a52 fix: ThirdPartyDriver messaging on Update page 2023-11-14 14:04:24 -08:00
Zack Spear
2edc062569 fix: remove var_dump Connect settings 2023-11-14 13:52:40 -08:00
Zack Spear
a9c4e69e01 fix: Connect settings myservers config parse 2023-11-14 13:52:06 -08:00
Zack Spear
5f987458ef fix: uninstall reboot-details include 2023-11-14 13:10:48 -08:00
Eli Bosley
376a19bac6 fix: set sha in test step as well. 2023-11-14 14:19:23 -05:00
Eli Bosley
a1c07370ca fix: try to set environment in docker build 2023-11-14 14:16:10 -05:00
Zack Spear
1efd6b7e18 chore: copyright comments 2023-11-13 14:56:12 -08:00
Eli Bosley
1a31b2c929 feat: run codegen and update build script 2023-11-13 12:49:53 -05:00
Eli Bosley
9623f238b3 feat: add environment to docker-compose 2023-11-13 12:49:53 -05:00
Eli Bosley
fa5658fd81 feat: swap to fragement usage on webcomponent 2023-11-13 12:49:53 -05:00
Eli Bosley
0fa76f5d09 feat: extraOrigins public, remove origin listener 2023-11-13 12:49:53 -05:00
Eli Bosley
b4f0a084f1 feat: fix codegen 2023-11-13 12:49:53 -05:00
Eli Bosley
7d49bb2f10 feat: regTy swapped 2023-11-13 12:49:53 -05:00
Eli Bosley
8dd99b7f32 fix: add serverName / description to dashboard payload 2023-11-13 12:49:53 -05:00
Eli Bosley
eaddb696d4 feat: new key types in API 2023-11-13 12:49:53 -05:00
renovate[bot]
898c4e5656 chore(deps): update dependency eslint to v8.53.0 2023-11-13 10:41:03 -05:00
Zack Spear
62900565fb refactor: translations class usage 2023-11-09 16:49:47 -08:00
Zack Spear
e409ab805d chore: file formatting 2023-11-09 16:32:10 -08:00
Zack Spear
463ff4a38a refactor: state class usage with getServerStateJson 2023-11-09 16:31:48 -08:00
Zack Spear
205552eda5 fix: web component translations class 2023-11-09 16:09:43 -08:00
Zack Spear
ca3ffdc603 refactor: downgrade reboot details class usage 2023-11-09 15:45:30 -08:00
Zack Spear
be9e1e34f4 refactor: update os component prop reboot version 2023-11-09 10:05:15 -08:00
Zack Spear
00f1c63c46 refactor: web components translation php to class 2023-11-09 09:53:39 -08:00
Zack Spear
1c8437733c refactor: translations.php 2023-11-08 17:22:18 -08:00
Zack Spear
a658206cd4 refactor(plg): state include ServerState class 2023-11-08 16:17:06 -08:00
Zack Spear
95554e9832 chore(web): lint & type check to build:dev & build:webgui 2023-11-08 16:15:54 -08:00
Zack Spear
fb31fb584b refactor(web): registration feedback 2023-11-08 16:15:26 -08:00
Zack Spear
051faa06ac fix(web): reboot required disable update check link 2023-11-08 15:07:25 -08:00
Zack Spear
0e0a652dff refactor(web): improved header reboot pill link 2023-11-08 15:06:31 -08:00
Zack Spear
1403a76b80 fix: downgrade remove erroneous file_get_contents 2023-11-08 14:50:15 -08:00
Zack Spear
c4c51e83c2 refactor: php $docroot null coalescing assignment 2023-11-07 16:36:40 -08:00
Zack Spear
c91fef9c5f refactor(web): updateOs store release response group filtering 2023-11-07 15:40:36 -08:00
Zack Spear
9c33ef8248 refactor: state.php with RebootDetails class for type and version 2023-11-07 14:49:41 -08:00
Zack Spear
05ce165b83 refactor(web): improved downgrade ux 2023-11-07 13:07:02 -08:00
Zack Spear
485fc2a3b6 refactor: plg file upload to unraid server script 2023-11-07 13:07:02 -08:00
Zack Spear
f45f5f7a9a fix(web): update CallbackButton import 2023-11-07 13:07:02 -08:00
Zack Spear
9b9a6998b7 refactor(web): update page redirect 2023-11-07 13:07:02 -08:00
Zack Spear
82d9dc644b fix(web): downgrade-not-available when downgrade initiated 2023-11-07 13:07:02 -08:00
ljm42
81678d4de5 plg: update showchanges script 2023-11-07 09:28:00 -08:00
ljm42
032fd9853e plg: disable header message in DefaultPageLayout.php 2023-11-06 16:08:26 -08:00
Zack Spear
bf60f1e5ac refactor(web): downgrade view release notes 2023-11-06 16:06:05 -08:00
Zack Spear
bda8f4e1b3 fix(web): downgrade status pill for no downgrade available 2023-11-06 16:05:47 -08:00
Zack Spear
8c7160de2e refactor(web): registration item less padding 2023-11-06 15:21:27 -08:00
Zack Spear
2bd460effb fix(web): preview and test releases usage 2023-11-06 14:48:38 -08:00
Zack Spear
fdadfe699c fix(web): upc dropdown updates external icon 2023-11-06 14:14:03 -08:00
Zack Spear
84c96371f5 chore: lint and type check fixes 2023-11-06 13:53:29 -08:00
Zack Spear
799b77f09b refactor: downgrade and update improvements with store refactors 2023-11-06 13:20:06 -08:00
Zack Spear
1d67fa4c56 refactor(web): callback send redirect types 2023-11-06 13:18:28 -08:00
Zack Spear
8534fec4b2 refactor(web): webgui will not use preview & test release urls 2023-11-06 13:16:22 -08:00
Zack Spear
5483861055 fix(web): Update OS auto redirect loop with account 2023-11-06 13:15:57 -08:00
Zack Spear
bb60cbbc18 refactor(web): state consolidation 2023-11-06 13:13:53 -08:00
Zack Spear
fe906c025e refactor(web): update page downgrade in progress messaging 2023-11-03 17:11:37 -07:00
Zack Spear
79e2e89a80 refactor(web): downgrade status 2023-11-03 16:51:48 -07:00
Zack Spear
c30b926134 fix: plg installer header version replacement 2023-11-03 08:55:21 -07:00
Zack Spear
a87d83de04 refactor(web): dropdown post connect install 2023-11-03 08:40:21 -07:00
Zack Spear
7b3bd08c15 fix(web): updateOs lint 2023-11-03 08:40:21 -07:00
Zack Spear
00375a4590 fix: updateOs auth group usage 2023-11-03 08:40:21 -07:00
Zack Spear
6619138b54 refactor(web): updateOs store release error handling 2023-11-03 08:40:21 -07:00
ljm42
e9a7fcf95b feat: patch DefaultPageLayout for web component 2023-11-03 08:40:21 -07:00
Zack Spear
be1f419d92 refactor(web): updateOs release groups 2023-11-03 08:40:21 -07:00
Zack Spear
3a6b511de3 refactor: Tools page downgrade icon rotation 2023-11-03 08:40:21 -07:00
Zack Spear
82f15afbd2 refactor(web): sessionStorage name change 2023-11-03 08:40:21 -07:00
Zack Spear
524867b4e2 refactor(web): sessionStorage account & purchase url overrides 2023-11-03 08:40:21 -07:00
Zack Spear
d289e06c0b fix(plg): Downgrade & Update page file locations 2023-11-03 08:40:21 -07:00
renovate[bot]
13f366472b chore(deps): update graphqlcodegenerator monorepo 2023-11-02 12:37:59 -04:00
renovate[bot]
830718cd2c fix(deps): update dependency graphql to v16.8.1 2023-11-02 07:37:44 -04:00
renovate[bot]
8fffc7725c chore(deps): update dependency @types/semver to v7.5.4 2023-11-02 07:37:23 -04:00
Zack Spear
6fa6beb270 chore(web): shared callback store parity 2023-11-01 13:36:17 -07:00
Zack Spear
36846d2377 chore(web): state todo 2023-11-01 13:36:17 -07:00
Zack Spear
ef962f5f5d fix(web): lint fixes 2023-11-01 13:36:17 -07:00
Zack Spear
5cbccb06ad fix(web): type errors 2023-11-01 13:36:17 -07:00
Zack Spear
220a64ebdc chore(web): type fixes 2023-11-01 13:36:17 -07:00
Zack Spear
3145e30cf1 chore: remove test osReleases static json 2023-11-01 13:36:17 -07:00
Zack Spear
ef198494b0 chore: add nuxt type-check to package scripts 2023-11-01 13:36:17 -07:00
Zack Spear
9f1e3c5fda refactor: shared callback store with ServerState 2023-11-01 13:36:17 -07:00
Zack Spear
ddf8dc7ebf fix(web): regTy on account payload 2023-11-01 13:36:17 -07:00
Zack Spear
8bdffdc7b0 fix: updateOs type check 2023-11-01 13:36:17 -07:00
ljm42
915cdc3e06 remove legacy unraid.net settings migration from plugin (#741) 2023-11-01 13:36:17 -07:00
ljm42
4601388f3f Fix Remote Access Apply button 2023-11-01 13:36:17 -07:00
ljm42
66913bd221 Pass wanip to checkdns 2023-11-01 13:36:17 -07:00
Zack Spear
f554c3d3e2 chore: package updates 2023-11-01 13:36:17 -07:00
Zack Spear
2104eebe02 chore: lint manual fixes 2023-11-01 13:36:17 -07:00
Zack Spear
caab570be6 chore: lint auto fixes 2023-11-01 13:36:17 -07:00
ljm42
ca93ac7143 Add VS Code settings from the webgui
* add recommended extensions
* associate .page files with PHP
* add sftp-template.json
2023-11-01 13:36:17 -07:00
Zack Spear
9e895aed58 refactor(web): state.php use apikey $registered 2023-11-01 13:36:17 -07:00
Zack Spear
af4f53ed04 refactor: use env vars for os releases urls 2023-11-01 13:36:17 -07:00
Zack Spear
e021c48daa refactor(web): refactor copy-to-webgui-repo script 2023-11-01 13:36:17 -07:00
Zack Spear
ed4aa3d62c refactor(web): improved updateOs store extensibility 2023-11-01 13:36:17 -07:00
Zack Spear
2aa491e6f2 refactor(web): use osVersionBranch to determine releases endpoint 2023-11-01 13:36:17 -07:00
Zack Spear
1098e0f0e9 fix(web): reg component conditional keyActions 2023-11-01 13:36:17 -07:00
Zack Spear
8903371409 refactor: update os translations & auto callback for Tools > Update to account 2023-11-01 13:36:17 -07:00
Zack Spear
749eab85bd refactor: prevent callback send to /Tools/Update 2023-11-01 13:36:17 -07:00
Zack Spear
86d4defa3e refactor: remove emphasis from upc dropdown check for update link 2023-11-01 13:36:17 -07:00
Zack Spear
41fd15e7e3 test: dev page 2023-11-01 13:36:17 -07:00
Zack Spear
c1cff9e95f refactor: renew to extend front-end facing copy 2023-11-01 13:36:17 -07:00
Zack Spear
30a0e7d082 refactor(web): updateOs callback prevent duplicate install 2023-11-01 13:36:17 -07:00
Zack Spear
c387a28dbd refactor(web): upc check for updates callback link 2023-11-01 13:36:17 -07:00
Zack Spear
207ae12522 refactor: updateOs shared store better branch handling 2023-11-01 13:36:17 -07:00
Zack Spear
22ebb06980 refactor(web): update os use sha256 key server lookup + callback handle multiple actions with update os 2023-11-01 13:36:17 -07:00
Zack Spear
c0319d56b0 fix(web): translation 2023-11-01 13:36:17 -07:00
Zack Spear
3aaac2c244 fix(web): installPlugin composable for os updates 2023-11-01 13:36:17 -07:00
Zack Spear
d8a66e7b22 refactor(web): shared callback store extensibility 2023-11-01 13:36:17 -07:00
Zack Spear
00838e5cb8 test(web): dev callback builder helper 2023-11-01 13:36:17 -07:00
Zack Spear
6deaf9c342 refactor(web): button disabled styles 2023-11-01 13:36:17 -07:00
Zack Spear
5d6d91cfbd refactor(web): header os version styling 2023-11-01 13:36:17 -07:00
Zack Spear
f35e0ab627 test(web): serverState seed data 2023-11-01 13:36:17 -07:00
Zack Spear
c5da9ea002 test: dev removev unused props 2023-11-01 13:36:17 -07:00
Zack Spear
9334322f11 refactor: updateOs 2023-11-01 13:36:17 -07:00
Zack Spear
57a039b7d8 refactor(web): translations 2023-11-01 13:36:17 -07:00
Zack Spear
2621137e31 refactor(web): check os update button 2023-11-01 13:36:17 -07:00
Zack Spear
7276e9ddc9 refactor(web): prevent os update check with callback data present 2023-11-01 13:36:17 -07:00
Zack Spear
4e60c0ac1e fix(web): connect graph error handling 2023-11-01 13:36:17 -07:00
Zack Spear
13df4923a1 refactor(plg): downgrade page 2023-11-01 13:36:17 -07:00
Zack Spear
0eb0bdc918 refactor(plg): clean up Update page 2023-11-01 13:36:17 -07:00
Zack Spear
aaaa93f79e chore(web): formatting 2023-11-01 13:36:17 -07:00
Zack Spear
280dbfa53a refactor(web): os status 2023-11-01 13:36:17 -07:00
Zack Spear
3a5f976ff6 refactor: updateOs store parity with web components 2023-11-01 13:36:17 -07:00
Zack Spear
ea417435ac refactor: add os releases urls 2023-11-01 13:36:17 -07:00
Zack Spear
ecb69ba059 refactor(web): button component 2023-11-01 13:36:17 -07:00
Zack Spear
35f6a6cd3c refactor(plg): registration page web component 2023-11-01 13:36:17 -07:00
Zack Spear
64dc4c922d chore(web): clean up 2023-11-01 13:36:17 -07:00
Zack Spear
33a1e20338 chore(web): dateTime param comment 2023-11-01 13:36:17 -07:00
Zack Spear
9e1320b272 refactor(web): rename time composable to dateTime 2023-11-01 13:36:17 -07:00
Zack Spear
93649d0557 refactor(web): update ineligible text + DateTime helper exports 2023-11-01 13:36:17 -07:00
Zack Spear
46181dfa08 fix(web): regUpdatesExpired use .isAfter 2023-11-01 13:36:17 -07:00
Zack Spear
44066b292e test: seed data 2023-11-01 13:36:17 -07:00
Zack Spear
2f84fae344 refactor(web): downgrade 2023-11-01 13:36:17 -07:00
Zack Spear
d75548e219 feat(web): downgrade os web component 2023-11-01 13:36:17 -07:00
Zack Spear
ad416413fe refactor(web): ineligible available update ui/ux 2023-11-01 13:36:17 -07:00
Zack Spear
f99ea0bf16 chore(web): clean up unsued props 2023-11-01 13:36:17 -07:00
Zack Spear
d97be1e7aa refactor(web): add helper url 2023-11-01 13:36:17 -07:00
Zack Spear
01019ad546 refactor(web): ineligible copy 2023-11-01 13:36:17 -07:00
Zack Spear
3d99061a07 chore(web): clean up unsued props 2023-11-01 13:36:17 -07:00
Zack Spear
4c6ed1b530 refactor(web): button ui / ux 2023-11-01 13:36:17 -07:00
Zack Spear
50f0d03735 test(web): real expiration time 2023-11-01 13:36:17 -07:00
Zack Spear
9461a3e889 refactor(web): tailwind prose black 2023-11-01 13:36:17 -07:00
Zack Spear
65a69b2009 fix(web): state $_SESSION usage 2023-11-01 13:36:17 -07:00
Zack Spear
c07e4f45fb refactor(web): remove consoles 2023-11-01 13:36:17 -07:00
Zack Spear
2fc8169d00 refactor(web): updates expiration no minutes and seconds 2023-11-01 13:36:17 -07:00
Zack Spear
a152943047 chore(web): remove @todo 2023-11-01 13:36:17 -07:00
Zack Spear
4444af6938 refactor(web): improved replaceRenew caching 2023-11-01 13:36:17 -07:00
Zack Spear
ed0b41a425 feat(web): guidValidation if new keyfile auto install 2023-11-01 13:36:17 -07:00
Zack Spear
41879fa27c fix(web): state php warnings 2023-11-01 13:36:17 -07:00
Zack Spear
110108daf6 refactor(web): WIP renewed key file check 2023-11-01 13:36:17 -07:00
Zack Spear
27deaf91cc refactor(web): update os styles for regExp expiration 2023-11-01 13:36:17 -07:00
Zack Spear
37d548db8c refactor(web): key server valid guid response type 2023-11-01 13:36:17 -07:00
Zack Spear
67c2e1f3cf refactor(web): updateOsActions usage 2023-11-01 13:36:17 -07:00
Zack Spear
efc55e77ef fix(web): default time format include am/pm 2023-11-01 13:36:17 -07:00
Zack Spear
a1a10777a5 refactor(web): card wrapper warning styles 2023-11-01 13:36:17 -07:00
Zack Spear
7282bde765 refactor(web): badge yellow text color black 2023-11-01 13:36:17 -07:00
Zack Spear
1a384e53ec refactor(web): button component tweaks 2023-11-01 13:36:17 -07:00
Zack Spear
00c07290ad feat(web): refactor generic updateOS with date comparison 2023-11-01 13:36:17 -07:00
Zack Spear
817f92d398 refactor: Registration component regExp usage & styles 2023-11-01 13:36:17 -07:00
Zack Spear
d943b67270 refactor: Registration component regExp usage & styles 2023-11-01 13:36:17 -07:00
Zack Spear
c171524dc6 test: dev seed data 2023-11-01 13:36:17 -07:00
Zack Spear
0dcff37419 test: updated static releases json 2023-11-01 13:36:17 -07:00
Zack Spear
65ebfc95d0 fix(web): card wrapper error border styles 2023-11-01 13:36:17 -07:00
Zack Spear
e8609526b0 refactor(web): improved ux for update os flash backup 2023-11-01 13:36:17 -07:00
Zack Spear
4bc0015b48 refactor(web): new key type callback payloads 2023-11-01 13:36:17 -07:00
Zack Spear
bfa667c1ab feat(web): update os create flash backup button 2023-11-01 13:36:17 -07:00
Zack Spear
cadbd65cf6 chore(web): button component comment 2023-11-01 13:36:17 -07:00
Zack Spear
eae6d75bca refactor(web): update os check includeNext defaults 2023-11-01 13:36:17 -07:00
Zack Spear
f4ab363901 refactor(web): improved regExp handling 2023-11-01 13:36:17 -07:00
Zack Spear
7c806fee8a fix(web): missing translations 2023-11-01 13:36:17 -07:00
Zack Spear
9f3fab6de4 refactor(web): header os version logic 2023-11-01 13:36:17 -07:00
Zack Spear
2c3c9c441e refactor(web): header os version spacing 2023-11-01 13:36:17 -07:00
Zack Spear
a7644ee487 refactor(web): button styles 2023-11-01 13:36:17 -07:00
Zack Spear
396b98da01 test(web): serverState 2023-11-01 13:36:17 -07:00
Zack Spear
d0da1f4e39 fix(web): missing translation 2023-11-01 13:36:17 -07:00
Zack Spear
a24e73da7e refactor(web): replaceCheck sessionStorage key 2023-11-01 13:36:17 -07:00
Zack Spear
3aa082fec1 refactor(web): update ui improvement 2023-11-01 13:36:17 -07:00
Zack Spear
70fd31afb6 fix(web): Registration key actions 2023-11-01 13:36:17 -07:00
Zack Spear
c299a794b2 refactor(web): KeyActions extensibility 2023-11-01 13:36:17 -07:00
Zack Spear
d3429f31a6 refactor(web): button hover display right icon 2023-11-01 13:36:17 -07:00
Zack Spear
7b951f3e3b refactor(web): Os Update component conditional error styles 2023-11-01 13:36:17 -07:00
Zack Spear
b0bd1b9635 fix(web): replace check request error handling 2023-11-01 13:36:17 -07:00
Zack Spear
10ab864a43 refactor(web): ReplaceCheck status feedback 2023-11-01 13:36:17 -07:00
Zack Spear
6a6f0e9c53 refactor(web): CardWrapper error styles prop 2023-11-01 13:36:17 -07:00
Zack Spear
8cd19bbc26 fix(web): missing translations 2023-11-01 13:36:17 -07:00
Zack Spear
7404c4ce6b refactor(web): docs url 2023-11-01 13:36:17 -07:00
Zack Spear
6d336fda23 refactor(web): upgrade expiration button white 2023-11-01 13:36:17 -07:00
Zack Spear
b9c45d96c1 chore(web): clean up replace check component 2023-11-01 13:36:17 -07:00
Zack Spear
b0e1d5dafb refactor(web): button component additional colors & size prop 2023-11-01 13:36:17 -07:00
Zack Spear
05369a49a4 refactor(web): registration ux/ui button placement 2023-11-01 13:36:17 -07:00
Zack Spear
04916756c6 fix(web): replaceCheck type 2023-11-01 13:36:17 -07:00
Zack Spear
2581254a02 refactor(web): key actions component filter props 2023-11-01 13:36:17 -07:00
Zack Spear
c1b509220e fix(web): replaceCheck type 2023-11-01 13:36:17 -07:00
Zack Spear
676ea0629b chore(web): concise param 2023-11-01 13:36:17 -07:00
Zack Spear
41d6ebe536 refactor(web): progress on regExp & dateTimeFormat from server 2023-11-01 13:36:17 -07:00
Zack Spear
422b93495a refactor(web): formatDate helper format to LLLL 2023-11-01 13:36:17 -07:00
Zack Spear
7246ee34bd feat(web): WIP key expiration 2023-11-01 13:36:17 -07:00
Zack Spear
4d3e8bee84 refactor(web): replace key eligibility using store 2023-11-01 13:36:17 -07:00
Zack Spear
7dffa1a701 refactor(web): HeaderOsVersion text size 2023-11-01 13:36:17 -07:00
Zack Spear
ba16411bf1 feat(web): start prep for new key type support 2023-11-01 13:36:17 -07:00
Zack Spear
de1da57286 fix(web): missing translation for update 2023-11-01 13:36:17 -07:00
Zack Spear
6687a1eba0 refactor(web): RegistrationItem props 2023-11-01 13:36:17 -07:00
Zack Spear
f0998271ba refactor(web): lan ip copy 2023-11-01 13:36:17 -07:00
Zack Spear
a84b972121 feat(web): registration too many devices messaging 2023-11-01 13:36:17 -07:00
Zack Spear
e5b51564fd fix(web): localStorage craftUrl for dev 2023-11-01 13:36:17 -07:00
Zack Spear
6ddcdf2812 chore(web): dev seed data 2023-11-01 13:36:17 -07:00
Zack Spear
bc177ad740 refactor(web): state regTo htmlspecialchars to match original registration.page 2023-11-01 13:36:17 -07:00
Zack Spear
7b471588ab feat(web): localStorage craftUrl for dev 2023-11-01 13:36:17 -07:00
Zack Spear
d7a4e4fde6 refactor(web): tailwind prose styles 2023-11-01 13:36:17 -07:00
Zack Spear
4986b69c62 refactor(web): registration item rounded 2023-11-01 13:36:17 -07:00
Zack Spear
7a22f4ac88 refactor(web): replace eligibility notes + passing keyfile 2023-11-01 13:36:17 -07:00
Zack Spear
f059b6fd0d refactor(web): keyServer validate types 2023-11-01 13:36:17 -07:00
Zack Spear
65fb41c562 refactor(web): replace check use UiBadge for status 2023-11-01 13:36:17 -07:00
Zack Spear
c3d8002a76 feat(web): registration replace eligibility docs btn 2023-11-01 13:36:17 -07:00
Zack Spear
6c98369719 feat(web): registration component ui / ux 2023-11-01 13:36:17 -07:00
Zack Spear
f5b0ca63e8 chore(web): remove console 2023-11-01 13:36:17 -07:00
Zack Spear
90303689db refactor: WIP registration update expiration 2023-11-01 13:36:17 -07:00
Zack Spear
17a5767108 refactor(web): registration page UI UX 2023-11-01 13:36:17 -07:00
Zack Spear
e04b619071 feat(web): WIP registration page UI UX 2023-11-01 13:36:17 -07:00
Zack Spear
858a93ccd2 feat(web): WIP registration page web component 2023-11-01 13:36:17 -07:00
Zack Spear
e22d1f0a6c refactor(web): update handle third-party drivers 2023-11-01 13:36:17 -07:00
Zack Spear
9994dd49f7 refactor(web): theme gamma opaque color for border 2023-11-01 13:36:17 -07:00
Zack Spear
5cf1740977 refactor: reboot detection passed to upc 2023-11-01 13:36:17 -07:00
Zack Spear
297bce3a89 refactor: downgrades working + reboot notice 2023-11-01 13:36:17 -07:00
Zack Spear
d8faef0146 refactor: WIP on downgrade and UI / UX 2023-11-01 13:36:17 -07:00
Zack Spear
57efcef072 feat: WIP first pass at UpdateOs page replacement component 2023-11-01 13:36:17 -07:00
Zack Spear
5c58a86d86 feat: WIP UpdateOs page component 2023-11-01 13:36:17 -07:00
Zack Spear
ab06ed75c3 refactor: update os callback action confirm 2023-11-01 13:36:17 -07:00
Zack Spear
6f812dad90 refactor: updateOs init callback includeNext true 2023-11-01 13:36:17 -07:00
Zack Spear
aa50d88575 refactor: generic updateOs store 2023-11-01 13:36:17 -07:00
Zack Spear
971e879744 refactor: genericized updateOs store to be shared with other repos 2023-11-01 13:36:17 -07:00
Zack Spear
dc2191f228 refactor: WIP updateOs store – response caching and update version checking 2023-11-01 13:36:17 -07:00
Zack Spear
a270b926b3 chore: dev static osReleases json 2023-11-01 13:36:17 -07:00
Zack Spear
051bcf1dc2 chore: dev server state seed data 2023-11-01 13:36:17 -07:00
Zack Spear
578e5ea6b7 chore: @todo callbackfeedback 2023-11-01 13:36:17 -07:00
Zack Spear
56525f8008 refactor: callback payload for updateOS use md5 2023-11-01 13:36:17 -07:00
Zack Spear
32559bab5d feat: server store isOsVersionStable 2023-11-01 13:36:17 -07:00
Zack Spear
cb1f3411ce refactor: callback payload for updateOS use md5 2023-11-01 13:36:17 -07:00
Zack Spear
6fb916eccd feat(web): WIP updateOs callback 2023-11-01 13:36:17 -07:00
Zack Spear
313736e3c6 refactor(web): callbackAction updateOs 2023-11-01 13:36:17 -07:00
Zack Spear
f8eccde99b refactor(web): callback OsRelease type 2023-11-01 13:36:17 -07:00
Zack Spear
c5cc372d7f refactor(web): install plugin composable extensibility 2023-11-01 13:36:17 -07:00
Zack Spear
8b5ba1aa97 wip: update os via upc 2023-11-01 13:36:17 -07:00
Eli Bosley
f4d6755f20 fix: stop using username to determine reg status
Use apikey to determine if you're signed in. That way if your API key is empty it won't attempt to connect / check cloud.
2023-09-29 15:40:38 -04:00
Zack Spear
ed8d69b27f refactor(web): cors error message 2023-09-11 14:03:28 -07:00
Zack Spear
ac216678c0 feat(web): finalize api cors error & settings field 2023-09-11 14:03:28 -07:00
Zack Spear
004ca2437f chore(web): comment remove temp forced upc error 2023-09-11 14:03:28 -07:00
Zack Spear
d96ea5a21a feat(plg): WIP extra origins support 2023-09-11 14:03:28 -07:00
Eli Bosley
c96190447e fix: allow null for the local entry in the myservers cfg 2023-09-11 14:51:38 -04:00
Zack Spear
7194a44822 fix(web): no plugin, don't show restart api button 2023-09-08 16:13:24 -07:00
Zack Spear
cceb33d791 feat(web): create script to move build to webgui repo 2023-09-08 15:20:01 -07:00
Eli Bosley
37565d55eb chore(release): 3.2.3 2023-09-08 09:33:29 -04:00
Eli Bosley
047b0388a7 fix: remove API restart command 2023-09-08 09:11:07 -04:00
Zack Spear
68b1be7477 fix(web): htmlspecialchars name & description 2023-09-07 14:54:45 -07:00
Zack Spear
c5edef47e2 fix(plg): preserve & restore new plg files on install / remove 2023-09-07 13:35:52 -07:00
Zack Spear
60cbbd5a60 fix(web): add missing translations 2023-09-07 12:44:13 -07:00
Zack Spear
98a42d32eb refactor(plg): preserve & restore new upc component dir on install & remove 2023-09-07 12:44:13 -07:00
291 changed files with 28806 additions and 14118 deletions

View File

@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create env file
run: |
@@ -23,7 +23,7 @@ jobs:
cat .env
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
@@ -43,7 +43,7 @@ jobs:
needs: [lint-web]
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create env file
run: |
@@ -55,7 +55,7 @@ jobs:
cat .env
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
@@ -68,7 +68,7 @@ jobs:
run: npm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: unraid-web
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components

View File

@@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Reconfigure git to use HTTP authenti:cation
@@ -38,7 +38,7 @@ jobs:
ssh://git@github.com/
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version-file: "api/.nvmrc"
@@ -69,7 +69,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
persist-credentials: false
@@ -81,10 +81,10 @@ jobs:
- name: Build Docker Compose
run: |
docker network create mothership_default
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: 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:
@@ -93,7 +93,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create env file
run: |
@@ -105,7 +105,7 @@ jobs:
cat .env
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
@@ -130,7 +130,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Add SSH deploy key
uses: shimataro/ssh-key-action@v2
@@ -139,7 +139,7 @@ jobs:
known_hosts: ${{ secrets.KNOWN_HOSTS }}
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version-file: "api/.nvmrc"
@@ -170,7 +170,7 @@ jobs:
echo "::set-output name=API_SHA256::${API_SHA256}"
- name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz
@@ -185,7 +185,7 @@ jobs:
needs: [lint-web]
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create env file
run: |
@@ -197,7 +197,7 @@ jobs:
cat .env
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
@@ -210,7 +210,7 @@ jobs:
run: npm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: unraid-web
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
@@ -227,9 +227,9 @@ jobs:
with:
timezoneLinux: "America/Los_Angeles"
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download unraid web components
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: unraid-web
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
@@ -242,7 +242,7 @@ jobs:
bash ./pkg_build.sh s
bash ./pkg_build.sh p
- name: Upload binary txz and plg to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: connect-files
path: |
@@ -259,19 +259,19 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Make Staging Release Folder
run: mkdir staging-release/
- name: Download unraid-api binary tgz
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: unraid-api
path: staging-release
- name: Download plugin binary tgz
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: connect-files
@@ -298,6 +298,18 @@ jobs:
source: staging-release
out_dir: unraid-api
- name: Upload Staging Plugin to Cloudflare Bucket
uses: jakejarvis/s3-sync-action@v0.5.1
env:
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: 'auto'
SOURCE_DIR: staging-release
DEST_DIR: unraid-api
create-draft-release:
# Only create new draft if this is a version tag
if: |
@@ -307,15 +319,15 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download unraid-api binary tgz
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: unraid-api
- name: Download plugin binary tgz
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: connect-files

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create env file
run: |
@@ -29,7 +29,7 @@ jobs:
cat .env
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
@@ -51,7 +51,7 @@ jobs:
needs: [lint-web]
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Create env file
run: |
@@ -63,7 +63,7 @@ jobs:
cat .env
- name: Install node
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
@@ -76,7 +76,7 @@ jobs:
run: npm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: unraid-web
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components

View File

@@ -24,15 +24,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
persist-credentials: true
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
with:
# network=host driver-opt needed to push to local registry
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: api
target: builder
@@ -57,16 +57,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
persist-credentials: true
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
with:
# network=host driver-opt needed to push to local registry
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: api
target: builder
@@ -97,16 +97,16 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
persist-credentials: true
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
with:
# network=host driver-opt needed to push to local registry
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: api
target: builder
@@ -127,7 +127,7 @@ jobs:
echo "::set-output name=API_SHA256::${API_SHA256}"
- name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz
@@ -155,7 +155,7 @@ jobs:
with:
timezoneLinux: "America/Los_Angeles"
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Build Plugin
run: |
cd source/dynamix.unraid.net
@@ -173,7 +173,7 @@ jobs:
# escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"${RELEASE_NOTES}")
# sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "plugins/dynamix.unraid.net.staging.plg"
- name: Upload binary txz and plg to Github artifacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: connect-files
path: |

View File

@@ -0,0 +1,57 @@
name: Publish Release to Digital Ocean
on:
release:
types: [published]
jobs:
publish-to-digital-ocean:
runs-on: ubuntu-latest
steps:
- name: Download Release Artifacts (Plugins)
uses: dsaltares/fetch-gh-release-asset@master
with:
file: ".*"
regex: true
token: ${{ secrets.GITHUB_TOKEN }}
target: "./"
version: "latest"
- uses: cardinalby/git-get-release-action@v1
id: release-info
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
latest: true
prerelease: false
- name: Get Release Changelog
run: |
notes=$(cat << EOF
${{ steps.release-info.outputs.body }}
EOF
)
escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$notes")
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.plg"
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.staging.plg"
- name: Upload All Release Files to DO Spaces
uses: BetaHuhn/do-spaces-action@v2
with:
access_key: ${{ secrets.DO_ACCESS_KEY }}
secret_key: ${{ secrets.DO_SECRET_KEY }}
space_name: ${{ secrets.DO_SPACE_NAME }}
space_region: ${{ secrets.DO_SPACE_REGION }}
source: "."
out_dir: unraid-api
- name: Upload Staging Plugin to Cloudflare Bucket
uses: jakejarvis/s3-sync-action@v0.5.1
env:
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: 'auto'
SOURCE_DIR: "."
DEST_DIR: unraid-api

3
.gitignore vendored
View File

@@ -50,8 +50,7 @@ typings/
.next
# Visual Studio Code workspace
.vscode/*
!.vscode/extensions.json
.vscode/sftp.json
# OSX
.DS_Store

10
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"recommendations": [
"natizyskunk.sftp",
"davidanson.vscode-markdownlint",
"bmewburn.vscode-intelephense-client",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"esbenp.prettier-vscode"
]
}

View File

@@ -1,7 +1,10 @@
{
"files.associations": {
"*.page": "php"
},
"editor.codeActionsOnSave": {
"source.fixAll": false,
"source.fixAll.eslint": true
"source.fixAll": "never",
"source.fixAll.eslint": "explicit"
},
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#78797d",

21
.vscode/sftp-template.json vendored Normal file
View File

@@ -0,0 +1,21 @@
{
"_comment": "rename this file to .vscode/sftp.json and replace name/host/privateKeyPath for your system",
"name": "Tower",
"host": "Tower.local",
"protocol": "sftp",
"port": 22,
"username": "root",
"privateKeyPath": "C:/Users/username/.ssh/tower",
"remotePath": "/",
"context": "plugin/source/dynamix.unraid.net/",
"uploadOnSave": true,
"useTempFile": false,
"openSsh": false,
"ignore": [
"// comment: ignore dot files/dirs in root of repo",
".github",
".vscode",
".git",
".DS_Store"
]
}

1
api/.dockerignore Normal file
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -2,6 +2,225 @@
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.5.1](https://github.com/unraid/api/compare/v3.5.0...v3.5.1) (2024-02-29)
### Bug Fixes
* build docker command updated to use dc.sh script ([0b40886](https://github.com/unraid/api/commit/0b40886e84f27a94dbf67ef4ca0cd8539ef3913e))
* date format in UnraidCheck.php ([#852](https://github.com/unraid/api/issues/852)) ([6465f2d](https://github.com/unraid/api/commit/6465f2d7b2394090f35e29cdd680d98ce37f3728))
* **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 @heroicons/vue to v2.1.1 ([#804](https://github.com/unraid/api/issues/804)) ([a0eb7ee](https://github.com/unraid/api/commit/a0eb7ee3ec459dbe1992a7f85bf194da30395a74))
* **deps:** update dependency focus-trap to v7.5.4 ([#788](https://github.com/unraid/api/issues/788)) ([fe000e8](https://github.com/unraid/api/commit/fe000e83825e82cac558d3277664a440e59c0e4a))
* **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))
* display dropdown for pro key no connect installed ([#848](https://github.com/unraid/api/issues/848)) ([b559604](https://github.com/unraid/api/commit/b55960429895b46627f1cd3ed1683ee527e62944))
* dropdown reboot link text ([#849](https://github.com/unraid/api/issues/849)) ([a8ed5e5](https://github.com/unraid/api/commit/a8ed5e5628bc71fb783a03c3db92d21805243738))
* os updates rc to stable ([bf1bd88](https://github.com/unraid/api/commit/bf1bd887d60ac085bf4aeae90f11be3b45ee1182))
* state connect values without connect installed ([e47de6c](https://github.com/unraid/api/commit/e47de6c2c5db7a2a1a9b24099feb02023b3a7bbf))
* state php breaking with double quotes in server description ([c6e92aa](https://github.com/unraid/api/commit/c6e92aa3157c9fe9e7b83580881ebcc1cbd03658))
* state php special chars for html attributes ([#853](https://github.com/unraid/api/issues/853)) ([dd4139c](https://github.com/unraid/api/commit/dd4139cf1a7ae5c6f9b00111c33ae124bb17e630))
* unraid-api missing start command + var defaults ([ceb4c58](https://github.com/unraid/api/commit/ceb4c587d20c7527f2b36a3278c310b0e657bfba))
* unraid-api.php $param1 fallback ([909c79c](https://github.com/unraid/api/commit/909c79c8c82500aea1a0d4d00766f788103c5fe3))
## [3.5.0](https://github.com/unraid/api/compare/v3.4.0...v3.5.0) (2024-02-07)
### Features
* add manage account link to all versions of upc dropdown ([678e620](https://github.com/unraid/api/commit/678e620c1902a376b1866265711d5722b4119d8e))
* add new staging url for connect website ([#841](https://github.com/unraid/api/issues/841)) ([4cfc07b](https://github.com/unraid/api/commit/4cfc07b6763dbb79b68cf01f7eaf7cf33370d4db))
* also ship to cloudflare ([#844](https://github.com/unraid/api/issues/844)) ([41c4210](https://github.com/unraid/api/commit/41c42103685209592b272f81a877702da04d0915))
* button add underline-hover-red style option ([f2fa5fa](https://github.com/unraid/api/commit/f2fa5fa49675ef461330be7b7eb3e3e4106983b0))
* changelog modal ([2ddbacd](https://github.com/unraid/api/commit/2ddbacd137cc5748244c3d25ac91f82e64d77f99))
* check update response modal ([39678f0](https://github.com/unraid/api/commit/39678f0bb0ddc5f87ea7f5ed80a0472100ea8b5d))
* create WebguiCheckForUpdate endpoint ([41d546e](https://github.com/unraid/api/commit/41d546eea5fcf6593d7b5047274c074bb89c1802))
* getOsReleaseBySha256 cached endpoint with keyfile header ([cd2413a](https://github.com/unraid/api/commit/cd2413abe8c5baab40e4e5974e08a5d18dce8e0d))
* new check update buttons in dropdown ([ef5fcb9](https://github.com/unraid/api/commit/ef5fcb96a324143da864df803acaa0da1cd00eb7))
* ship preview to different bucket ([#845](https://github.com/unraid/api/issues/845)) ([8e5d247](https://github.com/unraid/api/commit/8e5d247bca83d9e50977c9b16b212841ac9f70ad))
* ship production to different bucket ([#846](https://github.com/unraid/api/issues/846)) ([63c0875](https://github.com/unraid/api/commit/63c08758c76425e007b1779bb2f77b75bc45896e))
* unraidcheck callable from webgui with altUrl & json output ([ba8a67e](https://github.com/unraid/api/commit/ba8a67edfa043f442b11724227129f8d3f6cae0a))
* update modals ([8ad7d8b](https://github.com/unraid/api/commit/8ad7d8be9437e0caa0409da8f7322050919fbbaa))
* update os ignore release ([1955eb2](https://github.com/unraid/api/commit/1955eb23a3cdc30f0a67bc5950a047f83a860d99))
* update os notifications enabled usage + link to enable & more options to account app ([5c82aff](https://github.com/unraid/api/commit/5c82aff80dc7e6d8f4b23e52af29abc2b8576424))
* updateOs check response determines if update auth is required ([a9816d9](https://github.com/unraid/api/commit/a9816d9ad48ff80d87b5aeb236ff60c4979ad298))
* updateOs store call local server-side endpoint & add modal support ([be48447](https://github.com/unraid/api/commit/be48447f943828af281095c5a092ac686e729030))
* upgrade a ton of dependencies ([#842](https://github.com/unraid/api/issues/842)) ([94c1746](https://github.com/unraid/api/commit/94c174620c2347a3cf3d100404635f99a5b47287))
* WebguiCheckForUpdate using server-side check ([590deb1](https://github.com/unraid/api/commit/590deb130c301d4004fecdc211270583806b5593))
### Bug Fixes
* backport _var() PHP function to older versions of Unraid ([f53150e](https://github.com/unraid/api/commit/f53150e1fa33b3f45b66ad0dc5eaabc470564d45))
* changlog relative links and external links ([a789e20](https://github.com/unraid/api/commit/a789e204ce7b966e6c935923626538ac344aeefe))
* check update response modal expired key button styles ([92993e3](https://github.com/unraid/api/commit/92993e3e0b6240c83a6a64efedd8ddb3be3f9ef7))
* **deps:** update dependency ws to v8.16.0 ([#815](https://github.com/unraid/api/issues/815)) ([212020e](https://github.com/unraid/api/commit/212020e78d4de0576137058a3374837b4a43e02d))
* extraLinks when no updates available ([853a991](https://github.com/unraid/api/commit/853a9911e3fd7eec9bbc88468de78f87b448d477))
* ignore release localStorage ([62c45ec](https://github.com/unraid/api/commit/62c45ec9d7c68498bbcfe933a5b63e4759c7129c))
* lint ([83235f9](https://github.com/unraid/api/commit/83235f9db726f4582b9d353a66f2f5e8925b8e34))
* lint unused value ([2c7e53b](https://github.com/unraid/api/commit/2c7e53bf67d1f214201624b39786bfb7de6aa520))
* marked-base-url install ([416ba71](https://github.com/unraid/api/commit/416ba716aa750a094e8cd521a79f6deebcd37864))
* missing translations ([faf17e4](https://github.com/unraid/api/commit/faf17e41e81c11443bc062d8ce35a33d9ae9ebbc))
* regTm format after key install without page refresh ([f3ddb31](https://github.com/unraid/api/commit/f3ddb31f994de9192f7203698ecc5d7de673c6a3))
* regTm format when already set ([5ad911f](https://github.com/unraid/api/commit/5ad911f8133daa60de53da738d41c6a59e2f02cc))
* ServerUpdateOsResponse type ([78bdae8](https://github.com/unraid/api/commit/78bdae86c907142d3ee32d6715eaa8f5a974a1ed))
* State Class usage in other files ([4ad7f53](https://github.com/unraid/api/commit/4ad7f53ec145b2e6d2895619523e90c1daa3f68f))
* state data humanReadable switch fallthrus ([9144e39](https://github.com/unraid/api/commit/9144e39d39aa56af0ad897735d1a3545330920d0))
* state php usage from cli ([46fd321](https://github.com/unraid/api/commit/46fd321707c14cd1f265ee806f673500d87132dd))
* translations ([3fabd57](https://github.com/unraid/api/commit/3fabd5756674c06fa803729cf13d19c592d8d46a))
* type issue with changlelog modal visibility ([e3c3f6b](https://github.com/unraid/api/commit/e3c3f6bf0f1882788291db17bd74865fefc3abf6))
## [3.4.0](https://github.com/unraid/api/compare/v3.3.0...v3.4.0) (2024-01-11)
### Features
* add logrotate to cron in nestjs ([#839](https://github.com/unraid/api/issues/839)) ([5c91524](https://github.com/unraid/api/commit/5c91524d849147c0ac7925f3a2f1cce67ffe75de))
### Bug Fixes
* 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))
* excessive logging ([89cb254](https://github.com/unraid/api/commit/89cb2544ed0e0edd33b59f15d487487e22c0ae32))
* run hourly ([0425794](https://github.com/unraid/api/commit/0425794356a01262222e7dff2645d3629e00d0f6))
## [3.3.0](https://github.com/unraid/api/compare/v3.2.3...v3.3.0) (2024-01-09)
### Features
* add button to add current origin to extra origins setting ([8c15163](https://github.com/unraid/api/commit/8c15163b3b072122bff1f8f25de62594b1e67992))
* add environment to docker-compose ([2ee4683](https://github.com/unraid/api/commit/2ee46839095e3b8ee287cfe10f29ae9a39dcff68))
* 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))
* change sort order of Update/Downgrade ([#754](https://github.com/unraid/api/issues/754)) ([be96b3a](https://github.com/unraid/api/commit/be96b3aac709682a6517fa6e84beb586b9d8bf5c))
* check for OS updates via PHP ([#752](https://github.com/unraid/api/issues/752)) ([4496615](https://github.com/unraid/api/commit/44966157b80a51dfe01d927c2af2d010c04becc5))
* close log on exit ([d6ede86](https://github.com/unraid/api/commit/d6ede86eca6301342cdf35bf1f9365896b5e5009))
* disable account & key actions when unraid-api CORS error ([1d15406](https://github.com/unraid/api/commit/1d1540646a264038ae96f4063c31a40cd048d2f9))
* 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))
* improve check for OS updates via PHP ([cde12b2](https://github.com/unraid/api/commit/cde12b247f9bba97644750cd95a2b0db320ca1d9))
* local start command ([99b6007](https://github.com/unraid/api/commit/99b6007ba30353084a8bea54cc0e782fcc1bfea4))
* log config recreation reason ([f36c72f](https://github.com/unraid/api/commit/f36c72f5ad44b7e41d1726fa181dc2b9f594c72c))
* 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))
* npm scripts to prevent webgui builds with wrong urls ([279966a](https://github.com/unraid/api/commit/279966afa3c218fbe85bafe91ee40fff2eb59ef2))
* patch DefaultPageLayout for web component ([629fec6](https://github.com/unraid/api/commit/629fec64f911131e4ab3810c99028b484ce18b83))
* **plg:** WIP extra origins support ([85acaae](https://github.com/unraid/api/commit/85acaaee02dad98eeef8a8c4a09b463e84d593b4))
* regTy swapped ([564b25c](https://github.com/unraid/api/commit/564b25cf5ce0a62d40f8d63d44c81e9c8560e0be))
* run codegen and update build script ([07512ad](https://github.com/unraid/api/commit/07512adc13ee0d819db45ff6c5c5f58a0ba31141))
* server store isOsVersionStable ([b5ee4d4](https://github.com/unraid/api/commit/b5ee4d4ee632a7528e6f5df079cab0cb5ea656eb))
* stretch downgrade component buttons ([fa4f63e](https://github.com/unraid/api/commit/fa4f63e8bfca525ccfedb16f19d395bf11a68561))
* swap to fragement usage on webcomponent ([42733ab](https://github.com/unraid/api/commit/42733abf6e443516ff715569333422ce80d3b1d2))
* **web:** caseModel ([4174d0b](https://github.com/unraid/api/commit/4174d0bf2cac99af5db48e5642e0037d7425c952))
* **web:** create script to move build to webgui repo ([92df453](https://github.com/unraid/api/commit/92df453255fed45210d9a192c68bb27d3b0ee981))
* **web:** downgrade os web component ([45496ab](https://github.com/unraid/api/commit/45496ab7685d4bbfe591be46489260bac9b03474))
* **web:** finalize api cors error & settings field ([e1d9e16](https://github.com/unraid/api/commit/e1d9e16b8e80e0940a0078131ea629559e3238ec))
* **web:** guidValidation if new keyfile auto install ([0abb196](https://github.com/unraid/api/commit/0abb196d2c57ead4dca2adb2981ab79cdd1647c4))
* **web:** localStorage craftUrl for dev ([e646187](https://github.com/unraid/api/commit/e646187b04548c010cf26c7ae38a82ced6270394))
* **web:** refactor generic updateOS with date comparison ([91a753c](https://github.com/unraid/api/commit/91a753cd7018b89d53e9cd2d7c429ce53e291336))
* **web:** registration component ui / ux ([717d873](https://github.com/unraid/api/commit/717d8733bd4b8c87b6ae6f1cd66717056c5df876))
* **web:** registration replace eligibility docs btn ([b69285f](https://github.com/unraid/api/commit/b69285ff8ca5b896082b5f0e1aeba70f9a2c5129))
* **web:** registration too many devices messaging ([1c0b5a3](https://github.com/unraid/api/commit/1c0b5a317aadf6173405770878e6038d4d8b448f))
* **web:** start prep for new key type support ([5c5035a](https://github.com/unraid/api/commit/5c5035a5446516999729ddc56d1077ee512f14d3))
* **web:** update os create flash backup button ([50ba61c](https://github.com/unraid/api/commit/50ba61cf80b7df2d121962cf4ec4b10952e8eecb))
* **web:** WIP key expiration ([24618fe](https://github.com/unraid/api/commit/24618fe09db2109c2eb57ab1655ab0fb7d79fc90))
* **web:** WIP registration page UI UX ([559e5b8](https://github.com/unraid/api/commit/559e5b8698d5df80ca57f530a2bf2cb6f01e30c7))
* **web:** WIP registration page web component ([bd772a9](https://github.com/unraid/api/commit/bd772a9c97d49b57a0b5a0e6a367c9a4e3732086))
* **web:** WIP updateOs callback ([2ad55ed](https://github.com/unraid/api/commit/2ad55ed019155e46d8627ea5c1b82cd5e4351127))
* WIP first pass at UpdateOs page replacement component ([3a5d871](https://github.com/unraid/api/commit/3a5d871f1fd054720c3693705484072ff567ff28))
* WIP UpdateOs page component ([8e4c36d](https://github.com/unraid/api/commit/8e4c36d38ce4e70307f5d14c953d5103c8b7e8e4))
### Bug Fixes
* 6.10 view release notes js ([254d894](https://github.com/unraid/api/commit/254d894f39e512d1b4a0472180cb27090de256a0))
* add missing translation keys ([03b506c](https://github.com/unraid/api/commit/03b506cd4e68f23a85bbfd54205322a6a4f93e5b))
* add serverName / description to dashboard payload ([9677aff](https://github.com/unraid/api/commit/9677aff1cd0942f36a2845f3f105601c494efd9e))
* allow null for the local entry in the myservers cfg ([01157c8](https://github.com/unraid/api/commit/01157c86ea3838ca675d65528a882cf25d0019a6))
* azure and gray theme custom colors ([92e552c](https://github.com/unraid/api/commit/92e552c9c7f7804902f18eb2d71f8483671fe048))
* codegen on web run ([e2e67c2](https://github.com/unraid/api/commit/e2e67c21067a138d963f5f10760b84cf6a533542))
* combinedKnownOrigins in state.php for UPC ([b550eea](https://github.com/unraid/api/commit/b550eeae7077cbdbd6d004506bdc96d04c04bc4c))
* Connect settings myservers config parse ([1c1483a](https://github.com/unraid/api/commit/1c1483a5cc506deab9d858dabbb8388c8b1d1ec1))
* dateTime system settings ([56ccbff](https://github.com/unraid/api/commit/56ccbff61fb61ab67277100c525b80adf95e9b72))
* **deps:** update dependency graphql to v16.8.1 ([bff1b19](https://github.com/unraid/api/commit/bff1b19706bee1e3103e3a0a1d2fceb3154f9bba))
* **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))
* downgrade remove erroneous file_get_contents ([df9c918](https://github.com/unraid/api/commit/df9c91867cf3f7cf6b424a386d7e68bd510ec20f))
* exit with process.exit not process.exitcode ([dcb6def](https://github.com/unraid/api/commit/dcb6def1cf3365dca819feed101160c8ad0125dc))
* graphQL CORS error detection ([e5ea67f](https://github.com/unraid/api/commit/e5ea67fe5224fd5aaf06e1e63e7efc01974a10ac))
* header version thirdPartyDriversDownloading pill ([c2ff31c](https://github.com/unraid/api/commit/c2ff31c672bc30683062c6cefbd5e744a7a2a676))
* lint unused param var prefixed ([8d103a9](https://github.com/unraid/api/commit/8d103a9ca89139d7b4f513318a67bcc64c0daa0c))
* 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))
* missing translation ([81a9380](https://github.com/unraid/api/commit/81a93802993e7d95fb587cbfe3b598136a89348b))
* optional check on api.version to allow fallback to save value ([0ac4455](https://github.com/unraid/api/commit/0ac4455f78407eca7aa1d6ee360830067a1c5c3e))
* patch ShowChanges.php in 6.10 ([92d09c2](https://github.com/unraid/api/commit/92d09c2846c1bf64276e140c4cf4635e8bbfa94b))
* plg installer header version replacement ([7d0de2c](https://github.com/unraid/api/commit/7d0de2c8b3dc3c2d3c204e7846cf65d6df07545f))
* plg remove reboot-details path ([d54d90e](https://github.com/unraid/api/commit/d54d90ec04c67ee532cbcb77c4c5890545899e5a))
* **plg:** Downgrade & Update page file locations ([3fbb6b7](https://github.com/unraid/api/commit/3fbb6b70c1152d0691f3d74298908338e19cda53))
* **plg:** third party reboot detection ([f0ee640](https://github.com/unraid/api/commit/f0ee640767e446a829fd2e60033560786e5f63b0))
* plugin install should suppress output from `unraid-api stop` ([#757](https://github.com/unraid/api/issues/757)) ([3da5d95](https://github.com/unraid/api/commit/3da5d9573b499c84c25e33b26a2014e79bef40f7))
* rearrange exit hook to try to fix closing ([843d3f4](https://github.com/unraid/api/commit/843d3f41162c5dbcfd7803912b1879d7a182231a))
* refreshServerState check regExp ([7fca971](https://github.com/unraid/api/commit/7fca971cab40b6e5493e7e21baf85e3d6ba66b90))
* remove var_dump Connect settings ([9425f8b](https://github.com/unraid/api/commit/9425f8b133d44ac759d09158eadd13c81e7796fb))
* renew callback messaging in modal ([e98d065](https://github.com/unraid/api/commit/e98d0654237b111cf912eb5014dbcc5da0e92ca3))
* replaceRenew response cache use & purge ([ca85199](https://github.com/unraid/api/commit/ca851991ecb09720d70135d302aa93ad10a96d3a))
* set sha in test step as well. ([8af3367](https://github.com/unraid/api/commit/8af3367226f9a3bc51db65ffe5dd53d6c5aa0017))
* state php version checking ([494f5e9](https://github.com/unraid/api/commit/494f5e9935bc207b81098e84a0fe3e259939cf39))
* stop using username to determine reg status ([c5a6cd7](https://github.com/unraid/api/commit/c5a6cd7bf930d8bc94ccae45f5363c12fd1fccfc))
* ThirdPartyDriver messaging on Update page ([f23ad76](https://github.com/unraid/api/commit/f23ad762c04c3da918429a376146fe096a5030d5))
* try to set environment in docker build ([caece63](https://github.com/unraid/api/commit/caece63e7f180f94a7ee6b962c905296c6b987bb))
* uninstall reboot-details include ([3849462](https://github.com/unraid/api/commit/3849462f572659a43157a49511075f2d8cd5dd4c))
* unraid-api server state refresh after key extension use regExp ([490595f](https://github.com/unraid/api/commit/490595f9b420054e6c2fe40d868b902b262718af))
* updateOs auth group usage ([52b1ad9](https://github.com/unraid/api/commit/52b1ad9a7d3c9cdc989dd729d7828b0678349c27))
* updateOs type check ([ba230e2](https://github.com/unraid/api/commit/ba230e2643399fbfa1612059f235ccdf61f7f486))
* web component translations class ([6c81f6f](https://github.com/unraid/api/commit/6c81f6f70dcbe4f055a0041863fe275d6e01d6b9))
* **web:** azure & gray theme header font colors ([8a5c7c9](https://github.com/unraid/api/commit/8a5c7c9304a063b26d7ff2df5c174aa9f1c0f53c))
* **web:** card wrapper error border styles ([c71f420](https://github.com/unraid/api/commit/c71f420a4c9f7325127e3f38157dbc6255b3e139))
* **web:** connect graph error handling ([c239937](https://github.com/unraid/api/commit/c239937c407cfea0defde1994809a5c0a196cca2))
* **web:** default time format include am/pm ([31694cd](https://github.com/unraid/api/commit/31694cd7141e2ec0b0c3b4e4480d34d19c80adae))
* **web:** downgrade status pill for no downgrade available ([9d9ebb1](https://github.com/unraid/api/commit/9d9ebb1c6efd486a90dcd78ba63766e24be26d55))
* **web:** downgrade-not-available when downgrade initiated ([d060359](https://github.com/unraid/api/commit/d0603592596a3173889e9d06d57cfaa602eb80bb))
* **web:** installPlugin composable for os updates ([9fb024a](https://github.com/unraid/api/commit/9fb024a68d65905e5351cfa71ca64cdffa0fa74c))
* **web:** lint fixes ([224d637](https://github.com/unraid/api/commit/224d63773d505b8d65c9455fb94260ae617d9fe5))
* **web:** localStorage craftUrl for dev ([2e108da](https://github.com/unraid/api/commit/2e108da0db7de01d03ee3b0657a614355a61b208))
* **web:** missing translation ([74a8f27](https://github.com/unraid/api/commit/74a8f27643d7ba9c9d5dcd6a43b189a936dae648))
* **web:** missing translation for update ([cb46a94](https://github.com/unraid/api/commit/cb46a94c7238bf381fbfc48109b1dd648d2e4949))
* **web:** missing translations ([8ea733b](https://github.com/unraid/api/commit/8ea733b295a5f3bd922e867f544e5538873a5088))
* **web:** missing translations ([d2eed92](https://github.com/unraid/api/commit/d2eed9291de9297aa0d556f06b9b8f5f09734250))
* **web:** no plugin, don't show restart api button ([e628a8b](https://github.com/unraid/api/commit/e628a8b64fab4d1a5ce84af62abde3cd4c53ba96))
* **web:** preview and test releases usage ([4b8cfb4](https://github.com/unraid/api/commit/4b8cfb464e8296ce20d6ff3870949d739a86ca1b))
* **web:** reboot required disable update check link ([f029652](https://github.com/unraid/api/commit/f0296528bae52227ecbe281786ddf4d3a0cc940f))
* **web:** reg component conditional keyActions ([730dff2](https://github.com/unraid/api/commit/730dff2e6344f7ee076e1c67d82ef0783a5931b2))
* **web:** Registration key actions ([f7b1016](https://github.com/unraid/api/commit/f7b1016980c3f576b007a1d01184bf35f0eef311))
* **web:** regTy on account payload ([64b0b5e](https://github.com/unraid/api/commit/64b0b5eb5767d41012f6bcb9536030ec39e45af9))
* **web:** regUpdatesExpired use .isAfter ([5d67adf](https://github.com/unraid/api/commit/5d67adf4625a108e3374eb72714cdc1747b2a9c5))
* **web:** replace check request error handling ([c1491fe](https://github.com/unraid/api/commit/c1491fecdc327d78f8de7c0f04fda481fb47cb56))
* **web:** replaceCheck type ([1bd9729](https://github.com/unraid/api/commit/1bd9729b0197b49ca460912bbc56cd3b206d00dc))
* **web:** replaceCheck type ([8cc6020](https://github.com/unraid/api/commit/8cc602019a2c8a718b59590d166644a1cb4d16cc))
* **web:** state $_SESSION usage ([412392d](https://github.com/unraid/api/commit/412392dc1c5e612199e76ee7e1cae03705957e3d))
* **web:** state php warnings ([1460cab](https://github.com/unraid/api/commit/1460cabe6b041f9f9fb89ca474a7d7e872d31c39))
* **web:** translation ([cc85a49](https://github.com/unraid/api/commit/cc85a4903178999dbb80da50aa3b02ff38012172))
* **web:** type errors ([e6c57eb](https://github.com/unraid/api/commit/e6c57eb910a1c1f948a3104c4e7fc04ac8b2d327))
* **web:** upc dropdown updates external icon ([13936bb](https://github.com/unraid/api/commit/13936bb157f9097a19c7498fce252f3f86526ccb))
* **web:** update CallbackButton import ([eabfeca](https://github.com/unraid/api/commit/eabfeca618d3bf682a331c6d9e1f17b5facdcdca))
* **web:** Update OS auto redirect loop with account ([9b56fc3](https://github.com/unraid/api/commit/9b56fc3883f51942de9b1c8d1d1f30595fee7fa5))
* **web:** updateOs lint ([bd9e9d5](https://github.com/unraid/api/commit/bd9e9d55cc7bba432f65d78feee83526dbfff059))
* **web:** use dateTime format from server ([7090f38](https://github.com/unraid/api/commit/7090f38a9ab8b2d1dfce4095f4e2669d4d78a3e1))
### [3.2.3](https://github.com/unraid/api/compare/v3.2.2...v3.2.3) (2023-09-08)
### Bug Fixes
* **plg:** preserve & restore new plg files on install / remove ([7e1f59a](https://github.com/unraid/api/commit/7e1f59afd218235934a53ac4ea6fd166689269a4))
* remove API restart command ([0eb1530](https://github.com/unraid/api/commit/0eb1530d649647f47d26de459e394fd48e79b071))
* **web:** add missing translations ([0227a1e](https://github.com/unraid/api/commit/0227a1ed1bdf953eae7784fccf04dd94995f5114))
* **web:** htmlspecialchars name & description ([a874fd8](https://github.com/unraid/api/commit/a874fd8f4b2fdf5d261f3b167452532bf09059ab))
### [3.2.2](https://github.com/unraid/api/compare/v3.2.1...v3.2.2) (2023-09-07)

View File

@@ -1,20 +1,19 @@
###########################################################
# Development/Build Image
###########################################################
FROM node:18.17.1-alpine As development
FROM node:18.17.1-bookworm-slim As development
# Install build tools and dependencies
RUN apk add --no-cache \
RUN apt-get update -y && apt-get install -y \
bash \
# Real PS Command (needed for some dependencies)
procps \
alpine-sdk \
python3 \
libvirt-dev \
jq \
zstd
RUN mkdir /var/log/unraid-api/
zstd \
git \
build-essential
WORKDIR /app
@@ -33,7 +32,7 @@ COPY package.json package-lock.json ./
RUN npm i -g pkg zx
# Install deps
RUN npm ci
RUN npm i
EXPOSE 4000

View File

@@ -1,5 +1,6 @@
[api]
version="3.1.1+8efc0992"
version="3.4.0"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
@@ -15,5 +16,6 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken=""
accesstoken=""
refreshtoken=""
dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"

View File

@@ -1,5 +1,6 @@
[api]
version="3.1.1+8efc0992"
version="3.4.0"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
@@ -15,7 +16,8 @@ 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://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
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"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus]

143
api/dev/states/var.ini Normal file
View File

@@ -0,0 +1,143 @@
version="6.11.2"
MAX_ARRAYSZ="30"
MAX_CACHESZ="30"
NAME="Tower"
timeZone="Australia/Adelaide"
COMMENT="Dev Server"
SECURITY="user"
WORKGROUP="WORKGROUP"
DOMAIN=""
DOMAIN_SHORT=""
hideDotFiles="no"
localMaster="yes"
enableFruit="no"
USE_NETBIOS="yes"
USE_WSD="no"
WSD_OPT=""
USE_NTP="yes"
NTP_SERVER1="time1.google.com"
NTP_SERVER2="time2.google.com"
NTP_SERVER3="time3.google.com"
NTP_SERVER4="time4.google.com"
DOMAIN_LOGIN="Administrator"
SYS_MODEL="Dell R710"
SYS_ARRAY_SLOTS="24"
SYS_FLASH_SLOTS="1"
USE_SSL="auto"
PORT="80"
PORTSSL="443"
LOCAL_TLD="local"
BIND_MGT="no"
USE_TELNET="yes"
PORTTELNET="23"
USE_SSH="yes"
PORTSSH="22"
USE_UPNP="yes"
START_PAGE="Main"
startArray="no"
spindownDelay="0"
spinupGroups="no"
defaultFsType="xfs"
shutdownTimeout="90"
luksKeyfile="/tmp/unraid/keyfile"
poll_attributes="1800"
poll_attributes_default="1800"
poll_attributes_status="default"
queueDepth="auto"
nr_requests="Auto"
nr_requests_default="Auto"
nr_requests_status="default"
md_scheduler="auto"
md_scheduler_default="auto"
md_scheduler_status="default"
md_num_stripes="1280"
md_num_stripes_default="1280"
md_num_stripes_status="default"
md_queue_limit="80"
md_queue_limit_default="80"
md_queue_limit_status="default"
md_sync_limit="5"
md_sync_limit_default="5"
md_sync_limit_status="default"
md_write_method="auto"
md_write_method_default="auto"
md_write_method_status="default"
shareDisk="yes"
shareUser="e"
shareUserInclude=""
shareUserExclude=""
shareSMBEnabled="yes"
shareNFSEnabled="no"
shareInitialOwner="Administrator"
shareInitialGroup="Domain Users"
shareCacheEnabled="yes"
shareCacheFloor="2000000"
shareMoverSchedule="40 3 * * *"
shareMoverLogging="no"
fuse_remember="330"
fuse_remember_default="330"
fuse_remember_status="default"
fuse_directio="auto"
fuse_directio_default="auto"
fuse_directio_status="default"
fuse_useino="yes"
shareAvahiEnabled="yes"
shareAvahiSMBName="%h"
shareAvahiSMBModel="Xserve"
shfs_logging="1"
safeMode="no"
startMode="Normal"
configValid="yes"
joinStatus="Not joined"
deviceCount="4"
flashGUID="0000-0000-0000-000000000000"
flashProduct="DataTraveler_3.0"
flashVendor="KINGSTON"
regCheck=""
regFILE="/app/dev/Unraid.net/Pro.key"
regGUID="13FE-4200-C300-58C372A52B19"
regTy="Pro"
regTo="Eli Bosley"
regTm="1833409182"
regTm2="0"
regExp=""
regGen="0"
sbName="/boot/config/super.dat"
sbVersion="2.9.13"
sbUpdated="1596079143"
sbEvents="173"
sbState="1"
sbClean="yes"
sbSynced="1586819259"
sbSyncErrs="0"
sbSynced2="1586822456"
sbSyncExit="0"
sbNumDisks="5"
mdColor="green-blink"
mdNumDisks="4"
mdNumDisabled="1"
mdNumInvalid="1"
mdNumMissing="0"
mdNumNew="0"
mdNumErased="0"
mdResync="0"
mdResyncCorr="0"
mdResyncPos="0"
mdResyncDb="0"
mdResyncDt="0"
mdResyncAction="check P"
mdResyncSize="438960096"
mdState="STOPPED"
mdVersion="2.9.14"
fsState="Stopped"
fsProgress="Autostart disabled"
fsCopyPrcnt="0"
fsNumMounted="0"
fsNumUnmountable="0"
fsUnmountableMask=""
shareCount="0"
shareSMBCount="1"
shareNFSCount="0"
shareMoverActive="no"
reservedNames="parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31"
csrf_token="0000000000000000"

View File

@@ -45,12 +45,35 @@ services:
entrypoint: /bin/bash
environment:
- IS_DOCKER=true
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
profiles:
- builder
local:
networks:
- mothership_default
image: unraid-api:development
ports:
- "3001:3001"
build:
context: .
target: development
dockerfile: Dockerfile
<<: *volumes
command: npm run start:dev
environment:
- IS_DOCKER=true
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
profiles:
- builder
builder:
image: unraid-api:builder
environment:
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
build:
context: .
target: builder

13252
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.2.2",
"version": "3.5.1",
"main": "dist/index.js",
"bin": "dist/unraid-api.cjs",
"type": "module",
@@ -26,29 +26,31 @@
"compile": "tsup --config ./tsup.config.ts",
"bundle": "pkg . --public",
"build": "npm run compile && npm run bundle",
"build:docker": "docker-compose run --rm builder",
"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:local": "MOTHERSHIP_GRAPHQL_LINK='http://localhost:3000/graphql' graphql-codegen-esm --config codegen.yml --watch",
"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",
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
"test:watch": "vitest --segfault-retry=3 --no-threads",
"test": "vitest run --segfault-retry=3 --no-threads",
"test:watch": "vitest --segfault-retry=3 --pool=forks",
"test": "vitest run --segfault-retry=3 --pool=forks",
"coverage": "vitest run --segfault-retry=3 --coverage",
"patch:subscriptions-transport-ws": "node ./.scripts/patches/subscriptions-transport-ws.cjs",
"release": "standard-version",
"typesync": "typesync",
"install:unraid": "./scripts/install-in-unraid.sh",
"start:plugin": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin": "INTROSPECTION=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs start --debug'",
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
"restart:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs restart --debug'",
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
"start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'",
"start:docker": "docker compose run --rm builder-interactive",
"docker:dev": "docker-compose run --rm --service-ports dev",
"docker:test": "docker-compose run --rm builder npm run test"
"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"
},
"files": [
".env.staging",
@@ -57,14 +59,21 @@
"unraid-api"
],
"dependencies": {
"@apollo/client": "^3.7.12",
"@apollo/server": "^4.6.0",
"@graphql-codegen/client-preset": "^3.0.0",
"@graphql-tools/load-files": "^6.6.1",
"@graphql-tools/merge": "^8.4.0",
"@graphql-tools/schema": "^9.0.17",
"@graphql-tools/utils": "^9.2.1",
"@reduxjs/toolkit": "^1.9.5",
"@apollo/client": "^3.8.9",
"@apollo/server": "^4.10.0",
"@as-integrations/fastify": "^2.1.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@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",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.0",
"@nestjs/schedule": "^4.0.0",
"@reduxjs/toolkit": "^2.0.1",
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
"accesscontrol": "^2.2.1",
@@ -75,121 +84,126 @@
"bytes": "^3.1.2",
"cacheable-lookup": "^6.1.0",
"catch-exit": "^1.2.2",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cli-table": "^0.3.11",
"command-exists": "^1.2.9",
"convert": "^4.10.0",
"convert": "^4.14.1",
"cors": "^2.8.5",
"cross-fetch": "^4.0.0",
"docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5",
"dotenv": "^16.0.3",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"find-process": "^1.4.7",
"graphql": "^16.6.0",
"graphql": "^16.8.1",
"graphql-fields": "^2.0.3",
"graphql-scalars": "^1.21.3",
"graphql-scalars": "^1.22.4",
"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.12.1",
"graphql-ws": "^5.14.3",
"htpasswd-js": "^1.0.2",
"ini": "^4.1.0",
"ini": "^4.1.1",
"ip": "^1.1.8",
"jose": "^4.14.2",
"lodash": "^4.17.21",
"multi-ini": "^2.2.0",
"mustache": "^4.2.0",
"nanobus": "^4.5.0",
"nest-access-control": "^3.1.0",
"nestjs-pino": "^4.0.0",
"node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2",
"openid-client": "^5.4.0",
"openid-client": "^5.6.4",
"p-iteration": "^1.1.8",
"p-retry": "^4.6.2",
"passport-http-header-strategy": "^1.1.0",
"pidusage": "^3.0.2",
"reflect-metadata": "^0.1.13",
"pino": "^8.17.2",
"pino-http": "^9.0.0",
"pino-pretty": "^10.3.1",
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.4.0",
"semver": "^7.5.4",
"stoppable": "^1.1.0",
"subscriptions-transport-ws": "^0.11.0",
"systeminformation": "^5.21.2",
"ts-command-line-args": "^2.5.0",
"uuid": "^9.0.0",
"systeminformation": "^5.21.22",
"ts-command-line-args": "^2.5.1",
"uuid": "^9.0.1",
"ws": "^8.13.0",
"wtfnode": "^0.9.1",
"xhr2": "^0.2.1",
"zod": "^3.22.2"
"zod": "^3.22.4"
},
"devDependencies": {
"@babel/runtime": "^7.21.0",
"@graphql-codegen/add": "^4.0.1",
"@graphql-codegen/cli": "^3.3.0",
"@graphql-codegen/fragment-matcher": "^4.0.1",
"@graphql-codegen/import-types-preset": "^2.2.6",
"@graphql-codegen/typed-document-node": "^4.0.0",
"@graphql-codegen/typescript": "^3.0.3",
"@graphql-codegen/typescript-operations": "^3.0.3",
"@graphql-codegen/typescript-resolvers": "3.2.1",
"@babel/runtime": "^7.23.8",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@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-typed-document-node/core": "^3.2.0",
"@swc/core": "^1.3.81",
"@types/async-exit-hook": "^2.0.0",
"@types/btoa": "^1.2.3",
"@types/bytes": "^3.1.1",
"@types/cli-table": "^0.3.1",
"@types/command-exists": "^1.2.0",
"@nestjs/testing": "^10.3.0",
"@swc/core": "^1.3.102",
"@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/express": "^4.17.17",
"@types/graphql-fields": "^1.3.5",
"@types/graphql-type-uuid": "^0.2.3",
"@types/ini": "^1.3.31",
"@types/lodash": "^4.14.192",
"@types/mustache": "^4.2.2",
"@types/node": "^18.17.12",
"@types/pidusage": "^2.0.2",
"@types/pify": "^5.0.1",
"@types/semver": "^7.3.13",
"@types/sendmail": "^1.4.4",
"@types/stoppable": "^1.1.1",
"@types/uuid": "^9.0.1",
"@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/mustache": "^4.2.5",
"@types/node": "^20.11.0",
"@types/pidusage": "^2.0.5",
"@types/pify": "^5.0.4",
"@types/semver": "^7.5.6",
"@types/sendmail": "^1.4.7",
"@types/stoppable": "^1.1.3",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.4",
"@types/wtfnode": "^0.7.0",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@types/wtfnode": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"@unraid/eslint-config": "github:unraid/eslint-config",
"@vitest/coverage-v8": "^0.34.1",
"@vitest/ui": "^0.34.0",
"@vitest/coverage-v8": "^1.2.0",
"@vitest/ui": "^1.2.0",
"camelcase-keys": "^8.0.2",
"cz-conventional-changelog": "3.3.0",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^48.0.1",
"eslint-plugin-unused-imports": "^2.0.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",
"execa": "^7.1.1",
"filter-obj": "^5.1.0",
"got": "^13.0.0",
"graphql-codegen-typescript-validation-schema": "^0.11.0",
"graphql-codegen-typescript-validation-schema": "^0.12.1",
"ip-regex": "^5.0.0",
"json-difference": "^1.9.1",
"log4js": "^6.9.1",
"json-difference": "^1.16.0",
"map-obj": "^5.0.2",
"p-props": "^5.0.0",
"path-exists": "^5.0.0",
"path-type": "^5.0.0",
"pkg": "^5.8.1",
"pretty-bytes": "^6.1.0",
"pretty-bytes": "^6.1.1",
"pretty-ms": "^8.0.0",
"serialize-error": "^11.0.2",
"standard-version": "^9.5.0",
"tsup": "^7.0.0",
"typescript": "^4.9.4",
"typesync": "^0.11.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.34.0",
"zx": "^7.2.1"
"tsup": "^8.0.1",
"typescript": "^5.3.3",
"typesync": "^0.12.1",
"vite-tsconfig-paths": "^4.2.3",
"vitest": "^1.2.0",
"zx": "^7.2.3"
},
"optionalDependencies": {
"@vmngr/libvirt": "github:unraid/libvirt"

4
api/scripts/dc.sh Executable file
View File

@@ -0,0 +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 "$@"

View File

@@ -12,6 +12,7 @@ const runCommand = (command) => {
const getTags = (env = process.env) => {
if (env.GIT_SHA) {
console.log(`Using env vars for git tags: ${env.GIT_SHA} ${env.IS_TAGGED}`)
return {
shortSha: env.GIT_SHA,
isTagged: Boolean(env.IS_TAGGED)

View File

@@ -0,0 +1,43 @@
import { expect, test } from 'vitest';
// Preloading imports for faster tests
import '@app/common/allowed-origins';
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');
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
// Get allowed origins
expect(getAllowedOrigins()).toMatchInlineSnapshot(`
[
"/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",
]
`);
});

View File

@@ -16,34 +16,21 @@ vi.mock('@app/core/log', () => ({
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
addContext: vi.fn(),
removeContext: vi.fn(),
},
dashboardLogger: {
info: vi.fn(),
error: vi.fn((...input) => console.log(input)),
debug: vi.fn(),
trace: vi.fn(),
addContext: vi.fn(),
removeContext: vi.fn(),
},
emhttpLogger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
trace: vi.fn(),
addContext: vi.fn(),
removeContext: vi.fn(),
},
}));
vi.mock('@app/common/two-factor', () => ({
checkTwoFactorEnabled: vi.fn(() => ({
isRemoteEnabled: false,
isLocalEnabled: false,
})),
}));
vi.mock('@app/common/dashboard/boot-timestamp', () => ({
bootTimestamp: new Date('2022-06-10T04:35:58.276Z'),
}));
@@ -77,7 +64,7 @@ test('Returns generated data', async () => {
"case": {
"base64": "",
"error": "",
"icon": "case-model.png",
"icon": "",
"url": "",
},
},
@@ -107,6 +94,8 @@ test('Returns generated data', async () => {
"flashGuid": "0000-0000-0000-000000000000",
"regState": "PRO",
"regTy": "PRO",
"serverDescription": "Dev Server",
"serverName": "Tower",
},
"versions": {
"unraid": "6.11.2",

View File

@@ -1,360 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Returns default permissions 1`] = `
{
"admin": {
"extends": "user",
"permissions": [
{
"action": "read:any",
"attributes": "*",
"resource": "apikey",
},
{
"action": "read:any",
"attributes": "*",
"resource": "array",
},
{
"action": "read:any",
"attributes": "*",
"resource": "cpu",
},
{
"action": "read:any",
"attributes": "*",
"resource": "crash-reporting-enabled",
},
{
"action": "read:any",
"attributes": "*",
"resource": "device",
},
{
"action": "read:any",
"attributes": "*",
"resource": "device/unassigned",
},
{
"action": "read:any",
"attributes": "*",
"resource": "disk",
},
{
"action": "read:any",
"attributes": "*",
"resource": "disk/settings",
},
{
"action": "read:any",
"attributes": "*",
"resource": "display",
},
{
"action": "read:any",
"attributes": "*",
"resource": "docker/container",
},
{
"action": "read:any",
"attributes": "*",
"resource": "docker/network",
},
{
"action": "read:any",
"attributes": "*",
"resource": "flash",
},
{
"action": "read:any",
"attributes": "*",
"resource": "info",
},
{
"action": "read:any",
"attributes": "*",
"resource": "license-key",
},
{
"action": "read:any",
"attributes": "*",
"resource": "machine-id",
},
{
"action": "read:any",
"attributes": "*",
"resource": "memory",
},
{
"action": "read:any",
"attributes": "*",
"resource": "notifications",
},
{
"action": "read:any",
"attributes": "*",
"resource": "online",
},
{
"action": "read:any",
"attributes": "*",
"resource": "os",
},
{
"action": "read:any",
"attributes": "*",
"resource": "owner",
},
{
"action": "read:any",
"attributes": "*",
"resource": "parity-history",
},
{
"action": "read:any",
"attributes": "*",
"resource": "permission",
},
{
"action": "read:any",
"attributes": "*",
"resource": "registration",
},
{
"action": "read:any",
"attributes": "*",
"resource": "servers",
},
{
"action": "read:any",
"attributes": "*",
"resource": "service",
},
{
"action": "read:any",
"attributes": "*",
"resource": "service/emhttpd",
},
{
"action": "read:any",
"attributes": "*",
"resource": "service/unraid-api",
},
{
"action": "read:any",
"attributes": "*",
"resource": "services",
},
{
"action": "read:any",
"attributes": "*",
"resource": "share",
},
{
"action": "read:any",
"attributes": "*",
"resource": "software-versions",
},
{
"action": "read:any",
"attributes": "*",
"resource": "unraid-version",
},
{
"action": "read:any",
"attributes": "*",
"resource": "uptime",
},
{
"action": "read:any",
"attributes": "*",
"resource": "user",
},
{
"action": "read:any",
"attributes": "*",
"resource": "vars",
},
{
"action": "read:any",
"attributes": "*",
"resource": "vms",
},
{
"action": "read:any",
"attributes": "*",
"resource": "vms/domain",
},
{
"action": "read:any",
"attributes": "*",
"resource": "vms/network",
},
],
},
"guest": {
"permissions": [
{
"action": "read:any",
"attributes": "*",
"resource": "me",
},
{
"action": "read:any",
"attributes": "*",
"resource": "welcome",
},
],
},
"my_servers": {
"extends": "guest",
"permissions": [
{
"action": "read:any",
"attributes": "*",
"resource": "dashboard",
},
{
"action": "read:own",
"attributes": "*",
"resource": "two-factor",
},
{
"action": "read:any",
"attributes": "*",
"resource": "array",
},
{
"action": "read:any",
"attributes": "*",
"resource": "docker/container",
},
{
"action": "read:any",
"attributes": "*",
"resource": "docker/network",
},
{
"action": "read:any",
"attributes": "*",
"resource": "notifications",
},
{
"action": "read:any",
"attributes": "*",
"resource": "vms/domain",
},
{
"action": "read:any",
"attributes": "*",
"resource": "unraid-version",
},
],
},
"notifier": {
"extends": "guest",
"permissions": [
{
"action": "create:own",
"attributes": "*",
"resource": "notifications",
},
],
},
"upc": {
"extends": "guest",
"permissions": [
{
"action": "read:own",
"attributes": "*",
"resource": "apikey",
},
{
"action": "read:own",
"attributes": "*",
"resource": "cloud",
},
{
"action": "read:any",
"attributes": "*",
"resource": "config",
},
{
"action": "read:any",
"attributes": "*",
"resource": "crash-reporting-enabled",
},
{
"action": "read:any",
"attributes": "*",
"resource": "disk",
},
{
"action": "read:any",
"attributes": "*",
"resource": "display",
},
{
"action": "read:any",
"attributes": "*",
"resource": "flash",
},
{
"action": "read:any",
"attributes": "*",
"resource": "os",
},
{
"action": "read:any",
"attributes": "*",
"resource": "owner",
},
{
"action": "read:any",
"attributes": "*",
"resource": "permission",
},
{
"action": "read:any",
"attributes": "*",
"resource": "registration",
},
{
"action": "read:any",
"attributes": "*",
"resource": "servers",
},
{
"action": "read:any",
"attributes": "*",
"resource": "vars",
},
{
"action": "read:own",
"attributes": "*",
"resource": "connect",
},
{
"action": "update:own",
"attributes": "*",
"resource": "connect",
},
],
},
"user": {
"extends": "guest",
"permissions": [
{
"action": "read:own",
"attributes": "*",
"resource": "apikey",
},
{
"action": "read:any",
"attributes": "*",
"resource": "permission",
},
],
},
}
`;

View File

@@ -0,0 +1,403 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Returns default permissions 1`] = `
RolesBuilder {
"_grants": {
"admin": {
"$extend": [
"guest",
],
"apikey": {
"read:any": [
"*",
],
},
"array": {
"read:any": [
"*",
],
},
"cloud": {
"read:own": [
"*",
],
},
"config": {
"update:own": [
"*",
],
},
"connect": {
"read:own": [
"*",
],
"update:own": [
"*",
],
},
"cpu": {
"read:any": [
"*",
],
},
"crash-reporting-enabled": {
"read:any": [
"*",
],
},
"customizations": {
"read:any": [
"*",
],
},
"device": {
"read:any": [
"*",
],
},
"device/unassigned": {
"read:any": [
"*",
],
},
"disk": {
"read:any": [
"*",
],
},
"disk/settings": {
"read:any": [
"*",
],
},
"display": {
"read:any": [
"*",
],
},
"docker/container": {
"read:any": [
"*",
],
},
"docker/network": {
"read:any": [
"*",
],
},
"flash": {
"read:any": [
"*",
],
},
"info": {
"read:any": [
"*",
],
},
"license-key": {
"read:any": [
"*",
],
},
"logs": {
"read:any": [
"*",
],
},
"machine-id": {
"read:any": [
"*",
],
},
"memory": {
"read:any": [
"*",
],
},
"notifications": {
"create:any": [
"*",
],
"read:any": [
"*",
],
},
"online": {
"read:any": [
"*",
],
},
"os": {
"read:any": [
"*",
],
},
"owner": {
"read:any": [
"*",
],
},
"parity-history": {
"read:any": [
"*",
],
},
"permission": {
"read:any": [
"*",
],
},
"registration": {
"read:any": [
"*",
],
},
"servers": {
"read:any": [
"*",
],
},
"service": {
"read:any": [
"*",
],
},
"service/emhttpd": {
"read:any": [
"*",
],
},
"service/unraid-api": {
"read:any": [
"*",
],
},
"services": {
"read:any": [
"*",
],
},
"share": {
"read:any": [
"*",
],
},
"software-versions": {
"read:any": [
"*",
],
},
"unraid-version": {
"read:any": [
"*",
],
},
"uptime": {
"read:any": [
"*",
],
},
"user": {
"read:any": [
"*",
],
},
"vars": {
"read:any": [
"*",
],
},
"vms": {
"read:any": [
"*",
],
},
"vms/domain": {
"read:any": [
"*",
],
},
"vms/network": {
"read:any": [
"*",
],
},
},
"guest": {
"me": {
"read:any": [
"*",
],
},
"welcome": {
"read:any": [
"*",
],
},
},
"my_servers": {
"$extend": [
"guest",
],
"array": {
"read:any": [
"*",
],
},
"customizations": {
"read:any": [
"*",
],
},
"dashboard": {
"read:any": [
"*",
],
},
"docker/container": {
"read:any": [
"*",
],
},
"docker/network": {
"read:any": [
"*",
],
},
"logs": {
"read:any": [
"*",
],
},
"notifications": {
"read:any": [
"*",
],
},
"unraid-version": {
"read:any": [
"*",
],
},
"vms": {
"read:any": [
"*",
],
},
"vms/domain": {
"read:any": [
"*",
],
},
},
"notifier": {
"$extend": [
"guest",
],
"notifications": {
"create:own": [
"*",
],
},
},
"upc": {
"$extend": [
"guest",
],
"apikey": {
"read:own": [
"*",
],
},
"cloud": {
"read:own": [
"*",
],
},
"config": {
"read:any": [
"*",
],
"update:own": [
"*",
],
},
"connect": {
"read:own": [
"*",
],
"update:own": [
"*",
],
},
"crash-reporting-enabled": {
"read:any": [
"*",
],
},
"customizations": {
"read:any": [
"*",
],
},
"disk": {
"read:any": [
"*",
],
},
"display": {
"read:any": [
"*",
],
},
"flash": {
"read:any": [
"*",
],
},
"info": {
"read:any": [
"*",
],
},
"logs": {
"read:any": [
"*",
],
},
"os": {
"read:any": [
"*",
],
},
"owner": {
"read:any": [
"*",
],
},
"permission": {
"read:any": [
"*",
],
},
"registration": {
"read:any": [
"*",
],
},
"servers": {
"read:any": [
"*",
],
},
"vars": {
"read:any": [
"*",
],
},
},
},
"_isLocked": false,
}
`;

View File

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

View File

@@ -0,0 +1,167 @@
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(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});
test('it creates a MEMORY config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});
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(`
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"local": {
"2Fa": "yes",
"showT2Fa": "yes",
},
"notifier": {
"apikey": "",
},
"remote": {
"2Fa": "yes",
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});
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(`
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "Turned On",
},
"local": {
"2Fa": "yes",
"showT2Fa": "yes",
},
"notifier": {
"apikey": "",
},
"remote": {
"2Fa": "yes",
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});

View File

@@ -0,0 +1,39 @@
import { test, expect } from 'vitest';
import { parse } from 'ini';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { Serializer } from 'multi-ini';
test('MultiIni breaks when serializing an object with a boolean inside', async () => {
const objectToSerialize = {
root: {
anonMode: false,
},
};
const serializer = new Serializer({ keep_quotes: false });
expect(serializer.serialize(objectToSerialize)).toMatchInlineSnapshot(`
"[root]
anonMode=false
"
`)
});
test('MultiIni can safely serialize an object with a boolean inside', async () => {
const objectToSerialize = {
root: {
anonMode: false,
},
};
expect(safelySerializeObjectToIni(objectToSerialize)).toMatchInlineSnapshot(`
"[root]
anonMode="false"
"
`);
const result = safelySerializeObjectToIni(objectToSerialize);
expect(parse(result)).toMatchInlineSnapshot(`
{
"root": {
"anonMode": false,
},
}
`);
});

View File

@@ -0,0 +1,114 @@
import { test, expect } from 'vitest';
import { parseConfig } from '@app/core/utils/misc/parse-config';
import { Parser as MultiIniParser } from 'multi-ini';
import { readFile, writeFile } from 'fs/promises';
import { parse } from 'ini';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
const iniTestData = `["root"]
idx="0"
name="root"
desc="Console and webGui login account"
passwd="yes"
["xo"]
idx="1"
name="xo"
desc=""
passwd="yes"
["test_user"]
idx="2"
name="test_user"
desc=""
passwd="no"`;
test('it loads a config from a passed in ini file successfully', () => {
const res = parseConfig<any>({
file: iniTestData,
type: 'ini',
});
expect(res).toMatchInlineSnapshot(`
{
"root": {
"desc": "Console and webGui login account",
"idx": "0",
"name": "root",
"passwd": "yes",
},
"testUser": {
"desc": "",
"idx": "2",
"name": "test_user",
"passwd": "no",
},
"xo": {
"desc": "",
"idx": "1",
"name": "xo",
"passwd": "yes",
},
}
`);
expect(res?.root.desc).toEqual('Console and webGui login account');
});
test('it loads a config from disk properly', () => {
const path = './dev/states/var.ini';
const res = parseConfig<any>({ filePath: path, type: 'ini' });
expect(res.DOMAIN_SHORT).toEqual(undefined);
expect(res.domainShort).toEqual('');
expect(res.shareCount).toEqual('0');
});
test('Confirm Multi-Ini Parser Still Broken', () => {
const parser = new MultiIniParser();
const res = parser.parse(iniTestData);
expect(res).toMatchInlineSnapshot('{}');
});
test('Combine Ini and Multi-Ini to read and then write a file with quotes', async () => {
const parsedFile = parse(iniTestData);
expect(parsedFile).toMatchInlineSnapshot(`
{
"root": {
"desc": "Console and webGui login account",
"idx": "0",
"name": "root",
"passwd": "yes",
},
"test_user": {
"desc": "",
"idx": "2",
"name": "test_user",
"passwd": "no",
},
"xo": {
"desc": "",
"idx": "1",
"name": "xo",
"passwd": "yes",
},
}
`);
const ini = safelySerializeObjectToIni(parsedFile);
await writeFile('/tmp/test.ini', ini);
const file = await readFile('/tmp/test.ini', 'utf-8');
expect(file).toMatchInlineSnapshot(`
"[root]
idx="0"
name="root"
desc="Console and webGui login account"
passwd="yes"
[xo]
idx="1"
name="xo"
desc=""
passwd="yes"
[test_user]
idx="2"
name="test_user"
desc=""
passwd="no"
"
`);
});

View File

@@ -0,0 +1,9 @@
import { checkMothershipAuthentication } from "@app/graphql/resolvers/query/cloud/check-mothership-authentication";
import { expect, test } from "vitest";
import packageJson from '@app/../package.json'
test('It fails to authenticate with mothership with no credentials', async () => {
await expect(checkMothershipAuthentication('BAD', 'BAD')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`);
expect(packageJson.version).not.toBeNull();
await expect(checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
}, 15_000)

View File

@@ -0,0 +1,188 @@
import { expect, test } from 'vitest';
import { type Nginx } from '../../../../core/types/states/nginx';
import { getUrlForField, getUrlForServer, getServerIps, type NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
import { store } from '@app/store';
import { loadStateFiles } from '@app/store/modules/emhttp';
import { loadConfigFile } from '@app/store/modules/config';
test.each([
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
])('getUrlForField', ({ httpPort, httpsPort, url }) => {
const responseInsecure = getUrlForField({
port: httpPort,
url,
});
const responseSecure = getUrlForField({
portSsl: httpsPort,
url,
});
if (httpPort === 80) {
expect(responseInsecure.port).toBe('');
} else {
expect(responseInsecure.port).toBe(httpPort.toString());
}
if (httpsPort === 443) {
expect(responseSecure.port).toBe('');
} else {
expect(responseSecure.port).toBe(httpsPort.toString());
}
});
test('getUrlForServer - field exists, ssl disabled', () => {
const result = getUrlForServer({ nginx: { lanIp: '192.168.1.1', sslEnabled: false, httpPort: 123, httpsPort: 445 } as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
});
test('getUrlForServer - field exists, ssl yes', () => {
const result = getUrlForServer({
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 123, httpsPort: 445 } as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
});
test('getUrlForServer - field exists, ssl yes, port empty', () => {
const result = getUrlForServer(
{ nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
});
test('getUrlForServer - field exists, ssl auto', () => {
const getResult = async () => getUrlForServer({
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'auto', httpPort: 123, httpsPort: 445 } as const as Nginx,
field: 'lanIp',
});
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`);
});
test('getUrlForServer - field does not exist, ssl disabled', () => {
const getResult = async () => getUrlForServer(
{
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
ports: {
port: ':123', portSsl: ':445', defaultUrl: new URL('https://my-default-url.unraid.net'),
},
// @ts-expect-error Field doesn't exist
field: 'idontexist',
});
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
});
test('getUrlForServer - FQDN - field exists, port non-empty', () => {
const result = getUrlForServer({
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
field: 'lanFqdn',
});
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
});
test('getUrlForServer - FQDN - field exists, port empty', () => {
const result = getUrlForServer({ nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
field: 'lanFqdn',
});
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
});
test.each([
[{ nginx: { lanFqdn: 'my-fqdn.unraid.net', sslEnabled: false, sslMode: 'no', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'lanFqdn' as NginxUrlFields }],
[{ nginx: { wanFqdn: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn' as NginxUrlFields }],
[{ nginx: { wanFqdn6: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'auto', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn6' as NginxUrlFields }],
])('getUrlForServer - FQDN', ({ nginx, field }) => {
const result = getUrlForServer({ nginx, field });
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
});
test('getUrlForServer - field does not exist, ssl disabled', () => {
const getResult = async () => getUrlForServer({ nginx:
{ lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
// @ts-expect-error Field doesn't exist
field: 'idontexist' });
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
});
test('integration test, loading nginx ini and generating all URLs', async () => {
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
const urls = getServerIps();
expect(urls.urls).toMatchInlineSnapshot(`
[
{
"ipv4": "https://tower.local:4443/",
"ipv6": "https://tower.local:4443/",
"name": "Default",
"type": "DEFAULT",
},
{
"ipv4": "https://192.168.1.150:4443/",
"name": "LAN IPv4",
"type": "LAN",
},
{
"ipv4": "https://tower:4443/",
"name": "LAN Name",
"type": "MDNS",
},
{
"ipv4": "https://tower.local:4443/",
"name": "LAN MDNS",
"type": "MDNS",
},
{
"ipv4": "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443/",
"name": "LAN FQDN",
"type": "LAN",
},
{
"ipv4": "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443/",
"name": "WAN FQDN",
"type": "WAN",
},
{
"ipv4": "https://10-252-0-1.hash.myunraid.net:4443/",
"name": "WG FQDN 0",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-252-1-1.hash.myunraid.net:4443/",
"name": "WG FQDN 1",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-253-3-1.hash.myunraid.net:4443/",
"name": "WG FQDN 3",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-253-4-1.hash.myunraid.net:4443/",
"name": "WG FQDN 4",
"type": "WIREGUARD",
},
{
"ipv4": "https://10-253-5-1.hash.myunraid.net:4443/",
"name": "WG FQDN 55",
"type": "WIREGUARD",
},
]
`);
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

@@ -0,0 +1,111 @@
import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types';
import * as apiKeyCheckJobs from '@app/mothership/jobs/api-key-check-jobs';
import * as apiKeyValidator from '@app/mothership/api-key/validate-api-key-with-keyserver';
import { describe, expect, it, vi } from 'vitest';
import { type RecursivePartial } from '@app/types/index';
import { type RootState } from '@app/store/index';
import { logoutUser } from '@app/store/modules/config';
describe('apiKeyCheckJob Tests', () => {
it('API Check Job (with success)', async () => {
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
});
const dispatch = vi.fn();
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID);
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(true);
expect(validationSpy).toHaveBeenCalledOnce();
expect(dispatch).toHaveBeenLastCalledWith({
payload: API_KEY_STATUS.API_KEY_VALID,
type: 'apiKey/setApiKeyState',
});
});
it('API Check Job (with invalid length key)', async () => {
// Setup state
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
config: { remote: { apikey: 'too-short-key' } },
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
});
const dispatch = vi.fn();
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID);
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false);
expect(dispatch).toHaveBeenCalledWith(expect.any(Function));
expect(validationSpy).not.toHaveBeenCalled();
});
it('API Check Job (with a failure that throws an error - NETWORK_ERROR)', async () => {
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
});
const dispatch = vi.fn();
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
.mockResolvedValueOnce(API_KEY_STATUS.NETWORK_ERROR);
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`);
expect(validationSpy).toHaveBeenCalledOnce();
expect(dispatch).toHaveBeenCalledWith({
payload: API_KEY_STATUS.NETWORK_ERROR,
type: 'apiKey/setApiKeyState',
});
});
it('API Check Job (with a failure that throws an error - INVALID_RESPONSE)', async () => {
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
});
const dispatch = vi.fn();
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
.mockResolvedValueOnce(API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE);
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`);
expect(validationSpy).toHaveBeenCalledOnce();
expect(dispatch).toHaveBeenCalledWith({
payload: API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE,
type: 'apiKey/setApiKeyState',
});
}, 10_000);
it('API Check Job (with failure that results in a log out)', async () => {
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
});
const dispatch = vi.fn();
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
.mockResolvedValue(API_KEY_STATUS.API_KEY_INVALID);
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false);
expect(validationSpy).toHaveBeenCalledOnce();
expect(dispatch).toHaveBeenCalledTimes(1);
expect(dispatch).toHaveBeenCalledWith(expect.any(Function));
}, 10_000);
});

View File

@@ -0,0 +1,48 @@
import { beforeEach, expect, test, vi } from 'vitest';
// Preloading imports for faster tests
import '@app/mothership/utils/convert-to-fuzzy-time';
vi.mock('fs', () => ({
default: {
readFileSync: vi.fn().mockReturnValue('my-file'),
writeFileSync: vi.fn(),
existsSync: vi.fn(),
},
readFileSync: vi.fn().mockReturnValue('my-file'),
existsSync: vi.fn(),
}));
vi.mock('@graphql-tools/schema', () => ({
makeExecutableSchema: vi.fn(),
}));
vi.mock('@app/core/log', () => ({
default: { relayLogger: { trace: vi.fn() } },
relayLogger: { trace: vi.fn() },
logger: { trace: vi.fn() },
}));
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
const generateTestCases = () => {
const cases: Array<{ min: number; max: number }> = [];
for (let i = 0; i < 15; i += 1) {
const min = Math.round(Math.random() * 100);
const max = min + (Math.round(Math.random() * 20));
cases.push({ min, max });
}
return cases;
};
test.each(generateTestCases())('Successfully converts to fuzzy time %o', async ({ min, max }) => {
const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time');
const res = convertToFuzzyTime(min, max);
expect(res).toBeGreaterThanOrEqual(min);
expect(res).toBeLessThanOrEqual(max);
});

View File

@@ -56,7 +56,7 @@ test('After init returns values from cfg file for all fields', async () => {
expect(state).toMatchObject(
expect.objectContaining({
api: {
extraOrigins: '',
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
version: expect.any(String),
},
connectionStatus: {
@@ -114,7 +114,7 @@ test('updateUserConfig merges in changes to current state', async () => {
expect(state).toMatchObject(
expect.objectContaining({
api: {
extraOrigins: '',
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
version: expect.any(String),
},
connectionStatus: {

View File

@@ -984,6 +984,7 @@ test('After init returns values from cfg file for all fields', async () => {
"porttelnet": 23,
"queueDepth": "auto",
"regCheck": "Valid",
"regExp": "",
"regFile": "/app/dev/Unraid.net/Pro.key",
"regGen": "0",
"regGuid": "13FE-4200-C300-58C372A52B19",

View File

@@ -103,6 +103,7 @@ test('Returns parsed state file', async () => {
"porttelnet": 23,
"queueDepth": "auto",
"regCheck": "Valid",
"regExp": "",
"regFile": "/app/dev/Unraid.net/Pro.key",
"regGen": "0",
"regGuid": "13FE-4200-C300-58C372A52B19",

View File

@@ -75,15 +75,10 @@ export const getCloudData = async (
const cloud = await client.query({ query: getCloudDocument });
return cloud.data.cloud ?? null;
} catch (error: unknown) {
cliLogger.addContext(
'error-stack',
error instanceof Error ? error.stack : error
);
cliLogger.trace(
'Failed fetching cloud from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
cliLogger.removeContext('error-stack');
return null;
}
@@ -122,12 +117,10 @@ export const getServersData = async ({
);
return foundServers;
} catch (error: unknown) {
cliLogger.addContext('error', error);
cliLogger.trace(
'Failed fetching servers from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
cliLogger.removeContext('error');
return {
online: [],
offline: [],

View File

@@ -0,0 +1,10 @@
import { start } from '@app/cli/commands/start';
import { stop } from '@app/cli/commands/stop';
/**
* Stop a running API process and then start it again.
*/
export const restart = async () => {
await stop();
await start();
};

View File

@@ -12,6 +12,7 @@ import { API_VERSION } from '@app/environment';
*/
export const start = async () => {
// Set process title
process.title = 'unraid-api';
const runningProcesses = await getAllUnraidApiPids();
if (runningProcesses.length > 0) {

View File

@@ -1,6 +1,5 @@
import { cliLogger } from '@app/core/log';
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
import { setEnv } from '@app/cli/set-env';
import { sleep } from '@app/core/utils/misc/sleep';
import pRetry from 'p-retry';
@@ -9,8 +8,6 @@ import pRetry from 'p-retry';
*/
export const stop = async () => {
setEnv('LOG_TYPE', 'raw');
try {
await pRetry(async (attempts) => {
const runningApis = await getAllUnraidApiPids();
@@ -18,7 +15,7 @@ export const stop = async () => {
if (runningApis.length > 0) {
cliLogger.info('Stopping %s unraid-api process(es)...', runningApis.length);
runningApis.forEach(pid => process.kill(pid, 'SIGTERM'));
await sleep(50);
const newPids = await getAllUnraidApiPids();
if (newPids.length > 0) {
@@ -31,7 +28,7 @@ export const stop = async () => {
return true;
}, {
retries: 2,
minTimeout: 2_000,
minTimeout: 1_000,
factor: 1,
});
} catch (error: unknown) {

View File

@@ -8,65 +8,73 @@ import { getters } from '@app/store';
const command = mainOptions.command as unknown as string;
export const main = async (...argv: string[]) => {
cliLogger.addContext('envs', env);
cliLogger.debug('Loading env file');
cliLogger.removeContext('envs');
cliLogger.debug(env, 'Loading env file');
// Set envs
setEnv('LOG_TYPE', process.env.LOG_TYPE ?? (command === 'start' || mainOptions.debug ? 'pretty' : 'raw'));
cliLogger.addContext('paths', getters.paths());
cliLogger.debug('Starting CLI');
cliLogger.removeContext('paths');
// Set envs
setEnv('LOG_TYPE', 'pretty');
cliLogger.debug({ paths: getters.paths() }, 'Starting CLI');
setEnv('DEBUG', mainOptions.debug ?? false);
setEnv('ENVIRONMENT', process.env.ENVIRONMENT ?? 'production');
setEnv('PORT', process.env.PORT ?? mainOptions.port ?? '9000');
setEnv('LOG_LEVEL', process.env.LOG_LEVEL ?? mainOptions['log-level'] ?? 'INFO');
if (!process.env.LOG_TRANSPORT) {
if (process.env.ENVIRONMENT === 'production' && !mainOptions.debug) {
setEnv('LOG_TRANSPORT', 'file,errors');
setEnv('LOG_LEVEL', 'DEBUG');
} else if (!mainOptions.debug) {
// Staging Environment, backgrounded plugin
setEnv('LOG_TRANSPORT', 'file,errors');
setEnv('LOG_LEVEL', 'TRACE');
} else {
cliLogger.debug('In Debug Mode - Log Level Defaulting to: stdout');
}
}
setEnv('DEBUG', mainOptions.debug ?? false);
setEnv('ENVIRONMENT', process.env.ENVIRONMENT ?? 'production');
setEnv('PORT', process.env.PORT ?? mainOptions.port ?? '9000');
setEnv(
'LOG_LEVEL',
process.env.LOG_LEVEL ?? mainOptions['log-level'] ?? 'INFO'
);
if (!process.env.LOG_TRANSPORT) {
if (process.env.ENVIRONMENT === 'production' && !mainOptions.debug) {
setEnv('LOG_TRANSPORT', 'file');
setEnv('LOG_LEVEL', 'INFO');
} else if (!mainOptions.debug) {
// Staging Environment, backgrounded plugin
setEnv('LOG_TRANSPORT', 'file');
setEnv('LOG_LEVEL', 'TRACE');
} else {
cliLogger.debug('In Debug Mode - Log Level Defaulting to: stdout');
}
}
if (!command) {
// Run help command
parse<Flags>(args, { ...options, partial: true, stopAtFirstUnknown: true, argv: ['-h'] });
}
if (!command) {
// Run help command
parse<Flags>(args, {
...options,
partial: true,
stopAtFirstUnknown: true,
argv: ['-h'],
});
}
// Only import the command we need when we use it
const commands = {
start: import('@app/cli/commands/start').then(pkg => pkg.start),
stop: import('@app/cli/commands/stop').then(pkg => pkg.stop),
restart: import('@app/cli/commands/restart').then(pkg => pkg.restart),
'switch-env': import('@app/cli/commands/switch-env').then(pkg => pkg.switchEnv),
version: import('@app/cli/commands/version').then(pkg => pkg.version),
status: import('@app/cli/commands/status').then(pkg => pkg.status),
report: import('@app/cli/commands/report').then(pkg => pkg.report),
'validate-token': import('@app/cli/commands/validate-token').then(pkg => pkg.validateToken),
};
// Only import the command we need when we use it
const commands = {
start: import('@app/cli/commands/start').then((pkg) => pkg.start),
stop: import('@app/cli/commands/stop').then((pkg) => pkg.stop),
restart: import('@app/cli/commands/restart').then((pkg) => pkg.restart),
'switch-env': import('@app/cli/commands/switch-env').then(
(pkg) => pkg.switchEnv
),
version: import('@app/cli/commands/version').then((pkg) => pkg.version),
status: import('@app/cli/commands/status').then((pkg) => pkg.status),
report: import('@app/cli/commands/report').then((pkg) => pkg.report),
'validate-token': import('@app/cli/commands/validate-token').then(
(pkg) => pkg.validateToken
),
};
// Unknown command
if (!Object.keys(commands).includes(command)) {
throw new Error(`Invalid command "${command}"`);
}
// Unknown command
if (!Object.keys(commands).includes(command)) {
throw new Error(`Invalid command "${command}"`);
}
// Resolve the command import
const commandMethod = await commands[command];
// Resolve the command import
const commandMethod = await commands[command];
// Run the command
await commandMethod(...argv);
// Run the command
await commandMethod(...argv);
// Allow the process to exit
// Don't exit when we start though
if (!['start', 'restart'].includes(command)) {
// Ensure process is exited
process.exit(0);
}
// Allow the process to exit
// Don't exit when we start though
if (!['start', 'restart'].includes(command)) {
// Ensure process is exited
process.exit(0);
}
};

View File

@@ -1,93 +1,112 @@
import { getters, type RootState, store } from '@app/store';
import { uniq } from 'lodash';
import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network';
import {
getServerIps,
getUrlForField,
} from '@app/graphql/resolvers/subscription/network';
import { FileLoadStatus } from '@app/store/types';
import { logger } from '../core';
import { ENVIRONMENT, INTROSPECTION } from '@app/environment';
import { GRAPHQL_INTROSPECTION } from '@app/environment';
const getAllowedSocks = (): string[] => [
// Notifier bridge
'/var/run/unraid-notifications.sock',
// Notifier bridge
'/var/run/unraid-notifications.sock',
// Unraid PHP scripts
'/var/run/unraid-php.sock',
// Unraid PHP scripts
'/var/run/unraid-php.sock',
// CLI
'/var/run/unraid-cli.sock',
// CLI
'/var/run/unraid-cli.sock',
];
const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
const { emhttp } = state;
if (emhttp.status !== FileLoadStatus.LOADED) {
return [];
}
const getLocalAccessUrlsForServer = (
state: RootState = store.getState()
): string[] => {
const { emhttp } = state;
if (emhttp.status !== FileLoadStatus.LOADED) {
return [];
}
const { nginx } = emhttp;
try {
return [
getUrlForField({ url: 'localhost', port: nginx.httpPort }).toString(),
getUrlForField({ url: 'localhost', portSsl: nginx.httpsPort }).toString(),
];
} catch (error: unknown) {
logger.debug('Caught error in getLocalAccessUrlsForServer: \n%o', error);
return [];
}
const { nginx } = emhttp;
try {
return [
getUrlForField({
url: 'localhost',
port: nginx.httpPort,
}).toString(),
getUrlForField({
url: 'localhost',
portSsl: nginx.httpsPort,
}).toString(),
];
} catch (error: unknown) {
logger.debug(
'Caught error in getLocalAccessUrlsForServer: \n%o',
error
);
return [];
}
};
const getRemoteAccessUrlsForAllowedOrigins = (state: RootState = store.getState()): string[] => {
const { urls } = getServerIps(state);
const getRemoteAccessUrlsForAllowedOrigins = (
state: RootState = store.getState()
): string[] => {
const { urls } = getServerIps(state);
if (urls) {
return urls.reduce<string[]>((acc, curr) => {
if (curr.ipv4 && curr.ipv6) {
acc.push(curr.ipv4.toString());
} else if (curr.ipv4) {
acc.push(curr.ipv4.toString());
} else if (curr.ipv6) {
acc.push(curr.ipv6.toString());
}
if (urls) {
return urls.reduce<string[]>((acc, curr) => {
if (curr.ipv4 && curr.ipv6 || curr.ipv4) {
acc.push(curr.ipv4.toString());
} else if (curr.ipv6) {
acc.push(curr.ipv6.toString());
}
return acc;
}, []);
}
return acc;
}, []);
}
return [];
return [];
};
const getExtraOrigins = (): string[] => {
const { extraOrigins } = getters.config().api;
if (extraOrigins) {
return extraOrigins.split(', ').filter(origin => origin.startsWith('http://') || origin.startsWith('https://'));
}
const { extraOrigins } = getters.config().api;
if (extraOrigins) {
return extraOrigins
.replaceAll(' ', '')
.split(',')
.filter(
(origin) =>
origin.startsWith('http://') ||
origin.startsWith('https://')
);
}
return [];
return [];
};
const getConnectOrigins = () : string[] => {
const connectMain = 'https://connect.myunraid.net';
const connectStaging = 'https://staging.connect.myunraid.net';
const connectDev = 'https://dev-my.myunraid.net:4000';
const getConnectOrigins = (): string[] => {
const connectMain = 'https://connect.myunraid.net';
const connectStaging = 'https://connect-staging.myunraid.net';
const connectDev = 'https://dev-my.myunraid.net:4000';
return [
connectMain,
connectStaging,
connectDev
]
}
return [connectMain, connectStaging, connectDev];
};
const getApolloSandbox = (): string[] => {
if (INTROSPECTION || ENVIRONMENT === 'development') {
return ['https://studio.apollographql.com'];
}
return [];
}
if (GRAPHQL_INTROSPECTION) {
return ['https://studio.apollographql.com'];
}
return [];
};
export const getAllowedOrigins = (state: RootState = store.getState()): string[] => uniq([
...getAllowedSocks(),
...getLocalAccessUrlsForServer(),
...getRemoteAccessUrlsForAllowedOrigins(state),
...getExtraOrigins(),
...getConnectOrigins(),
...getApolloSandbox()
]).map(url => url.endsWith('/') ? url.slice(0, -1) : url);
export const getAllowedOrigins = (
state: RootState = store.getState()
): string[] =>
uniq([
...getAllowedSocks(),
...getLocalAccessUrlsForServer(),
...getRemoteAccessUrlsForAllowedOrigins(state),
...getExtraOrigins(),
...getConnectOrigins(),
...getApolloSandbox(),
]).map((url) => (url.endsWith('/') ? url.slice(0, -1) : url));

View File

@@ -1,58 +1,64 @@
import { ConnectListAllDomainsFlags } from '@vmngr/libvirt';
import { getHypervisor } from '@app/core/utils/vms/get-hypervisor';
import display from '@app/graphql/resolvers/query/display';
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 {
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,
};
}
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 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;
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(),
},
};
return {
name: 'dynamic-remote-access',
online: enabledStatus !== DynamicRemoteAccessType.DISABLED,
version: dynamicRemoteAccess.runningType,
uptime: {
timestamp: bootTimestamp.toISOString(),
},
};
};
const services = (): DashboardInput['services'] => {
const dynamicRemoteAccess = getDynamicRemoteAccessService();
return [
const dynamicRemoteAccess = getDynamicRemoteAccessService();
return [
{
name: 'unraid-api',
online: true,
@@ -66,61 +72,81 @@ const services = (): DashboardInput['services'] => {
};
const getData = async (): Promise<DashboardInput> => {
const emhttp = getters.emhttp();
const docker = getters.docker();
const emhttp = getters.emhttp();
const docker = getters.docker();
return {
vars: {
regState: emhttp.var.regState,
regTy: emhttp.var.regTy,
flashGuid: emhttp.var.flashGuid,
},
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: await display(),
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',
},
};
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();
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)
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
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
);
}
}
} 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;
return null;
};

View File

@@ -1,122 +0,0 @@
export interface Permission { resource: string, action: string, attributes: string }
export interface Role {
permissions: Array<Permission>
extends?: string;
}
export const admin: Role = {
extends: 'user',
permissions: [
// @NOTE: Uncomment the first line to enable creation of api keys.
// See the README.md for more information.
// @WARNING: This is currently unsupported, please be careful.
// { resource: 'apikey', action: 'create:any', attributes: '*' },
{ resource: 'apikey', action: 'read:any', attributes: '*' },
{ resource: 'array', action: 'read:any', attributes: '*' },
{ resource: 'cpu', action: 'read:any', attributes: '*' },
{
resource: 'crash-reporting-enabled',
action: 'read:any',
attributes: '*',
},
{ resource: 'device', action: 'read:any', attributes: '*' },
{ resource: 'device/unassigned', action: 'read:any', attributes: '*' },
{ resource: 'disk', action: 'read:any', attributes: '*' },
{ resource: 'disk/settings', action: 'read:any', attributes: '*' },
{ resource: 'display', action: 'read:any', attributes: '*' },
{ resource: 'docker/container', action: 'read:any', attributes: '*' },
{ resource: 'docker/network', action: 'read:any', attributes: '*' },
{ resource: 'flash', action: 'read:any', attributes: '*' },
{ resource: 'info', action: 'read:any', attributes: '*' },
{ resource: 'license-key', action: 'read:any', attributes: '*' },
{ resource: 'machine-id', action: 'read:any', attributes: '*' },
{ resource: 'memory', action: 'read:any', attributes: '*' },
{ resource: 'notifications', action: 'read:any', attributes: '*' },
{ resource: 'online', action: 'read:any', attributes: '*' },
{ resource: 'os', action: 'read:any', attributes: '*' },
{ resource: 'owner', action: 'read:any', attributes: '*' },
{ resource: 'parity-history', action: 'read:any', attributes: '*' },
{ resource: 'permission', action: 'read:any', attributes: '*' },
{ resource: 'registration', action: 'read:any', attributes: '*' },
{ resource: 'servers', action: 'read:any', attributes: '*' },
{ resource: 'service', action: 'read:any', attributes: '*' },
{ resource: 'service/emhttpd', action: 'read:any', attributes: '*' },
{ resource: 'service/unraid-api', action: 'read:any', attributes: '*' },
{ resource: 'services', action: 'read:any', attributes: '*' },
{ resource: 'share', action: 'read:any', attributes: '*' },
{ resource: 'software-versions', action: 'read:any', attributes: '*' },
{ resource: 'unraid-version', action: 'read:any', attributes: '*' },
{ resource: 'uptime', action: 'read:any', attributes: '*' },
{ resource: 'user', action: 'read:any', attributes: '*' },
{ resource: 'vars', action: 'read:any', attributes: '*' },
{ resource: 'vms', action: 'read:any', attributes: '*' },
{ resource: 'vms/domain', action: 'read:any', attributes: '*' },
{ resource: 'vms/network', action: 'read:any', attributes: '*' },
],
};
export const user: Role = {
extends: 'guest',
permissions: [
{ resource: 'apikey', action: 'read:own', attributes: '*' },
{ resource: 'permission', action: 'read:any', attributes: '*' },
],
};
export const upc: Role = {
extends: 'guest',
permissions: [
{ resource: 'apikey', action: 'read:own', attributes: '*' },
{ resource: 'cloud', action: 'read:own', attributes: '*' },
{ resource: 'config', action: 'read:any', attributes: '*' },
{ resource: 'crash-reporting-enabled', action: 'read:any', attributes: '*' },
{ resource: 'disk', action: 'read:any', attributes: '*' },
{ resource: 'display', action: 'read:any', attributes: '*' },
{ resource: 'flash', action: 'read:any', attributes: '*' },
{ resource: 'os', action: 'read:any', attributes: '*' },
{ resource: 'owner', action: 'read:any', attributes: '*' },
{ resource: 'permission', action: 'read:any', attributes: '*' },
{ resource: 'registration', action: 'read:any', attributes: '*' },
{ resource: 'servers', action: 'read:any', attributes: '*' },
{ resource: 'vars', action: 'read:any', attributes: '*' },
{ resource: 'connect', action: 'read:own', attributes: '*' },
{ resource: 'connect', action: 'update:own', attributes: '*' }
],
};
export const my_servers: Role = {
extends: 'guest',
permissions: [
{ resource: 'dashboard', action: 'read:any', attributes: '*' },
{ resource: 'two-factor', action: 'read:own', attributes: '*' },
{ resource: 'array', action: 'read:any', attributes: '*' },
{ resource: 'docker/container', action: 'read:any', attributes: '*' },
{ resource: 'docker/network', action: 'read:any', attributes: '*' },
{ resource: 'notifications', action: 'read:any', attributes: '*' },
{ resource: 'vms/domain', action: 'read:any', attributes: '*' },
{ resource: 'unraid-version', action: 'read:any', attributes: '*' },
],
};
export const notifier: Role = {
extends: 'guest',
permissions: [
{ resource: 'notifications', action: 'create:own', attributes: '*' },
],
};
export const guest: Role = {
permissions: [
{ resource: 'me', action: 'read:any', attributes: '*' },
{ resource: 'welcome', action: 'read:any', attributes: '*' },
],
};
export const permissions: Record<string, Role> = {
guest,
user,
admin,
upc,
my_servers,
notifier,
};

View File

@@ -1,137 +1,141 @@
import chalk from 'chalk';
import { configure, getLogger } from 'log4js';
import { serializeError } from 'serialize-error';
import { pino } from 'pino';
import { LOG_TRANSPORT, LOG_TYPE } from '@app/environment';
export const levels = ['ALL', 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'MARK', 'OFF'] as const;
import pretty from 'pino-pretty';
import { chmodSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
import { getters } from '@app/store/index';
import { join } from 'node:path';
const contextEnabled = Boolean(process.env.LOG_CONTEXT);
const stackEnabled = Boolean(process.env.LOG_STACKTRACE);
const tracingEnabled = Boolean(process.env.LOG_TRACING);
const fullLoggingPattern = chalk`{gray [%d]} %x\{id\} %[[%p]%] %[[%c]%] %m{gray %x\{context\}}${tracingEnabled ? ' %[%f:%l%]' : ''}`;
const minimumLoggingPattern = '%m';
const appenders = process.env.LOG_TRANSPORT?.split(',').map(transport => transport.trim()) ?? ['out'];
const level = levels[levels.indexOf(process.env.LOG_LEVEL?.toUpperCase() as typeof levels[number])] ?? 'INFO';
const logLayout = {
type: 'pattern',
// Depending on what this env is set to we'll either get raw or pretty logs
// The reason we do this is to allow the app to change this value
// This way pretty logs can be turned off programmatically
pattern: process.env.LOG_TYPE === 'pretty' ? fullLoggingPattern : minimumLoggingPattern,
tokens: {
id() {
return chalk`{gray [${process.pid}]}`;
},
context({ context }: { context?: any }) {
if (!contextEnabled || !context) {
return '';
}
const makeLoggingDirectoryIfNotExists = () => {
if (!existsSync(getters.paths()['log-base'])) {
console.log('Creating logging directory');
mkdirSync(getters.paths()['log-base']);
}
try {
const contextEntries = Object.entries(context)
.map(([key, value]) => [key, value instanceof Error ? (stackEnabled ? serializeError(value) : value) : value])
.filter(([key]) => key !== 'pid');
const cleanContext = Object.fromEntries(contextEntries);
return `\n${Object.entries(cleanContext).map(([key, value]) => `${key}=${JSON.stringify(value, null, 2)}`).join(' ')}`;
} catch (error: unknown) {
const errorInfo = error instanceof Error ? `${error.message}: ${error.stack ?? 'no stack'}` : 'Error not instance of error';
return `Error generating context: ${errorInfo}`;
}
},
},
chmodSync(getters.paths()['log-base'], 0o644);
if (
existsSync(`${getters.paths()['log-base']}/stdout.log`) &&
statSync(`${getters.paths()['log-base']}/stdout.log`).size > 5_000_000
) {
rmSync(`${getters.paths()['log-base']}/stdout.log`);
}
try {
rmSync(`${getters.paths()['log-base']}/stdout.log.*`);
} catch (e) {
// Ignore Error
}
};
if (process.env.NODE_ENV !== 'test') {
// We log to both the stdout and log file
// The log file should be changed to errors only unless in debug mode
configure({
appenders: {
file: {
type: 'file',
filename: '/var/log/unraid-api/stdout.log',
maxLogSize: 10_000_000,
backups: 0,
layout: {
...logLayout,
// File logs should always be pretty
pattern: fullLoggingPattern,
},
},
errorFile: {
type: 'file',
filename: '/var/log/unraid-api/stderr.log',
maxLogSize: 2_500_000,
backups: 0,
layout: {
...logLayout,
// File logs should always be pretty
pattern: fullLoggingPattern,
},
},
out: {
type: 'stdout',
layout: logLayout,
},
errors: { type: 'logLevelFilter', appender: 'errorFile', level: 'error' },
},
categories: {
default: {
appenders,
level,
enableCallStack: tracingEnabled,
},
},
});
if (LOG_TRANSPORT === 'file') {
makeLoggingDirectoryIfNotExists();
}
export const internalLogger = getLogger('internal');
export const logger = getLogger('app');
export const mothershipLogger = getLogger('mothership');
export const dashboardLogger = getLogger('dashboard');
export const emhttpLogger = getLogger('emhttp');
export const libvirtLogger = getLogger('libvirt');
export const graphqlLogger = getLogger('graphql');
export const dockerLogger = getLogger('docker');
export const cliLogger = getLogger('cli');
export const minigraphLogger = getLogger('minigraph');
export const cloudConnectorLogger = getLogger('cloud-connector');
export const upnpLogger = getLogger('upnp');
export const keyServerLogger = getLogger('key-server');
export const remoteAccessLogger = getLogger('remote-access');
export const remoteQueryLogger = getLogger('remote-query');
export const levels = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
] as const;
const level =
levels[
levels.indexOf(
process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number]
)
] ?? 'info';
export const logDestination = pino.destination({
dest:
LOG_TRANSPORT === 'file'
? join(getters.paths()['log-base'], 'stdout.log')
: 1,
minLength: 1_024,
sync: false,
});
const stream =
LOG_TYPE === 'pretty'
? pretty({
singleLine: true,
hideObject: false,
colorize: true,
ignore: 'time,hostname,pid',
destination: logDestination,
})
: logDestination;
export const logger = pino(
{
level,
timestamp: () => `,"time":"${new Date().toISOString()}"`,
formatters: {
level: (label: string) => ({ level: label }),
},
},
stream
);
export const internalLogger = logger.child({ logger: 'internal' });
export const appLogger = logger.child({ logger: 'app' });
export const mothershipLogger = logger.child({ logger: 'mothership' });
export const dashboardLogger = logger.child({ logger: 'dashboard' });
export const emhttpLogger = logger.child({ logger: 'emhttp' });
export const libvirtLogger = logger.child({ logger: 'libvirt' });
export const graphqlLogger = logger.child({ logger: 'graphql' });
export const dockerLogger = logger.child({ logger: 'docker' });
export const cliLogger = logger.child({ logger: 'cli' });
export const minigraphLogger = logger.child({ logger: 'minigraph' });
export const cloudConnectorLogger = logger.child({ logger: 'cloud-connector' });
export const upnpLogger = logger.child({ logger: 'upnp' });
export const keyServerLogger = logger.child({ logger: 'key-server' });
export const remoteAccessLogger = logger.child({ logger: 'remote-access' });
export const remoteQueryLogger = logger.child({ logger: 'remote-query' });
export const apiLogger = logger.child({ logger: 'api' });
export const loggers = [
logger,
mothershipLogger,
dashboardLogger,
emhttpLogger,
libvirtLogger,
graphqlLogger,
dockerLogger,
cliLogger,
minigraphLogger,
cloudConnectorLogger,
upnpLogger,
keyServerLogger,
remoteAccessLogger,
remoteQueryLogger,
internalLogger,
appLogger,
mothershipLogger,
dashboardLogger,
emhttpLogger,
libvirtLogger,
graphqlLogger,
dockerLogger,
cliLogger,
minigraphLogger,
cloudConnectorLogger,
upnpLogger,
keyServerLogger,
remoteAccessLogger,
remoteQueryLogger,
apiLogger,
];
// Send SIGUSR1 to increase log level
process.on('SIGUSR1', () => {
const level = typeof logger.level === 'string' ? logger.level : logger.level.levelStr;
const nextLevel = levels[levels.findIndex(_level => _level === level) + 1] ?? levels[0];
loggers.forEach(logger => {
logger.level = nextLevel;
});
internalLogger.mark('Log level changed from %s to %s', level, nextLevel);
const level = logger.level;
const nextLevel =
levels[levels.findIndex((_level) => _level === level) + 1] ?? levels[0];
loggers.forEach((logger) => {
logger.level = nextLevel;
});
internalLogger.info({
message: `Log level changed from ${level} to ${nextLevel}`,
});
});
// Send SIGUSR1 to decrease log level
process.on('SIGUSR2', () => {
const level = typeof logger.level === 'string' ? logger.level : logger.level.levelStr;
const nextLevel = levels[levels.findIndex(_level => _level === level) - 1] ?? levels[levels.length - 1];
loggers.forEach(logger => {
logger.level = nextLevel;
});
internalLogger.mark('Log level changed from %s to %s', level, nextLevel);
const level = logger.level;
const nextLevel =
levels[levels.findIndex((_level) => _level === level) - 1] ??
levels[levels.length - 1];
loggers.forEach((logger) => {
logger.level = nextLevel;
});
internalLogger.info({
message: `Log level changed from ${level} to ${nextLevel}`,
});
});

View File

@@ -0,0 +1,20 @@
import { writeFile } from 'fs/promises';
import { fileExists } from '@app/core/utils/files/file-exists';
export const setupLogRotation = async () => {
if (await fileExists('/etc/logrotate.d/unraid-api')) {
return;
} else {
await writeFile(
'/etc/logrotate.d/unraid-api',
`
/var/log/unraid-api/*.log {
rotate 1
missingok
size 5M
}
`,
{ mode: '644' }
);
}
};

View File

@@ -1,126 +0,0 @@
// import fs from 'fs';
// import { log } from '../log';
import type { CoreContext, CoreResult } from '@app/core/types';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { NotImplementedError } from '@app/core/errors/not-implemented-error';
import { AppError } from '@app/core/errors/app-error';
import { getters } from '@app/store';
interface Context extends CoreContext {
data: {
keyUri?: string;
trial?: boolean;
replacement?: boolean;
email?: string;
keyFile?: string;
};
}
interface Result extends CoreResult {
json: {
key?: string;
type?: string;
};
}
/**
* Register a license key.
*/
export const addLicenseKey = async (context: Context): Promise<Result | void> => {
ensurePermission(context.user, {
resource: 'license-key',
action: 'create',
possession: 'any',
});
// Const { data } = context;
const emhttp = getters.emhttp();
const guid = emhttp.var.regGuid;
// Const timestamp = new Date();
if (!guid) {
throw new AppError('guid missing');
}
throw new NotImplementedError();
// // Connect to unraid.net to request a trial key
// if (data?.trial) {
// const body = new FormData();
// body.append('guid', guid);
// body.append('timestamp', timestamp.getTime().toString());
// const key = await got('https://keys.lime-technology.com/account/trial', { method: 'POST', body })
// .then(response => JSON.parse(response.body))
// .catch(error => {
// log.error(error);
// throw new AppError(`Sorry, a HTTP ${error.status} error occurred while registering USB Flash GUID ${guid}`);
// });
// // Update the trial key file
// await fs.promises.writeFile('/boot/config/Trial.key', Buffer.from(key, 'base64'));
// return {
// text: 'Thank you for registering, your trial key has been accepted.',
// json: {
// key
// }
// };
// }
// // Connect to unraid.net to request a new replacement key
// if (data?.replacement) {
// const { email, keyFile } = data;
// if (!email || !keyFile) {
// throw new AppError('email or keyFile is missing');
// }
// const body = new FormData();
// body.append('guid', guid);
// body.append('timestamp', timestamp.getTime().toString());
// body.append('email', email);
// body.append('keyfile', keyFile);
// const { body: key } = await got('https://keys.lime-technology.com/account/license/transfer', { method: 'POST', body })
// .then(response => JSON.parse(response.body))
// .catch(error => {
// log.error(error);
// throw new AppError(`Sorry, a HTTP ${error.status} error occurred while issuing a replacement for USB Flash GUID ${guid}`);
// });
// // Update the trial key file
// await fs.promises.writeFile('/boot/config/Trial.key', Buffer.from(key, 'base64'));
// return {
// text: 'Thank you for registering, your trial key has been registered.',
// json: {
// key
// }
// };
// }
// // Register a new server
// if (data?.keyUri) {
// const parts = data.keyUri.split('.key')[0].split('/');
// const { [parts.length - 1]: keyType } = parts;
// // Download key blob
// const { body: key } = await got(data.keyUri)
// .then(response => JSON.parse(response.body))
// .catch(error => {
// log.error(error);
// throw new AppError(`Sorry, a HTTP ${error.status} error occurred while registering your key for USB Flash GUID ${guid}`);
// });
// // Save key file
// await fs.promises.writeFile(`/boot/config/${keyType}.key`, Buffer.from(key, 'base64'));
// return {
// text: `Thank you for registering, your ${keyType} key has been accepted.`,
// json: {
// type: keyType
// }
// };
// }
};

View File

@@ -23,7 +23,6 @@ interface Context extends CoreContext {
*/
export const addUser = async (context: Context): Promise<CoreResult> => {
const { data } = context;
// Check permissions
ensurePermission(context.user, {
resource: 'user',

View File

@@ -1,7 +1,8 @@
import camelCaseKeys from 'camelcase-keys';
import { docker, ensurePermission } from '@app/core/utils';
import { docker } from '@app/core/utils';
import { type CoreContext, type CoreResult } from '@app/core/types';
import { catchHandlers } from '@app/core/utils/misc/catch-handlers';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
export const getDockerNetworks = async (context: CoreContext): Promise<CoreResult> => {
const { user } = context;

View File

@@ -1,25 +0,0 @@
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { store } from '@app/store';
import { type QueryResolvers } from '@app/graphql/generated/api/types';
import { getArrayData } from '@app/core/modules/array/get-array-data';
/**
* Get array info.
* @returns Array state and array/disk capacity.
*/
export const getArray: QueryResolvers['array'] = (
_,
__,
context
) => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'array',
action: 'read',
possession: 'any',
});
return getArrayData(store.getState);
};

View File

@@ -5,14 +5,12 @@ import {
diskLayout,
} from 'systeminformation';
import { map as asyncMap } from 'p-iteration';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { type Context } from '@app/graphql/schema/utils';
import {
type Disk,
DiskInterfaceType,
DiskSmartStatus,
DiskFsType,
} from '@app/graphql/generated/api/types';
import { DiskFsType } from '@app/graphql/generated/api/types';
import { graphqlLogger } from '@app/core/log';
const getTemperature = async (
@@ -86,18 +84,8 @@ const parseDisk = async (
* Get all disks.
*/
export const getDisks = async (
context: Context,
options?: { temperature: boolean }
): Promise<Disk[]> => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'disk',
action: 'read',
possession: 'any',
});
// Return all fields but temperature
if (options?.temperature === false) {
const partitions = await blockDevices().then((devices) =>

View File

@@ -1,28 +0,0 @@
import { AppError } from '@app/core/errors/app-error';
import type { CoreResult, CoreContext } from '@app/core/types';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
/**
* Get all unassigned devices.
*/
export const getUnassignedDevices = async (context: CoreContext): Promise<CoreResult> => {
const { user } = context;
// Bail if the user doesn't have permission
ensurePermission(user, {
resource: 'devices/unassigned',
action: 'read',
possession: 'any',
});
const devices = [];
if (devices.length === 0) {
throw new AppError('No devices found.', 404);
}
return {
text: `Unassigned devices: ${JSON.stringify(devices, null, 2)}`,
json: devices,
};
};

View File

@@ -1,26 +0,0 @@
import type { CoreContext, CoreResult } from '@app/core/types';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { getters } from '@app/store';
/**
* Get all system vars.
*/
export const getVars = async (context: CoreContext): Promise<CoreResult> => {
const { user } = context;
// Bail if the user doesn't have permission
ensurePermission(user, {
resource: 'vars',
action: 'read',
possession: 'any',
});
const emhttp = getters.emhttp();
return {
text: `Vars: ${JSON.stringify(emhttp.var, null, 2)}`,
json: {
...emhttp.var,
},
};
};

View File

@@ -0,0 +1,23 @@
// Created from 'create-ts-index'
export * from './array';
export * from './debug';
export * from './disks';
export * from './docker';
export * from './services';
export * from './settings';
export * from './shares';
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';
export * from './get-me';
export * from './get-parity-history';
export * from './get-permissions';
export * from './get-services';
export * from './get-users';
export * from './get-welcome';

View File

@@ -1,7 +1,7 @@
import { execa } from 'execa';
import { ensurePermission } from '@app/core/utils';
import { type CoreContext, type CoreResult } from '@app/core/types';
import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
interface Result extends CoreResult {
json: {

View File

@@ -1,7 +1,8 @@
import type { CoreContext, CoreResult } from '@app/core/types/global';
import type { UserShare, DiskShare } from '@app/core/types/states/share';
import { AppError } from '@app/core/errors/app-error';
import { getShares, ensurePermission } from '@app/core/utils';
import { getShares } from '@app/core/utils';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
interface Context extends CoreContext {
params: {

View File

@@ -1,7 +1,6 @@
import { ConnectListAllDomainsFlags } from '@vmngr/libvirt';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { getHypervisor } from '@app/core/utils/vms/get-hypervisor';
import { VmState, type VmDomain, type VmsResolvers } from '@app/graphql/generated/api/types';
import { VmState, type VmDomain } from '@app/graphql/generated/api/types';
import { GraphQLError } from 'graphql';
const states = {
@@ -18,19 +17,7 @@ const states = {
/**
* Get vm domains.
*/
export const domainResolver: VmsResolvers['domain'] = async (
_,
__,
context
) => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'vms/domain',
action: 'read',
possession: 'any',
});
export const getDomains =async () => {
try {
const hypervisor = await getHypervisor();

View File

@@ -1,33 +1,190 @@
import { logger } from '@app/core/log';
import { permissions as defaultPermissions } from '@app/core/default-permissions';
import { AccessControl } from 'accesscontrol';
import { apiLogger } from '@app/core/log';
import { RolesBuilder } from 'nest-access-control';
export interface Permission {
role?: string;
resource: string;
action: string;
attributes: string;
}
export interface Role {
permissions: Array<Permission>;
extends?: string;
}
// Use built in permissions
const getPermissions = () => defaultPermissions;
// Build permissions array
const roles = getPermissions();
const permissions = Object.entries(roles).flatMap(([roleName, role]) => [
...(role?.permissions ?? []).map(permission => ({
...permission,
role: roleName,
})),
]);
// Grant permissions
const ac = new AccessControl(permissions);
// Extend roles
Object.entries(getPermissions()).forEach(([roleName, role]) => {
if (role.extends) {
ac.extendRole(roleName, role.extends);
}
});
logger.addContext('permissions', permissions);
logger.trace('Loaded permissions');
logger.removeContext('permissions');
export {
ac,
const roles: Record<string, Role> = {
admin: {
extends: 'guest',
permissions: [
{ resource: 'apikey', action: 'read:any', attributes: '*' },
{ resource: 'cloud', action: 'read:own', attributes: '*' },
{ resource: 'config', action: 'update:own', attributes: '*' },
{ resource: 'connect', action: 'read:own', attributes: '*' },
{ resource: 'connect', action: 'update:own', attributes: '*' },
{ resource: 'customizations', action: 'read:any', attributes: '*' },
{ resource: 'array', action: 'read:any', attributes: '*' },
{ resource: 'cpu', action: 'read:any', attributes: '*' },
{
resource: 'crash-reporting-enabled',
action: 'read:any',
attributes: '*',
},
{ resource: 'device', action: 'read:any', attributes: '*' },
{
resource: 'device/unassigned',
action: 'read:any',
attributes: '*',
},
{ resource: 'disk', action: 'read:any', attributes: '*' },
{ resource: 'disk/settings', action: 'read:any', attributes: '*' },
{ resource: 'display', action: 'read:any', attributes: '*' },
{
resource: 'docker/container',
action: 'read:any',
attributes: '*',
},
{ resource: 'docker/network', action: 'read:any', attributes: '*' },
{ resource: 'flash', action: 'read:any', attributes: '*' },
{ resource: 'info', action: 'read:any', attributes: '*' },
{ resource: 'license-key', action: 'read:any', attributes: '*' },
{ resource: 'logs', action: 'read:any', attributes: '*' },
{ resource: 'machine-id', action: 'read:any', attributes: '*' },
{ resource: 'memory', action: 'read:any', attributes: '*' },
{ resource: 'notifications', action: 'read:any', attributes: '*' },
{
resource: 'notifications',
action: 'create:any',
attributes: '*',
},
{ resource: 'online', action: 'read:any', attributes: '*' },
{ resource: 'os', action: 'read:any', attributes: '*' },
{ resource: 'owner', action: 'read:any', attributes: '*' },
{ resource: 'parity-history', action: 'read:any', attributes: '*' },
{ resource: 'permission', action: 'read:any', attributes: '*' },
{ resource: 'registration', action: 'read:any', attributes: '*' },
{ resource: 'servers', action: 'read:any', attributes: '*' },
{ resource: 'service', action: 'read:any', attributes: '*' },
{
resource: 'service/emhttpd',
action: 'read:any',
attributes: '*',
},
{
resource: 'service/unraid-api',
action: 'read:any',
attributes: '*',
},
{ resource: 'services', action: 'read:any', attributes: '*' },
{ resource: 'share', action: 'read:any', attributes: '*' },
{
resource: 'software-versions',
action: 'read:any',
attributes: '*',
},
{ resource: 'unraid-version', action: 'read:any', attributes: '*' },
{ resource: 'uptime', action: 'read:any', attributes: '*' },
{ resource: 'user', action: 'read:any', attributes: '*' },
{ resource: 'vars', action: 'read:any', attributes: '*' },
{ resource: 'vms', action: 'read:any', attributes: '*' },
{ resource: 'vms/domain', action: 'read:any', attributes: '*' },
{ resource: 'vms/network', action: 'read:any', attributes: '*' },
],
},
upc: {
extends: 'guest',
permissions: [
{ resource: 'apikey', action: 'read:own', attributes: '*' },
{ resource: 'cloud', action: 'read:own', attributes: '*' },
{ resource: 'config', action: 'read:any', attributes: '*' },
{
resource: 'crash-reporting-enabled',
action: 'read:any',
attributes: '*',
},
{ resource: 'customizations', action: 'read:any', attributes: '*' },
{ resource: 'disk', action: 'read:any', attributes: '*' },
{ resource: 'display', action: 'read:any', attributes: '*' },
{ resource: 'flash', action: 'read:any', attributes: '*' },
{ resource: 'info', action: 'read:any', attributes: '*' },
{ resource: 'logs', action: 'read:any', attributes: '*' },
{ resource: 'os', action: 'read:any', attributes: '*' },
{ resource: 'owner', action: 'read:any', attributes: '*' },
{ resource: 'permission', action: 'read:any', attributes: '*' },
{ resource: 'registration', action: 'read:any', attributes: '*' },
{ resource: 'servers', action: 'read:any', attributes: '*' },
{ resource: 'vars', action: 'read:any', attributes: '*' },
{ resource: 'config', action: 'update:own', attributes: '*' },
{ resource: 'connect', action: 'read:own', attributes: '*' },
{ resource: 'connect', action: 'update:own', attributes: '*' },
],
},
my_servers: {
extends: 'guest',
permissions: [
{ resource: 'array', action: 'read:any', attributes: '*' },
{ resource: 'customizations', action: 'read:any', attributes: '*' },
{ resource: 'dashboard', action: 'read:any', attributes: '*' },
{
resource: 'docker/container',
action: 'read:any',
attributes: '*',
},
{ resource: 'logs', action: 'read:any', attributes: '*' },
{ resource: 'docker/network', action: 'read:any', attributes: '*' },
{ resource: 'notifications', action: 'read:any', attributes: '*' },
{ resource: 'vms', action: 'read:any', attributes: '*' },
{ resource: 'vms/domain', action: 'read:any', attributes: '*' },
{ resource: 'unraid-version', action: 'read:any', attributes: '*' },
],
},
notifier: {
extends: 'guest',
permissions: [
{
resource: 'notifications',
action: 'create:own',
attributes: '*',
},
],
},
guest: {
permissions: [
{ resource: 'me', action: 'read:any', attributes: '*' },
{ resource: 'welcome', action: 'read:any', attributes: '*' },
],
},
};
export const setupPermissions = (): RolesBuilder => {
// First create an array of permissions that will be used as the base permission set for the app
const grantList = Object.entries(roles).reduce<Array<Permission>>(
(acc, [roleName, role]) => {
if (role.permissions) {
role.permissions.forEach((permission) => {
acc.push({
...permission,
role: roleName,
});
});
}
return acc;
},
[]
);
const ac = new RolesBuilder(grantList);
// Next, Extend roles
Object.entries(roles).forEach(([roleName, role]) => {
if (role.extends) {
ac.extendRole(roleName, role.extends);
}
});
apiLogger.debug('Possible Roles: %o', ac.getRoles());
return ac;
};
export const ac = null;

View File

@@ -6,12 +6,23 @@ const eventEmitter = new EventEmitter();
eventEmitter.setMaxListeners(30);
export enum PUBSUB_CHANNEL {
ARRAY = 'ARRAY',
DASHBOARD = 'DASHBOARD',
DISPLAY = 'DISPLAY',
INFO = 'INFO',
NOTIFICATION = 'NOTIFICATION',
OWNER = 'OWNER',
SERVERS = 'SERVERS',
VMS = 'VMS',
REGISTRATION = 'REGISTRATION',
}
export const pubsub = new PubSub({ eventEmitter });
/**
* Create a pubsub subscription.
* @param channel The pubsub channel to subscribe to.
*/
export const createSubscription = (channel: PUBSUB_CHANNEL) => {
return pubsub.asyncIterator(channel);
};

View File

@@ -113,8 +113,12 @@ export type Var = {
regFile: string;
regGen: string;
regGuid: string;
/** Registration time for key */
regTm: string;
/** Expiration of license for Trial Keys */
regTm2: string;
/** Expiration of Updates for non-legacy keys */
regExp: string | null;
/** Who the current Unraid key is registered to. */
regTo: string;
/** Which type of key this is. */

View File

@@ -1,3 +1,4 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import {
type SliceState as ConfigSliceState,
@@ -34,18 +35,18 @@ export const getWriteableConfig = <T extends ConfigType>(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const newState: ConfigObject<T> = {
api: {
version: api.version ?? initialState.api.version,
...(api.extraOrigins ? { extraOrigins: api.extraOrigins } : {}),
version: api?.version ?? initialState.api.version,
extraOrigins: api?.extraOrigins ?? initialState.api.extraOrigins,
},
local: {
...(local['2Fa'] === 'yes' ? { '2Fa': local['2Fa'] } : {}),
...(local.showT2Fa === 'yes' ? { showT2Fa: local.showT2Fa } : {}),
...(local?.['2Fa'] === 'yes' ? { '2Fa': local['2Fa'] } : {}),
...(local?.showT2Fa === 'yes' ? { showT2Fa: local.showT2Fa } : {}),
},
notifier: {
apikey: notifier.apikey ?? initialState.notifier.apikey,
},
remote: {
...(remote['2Fa'] === 'yes' ? { '2Fa': remote['2Fa'] } : {}),
...(remote?.['2Fa'] === 'yes' ? { '2Fa': remote['2Fa'] } : {}),
wanaccess: remote.wanaccess ?? initialState.remote.wanaccess,
wanport: remote.wanport ?? initialState.remote.wanport,
...(remote.upnpEnabled ? { upnpEnabled: remote.upnpEnabled } : {}),
@@ -61,16 +62,10 @@ export const getWriteableConfig = <T extends ConfigType>(
...(mode === 'memory'
? {
allowedOrigins:
remote.allowedOrigins ??
initialState.remote.allowedOrigins,
getAllowedOrigins().join(', ')
}
: {}),
...(remote.dynamicRemoteAccessType ===
DynamicRemoteAccessType.DISABLED
? {}
: {
dynamicRemoteAccessType: remote.dynamicRemoteAccessType,
}),
dynamicRemoteAccessType: remote.dynamicRemoteAccessType ?? DynamicRemoteAccessType.DISABLED,
},
upc: {
apikey: upc.apikey ?? initialState.upc.apikey,

View File

@@ -0,0 +1,10 @@
// Created from 'create-ts-index'
export * from './array';
export * from './authentication';
export * from './clients';
export * from './plugins';
export * from './shares';
export * from './validation';
export * from './vms';
export * from './casting';

View File

@@ -15,9 +15,7 @@ export const loadState = <T extends Record<string, unknown>>(filePath: string):
deep: true,
}) as T;
logger.addContext('config', config);
logger.trace('"%s" was loaded', filePath);
logger.removeContext('config');
logger.trace({ config }, '"%s" was loaded', filePath);
return config;
} catch (error: unknown) {

View File

@@ -0,0 +1,35 @@
import { AppError } from '@app/core/errors/app-error';
import { logger } from '@app/core/log';
import { type CancelableRequest, got, type Response } from 'got';
export const sendFormToKeyServer = async (url: string, data: Record<string, unknown>): Promise<CancelableRequest<Response<string>>> => {
if (!data) {
throw new AppError('Missing data field.');
}
// Create form
const form = new URLSearchParams();
Object.entries(data).forEach(([key, value]) => {
if (value !== undefined) {
form.append(key, String(value));
}
});
// Convert form to string
const body = form.toString();
logger.trace({form: body }, 'Sending form to key-server');
// Send form
return got(url, {
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded',
},
timeout: {
request: 5_000,
},
throwHttpErrors: true,
body,
});
};

View File

@@ -14,4 +14,7 @@ export const GRAPHQL_INTROSPECTION = Boolean(
export const PORT = process.env.PORT ?? '/var/run/unraid-api.sock';
export const DRY_RUN = process.env.DRY_RUN === 'true';
export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === 'true';
export const LOG_CORS = process.env.LOG_CORS === 'true';
export const LOG_CORS = process.env.LOG_CORS === 'true';
export const LOG_TYPE = process.env.LOG_TYPE as 'pretty' | 'raw' ?? 'pretty';
export const LOG_LEVEL = process.env.LOG_LEVEL as 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
export const LOG_TRANSPORT = process.env.LOG_TRANSPORT as 'file' | 'stdout';

View File

@@ -1,52 +0,0 @@
import { report } from '@app/cli/commands/report';
import { logger } from '@app/core/log';
import { apiKeyToUser } from '@app/graphql/index';
import { getters } from '@app/store/index';
import { execa } from 'execa';
import { type Response, type Request } from 'express';
import { stat, writeFile } from 'fs/promises';
import { join } from 'path';
const saveApiReport = async (pathToReport: string) => {
try {
const apiReport = await report('-vv', '--json');
logger.debug('Report object %o', apiReport);
await writeFile(
pathToReport,
JSON.stringify(apiReport, null, 2),
'utf-8'
);
} catch (error) {
logger.warn('Could not generate report for zip with error %o', error);
}
};
export const getLogs = async (req: Request, res: Response) => {
const apiKey = req.headers['x-api-key'] || req.query.apiKey;
const logPath = getters.paths()['log-base'];
try {
await saveApiReport(join(logPath, 'report.json'));
} catch (error) {
logger.warn('Could not generate report for zip with error %o', error);
}
const zipToWrite = join(logPath, '../unraid-api.tar.gz');
if (
apiKey &&
typeof apiKey === 'string' &&
(await apiKeyToUser(apiKey)).role !== 'guest'
) {
const exists = Boolean(await stat(logPath).catch(() => null));
if (exists) {
try {
await execa('tar', ['-czf', zipToWrite, logPath]);
return res.status(200).sendFile(zipToWrite);
} catch (error) {
return res.status(503).send(`Failed: ${error}`);
}
} else {
return res.status(404).send('No Logs Available');
}
}
return res.status(403).send('unauthorized');
};

View File

@@ -1,94 +0,0 @@
import get from 'lodash/get';
import * as core from '@app/core';
import { graphqlLogger } from '@app/core/log';
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
import { getCoreModule } from '@app/graphql/index';
import type { GraphQLFieldResolver, GraphQLSchema } from 'graphql';
import type { User } from '@app/core/types/states/user';
interface FuncDirective {
module: string;
data: object;
query: any;
extractFromResponse: string;
}
const funcDirectiveResolver: (directiveArgs: FuncDirective) => GraphQLFieldResolver<undefined, { user?: User }, { result?: any }> | undefined = ({
module: coreModule,
data,
query,
extractFromResponse,
}) => async (_, args, context) => {
const func = getCoreModule(coreModule);
const functionContext = {
query,
data,
user: context.user,
};
// Run function
const [error, coreMethodResult] = await Promise.resolve(func(functionContext, core))
.then(result => [undefined, result])
.catch(error_ => {
// Ensure we aren't leaking anything in production
if (process.env.NODE_ENV === 'production') {
graphqlLogger.error('Module:', coreModule, 'Error:', error_.message);
return [new Error(error_.message)];
}
return [error_];
});
// Bail if we can't get the method to run
if (error) {
return error;
}
// Get wanted result type or fallback to json
const result = coreMethodResult[args.result || 'json'];
// Allow fields to be extracted
if (extractFromResponse) {
return get(result, extractFromResponse);
}
return result;
};
/**
* Get the func directive - this is used to resolve @func directives in the graphql schema
* @returns Type definition and schema interceptor to create resolvers for @func directives
*/
export function getFuncDirective() {
const directiveName = 'func';
return {
funcDirectiveTypeDefs: /* GraphQL */`
directive @func(
module: String!
data: JSON
query: JSON
result: String
extractFromResponse: String
) on FIELD_DEFINITION
`,
funcDirectiveTransformer: (schema: GraphQLSchema): GraphQLSchema => mapSchema(schema, {
[MapperKind.MUTATION_ROOT_FIELD](fieldConfig) {
const funcDirective = getDirective(schema, fieldConfig, directiveName)?.[0] as FuncDirective | undefined;
if (funcDirective?.module) {
fieldConfig.resolve = funcDirectiveResolver(funcDirective);
}
return fieldConfig;
},
[MapperKind.QUERY_ROOT_FIELD](fieldConfig) {
const funcDirective = getDirective(schema, fieldConfig, directiveName)?.[0] as FuncDirective | undefined;
if (funcDirective?.module) {
fieldConfig.resolve = funcDirectiveResolver(funcDirective);
}
return fieldConfig;
},
}),
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,66 @@
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql.js';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
?.deferredFields;
if (!deferredFields) return true;
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
const fragName = fragDef?.name?.value;
const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every(field => data && field in data);
}

View File

@@ -5,41 +5,43 @@ export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
/** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */
DateTime: string;
DateTime: { input: string; output: string; }
/** A field whose value is a IPv4 address: https://en.wikipedia.org/wiki/IPv4. */
IPv4: any;
IPv4: { input: any; output: any; }
/** A field whose value is a IPv6 address: https://en.wikipedia.org/wiki/IPv6. */
IPv6: any;
IPv6: { input: any; output: any; }
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSON: { [key: string]: any };
JSON: { input: { [key: string]: any }; output: { [key: string]: any }; }
/** The `Long` scalar type represents 52-bit integers */
Long: number;
Long: { input: number; output: number; }
/** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */
Port: number;
Port: { input: number; output: number; }
/** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */
URL: URL;
URL: { input: URL; output: URL; }
};
export type AccessUrl = {
__typename?: 'AccessUrl';
ipv4?: Maybe<Scalars['URL']>;
ipv6?: Maybe<Scalars['URL']>;
name?: Maybe<Scalars['String']>;
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']>;
ipv6?: InputMaybe<Scalars['URL']>;
name?: InputMaybe<Scalars['String']>;
ipv4?: InputMaybe<Scalars['URL']['input']>;
ipv6?: InputMaybe<Scalars['URL']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
type: URL_TYPE;
};
@@ -50,15 +52,15 @@ export type ArrayCapacity = {
export type ArrayCapacityBytes = {
__typename?: 'ArrayCapacityBytes';
free?: Maybe<Scalars['Long']>;
total?: Maybe<Scalars['Long']>;
used?: Maybe<Scalars['Long']>;
free?: Maybe<Scalars['Long']['output']>;
total?: Maybe<Scalars['Long']['output']>;
used?: Maybe<Scalars['Long']['output']>;
};
export type ArrayCapacityBytesInput = {
free?: InputMaybe<Scalars['Long']>;
total?: InputMaybe<Scalars['Long']>;
used?: InputMaybe<Scalars['Long']>;
free?: InputMaybe<Scalars['Long']['input']>;
total?: InputMaybe<Scalars['Long']['input']>;
used?: InputMaybe<Scalars['Long']['input']>;
};
export type ArrayCapacityInput = {
@@ -73,9 +75,9 @@ export type ClientConnectedEvent = {
export type ClientConnectionEventData = {
__typename?: 'ClientConnectionEventData';
apiKey: Scalars['String'];
apiKey: Scalars['String']['output'];
type: ClientType;
version: Scalars['String'];
version: Scalars['String']['output'];
};
export type ClientDisconnectedEvent = {
@@ -98,7 +100,7 @@ export enum ClientType {
export type Config = {
__typename?: 'Config';
error?: Maybe<ConfigErrorState>;
valid?: Maybe<Scalars['Boolean']>;
valid?: Maybe<Scalars['Boolean']['output']>;
};
export enum ConfigErrorState {
@@ -114,10 +116,10 @@ export type Dashboard = {
array?: Maybe<DashboardArray>;
config?: Maybe<DashboardConfig>;
display?: Maybe<DashboardDisplay>;
id: Scalars['ID'];
lastPublish?: Maybe<Scalars['DateTime']>;
id: Scalars['ID']['output'];
lastPublish?: Maybe<Scalars['DateTime']['output']>;
network?: Maybe<Network>;
online?: Maybe<Scalars['Boolean']>;
online?: Maybe<Scalars['Boolean']['output']>;
os?: Maybe<DashboardOs>;
services?: Maybe<Array<Maybe<DashboardService>>>;
twoFactor?: Maybe<DashboardTwoFactor>;
@@ -128,13 +130,13 @@ export type Dashboard = {
export type DashboardApps = {
__typename?: 'DashboardApps';
installed?: Maybe<Scalars['Int']>;
started?: Maybe<Scalars['Int']>;
installed?: Maybe<Scalars['Int']['output']>;
started?: Maybe<Scalars['Int']['output']>;
};
export type DashboardAppsInput = {
installed: Scalars['Int'];
started: Scalars['Int'];
installed: Scalars['Int']['input'];
started: Scalars['Int']['input'];
};
export type DashboardArray = {
@@ -142,40 +144,40 @@ export type DashboardArray = {
/** Current array capacity */
capacity?: Maybe<ArrayCapacity>;
/** Current array state */
state?: Maybe<Scalars['String']>;
state?: Maybe<Scalars['String']['output']>;
};
export type DashboardArrayInput = {
/** Current array capacity */
capacity: ArrayCapacityInput;
/** Current array state */
state: Scalars['String'];
state: Scalars['String']['input'];
};
export type DashboardCase = {
__typename?: 'DashboardCase';
base64?: Maybe<Scalars['String']>;
error?: Maybe<Scalars['String']>;
icon?: Maybe<Scalars['String']>;
url?: Maybe<Scalars['String']>;
base64?: Maybe<Scalars['String']['output']>;
error?: Maybe<Scalars['String']['output']>;
icon?: Maybe<Scalars['String']['output']>;
url?: Maybe<Scalars['String']['output']>;
};
export type DashboardCaseInput = {
base64: Scalars['String'];
error?: InputMaybe<Scalars['String']>;
icon: Scalars['String'];
url: Scalars['String'];
base64: Scalars['String']['input'];
error?: InputMaybe<Scalars['String']['input']>;
icon: Scalars['String']['input'];
url: Scalars['String']['input'];
};
export type DashboardConfig = {
__typename?: 'DashboardConfig';
error?: Maybe<Scalars['String']>;
valid?: Maybe<Scalars['Boolean']>;
error?: Maybe<Scalars['String']['output']>;
valid?: Maybe<Scalars['Boolean']['output']>;
};
export type DashboardConfigInput = {
error?: InputMaybe<Scalars['String']>;
valid: Scalars['Boolean'];
error?: InputMaybe<Scalars['String']['input']>;
valid: Scalars['Boolean']['input'];
};
export type DashboardDisplay = {
@@ -202,37 +204,37 @@ export type DashboardInput = {
export type DashboardOs = {
__typename?: 'DashboardOs';
hostname?: Maybe<Scalars['String']>;
uptime?: Maybe<Scalars['DateTime']>;
hostname?: Maybe<Scalars['String']['output']>;
uptime?: Maybe<Scalars['DateTime']['output']>;
};
export type DashboardOsInput = {
hostname: Scalars['String'];
uptime: Scalars['DateTime'];
hostname: Scalars['String']['input'];
uptime: Scalars['DateTime']['input'];
};
export type DashboardService = {
__typename?: 'DashboardService';
name?: Maybe<Scalars['String']>;
online?: Maybe<Scalars['Boolean']>;
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<DashboardServiceUptime>;
version?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']['output']>;
};
export type DashboardServiceInput = {
name: Scalars['String'];
online: Scalars['Boolean'];
name: Scalars['String']['input'];
online: Scalars['Boolean']['input'];
uptime?: InputMaybe<DashboardServiceUptimeInput>;
version: Scalars['String'];
version: Scalars['String']['input'];
};
export type DashboardServiceUptime = {
__typename?: 'DashboardServiceUptime';
timestamp?: Maybe<Scalars['DateTime']>;
timestamp?: Maybe<Scalars['DateTime']['output']>;
};
export type DashboardServiceUptimeInput = {
timestamp: Scalars['DateTime'];
timestamp: Scalars['DateTime']['input'];
};
export type DashboardTwoFactor = {
@@ -248,53 +250,59 @@ export type DashboardTwoFactorInput = {
export type DashboardTwoFactorLocal = {
__typename?: 'DashboardTwoFactorLocal';
enabled?: Maybe<Scalars['Boolean']>;
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type DashboardTwoFactorLocalInput = {
enabled: Scalars['Boolean'];
enabled: Scalars['Boolean']['input'];
};
export type DashboardTwoFactorRemote = {
__typename?: 'DashboardTwoFactorRemote';
enabled?: Maybe<Scalars['Boolean']>;
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type DashboardTwoFactorRemoteInput = {
enabled: Scalars['Boolean'];
enabled: Scalars['Boolean']['input'];
};
export type DashboardVars = {
__typename?: 'DashboardVars';
flashGuid?: Maybe<Scalars['String']>;
regState?: Maybe<Scalars['String']>;
regTy?: Maybe<Scalars['String']>;
flashGuid?: Maybe<Scalars['String']['output']>;
regState?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<Scalars['String']['output']>;
serverDescription?: Maybe<Scalars['String']['output']>;
serverName?: Maybe<Scalars['String']['output']>;
};
export type DashboardVarsInput = {
flashGuid: Scalars['String'];
regState: Scalars['String'];
regTy: Scalars['String'];
flashGuid: Scalars['String']['input'];
regState: Scalars['String']['input'];
regTy: Scalars['String']['input'];
/** Server description */
serverDescription?: InputMaybe<Scalars['String']['input']>;
/** Name of the server */
serverName?: InputMaybe<Scalars['String']['input']>;
};
export type DashboardVersions = {
__typename?: 'DashboardVersions';
unraid?: Maybe<Scalars['String']>;
unraid?: Maybe<Scalars['String']['output']>;
};
export type DashboardVersionsInput = {
unraid: Scalars['String'];
unraid: Scalars['String']['input'];
};
export type DashboardVms = {
__typename?: 'DashboardVms';
installed?: Maybe<Scalars['Int']>;
started?: Maybe<Scalars['Int']>;
installed?: Maybe<Scalars['Int']['output']>;
started?: Maybe<Scalars['Int']['output']>;
};
export type DashboardVmsInput = {
installed: Scalars['Int'];
started: Scalars['Int'];
installed: Scalars['Int']['input'];
started: Scalars['Int']['input'];
};
export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQLEvent | UpdateEvent;
@@ -310,13 +318,13 @@ export enum EventType {
export type FullServerDetails = {
__typename?: 'FullServerDetails';
apiConnectedCount?: Maybe<Scalars['Int']>;
apiVersion?: Maybe<Scalars['String']>;
connectionTimestamp?: Maybe<Scalars['String']>;
apiConnectedCount?: Maybe<Scalars['Int']['output']>;
apiVersion?: Maybe<Scalars['String']['output']>;
connectionTimestamp?: Maybe<Scalars['String']['output']>;
dashboard?: Maybe<Dashboard>;
lastPublish?: Maybe<Scalars['String']>;
lastPublish?: Maybe<Scalars['String']['output']>;
network?: Maybe<Network>;
online?: Maybe<Scalars['Boolean']>;
online?: Maybe<Scalars['Boolean']['output']>;
};
export enum Importance {
@@ -325,48 +333,41 @@ export enum Importance {
WARNING = 'WARNING'
}
export enum KeyType {
BASIC = 'BASIC',
PLUS = 'PLUS',
PRO = 'PRO',
TRIAL = 'TRIAL'
}
export type KsServerDetails = {
__typename?: 'KsServerDetails';
accessLabel: Scalars['String'];
accessUrl: Scalars['String'];
apiKey?: Maybe<Scalars['String']>;
description: Scalars['String'];
dnsHash: Scalars['String'];
flashBackupDate?: Maybe<Scalars['Int']>;
flashBackupUrl: Scalars['String'];
flashProduct: Scalars['String'];
flashVendor: Scalars['String'];
guid: Scalars['String'];
ipsId: Scalars['String'];
keyType: KeyType;
licenseKey: Scalars['String'];
name: Scalars['String'];
plgVersion?: Maybe<Scalars['String']>;
signedIn: Scalars['Boolean'];
accessLabel: Scalars['String']['output'];
accessUrl: Scalars['String']['output'];
apiKey?: Maybe<Scalars['String']['output']>;
description: Scalars['String']['output'];
dnsHash: Scalars['String']['output'];
flashBackupDate?: Maybe<Scalars['Int']['output']>;
flashBackupUrl: Scalars['String']['output'];
flashProduct: Scalars['String']['output'];
flashVendor: Scalars['String']['output'];
guid: Scalars['String']['output'];
ipsId?: Maybe<Scalars['String']['output']>;
keyType?: Maybe<Scalars['String']['output']>;
licenseKey: Scalars['String']['output'];
name: Scalars['String']['output'];
plgVersion?: Maybe<Scalars['String']['output']>;
signedIn: Scalars['Boolean']['output'];
};
export type LegacyService = {
__typename?: 'LegacyService';
name?: Maybe<Scalars['String']>;
online?: Maybe<Scalars['Boolean']>;
uptime?: Maybe<Scalars['Int']>;
version?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Scalars['Int']['output']>;
version?: Maybe<Scalars['String']['output']>;
};
export type Mutation = {
__typename?: 'Mutation';
remoteGraphQLResponse: Scalars['Boolean'];
remoteMutation: Scalars['String'];
remoteSession?: Maybe<Scalars['Boolean']>;
remoteGraphQLResponse: Scalars['Boolean']['output'];
remoteMutation: Scalars['String']['output'];
remoteSession?: Maybe<Scalars['Boolean']['output']>;
sendNotification?: Maybe<Notification>;
sendPing?: Maybe<Scalars['Boolean']>;
sendPing?: Maybe<Scalars['Boolean']['output']>;
updateDashboard: Dashboard;
updateNetwork: Network;
};
@@ -412,20 +413,20 @@ export type NetworkInput = {
export type Notification = {
__typename?: 'Notification';
description?: Maybe<Scalars['String']>;
description?: Maybe<Scalars['String']['output']>;
importance?: Maybe<Importance>;
link?: Maybe<Scalars['String']>;
link?: Maybe<Scalars['String']['output']>;
status: NotificationStatus;
subject?: Maybe<Scalars['String']>;
title?: Maybe<Scalars['String']>;
subject?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
};
export type NotificationInput = {
description?: InputMaybe<Scalars['String']>;
description?: InputMaybe<Scalars['String']['input']>;
importance: Importance;
link?: InputMaybe<Scalars['String']>;
subject?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
link?: InputMaybe<Scalars['String']['input']>;
subject?: InputMaybe<Scalars['String']['input']>;
title?: InputMaybe<Scalars['String']['input']>;
};
export enum NotificationStatus {
@@ -437,7 +438,7 @@ export enum NotificationStatus {
export type PingEvent = {
__typename?: 'PingEvent';
data?: Maybe<Scalars['String']>;
data?: Maybe<Scalars['String']['output']>;
type: EventType;
};
@@ -453,26 +454,27 @@ export enum PingEventSource {
export type ProfileModel = {
__typename?: 'ProfileModel';
avatar?: Maybe<Scalars['String']>;
url?: Maybe<Scalars['String']>;
userId?: Maybe<Scalars['ID']>;
username?: Maybe<Scalars['String']>;
avatar?: Maybe<Scalars['String']['output']>;
cognito_id?: Maybe<Scalars['String']['output']>;
url?: Maybe<Scalars['String']['output']>;
userId?: Maybe<Scalars['ID']['output']>;
username?: Maybe<Scalars['String']['output']>;
};
export type Query = {
__typename?: 'Query';
apiVersion?: Maybe<Scalars['String']>;
apiVersion?: Maybe<Scalars['String']['output']>;
dashboard?: Maybe<Dashboard>;
ksServers: Array<KsServerDetails>;
online?: Maybe<Scalars['Boolean']>;
remoteQuery: Scalars['String'];
online?: Maybe<Scalars['Boolean']['output']>;
remoteQuery: Scalars['String']['output'];
servers: Array<Maybe<Server>>;
status?: Maybe<ServerStatus>;
};
export type QuerydashboardArgs = {
id: Scalars['String'];
id: Scalars['String']['input'];
};
@@ -538,20 +540,20 @@ export enum RemoteAccessEventActionType {
export type RemoteAccessEventData = {
__typename?: 'RemoteAccessEventData';
apiKey: Scalars['String'];
apiKey: Scalars['String']['output'];
type: RemoteAccessEventActionType;
url?: Maybe<AccessUrl>;
};
export type RemoteAccessInput = {
apiKey: Scalars['String'];
apiKey: Scalars['String']['input'];
type: RemoteAccessEventActionType;
url?: InputMaybe<AccessUrlInput>;
};
export type RemoteGraphQLClientInput = {
apiKey: Scalars['String'];
body: Scalars['String'];
apiKey: Scalars['String']['input'];
body: Scalars['String']['input'];
};
export type RemoteGraphQLEvent = {
@@ -563,9 +565,9 @@ export type RemoteGraphQLEvent = {
export type RemoteGraphQLEventData = {
__typename?: 'RemoteGraphQLEventData';
/** Contains mutation / subscription / query data in the form of body: JSON, variables: JSON */
body: Scalars['String'];
body: Scalars['String']['output'];
/** sha256 hash of the body */
sha256: Scalars['String'];
sha256: Scalars['String']['output'];
type: RemoteGraphQLEventType;
};
@@ -578,39 +580,39 @@ export enum RemoteGraphQLEventType {
export type RemoteGraphQLServerInput = {
/** Body - contains an object containing data: (GQL response data) or errors: (GQL Errors) */
body: Scalars['String'];
body: Scalars['String']['input'];
/** sha256 hash of the body */
sha256: Scalars['String'];
sha256: Scalars['String']['input'];
type: RemoteGraphQLEventType;
};
export type Server = {
__typename?: 'Server';
apikey?: Maybe<Scalars['String']>;
guid?: Maybe<Scalars['String']>;
lanip?: Maybe<Scalars['String']>;
localurl?: Maybe<Scalars['String']>;
name?: Maybe<Scalars['String']>;
apikey?: Maybe<Scalars['String']['output']>;
guid?: Maybe<Scalars['String']['output']>;
lanip?: Maybe<Scalars['String']['output']>;
localurl?: Maybe<Scalars['String']['output']>;
name?: Maybe<Scalars['String']['output']>;
owner?: Maybe<ProfileModel>;
remoteurl?: Maybe<Scalars['String']>;
remoteurl?: Maybe<Scalars['String']['output']>;
status?: Maybe<ServerStatus>;
wanip?: Maybe<Scalars['String']>;
wanip?: Maybe<Scalars['String']['output']>;
};
/** Defines server fields that have a TTL on them, for example last ping */
export type ServerFieldsWithTtl = {
__typename?: 'ServerFieldsWithTtl';
lastPing?: Maybe<Scalars['String']>;
lastPing?: Maybe<Scalars['String']['output']>;
};
export type ServerModel = {
apikey: Scalars['String'];
guid: Scalars['String'];
lanip: Scalars['String'];
localurl: Scalars['String'];
name: Scalars['String'];
remoteurl: Scalars['String'];
wanip: Scalars['String'];
apikey: Scalars['String']['output'];
guid: Scalars['String']['output'];
lanip: Scalars['String']['output'];
localurl: Scalars['String']['output'];
name: Scalars['String']['output'];
remoteurl: Scalars['String']['output'];
wanip: Scalars['String']['output'];
};
export enum ServerStatus {
@@ -621,16 +623,16 @@ export enum ServerStatus {
export type Service = {
__typename?: 'Service';
name?: Maybe<Scalars['String']>;
online?: Maybe<Scalars['Boolean']>;
name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Uptime>;
version?: Maybe<Scalars['String']>;
version?: Maybe<Scalars['String']['output']>;
};
export type Subscription = {
__typename?: 'Subscription';
events?: Maybe<Array<Event>>;
remoteSubscription: Scalars['String'];
remoteSubscription: Scalars['String']['output'];
servers: Array<Server>;
};
@@ -641,19 +643,19 @@ export type SubscriptionremoteSubscriptionArgs = {
export type TwoFactorLocal = {
__typename?: 'TwoFactorLocal';
enabled?: Maybe<Scalars['Boolean']>;
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type TwoFactorRemote = {
__typename?: 'TwoFactorRemote';
enabled?: Maybe<Scalars['Boolean']>;
enabled?: Maybe<Scalars['Boolean']['output']>;
};
export type TwoFactorWithToken = {
__typename?: 'TwoFactorWithToken';
local?: Maybe<TwoFactorLocal>;
remote?: Maybe<TwoFactorRemote>;
token?: Maybe<Scalars['String']>;
token?: Maybe<Scalars['String']['output']>;
};
export type TwoFactorWithoutToken = {
@@ -678,7 +680,7 @@ export type UpdateEvent = {
export type UpdateEventData = {
__typename?: 'UpdateEventData';
apiKey: Scalars['String'];
apiKey: Scalars['String']['output'];
type: UpdateType;
};
@@ -689,7 +691,7 @@ export enum UpdateType {
export type Uptime = {
__typename?: 'Uptime';
timestamp?: Maybe<Scalars['String']>;
timestamp?: Maybe<Scalars['String']['output']>;
};
export type UserProfileModelWithServers = {
@@ -700,16 +702,16 @@ export type UserProfileModelWithServers = {
export type Vars = {
__typename?: 'Vars';
expireTime?: Maybe<Scalars['DateTime']>;
flashGuid?: Maybe<Scalars['String']>;
expireTime?: Maybe<Scalars['DateTime']['output']>;
flashGuid?: Maybe<Scalars['String']['output']>;
regState?: Maybe<RegistrationState>;
regTm2?: Maybe<Scalars['String']>;
regTy?: Maybe<Scalars['String']>;
regTm2?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<Scalars['String']['output']>;
};
export type updateDashboardMutationVariables = Exact<{
data: DashboardInput;
apiKey: Scalars['String'];
apiKey: Scalars['String']['input'];
}>;
@@ -717,7 +719,7 @@ export type updateDashboardMutation = { __typename?: 'Mutation', updateDashboard
export type sendNotificationMutationVariables = Exact<{
notification: NotificationInput;
apiKey: Scalars['String'];
apiKey: Scalars['String']['input'];
}>;
@@ -725,7 +727,7 @@ export type sendNotificationMutation = { __typename?: 'Mutation', sendNotificati
export type updateNetworkMutationVariables = Exact<{
data: NetworkInput;
apiKey: Scalars['String'];
apiKey: Scalars['String']['input'];
}>;

View File

@@ -1,6 +1,6 @@
/* eslint-disable */
import { z } from 'zod'
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, KeyType, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql'
import { AccessUrlInput, ArrayCapacityBytesInput, ArrayCapacityInput, ClientType, ConfigErrorState, DashboardAppsInput, DashboardArrayInput, DashboardCaseInput, DashboardConfigInput, DashboardDisplayInput, DashboardInput, DashboardOsInput, DashboardServiceInput, DashboardServiceUptimeInput, DashboardTwoFactorInput, DashboardTwoFactorLocalInput, DashboardTwoFactorRemoteInput, DashboardVarsInput, DashboardVersionsInput, DashboardVmsInput, EventType, Importance, NetworkInput, NotificationInput, NotificationStatus, PingEventSource, RegistrationState, RemoteAccessEventActionType, RemoteAccessInput, RemoteGraphQLClientInput, RemoteGraphQLEventType, RemoteGraphQLServerInput, ServerStatus, URL_TYPE, UpdateType } from '@app/graphql/generated/client/graphql'
type Properties<T> = Required<{
[K in keyof T]: z.ZodType<T[K], any, T[K]>;
@@ -20,8 +20,6 @@ export const EventTypeSchema = z.nativeEnum(EventType);
export const ImportanceSchema = z.nativeEnum(Importance);
export const KeyTypeSchema = z.nativeEnum(KeyType);
export const NotificationStatusSchema = z.nativeEnum(NotificationStatus);
export const PingEventSourceSchema = z.nativeEnum(PingEventSource);
@@ -42,16 +40,16 @@ export function AccessUrlInputSchema(): z.ZodObject<Properties<AccessUrlInput>>
return z.object({
ipv4: definedNonNullAnySchema.nullish(),
ipv6: definedNonNullAnySchema.nullish(),
name: definedNonNullAnySchema.nullish(),
name: z.string().nullish(),
type: URL_TYPESchema
})
}
export function ArrayCapacityBytesInputSchema(): z.ZodObject<Properties<ArrayCapacityBytesInput>> {
return z.object({
free: definedNonNullAnySchema.nullish(),
total: definedNonNullAnySchema.nullish(),
used: definedNonNullAnySchema.nullish()
free: z.number().nullish(),
total: z.number().nullish(),
used: z.number().nullish()
})
}
@@ -63,31 +61,31 @@ export function ArrayCapacityInputSchema(): z.ZodObject<Properties<ArrayCapacity
export function DashboardAppsInputSchema(): z.ZodObject<Properties<DashboardAppsInput>> {
return z.object({
installed: definedNonNullAnySchema,
started: definedNonNullAnySchema
installed: z.number(),
started: z.number()
})
}
export function DashboardArrayInputSchema(): z.ZodObject<Properties<DashboardArrayInput>> {
return z.object({
capacity: z.lazy(() => ArrayCapacityInputSchema()),
state: definedNonNullAnySchema
state: z.string()
})
}
export function DashboardCaseInputSchema(): z.ZodObject<Properties<DashboardCaseInput>> {
return z.object({
base64: definedNonNullAnySchema,
error: definedNonNullAnySchema.nullish(),
icon: definedNonNullAnySchema,
url: definedNonNullAnySchema
base64: z.string(),
error: z.string().nullish(),
icon: z.string(),
url: z.string()
})
}
export function DashboardConfigInputSchema(): z.ZodObject<Properties<DashboardConfigInput>> {
return z.object({
error: definedNonNullAnySchema.nullish(),
valid: definedNonNullAnySchema
error: z.string().nullish(),
valid: z.boolean()
})
}
@@ -114,23 +112,23 @@ export function DashboardInputSchema(): z.ZodObject<Properties<DashboardInput>>
export function DashboardOsInputSchema(): z.ZodObject<Properties<DashboardOsInput>> {
return z.object({
hostname: definedNonNullAnySchema,
uptime: definedNonNullAnySchema
hostname: z.string(),
uptime: z.string()
})
}
export function DashboardServiceInputSchema(): z.ZodObject<Properties<DashboardServiceInput>> {
return z.object({
name: definedNonNullAnySchema,
online: definedNonNullAnySchema,
name: z.string(),
online: z.boolean(),
uptime: z.lazy(() => DashboardServiceUptimeInputSchema().nullish()),
version: definedNonNullAnySchema
version: z.string()
})
}
export function DashboardServiceUptimeInputSchema(): z.ZodObject<Properties<DashboardServiceUptimeInput>> {
return z.object({
timestamp: definedNonNullAnySchema
timestamp: z.string()
})
}
@@ -143,34 +141,36 @@ export function DashboardTwoFactorInputSchema(): z.ZodObject<Properties<Dashboar
export function DashboardTwoFactorLocalInputSchema(): z.ZodObject<Properties<DashboardTwoFactorLocalInput>> {
return z.object({
enabled: definedNonNullAnySchema
enabled: z.boolean()
})
}
export function DashboardTwoFactorRemoteInputSchema(): z.ZodObject<Properties<DashboardTwoFactorRemoteInput>> {
return z.object({
enabled: definedNonNullAnySchema
enabled: z.boolean()
})
}
export function DashboardVarsInputSchema(): z.ZodObject<Properties<DashboardVarsInput>> {
return z.object({
flashGuid: definedNonNullAnySchema,
regState: definedNonNullAnySchema,
regTy: definedNonNullAnySchema
flashGuid: z.string(),
regState: z.string(),
regTy: z.string(),
serverDescription: z.string().nullish(),
serverName: z.string().nullish()
})
}
export function DashboardVersionsInputSchema(): z.ZodObject<Properties<DashboardVersionsInput>> {
return z.object({
unraid: definedNonNullAnySchema
unraid: z.string()
})
}
export function DashboardVmsInputSchema(): z.ZodObject<Properties<DashboardVmsInput>> {
return z.object({
installed: definedNonNullAnySchema,
started: definedNonNullAnySchema
installed: z.number(),
started: z.number()
})
}
@@ -182,17 +182,17 @@ export function NetworkInputSchema(): z.ZodObject<Properties<NetworkInput>> {
export function NotificationInputSchema(): z.ZodObject<Properties<NotificationInput>> {
return z.object({
description: definedNonNullAnySchema.nullish(),
description: z.string().nullish(),
importance: ImportanceSchema,
link: definedNonNullAnySchema.nullish(),
subject: definedNonNullAnySchema.nullish(),
title: definedNonNullAnySchema.nullish()
link: z.string().nullish(),
subject: z.string().nullish(),
title: z.string().nullish()
})
}
export function RemoteAccessInputSchema(): z.ZodObject<Properties<RemoteAccessInput>> {
return z.object({
apiKey: definedNonNullAnySchema,
apiKey: z.string(),
type: RemoteAccessEventActionTypeSchema,
url: z.lazy(() => AccessUrlInputSchema().nullish())
})
@@ -200,15 +200,15 @@ export function RemoteAccessInputSchema(): z.ZodObject<Properties<RemoteAccessIn
export function RemoteGraphQLClientInputSchema(): z.ZodObject<Properties<RemoteGraphQLClientInput>> {
return z.object({
apiKey: definedNonNullAnySchema,
body: definedNonNullAnySchema
apiKey: z.string(),
body: z.string()
})
}
export function RemoteGraphQLServerInputSchema(): z.ZodObject<Properties<RemoteGraphQLServerInput>> {
return z.object({
body: definedNonNullAnySchema,
sha256: definedNonNullAnySchema,
body: z.string(),
sha256: z.string(),
type: RemoteGraphQLEventTypeSchema
})
}

View File

@@ -1,7 +1,5 @@
import { FatalAppError } from '@app/core/errors/fatal-error';
import { graphqlLogger } from '@app/core/log';
import { modules } from '@app/core';
import { getters } from '@app/store';
export const getCoreModule = (moduleName: string) => {
if (!Object.keys(modules).includes(moduleName)) {
@@ -10,16 +8,3 @@ export const getCoreModule = (moduleName: string) => {
return modules[moduleName];
};
export const apiKeyToUser = async (apiKey: string) => {
try {
const config = getters.config();
if (apiKey === config.remote.apikey) return { id: -1, description: 'My servers service account', name: 'my_servers', role: 'my_servers' };
if (apiKey === config.upc.apikey) return { id: -1, description: 'UPC service account', name: 'upc', role: 'upc' };
if (apiKey === config.notifier.apikey) return { id: -1, description: 'Notifier service account', name: 'notifier', role: 'notifier' };
} catch (error: unknown) {
graphqlLogger.debug('Failed looking up API key with "%s"', (error as Error).message);
}
return { id: -1, description: 'A guest user', name: 'guest', role: 'guest' };
};

View File

@@ -1,6 +1,7 @@
import { ensurePermission } from '@app/core/utils/index';
import { NODE_ENV } from '@app/environment';
import { type MutationResolvers } from '@app/graphql/generated/api/types';
import {
type ConnectSignInInput,
} from '@app/graphql/generated/api/types';
import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types';
import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver';
import { getters, store } from '@app/store/index';
@@ -9,31 +10,26 @@ import { FileLoadStatus } from '@app/store/types';
import { GraphQLError } from 'graphql';
import { decodeJwt } from 'jose';
export const connectSignIn: MutationResolvers['connectSignIn'] = async (
_,
args,
context
) => {
ensurePermission(context.user, {
resource: 'connect',
possession: 'own',
action: 'update',
});
export const connectSignIn = async (
input: ConnectSignInInput
): Promise<boolean> => {
if (getters.emhttp().status === FileLoadStatus.LOADED) {
const result = NODE_ENV === 'development' ? API_KEY_STATUS.API_KEY_VALID : await validateApiKeyWithKeyServer({
apiKey: args.input.apiKey,
flashGuid: getters.emhttp().var.flashGuid,
});
const result =
NODE_ENV === 'development'
? API_KEY_STATUS.API_KEY_VALID
: await validateApiKeyWithKeyServer({
apiKey: input.apiKey,
flashGuid: getters.emhttp().var.flashGuid,
});
if (result !== API_KEY_STATUS.API_KEY_VALID) {
throw new GraphQLError(
`Validating API Key Failed with Error: ${result}`
);
}
const userInfo = args.input.idToken
? decodeJwt(args.input.idToken)
: args.input.userInfo ?? null;
const userInfo = input.idToken
? decodeJwt(input.idToken)
: input.userInfo ?? null;
if (
!userInfo ||
!userInfo.preferred_username ||
@@ -47,10 +43,11 @@ export const connectSignIn: MutationResolvers['connectSignIn'] = async (
// @TODO once we deprecate old sign in method, switch this to do all validation requests
await store.dispatch(
loginUser({
avatar: typeof userInfo.avatar === 'string' ? userInfo.avatar : '',
avatar:
typeof userInfo.avatar === 'string' ? userInfo.avatar : '',
username: userInfo.preferred_username,
email: userInfo.email,
apikey: args.input.apiKey,
apikey: input.apiKey,
})
);
return true;

View File

@@ -1,19 +0,0 @@
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { type MutationResolvers } from '@app/graphql/generated/api/types';
import { store } from '@app/store/index';
import { logoutUser } from '@app/store/modules/config';
export const connectSignOut: MutationResolvers['connectSignOut'] = async (
_,
__,
context
) => {
ensurePermission(context.user, {
resource: 'connect',
possession: 'own',
action: 'update',
});
await store.dispatch(logoutUser({ reason: 'Manual Sign Out With API' }));
return true;
};

View File

@@ -1,10 +0,0 @@
import { type Resolvers } from '@app/graphql/generated/api/types';
import { sendNotification } from './notifications';
import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in';
import { connectSignOut } from '@app/graphql/resolvers/mutation/connect/connect-sign-out';
export const Mutation: Resolvers['Mutation'] = {
sendNotification,
connectSignIn,
connectSignOut
};

View File

@@ -1,23 +0,0 @@
/*!
* Copyright 2021 Lime Technology Inc. All rights reserved.
* Written by: Alexis Tyler
*/
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { ConfigErrorState, type QueryResolvers } from '@app/graphql/generated/api/types';
import { getters } from '@app/store';
export const config: QueryResolvers['config'] = async (_, __, context) => {
ensurePermission(context.user, {
resource: 'config',
action: 'read',
possession: 'any',
});
const emhttp = getters.emhttp();
return {
valid: emhttp.var.configValid,
error: emhttp.var.configValid ? null : ConfigErrorState[emhttp.var.configState] ?? ConfigErrorState.UNKNOWN_ERROR
};
};

View File

@@ -1,16 +0,0 @@
import { getDockerContainers } from "@app/core/modules/index";
import { ensurePermission } from "@app/core/utils/permissions/ensure-permission";
import { type QueryResolvers } from "@app/graphql/generated/api/types";
export const dockerContainersResolver: QueryResolvers['dockerContainers'] = async (_, __, context) => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'docker/container',
action: 'read',
possession: 'any',
});
return getDockerContainers();
}

View File

@@ -1,40 +0,0 @@
import { getArray } from '@app/core/modules/get-array';
import { type QueryResolvers } from '@app/graphql/generated/api/types';
import cloud from '@app/graphql/resolvers/query/cloud';
import { config } from '@app/graphql/resolvers/query/config';
import crashReportingEnabled from '@app/graphql/resolvers/query/crash-reporting-enabled';
import { disksResolver } from '@app/graphql/resolvers/query/disks';
import display from '@app/graphql/resolvers/query/display';
import { dockerContainersResolver } from '@app/graphql/resolvers/query/docker';
import flash from '@app/graphql/resolvers/query/flash';
import { notificationsResolver } from '@app/graphql/resolvers/query/notifications';
import online from '@app/graphql/resolvers/query/online';
import owner from '@app/graphql/resolvers/query/owner';
import { registration } from '@app/graphql/resolvers/query/registration';
import { server } from '@app/graphql/resolvers/query/server';
import { servers } from '@app/graphql/resolvers/query/servers';
import twoFactor from '@app/graphql/resolvers/query/two-factor';
import { vmsResolver } from '@app/graphql/resolvers/query/vms';
export const Query: QueryResolvers = {
array: getArray,
cloud,
config,
crashReportingEnabled,
disks: disksResolver,
dockerContainers: dockerContainersResolver,
display,
flash,
notifications: notificationsResolver,
online,
owner,
registration,
server,
servers,
twoFactor,
vms: vmsResolver,
info() {
// Returns an empty object because the subfield resolvers live at the root (allows for partial fetching)
return {};
},
};

View File

@@ -1,11 +1,9 @@
import {
baseboard,
cpu,
cpuFlags,
mem,
memLayout,
osInfo,
system,
versions,
} from 'systeminformation';
import { docker } from '@app/core/utils/clients/docker';
@@ -16,13 +14,10 @@ import {
type Display,
type Theme,
type Temperature,
type Baseboard,
type Versions,
type InfoMemory,
type MemoryLayout,
type System,
type Devices,
type InfoResolvers,
type Gpu,
} from '@app/graphql/generated/api/types';
import { getters } from '@app/store';
@@ -33,7 +28,6 @@ import toBytes from 'bytes';
import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version';
import { AppError } from '@app/core/errors/app-error';
import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
import { getMachineId } from '@app/core/utils/misc/get-machine-id';
import { execaCommandSync, execa } from 'execa';
import { pathExists } from 'path-exists';
import { filter as asyncFilter } from 'p-iteration';
@@ -58,19 +52,22 @@ export const generateApps = async (): Promise<InfoApps> => {
return { installed, started };
};
const generateOs = async (): Promise<InfoOs> => {
export const generateOs = async (): Promise<InfoOs> => {
const os = await osInfo();
return {
...os,
hostname: getters.emhttp().var.name,
uptime: bootTimestamp.toISOString(),
};
};
const generateCpu = async (): Promise<InfoCpu> => {
export const generateCpu = async (): Promise<InfoCpu> => {
const { cores, physicalCores, speedMin, speedMax, stepping, ...rest } =
await cpu();
const flags = await cpuFlags().then((flags) => flags.split(' '));
const flags = await cpuFlags()
.then((flags) => flags.split(' '))
.catch(() => []);
return {
...rest,
@@ -84,7 +81,7 @@ const generateCpu = async (): Promise<InfoCpu> => {
};
};
const generateDisplay = async (): Promise<Display> => {
export const generateDisplay = async (): Promise<Display> => {
const filePath = getters.paths()['dynamix-config'];
const state = loadState<DynamixConfig>(filePath);
if (!state) {
@@ -110,9 +107,7 @@ const generateDisplay = async (): Promise<Display> => {
};
};
const generateBaseboard = async (): Promise<Baseboard> => baseboard();
const generateVersions = async (): Promise<Versions> => {
export const generateVersions = async (): Promise<Versions> => {
const unraid = await getUnraidVersion();
const softwareVersions = await versions();
@@ -122,10 +117,10 @@ const generateVersions = async (): Promise<Versions> => {
};
};
const generateMemory = async (): Promise<InfoMemory> => {
export const generateMemory = async (): Promise<InfoMemory> => {
const layout = await memLayout().then((dims) =>
dims.map((dim) => dim as MemoryLayout)
);
).catch(() => []);
const info = await mem();
let max = info.total;
@@ -175,7 +170,7 @@ const generateMemory = async (): Promise<InfoMemory> => {
};
};
const generateDevices = async (): Promise<Devices> => {
export const generateDevices = async (): Promise<Devices> => {
/**
* Set device class to device.
* @param device The device to modify.
@@ -277,24 +272,24 @@ const generateDevices = async (): Promise<Devices> => {
* @ignore
* @private
*/
const systemGPUDevices: Promise<Gpu[]> = systemPciDevices().then(
(devices) => {
return devices.filter(
(device) => device.class === 'vga' && !device.allowed
).map(entry => {
const gpu: Gpu = {
blacklisted: entry.allowed,
class: entry.class,
id: entry.id,
productid: entry.product,
typeid: entry.typeid,
type: entry.manufacturer,
vendorname: entry.vendorname
}
return gpu;
});
}
).catch(() => []);
const systemGPUDevices: Promise<Gpu[]> = systemPciDevices()
.then((devices) => {
return devices
.filter((device) => device.class === 'vga' && !device.allowed)
.map((entry) => {
const gpu: Gpu = {
blacklisted: entry.allowed,
class: entry.class,
id: entry.id,
productid: entry.product,
typeid: entry.typeid,
type: entry.manufacturer,
vendorname: entry.vendorname,
};
return gpu;
});
})
.catch(() => []);
/**
* System usb devices.
@@ -422,13 +417,15 @@ const generateDevices = async (): Promise<Devices> => {
}) ?? [];
// Get all usb devices
const usbDevices = await execa('lsusb').then(async ({ stdout }) =>
parseUsbDevices(stdout)
.map(parseDevice)
.filter(filterBootDrive)
.filter(filterUsbHubs)
.map(sanitizeVendorName)
);
const usbDevices = await execa('lsusb')
.then(async ({ stdout }) =>
parseUsbDevices(stdout)
.map(parseDevice)
.filter(filterBootDrive)
.filter(filterUsbHubs)
.map(sanitizeVendorName)
)
.catch(() => []);
return usbDevices;
} catch (error: unknown) {
@@ -445,20 +442,3 @@ const generateDevices = async (): Promise<Devices> => {
usb: await getSystemUSBDevices(),
};
};
const generateMachineId = async (): Promise<string> => getMachineId();
const generateSystem = async (): Promise<System> => system();
export const infoSubResolvers: InfoResolvers = {
apps: async () => generateApps(),
baseboard: async () => generateBaseboard(),
cpu: async () => generateCpu(),
devices: async () => generateDevices(),
display: async () => generateDisplay(),
machineId: async () => generateMachineId(),
memory: async () => generateMemory(),
os: async () => generateOs(),
system: async () => generateSystem(),
versions: async () => generateVersions(),
};

View File

@@ -1,42 +0,0 @@
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { type QueryResolvers } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index';
export const notificationsResolver: QueryResolvers['notifications'] = async (
_,
{ filter: { offset, limit, importance, type } },
context
) => {
ensurePermission(context.user, {
possession: 'any',
resource: 'notifications',
action: 'read',
});
if (limit > 50) {
throw new Error('Limit must be less than 50');
}
return Object.values(getters.notifications().notifications)
.filter((notification) => {
if (
importance &&
importance !== notification.importance
) {
return false;
}
if (type && type !== notification.type) {
return false;
}
return true;
})
.sort(
(a, b) =>
new Date(b.timestamp ?? 0).getTime() -
new Date(a.timestamp ?? 0).getTime()
)
.slice(
offset,
limit + offset
);
};

View File

@@ -1 +0,0 @@
export default () => true;

View File

@@ -1,40 +0,0 @@
/*!
* Copyright 2021 Lime Technology Inc. All rights reserved.
* Written by: Alexis Tyler
*/
import { getKeyFile } from '@app/core/utils/misc/get-key-file';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { type Registration, type QueryResolvers } from '@app/graphql/generated/api/types';
import { getters } from '@app/store';
import { FileLoadStatus } from '@app/store/types';
export const registration: QueryResolvers['registration'] = async (_, __, context) => {
ensurePermission(context.user, {
resource: 'registration',
action: 'read',
possession: 'any',
});
const emhttp = getters.emhttp();
if (emhttp.status !== FileLoadStatus.LOADED || !emhttp.var?.regTy) {
return null;
}
const isTrial = emhttp.var.regTy?.toLowerCase() === 'trial';
const isExpired = emhttp.var.regTy.includes('expired');
const registration: Registration = {
guid: emhttp.var.regGuid,
type: emhttp.var.regTy,
state: emhttp.var.regState,
// Based on https://github.com/unraid/dynamix.unraid.net/blob/c565217fa8b2acf23943dc5c22a12d526cdf70a1/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php#L64
expiration:
(1_000 * (isTrial || isExpired ? Number(emhttp.var.regTm2) : 0)).toString(),
keyFile: {
location: emhttp.var.regFile,
contents: await getKeyFile(),
},
};
return registration;
};

View File

@@ -1,16 +0,0 @@
import { getServers } from '@app/graphql/schema/utils';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { type QueryResolvers } from '@app/graphql/generated/api/types';
export const server: QueryResolvers['server'] = async (_: unknown, { name }, context) => {
ensurePermission(context.user, {
resource: 'servers',
action: 'read',
possession: 'any',
});
const servers = getServers();
// Single server
return servers.find(server => server.name === name) ?? undefined;
};

View File

@@ -1,29 +0,0 @@
import { getServers } from '@app/graphql/schema/utils';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { ServerStatus, type Resolvers } from '../../generated/api/types';
export const servers: NonNullable<Resolvers['Query']>['servers'] = async (_, __, context) => {
ensurePermission(context.user, {
resource: 'servers',
action: 'read',
possession: 'any',
});
// All servers
const servers = getServers().map(server => ({
...server,
apikey: server.apikey ?? '',
guid: server.guid ?? '',
lanip: server.lanip ?? '',
localurl: server.localurl ?? '',
wanip: server.wanip ?? '',
name: server.name ?? '',
owner: {
...server.owner,
username: server.owner?.username ?? ''
},
remoteurl: server.remoteurl ?? '',
status: server.status ?? ServerStatus.OFFLINE
}))
return servers;
};

View File

@@ -1 +0,0 @@
export const vmsResolver = () => ({});

View File

@@ -1,28 +0,0 @@
import { DateTimeResolver, JSONResolver, PortResolver, UUIDResolver } from 'graphql-scalars';
import { Query } from '@app/graphql/resolvers/query';
import { Mutation } from '@app/graphql/resolvers/mutation';
import { Subscription } from '@app/graphql/resolvers/subscription';
import { UserAccount } from '@app/graphql/resolvers/user-account';
import { type Resolvers } from '../generated/api/types';
import { infoSubResolvers } from './query/info';
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long';
import { domainResolver } from '@app/core/modules/index';
export const resolvers: Resolvers = {
JSON: JSONResolver,
Long: GraphQLLong,
UUID: UUIDResolver,
DateTime: DateTimeResolver,
Port: PortResolver,
Query,
Mutation,
Subscription,
UserAccount,
Info: {
...infoSubResolvers,
},
Vms: {
domain: domainResolver,
},
};

View File

@@ -1,6 +1,6 @@
import { dashboardLogger } from '@app/core/log';
import { generateData } from '@app/common/dashboard/generate-data';
import { pubsub } from '@app/core/pubsub';
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';
@@ -63,12 +63,10 @@ export const publishToDashboard = async () => {
store.dispatch(saveDataPacket({ lastDataPacket: dataPacket }));
// Publish the updated data
dashboardLogger.addContext('update', dataPacket);
dashboardLogger.trace('Publishing update');
dashboardLogger.removeContext('update');
dashboardLogger.trace({ dataPacket } , 'Publishing update');
// Update local clients
await pubsub.publish('dashboard', {
await pubsub.publish(PUBSUB_CHANNEL.DASHBOARD, {
dashboard: dataPacket,
});
if (dataPacket) {

View File

@@ -1,69 +0,0 @@
import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { type Resolvers } from '@app/graphql/generated/api/types';
import { createSubscription } from '@app/graphql/schema/utils';
export const Subscription: Resolvers['Subscription'] = {
display: {
...createSubscription('display'),
},
apikeys: {
// Not sure how we're going to secure this
// ...createSubscription('apikeys')
},
config: {
...createSubscription('config'),
},
array: {
...createSubscription('array'),
},
dockerContainers: {
...createSubscription('docker/container'),
},
dockerNetworks: {
...createSubscription('docker/network'),
},
notificationAdded: {
subscribe: (_parent, _args, context) => {
ensurePermission(context.user, {
possession: 'any',
resource: 'notifications',
action: 'read',
});
return {
[Symbol.asyncIterator]: () =>
pubsub.asyncIterator(PUBSUB_CHANNEL.NOTIFICATION),
};
},
},
info: {
...createSubscription('info'),
},
servers: {
...createSubscription('servers'),
},
shares: {
...createSubscription('shares'),
},
unassignedDevices: {
...createSubscription('devices/unassigned'),
},
users: {
...createSubscription('users'),
},
vars: {
...createSubscription('vars'),
},
vms: {
...createSubscription('vms'),
},
registration: {
...createSubscription('registration'),
},
online: {
...createSubscription('online'),
},
owner: {
...createSubscription('owner'),
},
};

View File

@@ -1,60 +1,82 @@
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 NetworkInput, URL_TYPE, type AccessUrlInput } from '@app/graphql/generated/client/graphql';
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 { AccessUrlInputSchema, NetworkInputSchema } from '@app/graphql/generated/client/validators';
import {
AccessUrlInputSchema,
NetworkInputSchema,
} from '@app/graphql/generated/client/validators';
import { ZodError } from 'zod';
interface UrlForFieldInput {
url: string;
port?: number;
portSsl?: number;
url: string;
port?: number;
portSsl?: number;
}
interface UrlForFieldInputSecure extends UrlForFieldInput {
url: string;
portSsl: number;
url: string;
portSsl: number;
}
interface UrlForFieldInputInsecure extends UrlForFieldInput {
url: string;
port: number;
url: string;
port: number;
}
export const getUrlForField = ({ url, port, portSsl }: UrlForFieldInputInsecure | UrlForFieldInputSecure) => {
let portToUse = '';
let httpMode = 'https://';
export const getUrlForField = ({
url,
port,
portSsl,
}: UrlForFieldInputInsecure | UrlForFieldInputSecure) => {
let portToUse = '';
let httpMode = 'https://';
if (!url || url === '') {
throw new Error('No URL Provided');
}
if (!url || url === '') {
throw new Error('No URL Provided');
}
if (port) {
portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://';
} else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://';
} else {
throw new Error(`No ports specified for URL: ${url}`);
}
if (port) {
portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://';
} else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://';
} else {
throw new Error(`No ports specified for URL: ${url}`);
}
const urlString = `${httpMode}${url}${portToUse}`;
const urlString = `${httpMode}${url}${portToUse}`;
try {
return new URL(urlString);
} catch (error: unknown) {
throw new Error(`Failed to parse URL: ${urlString}`);
}
try {
return new URL(urlString);
} catch (error: unknown) {
throw new Error(`Failed to parse URL: ${urlString}`);
}
};
const fieldIsFqdn = (field: keyof Nginx) => field?.toLowerCase().includes('fqdn');
const fieldIsFqdn = (field: keyof Nginx) =>
field?.toLowerCase().includes('fqdn');
export type NginxUrlFields = Extract<keyof Nginx, 'lanIp' | 'lanIp6' | 'lanName' | 'lanMdns' | 'lanFqdn' | 'lanFqdn6' | 'wanFqdn' | 'wanFqdn6'>;
export type NginxUrlFields = Extract<
keyof Nginx,
| 'lanIp'
| 'lanIp6'
| 'lanName'
| 'lanMdns'
| 'lanFqdn'
| 'lanFqdn6'
| 'wanFqdn'
| 'wanFqdn6'
>;
/**
*
@@ -63,254 +85,307 @@ export type NginxUrlFields = Extract<keyof Nginx, 'lanIp' | 'lanIp6' | 'lanName'
* @returns a URL, created from the combination of inputs
* @throws Error when the URL cannot be created or the URL is invalid
*/
export const getUrlForServer = ({ nginx, field }: { nginx: Nginx; field: NginxUrlFields }): URL => {
if (nginx[field]) {
if (fieldIsFqdn(field)) {
return getUrlForField({ url: nginx[field], portSsl: nginx.httpsPort });
}
export const getUrlForServer = ({
nginx,
field,
}: {
nginx: Nginx;
field: NginxUrlFields;
}): URL => {
if (nginx[field]) {
if (fieldIsFqdn(field)) {
return getUrlForField({
url: nginx[field],
portSsl: nginx.httpsPort,
});
}
if (!nginx.sslEnabled) {// Use SSL = no
return getUrlForField({ url: nginx[field], port: nginx.httpPort });
}
if (!nginx.sslEnabled) {
// Use SSL = no
return getUrlForField({ url: nginx[field], port: nginx.httpPort });
}
if (nginx.sslMode === 'yes') {
return getUrlForField({ url: nginx[field], portSsl: nginx.httpsPort });
}
if (nginx.sslMode === 'yes') {
return getUrlForField({
url: nginx[field],
portSsl: nginx.httpsPort,
});
}
if (nginx.sslMode === 'auto') {
throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode auto`);
}
}
if (nginx.sslMode === 'auto') {
throw new Error(
`Cannot get IP Based URL for field: "${field}" SSL mode auto`
);
}
}
throw new Error(`IP URL Resolver: Could not resolve any access URL for field: "${field}", is FQDN?: ${fieldIsFqdn(field)}`);
throw new Error(
`IP URL Resolver: Could not resolve any access URL for field: "${field}", is FQDN?: ${fieldIsFqdn(
field
)}`
);
};
// eslint-disable-next-line complexity
export const getServerIps = (state: RootState = store.getState()): { urls: AccessUrlInput[]; errors: Error[] } => {
const { nginx } = state.emhttp;
const { remote: { wanport } } = state.config;
if (!nginx || Object.keys(nginx).length === 0) {
return { urls: [], errors: [new Error('Nginx Not Loaded')] };
}
export const getServerIps = (
state: RootState = store.getState()
): { urls: AccessUrlInput[]; errors: Error[] } => {
const { nginx } = state.emhttp;
const {
remote: { wanport },
} = state.config;
if (!nginx || Object.keys(nginx).length === 0) {
return { urls: [], errors: [new Error('Nginx Not Loaded')] };
}
const errors: Error[] = [];
const urls: AccessUrlInput[] = [];
const errors: Error[] = [];
const urls: AccessUrlInput[] = [];
try {
// Default URL
const defaultUrl = new URL(nginx.defaultUrl);
urls.push({
name: 'Default',
type: URL_TYPE.DEFAULT,
ipv4: defaultUrl,
ipv6: defaultUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Default URL
const defaultUrl = new URL(nginx.defaultUrl);
urls.push({
name: 'Default',
type: URL_TYPE.DEFAULT,
ipv4: defaultUrl,
ipv6: defaultUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan IP URL
const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' });
urls.push({
name: 'LAN IPv4',
type: URL_TYPE.LAN,
ipv4: lanIp4Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan IP URL
const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' });
urls.push({
name: 'LAN IPv4',
type: URL_TYPE.LAN,
ipv4: lanIp4Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan IP6 URL
const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' });
urls.push({
name: 'LAN IPv6',
type: URL_TYPE.LAN,
ipv4: lanIp6Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan IP6 URL
const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' });
urls.push({
name: 'LAN IPv6',
type: URL_TYPE.LAN,
ipv4: lanIp6Url,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan Name URL
const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' });
urls.push({
name: 'LAN Name',
type: URL_TYPE.MDNS,
ipv4: lanNameUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan Name URL
const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' });
urls.push({
name: 'LAN Name',
type: URL_TYPE.MDNS,
ipv4: lanNameUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan MDNS URL
const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' });
urls.push({
name: 'LAN MDNS',
type: URL_TYPE.MDNS,
ipv4: lanMdnsUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
try {
// Lan MDNS URL
const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' });
urls.push({
name: 'LAN MDNS',
type: URL_TYPE.MDNS,
ipv4: lanMdnsUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
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 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 {
// 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 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);
}
}
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) {
try {
// WG FQDN URL
const wgFqdnUrl = getUrlForField({ url: wgFqdn.fqdn, portSsl: nginx.httpsPort });
urls.push({
name: `WG FQDN ${wgFqdn.id}`,
type: URL_TYPE.WIREGUARD,
ipv4: wgFqdnUrl,
});
} 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) {
try {
// WG FQDN URL
const wgFqdnUrl = getUrlForField({
url: wgFqdn.fqdn,
portSsl: nginx.httpsPort,
});
urls.push({
name: `WG FQDN ${wgFqdn.id}`,
type: URL_TYPE.WIREGUARD,
ipv4: wgFqdnUrl,
});
} catch (error: unknown) {
if (error instanceof Error) {
errors.push(error);
} else {
logger.warn('Uncaught error in network resolver', error);
}
}
}
const safeUrls = urls.map((url) => AccessUrlInputSchema().safeParse(url)).reduce<AccessUrlInput[]>((acc, curr) => {
if (curr.success) {
acc.push(curr.data)
} else {
errors.push(curr.error)
}
return acc;
}, []);
const safeUrls = urls
.map((url) => AccessUrlInputSchema().safeParse(url))
.reduce<AccessUrlInput[]>((acc, curr) => {
if (curr.success) {
acc.push(curr.data);
} else {
errors.push(curr.error);
}
return acc;
}, []);
return { urls: safeUrls, errors };
return { urls: safeUrls, errors };
};
export const publishNetwork = async () => {
try {
const client = GraphQLClient.getInstance();
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.addContext('data', validatedNetwork);
dashboardLogger.info('Sending data packet for network');
dashboardLogger.removeContext('data');
const result = await client.mutate({
mutation: SEND_NETWORK_MUTATION,
variables: {
apiKey,
data: validatedNetwork,
},
});
dashboardLogger.addContext('sendNetworkResult', result);
dashboardLogger.debug('Sent network mutation with %s urls', datapacket.urls.length);
dashboardLogger.removeContext('sendNetworkResult');
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);
}
}
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

@@ -13,9 +13,7 @@ import { getters } from '@app/store/index';
export const executeRemoteGraphQLQuery = async (
data: RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData']
) => {
remoteQueryLogger.addContext('data', data);
remoteQueryLogger.debug('Executing remote query');
remoteQueryLogger.removeContext('data');
remoteQueryLogger.debug({ query: data }, 'Executing remote query');
const client = GraphQLClient.getInstance();
const apiKey = getters.config().remote.apikey;
const originalBody = data.body;
@@ -25,18 +23,14 @@ export const executeRemoteGraphQLQuery = async (
upcApiKey: apiKey
});
if (ENVIRONMENT === 'development') {
remoteQueryLogger.addContext('query', parsedQuery.query);
remoteQueryLogger.debug('[DEVONLY] Running query');
remoteQueryLogger.removeContext('query');
remoteQueryLogger.debug({ query: parsedQuery.query }, '[DEVONLY] Running query');
}
const localResult = await localClient.query({
query: parsedQuery.query,
variables: parsedQuery.variables,
});
if (localResult.data) {
remoteQueryLogger.addContext('data', localResult.data);
remoteQueryLogger.trace('Got data from remoteQuery request', data.sha256);
remoteQueryLogger.removeContext('data')
remoteQueryLogger.trace({ data: localResult.data }, 'Got data from remoteQuery request', data.sha256);
await client?.mutate({
mutation: SEND_REMOTE_QUERY_RESPONSE,
@@ -77,8 +71,6 @@ export const executeRemoteGraphQLQuery = async (
} catch (error) {
remoteQueryLogger.warn('Could not respond %o', error);
}
remoteQueryLogger.addContext('error', err);
remoteQueryLogger.error('Error executing remote query %s', err instanceof Error ? err.message: 'Unknown Error');
remoteQueryLogger.removeContext('error');
}
};

View File

@@ -1,6 +0,0 @@
export const UserAccount = {
__resolveType(obj: Record<string, unknown>) {
// Only a user has a password field, the current user aka "me" doesn't.
return obj.password ? 'User' : 'Me';
},
};

10
api/src/graphql/schema.ts Normal file
View File

@@ -0,0 +1,10 @@
import { makeExecutableSchema } from '@graphql-tools/schema';
import { resolvers } from '@app/graphql/resolvers/resolvers';
import { typeDefs } from '@app/graphql/schema/index';
const baseSchema = makeExecutableSchema({
typeDefs: typeDefs,
resolvers,
});
export const schema = (baseSchema);

View File

@@ -0,0 +1,42 @@
input authenticateInput {
password: String!
}
input addApiKeyInput {
name: String
key: String
userId: String
}
input updateApikeyInput {
description: String
expiresAt: Long!
}
type Query {
"""Get all API keys"""
apiKeys: [ApiKey]
}
type Mutation {
"""Get an existing API key"""
getApiKey(name: String!, input: authenticateInput): ApiKey
"""Create a new API key"""
addApikey(name: String!, input: updateApikeyInput): ApiKey
"""Update an existing API key"""
updateApikey(name: String!, input: updateApikeyInput): ApiKey
}
type Subscription {
apikeys: [ApiKey]
}
type ApiKey {
name: String!
key: String!
description: String
scopes: JSON!
expiresAt: Long!
}

View File

@@ -5,14 +5,14 @@ type Query {
type Mutation {
"""Start array"""
startArray: Array @func(module: "updateArray", data: { state: "start" })
startArray: Array
"""Stop array"""
stopArray: Array @func(module: "updateArray", data: { state: "stop" })
stopArray: Array
"""Add new disk to array"""
addDiskToArray(input: arrayDiskInput): Array @func(module: "addDiskToArray")
addDiskToArray(input: arrayDiskInput): Array
"""Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error."""
removeDiskFromArray(input: arrayDiskInput): Array @func(module: "removeDiskFromArray")
removeDiskFromArray(input: arrayDiskInput): Array
mountArrayDisk(id: ID!): Disk
unmountArrayDisk(id: ID!): Disk

View File

@@ -0,0 +1,26 @@
type Query {
parityHistory: [ParityCheck]
}
type Mutation {
"""Start parity check"""
startParityCheck(correct: Boolean): JSON
"""Pause parity check"""
pauseParityCheck: JSON
"""Resume parity check"""
resumeParityCheck: JSON
"""Cancel parity check"""
cancelParityCheck: JSON
}
type Subscription {
parityHistory: ParityCheck!
}
type ParityCheck {
date: String!
duration: Int!
speed: String!
status: String!
errors: String!
}

View File

@@ -0,0 +1,28 @@
scalar JSON
scalar Long
scalar UUID
scalar DateTime
scalar Port
type Welcome {
message: String!
}
type Query {
# This should always be available even for guest users
online: Boolean
info: Info
}
type Mutation {
login(username: String!, password: String!): String
sendNotification(notification: NotificationInput!): Notification
shutdown: String
reboot: String
}
type Subscription {
ping: String!
info: Info!
online: Boolean!
}

View File

@@ -0,0 +1,66 @@
type Query {
"""Single disk"""
disk(id: ID!): Disk
"""Mulitiple disks"""
disks: [Disk]!
}
type Disk {
# /dev/sdb
device: String!
# SSD
type: String!
# Samsung_SSD_860_QVO_1TB
name: String!
# Samsung
vendor: String!
# 1000204886016
size: Long!
# -1
bytesPerSector: Long!
# -1
totalCylinders: Long!
# -1
totalHeads: Long!
# -1
totalSectors: Long!
# -1
totalTracks: Long!
# -1
tracksPerCylinder: Long!
# -1
sectorsPerTrack: Long!
# 1B6Q
firmwareRevision: String!
# S4CZNF0M807232N
serialNum: String!
interfaceType: DiskInterfaceType!
smartStatus: DiskSmartStatus!
temperature: Long!
partitions: [DiskPartition!]
}
type DiskPartition {
name: String!
fsType: DiskFsType!
size: Long!
}
enum DiskFsType {
xfs
btrfs
vfat
zfs
}
enum DiskInterfaceType {
SAS
SATA
USB
PCIe
UNKNOWN
}
enum DiskSmartStatus {
OK
UNKNOWN
}

View File

@@ -0,0 +1,29 @@
type Query {
"""Docker network"""
dockerNetwork(id: ID!): DockerNetwork!
"""All Docker networks"""
dockerNetworks(all: Boolean): [DockerNetwork]!
}
type Subscription {
dockerNetwork(id: ID!): DockerNetwork!
dockerNetworks: [DockerNetwork]!
}
type DockerNetwork {
name: String
id: ID
created: String
scope: String
driver: String
enableIPv6: Boolean!
ipam: JSON
internal: Boolean!
attachable: Boolean!
ingress: Boolean!
configFrom: JSON
configOnly: Boolean!
containers: JSON
options: JSON
labels: JSON
}

View File

@@ -0,0 +1,21 @@
type Query {
registration: Registration
}
type Subscription {
registration: Registration!
}
type KeyFile {
location: String
contents: String
}
type Registration {
guid: String
type: registrationType
keyFile: KeyFile
state: RegistrationState
expiration: String
updateExpiration: String
}

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