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

View File

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

View File

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

View File

@@ -24,15 +24,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
persist-credentials: true persist-credentials: true
- uses: docker/setup-buildx-action@v2 - uses: docker/setup-buildx-action@v3
with: with:
# network=host driver-opt needed to push to local registry # network=host driver-opt needed to push to local registry
driver-opts: network=host driver-opts: network=host
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: api context: api
target: builder target: builder
@@ -57,16 +57,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
persist-credentials: true persist-credentials: true
- uses: docker/setup-buildx-action@v2 - uses: docker/setup-buildx-action@v3
with: with:
# network=host driver-opt needed to push to local registry # network=host driver-opt needed to push to local registry
driver-opts: network=host driver-opts: network=host
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: api context: api
target: builder target: builder
@@ -97,16 +97,16 @@ jobs:
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
persist-credentials: true persist-credentials: true
- uses: docker/setup-buildx-action@v2 - uses: docker/setup-buildx-action@v3
with: with:
# network=host driver-opt needed to push to local registry # network=host driver-opt needed to push to local registry
driver-opts: network=host driver-opts: network=host
- name: Build and push - name: Build and push
uses: docker/build-push-action@v4 uses: docker/build-push-action@v5
with: with:
context: api context: api
target: builder target: builder
@@ -127,7 +127,7 @@ jobs:
echo "::set-output name=API_SHA256::${API_SHA256}" echo "::set-output name=API_SHA256::${API_SHA256}"
- name: Upload tgz to Github artifacts - name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: unraid-api name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz path: ${{ github.workspace }}/api/deploy/release/*.tgz
@@ -155,7 +155,7 @@ jobs:
with: with:
timezoneLinux: "America/Los_Angeles" timezoneLinux: "America/Los_Angeles"
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Build Plugin - name: Build Plugin
run: | run: |
cd source/dynamix.unraid.net cd source/dynamix.unraid.net
@@ -173,7 +173,7 @@ jobs:
# escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"${RELEASE_NOTES}") # 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" # 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 - name: Upload binary txz and plg to Github artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: connect-files name: connect-files
path: | 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 .next
# Visual Studio Code workspace # Visual Studio Code workspace
.vscode/* .vscode/sftp.json
!.vscode/extensions.json
# OSX # OSX
.DS_Store .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": { "editor.codeActionsOnSave": {
"source.fixAll": false, "source.fixAll": "never",
"source.fixAll.eslint": true "source.fixAll.eslint": "explicit"
}, },
"workbench.colorCustomizations": { "workbench.colorCustomizations": {
"activityBar.activeBackground": "#78797d", "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. 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) ### [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 # 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 # Install build tools and dependencies
RUN apk add --no-cache \ RUN apt-get update -y && apt-get install -y \
bash \ bash \
# Real PS Command (needed for some dependencies) # Real PS Command (needed for some dependencies)
procps \ procps \
alpine-sdk \
python3 \ python3 \
libvirt-dev \ libvirt-dev \
jq \ jq \
zstd zstd \
git \
RUN mkdir /var/log/unraid-api/ build-essential
WORKDIR /app WORKDIR /app
@@ -33,7 +32,7 @@ COPY package.json package-lock.json ./
RUN npm i -g pkg zx RUN npm i -g pkg zx
# Install deps # Install deps
RUN npm ci RUN npm i
EXPOSE 4000 EXPOSE 4000

View File

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

View File

@@ -1,5 +1,6 @@
[api] [api]
version="3.1.1+8efc0992" version="3.4.0"
extraOrigins="https://google.com,https://test.com"
[local] [local]
[notifier] [notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5" apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
@@ -15,7 +16,8 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken="" idtoken=""
accesstoken="" accesstoken=""
refreshtoken="" 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] [upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810" apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus] [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 entrypoint: /bin/bash
environment: environment:
- IS_DOCKER=true - IS_DOCKER=true
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
profiles: profiles:
- builder - 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: builder:
image: unraid-api:builder image: unraid-api:builder
environment:
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
build: build:
context: . context: .
target: builder 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", "name": "@unraid/api",
"version": "3.2.2", "version": "3.5.1",
"main": "dist/index.js", "main": "dist/index.js",
"bin": "dist/unraid-api.cjs", "bin": "dist/unraid-api.cjs",
"type": "module", "type": "module",
@@ -26,29 +26,31 @@
"compile": "tsup --config ./tsup.config.ts", "compile": "tsup --config ./tsup.config.ts",
"bundle": "pkg . --public", "bundle": "pkg . --public",
"build": "npm run compile && npm run bundle", "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", "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": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config", "codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-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", "tsc": "tsc --noEmit",
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs", "lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs", "lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
"test:watch": "vitest --segfault-retry=3 --no-threads", "test:watch": "vitest --segfault-retry=3 --pool=forks",
"test": "vitest run --segfault-retry=3 --no-threads", "test": "vitest run --segfault-retry=3 --pool=forks",
"coverage": "vitest run --segfault-retry=3 --coverage", "coverage": "vitest run --segfault-retry=3 --coverage",
"patch:subscriptions-transport-ws": "node ./.scripts/patches/subscriptions-transport-ws.cjs", "patch:subscriptions-transport-ws": "node ./.scripts/patches/subscriptions-transport-ws.cjs",
"release": "standard-version", "release": "standard-version",
"typesync": "typesync", "typesync": "typesync",
"install:unraid": "./scripts/install-in-unraid.sh", "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: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'", "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: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", "build:dev": "./scripts/dc.sh build dev",
"docker:dev": "docker-compose run --rm --service-ports dev", "start:local": "./scripts/dc.sh run --rm --service-ports local",
"docker:test": "docker-compose run --rm builder npm run test" "start:ddev": "./scripts/dc.sh run --rm --service-ports dev",
"start:dtest": "./scripts/dc.sh run --rm builder npm run test"
}, },
"files": [ "files": [
".env.staging", ".env.staging",
@@ -57,14 +59,21 @@
"unraid-api" "unraid-api"
], ],
"dependencies": { "dependencies": {
"@apollo/client": "^3.7.12", "@apollo/client": "^3.8.9",
"@apollo/server": "^4.6.0", "@apollo/server": "^4.10.0",
"@graphql-codegen/client-preset": "^3.0.0", "@as-integrations/fastify": "^2.1.1",
"@graphql-tools/load-files": "^6.6.1", "@graphql-codegen/client-preset": "^4.1.0",
"@graphql-tools/merge": "^8.4.0", "@graphql-tools/load-files": "^7.0.0",
"@graphql-tools/schema": "^9.0.17", "@graphql-tools/merge": "^9.0.1",
"@graphql-tools/utils": "^9.2.1", "@graphql-tools/schema": "^10.0.2",
"@reduxjs/toolkit": "^1.9.5", "@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", "@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2", "@runonflux/nat-upnp": "^1.0.2",
"accesscontrol": "^2.2.1", "accesscontrol": "^2.2.1",
@@ -75,121 +84,126 @@
"bytes": "^3.1.2", "bytes": "^3.1.2",
"cacheable-lookup": "^6.1.0", "cacheable-lookup": "^6.1.0",
"catch-exit": "^1.2.2", "catch-exit": "^1.2.2",
"chalk": "^4.1.2",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cli-table": "^0.3.11", "cli-table": "^0.3.11",
"command-exists": "^1.2.9", "command-exists": "^1.2.9",
"convert": "^4.10.0", "convert": "^4.14.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"cross-fetch": "^4.0.0", "cross-fetch": "^4.0.0",
"docker-event-emitter": "^0.3.0", "docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5", "dockerode": "^3.3.5",
"dotenv": "^16.0.3", "dotenv": "^16.3.1",
"express": "^4.18.2", "express": "^4.18.2",
"find-process": "^1.4.7", "find-process": "^1.4.7",
"graphql": "^16.6.0", "graphql": "^16.8.1",
"graphql-fields": "^2.0.3", "graphql-fields": "^2.0.3",
"graphql-scalars": "^1.21.3", "graphql-scalars": "^1.22.4",
"graphql-subscriptions": "^2.0.0", "graphql-subscriptions": "^2.0.0",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2", "graphql-type-json": "^0.3.2",
"graphql-type-uuid": "^0.2.0", "graphql-type-uuid": "^0.2.0",
"graphql-ws": "^5.12.1", "graphql-ws": "^5.14.3",
"htpasswd-js": "^1.0.2", "htpasswd-js": "^1.0.2",
"ini": "^4.1.0", "ini": "^4.1.1",
"ip": "^1.1.8", "ip": "^1.1.8",
"jose": "^4.14.2", "jose": "^4.14.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multi-ini": "^2.2.0", "multi-ini": "^2.2.0",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"nanobus": "^4.5.0", "nanobus": "^4.5.0",
"nest-access-control": "^3.1.0",
"nestjs-pino": "^4.0.0",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2", "node-window-polyfill": "^1.0.2",
"openid-client": "^5.4.0", "openid-client": "^5.6.4",
"p-iteration": "^1.1.8", "p-iteration": "^1.1.8",
"p-retry": "^4.6.2", "p-retry": "^4.6.2",
"passport-http-header-strategy": "^1.1.0",
"pidusage": "^3.0.2", "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", "request": "^2.88.2",
"semver": "^7.4.0", "semver": "^7.5.4",
"stoppable": "^1.1.0", "stoppable": "^1.1.0",
"subscriptions-transport-ws": "^0.11.0", "systeminformation": "^5.21.22",
"systeminformation": "^5.21.2", "ts-command-line-args": "^2.5.1",
"ts-command-line-args": "^2.5.0", "uuid": "^9.0.1",
"uuid": "^9.0.0",
"ws": "^8.13.0", "ws": "^8.13.0",
"wtfnode": "^0.9.1", "wtfnode": "^0.9.1",
"xhr2": "^0.2.1", "xhr2": "^0.2.1",
"zod": "^3.22.2" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@babel/runtime": "^7.21.0", "@babel/runtime": "^7.23.8",
"@graphql-codegen/add": "^4.0.1", "@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^3.3.0", "@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^4.0.1", "@graphql-codegen/fragment-matcher": "^5.0.0",
"@graphql-codegen/import-types-preset": "^2.2.6", "@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typed-document-node": "^4.0.0", "@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^3.0.3", "@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^3.0.3", "@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-resolvers": "3.2.1", "@graphql-codegen/typescript-resolvers": "4.0.1",
"@graphql-typed-document-node/core": "^3.2.0", "@graphql-typed-document-node/core": "^3.2.0",
"@swc/core": "^1.3.81", "@nestjs/testing": "^10.3.0",
"@types/async-exit-hook": "^2.0.0", "@swc/core": "^1.3.102",
"@types/btoa": "^1.2.3", "@types/async-exit-hook": "^2.0.2",
"@types/bytes": "^3.1.1", "@types/btoa": "^1.2.5",
"@types/cli-table": "^0.3.1", "@types/bytes": "^3.1.4",
"@types/command-exists": "^1.2.0", "@types/cli-table": "^0.3.4",
"@types/command-exists": "^1.2.3",
"@types/dockerode": "^3.3.16", "@types/dockerode": "^3.3.16",
"@types/express": "^4.17.17", "@types/express": "^4.17.21",
"@types/graphql-fields": "^1.3.5", "@types/graphql-fields": "^1.3.9",
"@types/graphql-type-uuid": "^0.2.3", "@types/graphql-type-uuid": "^0.2.6",
"@types/ini": "^1.3.31", "@types/ini": "^4.1.0",
"@types/lodash": "^4.14.192", "@types/lodash": "^4.14.202",
"@types/mustache": "^4.2.2", "@types/mustache": "^4.2.5",
"@types/node": "^18.17.12", "@types/node": "^20.11.0",
"@types/pidusage": "^2.0.2", "@types/pidusage": "^2.0.5",
"@types/pify": "^5.0.1", "@types/pify": "^5.0.4",
"@types/semver": "^7.3.13", "@types/semver": "^7.5.6",
"@types/sendmail": "^1.4.4", "@types/sendmail": "^1.4.7",
"@types/stoppable": "^1.1.1", "@types/stoppable": "^1.1.3",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.7",
"@types/ws": "^8.5.4", "@types/ws": "^8.5.4",
"@types/wtfnode": "^0.7.0", "@types/wtfnode": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^5.58.0", "@typescript-eslint/parser": "^6.18.1",
"@unraid/eslint-config": "github:unraid/eslint-config", "@unraid/eslint-config": "github:unraid/eslint-config",
"@vitest/coverage-v8": "^0.34.1", "@vitest/coverage-v8": "^1.2.0",
"@vitest/ui": "^0.34.0", "@vitest/ui": "^1.2.0",
"camelcase-keys": "^8.0.2", "camelcase-keys": "^8.0.2",
"cz-conventional-changelog": "3.3.0", "cz-conventional-changelog": "3.3.0",
"eslint": "^8.38.0", "eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^3.6.0", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^48.0.1", "eslint-plugin-unicorn": "^50.0.1",
"eslint-plugin-unused-imports": "^2.0.0", "eslint-plugin-unused-imports": "^3.0.0",
"execa": "^7.1.1", "execa": "^7.1.1",
"filter-obj": "^5.1.0", "filter-obj": "^5.1.0",
"got": "^13.0.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", "ip-regex": "^5.0.0",
"json-difference": "^1.9.1", "json-difference": "^1.16.0",
"log4js": "^6.9.1",
"map-obj": "^5.0.2", "map-obj": "^5.0.2",
"p-props": "^5.0.0", "p-props": "^5.0.0",
"path-exists": "^5.0.0", "path-exists": "^5.0.0",
"path-type": "^5.0.0", "path-type": "^5.0.0",
"pkg": "^5.8.1", "pkg": "^5.8.1",
"pretty-bytes": "^6.1.0", "pretty-bytes": "^6.1.1",
"pretty-ms": "^8.0.0", "pretty-ms": "^8.0.0",
"serialize-error": "^11.0.2",
"standard-version": "^9.5.0", "standard-version": "^9.5.0",
"tsup": "^7.0.0", "tsup": "^8.0.1",
"typescript": "^4.9.4", "typescript": "^5.3.3",
"typesync": "^0.11.0", "typesync": "^0.12.1",
"vite-tsconfig-paths": "^4.2.0", "vite-tsconfig-paths": "^4.2.3",
"vitest": "^0.34.0", "vitest": "^1.2.0",
"zx": "^7.2.1" "zx": "^7.2.3"
}, },
"optionalDependencies": { "optionalDependencies": {
"@vmngr/libvirt": "github:unraid/libvirt" "@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) => { const getTags = (env = process.env) => {
if (env.GIT_SHA) { if (env.GIT_SHA) {
console.log(`Using env vars for git tags: ${env.GIT_SHA} ${env.IS_TAGGED}`)
return { return {
shortSha: env.GIT_SHA, shortSha: env.GIT_SHA,
isTagged: Boolean(env.IS_TAGGED) 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(), error: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
trace: vi.fn(), trace: vi.fn(),
addContext: vi.fn(),
removeContext: vi.fn(),
}, },
dashboardLogger: { dashboardLogger: {
info: vi.fn(), info: vi.fn(),
error: vi.fn((...input) => console.log(input)), error: vi.fn((...input) => console.log(input)),
debug: vi.fn(), debug: vi.fn(),
trace: vi.fn(), trace: vi.fn(),
addContext: vi.fn(),
removeContext: vi.fn(),
}, },
emhttpLogger: { emhttpLogger: {
info: vi.fn(), info: vi.fn(),
error: vi.fn(), error: vi.fn(),
debug: vi.fn(), debug: vi.fn(),
trace: 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', () => ({ vi.mock('@app/common/dashboard/boot-timestamp', () => ({
bootTimestamp: new Date('2022-06-10T04:35:58.276Z'), bootTimestamp: new Date('2022-06-10T04:35:58.276Z'),
})); }));
@@ -77,7 +64,7 @@ test('Returns generated data', async () => {
"case": { "case": {
"base64": "", "base64": "",
"error": "", "error": "",
"icon": "case-model.png", "icon": "",
"url": "", "url": "",
}, },
}, },
@@ -107,6 +94,8 @@ test('Returns generated data', async () => {
"flashGuid": "0000-0000-0000-000000000000", "flashGuid": "0000-0000-0000-000000000000",
"regState": "PRO", "regState": "PRO",
"regTy": "PRO", "regTy": "PRO",
"serverDescription": "Dev Server",
"serverName": "Tower",
}, },
"versions": { "versions": {
"unraid": "6.11.2", "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(state).toMatchObject(
expect.objectContaining({ expect.objectContaining({
api: { api: {
extraOrigins: '', extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
version: expect.any(String), version: expect.any(String),
}, },
connectionStatus: { connectionStatus: {
@@ -114,7 +114,7 @@ test('updateUserConfig merges in changes to current state', async () => {
expect(state).toMatchObject( expect(state).toMatchObject(
expect.objectContaining({ expect.objectContaining({
api: { api: {
extraOrigins: '', extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
version: expect.any(String), version: expect.any(String),
}, },
connectionStatus: { connectionStatus: {

View File

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

View File

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

View File

@@ -75,15 +75,10 @@ export const getCloudData = async (
const cloud = await client.query({ query: getCloudDocument }); const cloud = await client.query({ query: getCloudDocument });
return cloud.data.cloud ?? null; return cloud.data.cloud ?? null;
} catch (error: unknown) { } catch (error: unknown) {
cliLogger.addContext(
'error-stack',
error instanceof Error ? error.stack : error
);
cliLogger.trace( cliLogger.trace(
'Failed fetching cloud from local graphql with "%s"', 'Failed fetching cloud from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error' error instanceof Error ? error.message : 'Unknown Error'
); );
cliLogger.removeContext('error-stack');
return null; return null;
} }
@@ -122,12 +117,10 @@ export const getServersData = async ({
); );
return foundServers; return foundServers;
} catch (error: unknown) { } catch (error: unknown) {
cliLogger.addContext('error', error);
cliLogger.trace( cliLogger.trace(
'Failed fetching servers from local graphql with "%s"', 'Failed fetching servers from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error' error instanceof Error ? error.message : 'Unknown Error'
); );
cliLogger.removeContext('error');
return { return {
online: [], online: [],
offline: [], 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 () => { export const start = async () => {
// Set process title // Set process title
process.title = 'unraid-api'; process.title = 'unraid-api';
const runningProcesses = await getAllUnraidApiPids(); const runningProcesses = await getAllUnraidApiPids();
if (runningProcesses.length > 0) { if (runningProcesses.length > 0) {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 { pino } from 'pino';
import { configure, getLogger } from 'log4js'; import { LOG_TRANSPORT, LOG_TYPE } from '@app/environment';
import { serializeError } from 'serialize-error';
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 makeLoggingDirectoryIfNotExists = () => {
const stackEnabled = Boolean(process.env.LOG_STACKTRACE); if (!existsSync(getters.paths()['log-base'])) {
const tracingEnabled = Boolean(process.env.LOG_TRACING); console.log('Creating logging directory');
const fullLoggingPattern = chalk`{gray [%d]} %x\{id\} %[[%p]%] %[[%c]%] %m{gray %x\{context\}}${tracingEnabled ? ' %[%f:%l%]' : ''}`; mkdirSync(getters.paths()['log-base']);
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 '';
}
try { chmodSync(getters.paths()['log-base'], 0o644);
const contextEntries = Object.entries(context) if (
.map(([key, value]) => [key, value instanceof Error ? (stackEnabled ? serializeError(value) : value) : value]) existsSync(`${getters.paths()['log-base']}/stdout.log`) &&
.filter(([key]) => key !== 'pid'); statSync(`${getters.paths()['log-base']}/stdout.log`).size > 5_000_000
const cleanContext = Object.fromEntries(contextEntries); ) {
return `\n${Object.entries(cleanContext).map(([key, value]) => `${key}=${JSON.stringify(value, null, 2)}`).join(' ')}`; rmSync(`${getters.paths()['log-base']}/stdout.log`);
} catch (error: unknown) { }
const errorInfo = error instanceof Error ? `${error.message}: ${error.stack ?? 'no stack'}` : 'Error not instance of error'; try {
return `Error generating context: ${errorInfo}`; rmSync(`${getters.paths()['log-base']}/stdout.log.*`);
} } catch (e) {
}, // Ignore Error
}, }
}; };
if (process.env.NODE_ENV !== 'test') { if (LOG_TRANSPORT === 'file') {
// We log to both the stdout and log file makeLoggingDirectoryIfNotExists();
// 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,
},
},
});
} }
export const internalLogger = getLogger('internal'); export const levels = [
export const logger = getLogger('app'); 'trace',
export const mothershipLogger = getLogger('mothership'); 'debug',
export const dashboardLogger = getLogger('dashboard'); 'info',
export const emhttpLogger = getLogger('emhttp'); 'warn',
export const libvirtLogger = getLogger('libvirt'); 'error',
export const graphqlLogger = getLogger('graphql'); 'fatal',
export const dockerLogger = getLogger('docker'); ] as const;
export const cliLogger = getLogger('cli');
export const minigraphLogger = getLogger('minigraph'); const level =
export const cloudConnectorLogger = getLogger('cloud-connector'); levels[
export const upnpLogger = getLogger('upnp'); levels.indexOf(
export const keyServerLogger = getLogger('key-server'); process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number]
export const remoteAccessLogger = getLogger('remote-access'); )
export const remoteQueryLogger = getLogger('remote-query'); ] ?? '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 = [ export const loggers = [
logger, internalLogger,
mothershipLogger, appLogger,
dashboardLogger, mothershipLogger,
emhttpLogger, dashboardLogger,
libvirtLogger, emhttpLogger,
graphqlLogger, libvirtLogger,
dockerLogger, graphqlLogger,
cliLogger, dockerLogger,
minigraphLogger, cliLogger,
cloudConnectorLogger, minigraphLogger,
upnpLogger, cloudConnectorLogger,
keyServerLogger, upnpLogger,
remoteAccessLogger, keyServerLogger,
remoteQueryLogger, remoteAccessLogger,
remoteQueryLogger,
apiLogger,
]; ];
// Send SIGUSR1 to increase log level // Send SIGUSR1 to increase log level
process.on('SIGUSR1', () => { process.on('SIGUSR1', () => {
const level = typeof logger.level === 'string' ? logger.level : logger.level.levelStr; const level = logger.level;
const nextLevel = levels[levels.findIndex(_level => _level === level) + 1] ?? levels[0]; const nextLevel =
loggers.forEach(logger => { levels[levels.findIndex((_level) => _level === level) + 1] ?? levels[0];
logger.level = nextLevel; loggers.forEach((logger) => {
}); logger.level = nextLevel;
internalLogger.mark('Log level changed from %s to %s', level, nextLevel); });
internalLogger.info({
message: `Log level changed from ${level} to ${nextLevel}`,
});
}); });
// Send SIGUSR1 to decrease log level // Send SIGUSR1 to decrease log level
process.on('SIGUSR2', () => { process.on('SIGUSR2', () => {
const level = typeof logger.level === 'string' ? logger.level : logger.level.levelStr; const level = logger.level;
const nextLevel = levels[levels.findIndex(_level => _level === level) - 1] ?? levels[levels.length - 1]; const nextLevel =
loggers.forEach(logger => { levels[levels.findIndex((_level) => _level === level) - 1] ??
logger.level = nextLevel; levels[levels.length - 1];
}); loggers.forEach((logger) => {
internalLogger.mark('Log level changed from %s to %s', level, nextLevel); 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> => { export const addUser = async (context: Context): Promise<CoreResult> => {
const { data } = context; const { data } = context;
// Check permissions // Check permissions
ensurePermission(context.user, { ensurePermission(context.user, {
resource: 'user', resource: 'user',

View File

@@ -1,7 +1,8 @@
import camelCaseKeys from 'camelcase-keys'; 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 { type CoreContext, type CoreResult } from '@app/core/types';
import { catchHandlers } from '@app/core/utils/misc/catch-handlers'; 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> => { export const getDockerNetworks = async (context: CoreContext): Promise<CoreResult> => {
const { user } = context; 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, diskLayout,
} from 'systeminformation'; } from 'systeminformation';
import { map as asyncMap } from 'p-iteration'; 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 { import {
type Disk, type Disk,
DiskInterfaceType, DiskInterfaceType,
DiskSmartStatus, DiskSmartStatus,
DiskFsType,
} from '@app/graphql/generated/api/types'; } from '@app/graphql/generated/api/types';
import { DiskFsType } from '@app/graphql/generated/api/types';
import { graphqlLogger } from '@app/core/log'; import { graphqlLogger } from '@app/core/log';
const getTemperature = async ( const getTemperature = async (
@@ -86,18 +84,8 @@ const parseDisk = async (
* Get all disks. * Get all disks.
*/ */
export const getDisks = async ( export const getDisks = async (
context: Context,
options?: { temperature: boolean } options?: { temperature: boolean }
): Promise<Disk[]> => { ): Promise<Disk[]> => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'disk',
action: 'read',
possession: 'any',
});
// Return all fields but temperature // Return all fields but temperature
if (options?.temperature === false) { if (options?.temperature === false) {
const partitions = await blockDevices().then((devices) => 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 { execa } from 'execa';
import { ensurePermission } from '@app/core/utils';
import { type CoreContext, type CoreResult } from '@app/core/types'; import { type CoreContext, type CoreResult } from '@app/core/types';
import { cleanStdout } from '@app/core/utils/misc/clean-stdout'; import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
interface Result extends CoreResult { interface Result extends CoreResult {
json: { json: {

View File

@@ -1,7 +1,8 @@
import type { CoreContext, CoreResult } from '@app/core/types/global'; import type { CoreContext, CoreResult } from '@app/core/types/global';
import type { UserShare, DiskShare } from '@app/core/types/states/share'; import type { UserShare, DiskShare } from '@app/core/types/states/share';
import { AppError } from '@app/core/errors/app-error'; 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 { interface Context extends CoreContext {
params: { params: {

View File

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

View File

@@ -1,33 +1,190 @@
import { logger } from '@app/core/log'; import { apiLogger } from '@app/core/log';
import { permissions as defaultPermissions } from '@app/core/default-permissions'; import { RolesBuilder } from 'nest-access-control';
import { AccessControl } from 'accesscontrol';
export interface Permission {
role?: string;
resource: string;
action: string;
attributes: string;
}
export interface Role {
permissions: Array<Permission>;
extends?: string;
}
// Use built in permissions // Use built in permissions
const getPermissions = () => defaultPermissions; const roles: Record<string, Role> = {
admin: {
// Build permissions array extends: 'guest',
const roles = getPermissions(); permissions: [
const permissions = Object.entries(roles).flatMap(([roleName, role]) => [ { resource: 'apikey', action: 'read:any', attributes: '*' },
...(role?.permissions ?? []).map(permission => ({ { resource: 'cloud', action: 'read:own', attributes: '*' },
...permission, { resource: 'config', action: 'update:own', attributes: '*' },
role: roleName, { resource: 'connect', action: 'read:own', attributes: '*' },
})), { resource: 'connect', action: 'update:own', attributes: '*' },
]); { resource: 'customizations', action: 'read:any', attributes: '*' },
{ resource: 'array', action: 'read:any', attributes: '*' },
// Grant permissions { resource: 'cpu', action: 'read:any', attributes: '*' },
const ac = new AccessControl(permissions); {
resource: 'crash-reporting-enabled',
// Extend roles action: 'read:any',
Object.entries(getPermissions()).forEach(([roleName, role]) => { attributes: '*',
if (role.extends) { },
ac.extendRole(roleName, role.extends); { resource: 'device', action: 'read:any', attributes: '*' },
} {
}); resource: 'device/unassigned',
action: 'read:any',
logger.addContext('permissions', permissions); attributes: '*',
logger.trace('Loaded permissions'); },
logger.removeContext('permissions'); { resource: 'disk', action: 'read:any', attributes: '*' },
{ resource: 'disk/settings', action: 'read:any', attributes: '*' },
export { { resource: 'display', action: 'read:any', attributes: '*' },
ac, {
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); eventEmitter.setMaxListeners(30);
export enum PUBSUB_CHANNEL { export enum PUBSUB_CHANNEL {
ARRAY = 'ARRAY',
DASHBOARD = 'DASHBOARD',
DISPLAY = 'DISPLAY', DISPLAY = 'DISPLAY',
INFO = 'INFO', INFO = 'INFO',
NOTIFICATION = 'NOTIFICATION', NOTIFICATION = 'NOTIFICATION',
OWNER = 'OWNER', OWNER = 'OWNER',
SERVERS = 'SERVERS', SERVERS = 'SERVERS',
VMS = 'VMS',
REGISTRATION = 'REGISTRATION',
} }
export const pubsub = new PubSub({ eventEmitter }); 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; regFile: string;
regGen: string; regGen: string;
regGuid: string; regGuid: string;
/** Registration time for key */
regTm: string; regTm: string;
/** Expiration of license for Trial Keys */
regTm2: string; regTm2: string;
/** Expiration of Updates for non-legacy keys */
regExp: string | null;
/** Who the current Unraid key is registered to. */ /** Who the current Unraid key is registered to. */
regTo: string; regTo: string;
/** Which type of key this is. */ /** 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 { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { import {
type SliceState as ConfigSliceState, type SliceState as ConfigSliceState,
@@ -34,18 +35,18 @@ export const getWriteableConfig = <T extends ConfigType>(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const newState: ConfigObject<T> = { const newState: ConfigObject<T> = {
api: { api: {
version: api.version ?? initialState.api.version, version: api?.version ?? initialState.api.version,
...(api.extraOrigins ? { extraOrigins: api.extraOrigins } : {}), extraOrigins: api?.extraOrigins ?? initialState.api.extraOrigins,
}, },
local: { local: {
...(local['2Fa'] === 'yes' ? { '2Fa': local['2Fa'] } : {}), ...(local?.['2Fa'] === 'yes' ? { '2Fa': local['2Fa'] } : {}),
...(local.showT2Fa === 'yes' ? { showT2Fa: local.showT2Fa } : {}), ...(local?.showT2Fa === 'yes' ? { showT2Fa: local.showT2Fa } : {}),
}, },
notifier: { notifier: {
apikey: notifier.apikey ?? initialState.notifier.apikey, apikey: notifier.apikey ?? initialState.notifier.apikey,
}, },
remote: { remote: {
...(remote['2Fa'] === 'yes' ? { '2Fa': remote['2Fa'] } : {}), ...(remote?.['2Fa'] === 'yes' ? { '2Fa': remote['2Fa'] } : {}),
wanaccess: remote.wanaccess ?? initialState.remote.wanaccess, wanaccess: remote.wanaccess ?? initialState.remote.wanaccess,
wanport: remote.wanport ?? initialState.remote.wanport, wanport: remote.wanport ?? initialState.remote.wanport,
...(remote.upnpEnabled ? { upnpEnabled: remote.upnpEnabled } : {}), ...(remote.upnpEnabled ? { upnpEnabled: remote.upnpEnabled } : {}),
@@ -61,16 +62,10 @@ export const getWriteableConfig = <T extends ConfigType>(
...(mode === 'memory' ...(mode === 'memory'
? { ? {
allowedOrigins: allowedOrigins:
remote.allowedOrigins ?? getAllowedOrigins().join(', ')
initialState.remote.allowedOrigins,
} }
: {}), : {}),
...(remote.dynamicRemoteAccessType === dynamicRemoteAccessType: remote.dynamicRemoteAccessType ?? DynamicRemoteAccessType.DISABLED,
DynamicRemoteAccessType.DISABLED
? {}
: {
dynamicRemoteAccessType: remote.dynamicRemoteAccessType,
}),
}, },
upc: { upc: {
apikey: upc.apikey ?? initialState.upc.apikey, 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, deep: true,
}) as T; }) as T;
logger.addContext('config', config); logger.trace({ config }, '"%s" was loaded', filePath);
logger.trace('"%s" was loaded', filePath);
logger.removeContext('config');
return config; return config;
} catch (error: unknown) { } 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 PORT = process.env.PORT ?? '/var/run/unraid-api.sock';
export const DRY_RUN = process.env.DRY_RUN === 'true'; export const DRY_RUN = process.env.DRY_RUN === 'true';
export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === '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 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 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 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 */ /** All built-in and custom scalars, mapped to their actual values */
export type Scalars = { export type Scalars = {
ID: string; ID: { input: string; output: string; }
String: string; String: { input: string; output: string; }
Boolean: boolean; Boolean: { input: boolean; output: boolean; }
Int: number; Int: { input: number; output: number; }
Float: 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. */ /** 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. */ /** 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. */ /** 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). */ /** 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 */ /** 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 */ /** 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. */ /** 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 = { export type AccessUrl = {
__typename?: 'AccessUrl'; __typename?: 'AccessUrl';
ipv4?: Maybe<Scalars['URL']>; ipv4?: Maybe<Scalars['URL']['output']>;
ipv6?: Maybe<Scalars['URL']>; ipv6?: Maybe<Scalars['URL']['output']>;
name?: Maybe<Scalars['String']>; name?: Maybe<Scalars['String']['output']>;
type: URL_TYPE; type: URL_TYPE;
}; };
export type AccessUrlInput = { export type AccessUrlInput = {
ipv4?: InputMaybe<Scalars['URL']>; ipv4?: InputMaybe<Scalars['URL']['input']>;
ipv6?: InputMaybe<Scalars['URL']>; ipv6?: InputMaybe<Scalars['URL']['input']>;
name?: InputMaybe<Scalars['String']>; name?: InputMaybe<Scalars['String']['input']>;
type: URL_TYPE; type: URL_TYPE;
}; };
@@ -50,15 +52,15 @@ export type ArrayCapacity = {
export type ArrayCapacityBytes = { export type ArrayCapacityBytes = {
__typename?: 'ArrayCapacityBytes'; __typename?: 'ArrayCapacityBytes';
free?: Maybe<Scalars['Long']>; free?: Maybe<Scalars['Long']['output']>;
total?: Maybe<Scalars['Long']>; total?: Maybe<Scalars['Long']['output']>;
used?: Maybe<Scalars['Long']>; used?: Maybe<Scalars['Long']['output']>;
}; };
export type ArrayCapacityBytesInput = { export type ArrayCapacityBytesInput = {
free?: InputMaybe<Scalars['Long']>; free?: InputMaybe<Scalars['Long']['input']>;
total?: InputMaybe<Scalars['Long']>; total?: InputMaybe<Scalars['Long']['input']>;
used?: InputMaybe<Scalars['Long']>; used?: InputMaybe<Scalars['Long']['input']>;
}; };
export type ArrayCapacityInput = { export type ArrayCapacityInput = {
@@ -73,9 +75,9 @@ export type ClientConnectedEvent = {
export type ClientConnectionEventData = { export type ClientConnectionEventData = {
__typename?: 'ClientConnectionEventData'; __typename?: 'ClientConnectionEventData';
apiKey: Scalars['String']; apiKey: Scalars['String']['output'];
type: ClientType; type: ClientType;
version: Scalars['String']; version: Scalars['String']['output'];
}; };
export type ClientDisconnectedEvent = { export type ClientDisconnectedEvent = {
@@ -98,7 +100,7 @@ export enum ClientType {
export type Config = { export type Config = {
__typename?: 'Config'; __typename?: 'Config';
error?: Maybe<ConfigErrorState>; error?: Maybe<ConfigErrorState>;
valid?: Maybe<Scalars['Boolean']>; valid?: Maybe<Scalars['Boolean']['output']>;
}; };
export enum ConfigErrorState { export enum ConfigErrorState {
@@ -114,10 +116,10 @@ export type Dashboard = {
array?: Maybe<DashboardArray>; array?: Maybe<DashboardArray>;
config?: Maybe<DashboardConfig>; config?: Maybe<DashboardConfig>;
display?: Maybe<DashboardDisplay>; display?: Maybe<DashboardDisplay>;
id: Scalars['ID']; id: Scalars['ID']['output'];
lastPublish?: Maybe<Scalars['DateTime']>; lastPublish?: Maybe<Scalars['DateTime']['output']>;
network?: Maybe<Network>; network?: Maybe<Network>;
online?: Maybe<Scalars['Boolean']>; online?: Maybe<Scalars['Boolean']['output']>;
os?: Maybe<DashboardOs>; os?: Maybe<DashboardOs>;
services?: Maybe<Array<Maybe<DashboardService>>>; services?: Maybe<Array<Maybe<DashboardService>>>;
twoFactor?: Maybe<DashboardTwoFactor>; twoFactor?: Maybe<DashboardTwoFactor>;
@@ -128,13 +130,13 @@ export type Dashboard = {
export type DashboardApps = { export type DashboardApps = {
__typename?: 'DashboardApps'; __typename?: 'DashboardApps';
installed?: Maybe<Scalars['Int']>; installed?: Maybe<Scalars['Int']['output']>;
started?: Maybe<Scalars['Int']>; started?: Maybe<Scalars['Int']['output']>;
}; };
export type DashboardAppsInput = { export type DashboardAppsInput = {
installed: Scalars['Int']; installed: Scalars['Int']['input'];
started: Scalars['Int']; started: Scalars['Int']['input'];
}; };
export type DashboardArray = { export type DashboardArray = {
@@ -142,40 +144,40 @@ export type DashboardArray = {
/** Current array capacity */ /** Current array capacity */
capacity?: Maybe<ArrayCapacity>; capacity?: Maybe<ArrayCapacity>;
/** Current array state */ /** Current array state */
state?: Maybe<Scalars['String']>; state?: Maybe<Scalars['String']['output']>;
}; };
export type DashboardArrayInput = { export type DashboardArrayInput = {
/** Current array capacity */ /** Current array capacity */
capacity: ArrayCapacityInput; capacity: ArrayCapacityInput;
/** Current array state */ /** Current array state */
state: Scalars['String']; state: Scalars['String']['input'];
}; };
export type DashboardCase = { export type DashboardCase = {
__typename?: 'DashboardCase'; __typename?: 'DashboardCase';
base64?: Maybe<Scalars['String']>; base64?: Maybe<Scalars['String']['output']>;
error?: Maybe<Scalars['String']>; error?: Maybe<Scalars['String']['output']>;
icon?: Maybe<Scalars['String']>; icon?: Maybe<Scalars['String']['output']>;
url?: Maybe<Scalars['String']>; url?: Maybe<Scalars['String']['output']>;
}; };
export type DashboardCaseInput = { export type DashboardCaseInput = {
base64: Scalars['String']; base64: Scalars['String']['input'];
error?: InputMaybe<Scalars['String']>; error?: InputMaybe<Scalars['String']['input']>;
icon: Scalars['String']; icon: Scalars['String']['input'];
url: Scalars['String']; url: Scalars['String']['input'];
}; };
export type DashboardConfig = { export type DashboardConfig = {
__typename?: 'DashboardConfig'; __typename?: 'DashboardConfig';
error?: Maybe<Scalars['String']>; error?: Maybe<Scalars['String']['output']>;
valid?: Maybe<Scalars['Boolean']>; valid?: Maybe<Scalars['Boolean']['output']>;
}; };
export type DashboardConfigInput = { export type DashboardConfigInput = {
error?: InputMaybe<Scalars['String']>; error?: InputMaybe<Scalars['String']['input']>;
valid: Scalars['Boolean']; valid: Scalars['Boolean']['input'];
}; };
export type DashboardDisplay = { export type DashboardDisplay = {
@@ -202,37 +204,37 @@ export type DashboardInput = {
export type DashboardOs = { export type DashboardOs = {
__typename?: 'DashboardOs'; __typename?: 'DashboardOs';
hostname?: Maybe<Scalars['String']>; hostname?: Maybe<Scalars['String']['output']>;
uptime?: Maybe<Scalars['DateTime']>; uptime?: Maybe<Scalars['DateTime']['output']>;
}; };
export type DashboardOsInput = { export type DashboardOsInput = {
hostname: Scalars['String']; hostname: Scalars['String']['input'];
uptime: Scalars['DateTime']; uptime: Scalars['DateTime']['input'];
}; };
export type DashboardService = { export type DashboardService = {
__typename?: 'DashboardService'; __typename?: 'DashboardService';
name?: Maybe<Scalars['String']>; name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']>; online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<DashboardServiceUptime>; uptime?: Maybe<DashboardServiceUptime>;
version?: Maybe<Scalars['String']>; version?: Maybe<Scalars['String']['output']>;
}; };
export type DashboardServiceInput = { export type DashboardServiceInput = {
name: Scalars['String']; name: Scalars['String']['input'];
online: Scalars['Boolean']; online: Scalars['Boolean']['input'];
uptime?: InputMaybe<DashboardServiceUptimeInput>; uptime?: InputMaybe<DashboardServiceUptimeInput>;
version: Scalars['String']; version: Scalars['String']['input'];
}; };
export type DashboardServiceUptime = { export type DashboardServiceUptime = {
__typename?: 'DashboardServiceUptime'; __typename?: 'DashboardServiceUptime';
timestamp?: Maybe<Scalars['DateTime']>; timestamp?: Maybe<Scalars['DateTime']['output']>;
}; };
export type DashboardServiceUptimeInput = { export type DashboardServiceUptimeInput = {
timestamp: Scalars['DateTime']; timestamp: Scalars['DateTime']['input'];
}; };
export type DashboardTwoFactor = { export type DashboardTwoFactor = {
@@ -248,53 +250,59 @@ export type DashboardTwoFactorInput = {
export type DashboardTwoFactorLocal = { export type DashboardTwoFactorLocal = {
__typename?: 'DashboardTwoFactorLocal'; __typename?: 'DashboardTwoFactorLocal';
enabled?: Maybe<Scalars['Boolean']>; enabled?: Maybe<Scalars['Boolean']['output']>;
}; };
export type DashboardTwoFactorLocalInput = { export type DashboardTwoFactorLocalInput = {
enabled: Scalars['Boolean']; enabled: Scalars['Boolean']['input'];
}; };
export type DashboardTwoFactorRemote = { export type DashboardTwoFactorRemote = {
__typename?: 'DashboardTwoFactorRemote'; __typename?: 'DashboardTwoFactorRemote';
enabled?: Maybe<Scalars['Boolean']>; enabled?: Maybe<Scalars['Boolean']['output']>;
}; };
export type DashboardTwoFactorRemoteInput = { export type DashboardTwoFactorRemoteInput = {
enabled: Scalars['Boolean']; enabled: Scalars['Boolean']['input'];
}; };
export type DashboardVars = { export type DashboardVars = {
__typename?: 'DashboardVars'; __typename?: 'DashboardVars';
flashGuid?: Maybe<Scalars['String']>; flashGuid?: Maybe<Scalars['String']['output']>;
regState?: Maybe<Scalars['String']>; regState?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<Scalars['String']>; regTy?: Maybe<Scalars['String']['output']>;
serverDescription?: Maybe<Scalars['String']['output']>;
serverName?: Maybe<Scalars['String']['output']>;
}; };
export type DashboardVarsInput = { export type DashboardVarsInput = {
flashGuid: Scalars['String']; flashGuid: Scalars['String']['input'];
regState: Scalars['String']; regState: Scalars['String']['input'];
regTy: Scalars['String']; regTy: Scalars['String']['input'];
/** Server description */
serverDescription?: InputMaybe<Scalars['String']['input']>;
/** Name of the server */
serverName?: InputMaybe<Scalars['String']['input']>;
}; };
export type DashboardVersions = { export type DashboardVersions = {
__typename?: 'DashboardVersions'; __typename?: 'DashboardVersions';
unraid?: Maybe<Scalars['String']>; unraid?: Maybe<Scalars['String']['output']>;
}; };
export type DashboardVersionsInput = { export type DashboardVersionsInput = {
unraid: Scalars['String']; unraid: Scalars['String']['input'];
}; };
export type DashboardVms = { export type DashboardVms = {
__typename?: 'DashboardVms'; __typename?: 'DashboardVms';
installed?: Maybe<Scalars['Int']>; installed?: Maybe<Scalars['Int']['output']>;
started?: Maybe<Scalars['Int']>; started?: Maybe<Scalars['Int']['output']>;
}; };
export type DashboardVmsInput = { export type DashboardVmsInput = {
installed: Scalars['Int']; installed: Scalars['Int']['input'];
started: Scalars['Int']; started: Scalars['Int']['input'];
}; };
export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQLEvent | UpdateEvent; export type Event = ClientConnectedEvent | ClientDisconnectedEvent | ClientPingEvent | RemoteAccessEvent | RemoteGraphQLEvent | UpdateEvent;
@@ -310,13 +318,13 @@ export enum EventType {
export type FullServerDetails = { export type FullServerDetails = {
__typename?: 'FullServerDetails'; __typename?: 'FullServerDetails';
apiConnectedCount?: Maybe<Scalars['Int']>; apiConnectedCount?: Maybe<Scalars['Int']['output']>;
apiVersion?: Maybe<Scalars['String']>; apiVersion?: Maybe<Scalars['String']['output']>;
connectionTimestamp?: Maybe<Scalars['String']>; connectionTimestamp?: Maybe<Scalars['String']['output']>;
dashboard?: Maybe<Dashboard>; dashboard?: Maybe<Dashboard>;
lastPublish?: Maybe<Scalars['String']>; lastPublish?: Maybe<Scalars['String']['output']>;
network?: Maybe<Network>; network?: Maybe<Network>;
online?: Maybe<Scalars['Boolean']>; online?: Maybe<Scalars['Boolean']['output']>;
}; };
export enum Importance { export enum Importance {
@@ -325,48 +333,41 @@ export enum Importance {
WARNING = 'WARNING' WARNING = 'WARNING'
} }
export enum KeyType {
BASIC = 'BASIC',
PLUS = 'PLUS',
PRO = 'PRO',
TRIAL = 'TRIAL'
}
export type KsServerDetails = { export type KsServerDetails = {
__typename?: 'KsServerDetails'; __typename?: 'KsServerDetails';
accessLabel: Scalars['String']; accessLabel: Scalars['String']['output'];
accessUrl: Scalars['String']; accessUrl: Scalars['String']['output'];
apiKey?: Maybe<Scalars['String']>; apiKey?: Maybe<Scalars['String']['output']>;
description: Scalars['String']; description: Scalars['String']['output'];
dnsHash: Scalars['String']; dnsHash: Scalars['String']['output'];
flashBackupDate?: Maybe<Scalars['Int']>; flashBackupDate?: Maybe<Scalars['Int']['output']>;
flashBackupUrl: Scalars['String']; flashBackupUrl: Scalars['String']['output'];
flashProduct: Scalars['String']; flashProduct: Scalars['String']['output'];
flashVendor: Scalars['String']; flashVendor: Scalars['String']['output'];
guid: Scalars['String']; guid: Scalars['String']['output'];
ipsId: Scalars['String']; ipsId?: Maybe<Scalars['String']['output']>;
keyType: KeyType; keyType?: Maybe<Scalars['String']['output']>;
licenseKey: Scalars['String']; licenseKey: Scalars['String']['output'];
name: Scalars['String']; name: Scalars['String']['output'];
plgVersion?: Maybe<Scalars['String']>; plgVersion?: Maybe<Scalars['String']['output']>;
signedIn: Scalars['Boolean']; signedIn: Scalars['Boolean']['output'];
}; };
export type LegacyService = { export type LegacyService = {
__typename?: 'LegacyService'; __typename?: 'LegacyService';
name?: Maybe<Scalars['String']>; name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']>; online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Scalars['Int']>; uptime?: Maybe<Scalars['Int']['output']>;
version?: Maybe<Scalars['String']>; version?: Maybe<Scalars['String']['output']>;
}; };
export type Mutation = { export type Mutation = {
__typename?: 'Mutation'; __typename?: 'Mutation';
remoteGraphQLResponse: Scalars['Boolean']; remoteGraphQLResponse: Scalars['Boolean']['output'];
remoteMutation: Scalars['String']; remoteMutation: Scalars['String']['output'];
remoteSession?: Maybe<Scalars['Boolean']>; remoteSession?: Maybe<Scalars['Boolean']['output']>;
sendNotification?: Maybe<Notification>; sendNotification?: Maybe<Notification>;
sendPing?: Maybe<Scalars['Boolean']>; sendPing?: Maybe<Scalars['Boolean']['output']>;
updateDashboard: Dashboard; updateDashboard: Dashboard;
updateNetwork: Network; updateNetwork: Network;
}; };
@@ -412,20 +413,20 @@ export type NetworkInput = {
export type Notification = { export type Notification = {
__typename?: 'Notification'; __typename?: 'Notification';
description?: Maybe<Scalars['String']>; description?: Maybe<Scalars['String']['output']>;
importance?: Maybe<Importance>; importance?: Maybe<Importance>;
link?: Maybe<Scalars['String']>; link?: Maybe<Scalars['String']['output']>;
status: NotificationStatus; status: NotificationStatus;
subject?: Maybe<Scalars['String']>; subject?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']>; title?: Maybe<Scalars['String']['output']>;
}; };
export type NotificationInput = { export type NotificationInput = {
description?: InputMaybe<Scalars['String']>; description?: InputMaybe<Scalars['String']['input']>;
importance: Importance; importance: Importance;
link?: InputMaybe<Scalars['String']>; link?: InputMaybe<Scalars['String']['input']>;
subject?: InputMaybe<Scalars['String']>; subject?: InputMaybe<Scalars['String']['input']>;
title?: InputMaybe<Scalars['String']>; title?: InputMaybe<Scalars['String']['input']>;
}; };
export enum NotificationStatus { export enum NotificationStatus {
@@ -437,7 +438,7 @@ export enum NotificationStatus {
export type PingEvent = { export type PingEvent = {
__typename?: 'PingEvent'; __typename?: 'PingEvent';
data?: Maybe<Scalars['String']>; data?: Maybe<Scalars['String']['output']>;
type: EventType; type: EventType;
}; };
@@ -453,26 +454,27 @@ export enum PingEventSource {
export type ProfileModel = { export type ProfileModel = {
__typename?: 'ProfileModel'; __typename?: 'ProfileModel';
avatar?: Maybe<Scalars['String']>; avatar?: Maybe<Scalars['String']['output']>;
url?: Maybe<Scalars['String']>; cognito_id?: Maybe<Scalars['String']['output']>;
userId?: Maybe<Scalars['ID']>; url?: Maybe<Scalars['String']['output']>;
username?: Maybe<Scalars['String']>; userId?: Maybe<Scalars['ID']['output']>;
username?: Maybe<Scalars['String']['output']>;
}; };
export type Query = { export type Query = {
__typename?: 'Query'; __typename?: 'Query';
apiVersion?: Maybe<Scalars['String']>; apiVersion?: Maybe<Scalars['String']['output']>;
dashboard?: Maybe<Dashboard>; dashboard?: Maybe<Dashboard>;
ksServers: Array<KsServerDetails>; ksServers: Array<KsServerDetails>;
online?: Maybe<Scalars['Boolean']>; online?: Maybe<Scalars['Boolean']['output']>;
remoteQuery: Scalars['String']; remoteQuery: Scalars['String']['output'];
servers: Array<Maybe<Server>>; servers: Array<Maybe<Server>>;
status?: Maybe<ServerStatus>; status?: Maybe<ServerStatus>;
}; };
export type QuerydashboardArgs = { export type QuerydashboardArgs = {
id: Scalars['String']; id: Scalars['String']['input'];
}; };
@@ -538,20 +540,20 @@ export enum RemoteAccessEventActionType {
export type RemoteAccessEventData = { export type RemoteAccessEventData = {
__typename?: 'RemoteAccessEventData'; __typename?: 'RemoteAccessEventData';
apiKey: Scalars['String']; apiKey: Scalars['String']['output'];
type: RemoteAccessEventActionType; type: RemoteAccessEventActionType;
url?: Maybe<AccessUrl>; url?: Maybe<AccessUrl>;
}; };
export type RemoteAccessInput = { export type RemoteAccessInput = {
apiKey: Scalars['String']; apiKey: Scalars['String']['input'];
type: RemoteAccessEventActionType; type: RemoteAccessEventActionType;
url?: InputMaybe<AccessUrlInput>; url?: InputMaybe<AccessUrlInput>;
}; };
export type RemoteGraphQLClientInput = { export type RemoteGraphQLClientInput = {
apiKey: Scalars['String']; apiKey: Scalars['String']['input'];
body: Scalars['String']; body: Scalars['String']['input'];
}; };
export type RemoteGraphQLEvent = { export type RemoteGraphQLEvent = {
@@ -563,9 +565,9 @@ export type RemoteGraphQLEvent = {
export type RemoteGraphQLEventData = { export type RemoteGraphQLEventData = {
__typename?: 'RemoteGraphQLEventData'; __typename?: 'RemoteGraphQLEventData';
/** Contains mutation / subscription / query data in the form of body: JSON, variables: JSON */ /** 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 hash of the body */
sha256: Scalars['String']; sha256: Scalars['String']['output'];
type: RemoteGraphQLEventType; type: RemoteGraphQLEventType;
}; };
@@ -578,39 +580,39 @@ export enum RemoteGraphQLEventType {
export type RemoteGraphQLServerInput = { export type RemoteGraphQLServerInput = {
/** Body - contains an object containing data: (GQL response data) or errors: (GQL Errors) */ /** 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 hash of the body */
sha256: Scalars['String']; sha256: Scalars['String']['input'];
type: RemoteGraphQLEventType; type: RemoteGraphQLEventType;
}; };
export type Server = { export type Server = {
__typename?: 'Server'; __typename?: 'Server';
apikey?: Maybe<Scalars['String']>; apikey?: Maybe<Scalars['String']['output']>;
guid?: Maybe<Scalars['String']>; guid?: Maybe<Scalars['String']['output']>;
lanip?: Maybe<Scalars['String']>; lanip?: Maybe<Scalars['String']['output']>;
localurl?: Maybe<Scalars['String']>; localurl?: Maybe<Scalars['String']['output']>;
name?: Maybe<Scalars['String']>; name?: Maybe<Scalars['String']['output']>;
owner?: Maybe<ProfileModel>; owner?: Maybe<ProfileModel>;
remoteurl?: Maybe<Scalars['String']>; remoteurl?: Maybe<Scalars['String']['output']>;
status?: Maybe<ServerStatus>; 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 */ /** Defines server fields that have a TTL on them, for example last ping */
export type ServerFieldsWithTtl = { export type ServerFieldsWithTtl = {
__typename?: 'ServerFieldsWithTtl'; __typename?: 'ServerFieldsWithTtl';
lastPing?: Maybe<Scalars['String']>; lastPing?: Maybe<Scalars['String']['output']>;
}; };
export type ServerModel = { export type ServerModel = {
apikey: Scalars['String']; apikey: Scalars['String']['output'];
guid: Scalars['String']; guid: Scalars['String']['output'];
lanip: Scalars['String']; lanip: Scalars['String']['output'];
localurl: Scalars['String']; localurl: Scalars['String']['output'];
name: Scalars['String']; name: Scalars['String']['output'];
remoteurl: Scalars['String']; remoteurl: Scalars['String']['output'];
wanip: Scalars['String']; wanip: Scalars['String']['output'];
}; };
export enum ServerStatus { export enum ServerStatus {
@@ -621,16 +623,16 @@ export enum ServerStatus {
export type Service = { export type Service = {
__typename?: 'Service'; __typename?: 'Service';
name?: Maybe<Scalars['String']>; name?: Maybe<Scalars['String']['output']>;
online?: Maybe<Scalars['Boolean']>; online?: Maybe<Scalars['Boolean']['output']>;
uptime?: Maybe<Uptime>; uptime?: Maybe<Uptime>;
version?: Maybe<Scalars['String']>; version?: Maybe<Scalars['String']['output']>;
}; };
export type Subscription = { export type Subscription = {
__typename?: 'Subscription'; __typename?: 'Subscription';
events?: Maybe<Array<Event>>; events?: Maybe<Array<Event>>;
remoteSubscription: Scalars['String']; remoteSubscription: Scalars['String']['output'];
servers: Array<Server>; servers: Array<Server>;
}; };
@@ -641,19 +643,19 @@ export type SubscriptionremoteSubscriptionArgs = {
export type TwoFactorLocal = { export type TwoFactorLocal = {
__typename?: 'TwoFactorLocal'; __typename?: 'TwoFactorLocal';
enabled?: Maybe<Scalars['Boolean']>; enabled?: Maybe<Scalars['Boolean']['output']>;
}; };
export type TwoFactorRemote = { export type TwoFactorRemote = {
__typename?: 'TwoFactorRemote'; __typename?: 'TwoFactorRemote';
enabled?: Maybe<Scalars['Boolean']>; enabled?: Maybe<Scalars['Boolean']['output']>;
}; };
export type TwoFactorWithToken = { export type TwoFactorWithToken = {
__typename?: 'TwoFactorWithToken'; __typename?: 'TwoFactorWithToken';
local?: Maybe<TwoFactorLocal>; local?: Maybe<TwoFactorLocal>;
remote?: Maybe<TwoFactorRemote>; remote?: Maybe<TwoFactorRemote>;
token?: Maybe<Scalars['String']>; token?: Maybe<Scalars['String']['output']>;
}; };
export type TwoFactorWithoutToken = { export type TwoFactorWithoutToken = {
@@ -678,7 +680,7 @@ export type UpdateEvent = {
export type UpdateEventData = { export type UpdateEventData = {
__typename?: 'UpdateEventData'; __typename?: 'UpdateEventData';
apiKey: Scalars['String']; apiKey: Scalars['String']['output'];
type: UpdateType; type: UpdateType;
}; };
@@ -689,7 +691,7 @@ export enum UpdateType {
export type Uptime = { export type Uptime = {
__typename?: 'Uptime'; __typename?: 'Uptime';
timestamp?: Maybe<Scalars['String']>; timestamp?: Maybe<Scalars['String']['output']>;
}; };
export type UserProfileModelWithServers = { export type UserProfileModelWithServers = {
@@ -700,16 +702,16 @@ export type UserProfileModelWithServers = {
export type Vars = { export type Vars = {
__typename?: 'Vars'; __typename?: 'Vars';
expireTime?: Maybe<Scalars['DateTime']>; expireTime?: Maybe<Scalars['DateTime']['output']>;
flashGuid?: Maybe<Scalars['String']>; flashGuid?: Maybe<Scalars['String']['output']>;
regState?: Maybe<RegistrationState>; regState?: Maybe<RegistrationState>;
regTm2?: Maybe<Scalars['String']>; regTm2?: Maybe<Scalars['String']['output']>;
regTy?: Maybe<Scalars['String']>; regTy?: Maybe<Scalars['String']['output']>;
}; };
export type updateDashboardMutationVariables = Exact<{ export type updateDashboardMutationVariables = Exact<{
data: DashboardInput; data: DashboardInput;
apiKey: Scalars['String']; apiKey: Scalars['String']['input'];
}>; }>;
@@ -717,7 +719,7 @@ export type updateDashboardMutation = { __typename?: 'Mutation', updateDashboard
export type sendNotificationMutationVariables = Exact<{ export type sendNotificationMutationVariables = Exact<{
notification: NotificationInput; notification: NotificationInput;
apiKey: Scalars['String']; apiKey: Scalars['String']['input'];
}>; }>;
@@ -725,7 +727,7 @@ export type sendNotificationMutation = { __typename?: 'Mutation', sendNotificati
export type updateNetworkMutationVariables = Exact<{ export type updateNetworkMutationVariables = Exact<{
data: NetworkInput; data: NetworkInput;
apiKey: Scalars['String']; apiKey: Scalars['String']['input'];
}>; }>;

View File

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

View File

@@ -1,7 +1,5 @@
import { FatalAppError } from '@app/core/errors/fatal-error'; import { FatalAppError } from '@app/core/errors/fatal-error';
import { graphqlLogger } from '@app/core/log';
import { modules } from '@app/core'; import { modules } from '@app/core';
import { getters } from '@app/store';
export const getCoreModule = (moduleName: string) => { export const getCoreModule = (moduleName: string) => {
if (!Object.keys(modules).includes(moduleName)) { if (!Object.keys(modules).includes(moduleName)) {
@@ -10,16 +8,3 @@ export const getCoreModule = (moduleName: string) => {
return modules[moduleName]; 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 { 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 { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types';
import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver'; import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver';
import { getters, store } from '@app/store/index'; import { getters, store } from '@app/store/index';
@@ -9,31 +10,26 @@ import { FileLoadStatus } from '@app/store/types';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { decodeJwt } from 'jose'; import { decodeJwt } from 'jose';
export const connectSignIn: MutationResolvers['connectSignIn'] = async ( export const connectSignIn = async (
_, input: ConnectSignInInput
args, ): Promise<boolean> => {
context
) => {
ensurePermission(context.user, {
resource: 'connect',
possession: 'own',
action: 'update',
});
if (getters.emhttp().status === FileLoadStatus.LOADED) { if (getters.emhttp().status === FileLoadStatus.LOADED) {
const result = NODE_ENV === 'development' ? API_KEY_STATUS.API_KEY_VALID : await validateApiKeyWithKeyServer({ const result =
apiKey: args.input.apiKey, NODE_ENV === 'development'
flashGuid: getters.emhttp().var.flashGuid, ? API_KEY_STATUS.API_KEY_VALID
}); : await validateApiKeyWithKeyServer({
apiKey: input.apiKey,
flashGuid: getters.emhttp().var.flashGuid,
});
if (result !== API_KEY_STATUS.API_KEY_VALID) { if (result !== API_KEY_STATUS.API_KEY_VALID) {
throw new GraphQLError( throw new GraphQLError(
`Validating API Key Failed with Error: ${result}` `Validating API Key Failed with Error: ${result}`
); );
} }
const userInfo = args.input.idToken const userInfo = input.idToken
? decodeJwt(args.input.idToken) ? decodeJwt(input.idToken)
: args.input.userInfo ?? null; : input.userInfo ?? null;
if ( if (
!userInfo || !userInfo ||
!userInfo.preferred_username || !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 // @TODO once we deprecate old sign in method, switch this to do all validation requests
await store.dispatch( await store.dispatch(
loginUser({ loginUser({
avatar: typeof userInfo.avatar === 'string' ? userInfo.avatar : '', avatar:
typeof userInfo.avatar === 'string' ? userInfo.avatar : '',
username: userInfo.preferred_username, username: userInfo.preferred_username,
email: userInfo.email, email: userInfo.email,
apikey: args.input.apiKey, apikey: input.apiKey,
}) })
); );
return true; 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 { import {
baseboard,
cpu, cpu,
cpuFlags, cpuFlags,
mem, mem,
memLayout, memLayout,
osInfo, osInfo,
system,
versions, versions,
} from 'systeminformation'; } from 'systeminformation';
import { docker } from '@app/core/utils/clients/docker'; import { docker } from '@app/core/utils/clients/docker';
@@ -16,13 +14,10 @@ import {
type Display, type Display,
type Theme, type Theme,
type Temperature, type Temperature,
type Baseboard,
type Versions, type Versions,
type InfoMemory, type InfoMemory,
type MemoryLayout, type MemoryLayout,
type System,
type Devices, type Devices,
type InfoResolvers,
type Gpu, type Gpu,
} from '@app/graphql/generated/api/types'; } from '@app/graphql/generated/api/types';
import { getters } from '@app/store'; import { getters } from '@app/store';
@@ -33,7 +28,6 @@ import toBytes from 'bytes';
import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version'; import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version';
import { AppError } from '@app/core/errors/app-error'; import { AppError } from '@app/core/errors/app-error';
import { cleanStdout } from '@app/core/utils/misc/clean-stdout'; 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 { execaCommandSync, execa } from 'execa';
import { pathExists } from 'path-exists'; import { pathExists } from 'path-exists';
import { filter as asyncFilter } from 'p-iteration'; import { filter as asyncFilter } from 'p-iteration';
@@ -58,19 +52,22 @@ export const generateApps = async (): Promise<InfoApps> => {
return { installed, started }; return { installed, started };
}; };
const generateOs = async (): Promise<InfoOs> => { export const generateOs = async (): Promise<InfoOs> => {
const os = await osInfo(); const os = await osInfo();
return { return {
...os, ...os,
hostname: getters.emhttp().var.name,
uptime: bootTimestamp.toISOString(), uptime: bootTimestamp.toISOString(),
}; };
}; };
const generateCpu = async (): Promise<InfoCpu> => { export const generateCpu = async (): Promise<InfoCpu> => {
const { cores, physicalCores, speedMin, speedMax, stepping, ...rest } = const { cores, physicalCores, speedMin, speedMax, stepping, ...rest } =
await cpu(); await cpu();
const flags = await cpuFlags().then((flags) => flags.split(' ')); const flags = await cpuFlags()
.then((flags) => flags.split(' '))
.catch(() => []);
return { return {
...rest, ...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 filePath = getters.paths()['dynamix-config'];
const state = loadState<DynamixConfig>(filePath); const state = loadState<DynamixConfig>(filePath);
if (!state) { if (!state) {
@@ -110,9 +107,7 @@ const generateDisplay = async (): Promise<Display> => {
}; };
}; };
const generateBaseboard = async (): Promise<Baseboard> => baseboard(); export const generateVersions = async (): Promise<Versions> => {
const generateVersions = async (): Promise<Versions> => {
const unraid = await getUnraidVersion(); const unraid = await getUnraidVersion();
const softwareVersions = await versions(); 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) => const layout = await memLayout().then((dims) =>
dims.map((dim) => dim as MemoryLayout) dims.map((dim) => dim as MemoryLayout)
); ).catch(() => []);
const info = await mem(); const info = await mem();
let max = info.total; 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. * Set device class to device.
* @param device The device to modify. * @param device The device to modify.
@@ -277,24 +272,24 @@ const generateDevices = async (): Promise<Devices> => {
* @ignore * @ignore
* @private * @private
*/ */
const systemGPUDevices: Promise<Gpu[]> = systemPciDevices().then( const systemGPUDevices: Promise<Gpu[]> = systemPciDevices()
(devices) => { .then((devices) => {
return devices.filter( return devices
(device) => device.class === 'vga' && !device.allowed .filter((device) => device.class === 'vga' && !device.allowed)
).map(entry => { .map((entry) => {
const gpu: Gpu = { const gpu: Gpu = {
blacklisted: entry.allowed, blacklisted: entry.allowed,
class: entry.class, class: entry.class,
id: entry.id, id: entry.id,
productid: entry.product, productid: entry.product,
typeid: entry.typeid, typeid: entry.typeid,
type: entry.manufacturer, type: entry.manufacturer,
vendorname: entry.vendorname vendorname: entry.vendorname,
} };
return gpu; return gpu;
}); });
} })
).catch(() => []); .catch(() => []);
/** /**
* System usb devices. * System usb devices.
@@ -422,13 +417,15 @@ const generateDevices = async (): Promise<Devices> => {
}) ?? []; }) ?? [];
// Get all usb devices // Get all usb devices
const usbDevices = await execa('lsusb').then(async ({ stdout }) => const usbDevices = await execa('lsusb')
parseUsbDevices(stdout) .then(async ({ stdout }) =>
.map(parseDevice) parseUsbDevices(stdout)
.filter(filterBootDrive) .map(parseDevice)
.filter(filterUsbHubs) .filter(filterBootDrive)
.map(sanitizeVendorName) .filter(filterUsbHubs)
); .map(sanitizeVendorName)
)
.catch(() => []);
return usbDevices; return usbDevices;
} catch (error: unknown) { } catch (error: unknown) {
@@ -445,20 +442,3 @@ const generateDevices = async (): Promise<Devices> => {
usb: await getSystemUSBDevices(), 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 { dashboardLogger } from '@app/core/log';
import { generateData } from '@app/common/dashboard/generate-data'; 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 { getters, store } from '@app/store';
import { saveDataPacket } from '@app/store/modules/dashboard'; import { saveDataPacket } from '@app/store/modules/dashboard';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@@ -63,12 +63,10 @@ export const publishToDashboard = async () => {
store.dispatch(saveDataPacket({ lastDataPacket: dataPacket })); store.dispatch(saveDataPacket({ lastDataPacket: dataPacket }));
// Publish the updated data // Publish the updated data
dashboardLogger.addContext('update', dataPacket); dashboardLogger.trace({ dataPacket } , 'Publishing update');
dashboardLogger.trace('Publishing update');
dashboardLogger.removeContext('update');
// Update local clients // Update local clients
await pubsub.publish('dashboard', { await pubsub.publish(PUBSUB_CHANNEL.DASHBOARD, {
dashboard: dataPacket, dashboard: dataPacket,
}); });
if (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 { GraphQLClient } from '@app/mothership/graphql-client';
import { type Nginx } from '@app/core/types/states/nginx'; import { type Nginx } from '@app/core/types/states/nginx';
import { type RootState, store, getters } from '@app/store'; 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 { dashboardLogger, logger } from '@app/core';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { SEND_NETWORK_MUTATION } from '@app/graphql/mothership/mutations'; import { SEND_NETWORK_MUTATION } from '@app/graphql/mothership/mutations';
import { saveNetworkPacket } from '@app/store/modules/dashboard'; import { saveNetworkPacket } from '@app/store/modules/dashboard';
import { ApolloError } from '@apollo/client/core/core.cjs'; 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'; import { ZodError } from 'zod';
interface UrlForFieldInput { interface UrlForFieldInput {
url: string; url: string;
port?: number; port?: number;
portSsl?: number; portSsl?: number;
} }
interface UrlForFieldInputSecure extends UrlForFieldInput { interface UrlForFieldInputSecure extends UrlForFieldInput {
url: string; url: string;
portSsl: number; portSsl: number;
} }
interface UrlForFieldInputInsecure extends UrlForFieldInput { interface UrlForFieldInputInsecure extends UrlForFieldInput {
url: string; url: string;
port: number; port: number;
} }
export const getUrlForField = ({ url, port, portSsl }: UrlForFieldInputInsecure | UrlForFieldInputSecure) => { export const getUrlForField = ({
let portToUse = ''; url,
let httpMode = 'https://'; port,
portSsl,
}: UrlForFieldInputInsecure | UrlForFieldInputSecure) => {
let portToUse = '';
let httpMode = 'https://';
if (!url || url === '') { if (!url || url === '') {
throw new Error('No URL Provided'); throw new Error('No URL Provided');
} }
if (port) { if (port) {
portToUse = port === 80 ? '' : `:${port}`; portToUse = port === 80 ? '' : `:${port}`;
httpMode = 'http://'; httpMode = 'http://';
} else if (portSsl) { } else if (portSsl) {
portToUse = portSsl === 443 ? '' : `:${portSsl}`; portToUse = portSsl === 443 ? '' : `:${portSsl}`;
httpMode = 'https://'; httpMode = 'https://';
} else { } else {
throw new Error(`No ports specified for URL: ${url}`); throw new Error(`No ports specified for URL: ${url}`);
} }
const urlString = `${httpMode}${url}${portToUse}`; const urlString = `${httpMode}${url}${portToUse}`;
try { try {
return new URL(urlString); return new URL(urlString);
} catch (error: unknown) { } catch (error: unknown) {
throw new Error(`Failed to parse URL: ${urlString}`); 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 * @returns a URL, created from the combination of inputs
* @throws Error when the URL cannot be created or the URL is invalid * @throws Error when the URL cannot be created or the URL is invalid
*/ */
export const getUrlForServer = ({ nginx, field }: { nginx: Nginx; field: NginxUrlFields }): URL => { export const getUrlForServer = ({
if (nginx[field]) { nginx,
if (fieldIsFqdn(field)) { field,
return getUrlForField({ url: nginx[field], portSsl: nginx.httpsPort }); }: {
} 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 if (!nginx.sslEnabled) {
return getUrlForField({ url: nginx[field], port: nginx.httpPort }); // Use SSL = no
} return getUrlForField({ url: nginx[field], port: nginx.httpPort });
}
if (nginx.sslMode === 'yes') { if (nginx.sslMode === 'yes') {
return getUrlForField({ url: nginx[field], portSsl: nginx.httpsPort }); return getUrlForField({
} url: nginx[field],
portSsl: nginx.httpsPort,
});
}
if (nginx.sslMode === 'auto') { if (nginx.sslMode === 'auto') {
throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode 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 // eslint-disable-next-line complexity
export const getServerIps = (state: RootState = store.getState()): { urls: AccessUrlInput[]; errors: Error[] } => { export const getServerIps = (
const { nginx } = state.emhttp; state: RootState = store.getState()
const { remote: { wanport } } = state.config; ): { urls: AccessUrlInput[]; errors: Error[] } => {
if (!nginx || Object.keys(nginx).length === 0) { const { nginx } = state.emhttp;
return { urls: [], errors: [new Error('Nginx Not Loaded')] }; const {
} remote: { wanport },
} = state.config;
if (!nginx || Object.keys(nginx).length === 0) {
return { urls: [], errors: [new Error('Nginx Not Loaded')] };
}
const errors: Error[] = []; const errors: Error[] = [];
const urls: AccessUrlInput[] = []; const urls: AccessUrlInput[] = [];
try { try {
// Default URL // Default URL
const defaultUrl = new URL(nginx.defaultUrl); const defaultUrl = new URL(nginx.defaultUrl);
urls.push({ urls.push({
name: 'Default', name: 'Default',
type: URL_TYPE.DEFAULT, type: URL_TYPE.DEFAULT,
ipv4: defaultUrl, ipv4: defaultUrl,
ipv6: defaultUrl, ipv6: defaultUrl,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// Lan IP URL // Lan IP URL
const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' }); const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' });
urls.push({ urls.push({
name: 'LAN IPv4', name: 'LAN IPv4',
type: URL_TYPE.LAN, type: URL_TYPE.LAN,
ipv4: lanIp4Url, ipv4: lanIp4Url,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// Lan IP6 URL // Lan IP6 URL
const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' }); const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' });
urls.push({ urls.push({
name: 'LAN IPv6', name: 'LAN IPv6',
type: URL_TYPE.LAN, type: URL_TYPE.LAN,
ipv4: lanIp6Url, ipv4: lanIp6Url,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// Lan Name URL // Lan Name URL
const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' }); const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' });
urls.push({ urls.push({
name: 'LAN Name', name: 'LAN Name',
type: URL_TYPE.MDNS, type: URL_TYPE.MDNS,
ipv4: lanNameUrl, ipv4: lanNameUrl,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// Lan MDNS URL // Lan MDNS URL
const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' }); const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' });
urls.push({ urls.push({
name: 'LAN MDNS', name: 'LAN MDNS',
type: URL_TYPE.MDNS, type: URL_TYPE.MDNS,
ipv4: lanMdnsUrl, ipv4: lanMdnsUrl,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// Lan FQDN URL // Lan FQDN URL
const lanFqdnUrl = getUrlForServer({ nginx, field: 'lanFqdn' }); const lanFqdnUrl = getUrlForServer({ nginx, field: 'lanFqdn' });
urls.push({ urls.push({
name: 'LAN FQDN', name: 'LAN FQDN',
type: URL_TYPE.LAN, type: URL_TYPE.LAN,
ipv4: lanFqdnUrl, ipv4: lanFqdnUrl,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// Lan FQDN6 URL // Lan FQDN6 URL
const lanFqdn6Url = getUrlForServer({ nginx, field: 'lanFqdn6' }); const lanFqdn6Url = getUrlForServer({ nginx, field: 'lanFqdn6' });
urls.push({ urls.push({
name: 'LAN FQDNv6', name: 'LAN FQDNv6',
type: URL_TYPE.LAN, type: URL_TYPE.LAN,
ipv6: lanFqdn6Url, ipv6: lanFqdn6Url,
}); });
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
errors.push(error); errors.push(error);
} else { } else {
logger.warn('Uncaught error in network resolver', error); logger.warn('Uncaught error in network resolver', error);
} }
} }
try { try {
// WAN FQDN URL // WAN FQDN URL
const wanFqdnUrl = getUrlForField({ url: nginx.wanFqdn, portSsl: Number(wanport || 443) }); const wanFqdnUrl = getUrlForField({
urls.push({ url: nginx.wanFqdn,
name: 'WAN FQDN', portSsl: Number(wanport || 443),
type: URL_TYPE.WAN, });
ipv4: wanFqdnUrl, urls.push({
}); name: 'WAN FQDN',
} catch (error: unknown) { type: URL_TYPE.WAN,
if (error instanceof Error) { ipv4: wanFqdnUrl,
errors.push(error); });
} else { } catch (error: unknown) {
logger.warn('Uncaught error in network resolver', error); if (error instanceof Error) {
} errors.push(error);
} } else {
logger.warn('Uncaught error in network resolver', error);
}
}
try { try {
// WAN FQDN6 URL // WAN FQDN6 URL
const wanFqdn6Url = getUrlForField({ url: nginx.wanFqdn6, portSsl: Number(wanport) }); const wanFqdn6Url = getUrlForField({
urls.push({ url: nginx.wanFqdn6,
name: 'WAN FQDNv6', portSsl: Number(wanport),
type: URL_TYPE.WAN, });
ipv6: wanFqdn6Url, urls.push({
}); name: 'WAN FQDNv6',
} catch (error: unknown) { type: URL_TYPE.WAN,
if (error instanceof Error) { ipv6: wanFqdn6Url,
errors.push(error); });
} else { } catch (error: unknown) {
logger.warn('Uncaught error in network resolver', error); if (error instanceof Error) {
} errors.push(error);
} } else {
logger.warn('Uncaught error in network resolver', error);
}
}
for (const wgFqdn of nginx.wgFqdns) { for (const wgFqdn of nginx.wgFqdns) {
try { try {
// WG FQDN URL // WG FQDN URL
const wgFqdnUrl = getUrlForField({ url: wgFqdn.fqdn, portSsl: nginx.httpsPort }); const wgFqdnUrl = getUrlForField({
urls.push({ url: wgFqdn.fqdn,
name: `WG FQDN ${wgFqdn.id}`, portSsl: nginx.httpsPort,
type: URL_TYPE.WIREGUARD, });
ipv4: wgFqdnUrl, urls.push({
}); name: `WG FQDN ${wgFqdn.id}`,
} catch (error: unknown) { type: URL_TYPE.WIREGUARD,
if (error instanceof Error) { ipv4: wgFqdnUrl,
errors.push(error); });
} else { } catch (error: unknown) {
logger.warn('Uncaught error in network resolver', error); 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) => { const safeUrls = urls
if (curr.success) { .map((url) => AccessUrlInputSchema().safeParse(url))
acc.push(curr.data) .reduce<AccessUrlInput[]>((acc, curr) => {
} else { if (curr.success) {
errors.push(curr.error) acc.push(curr.data);
} } else {
return acc; errors.push(curr.error);
}, []); }
return acc;
}, []);
return { urls: safeUrls, errors }; return { urls: safeUrls, errors };
}; };
export const publishNetwork = async () => { export const publishNetwork = async () => {
try { try {
const client = GraphQLClient.getInstance(); const client = GraphQLClient.getInstance();
const datapacket = getServerIps(); const datapacket = getServerIps();
if (datapacket.errors ) { if (datapacket.errors) {
const zodErrors = datapacket.errors.filter(error => error instanceof ZodError) const zodErrors = datapacket.errors.filter(
if (zodErrors.length) { (error) => error instanceof ZodError
dashboardLogger.warn('Validation Errors Encountered with Network Payload: %s', zodErrors.map(error => error.message).join(',')) );
} if (zodErrors.length) {
} dashboardLogger.warn(
const networkPacket: NetworkInput = { accessUrls: datapacket.urls } 'Validation Errors Encountered with Network Payload: %s',
const validatedNetwork = NetworkInputSchema().parse(networkPacket); zodErrors.map((error) => error.message).join(',')
);
const { lastNetworkPacket } = getters.dashboard(); }
const { apikey: apiKey } = getters.config().remote; }
if (isEqual(JSON.stringify(lastNetworkPacket), JSON.stringify(validatedNetwork))) { const networkPacket: NetworkInput = { accessUrls: datapacket.urls };
dashboardLogger.trace('[DASHBOARD] Skipping Update'); const validatedNetwork = NetworkInputSchema().parse(networkPacket);
} else if (client) {
dashboardLogger.addContext('data', validatedNetwork); const { lastNetworkPacket } = getters.dashboard();
dashboardLogger.info('Sending data packet for network'); const { apikey: apiKey } = getters.config().remote;
dashboardLogger.removeContext('data'); if (
const result = await client.mutate({ isEqual(
mutation: SEND_NETWORK_MUTATION, JSON.stringify(lastNetworkPacket),
variables: { JSON.stringify(validatedNetwork)
apiKey, )
data: validatedNetwork, ) {
}, dashboardLogger.trace('[DASHBOARD] Skipping Update');
}); } else if (client) {
dashboardLogger.addContext('sendNetworkResult', result); dashboardLogger.info(
dashboardLogger.debug('Sent network mutation with %s urls', datapacket.urls.length); { validatedNetwork },
dashboardLogger.removeContext('sendNetworkResult'); 'Sending data packet for network'
store.dispatch(saveNetworkPacket({ lastNetworkPacket: validatedNetwork })); );
} const result = await client.mutate({
} catch (error: unknown) { mutation: SEND_NETWORK_MUTATION,
dashboardLogger.trace('ERROR', error); variables: {
if (error instanceof ApolloError) { apiKey,
dashboardLogger.error('Failed publishing with GQL Errors: %s, \nClient Errors: %s', error.graphQLErrors.map(error => error.message).join(','), error.clientErrors.join(', ')); data: validatedNetwork,
} else { },
dashboardLogger.error(error); });
} 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 ( export const executeRemoteGraphQLQuery = async (
data: RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData'] data: RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData']
) => { ) => {
remoteQueryLogger.addContext('data', data); remoteQueryLogger.debug({ query: data }, 'Executing remote query');
remoteQueryLogger.debug('Executing remote query');
remoteQueryLogger.removeContext('data');
const client = GraphQLClient.getInstance(); const client = GraphQLClient.getInstance();
const apiKey = getters.config().remote.apikey; const apiKey = getters.config().remote.apikey;
const originalBody = data.body; const originalBody = data.body;
@@ -25,18 +23,14 @@ export const executeRemoteGraphQLQuery = async (
upcApiKey: apiKey upcApiKey: apiKey
}); });
if (ENVIRONMENT === 'development') { if (ENVIRONMENT === 'development') {
remoteQueryLogger.addContext('query', parsedQuery.query); remoteQueryLogger.debug({ query: parsedQuery.query }, '[DEVONLY] Running query');
remoteQueryLogger.debug('[DEVONLY] Running query');
remoteQueryLogger.removeContext('query');
} }
const localResult = await localClient.query({ const localResult = await localClient.query({
query: parsedQuery.query, query: parsedQuery.query,
variables: parsedQuery.variables, variables: parsedQuery.variables,
}); });
if (localResult.data) { if (localResult.data) {
remoteQueryLogger.addContext('data', localResult.data); remoteQueryLogger.trace({ data: localResult.data }, 'Got data from remoteQuery request', data.sha256);
remoteQueryLogger.trace('Got data from remoteQuery request', data.sha256);
remoteQueryLogger.removeContext('data')
await client?.mutate({ await client?.mutate({
mutation: SEND_REMOTE_QUERY_RESPONSE, mutation: SEND_REMOTE_QUERY_RESPONSE,
@@ -77,8 +71,6 @@ export const executeRemoteGraphQLQuery = async (
} catch (error) { } catch (error) {
remoteQueryLogger.warn('Could not respond %o', 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.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 { type Mutation {
"""Start array""" """Start array"""
startArray: Array @func(module: "updateArray", data: { state: "start" }) startArray: Array
"""Stop array""" """Stop array"""
stopArray: Array @func(module: "updateArray", data: { state: "stop" }) stopArray: Array
"""Add new disk to 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.""" """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 mountArrayDisk(id: ID!): Disk
unmountArrayDisk(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