Compare commits

...

316 Commits

Author SHA1 Message Date
Zack Spear
5972a15f48 test: local dev dynamic serverState 2023-12-20 13:13:00 -05: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
258 changed files with 21992 additions and 11571 deletions

View File

@@ -81,10 +81,10 @@ jobs:
- name: Build Docker Compose
run: |
docker network create mothership_default
docker-compose build builder
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build builder
- name: Run Docker Compose
run: docker-compose run builder npm run coverage
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run builder npm run coverage
lint-web:
defaults:

3
.gitignore vendored
View File

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

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

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

View File

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

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

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

1
api/.dockerignore Normal file
View File

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

View File

@@ -2,6 +2,16 @@
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.2.3](https://github.com/unraid/api/compare/v3.2.2...v3.2.3) (2023-09-08)
### Bug Fixes
* **plg:** preserve & restore new plg files on install / remove ([7e1f59a](https://github.com/unraid/api/commit/7e1f59afd218235934a53ac4ea6fd166689269a4))
* remove API restart command ([0eb1530](https://github.com/unraid/api/commit/0eb1530d649647f47d26de459e394fd48e79b071))
* **web:** add missing translations ([0227a1e](https://github.com/unraid/api/commit/0227a1ed1bdf953eae7784fccf04dd94995f5114))
* **web:** htmlspecialchars name & description ([a874fd8](https://github.com/unraid/api/commit/a874fd8f4b2fdf5d261f3b167452532bf09059ab))
### [3.2.2](https://github.com/unraid/api/compare/v3.2.1...v3.2.2) (2023-09-07)

View File

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

View File

@@ -1,5 +1,6 @@
[api]
version="3.1.1+8efc0992"
version="3.2.3+075d7f25"
extraOrigins=""
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
@@ -15,5 +16,6 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken=""
accesstoken=""
refreshtoken=""
dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"

View File

@@ -1,5 +1,6 @@
[api]
version="3.1.1+8efc0992"
version="3.2.3+075d7f25"
extraOrigins=""
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
@@ -16,6 +17,7 @@ idtoken=""
accesstoken=""
refreshtoken=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus]

View File

@@ -45,12 +45,17 @@ services:
entrypoint: /bin/bash
environment:
- IS_DOCKER=true
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
profiles:
- builder
builder:
image: unraid-api:builder
environment:
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
build:
context: .
target: builder

7077
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "3.2.2",
"version": "3.2.3",
"main": "dist/index.js",
"bin": "dist/unraid-api.cjs",
"type": "module",
@@ -26,11 +26,11 @@
"compile": "tsup --config ./tsup.config.ts",
"bundle": "pkg . --public",
"build": "npm run compile && npm run bundle",
"build:docker": "docker-compose run --rm builder",
"build:docker": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder",
"build-pkg": "./scripts/build.mjs",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config",
"codegen:local": "MOTHERSHIP_GRAPHQL_LINK='http://localhost:3000/graphql' graphql-codegen-esm --config codegen.yml --watch",
"codegen:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 MOTHERSHIP_GRAPHQL_LINK='https://mothership.localhost/ws' graphql-codegen-esm --config codegen.yml --watch",
"tsc": "tsc --noEmit",
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
@@ -41,14 +41,16 @@
"release": "standard-version",
"typesync": "typesync",
"install:unraid": "./scripts/install-in-unraid.sh",
"start:plugin": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin": "INTROSPECTION=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs start --debug'",
"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 --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
"start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'",
"start:docker": "docker compose run --rm builder-interactive",
"docker:dev": "docker-compose run --rm --service-ports dev",
"docker:test": "docker-compose run --rm builder npm run test"
"build:dev": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build dev",
"docker:dev": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm --service-ports dev",
"docker:test": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder npm run test"
},
"files": [
".env.staging",
@@ -59,11 +61,17 @@
"dependencies": {
"@apollo/client": "^3.7.12",
"@apollo/server": "^4.6.0",
"@graphql-codegen/client-preset": "^3.0.0",
"@as-integrations/fastify": "^2.1.1",
"@graphql-codegen/client-preset": "^4.0.0",
"@graphql-tools/load-files": "^6.6.1",
"@graphql-tools/merge": "^8.4.0",
"@graphql-tools/schema": "^9.0.17",
"@graphql-tools/utils": "^9.2.1",
"@nestjs/apollo": "^12.0.11",
"@nestjs/core": "^10.2.9",
"@nestjs/graphql": "^12.0.11",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-fastify": "^10.2.9",
"@reduxjs/toolkit": "^1.9.5",
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
@@ -75,8 +83,9 @@
"bytes": "^3.1.2",
"cacheable-lookup": "^6.1.0",
"catch-exit": "^1.2.2",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cli-table": "^0.3.11",
"command-exists": "^1.2.9",
"convert": "^4.10.0",
@@ -87,14 +96,14 @@
"dotenv": "^16.0.3",
"express": "^4.18.2",
"find-process": "^1.4.7",
"graphql": "^16.6.0",
"graphql": "^16.8.1",
"graphql-fields": "^2.0.3",
"graphql-scalars": "^1.21.3",
"graphql-subscriptions": "^2.0.0",
"graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2",
"graphql-type-uuid": "^0.2.0",
"graphql-ws": "^5.12.1",
"graphql-ws": "^5.14.2",
"htpasswd-js": "^1.0.2",
"ini": "^4.1.0",
"ip": "^1.1.8",
@@ -103,17 +112,22 @@
"multi-ini": "^2.2.0",
"mustache": "^4.2.0",
"nanobus": "^4.5.0",
"nest-access-control": "^3.1.0",
"nestjs-pino": "^3.5.0",
"node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2",
"openid-client": "^5.4.0",
"p-iteration": "^1.1.8",
"p-retry": "^4.6.2",
"passport-http-header-strategy": "^1.1.0",
"pidusage": "^3.0.2",
"pino": "^8.16.2",
"pino-http": "^8.5.1",
"pino-pretty": "^10.2.3",
"reflect-metadata": "^0.1.13",
"request": "^2.88.2",
"semver": "^7.4.0",
"stoppable": "^1.1.0",
"subscriptions-transport-ws": "^0.11.0",
"systeminformation": "^5.21.2",
"ts-command-line-args": "^2.5.0",
"uuid": "^9.0.0",
@@ -124,15 +138,16 @@
},
"devDependencies": {
"@babel/runtime": "^7.21.0",
"@graphql-codegen/add": "^4.0.1",
"@graphql-codegen/cli": "^3.3.0",
"@graphql-codegen/fragment-matcher": "^4.0.1",
"@graphql-codegen/import-types-preset": "^2.2.6",
"@graphql-codegen/typed-document-node": "^4.0.0",
"@graphql-codegen/typescript": "^3.0.3",
"@graphql-codegen/typescript-operations": "^3.0.3",
"@graphql-codegen/typescript-resolvers": "3.2.1",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typed-document-node": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.0.0",
"@graphql-codegen/typescript-resolvers": "4.0.1",
"@graphql-typed-document-node/core": "^3.2.0",
"@nestjs/testing": "^10.2.10",
"@swc/core": "^1.3.81",
"@types/async-exit-hook": "^2.0.0",
"@types/btoa": "^1.2.3",
@@ -174,7 +189,6 @@
"graphql-codegen-typescript-validation-schema": "^0.11.0",
"ip-regex": "^5.0.0",
"json-difference": "^1.9.1",
"log4js": "^6.9.1",
"map-obj": "^5.0.2",
"p-props": "^5.0.0",
"path-exists": "^5.0.0",
@@ -182,7 +196,6 @@
"pkg": "^5.8.1",
"pretty-bytes": "^6.1.0",
"pretty-ms": "^8.0.0",
"serialize-error": "^11.0.2",
"standard-version": "^9.5.0",
"tsup": "^7.0.0",
"typescript": "^4.9.4",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,167 @@
import { test, expect } from 'vitest';
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer';
import { initialState } from '@app/store/modules/config';
import { cloneDeep } from 'lodash';
test('it creates a FLASH config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
});
test('it creates a MEMORY config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.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://staging.connect.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,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

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

View File

@@ -0,0 +1,12 @@
import { setEnv } from '@app/cli/set-env';
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 () => {
setEnv('LOG_TRANSPORT', 'stdout');
await stop();
await start();
};

View File

@@ -6,12 +6,15 @@ import { logToSyslog } from '@app/cli/log-to-syslog';
import { getters } from '@app/store';
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
import { API_VERSION } from '@app/environment';
import { setEnv } from '@app/cli/set-env';
/**
* Start a new API process.
*/
export const start = async () => {
// Set process title
setEnv('LOG_TRANSPORT', 'stdout');
process.title = 'unraid-api';
const runningProcesses = await getAllUnraidApiPids();
if (runningProcesses.length > 0) {

View File

@@ -9,7 +9,7 @@ import pRetry from 'p-retry';
*/
export const stop = async () => {
setEnv('LOG_TYPE', 'raw');
setEnv('LOG_TRANSPORT', 'stdout');
try {
await pRetry(async (attempts) => {

View File

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

View File

@@ -3,7 +3,7 @@ import { uniq } from 'lodash';
import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network';
import { FileLoadStatus } from '@app/store/types';
import { logger } from '../core';
import { ENVIRONMENT, INTROSPECTION } from '@app/environment';
import { GRAPHQL_INTROSPECTION } from '@app/environment';
const getAllowedSocks = (): string[] => [
// Notifier bridge
@@ -76,7 +76,7 @@ const getConnectOrigins = () : string[] => {
}
const getApolloSandbox = (): string[] => {
if (INTROSPECTION || ENVIRONMENT === 'development') {
if (GRAPHQL_INTROSPECTION) {
return ['https://studio.apollographql.com'];
}
return [];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -86,18 +86,8 @@ const parseDisk = async (
* Get all disks.
*/
export const getDisks = async (
context: Context,
options?: { temperature: boolean }
): Promise<Disk[]> => {
const { user } = context;
// Check permissions
ensurePermission(user, {
resource: 'disk',
action: 'read',
possession: 'any',
});
// Return all fields but temperature
if (options?.temperature === false) {
const partitions = await blockDevices().then((devices) =>

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
// Created from 'create-ts-index'
export * from './array';
export * from './debug';
export * from './disks';
export * from './docker';
export * from './services';
export * from './settings';
export * from './shares';
export * from './users';
export * from './vms';
export * from './add-share';
export * from './add-user';
export * from './get-all-shares';
export * from './get-apps';
export * from './get-devices';
export * from './get-disks';
export * from './get-me';
export * from './get-parity-history';
export * from './get-permissions';
export * from './get-services';
export * from './get-users';
export * from './get-welcome';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,65 @@
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
}
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,34 @@
type Query {
server: Server
servers: [Server!]!
}
type Subscription {
server: Server
}
enum ServerStatus {
online
offline
never_connected
}
type ProfileModel {
userId: ID
username: String
url: String
avatar: String
}
type Server {
owner: ProfileModel!
guid: String!
apikey: String!
name: String!
status: ServerStatus!
wanip: String!
lanip: String!
localurl: String!
remoteurl: String!
}

View File

@@ -1,6 +1,6 @@
type Query {
"""Network Shares"""
shares: [Share] @func(module: "getAllShares")
shares: [Share]
}
type Subscription {

View File

@@ -0,0 +1,62 @@
type Query {
unassignedDevices: [UnassignedDevice]
}
type Subscription {
unassignedDevices: [UnassignedDevice!]
}
type UnassignedDevice {
devlinks: String
devname: String
devpath: String
devtype: String
idAta: String
idAtaDownloadMicrocode: String
idAtaFeatureSetAam: String
idAtaFeatureSetAamCurrentValue: String
idAtaFeatureSetAamEnabled: String
idAtaFeatureSetAamVendorRecommendedValue: String
idAtaFeatureSetApm: String
idAtaFeatureSetApmCurrentValue: String
idAtaFeatureSetApmEnabled: String
idAtaFeatureSetHpa: String
idAtaFeatureSetHpaEnabled: String
idAtaFeatureSetPm: String
idAtaFeatureSetPmEnabled: String
idAtaFeatureSetPuis: String
idAtaFeatureSetPuisEnabled: String
idAtaFeatureSetSecurity: String
idAtaFeatureSetSecurityEnabled: String
idAtaFeatureSetSecurityEnhancedEraseUnitMin: String
idAtaFeatureSetSecurityEraseUnitMin: String
idAtaFeatureSetSmart: String
idAtaFeatureSetSmartEnabled: String
idAtaRotationRateRpm: String
idAtaSata: String
idAtaSataSignalRateGen1: String
idAtaSataSignalRateGen2: String
idAtaWriteCache: String
idAtaWriteCacheEnabled: String
idBus: String
idModel: String
idModelEnc: String
idPartTableType: String
idPath: String
idPathTag: String
idRevision: String
idSerial: String
idSerialShort: String
idType: String
idWwn: String
idWwnWithExtension: String
major: String
minor: String
subsystem: String
usecInitialized: String
partitions: [Partition]
temp: Int
name: String
mounted: Boolean
mount: Mount
}

View File

@@ -0,0 +1,17 @@
type Query {
"""Current user account"""
me: Me
}
"""The current user"""
type Me implements UserAccount {
id: ID!
name: String!
description: String!
roles: String!
permissions: JSON
}
type Subscription {
me: Me
}

View File

@@ -0,0 +1,50 @@
interface UserAccount {
id: ID!
name: String!
description: String!
roles: String!
}
input usersInput {
slim: Boolean
}
type Query {
"""User account"""
user(id: ID!): User
"""User accounts"""
users(input: usersInput): [User!]!
}
input addUserInput {
name: String!
password: String!
description: String
}
input deleteUserInput {
name: String!
}
type Mutation {
"""Add a new user"""
addUser(input: addUserInput!): User
"""Delete a user"""
deleteUser(input: deleteUserInput!): User
}
type Subscription {
user(id: ID!): User!
users: [User]!
}
"""A local user account"""
type User implements UserAccount {
id: ID!
"""A unique name for the user"""
name: String!
description: String!
roles: String!
"""If the account has a password set"""
password: Boolean
}

View File

@@ -0,0 +1,291 @@
type Query {
vars: Vars
}
type Subscription {
vars: Vars!
}
enum ConfigErrorState {
UNKNOWN_ERROR
INVALID
NO_KEY_SERVER
WITHDRAWN
}
type Vars {
"""
Unraid version
"""
version: String
maxArraysz: Int
maxCachesz: Int
"""
Machine hostname
"""
name: String
timeZone: String
comment: String
security: String
workgroup: String
domain: String
domainShort: String
hideDotFiles: Boolean
localMaster: Boolean
enableFruit: String
"""
Should a NTP server be used for time sync?
"""
useNtp: Boolean
"""
NTP Server 1
"""
ntpServer1: String
"""
NTP Server 2
"""
ntpServer2: String
"""
NTP Server 3
"""
ntpServer3: String
"""
NTP Server 4
"""
ntpServer4: String
domainLogin: String
sysModel: String
sysArraySlots: Int
sysCacheSlots: Int
sysFlashSlots: Int
useSsl: Boolean
"""
Port for the webui via HTTP
"""
port: Int
"""
Port for the webui via HTTPS
"""
portssl: Int
localTld: String
bindMgt: Boolean
"""
Should telnet be enabled?
"""
useTelnet: Boolean
porttelnet: Int
useSsh: Boolean
portssh: Int
startPage: String
startArray: Boolean
spindownDelay: String
queueDepth: String
spinupGroups: Boolean
defaultFormat: String
defaultFsType: String
shutdownTimeout: Int
luksKeyfile: String
pollAttributes: String
pollAttributesDefault: String
pollAttributesStatus: String
nrRequests: Int
nrRequestsDefault: Int
nrRequestsStatus: String
mdNumStripes: Int
mdNumStripesDefault: Int
mdNumStripesStatus: String
mdSyncWindow: Int
mdSyncWindowDefault: Int
mdSyncWindowStatus: String
mdSyncThresh: Int
mdSyncThreshDefault: Int
mdSyncThreshStatus: String
mdWriteMethod: Int
mdWriteMethodDefault: String
mdWriteMethodStatus: String
shareDisk: String
shareUser: String
shareUserInclude: String
shareUserExclude: String
shareSmbEnabled: Boolean
shareNfsEnabled: Boolean
shareAfpEnabled: Boolean
shareInitialOwner: String
shareInitialGroup: String
shareCacheEnabled: Boolean
shareCacheFloor: String
shareMoverSchedule: String
shareMoverLogging: Boolean
fuseRemember: String
fuseRememberDefault: String
fuseRememberStatus: String
fuseDirectio: String
fuseDirectioDefault: String
fuseDirectioStatus: String
shareAvahiEnabled: Boolean
shareAvahiSmbName: String
shareAvahiSmbModel: String
shareAvahiAfpName: String
shareAvahiAfpModel: String
safeMode: Boolean
startMode: String
configValid: Boolean
configError: ConfigErrorState
joinStatus: String
deviceCount: Int
flashGuid: String
flashProduct: String
flashVendor: String
regCheck: String
regFile: String
regGuid: String
regTy: String
regState: RegistrationState
"""
Registration owner
"""
regTo: String
regTm: String
regTm2: String
regGen: String
sbName: String
sbVersion: String
sbUpdated: String
sbEvents: Int
sbState: String
sbClean: Boolean
sbSynced: Int
sbSyncErrs: Int
sbSynced2: Int
sbSyncExit: String
sbNumDisks: Int
mdColor: String
mdNumDisks: Int
mdNumDisabled: Int
mdNumInvalid: Int
mdNumMissing: Int
mdNumNew: Int
mdNumErased: Int
mdResync: Int
mdResyncCorr: String
mdResyncPos: String
mdResyncDb: String
mdResyncDt: String
mdResyncAction: String
mdResyncSize: Int
mdState: String
mdVersion: String
cacheNumDevices: Int
cacheSbNumDisks: Int
fsState: String
"""
Human friendly string of array events happening
"""
fsProgress: String
"""
Percentage from 0 - 100 while upgrading a disk or swapping parity drives
"""
fsCopyPrcnt: Int
fsNumMounted: Int
fsNumUnmountable: Int
fsUnmountableMask: String
"""
Total amount of user shares
"""
shareCount: Int
"""
Total amount shares with SMB enabled
"""
shareSmbCount: Int
"""
Total amount shares with NFS enabled
"""
shareNfsCount: Int
"""
Total amount shares with AFP enabled
"""
shareAfpCount: Int
shareMoverActive: Boolean
csrfToken: String
}
enum mdState {
SWAP_DSBL
STARTED
}
enum registrationType {
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
INVALID
TRIAL
}
enum RegistrationState {
TRIAL
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
"""
Trial Expired
"""
EEXPIRED
"""
GUID Error
"""
EGUID
"""
Multiple License Keys Present
"""
EGUID1
"""
Invalid installation
"""
ETRIAL
"""
No Keyfile
"""
ENOKEYFILE
"""
No Keyfile
"""
ENOKEYFILE1
"""
Missing key file
"""
ENOKEYFILE2
"""
No Flash
"""
ENOFLASH
ENOFLASH1
ENOFLASH2
ENOFLASH3
ENOFLASH4
ENOFLASH5
ENOFLASH6
ENOFLASH7
"""
BLACKLISTED
"""
EBLACKLISTED
"""
BLACKLISTED
"""
EBLACKLISTED1
"""
BLACKLISTED
"""
EBLACKLISTED2
"""
Trial Requires Internet Connection
"""
ENOCONN
}

View File

@@ -39,7 +39,7 @@ export const createSubscription = (channel: string, resource?: string) => ({
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const getLocalServer = (getState = store.getState): Array<Server> => {
export const getLocalServer = (getState = store.getState): Array<Server> => {
const { emhttp, config, minigraph } = getState();
const guid = emhttp.var.regGuid;
const { name } = emhttp.var;
@@ -58,7 +58,7 @@ const getLocalServer = (getState = store.getState): Array<Server> => {
},
guid,
apikey: config.remote.apikey ?? '',
name,
name: name ?? 'Local Server',
status:
minigraph.status === MinigraphStatus.CONNECTED
? ServerStatus.ONLINE

View File

@@ -1,46 +0,0 @@
import { mergeTypeDefs } from '@graphql-tools/merge';
import { gql } from 'graphql-tag';
import { typeDefs } from '@app/graphql/schema/index';
export const baseTypes = [
gql`
scalar JSON
scalar Long
scalar UUID
scalar DateTime
scalar Port
directive @subscription(channel: String!) on FIELD_DEFINITION
type Welcome {
message: String!
}
type Query {
# This should always be available even for guest users
welcome: Welcome @func(module: "getWelcome")
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!
}
`,
];
export const types = mergeTypeDefs([
...baseTypes,
typeDefs,
]);
export default types;

View File

@@ -12,20 +12,23 @@ import { loadStateFiles } from '@app/store/modules/emhttp';
import { StateManager } from '@app/store/watch/state-watch';
import { setupRegistrationKeyWatch } from '@app/store/watch/registration-watch';
import { loadRegistrationKey } from '@app/store/modules/registration';
import { createApolloExpressServer } from '@app/server';
import { unlinkSync } from 'fs';
import { fileExistsSync } from '@app/core/utils/files/file-exists';
import { PORT, environment } from '@app/environment';
import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event';
import { PingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs';
import { type BaseContext, type ApolloServer } from '@apollo/server';
import { setupDynamixConfigWatch } from '@app/store/watch/dynamix-config-watch';
import { setupVarRunWatch } from '@app/store/watch/var-run-watch';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file';
import { startMiddlewareListeners } from '@app/store/listeners/listener-middleware';
import { validateApiKeyIfPresent } from '@app/store/listeners/api-key-listener';
import { bootstrapNestServer } from '@app/unraid-api/main';
import { type NestFastifyApplication } from '@nestjs/platform-fastify';
import { type RawServerDefault } from 'fastify';
import { setupLogRotation } from '@app/core/logrotate/setup-logrotate';
import * as env from '@app/environment';
let server: ApolloServer<BaseContext>;
let server: NestFastifyApplication<RawServerDefault>;
const unlinkUnixPort = () => {
if (isNaN(parseInt(PORT, 10))) {
@@ -36,6 +39,9 @@ const unlinkUnixPort = () => {
void am(
async () => {
environment.IS_MAIN_PROCESS = true;
logger.debug('ENV %o', env);
const cacheable = new CacheableLookup();
Object.assign(global, { WebSocket: require('ws') });
@@ -47,6 +53,8 @@ void am(
// Must occur before config is loaded to ensure that the handler can fix broken configs
await startStoreSync();
await setupLogRotation();
// Load my servers config file into store
await store.dispatch(loadConfigFile());
@@ -78,8 +86,7 @@ void am(
unlinkUnixPort();
// Start webserver
server = await createApolloExpressServer();
server = await bootstrapNestServer();
PingTimeoutJobs.init();
startMiddlewareListeners();
@@ -88,6 +95,7 @@ void am(
// On process exit stop HTTP server - this says it supports async but it doesnt seem to
exitHook(() => {
server?.close?.();
// If port is unix socket, delete socket before exiting
unlinkUnixPort();
@@ -96,16 +104,11 @@ void am(
});
},
async (error: NodeJS.ErrnoException) => {
// Log error to syslog
logger.error('API-GLOBAL-ERROR', error);
shutdownApiEvent();
// Stop server
logger.debug('Stopping HTTP server');
logger.error('API-GLOBAL-ERROR %s %s', error.message, error.stack);
if (server) {
await server.stop();
await server?.close?.();
}
shutdownApiEvent();
// Kill application
process.exitCode = 1;
}

View File

@@ -12,7 +12,7 @@ import { type Response } from 'got';
export const validateApiKeyWithKeyServer = async ({ flashGuid, apiKey }: { flashGuid: string; apiKey: string }): Promise<API_KEY_STATUS> => {
// If we're still loading config state, just return the config is loading
ksLog.log('Validating API Key with KeyServer');
ksLog.info('Validating API Key with KeyServer');
// Send apiKey, etc. to key-server for verification
let response: Response<string>;
@@ -22,9 +22,7 @@ export const validateApiKeyWithKeyServer = async ({ flashGuid, apiKey }: { flash
apikey: apiKey,
});
} catch (error: unknown) {
ksLog.addContext('networkError', error);
ksLog.error('Caught error reaching Key Server');
ksLog.removeContext('networkError');
ksLog.error({ error }, 'Caught error reaching Key Server');
return API_KEY_STATUS.NETWORK_ERROR;
}

View File

@@ -33,7 +33,7 @@ const getWebsocketWithMothershipHeaders = () => {
headers: getMothershipWebsocketHeaders(),
});
}
}
};
};
const delayFn = buildDelayFunction({
@@ -89,7 +89,7 @@ export class GraphQLClient {
const isStateValid = isAPIStateDataFullyLoaded() && isApiKeyValid();
if (!GraphQLClient.instance && isStateValid) {
minigraphLogger.debug("Creating a new Apollo Client Instance");
minigraphLogger.debug('Creating a new Apollo Client Instance');
GraphQLClient.instance = GraphQLClient.createGraphqlClient();
}
@@ -128,10 +128,10 @@ export class GraphQLClient {
logoutUser({ reason: 'Invalid API Key on Mothership' })
);
}
const getDelay = delayFn(count);
store.dispatch(setMothershipTimeout(getDelay));
minigraphLogger.info('Delay currently is', getDelay);
minigraphLogger.info('Delay currently is: %i', getDelay);
return getDelay;
},
attempts: { max: Infinity },

View File

@@ -0,0 +1,56 @@
import { OAUTH_CLIENT_ID, OAUTH_OPENID_CONFIGURATION_URL } from '@app/consts';
import { mothershipLogger } from '@app/core';
import { getters, store } from '@app/store';
import { updateAccessTokens } from '@app/store/modules/config';
import { Cron, Expression, Initializer } from '@reflet/cron';
import { Issuer } from 'openid-client';
export class TokenRefresh extends Initializer<typeof TokenRefresh> {
private issuer: Issuer | null = null;
@Cron.PreventOverlap
@Cron(Expression.EVERY_DAY_AT_NOON)
@Cron.RunOnInit
async getNewTokens() {
const {
remote: { refreshtoken },
} = getters.config();
if (!refreshtoken) {
mothershipLogger.debug('No JWT refresh token configured');
return;
}
if (!this.issuer) {
try {
this.issuer = await Issuer.discover(
OAUTH_OPENID_CONFIGURATION_URL
);
mothershipLogger.trace(
'Discovered Issuer %s',
this.issuer.issuer
);
} catch (error: unknown) {
mothershipLogger.error({ error }, 'Failed to discover issuer');
return;
}
}
const client = new this.issuer.Client({
client_id: OAUTH_CLIENT_ID,
token_endpoint_auth_method: 'none',
});
const newTokens = await client.refresh(refreshtoken);
mothershipLogger.debug('tokens %o', newTokens);
if (newTokens.access_token && newTokens.id_token) {
store.dispatch(
updateAccessTokens({
accesstoken: newTokens.access_token,
idtoken: newTokens.id_token,
})
);
}
}
}

View File

@@ -0,0 +1,135 @@
/* eslint-disable max-depth */
import { minigraphLogger, mothershipLogger } from '@app/core/log';
import { GraphQLClient } from './graphql-client';
import { store } from '@app/store';
import {
startDashboardProducer,
stopDashboardProducer,
} from '@app/store/modules/dashboard';
import {
EVENTS_SUBSCRIPTION,
RemoteAccess_Fragment,
RemoteGraphQL_Fragment,
} from '@app/graphql/mothership/subscriptions';
import { ClientType } from '@app/graphql/generated/client/graphql';
import { notNull } from '@app/utils';
import { handleRemoteAccessEvent } from '@app/store/actions/handle-remote-access-event';
import { useFragment } from '@app/graphql/generated/client/fragment-masking';
import { handleRemoteGraphQLEvent } from '@app/store/actions/handle-remote-graphql-event';
import {
setSelfDisconnected,
setSelfReconnected,
} from '@app/store/modules/minigraph';
export const subscribeToEvents = async (apiKey: string) => {
minigraphLogger.info('Subscribing to Events');
const client = GraphQLClient.getInstance();
if (!client) {
throw new Error('Unable to use client - state must not be loaded');
}
const eventsSub = client.subscribe({
query: EVENTS_SUBSCRIPTION,
fetchPolicy: 'no-cache',
});
eventsSub.subscribe(async ({ data, errors }) => {
if (errors) {
mothershipLogger.error(
'GraphQL Error with events subscription: %s',
errors.join(',')
);
} else if (data) {
mothershipLogger.trace({ events: data.events }, 'Got events from mothership');
for (const event of data.events?.filter(notNull) ?? []) {
switch (event.__typename) {
case 'ClientConnectedEvent': {
const {
connectedData: { type, apiKey: eventApiKey },
} = event;
// Another server connected to Mothership
if (type === ClientType.API) {
if (eventApiKey === apiKey) {
// We are online, clear timeout waiting if it's set
store.dispatch(setSelfReconnected());
}
}
// Dashboard Connected to Mothership
if (
type === ClientType.DASHBOARD &&
apiKey === eventApiKey
) {
store.dispatch(startDashboardProducer());
}
break;
}
case 'ClientDisconnectedEvent': {
const {
disconnectedData: { type, apiKey: eventApiKey },
} = event;
// Server Disconnected From Mothership
if (type === ClientType.API) {
if (eventApiKey === apiKey) {
store.dispatch(setSelfDisconnected());
}
}
// The dashboard was closed or went idle
if (
type === ClientType.DASHBOARD &&
apiKey === eventApiKey
) {
store.dispatch(stopDashboardProducer());
}
break;
}
case 'RemoteAccessEvent': {
const eventAsRemoteAccessEvent = useFragment(
RemoteAccess_Fragment,
event
);
if (eventAsRemoteAccessEvent.data.apiKey === apiKey) {
void store.dispatch(
handleRemoteAccessEvent(
eventAsRemoteAccessEvent
)
);
}
break;
}
case 'RemoteGraphQLEvent': {
const eventAsRemoteGraphQLEvent = useFragment(
RemoteGraphQL_Fragment,
event
);
// No need to check API key here anymore
void store.dispatch(
handleRemoteGraphQLEvent(eventAsRemoteGraphQLEvent)
);
break;
}
case 'UpdateEvent': {
break;
}
default:
break;
}
}
}
});
};

View File

@@ -0,0 +1,25 @@
import { type DelayFunctionOptions } from '@apollo/client/link/retry/delayFunction';
export function buildDelayFunction(
delayOptions?: DelayFunctionOptions,
): (count: number) => number {
const { initial = 10_000, jitter = true, max = Infinity } = delayOptions ?? {};
// If we're jittering, baseDelay is half of the maximum delay for that
// attempt (and is, on average, the delay we will encounter).
// If we're not jittering, adjust baseDelay so that the first attempt
// lines up with initialDelay, for everyone's sanity.
const baseDelay = jitter ? initial : initial / 2;
return (count: number) => {
// eslint-disable-next-line no-mixed-operators
let delay = Math.min(max, baseDelay * 2 ** count);
if (jitter) {
// We opt for a full jitter approach for a mostly uniform distribution,
// but bound it within initialDelay and delay for everyone's sanity.
// eslint-disable-next-line operator-assignment
delay = Math.random() * delay;
}
return Math.round(delay);
};
}

View File

@@ -1,76 +0,0 @@
import { type NextFunction, type Request, type Response } from 'express';
import { logger } from '@app/core';
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { LOG_CORS } from '@app/environment';
const getOriginGraphqlError = () => ({
data: null,
errors: [
{
message:
'The CORS policy for this site does not allow access from the specified Origin.',
},
],
});
/**
* Middleware to check a users origin and send a GraphQL error if they are not using a valid one
* @param req Express Request
* @param res Express Response
* @param next Express NextFunction
* @returns void
*/
export const originMiddleware = (
req: Request,
res: Response,
next: NextFunction
): void => {
if (req.method === 'GET' && req.query.apiKey && !req.headers.origin) {
// Bypass GET request headers on requests to the log endpoint
return next();
}
// Dev Mode Bypass
const origin = req.get('Origin')?.toLowerCase() ?? '';
const allowedOrigins = getAllowedOrigins();
if (process.env.BYPASS_CORS_CHECKS === 'true') {
logger.addContext('cors', allowedOrigins);
logger.warn(`BYPASSING_CORS_CHECK: %o`, req.headers);
logger.removeContext('cors');
next();
return;
} else {
if (LOG_CORS) {
logger.addContext('origins', allowedOrigins.join(', '));
logger.trace(`Current Origin: ${origin ?? 'undefined'}`);
logger.removeContext('origins');
}
}
// Disallow requests with no origin
// (like mobile apps, curl requests or viewing /graphql directly)
if (!origin) {
logger.debug('No origin provided, denying CORS!');
res.status(403).send(getOriginGraphqlError());
return;
}
if (LOG_CORS) {
logger.trace(`📒 Checking "${origin}" for CORS access.`);
}
// Only allow known origins
if (!allowedOrigins.includes(origin)) {
logger.error(
'❌ %s is not in the allowed origins list, denying CORS!',
origin
);
res.status(403).send(getOriginGraphqlError());
return;
}
if (LOG_CORS) {
logger.trace('✔️ Origin check passed, granting CORS!');
}
next();
};

View File

@@ -1,355 +0,0 @@
import path from 'path';
import cors from 'cors';
import { watch } from 'chokidar';
import express, { json, type Request, type Response } from 'express';
import http from 'http';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { logger, pubsub, graphqlLogger } from '@app/core';
import { verifyTwoFactorToken } from '@app/common/two-factor';
import display from '@app/graphql/resolvers/query/display';
import { getters } from '@app/store';
import { schema } from '@app/graphql/schema';
import { execute, subscribe } from 'graphql';
import { GRAPHQL_WS, SubscriptionServer } from 'subscriptions-transport-ws';
import { wsHasConnected, wsHasDisconnected } from '@app/ws';
import { apiKeyToUser } from '@app/graphql';
import { randomUUID } from 'crypto';
import { getServerAddress } from '@app/common/get-server-address';
import { originMiddleware } from '@app/originMiddleware';
import { API_VERSION, GRAPHQL_INTROSPECTION, PORT } from '@app/environment';
import {
getBannerPathIfPresent,
getCasePathIfPresent,
} from '@app/core/utils/images/image-file-helpers';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { GRAPHQL_TRANSPORT_WS_PROTOCOL } from 'graphql-ws';
import { getLogs } from '@app/graphql/express/get-logs';
const configFilePath = path.join(
getters.paths()['dynamix-base'],
'case-model.cfg'
);
const customImageFilePath = path.join(
getters.paths()['dynamix-base'],
'case-model.png'
);
const updatePubsub = async () => {
await pubsub.publish('display', {
display: await display(),
});
};
// Update pub/sub when config/image file is added/updated/removed
watch(configFilePath).on('all', updatePubsub);
watch(customImageFilePath).on('all', updatePubsub);
export const createApolloExpressServer = async () => {
// Try and load the HTTP server
graphqlLogger.debug('Starting HTTP server');
const app = express();
const httpServer = http.createServer(app);
app.use(json());
// Cors
app.use(cors());
app.use(originMiddleware);
// Add Unraid API version header
app.use(async (_req, res, next) => {
// Only get the machine ID on first request
// We do this to avoid using async in the main server function
if (!app.get('x-unraid-api-version')) {
app.set('x-unraid-api-version', API_VERSION);
}
// Update header with unraid API version
res.set('x-unraid-api-version', app.get('x-unraid-api-version'));
next();
});
// Log only if the server actually binds to the port
httpServer.on('listening', () => {
logger.info('Server is up! %s', getServerAddress(httpServer));
});
// graphql-ws
const graphqlWs = new WebSocketServer({ noServer: true });
// subscriptions-transport-ws
const subTransWs = new WebSocketServer({
noServer: true,
});
// graphql-ws setup
const graphqlWsServer = useServer<
{ 'x-api-key': string },
{ context: { user: any; websocketId: string } }
>(
{
schema,
onError(ctx, message, errors) {
logger.debug('%o %o %o', ctx, message, errors);
},
async onConnect(ctx) {
logger.debug(
'Connecting new client with params: %o',
ctx.connectionParams
);
const params: unknown = ctx.connectionParams?.['x-api-key'];
if (params && typeof params === 'string') {
const apiKey = params;
const user = await apiKeyToUser(apiKey);
const websocketId = randomUUID();
logger.debug('User is %o', user);
ctx.extra.context = { user, websocketId };
return true;
}
return {};
},
context: (ctx) => {
return ctx.extra.context;
},
},
graphqlWs
);
// subscriptions-transport-ws setup
const subscriptionsTransportServer = SubscriptionServer.create(
{
// This is the `schema` we just created.
schema,
// These are imported from `graphql`.
execute,
subscribe,
// Ensure keep-alive packets are sent
keepAlive: 10_000,
// Providing `onConnect` is the `SubscriptionServer` equivalent to the
// `context` function in `ApolloServer`. Please [see the docs](https://github.com/apollographql/subscriptions-transport-ws#constructoroptions-socketoptions--socketserver)
// for more information on this hook.
async onConnect(connectionParams: { 'x-api-key': string }) {
const apiKey = connectionParams['x-api-key'];
const user = await apiKeyToUser(apiKey);
const websocketId = randomUUID();
graphqlLogger.addContext('websocketId', websocketId);
graphqlLogger.debug('%s connected', user.name);
graphqlLogger.removeContext('websocketId');
// Update ws connection count and other needed values
wsHasConnected(websocketId);
return {
user,
websocketId,
};
},
async onDisconnect(
_,
websocketContext: {
initPromise: Promise<
| boolean
| {
user: {
name: string;
};
websocketId: string;
}
>;
}
) {
const context = await websocketContext.initPromise;
// The websocket has disconnected before init event has resolved
// @see: https://github.com/apollographql/subscriptions-transport-ws/issues/349
if (context === true || context === false) {
// This seems to also happen if a tab is left open and then a server starts up
// The tab hits the server over and over again without sending init
graphqlLogger.debug('unknown disconnected');
return;
}
const { user, websocketId } = context;
graphqlLogger.addContext('websocketId', websocketId);
graphqlLogger.debug('%s disconnected.', user.name);
graphqlLogger.removeContext('websocketId');
// Update ws connection count and other needed values
wsHasDisconnected(websocketId);
},
},
subTransWs
);
const apolloServerPluginOnExit = {
async serverWillStart() {
return {
/**
* When the app exits this will be run.
*/
async drainServer() {
// Close all connections to subscriptions server
subscriptionsTransportServer.close();
graphqlWsServer.dispose();
},
};
},
};
// Create graphql instance
const apolloServer = new ApolloServer({
schema,
plugins: [
apolloServerPluginOnExit,
ApolloServerPluginDrainHttpServer({ httpServer }),
],
introspection: GRAPHQL_INTROSPECTION,
});
await apolloServer.start();
app.get('/graphql/api/logs', getLogs);
app.get(
'/graphql/api/customizations/:type',
async (req: Request, res: Response) => {
// @TODO - Clean up this function
const apiKey = req.headers['x-api-key'];
if (
apiKey &&
typeof apiKey === 'string' &&
(await apiKeyToUser(apiKey)).role !== 'guest'
) {
if (req.params.type === 'banner') {
const path = await getBannerPathIfPresent();
if (path) {
res.sendFile(path);
return;
}
} else if (req.params.type === 'case') {
const path = await getCasePathIfPresent();
if (path) {
res.sendFile(path);
return;
}
}
return res
.status(404)
.send('no customization of this type found');
}
return res.status(403).send('unauthorized');
}
);
app.use(
'/graphql',
cors(),
json(),
expressMiddleware(apolloServer, {
context: async ({ req }) => {
// Normal Websocket connection
/* if (connection && Object.keys(connection.context).length >= 1) {
// Check connection for metadata
return {
...connection.context,
};
} */
// Normal HTTP connection
if (
req &&
req.headers['x-api-key'] &&
typeof req.headers['x-api-key'] === 'string'
) {
const apiKey = req.headers['x-api-key'];
const user = await apiKeyToUser(apiKey);
return {
user,
};
}
throw new Error('Invalid API key');
},
})
);
httpServer.on('upgrade', (req, socket, head) => {
// extract websocket subprotocol from header
const protocol = req.headers['sec-websocket-protocol'];
const protocols = Array.isArray(protocol)
? protocol
: protocol?.split(',').map((p) => p.trim());
// decide which websocket server to use
const wss =
protocols?.includes(GRAPHQL_WS) && // subscriptions-transport-ws subprotocol
!protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL) // graphql-ws subprotocol
? subTransWs
: // graphql-ws will welcome its own subprotocol and
// gracefully reject invalid ones. if the client supports
// both transports, graphql-ws will prevail
graphqlWs;
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
// List all endpoints at start of server
app.get('/', (_, res: Response) => res.status(200).send('OK'));
app.post('/verify', async (req, res) => {
try {
// Check two-factor token is valid
verifyTwoFactorToken(req.body?.username, req.body?.token);
// Success
logger.debug('2FA token valid, allowing login.');
// Allow the user to pass
res.sendStatus(204);
return;
} catch (error: unknown) {
logger.addContext('error', error);
logger.error('Failed validating 2FA token.');
logger.removeContext('error');
// User failed verification
res.status(401);
res.send((error as Error).message);
}
});
// Handle errors by logging them and returning a 500.
app.use(
(
error: Error & { stackTrace?: string; status?: number },
_,
res: Response,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__
) => {
// Don't log CORS errors
if (error.message.includes('CORS')) return;
logger.error(error);
if (error.stack) {
error.stackTrace = error.stack;
}
res.status(error.status ?? 500).send(error);
}
);
httpServer.listen(PORT);
return apolloServer;
};

View File

@@ -0,0 +1,62 @@
import {
type SetupRemoteAccessInput,
WAN_ACCESS_TYPE,
WAN_FORWARD_TYPE,
} from '@app/graphql/generated/api/types';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { type AppDispatch, type RootState } from '@app/store/index';
import { type MyServersConfig } from '@app/types/my-servers-config';
import { createAsyncThunk } from '@reduxjs/toolkit';
const getDynamicRemoteAccessType = (
accessType: WAN_ACCESS_TYPE,
forwardType?: WAN_FORWARD_TYPE | undefined | null
): DynamicRemoteAccessType => {
// If access is disabled or always, DRA is disabled
if (
accessType === WAN_ACCESS_TYPE.DISABLED ||
accessType === WAN_ACCESS_TYPE.ALWAYS
) {
return DynamicRemoteAccessType.DISABLED;
}
// if access is enabled and forward type is UPNP, DRA is UPNP, otherwise it is static
return forwardType === WAN_FORWARD_TYPE.UPNP
? DynamicRemoteAccessType.UPNP
: DynamicRemoteAccessType.STATIC;
};
export const setupRemoteAccessThunk = createAsyncThunk<
Pick<
MyServersConfig['remote'],
'wanaccess' | 'wanport' | 'dynamicRemoteAccessType' | 'upnpEnabled'
>,
SetupRemoteAccessInput,
{ state: RootState; dispatch: AppDispatch }
>('config/setupRemoteAccess', async (payload) => {
if (payload.accessType === WAN_ACCESS_TYPE.DISABLED) {
return {
wanaccess: 'no',
wanport: '',
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
upnpEnabled: 'no',
}
}
if (payload.forwardType === WAN_FORWARD_TYPE.STATIC && !payload.port) {
throw new Error('Missing port for WAN forward type STATIC');
}
return {
wanaccess: payload.accessType === WAN_ACCESS_TYPE.ALWAYS ? 'yes' : 'no',
wanport:
payload.forwardType === WAN_FORWARD_TYPE.STATIC
? String(payload.port)
: '',
dynamicRemoteAccessType: getDynamicRemoteAccessType(
payload.accessType,
payload.forwardType
),
upnpEnabled: payload.forwardType === WAN_FORWARD_TYPE.UPNP ? 'yes' : 'no',
};
});

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