Compare commits

...

223 Commits

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

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

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

* feat: limit max repo size to 100MB

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

---------

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

* feat: run codegen

* fix: got and reflect metadata revert version

* fix: pino version mismatch

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

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

* chore: add missing web component translations

* chore: translations sort unique ascending, case insensitive

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

* feat: update gitignore

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

* chore(plg): remove debug echo

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

* refactor(web): Eligible for free feature updates
2024-05-02 14:04:16 -07:00
Zack Spear
f8b143904b fix: prevent corrupt case model in state.php (#874)
fix: prevent corruprt case model in state.php
2024-05-02 14:00:29 -07:00
Zack Spear
31a5413643 feat(web): registration page array status messaging 2024-05-01 12:21:24 -07:00
Zack Spear
a95fc5ed07 chore: lint fix 2024-05-01 12:21:24 -07:00
Zack Spear
fcd7bb790e refactor(web): ineligible release messaging 2024-05-01 12:21:24 -07:00
Zack Spear
008e10948e fix: prevent local dev from throwing ssl error 2024-05-01 12:21:24 -07:00
Zack Spear
c97a4f1268 feat: registration page server error heading + subheading 2024-05-01 12:21:24 -07:00
Zack Spear
3eba95b8cc feat: array state on registration page 2024-05-01 12:21:24 -07:00
Zack Spear
2bf8f0b937 fix(api): readme discord url 2024-04-30 17:34:46 -07:00
Zack Spear
9ae45d1258 fix(web): discord url 2024-04-30 17:34:46 -07:00
Zack Spear
1835a4cf3f chore(plg): comment explain web component downgrade prevention 2024-04-30 17:12:33 -07:00
Zack Spear
2ab44b894d feat(plg): plg install prevent web component downgrade 2024-04-30 17:12:33 -07:00
Zack Spear
1108f49b07 feat: postbuild script to add timestamp to web component manifest 2024-04-30 17:12:33 -07:00
ljm42
cc69213beb Feat: Flash Backup requires connection to mothership (#868)
* fix: branding

* feat: flash backup requires connection to mothership

* feat: flash backup requires connection to mothership
2024-04-26 12:01:42 -04:00
ljm42
460e557dd8 Flash Backup: exclude large files from repo (#866) 2024-04-23 21:21:01 -04:00
Zack Spear
05e29468d2 refactor: trial messaging replace pro with unleashed (#865)
* refactor: trial messaging replace pro with unleashed

* fix: trial messaging grammar

* refactor: web component translations trial messaging
2024-04-03 13:46:25 -04:00
ljm42
4d3a311fb4 Feat: add support for outgoing proxies (#863) 2024-03-27 15:14:18 -07:00
Zack Spear
bc62d210ec refactor: config error messages (#862) 2024-03-26 12:30:34 -04:00
Eli Bosley
43d3ea6553 chore(release): 3.6.0 2024-03-26 10:19:31 -04:00
Zack Spear
882e3e1ef4 feat: server config enum message w/ ineligible support (#861)
* test: serverState local components data tweaks

* feat: server config enum message w/ ineligible support

* refactor: config error messages

* chore: lint
2024-03-26 09:57:04 -04:00
Eli Bosley
b33c86c99c chore(release): 3.5.3 2024-03-25 09:22:14 -04:00
Zack Spear
cd0248e4c9 refactor: upgrade action button for unleashed to lifetime (#859) 2024-03-20 10:27:33 -04:00
Zack Spear
ecb3ed5003 fix: regDevs usage to allow more flexibility for STARTER (#860)
* fix: regDevs usage to allow more flexibility for STARTER

* fix: lint and type-check
2024-03-20 08:50:02 -04:00
Zack Spear
0569339a41 refactor(upc): remove UpdateDNS requests 2024-03-12 16:36:04 -07:00
ljm42
3e9faead43 Replace UpdateDNS.php with a stub (#857)
* This new stub file makes no network calls and always returns success
* It is meant to be backwards compatible with older releases of Unraid that expect the script to exist
2024-03-12 15:57:17 -04:00
Eli Bosley
6e700b2385 chore(release): 3.5.2 2024-03-06 10:20:09 -05:00
renovate[bot]
464fc4993c fix(deps): update dependency vue-i18n to v9.10.1 (#813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:08:22 -05:00
renovate[bot]
4316c72809 chore(deps): update dependency terser to v5.28.1 (#802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:08:05 -05:00
renovate[bot]
ce0cebe09c chore(deps): update dependency node to v18.19.1 (#801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:07:51 -05:00
renovate[bot]
23b90a0d56 fix(deps): update dependency wretch to v2.8.0 (#814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:07:38 -05:00
Zack Spear
3f8b3536b5 refactor: ENOKEYFILE messaging + button order (#856) 2024-03-05 15:07:20 -05:00
Zack Spear
0dcf785b45 fix: update os check modal button conditionals 2024-02-29 14:49:20 -08:00
Zack Spear
8cf4aff622 fix: update os check modal ineligible date format 2024-02-29 14:16:09 -08:00
Eli Bosley
cefda7c42b chore(release): 3.5.1 2024-02-29 12:43:48 -05:00
Eli Bosley
0393b2382c fix: build docker command updated to use dc.sh script 2024-02-29 12:43:41 -05:00
renovate[bot]
23e900f7fd chore(deps): update docker/setup-buildx-action action to v3 (#827)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:36:32 -05:00
renovate[bot]
2d6aafc257 chore(deps): update dependency eslint to v8.57.0 (#798)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:55 -05:00
renovate[bot]
b191efece1 chore(deps): update vitest monorepo to v1.3.1 (#784)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:40 -05:00
renovate[bot]
2a7f0043f5 fix(deps): update dependency @heroicons/vue to v2.1.1 (#804)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:17 -05:00
renovate[bot]
607c7e3704 fix(deps): update dependency @apollo/client to v3.9.5 (#785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:29:01 -05:00
renovate[bot]
c246a443c5 fix(deps): update dependency graphql-ws to v5.15.0 (#790)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:25:35 -05:00
renovate[bot]
fd495e1f5c fix(deps): update dependency focus-trap to v7.5.4 (#788)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 09:24:58 -05:00
Zack Spear
621a06cafa fix: unraid-api.php $param1 fallback 2024-02-28 21:11:21 -08:00
Zack Spear
f890b05151 fix: unraid-api missing start command + var defaults 2024-02-28 21:11:21 -08:00
Zack Spear
567d8fdd6d fix: state php special chars for html attributes (#853)
* fix: state php special chars for html attributes

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

* feat: add url to plg
2024-01-12 13:42:15 -05:00
Eli Bosley
d73324a141 feat: upgrade a ton of dependencies (#842)
* feat: upgrade a ton of dependencies
2024-01-12 13:05:51 -05:00
renovate[bot]
7061be60f4 chore(deps): update docker/build-push-action action to v5 (#826)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-12 11:03:19 -05:00
renovate[bot]
2a65f64ac1 fix(deps): update dependency ws to v8.16.0 (#815)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-01-12 11:03:02 -05:00
159 changed files with 18263 additions and 11311 deletions

View File

@@ -81,10 +81,10 @@ jobs:
- name: Build Docker Compose
run: |
docker network create mothership_default
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build builder
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose build builder
- name: Run Docker Compose
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run builder npm run coverage
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose run builder npm run coverage
lint-web:
defaults:
@@ -298,6 +298,18 @@ jobs:
source: staging-release
out_dir: unraid-api
- name: Upload Staging Plugin to Cloudflare Bucket
uses: jakejarvis/s3-sync-action@v0.5.1
env:
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: 'auto'
SOURCE_DIR: staging-release
DEST_DIR: unraid-api
create-draft-release:
# Only create new draft if this is a version tag
if: |

View File

@@ -27,12 +27,12 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: true
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
with:
# network=host driver-opt needed to push to local registry
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: api
target: builder
@@ -60,13 +60,13 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: true
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
with:
# network=host driver-opt needed to push to local registry
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: api
target: builder
@@ -100,13 +100,13 @@ jobs:
uses: actions/checkout@v4
with:
persist-credentials: true
- uses: docker/setup-buildx-action@v2
- uses: docker/setup-buildx-action@v3
with:
# network=host driver-opt needed to push to local registry
driver-opts: network=host
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: api
target: builder

View File

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

4
.gitignore vendored
View File

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

View File

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

11
api/.env.test Normal file
View File

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

View File

@@ -1 +1 @@
18.17.1
18.19.1

View File

@@ -2,6 +2,169 @@
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.8.1](https://github.com/unraid/api/compare/v3.8.0...v3.8.1) (2024-08-13)
## [3.8.0](https://github.com/unraid/api/compare/v3.7.1...v3.8.0) (2024-08-13)
### Features
* always force push ([662f3ce](https://github.com/unraid/api/commit/662f3ce440593e609c64364726f7da16dda0972b))
* don't allow flash backup repos larger than 500MB ([#890](https://github.com/unraid/api/issues/890)) ([30a32f5](https://github.com/unraid/api/commit/30a32f5fe684bb32c084c4125aade5e63ffd788b))
* downgradeOs callback for non stable osCurrentBranch ([17c4489](https://github.com/unraid/api/commit/17c4489e97bda504ca45e360591655ded166c355))
* settings through the API ([#867](https://github.com/unraid/api/issues/867)) ([e73624b](https://github.com/unraid/api/commit/e73624be6be8bc2c70d898b8601a88cc8d20a3e4))
* swap to docker compose from docker-compose ([ec16a6a](https://github.com/unraid/api/commit/ec16a6aab1a2d5c836387da438fbeade07d23425))
### Bug Fixes
* apolloClient types ([f14c767](https://github.com/unraid/api/commit/f14c7673735b92aa167e9e8dcb14a045bcfea994))
* **deps:** update dependency @vue/apollo-composable to v4.0.2 ([#787](https://github.com/unraid/api/issues/787)) ([edfc846](https://github.com/unraid/api/commit/edfc8464b0e0c2f38003ae8420e81532fd18351f))
* formattedRegTm type ([748906e](https://github.com/unraid/api/commit/748906e15d30c6162e2f08f28724c9104c81d123))
* i18n t prop type ([96d519f](https://github.com/unraid/api/commit/96d519f3e6b96ea7c4dc60616522216de20ee140))
* lint error for web components ([bc27b20](https://github.com/unraid/api/commit/bc27b20524934cf896efb84a131cd270431c508c))
* lint issues ([853dc19](https://github.com/unraid/api/commit/853dc195b13fff29160afb44f9ff11d4dd6a3232))
* swap undefined to null ([ebba976](https://github.com/unraid/api/commit/ebba9769873a6536e3fce65978e6475d93280560))
* tailwind config types ([0f77e55](https://github.com/unraid/api/commit/0f77e5596db3356b5dc05129b3ce215a8809e1dc))
* ts-expect-error unneeded ([ee4d4e9](https://github.com/unraid/api/commit/ee4d4e9f12b4488ff39445bc72c1b83a9d93e993))
* type check ([606aad7](https://github.com/unraid/api/commit/606aad703d91b72a14e15da3100dfa355052ed58))
* type errors round 1 ([977d5da](https://github.com/unraid/api/commit/977d5daf04012f16e7b6602167338f0bc363735a))
* update status button alignment ([4f2deaf](https://github.com/unraid/api/commit/4f2deaf70e5caa9f29fc5b2974b278f80b7b3a8a))
### [3.7.1](https://github.com/unraid/api/compare/v3.7.0...v3.7.1) (2024-05-15)
### Bug Fixes
* reboot required and available edge case ([#885](https://github.com/unraid/api/issues/885)) ([76e9cdf](https://github.com/unraid/api/commit/76e9cdf81f06a19c2e4c9a40a4d8e062bad2a607))
## [3.7.0](https://github.com/unraid/api/compare/v3.6.0...v3.7.0) (2024-05-14)
### Features
* add a timestamp to flash backup ([#877](https://github.com/unraid/api/issues/877)) ([b868fd4](https://github.com/unraid/api/commit/b868fd46c3886b2182245a61f20be6df65e46abe))
* add support for outgoing proxies ([#863](https://github.com/unraid/api/issues/863)) ([223693e](https://github.com/unraid/api/commit/223693e0981d5f2884a1f8b8baf03d4dc58e8cb2))
* array state on registration page ([d36fef0](https://github.com/unraid/api/commit/d36fef0545ddb820e67e8bc6cb42ea3644021d66))
* downgradeOs callback ([154a976](https://github.com/unraid/api/commit/154a976109f0a32653a2851988420707631327ca))
* Flash Backup requires connection to mothership ([#868](https://github.com/unraid/api/issues/868)) ([d127208](https://github.com/unraid/api/commit/d127208b5e0f7f9991f515f95b0e266d38cf3287))
* **plg:** install prevent downgrade of shared page & php files ([#873](https://github.com/unraid/api/issues/873)) ([4ac72b1](https://github.com/unraid/api/commit/4ac72b16692c4246c9d2c0b53b23d8b2d95f5de6))
* **plg:** plg install prevent web component downgrade ([8703bd4](https://github.com/unraid/api/commit/8703bd498108f5c05562584a708bd2306e53f7a6))
* postbuild script to add timestamp to web component manifest ([47f08ea](https://github.com/unraid/api/commit/47f08ea3594a91098f67718c0123110c7b5f86f7))
* registration page server error heading + subheading ([6038ebd](https://github.com/unraid/api/commit/6038ebdf39bf47f2cb5c0b1de84764795374f018))
* remove cron to download JS daily ([#864](https://github.com/unraid/api/issues/864)) ([33f6d6b](https://github.com/unraid/api/commit/33f6d6b343de07dbe70de863926906736d42f371)), closes [#529](https://github.com/unraid/api/issues/529)
* ui to allow second update without reboot ([b0f2d10](https://github.com/unraid/api/commit/b0f2d102917f54ab33f0ad10863522b8ff8e3ce5))
* UI Update OS Cancel ([7c02308](https://github.com/unraid/api/commit/7c02308964d5e21990427a2c626c9db2d9e1fed0))
* UnraidUpdateCancel script ([b73bdc0](https://github.com/unraid/api/commit/b73bdc021764762ed12dca494e1345412a45c677))
* **web:** callback types myKeys & linkKey ([c88ee01](https://github.com/unraid/api/commit/c88ee01827396c3fa8a30bb88c4be712c80b1f4f))
* **web:** Registration key linked to account status ([8f6182d](https://github.com/unraid/api/commit/8f6182d426453b73aa19c5f0f59469fa07571694))
* **web:** registration page array status messaging ([23ef5a9](https://github.com/unraid/api/commit/23ef5a975e0d5ff0c246c2df5e6c2cb38979d12a))
### Bug Fixes
* **api:** readme discord url ([ffd5c6a](https://github.com/unraid/api/commit/ffd5c6afb64956e76df22c77104a21bc22798008))
* keep minor enhancements from [#872](https://github.com/unraid/api/issues/872) ([#878](https://github.com/unraid/api/issues/878)) ([94a5aa8](https://github.com/unraid/api/commit/94a5aa87b9979fe0f02f884ac61298473bb3271a))
* plugin file deployment script ([780d87d](https://github.com/unraid/api/commit/780d87d6589a5469f47ac3fdfd50610ecfc394c8))
* prevent corrupt case model in state.php ([#874](https://github.com/unraid/api/issues/874)) ([4ad31df](https://github.com/unraid/api/commit/4ad31dfea9192146dbd2c90bc64a913c696ab0b7))
* prevent local dev from throwing ssl error ([051f647](https://github.com/unraid/api/commit/051f6474becf3b25b242cdc6ceee67247b79f8ba))
* rc.flashbackup needs to check both signed in and connected ([#882](https://github.com/unraid/api/issues/882)) ([ac8068c](https://github.com/unraid/api/commit/ac8068c9b084622d46fe2c9cb320b793c9ea8c52))
* update os cancel refresh on update page ([213c16b](https://github.com/unraid/api/commit/213c16ba3d5a84ebf4965f9d2f4024c66605a613))
* **web:** discord url ([1a6f4c6](https://github.com/unraid/api/commit/1a6f4c6db4ef0e5eefac467ec6583b14cb3546c4))
* **web:** lint unused rebootVersion ([e198ec9](https://github.com/unraid/api/commit/e198ec9d458e262c412c2dcb5a9d279238de1730))
* **web:** registration component remove unused ref ([76f556b](https://github.com/unraid/api/commit/76f556bd64b95ba96af795c9edfa045ebdff4444))
## [3.6.0](https://github.com/unraid/api/compare/v3.5.3...v3.6.0) (2024-03-26)
### Features
* server config enum message w/ ineligible support ([#861](https://github.com/unraid/api/issues/861)) ([4d3a351](https://github.com/unraid/api/commit/4d3a3510777090788573f4cee83694a0dc6f8df5))
### [3.5.3](https://github.com/unraid/api/compare/v3.5.2...v3.5.3) (2024-03-25)
### Bug Fixes
* regDevs usage to allow more flexibility for STARTER ([#860](https://github.com/unraid/api/issues/860)) ([92a9600](https://github.com/unraid/api/commit/92a9600f3a242c5f263f1672eab81054b9cf4fae))
### [3.5.2](https://github.com/unraid/api/compare/v3.5.1...v3.5.2) (2024-03-06)
### Bug Fixes
* **deps:** update dependency vue-i18n to v9.10.1 ([#813](https://github.com/unraid/api/issues/813)) ([69b599c](https://github.com/unraid/api/commit/69b599c5ed8d44864201a32b4d952427d454dc74))
* **deps:** update dependency wretch to v2.8.0 ([#814](https://github.com/unraid/api/issues/814)) ([66900b4](https://github.com/unraid/api/commit/66900b495b82b923264897d38b1529a22b10aa1c))
* update os check modal button conditionals ([282a836](https://github.com/unraid/api/commit/282a83625f417ccefe090b65cc6b73a084727a87))
* update os check modal ineligible date format ([83083de](https://github.com/unraid/api/commit/83083de1e698f73a35635ae6047dcf49fd4b8114))
### [3.5.1](https://github.com/unraid/api/compare/v3.5.0...v3.5.1) (2024-02-29)
### Bug Fixes
* build docker command updated to use dc.sh script ([0b40886](https://github.com/unraid/api/commit/0b40886e84f27a94dbf67ef4ca0cd8539ef3913e))
* date format in UnraidCheck.php ([#852](https://github.com/unraid/api/issues/852)) ([6465f2d](https://github.com/unraid/api/commit/6465f2d7b2394090f35e29cdd680d98ce37f3728))
* **deps:** update dependency @apollo/client to v3.9.5 ([#785](https://github.com/unraid/api/issues/785)) ([75b98bc](https://github.com/unraid/api/commit/75b98bc1cbca5b66ae72f52a0b6f5f58230a2473))
* **deps:** update dependency @heroicons/vue to v2.1.1 ([#804](https://github.com/unraid/api/issues/804)) ([a0eb7ee](https://github.com/unraid/api/commit/a0eb7ee3ec459dbe1992a7f85bf194da30395a74))
* **deps:** update dependency focus-trap to v7.5.4 ([#788](https://github.com/unraid/api/issues/788)) ([fe000e8](https://github.com/unraid/api/commit/fe000e83825e82cac558d3277664a440e59c0e4a))
* **deps:** update dependency graphql-ws to v5.15.0 ([#790](https://github.com/unraid/api/issues/790)) ([4773b13](https://github.com/unraid/api/commit/4773b132167d740d4c996efe22e0f1b99576fb9b))
* display dropdown for pro key no connect installed ([#848](https://github.com/unraid/api/issues/848)) ([b559604](https://github.com/unraid/api/commit/b55960429895b46627f1cd3ed1683ee527e62944))
* dropdown reboot link text ([#849](https://github.com/unraid/api/issues/849)) ([a8ed5e5](https://github.com/unraid/api/commit/a8ed5e5628bc71fb783a03c3db92d21805243738))
* os updates rc to stable ([bf1bd88](https://github.com/unraid/api/commit/bf1bd887d60ac085bf4aeae90f11be3b45ee1182))
* state connect values without connect installed ([e47de6c](https://github.com/unraid/api/commit/e47de6c2c5db7a2a1a9b24099feb02023b3a7bbf))
* state php breaking with double quotes in server description ([c6e92aa](https://github.com/unraid/api/commit/c6e92aa3157c9fe9e7b83580881ebcc1cbd03658))
* state php special chars for html attributes ([#853](https://github.com/unraid/api/issues/853)) ([dd4139c](https://github.com/unraid/api/commit/dd4139cf1a7ae5c6f9b00111c33ae124bb17e630))
* unraid-api missing start command + var defaults ([ceb4c58](https://github.com/unraid/api/commit/ceb4c587d20c7527f2b36a3278c310b0e657bfba))
* unraid-api.php $param1 fallback ([909c79c](https://github.com/unraid/api/commit/909c79c8c82500aea1a0d4d00766f788103c5fe3))
## [3.5.0](https://github.com/unraid/api/compare/v3.4.0...v3.5.0) (2024-02-07)
### Features
* add manage account link to all versions of upc dropdown ([678e620](https://github.com/unraid/api/commit/678e620c1902a376b1866265711d5722b4119d8e))
* add new staging url for connect website ([#841](https://github.com/unraid/api/issues/841)) ([4cfc07b](https://github.com/unraid/api/commit/4cfc07b6763dbb79b68cf01f7eaf7cf33370d4db))
* also ship to cloudflare ([#844](https://github.com/unraid/api/issues/844)) ([41c4210](https://github.com/unraid/api/commit/41c42103685209592b272f81a877702da04d0915))
* button add underline-hover-red style option ([f2fa5fa](https://github.com/unraid/api/commit/f2fa5fa49675ef461330be7b7eb3e3e4106983b0))
* changelog modal ([2ddbacd](https://github.com/unraid/api/commit/2ddbacd137cc5748244c3d25ac91f82e64d77f99))
* check update response modal ([39678f0](https://github.com/unraid/api/commit/39678f0bb0ddc5f87ea7f5ed80a0472100ea8b5d))
* create WebguiCheckForUpdate endpoint ([41d546e](https://github.com/unraid/api/commit/41d546eea5fcf6593d7b5047274c074bb89c1802))
* getOsReleaseBySha256 cached endpoint with keyfile header ([cd2413a](https://github.com/unraid/api/commit/cd2413abe8c5baab40e4e5974e08a5d18dce8e0d))
* new check update buttons in dropdown ([ef5fcb9](https://github.com/unraid/api/commit/ef5fcb96a324143da864df803acaa0da1cd00eb7))
* ship preview to different bucket ([#845](https://github.com/unraid/api/issues/845)) ([8e5d247](https://github.com/unraid/api/commit/8e5d247bca83d9e50977c9b16b212841ac9f70ad))
* ship production to different bucket ([#846](https://github.com/unraid/api/issues/846)) ([63c0875](https://github.com/unraid/api/commit/63c08758c76425e007b1779bb2f77b75bc45896e))
* unraidcheck callable from webgui with altUrl & json output ([ba8a67e](https://github.com/unraid/api/commit/ba8a67edfa043f442b11724227129f8d3f6cae0a))
* update modals ([8ad7d8b](https://github.com/unraid/api/commit/8ad7d8be9437e0caa0409da8f7322050919fbbaa))
* update os ignore release ([1955eb2](https://github.com/unraid/api/commit/1955eb23a3cdc30f0a67bc5950a047f83a860d99))
* update os notifications enabled usage + link to enable & more options to account app ([5c82aff](https://github.com/unraid/api/commit/5c82aff80dc7e6d8f4b23e52af29abc2b8576424))
* updateOs check response determines if update auth is required ([a9816d9](https://github.com/unraid/api/commit/a9816d9ad48ff80d87b5aeb236ff60c4979ad298))
* updateOs store call local server-side endpoint & add modal support ([be48447](https://github.com/unraid/api/commit/be48447f943828af281095c5a092ac686e729030))
* upgrade a ton of dependencies ([#842](https://github.com/unraid/api/issues/842)) ([94c1746](https://github.com/unraid/api/commit/94c174620c2347a3cf3d100404635f99a5b47287))
* WebguiCheckForUpdate using server-side check ([590deb1](https://github.com/unraid/api/commit/590deb130c301d4004fecdc211270583806b5593))
### Bug Fixes
* backport _var() PHP function to older versions of Unraid ([f53150e](https://github.com/unraid/api/commit/f53150e1fa33b3f45b66ad0dc5eaabc470564d45))
* changlog relative links and external links ([a789e20](https://github.com/unraid/api/commit/a789e204ce7b966e6c935923626538ac344aeefe))
* check update response modal expired key button styles ([92993e3](https://github.com/unraid/api/commit/92993e3e0b6240c83a6a64efedd8ddb3be3f9ef7))
* **deps:** update dependency ws to v8.16.0 ([#815](https://github.com/unraid/api/issues/815)) ([212020e](https://github.com/unraid/api/commit/212020e78d4de0576137058a3374837b4a43e02d))
* extraLinks when no updates available ([853a991](https://github.com/unraid/api/commit/853a9911e3fd7eec9bbc88468de78f87b448d477))
* ignore release localStorage ([62c45ec](https://github.com/unraid/api/commit/62c45ec9d7c68498bbcfe933a5b63e4759c7129c))
* lint ([83235f9](https://github.com/unraid/api/commit/83235f9db726f4582b9d353a66f2f5e8925b8e34))
* lint unused value ([2c7e53b](https://github.com/unraid/api/commit/2c7e53bf67d1f214201624b39786bfb7de6aa520))
* marked-base-url install ([416ba71](https://github.com/unraid/api/commit/416ba716aa750a094e8cd521a79f6deebcd37864))
* missing translations ([faf17e4](https://github.com/unraid/api/commit/faf17e41e81c11443bc062d8ce35a33d9ae9ebbc))
* regTm format after key install without page refresh ([f3ddb31](https://github.com/unraid/api/commit/f3ddb31f994de9192f7203698ecc5d7de673c6a3))
* regTm format when already set ([5ad911f](https://github.com/unraid/api/commit/5ad911f8133daa60de53da738d41c6a59e2f02cc))
* ServerUpdateOsResponse type ([78bdae8](https://github.com/unraid/api/commit/78bdae86c907142d3ee32d6715eaa8f5a974a1ed))
* State Class usage in other files ([4ad7f53](https://github.com/unraid/api/commit/4ad7f53ec145b2e6d2895619523e90c1daa3f68f))
* state data humanReadable switch fallthrus ([9144e39](https://github.com/unraid/api/commit/9144e39d39aa56af0ad897735d1a3545330920d0))
* state php usage from cli ([46fd321](https://github.com/unraid/api/commit/46fd321707c14cd1f265ee806f673500d87132dd))
* translations ([3fabd57](https://github.com/unraid/api/commit/3fabd5756674c06fa803729cf13d19c592d8d46a))
* type issue with changlelog modal visibility ([e3c3f6b](https://github.com/unraid/api/commit/e3c3f6bf0f1882788291db17bd74865fefc3abf6))
## [3.4.0](https://github.com/unraid/api/compare/v3.3.0...v3.4.0) (2024-01-11)

View File

@@ -1,7 +1,7 @@
###########################################################
# Development/Build Image
###########################################################
FROM node:18.17.1-bookworm-slim As development
FROM node:18.19.1-bookworm-slim As development
# Install build tools and dependencies
RUN apt-get update -y && apt-get install -y \

58
api/README.md Normal file
View File

@@ -0,0 +1,58 @@
# @unraid/api
## Installation
Install the production plugin via the apps tab (search for "my servers") on Unraid 6.9.2 or later.
## CLI
If you're on a unraid v6.9.2 or later machine this should be available by running `unraid-api` in any directory.
```bash
root@Devon:~# unraid-api
Unraid API
Thanks for using the official Unraid API
Usage:
$ unraid-api command <options>
Commands:
start/stop/restart/version/status/report/switch-env
Options:
-h, --help Prints this usage guide.
-d, --debug Enabled debug mode.
-p, --port string Set the graphql port.
--environment production/staging/development Set the working environment.
--log-level ALL/TRACE/DEBUG/INFO/WARN/ERROR/FATAL/MARK/OFF Set the log level.
Copyright © 2022 Lime Technology, Inc.
```
## Report
To view the current status of the unraid-api and its connection to mothership, run:
```
unraid-api report
```
To view verbose data (anonymized), run:
```
unraid-api report -v
```
To view non-anonymized verbose data, run:
```
unraid-api report -vv
```
## Secrets
If you found this file you're likely a developer. If you'd like to know more about the API and when it's available please join [our discord](https://discord.unraid.net/).
## License
Copyright 2019-2022 Lime Technology Inc. All rights reserved.

View File

@@ -1,5 +1,5 @@
[api]
version="3.3.0+eff31423"
version="3.5.2+20f10951"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]

View File

@@ -1,5 +1,5 @@
[api]
version="3.3.0+eff31423"
version="3.5.2+20f10951"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
@@ -16,9 +16,10 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken=""
accesstoken=""
refreshtoken=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus]
minigraph="ERROR_RETRYING"
upnpStatus="Success: UPNP Lease Renewed [4/24/2024 5:04:54 PM] Public Port [59138] Local Port [443]"

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

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

10054
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.4.0",
"version": "3.8.1",
"main": "dist/index.js",
"bin": "dist/unraid-api.cjs",
"type": "module",
@@ -26,7 +26,7 @@
"compile": "tsup --config ./tsup.config.ts",
"bundle": "pkg . --public",
"build": "npm run compile && npm run bundle",
"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:docker": "./scripts/dc.sh run --rm builder",
"build-pkg": "./scripts/build.mjs",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config",
@@ -34,8 +34,8 @@
"tsc": "tsc --noEmit",
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
"test:watch": "vitest --segfault-retry=3 --no-threads",
"test": "vitest run --segfault-retry=3 --no-threads",
"test:watch": "vitest --segfault-retry=3 --pool=forks",
"test": "vitest run --segfault-retry=3 --pool=forks",
"coverage": "vitest run --segfault-retry=3 --coverage",
"patch:subscriptions-transport-ws": "node ./.scripts/patches/subscriptions-transport-ws.cjs",
"release": "standard-version",
@@ -59,21 +59,21 @@
"unraid-api"
],
"dependencies": {
"@apollo/client": "^3.7.12",
"@apollo/server": "^4.6.0",
"@apollo/client": "^3.10.4",
"@apollo/server": "^4.10.4",
"@as-integrations/fastify": "^2.1.1",
"@graphql-codegen/client-preset": "^4.0.0",
"@graphql-codegen/client-preset": "^4.2.5",
"@graphql-tools/load-files": "^7.0.0",
"@graphql-tools/merge": "^9.0.0",
"@graphql-tools/schema": "^10.0.0",
"@graphql-tools/utils": "^10.0.0",
"@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",
"@nestjs/schedule": "^4.0.0",
"@reduxjs/toolkit": "^1.9.5",
"@graphql-tools/merge": "^9.0.4",
"@graphql-tools/schema": "^10.0.3",
"@graphql-tools/utils": "^10.2.0",
"@nestjs/apollo": "^12.1.0",
"@nestjs/core": "^10.3.8",
"@nestjs/graphql": "^12.1.1",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.8",
"@nestjs/schedule": "^4.0.2",
"@reduxjs/toolkit": "^2.2.4",
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
"accesscontrol": "^2.2.1",
@@ -84,126 +84,126 @@
"bytes": "^3.1.2",
"cacheable-lookup": "^6.1.0",
"catch-exit": "^1.2.2",
"chokidar": "^3.5.3",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"class-validator": "^0.14.1",
"cli-table": "^0.3.11",
"command-exists": "^1.2.9",
"convert": "^4.10.0",
"convert": "^4.14.1",
"cors": "^2.8.5",
"cross-fetch": "^4.0.0",
"docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"find-process": "^1.4.7",
"graphql": "^16.8.1",
"graphql-fields": "^2.0.3",
"graphql-scalars": "^1.21.3",
"graphql-scalars": "^1.23.0",
"graphql-subscriptions": "^2.0.0",
"graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2",
"graphql-type-uuid": "^0.2.0",
"graphql-ws": "^5.14.2",
"graphql-ws": "^5.16.0",
"htpasswd-js": "^1.0.2",
"ini": "^4.1.0",
"ip": "^1.1.8",
"jose": "^4.14.2",
"ini": "^4.1.2",
"ip": "^2.0.1",
"jose": "^5.3.0",
"lodash": "^4.17.21",
"multi-ini": "^2.2.0",
"mustache": "^4.2.0",
"nanobus": "^4.5.0",
"nest-access-control": "^3.1.0",
"nestjs-pino": "^3.5.0",
"nestjs-pino": "^4.0.0",
"node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2",
"openid-client": "^5.4.0",
"openid-client": "^5.6.5",
"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",
"pino": "^9.1.0",
"pino-http": "^9.0.0",
"pino-pretty": "^11.0.0",
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.4.0",
"semver": "^7.6.2",
"stoppable": "^1.1.0",
"systeminformation": "^5.21.2",
"ts-command-line-args": "^2.5.0",
"uuid": "^9.0.0",
"ws": "^8.13.0",
"wtfnode": "^0.9.1",
"systeminformation": "^5.22.9",
"ts-command-line-args": "^2.5.1",
"uuid": "^9.0.1",
"ws": "^8.17.0",
"wtfnode": "^0.9.2",
"xhr2": "^0.2.1",
"zod": "^3.22.2"
"zod": "^3.23.8"
},
"devDependencies": {
"@babel/runtime": "^7.21.0",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@babel/runtime": "^7.24.5",
"@graphql-codegen/add": "^5.0.2",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/fragment-matcher": "^5.0.2",
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typed-document-node": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.0.0",
"@graphql-codegen/typescript-resolvers": "4.0.1",
"@graphql-codegen/typed-document-node": "^5.0.6",
"@graphql-codegen/typescript": "^4.0.6",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@graphql-codegen/typescript-resolvers": "4.0.6",
"@graphql-typed-document-node/core": "^3.2.0",
"@nestjs/testing": "^10.2.10",
"@swc/core": "^1.3.81",
"@types/async-exit-hook": "^2.0.0",
"@types/btoa": "^1.2.3",
"@types/bytes": "^3.1.1",
"@types/cli-table": "^0.3.1",
"@types/command-exists": "^1.2.0",
"@types/dockerode": "^3.3.16",
"@types/express": "^4.17.17",
"@types/graphql-fields": "^1.3.5",
"@types/graphql-type-uuid": "^0.2.3",
"@types/ini": "^1.3.31",
"@types/lodash": "^4.14.192",
"@types/mustache": "^4.2.2",
"@types/node": "^18.17.12",
"@types/pidusage": "^2.0.2",
"@types/pify": "^5.0.1",
"@types/semver": "^7.3.13",
"@types/sendmail": "^1.4.4",
"@types/stoppable": "^1.1.1",
"@types/uuid": "^9.0.1",
"@types/ws": "^8.5.4",
"@types/wtfnode": "^0.7.0",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"@nestjs/testing": "^10.3.8",
"@swc/core": "^1.5.7",
"@types/async-exit-hook": "^2.0.2",
"@types/btoa": "^1.2.5",
"@types/bytes": "^3.1.4",
"@types/cli-table": "^0.3.4",
"@types/command-exists": "^1.2.3",
"@types/dockerode": "^3.3.29",
"@types/express": "^4.17.21",
"@types/graphql-fields": "^1.3.9",
"@types/graphql-type-uuid": "^0.2.6",
"@types/ini": "^4.1.0",
"@types/lodash": "^4.17.1",
"@types/mustache": "^4.2.5",
"@types/node": "^20.12.12",
"@types/pidusage": "^2.0.5",
"@types/pify": "^5.0.4",
"@types/semver": "^7.5.8",
"@types/sendmail": "^1.4.7",
"@types/stoppable": "^1.1.3",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"@types/wtfnode": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@unraid/eslint-config": "github:unraid/eslint-config",
"@vitest/coverage-v8": "^0.34.1",
"@vitest/ui": "^0.34.0",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"camelcase-keys": "^8.0.2",
"cz-conventional-changelog": "3.3.0",
"eslint": "^8.38.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-unicorn": "^48.0.1",
"eslint-plugin-unused-imports": "^2.0.0",
"eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unused-imports": "^3.2.0",
"execa": "^7.1.1",
"filter-obj": "^5.1.0",
"got": "^13.0.0",
"graphql-codegen-typescript-validation-schema": "^0.11.0",
"got": "^13",
"graphql-codegen-typescript-validation-schema": "^0.14.1",
"ip-regex": "^5.0.0",
"json-difference": "^1.9.1",
"json-difference": "^1.16.1",
"map-obj": "^5.0.2",
"p-props": "^5.0.0",
"path-exists": "^5.0.0",
"path-type": "^5.0.0",
"pkg": "^5.8.1",
"pretty-bytes": "^6.1.0",
"pretty-bytes": "^6.1.1",
"pretty-ms": "^8.0.0",
"standard-version": "^9.5.0",
"tsup": "^7.0.0",
"typescript": "^4.9.4",
"typesync": "^0.11.0",
"vite-tsconfig-paths": "^4.2.0",
"vitest": "^0.34.0",
"zx": "^7.2.1"
"tsup": "^8.0.2",
"typescript": "^5.4.5",
"typesync": "^0.12.1",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"zx": "^7.2.3"
},
"optionalDependencies": {
"@vmngr/libvirt": "github:unraid/libvirt"

View File

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

View File

@@ -36,7 +36,7 @@ test('Returns allowed origins', async () => {
"https://google.com",
"https://test.com",
"https://connect.myunraid.net",
"https://staging.connect.myunraid.net",
"https://connect-staging.myunraid.net",
"https://dev-my.myunraid.net:4000",
]
`);

View File

@@ -54,7 +54,7 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
},
"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",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
@@ -146,7 +146,7 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
"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",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,7 @@ const getRemoteAccessUrlsForAllowedOrigins = (
return [];
};
const getExtraOrigins = (): string[] => {
export const getExtraOrigins = (): string[] => {
const { extraOrigins } = getters.config().api;
if (extraOrigins) {
return extraOrigins
@@ -86,7 +86,7 @@ const getExtraOrigins = (): string[] => {
const getConnectOrigins = (): string[] => {
const connectMain = 'https://connect.myunraid.net';
const connectStaging = 'https://staging.connect.myunraid.net';
const connectStaging = 'https://connect-staging.myunraid.net';
const connectDev = 'https://dev-my.myunraid.net:4000';
return [connectMain, connectStaging, connectDev];

View File

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

View File

@@ -2,7 +2,7 @@
import * as Types from '@app/graphql/generated/api/types';
import { z } from 'zod'
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
type Properties<T> = Required<{
@@ -702,6 +702,15 @@ export function RelayResponseSchema(): z.ZodObject<Properties<RelayResponse>> {
})
}
export function RemoteAccessSchema(): z.ZodObject<Properties<RemoteAccess>> {
return z.object({
__typename: z.literal('RemoteAccess').optional(),
accessType: WAN_ACCESS_TYPESchema,
forwardType: WAN_FORWARD_TYPESchema.nullish(),
port: z.number().nullish()
})
}
export function ServerSchema(): z.ZodObject<Properties<Server>> {
return z.object({
__typename: z.literal('Server').optional(),
@@ -852,6 +861,15 @@ export function UserSchema(): z.ZodObject<Properties<User>> {
})
}
export function UserAccountSchema(): z.ZodObject<Properties<UserAccount>> {
return z.object({
description: z.string(),
id: z.string(),
name: z.string(),
roles: z.string()
})
}
export function VarsSchema(): z.ZodObject<Properties<Vars>> {
return z.object({
__typename: z.literal('Vars').optional(),

View File

@@ -874,6 +874,7 @@ export type Query = {
dockerNetwork: DockerNetwork;
/** All Docker networks */
dockerNetworks: Array<Maybe<DockerNetwork>>;
extraAllowedOrigins: Array<Scalars['String']['output']>;
flash?: Maybe<Flash>;
info?: Maybe<Info>;
/** Current user account */
@@ -883,6 +884,7 @@ export type Query = {
owner?: Maybe<Owner>;
parityHistory?: Maybe<Array<Maybe<ParityCheck>>>;
registration?: Maybe<Registration>;
remoteAccess: RemoteAccess;
server?: Maybe<Server>;
servers: Array<Server>;
/** Network Shares */
@@ -990,6 +992,13 @@ export type RelayResponse = {
timeout?: Maybe<Scalars['String']['output']>;
};
export type RemoteAccess = {
__typename?: 'RemoteAccess';
accessType: WAN_ACCESS_TYPE;
forwardType?: Maybe<WAN_FORWARD_TYPE>;
port?: Maybe<Scalars['Port']['output']>;
};
export type Server = {
__typename?: 'Server';
apikey: Scalars['String']['output'];
@@ -1646,6 +1655,7 @@ export type ResolversTypes = ResolversObject<{
Registration: ResolverTypeWrapper<Registration>;
RegistrationState: RegistrationState;
RelayResponse: ResolverTypeWrapper<RelayResponse>;
RemoteAccess: ResolverTypeWrapper<RemoteAccess>;
Server: ResolverTypeWrapper<Server>;
ServerStatus: ServerStatus;
Service: ResolverTypeWrapper<Service>;
@@ -1739,6 +1749,7 @@ export type ResolversParentTypes = ResolversObject<{
Query: {};
Registration: Registration;
RelayResponse: RelayResponse;
RemoteAccess: RemoteAccess;
Server: Server;
Service: Service;
SetupRemoteAccessInput: SetupRemoteAccessInput;
@@ -2312,6 +2323,7 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
dockerContainers?: Resolver<Array<ResolversTypes['DockerContainer']>, ParentType, ContextType, Partial<QuerydockerContainersArgs>>;
dockerNetwork?: Resolver<ResolversTypes['DockerNetwork'], ParentType, ContextType, RequireFields<QuerydockerNetworkArgs, 'id'>>;
dockerNetworks?: Resolver<Array<Maybe<ResolversTypes['DockerNetwork']>>, ParentType, ContextType, Partial<QuerydockerNetworksArgs>>;
extraAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
flash?: Resolver<Maybe<ResolversTypes['Flash']>, ParentType, ContextType>;
info?: Resolver<Maybe<ResolversTypes['Info']>, ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['Me']>, ParentType, ContextType>;
@@ -2320,6 +2332,7 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
owner?: Resolver<Maybe<ResolversTypes['Owner']>, ParentType, ContextType>;
parityHistory?: Resolver<Maybe<Array<Maybe<ResolversTypes['ParityCheck']>>>, ParentType, ContextType>;
registration?: Resolver<Maybe<ResolversTypes['Registration']>, ParentType, ContextType>;
remoteAccess?: Resolver<ResolversTypes['RemoteAccess'], ParentType, ContextType>;
server?: Resolver<Maybe<ResolversTypes['Server']>, ParentType, ContextType>;
servers?: Resolver<Array<ResolversTypes['Server']>, ParentType, ContextType>;
shares?: Resolver<Maybe<Array<Maybe<ResolversTypes['Share']>>>, ParentType, ContextType>;
@@ -2347,6 +2360,13 @@ export type RelayResponseResolvers<ContextType = Context, ParentType extends Res
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type RemoteAccessResolvers<ContextType = Context, ParentType extends ResolversParentTypes['RemoteAccess'] = ResolversParentTypes['RemoteAccess']> = ResolversObject<{
accessType?: Resolver<ResolversTypes['WAN_ACCESS_TYPE'], ParentType, ContextType>;
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ServerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Server'] = ResolversParentTypes['Server']> = ResolversObject<{
apikey?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
guid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2755,6 +2775,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Query?: QueryResolvers<ContextType>;
Registration?: RegistrationResolvers<ContextType>;
RelayResponse?: RelayResponseResolvers<ContextType>;
RemoteAccess?: RemoteAccessResolvers<ContextType>;
Server?: ServerResolvers<ContextType>;
Service?: ServiceResolvers<ContextType>;
Share?: ShareResolvers<ContextType>;

View File

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

View File

@@ -27,12 +27,23 @@ enum WAN_FORWARD_TYPE {
STATIC
}
type RemoteAccess {
accessType: WAN_ACCESS_TYPE!
forwardType: WAN_FORWARD_TYPE
port: Port
}
input SetupRemoteAccessInput {
accessType: WAN_ACCESS_TYPE!
forwardType: WAN_FORWARD_TYPE
port: Port
}
type Query {
remoteAccess: RemoteAccess!
extraAllowedOrigins: [String!]!
}
type Mutation {
connectSignIn(input: ConnectSignInInput!): Boolean!
connectSignOut: Boolean!

View File

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

View File

@@ -1,5 +1,4 @@
import { apiLogger } from '@app/core/log';
import { BYPASS_PERMISSION_CHECKS } from '@app/environment';
import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy';
import { IS_PUBLIC_KEY } from '@app/unraid-api/auth/public.decorator';
import {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { AuthService } from './auth.service';
describe('AuthService', () => {

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { ArrayResolver } from './array.resolver';
describe('ArrayResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { CloudResolver } from './cloud.resolver';
describe('CloudResolver', () => {

View File

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

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { ConfigResolver } from './config.resolver';
describe('ConfigResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { DisksResolver } from './disks.resolver';
describe('DisksResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { DisplayResolver } from './display.resolver';
describe('DisplayResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { DockerContainersResolver } from './docker-containers.resolver';
describe('DockerContainersResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { FlashResolver } from './flash.resolver';
describe('FlashResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { InfoResolver } from './info.resolver';
describe('InfoResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { NotificationsResolver } from './notifications.resolver';
describe('NotificationsResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { OnlineResolver } from './online.resolver';
describe('OnlineResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { OwnerResolver } from './owner.resolver';
describe('OwnerResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { RegistrationResolver } from './registration.resolver';
describe('RegistrationResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { VarsResolver } from './vars.resolver';
describe('VarsResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { VmsResolver } from './vms.resolver';
describe('VmsResolver', () => {

View File

@@ -1,4 +1,4 @@
import { Test, TestingModule } from '@nestjs/testing';
import { Test, type TestingModule } from '@nestjs/testing';
import { RestService } from './rest.service';
describe('RestService', () => {

View File

@@ -7,7 +7,6 @@ export default defineConfig(() => {
// Manually set NODE_ENV to make sure we always run tests in test mode
process.env.NODE_ENV = 'test';
return {
plugins: [tsconfigPaths()],
test: {
globals: true,

View File

@@ -161,11 +161,8 @@ exit 0
<?
$msini = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
# for convenience, scan myservers.cfg for deleteOnUninstall="no" and if that exists,
# then skip the rest of the cleanup.
$deleteOnUninstall = ($msini === false || empty($msini['plugin']['deleteOnUninstall']) || $msini['plugin']['deleteOnUninstall'] == 'yes');
if (!$deleteOnUninstall) {
# if no_delete_on_uninstall exists on flash drive then skip the rest of the cleanup (useful when switching between staging and production)
if (file_exists("/boot/config/plugins/dynamix.my.servers/no_delete_on_uninstall")) {
exit(0);
}
@@ -313,21 +310,31 @@ if [ -e /etc/rc.d/rc.unraid-api ]; then
FILE=/usr/local/emhttp/plugins/dynamix/Registration.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix/include/Wrappers.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/include/ShowChanges.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/showchanges && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/sbin/upgradepkg && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components && [[ -d "$DIR-" ]] && mv -f "$DIR-" "$DIR"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
# certain instances where the directory is not present and others where it is, ensure we delete it before we restore it
if [[ -d "$DIR" ]]; then
rm -rf "$DIR"
fi
if [[ -d "$DIR-" ]]; then
mv -f "$DIR-" "$DIR"
fi
# delete plugin files from flash drive and OS
rm -f /boot/config/plugins/dynamix.my.servers/.gitignore
rm -f /etc/rc.d/rc.unraid-api
@@ -405,22 +412,53 @@ echo
# NOTE: any 'exit 1' after this point will result in a broken install
# Preserve in case plugin is removed
FILE=/usr/local/emhttp/plugins/dynamix/Registration.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/sbin/upgradepkg && [[ -f "$FILE" ]] && cp -f "$FILE" "$FILE-"
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components && [[ -d "$DIR" ]] && mv -f "$DIR" "$DIR-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
# Loop through the array of preserveFilesDirs and perform actions
# string param format
# "{move|copy|move_dir}:{path}:{preventDowngrade|skip}"
# move: move the file to a backup file
# copy: copy the file to a backup file
# move_dir: move the directory to a backup directory
# preventDowngrade: during plg install, if the file exists, do not overwrite it if the plg manifest version is less than the installed webgui version
# skip: do not perform any action if there is a manifest version difference
preserveFilesDirs=(
"move:/usr/local/emhttp/plugins/dynamix/Registration.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page:skip"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:skip"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php:preventDowngrade"
"copy:/sbin/upgradepkg:skip"
"move_dir:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components:move_dir:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php:preventDowngrade"
)
preserveAction() {
local action="$1"
local path="$2"
if [[ "$action" == "move" ]]; then
[[ -f "$path" ]] && mv -f "$path" "$path-"
elif [[ "$action" == "copy" ]]; then
[[ -f "$path" ]] && cp -f "$path" "$path-"
elif [[ "$action" == "move_dir" ]]; then
[[ -d "$path" ]] && mv -f "$path" "$path-"
fi
}
# Loop through the array of preserveFilesDirs and perform actions
for obj in "${preserveFilesDirs[@]}"
do
IFS=':' read -r action path preventType <<< "$obj"
preserveAction "$action" "$path" "$preventType"
done
# patch DefaultPageLayout.php
# search text: <?=_('Version')?>: <?=_var($var,'version','?')?><?=$notes?>
@@ -499,6 +537,103 @@ if test -f "${FILE}" && grep -q "top.Shadowbox" "${FILE}" &>/dev/null; then
sed -i 's/top.Shadowbox/parent.Shadowbox/gm' "${FILE}"
fi
# ensure _var() is defined, brings older versions of Unraid in sync with 6.12.0
FILE=/usr/local/emhttp/plugins/dynamix/include/Wrappers.php
if test -f "${FILE}" && ! grep -q "function _var" "${FILE}" &>/dev/null; then
ADDTEXT1=$(
cat <<'END_HEREDOC'
// backported by Unraid Connect
function _var(&$name, $key=null, $default='') {
return is_null($key) ? ($name ?? $default) : ($name[$key] ?? $default);
}
END_HEREDOC
)
fi
# ensure my_logger() is defined, brings older versions of Unraid in sync with 6.13.0
if test -f "${FILE}" && ! grep -q "function my_logger" "${FILE}" &>/dev/null; then
ADDTEXT2=$(
cat <<'END_HEREDOC'
// backported by Unraid Connect
// ensure params passed to logger are properly escaped
function my_logger($message, $logger='webgui') {
exec('logger -t '.escapeshellarg($logger).' -- '.escapeshellarg($message));
}
END_HEREDOC
)
fi
# ensure http_get_contents() is defined, brings older versions of Unraid in sync with 6.13.0
if test -f "${FILE}" && ! grep -q "function http_get_contents" "${FILE}" &>/dev/null; then
ADDTEXT3=$(
cat <<'END_HEREDOC'
// backported by Unraid Connect
// Original PHP code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
// https://www.the-art-of-web.com/php/http-get-contents/
// Modified for Unraid
/**
* Fetches URL and returns content
* @param string $url The URL to fetch
* @param array $opts Array of options to pass to curl_setopt()
* @param array $getinfo Empty array passed by reference, will contain results of curl_getinfo and curl_error
* @return string|false $out The fetched content
*/
function http_get_contents(string $url, array $opts = [], array &$getinfo = NULL) {
$ch = curl_init();
if(isset($getinfo)) {
curl_setopt($ch, CURLINFO_HEADER_OUT, TRUE);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($ch, CURLOPT_TIMEOUT, 45);
curl_setopt($ch, CURLOPT_ENCODING, "");
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_REFERER, "");
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Unraid');
if(is_array($opts) && $opts) {
foreach($opts as $key => $val) {
curl_setopt($ch, $key, $val);
}
}
$out = curl_exec($ch);
if (curl_errno($ch) == 23) {
// error 23 detected, try CURLOPT_ENCODING = "deflate"
curl_setopt($ch, CURLOPT_ENCODING, "deflate");
$out = curl_exec($ch);
}
if (isset($getinfo)) {
$getinfo = curl_getinfo($ch);
}
if ($errno = curl_errno($ch)) {
$msg = "Curl error $errno: " . (curl_error($ch) ?: curl_strerror($errno)) . ". Requested url: '$url'";
if(isset($getinfo)) {
$getinfo['error'] = $msg;
}
my_logger($msg, "http_get_contents");
}
curl_close($ch);
return $out;
}
END_HEREDOC
)
fi
if [[ -n "${ADDTEXT1}" || -n "${ADDTEXT2}" || -n "${ADDTEXT3}" ]]; then
TMP="$FILE.$RANDOM"
cp -f "$FILE" "$TMP"
cp -f "$FILE" "$FILE-"
# delete last line of the file if it contains `?>`
if test $( tail -n 1 "${TMP}" ) = '?>' ; then
sed -i '$ d' "${TMP}"
fi
[[ -n "${ADDTEXT1}" ]] && echo "${ADDTEXT1}" >>"${TMP}"
[[ -n "${ADDTEXT2}" ]] && echo "${ADDTEXT2}" >>"${TMP}"
[[ -n "${ADDTEXT3}" ]] && echo "${ADDTEXT3}" >>"${TMP}"
echo "?>" >>"${TMP}"
mv "${TMP}" "${FILE}"
fi
# install the main txz
upgradepkg --install-new --reinstall "${MAINTXZ}"
@@ -566,7 +701,7 @@ if [ "${PLGTYPE}" = "staging" ]; then
CHANGED=yes
[[ ! -f "$FILE-" ]] && cp "$FILE" "$FILE-"
OLD="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/\";"
NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect-staging.myunraid.net https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
sed -i "s#${OLD}#${NEW}#" "${FILE}"
fi
fi
@@ -590,6 +725,54 @@ if [[ "${CHANGED}" == "yes" ]]; then
fi
fi
# Prevent web component file downgrade if the webgui version is newer than the plugin version
# Function to extract "ts" value from JSON file
extract_ts() {
local filepath="$1"
local ts_value=null
ts_value=$(jq -r '.ts' "$filepath" 2>/dev/null)
echo "$ts_value"
}
preventDowngradeAction() {
local action="$1"
local path="$2"
local preventType="$3" # preventDowngrade or skip
# if skip, do nothing
if [[ "$preventType" == "skip" ]]; then
return
fi
# restore the "backup" but keep the original backup for the uninstall plg script
# otherwise, the uninstall script will NOT be able to restore the original file
if [[ "$action" == "move" || "$action" == "copy" ]]; then
[[ -f "$path-" ]] && cp -f "$path-" "$path"
elif [[ "$action" == "move_dir" ]]; then
# if directory exists rm the original and copy the backup
# glob expansion via "$path-/"* …yes the * is necessary as we want to copy the contents of the directory
[[ -d "$path-" ]] && rm -rf "$path" && mkdir "$path" && cp -rf "$path-/"* "$path"
fi
}
# Extract "ts" values from both files
plgWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components"
backupWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components-"
plgManifestTs=$(extract_ts "$plgWebComponentPath/manifest.json")
webguiManifestTs=$(extract_ts "$backupWebComponentPath/manifest.json")
# Compare the "ts" values and return the file path of the higher value
if [[ "$webguiManifestTs" -gt "$plgManifestTs" ]]; then
# Loop through the array of preserveFilesDirs and perform actions
for obj in "${preserveFilesDirs[@]}"
do
IFS=':' read -r action path preventType <<< "$obj"
preventDowngradeAction "$action" "$path" "$preventType"
done
echo "♻️ Reverted to stock web component files"
fi
# start background process to install/start the api and flash backup
echo
if [ -f /var/local/emhttp/var.ini ]; then

View File

@@ -1,5 +1,9 @@
#!/bin/bash
# Arguments
# $1: SSH server name
# $2: {--wc-deploy|--wc-build|--wc-skip} / deploy or build web components w/o prompt
# Path to store the last used server name
state_file="$HOME/.deploy_state"
@@ -38,6 +42,30 @@ echo "$rsync_command"
eval "$rsync_command"
exit_code=$?
# if $2 is --wc-deploy, deploy the web components without prompting
if [ "$2" = "--wc-deploy" ]; then
deploy="yes"
elif [ "$2" = "--wc-build" ]; then
deploy="build"
elif [ "$2" = "--wc-skip" ]; then
deploy="no"
fi
# if not deploy yes then ask
if [ -z "$deploy" ]; then
echo
echo
read -rp "Do you want to also deploy the built web components? (yes/no/build): " deploy
fi
if [ "$deploy" = "yes" ]; then
cd web || exit
npm run deploy-wc:dev
elif [ "$deploy" = "build" ]; then
cd web || exit
npm run build:dev
fi
# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS

View File

@@ -0,0 +1,217 @@
#!/bin/bash
# This file is /etc/rc.d/rc.flash_backup
# use at queue "f" for flash backup
QUEUE=" -q f "
TASKNAME="/etc/rc.d/rc.flash_backup watch"
TASKACTION="/usr/local/emhttp/plugins/dynamix.my.servers/scripts/UpdateFlashBackup update"
last=$(date +%s)
# set GIT_OPTIONAL_LOCKS=0 globally to reduce/eliminate writes to /boot
export GIT_OPTIONAL_LOCKS=0
FAST=1 # 1 second delay when waiting for git
SLOW=10 # 10 second delay when waiting for git
# wait for existing git commands to complete
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
_waitforgit() {
while [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; do
sleep "$1"
done
}
# log to syslog, then wait for existing git commands to complete
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
_waitforgitlog() {
if [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; then
logger "waiting for current backup to complete" --tag flash_backup
_waitforgit "$1"
fi
}
status() {
_connected && CONNECTED="system is connected to Unraid Connect Cloud." || CONNECTED="system is not connected to Unraid Connect Cloud."
if _watching; then
echo "flash backup monitor is running. ${CONNECTED}"
_hasqueue && echo "changes detected, backup queued."
exit 0
else
if _enabled; then
echo "flash backup is enabled but the monitor is not running. ${CONNECTED}"
else
echo "flash backup is disabled so the monitor is disabled. ${CONNECTED}"
fi
exit 1
fi
}
start() {
_start
exit 0
}
stop() {
_stop
exit 0
}
reload() {
_start
sleep 1
status
}
_start() {
# Note: can start if not signed in, but watcher loop will not process until signed in
# only run if flash_backup is enabled
if ! _enabled; then
logger "flash backup disabled, exiting" --tag flash_backup
exit 1
fi
_stop
# start watcher loop as background process
exec ${TASKNAME} &>/dev/null &
}
_stop() {
if _watching; then
logger "stop watching for file changes" --tag flash_backup
# terminate watcher loop/process
pkill --full "${TASKNAME}" &>/dev/null
fi
# do not flush. better to have unsaved changes than to corrupt the backup during shutdown
# note that an existing git process could still be running
}
flush() {
# remove any queued jobs
_removequeue
# wait for existing git commands to finish before flushing
_waitforgitlog "${FAST}"
logger "flush: ${TASKACTION}" --tag flash_backup
# if _connected, push any changes ad-hoc
if _connected; then
# shellcheck disable=SC2086
echo "${TASKACTION}_nolimit &>/dev/null" | at ${QUEUE} -M now &>/dev/null
fi
}
_watching() {
local flash_backup_pid
flash_backup_pid=$(pgrep --full "${TASKNAME}")
if [[ ${flash_backup_pid} ]]; then
return 0
fi
return 1
}
_watch() {
# safely clean up git *.lock files
_clearlocks
# flush: this will ensure we start with a clean repo
flush
# wait for flush to complete
sleep 3
_waitforgitlog "${FAST}"
logger "start watching for file changes" --tag flash_backup
# start watcher loop
while true; do
# if system is connected to Unraid Connect Cloud, see if there are updates to process
_connected && _f1
sleep 60
done
}
_f1() {
# wait for existing git commands to finish before checking for updates
_waitforgit "${SLOW}"
if [ "$(git -C /boot status -s)" ]; then
_hasqueue || _f2
elif _haserror && _beenawhile; then
# we are in an error state and it has been 3 hours since we last tried submitting. run the task now.
_runtaskaction
fi
}
_f2() {
if ! _haserror || [[ $(($(date +"%M") % 10)) -eq 0 ]]; then
logger "adding task: ${TASKACTION}" --tag flash_backup
fi
sed -i "s@uptodate=yes@uptodate=no@" /var/local/emhttp/flashbackup.ini &>/dev/null
_runtaskaction
}
_hasqueue() {
# returns false if the queue is empty, true otherwise
# shellcheck disable=SC2086
if [ -z "$(atq ${QUEUE})" ]; then
return 1
fi
return 0
}
_removequeue() {
# delete any at jobs in queue f
# @TODO shellcheck SC2162
# shellcheck disable=SC2086
atq ${QUEUE} | while read line; do
id=$(echo ${line} | cut -d " " -f 1)
atrm ${id}
done
}
_runtaskaction() {
# shellcheck disable=SC2086
echo "${TASKACTION} &>/dev/null" | at ${QUEUE} -M now +1 minute &>/dev/null
last=$(date +%s)
}
_enabled() {
local output
output=$(git -C /boot config --get remote.origin.url 2>&1)
if [[ "${output}" == *"backup.unraid.net"* ]]; then
return 0
fi
return 1
}
_connected() {
CFG=/var/local/emhttp/myservers.cfg
[[ ! -f "${CFG}" ]] && return 1
# shellcheck disable=SC1090
source <(sed -nr '/\[remote\]/,/\[/{/username/p}' "${CFG}" 2>/dev/null)
# ensure signed in
if [ -z "${username}" ]; then
return 1
fi
# shellcheck disable=SC1090
source <(sed -nr '/\[connectionStatus\]/,/\[/{/minigraph/p}' "${CFG}" 2>/dev/null)
# ensure connected
if [[ -z "${minigraph}" || "${minigraph}" != "CONNECTED" ]]; then
return 1
fi
return 0
}
_haserror() {
errorstring=$(awk -F "=" '/error/ {print $2}' /var/local/emhttp/flashbackup.ini 2>&1 || echo '')
if [ ${#errorstring} -le 2 ]; then
return 1
fi
return 0
}
_beenawhile() {
now=$(date +%s)
age=$((now - last))
maxage=$((3 * 60 * 60)) # three hours
[[ $age -gt $maxage ]] && return 0
return 1
}
# wait for git commands to end, then delete any stale lock files
_clearlocks() {
_waitforgitlog "${FAST}"
find /boot/.git -type f -name '*.lock' -delete
}
case "$1" in
'status')
status
;;
'start')
start
;;
'stop')
stop
;;
'reload')
reload
;;
'flush')
flush
;;
'watch')
_watch
;;
*)
echo "usage $0 status|start|stop|reload|flush"
;;
esac

View File

@@ -0,0 +1,182 @@
#!/bin/bash
# unraid-api-handler
flash="/boot/config/plugins/dynamix.my.servers"
[[ ! -d "${flash}" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
[[ ! -f "${flash}/env" ]] && echo 'env=production' >"${flash}/env"
# define env to avoid shellcheck SC2154. Will be overridden by the source command below
env=production
# shellcheck disable=SC1091
source "${flash}/env"
api_base_directory="/usr/local/bin"
# Only allow specific envs
if [ "${env}" != "staging" ] && [ "${env}" != "production" ]; then
echo "\"${env}\" is an unsupported env. Please use \"staging\" or \"production\"."
exit 1
fi
switchenv() {
stop
# Get current environment from file
local envFile="${flash}/env"
local currentEnv
currentEnv=$(
# shellcheck disable=SC1090
source "${envFile}"
echo "${env}"
)
if [[ "${currentEnv}" = "production" ]]; then
echo "Switching from production to staging"
echo 'env="staging"' >"${envFile}"
cp "${api_base_directory}/unraid-api/.env.staging" "${api_base_directory}/unraid-api/.env"
elif [[ "${currentEnv}" = "staging" ]]; then
echo "Switching from staging to production"
echo 'env="production"' >"${envFile}"
cp "${api_base_directory}/unraid-api/.env.production" "${api_base_directory}/unraid-api/.env"
fi
echo "Run \"unraid-api start\" to start the API."
}
raiseloglevel() {
kill -s SIGUSR2 "$(pidof unraid-api)"
}
lowerloglevel() {
kill -s SIGUSR1 "$(pidof unraid-api)"
}
status() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" status
}
start() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" start 2>&1 | logger &
}
report() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" report "$1" "$2"
}
startdebug() {
LOG_CONTEXT=true LOG_STACKTRACE=true LOG_TRACING=true LOG_LEVEL=debug "${api_base_directory}/unraid-api/unraid-api" start --debug
}
stop() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" stop 2>/dev/null
}
reload() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" restart
}
_install() {
# process file from commandline
if [[ -n "$1" ]]; then
file=$(realpath "${flash}/$1")
if [[ "${file}" == "${flash}"* ]] && [[ "${file}" == *".tgz" || "${file}" == *".zip" ]] && [[ -f "${file}" ]]; then
[[ "${file}" == *".tgz" ]] && ext=tgz || ext=zip
echo "installing $1"
cp "${file}" "${flash}/unraid-api.${ext}"
else
echo "invalid installation file: $1"
exit 1
fi
fi
# If this was downloaded from a Github action it'll be a zip with a tgz inside
# Let's extract the tgz and rename it for the next step
if [[ -f "${flash}/unraid-api.zip" ]]; then
for f in ${flash}/unraid-api.zip; do unzip -p "${f}" >"${flash}/${f%.zip}.tgz"; done
rm -f "${flash}/unraid-api.zip"
fi
# Ensure installation tgz exists
[[ ! -f "${flash}/unraid-api.tgz" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
# Stop old process
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
# Install unraid-api
rm -rf "${api_base_directory}/unraid-api"
mkdir -p "${api_base_directory}/unraid-api"
tar -C "${api_base_directory}/unraid-api" -xzf "${flash}/unraid-api.tgz" --strip 1
# Reset permissions
rm -f "${flash}/data/permissions.json"
# Copy env file
cp "${api_base_directory}/unraid-api/.env.${env}" "${api_base_directory}/unraid-api/.env"
# Copy wc files from flash
if [ -f "${flash}/webComps/unraid.min.js" ]; then
rm -rf /usr/local/emhttp/webGui/webComps
mkdir -p /usr/local/emhttp/webGui/webComps
cp ${flash}/webComps/* /usr/local/emhttp/webGui/webComps
else
# not fatal, previous version of unraid.min.js should still exist in /usr/local/emhttp/webGui/webComps
echo "Note: ${flash}/webComps/unraid.min.js is missing"
fi
# bail if expected file does not exist
[[ ! -f "${api_base_directory}/unraid-api/unraid-api" ]] && echo "unraid-api install failed" && exit 1
}
install() {
# Install the files
_install "$1"
# if nginx is running, start the api. if not, it will be started by rc.nginx
if /etc/rc.d/rc.nginx status &>/dev/null; then
# Start new process
start
# Note: do not run another unraid-api command until you see "UNRAID API started successfully!" in syslog
sleep 3
echo "unraid-api installed and started"
else
echo "unraid-api installed"
fi
exit 0
}
uninstall() {
# Stop old process
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
# Remove all unraid-api files
rm -rf "${api_base_directory}/unraid-api"
rm -f /var/run/unraid-api.sock
}
case "$1" in
'status')
status
;;
'start')
start
;;
'report')
report "$2" "$3"
;;
'switch-env')
switchenv
;;
'start-debug')
startdebug
;;
'raise-log-level')
raiseloglevel
;;
'lower-log-level')
lowerloglevel
;;
'stop')
stop
;;
'reload')
reload
;;
'restart')
reload
;;
'install')
install "$2"
;;
'_install')
_install "$2"
;;
'uninstall')
uninstall
;;
*)
echo "usage $0 status|start|report|switch-env|start-debug|raise-log-level|lower-log-level|stop|reload|install|uninstall"
;;
esac

View File

@@ -0,0 +1,79 @@
#!/bin/bash
# passes `shellcheck` and `shfmt -i 2`
[[ "$1" == "s" ]] && env=staging
[[ "$1" == "p" ]] && env=production
[[ -z "${env}" ]] && echo "usage: [s|p]" && exit 1
DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
MAINDIR=$(dirname "$(dirname "${DIR}")")
tmpdir=/tmp/tmp.$((RANDOM * 19318203981230 + 40))
pluginSrc=$(basename "${DIR}")
plugin="${pluginSrc}"
[[ "${env}" == "staging" ]] && plugin="${plugin}.staging" && cp "${MAINDIR}/plugins/${pluginSrc}.plg" "${MAINDIR}/plugins/${plugin}.plg"
version=$(date +"%Y.%m.%d.%H%M")
plgfile="${MAINDIR}/plugins/${plugin}.plg"
txzfile="${MAINDIR}/archive/${plugin}-${version}.txz"
# create txz package
mkdir -p "$(dirname "${txzfile}")"
mkdir -p "${tmpdir}"
# shellcheck disable=SC2046
cp --parents -f $(find . -type f ! \( -iname ".DS_Store" -o -iname "pkg_build.sh" -o -iname "makepkg" -o -iname "explodepkg" -o -iname "sftp-config.json" \)) "${tmpdir}/"
cd "${tmpdir}" || exit 1
if [[ "${env}" == "staging" ]]; then
# create README.md for staging plugin
mv "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net" "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net.staging"
sed -i "s@\*\*Unraid Connect\*\*@\*\*Unraid Connect \(staging\)\*\*@" "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net.staging/README.md"
sed -i "s@dynamix.unraid.net.plg@dynamix.unraid.net.staging.plg@" "${tmpdir}/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page"
fi
chmod 0755 -R .
sudo chown root:root -R .
sudo "${MAINDIR}/source/dynamix.unraid.net/makepkg" -l y -c y "${txzfile}"
sudo rm -rf "${tmpdir}"
md5=$(md5sum "${txzfile}" | cut -f 1 -d ' ')
echo "MD5: ${md5}"
sha256=$(sha256sum "${txzfile}" | cut -f 1 -d ' ')
echo "SHA256: ${sha256}"
# test txz package
mkdir -p "${tmpdir}"
cd "${tmpdir}" || exit 1
RET=$(sudo "${MAINDIR}/source/dynamix.unraid.net/explodepkg" "${txzfile}" 2>&1 >/dev/null)
sudo rm -rf "${tmpdir}"
[[ "${RET}" != "" ]] && echo "Error: invalid txz package created: ${txzfile}" && exit 1
cd "${DIR}" || exit 1
# define vars for plg
pluginURL="https://stable.dl.unraid.net/unraid-api/\&name;.plg"
downloadserver="https://stable.dl.unraid.net"
js_dl_server="https://registration.unraid.net"
if [[ "${env}" == "staging" ]]; then
pluginURL="https://preview.dl.unraid.net/unraid-api/\&name;.plg"
downloadserver="https://preview.dl.unraid.net"
js_dl_server="https://registration-dev.unraid.net"
fi
# update plg file
sed -i -E "s#(ENTITY name\s*)\".*\"#\1\"${plugin}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY env\s*)\".*\"#\1\"${env}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY version\s*)\".*\"#\1\"${version}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY pluginURL\s*)\".*\"#\1\"${pluginURL}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY MD5\s*)\".*\"#\1\"${md5}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY SHA256\s*)\".*\"#\1\"${sha256}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY downloadserver\s*)\".*\"#\1\"${downloadserver}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY js_dl_server\s*)\".*\"#\1\"${js_dl_server}\"#g" "${plgfile}"
# set from environment vars
sed -i -E "s#(ENTITY API_version\s*)\".*\"#\1\"${API_VERSION}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY API_MD5\s*)\".*\"#\1\"${API_MD5}\"#g" "${plgfile}"
sed -i -E "s#(ENTITY API_SHA256\s*)\".*\"#\1\"${API_SHA256}\"#g" "${plgfile}"
# add changelog for major versions
# sed -i "/<CHANGES>/a ###${version}\n" ${plgfile}
echo
grep -E "ENTITY (name|pluginURL|env|version|MD5|SHA256|node_api_version)" "${plgfile}"
echo
echo "${env} plugin: ${plgfile}"
echo "${env} txz: ${txzfile}"

View File

@@ -14,6 +14,7 @@ Tag="globe"
* all copies or substantial portions of the Software.
*/
require_once "$docroot/plugins/dynamix.my.servers/include/state.php";
require_once "$docroot/webGui/include/Wrappers.php";
$serverState = new ServerState();
$keyfile = $serverState->keyfileBase64;
@@ -140,7 +141,7 @@ function registerServer(button) {
button.form.submit();
});
<?else:?>
// give the unraid-api time to call rc.nginx and UpdateDNS before refreshing the page
// give the unraid-api time to call rc.nginx before refreshing the page
const delay = 4000;
setTimeout(function() {
button.form.submit();
@@ -256,10 +257,10 @@ function changeRemoteAccess(dropdown) {
$useConnectMsgTxt = '';
break;
case 'DYNAMIC_MANUAL':
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/connect/remote-access' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>.";
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/go/connect-remote-access/' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>.";
break;
case 'DYNAMIC_UPNP':
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/connect/remote-access' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>, a random WAN port will be assigned by UPnP.";
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/go/connect-remote-access/' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>, a random WAN port will be assigned by UPnP.";
break;
case 'ALWAYS_MANUAL':
$remoteAccessMsgTxt = "Remote Access is always on.";
@@ -521,7 +522,7 @@ _(Allow Remote Access)_:
<?if(!$isRegistered): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span></dd></dl>
<?elseif(!$isMiniGraphConnected && $myServersFlashCfg['remote']['wanaccess']!="yes"): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span></dd></dl>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span></dd></dl>
<?elseif(!$hasMyUnraidNetCert): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you Provision a myunraid.net SSL Cert)_</span><input type="hidden" id="wanport" value="0"></dd></dl>
<?elseif(!$boolWebUIAuth): // NOTE: manually added close tags so the next section would not be indented ?>
@@ -546,7 +547,7 @@ _(Allow Remote Access)_:
<?endif?>
&nbsp;
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=http_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
<div markdown="1" id="wanpanel" style="display:'none'">
@@ -626,8 +627,8 @@ _(Enable Transparent 2FA for Local Access)_<!-- do not index -->:
_(Flash backup)_:
<?if(!$isRegistered):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span>
<?elseif(!$isMiniGraphConnected && empty($flashbackup_status['activated'])):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span>
<?elseif(!$isMiniGraphConnected):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span>
<?else: // begin show flash backup form ?>
: <span id='flashbackuptext'><span class='blue p0'>_(Loading)_ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i></span></span>
@@ -646,7 +647,7 @@ _(Flash backup)_:
<div markdown="1" id="inactivespanel" style="display:none">
&nbsp;
<?if(disk_free_space('/boot') > 1024*1000*1000):?>
: <button type="button" onclick="enableFlashBackup(this)">_(Activate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/connect/help#automated-flash-backup" target="_blank">_(More information.)_</a></span>
: <button type="button" onclick="enableFlashBackup(this)">_(Activate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/go/connect-flash-backup/" target="_blank">_(More information.)_</a></span>
<?else:?>
: <button type="button" disabled>_(Activate)_</button> <span><i class="fa fa-warning icon warning"></i> _(In order to activate Flash Backup there must be at least 1GB of free space on your flash drive.)_</span>
<?endif?>
@@ -673,7 +674,7 @@ _(Flash backup)_:
</div>
<div markdown="1" id="activepanel" style="display:none">
&nbsp;
: <button type="button" onclick="enableFlashBackup(this)">_(Deactivate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/connect/help#automated-flash-backup" target="_blank">_(More information.)_</a></span>
: <button type="button" onclick="enableFlashBackup(this)">_(Deactivate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/go/connect-flash-backup/" target="_blank">_(More information.)_</a></span>
<?if (in_array($_COOKIE['UPC_ENV']??'', ['development','staging']) && file_exists("/var/log/gitflash") && filesize("/var/log/gitflash")):?>
&nbsp;

View File

@@ -70,8 +70,8 @@ function save_flash_backup_state($loading='') {
rename($flashbackup_tmp, $flashbackup_ini);
}
function load_flash_backup_state() {
global $arrState,$flashbackup_ini,$isRegistered;
function default_flash_backup_state() {
global $arrState;
$arrState = [
'activated' => 'no',
@@ -80,6 +80,12 @@ function load_flash_backup_state() {
'error' => '',
'remoteerror' => ''
];
}
function load_flash_backup_state() {
global $arrState,$flashbackup_ini,$isRegistered;
default_flash_backup_state();
$arrNewState = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
if ($arrNewState) {
@@ -120,7 +126,7 @@ function set_git_config($name, $value) {
function readFromFile($file): string {
$text = "";
if (file_exists($file)) {
if (file_exists($file) && filesize($file) > 0) {
$fp = fopen($file,"r");
if (flock($fp, LOCK_EX)) {
$text = fread($fp, filesize($file));
@@ -193,6 +199,7 @@ function deleteLocalRepo() {
if (is_dir($mainGitDir)) {
rename($mainGitDir, $tmpGitDir);
exec('echo "rm -rf '.$tmpGitDir.' &>/dev/null" | at -q f -M now &>/dev/null');
write_log("local repo deleted");
}
// reset state
@@ -201,6 +208,7 @@ function deleteLocalRepo() {
$arrState['loading'] = '';
$arrState['error'] = '';
$arrState['remoteerror'] = '';
save_flash_backup_state();
}
$validCommands = [
@@ -277,7 +285,14 @@ if ($pgrep_output[0] != "0") {
// check if signed-in
if (!$isRegistered) {
response_complete(406, array('error' => 'Must be signed in to My Servers to use Flash Backup'));
default_flash_backup_state();
response_complete(406, array('error' => 'Must be signed in to Unraid Connect to use Flash Backup'));
}
// check if connected to Unraid Connect Cloud
if (!$isConnected) {
default_flash_backup_state();
response_complete(406, array('error' => 'Must be connected to Unraid Connect Cloud to use Flash Backup'));
}
// keyfile
@@ -314,12 +329,46 @@ if (!empty($loadingMessage)) {
}
if ($command == 'deactivate') {
exec_log('git -C /boot remote remove origin');
exec('/etc/rc.d/rc.flash_backup stop &>/dev/null');
deleteLocalRepo();
response_complete(200, '{}');
}
// determine size of local repo
$maxRepoSize = 100 * 1000; // 100 MB, for comparison without output of 'du -s'
$repoDelFlag = '/boot/config/plugins/dynamix.my.servers/repodeleted';
$output = [];
if (file_exists('/boot/.git')) exec('du -s /boot/.git/ | cut -f 1', $output);
$repoSize = ($output && $output[0]) ? intval($output[0]) : 0;
if ($repoSize > $maxRepoSize) {
// the local repo is too large
$okToDelRepo = true;
if (file_exists($repoDelFlag)) {
// the local repo is too large, but we have already auto-deleted it in the past. determine how long ago this happened
$repoDelTime = intval(@trim(@file_get_contents($repoDelFlag))); // epoch
$repoAge = round((time()-$repoDelTime)/(60*60*24)); // days
$repoMaxAge = 90; // days
if ($repoAge < $repoMaxAge) {
// the local repo was deleted and recreated less than repoMaxAge days ago, do not delete
write_log("local repo is too large ($repoSize > $maxRepoSize) but was auto-deleted recently ($repoAge < $repoMaxAge)");
$okToDelRepo = false;
}
}
if ($okToDelRepo) {
// the local repo is too large, delete and reactivate it
write_log("local repo is too large ($repoSize > $maxRepoSize), about to delete and reactivate");
file_put_contents($repoDelFlag, time());
exec('/etc/rc.d/rc.flash_backup stop &>/dev/null');
deleteLocalRepo();
// change command to 'activate' and continue script
$command = 'activate';
$loadingMessage = 'Activating';
save_flash_backup_state($loadingMessage);
}
} else {
write_log("local repo size is acceptable ($repoSize < $maxRepoSize)");
}
// build a list of sha256 hashes of the bzfiles
$bzfilehashes = [];
$allbzfiles = ['bzimage','bzfirmware','bzmodules','bzroot','bzroot-gui'];
@@ -422,29 +471,29 @@ if (!file_exists('/boot/.git/info/exclude')) {
}
// setup a nice git description
$gitdesc_text='Unraid flash drive for '.$var['NAME']."\n";
$gitdesc_file='/boot/.git/description';
if (!file_exists($gitdesc_file) || strpos(file_get_contents($gitdesc_file),$var['NAME']) === false) {
file_put_contents($gitdesc_file, 'Unraid flash drive for '.$var['NAME']."\n");
if (!file_exists($gitdesc_file) || (file_get_contents($gitdesc_file) != $gitdesc_text)) {
file_put_contents($gitdesc_file, $gitdesc_text);
}
// configure git to use the noprivatekeys filter
set_git_config('filter.noprivatekeys.clean', '/usr/local/emhttp/plugins/dynamix.my.servers/scripts/git-noprivatekeys-clean');
// configure git to apply the noprivatekeys filter to wireguard config files
$gitattributes_file='/boot/.gitattributes';
if (!file_exists($gitattributes_file) || strpos(file_get_contents($gitattributes_file),'noprivatekeys') === false) {
file_put_contents($gitattributes_file, '# file managed by Unraid, do not modify
$gitattributes_text='# file managed by Unraid, do not modify
config/wireguard/*.cfg filter=noprivatekeys
config/wireguard/*.conf filter=noprivatekeys
config/wireguard/peers/*.conf filter=noprivatekeys
');
';
$gitattributes_file='/boot/.gitattributes';
if (!file_exists($gitattributes_file) || (file_get_contents($gitattributes_file) != $gitattributes_text)) {
file_put_contents($gitattributes_file, $gitattributes_text);
}
// setup git ignore for files we dont need in the flash backup
$gitexclude_file='/boot/.git/info/exclude';
if (!file_exists($gitexclude_file) || strpos(file_get_contents($gitexclude_file),'# version 1.0') === false) {
file_put_contents($gitexclude_file, '# file managed by Unraid, do not modify
# version 1.0
// setup master git exclude file to specify what to include/exclude from repo
$gitexclude_text = '# file managed by Unraid, do not modify
# version 1.2
# Blacklist everything
/*
@@ -479,8 +528,20 @@ config/plugins/**/*.tar.bz2
config/plugins-error
config/plugins-old-versions
config/plugins/dockerMan/images
config/plugins/dynamix.file.integrity/logs
config/wireguard/peers/*.png
');
';
// find large files to exclude from flash backup
$oversize_files = $return_var = null;
exec('find /boot/config -type f -size +10M 2>/dev/null | sed "s|^/boot/||g" 2>/dev/null', $oversize_files, $return_var);
if ($oversize_files && is_array($oversize_files)) {
$gitexclude_text .= "\n# Blacklist large files on this system\n".implode("\n", $oversize_files)."\n";
}
$gitexclude_file='/boot/.git/info/exclude';
if (!file_exists($gitexclude_file) || (file_get_contents($gitexclude_file) != $gitexclude_text)) {
file_put_contents($gitexclude_file, $gitexclude_text);
}
// ensure git user is configured
@@ -529,7 +590,7 @@ if (empty($SSH_PORT)) {
} else {
$arrState['loading'] = '';
if (stripos(implode($ssh_output),'permission denied') !== false) {
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
} else {
$arrState['error'] = 'Unable to connect to backup.unraid.net:22';
}
@@ -557,7 +618,7 @@ if ($command == 'activate') {
exec_log('git -C /boot checkout -B master origin/master');
// establish status
exec_log('git -C /boot status --porcelain 2>&1', $status_output, $return_var);
exec_log('git -C /boot status --porcelain', $status_output, $return_var);
if ($return_var != 0) {
// detect git submodule
@@ -600,7 +661,7 @@ if ($command == 'activate') {
}
// detect corruption #1
exec_log('git -C /boot show --summary 2>&1', $show_output, $return_var);
exec_log('git -C /boot show --summary', $show_output, $return_var);
if ($return_var != 0) {
if (stripos(implode($show_output),'fatal: your current branch appears to be broken') !== false) {
$arrState['error'] = 'Error: Backup corrupted';
@@ -616,24 +677,31 @@ if ($command == 'activate') {
} // end check for ($command == 'activate')
if ($command == 'update' || $command == 'activate') {
// note: this section only runs if there are changes detected
if ($arrState['uptodate'] == 'no') {
// increment git commit counter
appendToFile($commitCountFile, $time."\n");
// find files that are in repo but should not be, according to /boot/.git/info/exclude and various .gitignore files
$invalid_files = $return_var = null;
exec_log('git -C /boot ls-files --cached --ignored --exclude-standard', $invalid_files, $return_var);
foreach ((array) $invalid_files as $invalid_file) {
// remove each of these files from the repo
// this prevents future changes from being tracked but does not remove the file from history.
exec_log("git -C /boot rm --cached --ignore-unmatch '$invalid_file'");
}
// add and commit all file changes
exec_log('git -C /boot add -A');
exec_log('git -C /boot commit -m ' . escapeshellarg($commitmsg));
// push changes upstream
exec_log('git -C /boot push --set-upstream origin master', $push_output, $return_var);
if ($return_var != 0) {
exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var);
}
// push changes upstream
exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var);
if ($return_var != 0) {
// check for permission denied
if (stripos(implode($push_output),'permission denied') !== false) {
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
} elseif (stripos(implode($push_output),'fatal: loose object') !== false && stripos(implode($push_output),'is corrupt') !== false) {
// detect corruption #2
$arrState['error'] = 'Error: Backup corrupted';

View File

@@ -30,8 +30,7 @@ if (!document.getElementsByTagName(modalsWebComponent).length) {
$i18nHost.appendChild($modals);
}
</script>
<?
echo "
<unraid-i18n-host>
<unraid-user-profile server='" . $serverState->getServerStateJson() . "'></unraid-user-profile>
</unraid-i18n-host>";
<unraid-user-profile server="<?= $serverState->getServerStateJsonForHtmlAttr() ?>"></unraid-user-profile>
</unraid-i18n-host>

View File

@@ -14,15 +14,25 @@
* Usage:
* ```
* $rebootDetails = new RebootDetails();
* $rebootType = $rebootDetails->getRebootType();
* $rebootType = $rebootDetails->rebootType;
* ```
*/
class RebootDetails
{
/**
* @var string $rebootType Stores the type of reboot required, which can be 'update', 'downgrade', or 'thirdPartyDriversDownloading'.
*/
private $rebootType = '';
const CURRENT_CHANGES_TXT_PATH = '/boot/changes.txt';
const CURRENT_README_RELATIVE_PATH = 'plugins/unRAIDServer/README.md';
const CURRENT_VERSION_PATH = '/etc/unraid-version';
const PREVIOUS_BZ_ROOT_PATH = '/boot/previous/bzroot';
const PREVIOUS_CHANGES_TXT_PATH = '/boot/previous/changes.txt';
private $currentVersion = '';
public $rebootType = ''; // 'update', 'downgrade', 'thirdPartyDriversDownloading'
public $rebootReleaseDate = '';
public $rebootVersion = '';
public $previousReleaseDate = '';
public $previousVersion = '';
/**
* Constructs a new RebootDetails object and automatically detects the reboot type during initialization.
@@ -40,66 +50,119 @@ class RebootDetails
{
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$rebootReadme = @file_get_contents("$docroot/plugins/unRAIDServer/README.md", false, null, 0, 20) ?: '';
/**
* Read the reboot readme, and see if it says "REBOOT REQUIRED" or "DOWNGRADE"
* only relying on the README.md file to save reads from the flash drive.
* because we started allowing downgrades from the account.unraid.net Update OS page, we can't
* fully rely on the README.md value of being accurate.
* For instance if on 6.13.0-beta.2.1 then chose to "Downgrade" to 6.13.0-beta.1.10 from the account app
* the README.md file would still say "REBOOT REQUIRED".
*/
$rebootReadme = @file_get_contents("$docroot/" . self::CURRENT_README_RELATIVE_PATH, false, null, 0, 20) ?: '';
$rebootDetected = preg_match("/^\*\*(REBOOT REQUIRED|DOWNGRADE)/", $rebootReadme);
if (!$rebootDetected) {
return;
}
/**
* if a reboot is required, then:
* get current Unraid version from /etc/unraid-version
* then get the version of the last update from self::CURRENT_CHANGES_TXT_PATH
* if they're different, then a reboot is required
* if the version in self::CURRENT_CHANGES_TXT_PATH is less than the current version, then a downgrade is required
* if the version in self::CURRENT_CHANGES_TXT_PATH is greater than the current version, then an update is required
*/
$this->setCurrentVersion();
$this->setRebootDetails();
if ($this->currentVersion == '' || $this->rebootVersion == '') {
return; // return to prevent potential incorrect outcome
}
$rebootForDowngrade = $rebootDetected && strpos($rebootReadme, 'DOWNGRADE') !== false;
$rebootForUpdate = $rebootDetected && strpos($rebootReadme, 'REBOOT REQUIRED') !== false;
$this->rebootType = $rebootForDowngrade ? 'downgrade' : ($rebootForUpdate ? 'update' : '');
$compareVersions = version_compare($this->rebootVersion, $this->currentVersion);
switch ($compareVersions) {
case -1:
$this->setRebootType('downgrade');
break;
case 0:
// we should never get here, but if we do, then no reboot is required and just return
return;
case 1:
$this->setRebootType('update');
break;
}
// Detect if third-party drivers were part of the update process
$processWaitingThirdPartyDrivers = "inotifywait -q /boot/changes.txt -e move_self,delete_self";
$processWaitingThirdPartyDrivers = "inotifywait -q " . self::CURRENT_CHANGES_TXT_PATH . " -e move_self,delete_self";
// Run the ps command to list processes and check if the process is running
$ps_command = "ps aux | grep -E \"$processWaitingThirdPartyDrivers\" | grep -v \"grep -E\"";
$output = shell_exec($ps_command) ?? '';
if ($this->rebootType != '' && strpos($output, $processWaitingThirdPartyDrivers) !== false) {
$this->rebootType = 'thirdPartyDriversDownloading';
$this->setRebootType('thirdPartyDriversDownloading');
}
}
/**
* Gets the type of reboot required, which can be 'update', 'downgrade', or 'thirdPartyDriversDownloading'.
*
* @return string The type of reboot required.
*/
public function getRebootType()
{
return $this->rebootType;
}
/**
* Detects and retrieves the version information related to the system reboot based on the contents of the '/boot/changes.txt' file.
*
* @return string The system version information or 'Not found' if not found, or 'File not found' if the file is not present.
*/
public function getRebootVersion()
private function readChangesTxt(string $file_path = self::CURRENT_CHANGES_TXT_PATH)
{
$file_path = '/boot/changes.txt';
// Check if the file exists
if (file_exists($file_path)) {
// Open the file for reading
$file = fopen($file_path, 'r');
// Read the file line by line until we find a line that starts with '# Version'
while (($line = fgets($file)) !== false) {
if (strpos($line, '# Version') === 0) {
// Use a regular expression to extract the full version string
if (preg_match('/# Version\s+(\S+)/', $line, $matches)) {
$fullVersion = $matches[1];
return $fullVersion;
} else {
return 'Not found';
}
exec("head -n4 $file_path", $rows);
foreach ($rows as $row) {
$i = stripos($row,'version');
if ($i !== false) {
[$version, $releaseDate] = explode(' ', trim(substr($row, $i+7)));
break;
}
}
// Close the file
fclose($file);
return [
'releaseDate' => $releaseDate ?? 'Not found',
'version' => $version ?? 'Not found',
];
} else {
return 'File not found';
}
}
/**
* Sets the current version of the Unraid server for comparison with the reboot version.
*/
private function setCurrentVersion() {
// output ex: version="6.13.0-beta.2.1"
$raw = @file_get_contents(self::CURRENT_VERSION_PATH) ?: '';
// Regular expression to match the version between the quotes
$pattern = '/version="([^"]+)"/';
if (preg_match($pattern, $raw, $matches)) {
$this->currentVersion = $matches[1];
}
}
private function setRebootDetails()
{
$rebootDetails = $this->readChangesTxt();
$this->rebootReleaseDate = $rebootDetails['releaseDate'];
$this->rebootVersion = $rebootDetails['version'];
}
private function setRebootType($rebootType)
{
$this->rebootType = $rebootType;
}
/**
* If self::PREVIOUS_BZ_ROOT_PATH exists, then the user has the option to downgrade to the previous version.
* Parse the text file /boot/previous/changes.txt to get the version number of the previous version.
* Then we move some files around and reboot.
*/
public function setPrevious()
{
if (@file_exists(self::PREVIOUS_BZ_ROOT_PATH) && @file_exists(self::PREVIOUS_CHANGES_TXT_PATH)) {
$parseOutput = $this->readChangesTxt(self::PREVIOUS_CHANGES_TXT_PATH);
$this->previousVersion = $parseOutput['version'];
$this->previousReleaseDate = $parseOutput['releaseDate'];
}
}
}

View File

@@ -15,6 +15,7 @@ $webguiGlobals = $GLOBALS;
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
/**
* ServerState class encapsulates server-related information and settings.
*
@@ -33,11 +34,20 @@ class ServerState
protected $webguiGlobals;
private $var;
private $flashbackupCfg;
private $apiKey = '';
private $apiVersion = '';
private $avatar = '';
private $email = '';
private $extraOrigins = [];
private $flashBackupActivated = '';
private $hasRemoteApikey = false;
private $registeredTime = '';
private $username = '';
private $connectPluginInstalled = '';
private $connectPluginVersion;
private $configErrorEnum = [
"error" => 'UNKNOWN_ERROR',
"ineligible" => 'INELIGIBLE',
"invalid" => 'INVALID',
"nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN',
@@ -47,16 +57,19 @@ class ServerState
private $rebootDetails;
private $caseModel = '';
private $keyfileBase64UrlSafe = '';
private $updateOsCheck;
private $updateOsNotificationsEnabled = false;
private $updateOsResponse;
private $updateOsIgnoredReleases = [];
public $myServersFlashCfg = [];
public $myServersMemoryCfg = [];
public $host = 'unknown';
public $combinedKnownOrigins = [];
public $nginxCfg;
public $flashbackupStatus;
public $registered;
public $nginxCfg = [];
public $flashbackupStatus = [];
public $registered = false;
public $myServersMiniGraphConnected = false;
public $keyfileBase64 = '';
@@ -74,11 +87,42 @@ class ServerState
// echo "<pre>" . json_encode($this->webguiGlobals, JSON_PRETTY_PRINT) . "</pre>";
$this->var = (array)parse_ini_file('state/var.ini');
$this->nginxCfg = parse_ini_file('/var/local/emhttp/nginx.ini');
$this->nginxCfg = @parse_ini_file('/var/local/emhttp/nginx.ini') ?? [];
$this->flashbackupCfg = '/var/local/emhttp/flashbackup.ini';
$this->flashbackupStatus = (file_exists($this->flashbackupCfg)) ? @parse_ini_file($this->flashbackupCfg) : [];
$this->osVersion = $this->var['version'];
$this->osVersionBranch = trim(@exec('plugin category /var/log/plugins/unRAIDServer.plg') ?? 'stable');
$caseModelFile = '/boot/config/plugins/dynamix/case-model.cfg';
$this->caseModel = file_exists($caseModelFile) ? htmlspecialchars(@file_get_contents($caseModelFile), ENT_HTML5, 'UTF-8') : '';
$this->rebootDetails = new RebootDetails();
$this->keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
if ($this->keyfileBase64 !== false) {
$this->keyfileBase64 = @base64_encode($this->keyfileBase64);
$this->keyfileBase64UrlSafe = str_replace(['+', '/', '='], ['-', '_', ''], trim($this->keyfileBase64));
}
$this->updateOsCheck = new UnraidOsCheck();
$this->updateOsIgnoredReleases = $this->updateOsCheck->getIgnoredReleases();
$this->updateOsNotificationsEnabled = !empty(@$this->getWebguiGlobal('notify', 'unraidos'));
$this->updateOsResponse = $this->updateOsCheck->getUnraidOSCheckResult();
$this->setConnectValues();
}
/**
* Retrieve the value of a webgui global setting.
*/
public function getWebguiGlobal(string $key, string $subkey = null) {
if (!$subkey) {
return _var($this->webguiGlobals, $key, '');
}
$keyArray = _var($this->webguiGlobals, $key, []);
return _var($keyArray, $subkey, '');
}
private function setConnectValues() {
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) {
$this->connectPluginInstalled = 'dynamix.unraid.net.plg';
}
@@ -89,11 +133,29 @@ class ServerState
$this->connectPluginInstalled .= '_installFailed';
}
// exit early if the plugin is not installed
if (!$this->connectPluginInstalled) {
return;
}
$this->connectPluginVersion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
: (file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
: 'base-' . $this->var['version']);
$this->getMyServersCfgValues();
$this->getConnectKnownOrigins();
$this->getFlashBackupStatus();
}
private function getFlashBackupStatus() {
$flashbackupCfg = '/var/local/emhttp/flashbackup.ini';
$this->flashbackupStatus = (file_exists($flashbackupCfg)) ? @parse_ini_file($flashbackupCfg) : [];
$this->flashBackupActivated = empty($this->flashbackupStatus['activated']) ? '' : 'true';
}
private function getMyServersCfgValues() {
/**
* @todo can we read this from somewhere other than the flash? Connect page uses this path and /boot/config/plugins/dynamix.my.servers/myservers.cfg…
* - $myservers_memory_cfg_path ='/var/local/emhttp/myservers.cfg';
@@ -118,15 +180,17 @@ class ServerState
$this->myServersFlashCfg['remote']['dynamicRemoteAccessType'] = "DISABLED";
}
$this->osVersion = $this->var['version'];
$this->osVersionBranch = trim(@exec('plugin category /var/log/plugins/unRAIDServer.plg') ?? 'stable');
$this->apiKey = $this->myServersFlashCfg['upc']['apikey'] ?? '';
$this->apiVersion = $this->myServersFlashCfg['api']['version'] ?? '';
$this->avatar = (!empty($this->myServersFlashCfg['remote']['avatar']) && $this->connectPluginInstalled) ? $this->myServersFlashCfg['remote']['avatar'] : '';
$this->email = $this->myServersFlashCfg['remote']['email'] ?? '';
$this->hasRemoteApikey = !empty($this->myServersFlashCfg['remote']['apikey']);
$this->registered = !empty($this->myServersFlashCfg['remote']['apikey']) && $this->connectPluginInstalled;
$this->registeredTime = $this->myServersFlashCfg['remote']['regWizTime'] ?? '';
$this->username = $this->myServersFlashCfg['remote']['username'] ?? '';
}
$caseModelFile = '/boot/config/plugins/dynamix/case-model.cfg';
$this->caseModel = file_exists($caseModelFile) ? file_get_contents($caseModelFile) : '';
$this->rebootDetails = new RebootDetails();
private function getConnectKnownOrigins() {
/**
* Allowed origins warning displayed when the current webGUI URL is NOT included in the known lists of allowed origins.
* Include localhost in the test, but only display HTTP(S) URLs that do not include localhost.
@@ -141,6 +205,11 @@ class ServerState
$combinedOrigins = $allowedOrigins . "," . $extraOrigins; // combine the two strings for easier searching
$combinedOrigins = str_replace(" ", "", $combinedOrigins); // replace any spaces with nothing
$hostNotKnown = stripos($combinedOrigins, $this->host) === false; // check if the current host is in the combined list of origins
if ($extraOrigins) {
$this->extraOrigins = explode(",", $extraOrigins);
}
if ($hostNotKnown) {
$this->combinedKnownOrigins = explode(",", $combinedOrigins);
@@ -157,25 +226,8 @@ class ServerState
}
}
}
$this->keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
if ($this->keyfileBase64 !== false) {
$this->keyfileBase64 = @base64_encode($this->keyfileBase64);
$this->keyfileBase64UrlSafe = str_replace(['+', '/', '='], ['-', '_', ''], trim($this->keyfileBase64));
}
/**
* updateOsResponse is provided by the dynamix.plugin.manager/scripts/unraidcheck script saving to /tmp/unraidcheck/result.json
*/
$this->updateOsResponse = @json_decode(@file_get_contents('/tmp/unraidcheck/result.json'), true);
}
/**
* Retrieve the value of a webgui global setting.
*/
public function getWebguiGlobal(string $key) {
return $this->webguiGlobals[$key];
}
/**
* Retrieve the server information as an associative array
*
@@ -184,71 +236,84 @@ class ServerState
public function getServerState()
{
$serverState = [
"apiKey" => $this->myServersFlashCfg['upc']['apikey'] ?? '',
"apiVersion" => $this->myServersFlashCfg['api']['version'] ?? '',
"avatar" => (!empty($this->myServersFlashCfg['remote']['avatar']) && $this->connectPluginInstalled) ? $this->myServersFlashCfg['remote']['avatar'] : '',
"array" => [
"state" => @$this->getWebguiGlobal('var', 'fsState'),
"progress" => @$this->getWebguiGlobal('var', 'fsProgress'),
],
"apiKey" => $this->apiKey,
"apiVersion" => $this->apiVersion,
"avatar" => $this->avatar,
"caseModel" => $this->caseModel,
"config" => [
'valid' => ($this->var['configValid'] === 'yes'),
'error' => isset($this->configErrorEnum[$this->var['configValid']]) ? $this->configErrorEnum[$this->var['configValid']] : 'UNKNOWN_ERROR',
'error' => isset($this->configErrorEnum[$this->var['configValid']]) ? $this->configErrorEnum[$this->var['configValid']] : null,
],
"connectPluginInstalled" => $this->connectPluginInstalled,
"connectPluginVersion" => $this->connectPluginVersion,
"csrf" => $this->var['csrf_token'],
"dateTimeFormat" => [
"date" => @$this->getWebguiGlobal('display')['date'] ?? '',
"time" => @$this->getWebguiGlobal('display')['time'] ?? '',
"date" => @$this->getWebguiGlobal('display', 'date') ?? '',
"time" => @$this->getWebguiGlobal('display', 'time') ?? '',
],
"description" => $this->var['COMMENT'] ? htmlspecialchars($this->var['COMMENT']) : '',
"description" => $this->var['COMMENT'] ? htmlspecialchars($this->var['COMMENT'], ENT_HTML5, 'UTF-8') : '',
"deviceCount" => $this->var['deviceCount'],
"email" => $this->myServersFlashCfg['remote']['email'] ?? '',
"email" => $this->email,
"expireTime" => 1000 * (($this->var['regTy'] === 'Trial' || strstr($this->var['regTy'], 'expired')) ? $this->var['regTm2'] : 0),
"extraOrigins" => explode(',', $this->myServersFlashCfg['api']['extraOrigins'] ?? ''),
"extraOrigins" => $this->extraOrigins,
"flashProduct" => $this->var['flashProduct'],
"flashVendor" => $this->var['flashVendor'],
"flashBackupActivated" => empty($this->flashbackupStatus['activated']) ? '' : 'true',
"flashBackupActivated" => $this->flashBackupActivated,
"guid" => $this->var['flashGUID'],
"hasRemoteApikey" => !empty($this->myServersFlashCfg['remote']['apikey']),
"internalPort" => $_SERVER['SERVER_PORT'],
"hasRemoteApikey" => $this->hasRemoteApikey,
"internalPort" => _var($_SERVER, 'SERVER_PORT'),
"keyfile" => $this->keyfileBase64UrlSafe,
"lanIp" => ipaddr(),
"locale" => (!empty($_SESSION) && $_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
"model" => $this->var['SYS_MODEL'],
"name" => htmlspecialchars($this->var['NAME']),
"model" => $this->var['SYS_MODEL'] ? htmlspecialchars($this->var['SYS_MODEL'], ENT_HTML5, 'UTF-8') : '',
"name" => htmlspecialchars($this->var['NAME'], ENT_HTML5, 'UTF-8'),
"osVersion" => $this->osVersion,
"osVersionBranch" => $this->osVersionBranch,
"protocol" => $_SERVER['REQUEST_SCHEME'],
"rebootType" => $this->rebootDetails->getRebootType(),
"regDev" => @(int)$this->var['regDev'] ?? 0,
"protocol" => _var($_SERVER, 'REQUEST_SCHEME'),
"rebootType" => $this->rebootDetails->rebootType,
"rebootVersion" => $this->rebootDetails->rebootVersion,
"regDevs" => @(int)$this->var['regDevs'] ?? 0,
"regGen" => @(int)$this->var['regGen'],
"regGuid" => @$this->var['regGUID'] ?? '',
"regTo" => @htmlspecialchars($this->var['regTo']) ?? '',
"regTo" => @htmlspecialchars($this->var['regTo'], ENT_HTML5, 'UTF-8') ?? '',
"regTm" => $this->var['regTm'] ? @$this->var['regTm'] * 1000 : '', // JS expects milliseconds
"regTy" => @$this->var['regTy'] ?? '',
"regExp" => $this->var['regExp'] ? @$this->var['regExp'] * 1000 : '', // JS expects milliseconds
"registered" => $this->registered,
"registeredTime" => $this->myServersFlashCfg['remote']['regWizTime'] ?? '',
"site" => $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'],
"registeredTime" => $this->registeredTime,
"site" => _var($_SERVER, 'REQUEST_SCHEME') . "://" . _var($_SERVER, 'HTTP_HOST'),
"state" => strtoupper(empty($this->var['regCheck']) ? $this->var['regTy'] : $this->var['regCheck']),
"theme" => [
"banner" => !empty($this->getWebguiGlobal('display')['banner']),
"bannerGradient" => $this->getWebguiGlobal('display')['showBannerGradient'] === 'yes' ?? false,
"bgColor" => ($this->getWebguiGlobal('display')['background']) ? '#' . $this->getWebguiGlobal('display')['background'] : '',
"descriptionShow" => (!empty($this->getWebguiGlobal('display')['headerdescription']) && $this->getWebguiGlobal('display')['headerdescription'] !== 'no'),
"metaColor" => ($this->getWebguiGlobal('display')['headermetacolor'] ?? '') ? '#' . $this->getWebguiGlobal('display')['headermetacolor'] : '',
"name" => $this->getWebguiGlobal('display')['theme'],
"textColor" => ($this->getWebguiGlobal('display')['header']) ? '#' . $this->getWebguiGlobal('display')['header'] : '',
"banner" => !empty($this->getWebguiGlobal('display', 'banner')),
"bannerGradient" => $this->getWebguiGlobal('display', 'showBannerGradient') === 'yes' ?? false,
"bgColor" => ($this->getWebguiGlobal('display', 'background')) ? '#' . $this->getWebguiGlobal('display', 'background') : '',
"descriptionShow" => (!empty($this->getWebguiGlobal('display', 'headerdescription')) && $this->getWebguiGlobal('display', 'headerdescription') !== 'no'),
"metaColor" => ($this->getWebguiGlobal('display', 'headermetacolor') ?? '') ? '#' . $this->getWebguiGlobal('display', 'headermetacolor') : '',
"name" => $this->getWebguiGlobal('display', 'theme'),
"textColor" => ($this->getWebguiGlobal('display', 'header')) ? '#' . $this->getWebguiGlobal('display', 'header') : '',
],
"ts" => time(),
"uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))),
"username" => $this->myServersFlashCfg['remote']['username'] ?? '',
"wanFQDN" => $this->nginxCfg['NGINX_WANFQDN'] ?? '',
"username" => $this->username,
"wanFQDN" => @$this->nginxCfg['NGINX_WANFQDN'] ?? '',
];
if ($this->combinedKnownOrigins) {
$serverState['combinedKnownOrigins'] = $this->combinedKnownOrigins;
}
if ($this->updateOsIgnoredReleases) {
$serverState['updateOsIgnoredReleases'] = $this->updateOsIgnoredReleases;
}
if ($this->updateOsNotificationsEnabled) {
$serverState['updateOsNotificationsEnabled'] = $this->updateOsNotificationsEnabled;
}
if ($this->updateOsResponse) {
$serverState['updateOsResponse'] = $this->updateOsResponse;
}
@@ -257,11 +322,21 @@ class ServerState
}
/**
* Retrieve the server information as a JSON string
* Retrieve the server information as JSON
*
* @return string A JSON string containing server information.
* @return string
*/
public function getServerStateJson() {
return json_encode($this->getServerState());
}
/**
* Retrieve the server information as JSON string with converted special characters to HTML entities
*
* @return string
*/
public function getServerStateJsonForHtmlAttr() {
$json = json_encode($this->getServerState());
return htmlspecialchars($json, ENT_QUOTES, 'UTF-8');
}
}

View File

@@ -72,13 +72,13 @@ class WebComponentTranslations
'<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>' => '<p>' . _('To continue using Unraid OS you may purchase a license key.') . ' ' . _('Alternately, you may request a Trial extension.') . '</p>',
'<p>To support more storage devices as your server grows, click Upgrade Key.</p>' => '<p>' . _('To support more storage devices as your server grows, click Upgrade Key.') . '</p>',
'<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>' => '<p>' . _('You have used all your Trial extensions.') . ' ' . _('To continue using Unraid OS you may purchase a license key.') . '</p>',
'<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>' => '<p>' . _('Your **Trial** key includes all the functionality and device support of a **Pro** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* until the next time you Stop the array or reboot your server') . '</p><p>' . _('At that point you may either purchase a license key or request a *Trial* extension.') . '</p>',
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>' => '<p>' . _('Your **Trial** key includes all the functionality and device support of an **Unleashed** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* until the next time you Stop the array or reboot your server') . '</p><p>' . _('At that point you may either purchase a license key or request a *Trial* extension.') . '</p>',
'<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>' => '<p>' . _('Your license key file is corrupted or missing.') . ' ' . _('The key file should be located in the /config directory on your USB Flash boot device') . '</p><p>' . _('If you do not have a backup copy of your license key file you may attempt to recover your key with your Unraid.net account') . '</p><p>' . _('If this was an expired Trial installation, you may purchase a license key.') . '</p>',
'<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>' => '<p>' . _('Your license key file is corrupted or missing.') . ' ' . _('The key file should be located in the /config directory on your USB Flash boot device') . '</p><p>' . _('If you do not have a backup copy of your license key file you may attempt to recover your key with your Unraid.net account') . '</p><p>' . _('If this was an expired Trial installation, you may purchase a license key.') . '</p>',
'<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class="list-disc pl-16px"><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>' => '<p>' . _('Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key.') . ' ' . _('A <em>Trial</em> key provides all the functionality of a Pro Registration key.') . '</p><p>' . _('Registration keys are bound to your USB Flash boot device serial number (GUID).') . ' ' . _('Please use a high quality name brand device at least 1GB in size.') . '</p><p>' . _('Note: USB memory card readers are generally not supported because most do not present unique serial numbers.') . '</p><p>' . _('*Important:*') . '</p><ul class="list-disc pl-16px"><li>' . _('Please make sure your server time is accurate to within 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>',
'<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class="list-disc pl-16px"><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>' => '<p>' . _('Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key.') . ' ' . _('A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.') . '</p><p>' . _('Registration keys are bound to your USB Flash boot device serial number (GUID).') . ' ' . _('Please use a high quality name brand device at least 1GB in size.') . '</p><p>' . _('Note: USB memory card readers are generally not supported because most do not present unique serial numbers.') . '</p><p>' . _('*Important:*') . '</p><ul class="list-disc pl-16px"><li>' . _('Please make sure your server time is accurate to within 5 minutes') . '</li><li>' . _('Please make sure there is a DNS server specified') . '</li>>' . '</ul>',
'<p>Your Trial key requires an internet connection.</p><p><a href="/Settings/NetworkSettings" class="underline">Please check Settings > Network</a></p>' => '<p>' . _('Your Trial key requires an internet connection') . '</p><p><a href="/Settings/NetworkSettings" class="underline">' . _('Please check Settings > Network') . '</a></p>',
'<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>',
'A Trial key provides all the functionality of a Pro Registration key' => _('A Trial key provides all the functionality of a Pro Registration key'),
'A Trial key provides all the functionality of an Unleashed Registration key' => _('A Trial key provides all the functionality of an Unleashed Registration key'),
'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'),
'ago' => _('ago'),
'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin.') . ' ' . _('Get started by installing the plugin.'),
@@ -89,8 +89,10 @@ class WebComponentTranslations
'Beta' => _('Beta'),
'Blacklisted USB Flash GUID' => _('Blacklisted USB Flash GUID'),
'BLACKLISTED' => _('BLACKLISTED'),
'Calculating OS Update Eligibility…' => _('Calculating OS Update Eligibility…'),
'Calculating trial expiration…' => _('Calculating trial expiration…'),
'Callback redirect type not present or incorrect' => _('Callback redirect type not present or incorrect'),
'Cancel {0}' => sprintf(_('Cancel %s'), '{0}'),
'Cancel' => _('Cancel'),
'Cannot access your USB Flash boot device' => _('Cannot access your USB Flash boot device'),
'Cannot validate Unraid Trial key' => _('Cannot validate Unraid Trial key'),
@@ -107,8 +109,10 @@ class WebComponentTranslations
'Close' => _('Close'),
'Configure Connect Features' => _('Configure Connect Features'),
'Confirm and start update' => _('Confirm and start update'),
'Confirm to Install Unraid OS {0}' => sprintf(_('Confirm to Install Unraid OS %s'), '{0}'),
'Connected' => _('Connected'),
'Contact Support' => _('Contact Support'),
'Continue' => _('Continue'),
'Copied' => _('Copied'),
'Copy Key URL' => _('Copy Key URL'),
'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{0}'),
@@ -122,24 +126,30 @@ class WebComponentTranslations
'Downgrade Unraid OS to {0}' => sprintf(_('Downgrade Unraid OS to %s'), '{0}'),
'Downgrade Unraid OS' => _('Downgrade Unraid OS'),
'Downgrades are only recommended if you\'re unable to solve a critical issue.' => _('Downgrades are only recommended if you\'re unable to solve a critical issue.'),
'Download Diagnostics' => _('Download Diagnostics'),
'Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.' => _('Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.'),
'Download unraid-api Logs' => _('Download unraid-api Logs'),
'Dynamic Remote Access' => _('Dynamic Remote Access'),
'Enable update notifications' => _('Enable update notifications'),
'Enhance your experience with Unraid Connect' => _('Enhance your experience with Unraid Connect'),
'Enhance your Unraid experience with Connect' => _('Enhance your Unraid experience with Connect'),
'Enhance your Unraid experience' => _('Enhance your Unraid experience'),
'Error creatiing a trial key. Please try again later.' => _('Error creatiing a trial key. Please try again later.'),
'Error creating a trial key. Please try again later.' => _('Error creating a trial key. Please try again later.'),
'Error Parsing Changelog • {0}' => sprintf(_('Error Parsing Changelog • %s'), '{0}'),
'Error' => _('Error'),
'Expired {0}' => sprintf(_('Expired %s'), '{0}'),
'Expired' => _('Expired'),
'Expires at {0}' => sprintf(_('Expires at %s'), '{0}'),
'Expires in {0}' => sprintf(_('Expires in %s'), '{0}'),
'Extend License to Update' => _('Extend License to Update'),
'Extend License' => _('Extend License'),
'Extend Trial' => _('Extend Trial'),
'Extending your free trial by 15 days' => _('Extending your free trial by 15 days'),
'Extension Installed' => _('Extension Installed'),
'Failed to {0} {1} Key' => sprintf(_('Failed to %1s %2s Key'), '{0}', '{1}'),
'Failed to install key' => _('Failed to install key'),
'Failed to update Connect account configuration' => _('Failed to update Connect account configuration'),
'Fetching & parsing changelog…' => _('Fetching & parsing changelog…'),
'Fix Error' => _('Fix Error'),
'Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.' => sprintf(_('Flash Backup is not available. Navigate to %s/Main/Settings/Flash to try again then come back to this page.'), '{0}'),
'Flash GUID Error' => _('Flash GUID Error'),
@@ -152,18 +162,24 @@ class WebComponentTranslations
'Go to Connect plugin settings' => _('Go to Connect plugin settings'),
'Go to Connect' => _('Go to Connect'),
'Go to Management Access Now' => _('Go to Management Access Now'),
'Go to Settings > Notifications to enable automatic OS update notifications for future releases.' => _('Go to Settings > Notifications to enable automatic OS update notifications for future releases.'),
'Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.' => _('Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.'),
'Go to Tools > Management Access to ensure your backup is up-to-date.' => _('Go to Tools > Management Access to ensure your backup is up-to-date.'),
'Go to Tools > Registration to fix' => _('Go to Tools > Registration to fix'),
'Go to Tools > Registration to Learn More' => _('Go to Tools > Registration to Learn More'),
'Go to Tools > Update OS for more options.' => _('Go to Tools > Update OS for more options.'),
'Go to Tools > Update' => _('Go to Tools > Update'),
'hour' => sprintf(_('%s hour'), '{n}') . ' | ' . sprintf(_('%s hours'), '{n}'),
'I have made a Flash Backup' => _('I have made a Flash Backup'),
'If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.' => _('If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.'),
'Ignore this message if you are currently connected via Remote Access or VPN.' => _('Ignore this message if you are currently connected via Remote Access or VPN.'),
'Ignore this release until next reboot' => _('Ignore this release until next reboot'),
'Ignored Releases' => _('Ignored Releases'),
'In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.' => _('In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.'),
'Install Connect' => _('Install Connect'),
'Install Recovered' => _('Install Recovered'),
'Install Replaced' => _('Install Replaced'),
'Install Unraid OS {0}' => sprintf(_('Install Unraid OS %s'), '{0}'),
'Install' => _('Install'),
'Installed' => _('Installed'),
'Installing Extended Trial' => _('Installing Extended Trial'),
@@ -175,6 +191,9 @@ class WebComponentTranslations
'Invalid API Key Format' => _('Invalid API Key Format'),
'Invalid API Key' => _('Invalid API Key'),
'Invalid installation' => _('Invalid installation'),
'It\s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'),
'Key ineligible for {0}' => sprintf(_('Key ineligible for %s'), '{0}'),
'Key ineligible for future releases' => _('Key ineligible for future releases'),
'Keyfile required to check replacement status' => _('Keyfile required to check replacement status'),
'LAN IP {0}' => sprintf(_('LAN IP %s'), '{0}'),
'LAN IP Copied' => _('LAN IP Copied'),
@@ -182,12 +201,15 @@ class WebComponentTranslations
'Last checked: {0}' => sprintf(_('Last checked: %s'), '{0}'),
'Learn more about the error' => _('Learn more about the error'),
'Learn more and fix' => _('Learn more and fix'),
'Learn more and link your key to your account' => _('Learn more and link your key to your account'),
'Learn More' => _('Learn More'),
'Learn more' => _('Learn more'),
'Let\'s Unleash your Hardware!' => _('Let\'s Unleash your Hardware!'),
'License key actions' => _('License key actions'),
'License key type' => _('License key type'),
'License Management' => _('License Management'),
'Link Key' => _('Link Key'),
'Linked to Unraid.net account' => _('Linked to Unraidnet account'),
'Loading' => _('Loading'),
'Manage Unraid.net Account in new tab' => _('Manage Unraid.net Account in new tab'),
'Manage Unraid.net Account' => _('Manage Unraid.net Account'),
@@ -196,6 +218,7 @@ class WebComponentTranslations
'minute' => sprintf(_('%s minute'), '{n}') . ' | ' . sprintf(_('%s minutes'), '{n}'),
'Missing key file' => _('Missing key file'),
'month' => sprintf(_('%s month'), '{n}') . ' | ' . sprintf(_('%s months'), '{n}'),
'More options' => _('More options'),
'Multiple License Keys Present' => _('Multiple License Keys Present'),
'Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.' => _('Never ever be left without a backup of your config.') . ' ' . _('If you need to change flash drives, generate a backup from Connect and be up and running in minutes.'),
'New Version: {0}' => sprintf(_('New Version: %s'), '{0}'),
@@ -204,14 +227,18 @@ class WebComponentTranslations
'No Keyfile' => _('No Keyfile'),
'No thanks' => _('No thanks'),
'No USB flash configuration data' => _('No USB flash configuration data'),
'Not Linked' => _('Not Linked'),
'On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.' => _('On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.'),
'Online Flash Backup' => _('Online Flash Backup'),
'Open a bug report' => _('Open a bug report'),
'Open Dropdown' => _('Open Dropdown'),
'Opens Connect in new tab' => _('Opens Connect in new tab'),
'Original release date {0}' => sprintf(_('Original release date %s'), '{0}'),
'OS Update Eligibility Expired' => _('OS Update Eligibility Expired'),
'Performing actions' => _('Performing actions'),
'Please confirm the update details below' => _('Please confirm the update details below'),
'Please finish the initiated downgrade to enable updates.' => _('Please finish the initiated downgrade to enable updates.'),
'Please finish the initiated update to enable a downgrade.' => _('Please finish the initiated update to enable a downgrade.'),
'Please fix any errors and try again.' => _('Please fix any errors and try again.'),
'Please keep this window open while we perform some actions' => _('Please keep this window open while we perform some actions'),
'Please keep this window open' => _('Please keep this window open'),
@@ -238,14 +265,20 @@ class WebComponentTranslations
'Recover Key' => _('Recover Key'),
'Recovered' => _('Recovered'),
'Redeem Activation Code' => _('Redeem Activation Code'),
'Refresh' => _('Refresh'),
'Registered on' => _('Registered on'),
'Registered to' => _('Registered to'),
'Registration key / USB Flash GUID mismatch' => _('Registration key / USB Flash GUID mismatch'),
'Release date {0}' => sprintf(_('Release date %s'), '{0}'),
'Release requires verification to update' => _('Release requires verification to update'),
'Reload' => _('Reload'),
'Remark: Unraid\'s WAN IPv4 {0} does not match your client\'s WAN IPv4 {1}.' => sprintf(_('Remark: Unraid\'s WAN IPv4 %1s does not match your client\'s WAN IPv4 %2s.'), '{0}', '{1}'),
'Remark: your WAN IPv4 is {0}' => sprintf(_('Remark: your WAN IPv4 is %s'), '{0}'),
'Remove from ignore list' => _('Remove from ignore list'),
'Remove' => _('Remove'),
'Replace Key' => _('Replace Key'),
'Replaced' => _('Replaced'),
'Requires the local unraid-api to be running successfully' => _('Requires the local unraid-api to be running successfully'),
'Restarting unraid-api…' => _('Restarting unraid-api…'),
'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'),
'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'),
@@ -257,6 +290,7 @@ class WebComponentTranslations
'Sign In to utilize Unraid Connect' => _('Sign In to utilize Unraid Connect'),
'Sign In to your Unraid.net account to get started' => _('Sign In to your Unraid.net account to get started'),
'Sign In with Unraid.net Account' => _('Sign In with Unraid.net Account'),
'Sign In' => _('Sign In'),
'Sign Out Failed' => _('Sign Out Failed'),
'Sign Out of Unraid.net' => _('Sign Out of Unraid.net'),
'Sign Out requires the local unraid-api to be running' => _('Sign Out requires the local unraid-api to be running'),
@@ -298,6 +332,7 @@ class WebComponentTranslations
'Unable to fetch client WAN IPv4' => _('Unable to fetch client WAN IPv4'),
'Unable to open release notes' => _('Unable to open release notes'),
'Unknown error' => _('Unknown error'),
'Unknown' => _('Unknown'),
'unlimited' => _('unlimited'),
'Unraid {0} Available' => sprintf(_('Unraid %s Available'), '{0}'),
'Unraid {0} Update Available' => sprintf(_('Unraid %s Update Available'), '{0}'),
@@ -310,10 +345,13 @@ class WebComponentTranslations
'Unraid logo animating with a wave like effect' => _('Unraid logo animating with a wave like effect'),
'Unraid OS {0} Released' => sprintf(_('Unraid OS %s Released'), '{0}'),
'Unraid OS {0} Update Available' => sprintf(_('Unraid OS %s Update Available'), '{0}'),
'Unraid OS is up-to-date' => _('Unraid OS is up-to-date'),
'Unraid OS Update Available' => _('Unraid OS Update Available'),
'unraid-api is offline' => _('unraid-api is offline'),
'Up-to-date with eligible releases' => _('Up-to-date with eligible releases'),
'Up-to-date' => _('Up-to-date'),
'Update Available' => _('Update Available'),
'Update Released' => _('Update Released'),
'Update Unraid OS confirmation required' => _('Update Unraid OS confirmation required'),
'Update Unraid OS' => _('Update Unraid OS'),
'Updating 3rd party drivers' => _('Updating 3rd party drivers'),
@@ -322,15 +360,20 @@ class WebComponentTranslations
'Uptime {0}' => sprintf(_('Uptime %s'), '{0}'),
'USB Flash device error' => _('USB Flash device error'),
'USB Flash has no serial number' => _('USB Flash has no serial number'),
'Verify to Update' => _('Verify to Update'),
'Version available for restore {0}' => sprintf(_('Version available for restore %s'), '{0}'),
'Version: {0}' => sprintf(_('Version: %s'), '{0}'),
'View Available Updates' => _('View Available Updates'),
'View Changelog & Update' => _('View Changelog & Update'),
'View Changelog for {0}' => sprintf(_('View Changelog for %s'), '{0}'),
'View Changelog on Docs' => _('View Changelog on Docs'),
'View Changelog to Start Update' => _('View Changelog to Start Update'),
'View Changelog' => _('View Changelog'),
'View on Docs' => _('View on Docs'),
'View release notes' => _('View release notes'),
'We recommend backing up your USB Flash Boot Device before starting the update.' => _('We recommend backing up your USB Flash Boot Device before starting the update.'),
'year' => sprintf(_('%s year'), '{n}') . ' | ' . sprintf(_('%s years'), '{n}'),
'You are still eligible to access OS updates that were published on or before {1}.' => sprintf(_('You are still eligible to access OS updates that were published on or before %s.'), '{1}'),
'You can also manually create a new backup by clicking the Create Flash Backup button.' => _('You can also manually create a new backup by clicking the Create Flash Backup button.'),
'You can manually create a backup by clicking the Create Flash Backup button.' => _('You can manually create a backup by clicking the Create Flash Backup button.'),
'You have already activated the Flash Backup feature via the Unraid Connect plugin.' => _('You have already activated the Flash Backup feature via the Unraid Connect plugin.'),
@@ -339,7 +382,10 @@ class WebComponentTranslations
'You may still update to releases dated prior to your update expiration date.' => _('You may still update to releases dated prior to your update expiration date.'),
'You\'re one step closer to enhancing your Unraid experience' => _('You\'re one step closer to enhancing your Unraid experience'),
'Your {0} Key has been replaced!' => sprintf(_('Your %s Key has been replaced!'), '{0}'),
'Your free Trial key provides all the functionality of a Pro Registration key' => _('Your free Trial key provides all the functionality of a Pro Registration key'),
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.' => sprintf(_('Your %s license included one year of free updates at the time of purchase.'), '{0}') . ' ' . _('You are now eligible to extend your license and access the latest OS updates.') . ' ' . sprintf(_('You are still eligible to access OS updates that were published on or before %s.'), '{1}'),
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.' => sprintf(_('Your %s license included one year of free updates at the time of purchase.'), '{0}') . ' ' . _('You are now eligible to extend your license and access the latest OS updates.'),
'Your free Trial key provides all the functionality of an Unleashed Registration key' => _('Your free Trial key provides all the functionality of an Unleashed Registration key'),
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
'Your Trial has expired' => _('Your Trial has expired'),
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
];

View File

@@ -11,6 +11,7 @@
$cli = php_sapi_name() == 'cli';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
/**
* @name response_complete
@@ -49,16 +50,20 @@ if ($cli) {
if ($argc > 2) $param1 = $argv[2];
} else {
$command = $_POST['command'];
$param1 = $_POST['param1'];
$param1 = $_POST['param1'] ?? '';
}
if (!in_array($command, $validCommands)) $command = 'none';
if (!file_exists('/usr/local/sbin/unraid-api') || !file_exists('/usr/local/bin/unraid-api/unraid-api')) {
response_complete(406, array('error' => 'Please reinstall the My Servers plugin'));
response_complete(406, array('error' => 'Please reinstall the Unraid Connect plugin'));
}
$output = [];
$retval = null;
switch ($command) {
case 'start':
exec('unraid-api start 2>/dev/null', $output, $retval);
$output = implode(PHP_EOL, $output);
response_complete(200, array('result' => $output), $output);
break;
@@ -81,7 +86,7 @@ switch ($command) {
response_complete(200, array('result' => $output), $output);
break;
case 'wanip':
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
$wanip = trim(http_get_contents("https://wanip4.unraid.net/"));
response_complete(200, array('result' => $wanip), $wanip);
break;
case 'none':

View File

@@ -13,37 +13,17 @@ Tag="upload"
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
/**
* @note icon-update is rotated via CSS in myservers1.php
*/
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
// Create an instance of the RebootDetails class
$rebootDetails = new RebootDetails();
/**
* @note icon-update is rotated via CSS in myservers1.php
*
* If /boot/previous/bzroot exists, then the user has the option to downgrade to the previous version.
* Parse the text file /boot/previous/changes.txt to get the version number of the previous version.
* Then we move some files around and reboot.
*/
$restoreVersion = $restoreBranch = $restoreVersionReleaseDate = 'unknown';
$restoreExists = file_exists('/boot/previous/bzroot');
$restoreChangelogPath = '/boot/previous/changes.txt';
// Get the current reboot details if there are any
$rebootDetails->setPrevious();
$serverNameEscaped = htmlspecialchars(str_replace(' ', '_', strtolower($var['NAME'])));
if (file_exists($restoreChangelogPath)) {
exec("head -n4 $restoreChangelogPath", $rows);
foreach ($rows as $row) {
$i = stripos($row,'version');
if ($i !== false) {
[$restoreVersion, $restoreVersionReleaseDate] = explode(' ', trim(substr($row, $i+7)));
break;
}
}
$restoreBranch = strpos($restoreVersion, 'rc') !== false
? _('Next')
: (strpos($restoreVersion, 'beta') !== false
? _('Beta')
: _('Stable'));
}
?>
<script>
@@ -139,7 +119,7 @@ function startDowngrade() {
$.get(
'/plugins/dynamix.plugin.manager/include/Downgrade.php',
{
version: '<?=$restoreVersion?>',
version: '<?= $rebootDetails->previousVersion ?>',
},
function() {
refresh();
@@ -150,7 +130,7 @@ function startDowngrade() {
function confirmDowngrade() {
swal({
title: "_(Confirm Downgrade)_",
text: "<?= $restoreVersion ?><br>_(A reboot will be required)_",
text: "<?= $rebootDetails->previousVersion ?><br>_(A reboot will be required)_",
html: true,
type: 'warning',
showCancelButton: true,
@@ -167,7 +147,7 @@ function confirmDowngrade() {
<unraid-i18n-host>
<unraid-downgrade-os
reboot-version="<?= $rebootDetails->getRebootVersion() ?>"
restore-version="<?= $restoreExists && $restoreVersion != 'unknown' ? $restoreVersion : '' ?>"
restore-release-date="<?= $restoreExists && $restoreVersionReleaseDate != 'unknown' ? $restoreVersionReleaseDate : '' ?>"></unraid-downgrade-os>
reboot-version="<?= $rebootDetails->rebootVersion ?>"
restore-version="<?= $rebootDetails->previousVersion ?>"
restore-release-date="<?= $rebootDetails->previousReleaseDate ?>"></unraid-downgrade-os>
</unraid-i18n-host>

View File

@@ -46,5 +46,5 @@ function flashBackup() {
</script>
<unraid-i18n-host>
<unraid-update-os reboot-version="<?= $rebootDetails->getRebootVersion() ?>"></unraid-update-os>
<unraid-update-os reboot-version="<?= $rebootDetails->rebootVersion ?>"></unraid-update-os>
</unraid-i18n-host>

View File

@@ -0,0 +1,261 @@
<?php
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
/**
* Abstracting this code into a separate file allows us to use it in multiple places without duplicating code.
* 1. unraidcheck script can call this
* require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
* $unraidOsCheck = new UnraidOsCheck();
* $unraidOsCheck->checkForUpdate();
*
* 2. Unraid webgui web components can GET this file with action params to get updates, ignore updates, etc.
* - EX: Unraid webgui web components can check for updates via a GET request and receive a response with the json file directly
* - this is useful for the UPC to check for updates and display a model based on the value
* - `/plugins/dynamix.plugin.manager/scripts/unraidcheck.php?json=true`
* - note the json=true query param to receive a json response
*
* @param action {'check'|'removeAllIgnored'|'removeIgnoredVersion'|'ignoreVersion'} - the action to perform
* @param version {string} - the version to ignore or remove
* @param json {string} - if set to true, will return the json response from the external request
* @param altUrl {URL} - if set, will use this url instead of the default
*/
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/plugins/dynamix.plugin.manager/include/PluginHelpers.php";
class UnraidOsCheck
{
private const BASE_RELEASES_URL = 'https://releases.unraid.net/os';
private const JSON_FILE_IGNORED = '/tmp/unraidcheck/ignored.json';
private const JSON_FILE_IGNORED_KEY = 'updateOsIgnoredReleases';
private const JSON_FILE_RESULT = '/tmp/unraidcheck/result.json';
private const PLG_PATH = '/usr/local/emhttp/plugins/unRAIDServer/unRAIDServer.plg';
public function __construct()
{
$isGetRequest = !empty($_SERVER) && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'GET';
$getHasAction = $_GET !== null && !empty($_GET) && isset($_GET['action']);
if ($isGetRequest && $getHasAction) {
$this->handleGetRequestWithActions();
}
}
private function handleGetRequestWithActions()
{
switch ($_GET['action']) {
case 'check':
$this->checkForUpdate();
break;
case 'removeAllIgnored':
$this->removeAllIgnored();
break;
case 'removeIgnoredVersion':
if (isset($_GET['version'])) {
$this->removeIgnoredVersion($_GET['version']);
}
break;
case 'ignoreVersion':
if (isset($_GET['version'])) {
$this->ignoreVersion($_GET['version']);
}
break;
default:
$this->respondWithError(400, "Unhandled action");
break;
}
}
public function getUnraidOSCheckResult()
{
if (file_exists(self::JSON_FILE_RESULT)) {
return $this->readJsonFile(self::JSON_FILE_RESULT);
}
}
public function getIgnoredReleases()
{
if (!file_exists(self::JSON_FILE_IGNORED)) {
return [];
}
$ignoredData = $this->readJsonFile(self::JSON_FILE_IGNORED);
if (is_array($ignoredData) && array_key_exists(self::JSON_FILE_IGNORED_KEY, $ignoredData)) {
return $ignoredData[self::JSON_FILE_IGNORED_KEY];
}
return [];
}
/** @todo clean up this method to be more extensible */
public function checkForUpdate()
{
// Multi-language support
if (!function_exists('_')) {
function _($text) {return $text;}
}
// this command will set the $notify array
extract(parse_plugin_cfg('dynamix', true));
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
$params = [];
$params['branch'] = plugin('category', self::PLG_PATH, 'stable');
$params['current_version'] = plugin('version', self::PLG_PATH) ?: _var($var,'version');
if (_var($var,'regExp')) $params['update_exp'] = date('Y-m-d', _var($var,'regExp')*1);
$defaultUrl = self::BASE_RELEASES_URL;
// pass a param of altUrl to use the provided url instead of the default
$parsedAltUrl = (array_key_exists('altUrl',$_GET) && $_GET['altUrl']) ? $_GET['altUrl'] : null;
// if $parsedAltUrl pass to params
if ($parsedAltUrl) $params['altUrl'] = $parsedAltUrl;
$urlbase = $parsedAltUrl ?? $defaultUrl;
$url = $urlbase.'?'.http_build_query($params);
$curlinfo = [];
$response = http_get_contents($url,[],$curlinfo);
if (array_key_exists('error', $curlinfo)) {
$response = json_encode(array('error' => $curlinfo['error']), JSON_PRETTY_PRINT);
}
$responseMutated = json_decode($response, true);
if (!$responseMutated) {
$response = json_encode(array('error' => 'Invalid response from '.$urlbase), JSON_PRETTY_PRINT);
$responseMutated = json_decode($response, true);
}
// add params that were used for debugging
$responseMutated['params'] = $params;
// store locally for UPC to access
$this->writeJsonFile(self::JSON_FILE_RESULT, $responseMutated);
// if we have a query param of json=true then just output the json
if (array_key_exists('json',$_GET) && $_GET['json']) {
header('Content-Type: application/json');
echo $response;
exit(0);
}
// send notification if a newer version is available and not ignored
$isNewerVersion = array_key_exists('isNewer',$responseMutated) ? $responseMutated['isNewer'] : false;
$isReleaseIgnored = array_key_exists('version',$responseMutated) ? in_array($responseMutated['version'], $this->getIgnoredReleases()) : false;
if ($responseMutated && $isNewerVersion && !$isReleaseIgnored) {
$output = _var($notify,'plugin');
$server = strtoupper(_var($var,'NAME','server'));
$newver = (array_key_exists('version',$responseMutated) && $responseMutated['version']) ? $responseMutated['version'] : 'unknown';
$script = '/usr/local/emhttp/webGui/scripts/notify';
$event = "System - Unraid [$newver]";
$subject = "Notice [$server] - Version update $newver";
$description = "A new version of Unraid is available";
exec("$script -e ".escapeshellarg($event)." -s ".escapeshellarg($subject)." -d ".escapeshellarg($description)." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
}
exit(0);
}
private function removeAllIgnored()
{
if (file_exists(self::JSON_FILE_IGNORED)) {
$this->deleteJsonFile(self::JSON_FILE_IGNORED);
$this->respondWithSuccess([]);
}
// fail silently if file doesn't exist
}
private function removeIgnoredVersion($removeVersion)
{
if ($this->isValidSemVerFormat($removeVersion)) {
if (file_exists(self::JSON_FILE_IGNORED)) {
$existingData = $this->readJsonFile(self::JSON_FILE_IGNORED);
if (isset($existingData[self::JSON_FILE_IGNORED_KEY])) {
$existingData[self::JSON_FILE_IGNORED_KEY] = array_diff($existingData[self::JSON_FILE_IGNORED_KEY], [$removeVersion]);
$this->writeJsonFile(self::JSON_FILE_IGNORED, $existingData);
$this->respondWithSuccess($existingData);
} else {
$this->respondWithError(400, "No versions to remove in the JSON file");
}
} else {
$this->respondWithError(400, "No JSON file found");
}
} else {
$this->respondWithError(400, "Invalid removeVersion format");
}
}
private function ignoreVersion($version)
{
if ($this->isValidSemVerFormat($version)) {
$newData = [$this::JSON_FILE_IGNORED_KEY => [$version]];
$existingData = file_exists(self::JSON_FILE_IGNORED) ? $this->readJsonFile(self::JSON_FILE_IGNORED) : [];
if (isset($existingData[self::JSON_FILE_IGNORED_KEY])) {
$existingData[self::JSON_FILE_IGNORED_KEY][] = $version;
} else {
$existingData[self::JSON_FILE_IGNORED_KEY] = [$version];
}
$this->writeJsonFile(self::JSON_FILE_IGNORED, $existingData);
$this->respondWithSuccess($existingData);
} else {
$this->respondWithError(400, "Invalid version format");
}
}
private function isValidSemVerFormat($version)
{
return preg_match('/^\d+\.\d+(\.\d+)?(-.+)?$/', $version);
}
private function readJsonFile($file)
{
return @json_decode(@file_get_contents($file), true) ?? [];
}
private function writeJsonFile($file, $data)
{
if (!is_dir(dirname($file))) { // prevents errors when directory doesn't exist
mkdir(dirname($file));
}
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}
private function deleteJsonFile($file)
{
unlink($file);
}
private function respondWithError($statusCode, $message)
{
http_response_code($statusCode);
echo $message;
}
private function respondWithSuccess($data)
{
http_response_code(200);
header('Content-Type: application/json');
echo json_encode($data, JSON_PRETTY_PRINT);
}
}
// Instantiate and handle the request for GET requests with actions vars are duplicated here for multi-use of this file
$isGetRequest = !empty($_SERVER) && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'GET';
$getHasAction = $_GET !== null && !empty($_GET) && isset($_GET['action']);
if ($isGetRequest && $getHasAction) {
new UnraidOsCheck();
}

View File

@@ -0,0 +1,60 @@
<?php
class UnraidUpdateCancel
{
private $PLG_FILENAME;
private $PLG_BOOT;
private $PLG_VAR;
private $USR_LOCAL_PLUGIN_UNRAID_PATH;
public function __construct() {
$this->PLG_FILENAME = "unRAIDServer.plg";
$this->PLG_BOOT = "/boot/config/plugins/{$this->PLG_FILENAME}";
$this->PLG_VAR = "/var/log/plugins/{$this->PLG_FILENAME}";
$this->USR_LOCAL_PLUGIN_UNRAID_PATH = "/usr/local/emhttp/plugins/unRAIDServer";
// Handle the cancellation
$revertResult = $this->revertFiles();
// Return JSON response for front-end client
$statusCode = $revertResult['success'] ? 200 : 500;
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($revertResult);
}
public function revertFiles() {
try {
$command = '/sbin/mount | grep -q "/boot/previous/bz"';
exec($command, $output, $returnCode);
if ($returnCode !== 0) {
return ['success' => true]; // Nothing to revert
}
// Clear the results of previous unraidcheck run
@unlink("/tmp/unraidcheck/result.json");
// Revert changes made by unRAIDServer.plg
shell_exec("mv -f /boot/previous/* /boot");
unlink($this->PLG_BOOT);
unlink($this->PLG_VAR);
symlink("{$this->USR_LOCAL_PLUGIN_UNRAID_PATH}/{$this->PLG_FILENAME}", $this->PLG_VAR);
// Restore README.md by echoing the content into the file
$readmeFile = "{$this->USR_LOCAL_PLUGIN_UNRAID_PATH}/README.md";
$readmeContent = "**Unraid OS**\n\n";
$readmeContent .= "Unraid OS by [Lime Technology, Inc.](https://lime-technology.com).\n";
file_put_contents($readmeFile, $readmeContent);
return ['success' => true]; // Upgrade handled successfully
} catch (\Throwable $th) {
return [
'success' => false,
'message' => $th->getMessage(),
];
}
}
}
// Self instantiate the class and handle the cancellation
new UnraidUpdateCancel();

View File

@@ -13,60 +13,7 @@
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/plugins/dynamix.plugin.manager/include/PluginHelpers.php";
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
// Multi-language support
if (!function_exists('_')) {
function _($text) {return $text;}
}
extract(parse_plugin_cfg('dynamix', true));
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
$script = "$docroot/webGui/scripts/notify";
$server = strtoupper(_var($var,'NAME','server'));
$output = _var($notify,'plugin');
$plg = '/var/log/plugins/unRAIDServer.plg';
$params = [];
$params['branch'] = plugin('category', $plg, 'stable');
$params['current_version'] = plugin('version', $plg) ?: _var($var,'version');
if (_var($var,'regExp')) $params['update_exp'] = date('m-d-Y', _var($var,'regExp')*1);
$urlbase = 'https://releases.unraid.net/os';
$url = $urlbase.'?'.http_build_query($params);
$response = "";
// use error handler to convert warnings from file_get_contents to errors so they can be captured
function warning_as_error($severity, $message, $filename, $lineno) {
throw new ErrorException($message, 0, $severity, $filename, $lineno);
}
set_error_handler("warning_as_error");
try {
$response = file_get_contents($url);
} catch (Exception $e) {
$response = json_encode(array('error' => $e->getMessage()), JSON_PRETTY_PRINT);
}
restore_error_handler();
$json = json_decode($response, true);
if (!$json) {
$response = json_encode(array('error' => 'Invalid response from '.$urlbase), JSON_PRETTY_PRINT);
$json = json_decode($response, true);
}
// add params that were sent to $urlbase
$json['params'] = $params;
// store locally for UPC to access
$file = '/tmp/unraidcheck/result.json';
if (!is_dir(dirname($file))) mkdir(dirname($file));
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT));
// send notification if a newer version is available
if ($json && array_key_exists('isNewer',$json) && $json['isNewer']) {
$newver = (array_key_exists('version',$json) && $json['version']) ? $json['version'] : 'unknown';
exec("$script -e ".escapeshellarg("System - Unraid [$newver]")." -s ".escapeshellarg("Notice [$server] - Version update $newver")." -d ".escapeshellarg("A new version of Unraid is available")." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
}
exit(0);
?>
$unraidOsCheck = new UnraidOsCheck();
$unraidOsCheck->checkForUpdate();

View File

@@ -1,6 +1,6 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -11,429 +11,12 @@
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
// add translations
$_SERVER['REQUEST_URI'] = 'settings';
require_once "$docroot/webGui/include/Translations.php";
require_once "$docroot/webGui/include/Helpers.php";
function host_lookup_ip($host) {
$result = @dns_get_record($host, DNS_A);
$ip = ($result) ? $result[0]['ip']??'' : '';
return($ip);
}
function rebindDisabled() {
global $isLegacyCert;
$rebindtesturl = $isLegacyCert ? "rebindtest.unraid.net" : "rebindtest.myunraid.net";
// DNS Rebind Protection - this checks the server but clients could still have issues
$validResponse = array("192.168.42.42", "fd42");
$response = host_lookup_ip($rebindtesturl);
return in_array(explode('::',$response)[0], $validResponse);
}
function format_port($port) {
return ($port != 80 && $port != 443) ? ':'.$port : '';
}
function anonymize_host($host) {
global $anon;
if ($anon) {
$host = preg_replace('/.*\.myunraid\.net/', '*.hash.myunraid.net', $host);
$host = preg_replace('/.*\.unraid\.net/', 'hash.unraid.net', $host);
}
return $host;
}
function anonymize_ip($ip) {
global $anon;
if ($anon && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
$ip = "[redacted]";
}
return $ip;
}
function generate_internal_host($host, $ip) {
if (strpos($host,'.myunraid.net') !== false) {
$host = str_replace('*', str_replace('.', '-', $ip), $host);
}
return $host;
}
function generate_external_host($host, $ip) {
if (strpos($host,'.myunraid.net') !== false) {
$host = str_replace('*', str_replace('.', '-', $ip), $host);
} elseif (strpos($host,'.unraid.net') !== false) {
$host = "www.".$host;
}
return $host;
}
function verbose_output($httpcode, $result) {
global $cli, $verbose, $anon, $plgversion, $post, $var, $isRegistered, $myservers, $reloadNginx, $nginx, $isLegacyCert;
global $remoteaccess;
global $icon_warn, $icon_ok;
if (!$cli || !$verbose) return;
if ($anon) echo "(Output is anonymized, use '-vv' to see full details)".PHP_EOL;
echo "Unraid OS {$var['version']}".((strpos($plgversion, "base-") === false) ? " with My Servers plugin version {$plgversion}" : '').PHP_EOL;
echo ($isRegistered) ? "{$icon_ok}Signed in to Unraid.net as {$myservers['remote']['username']}".PHP_EOL : "{$icon_warn}Not signed in to Unraid.net".PHP_EOL ;
echo "Use SSL is {$nginx['NGINX_USESSL']}".PHP_EOL;
echo (rebindDisabled()) ? "{$icon_ok}Rebind protection is disabled" : "{$icon_warn}Rebind protection is enabled";
echo " for ".($isLegacyCert ? "unraid.net" : "myunraid.net").PHP_EOL;
if ($post) {
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
// check the data
$certhostname = $nginx['NGINX_CERTNAME'];
if ($certhostname) {
// $certhostname is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$certhostip = host_lookup_ip(generate_internal_host($certhostname, $post['internalip']));
$certhosterr = ($certhostip != $post['internalip']);
}
if ($post['internalhostname'] != $certhostname) {
// $post['internalhostname'] is $nginx['NGINX_LANMDNS'] (no cert, or Server_unraid_bundle.pem) || $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$internalhostip = host_lookup_ip(generate_internal_host($post['internalhostname'], $post['internalip']));
$internalhosterr = ($internalhostip != $post['internalip']);
}
if (!empty($post['externalhostname'])) {
// $post['externalhostname'] is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$externalhostip = host_lookup_ip(generate_external_host($post['externalhostname'], $wanip));
$externalhosterr = ($externalhostip != $wanip);
}
// anonymize data. no caclulations can be done with this data beyond this point.
if ($anon) {
if (!empty($certhostip)) $certhostip = anonymize_ip($certhostip);
if (!empty($certhostname)) $certhostname = anonymize_host($certhostname);
if (!empty($internalhostip)) $internalhostip = anonymize_ip($internalhostip);
if (!empty($externalhostip)) $externalhostip = anonymize_ip($externalhostip);
if (!empty($wanip)) $wanip = anonymize_ip($wanip);
if (!empty($post['internalip'])) $post['internalip'] = anonymize_ip($post['internalip']);
if (!empty($post['internalhostname'])) $post['internalhostname'] = anonymize_host($post['internalhostname']);
if (!empty($post['externalhostname'])) $post['externalhostname'] = anonymize_host($post['externalhostname']);
if (!empty($post['externalport'])) $post['externalport'] = "[redacted]";
}
// always anonymize the keyfile
if (!empty($post['keyfile'])) $post['keyfile'] = "[redacted]";
// output notes
if (!empty($post['internalprotocol']) && !empty($post['internalhostname']) && !empty($post['internalport'])) {
$localurl = $post['internalprotocol']."://".generate_internal_host($post['internalhostname'], $post['internalip']).format_port($post['internalport']);
echo 'Local Access url: '.$localurl.PHP_EOL;
if ($internalhostip) {
// $internalhostip will not be defined for .local domains, ok to skip
echo ($internalhosterr) ? $icon_warn : $icon_ok;
echo generate_internal_host($post['internalhostname'], $post['internalip'])." resolves to {$internalhostip}";
echo ($internalhosterr) ? ", it should resolve to {$post['internalip']}" : "";
echo PHP_EOL;
}
if ($certhostname) {
echo ($certhosterr) ? $icon_warn : $icon_ok;
echo generate_internal_host($certhostname, $post['internalip']).' ';
echo ($certhostip) ? "resolves to {$certhostip}" : "does not resolve to an IP address";
echo ($certhosterr) ? ", it should resolve to {$post['internalip']}" : "";
echo PHP_EOL;
}
if ($remoteaccess == 'yes' && !empty($post['externalprotocol']) && !empty($post['externalhostname']) && !empty($post['externalport'])) {
$remoteurl = $post['externalprotocol']."://".generate_external_host($post['externalhostname'], $wanip).format_port($post['externalport']);
echo 'Remote Access url: '.$remoteurl.PHP_EOL;
echo ($externalhosterr) ? $icon_warn : $icon_ok;
echo generate_external_host($post['externalhostname'], $wanip).' ';
echo ($externalhosterr) ? "does not resolve to an IP address" : "resolves to {$externalhostip}";
echo PHP_EOL;
}
if ($reloadNginx) {
echo "IP address changes were detected, nginx was reloaded".PHP_EOL;
}
}
// output post data
echo PHP_EOL.'Request:'.PHP_EOL;
echo @json_encode($post, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
}
if ($result) {
echo "Response (HTTP $httpcode):".PHP_EOL;
$mutatedResult = is_array($result) ? json_encode($result) : $result;
echo @json_encode(@json_decode($mutatedResult, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
}
}
/**
* @name response_complete
* @param {HTTP Response Status Code} $httpcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {String|Array} $result - strings are assumed to be encoded JSON. Arrays will be encoded to JSON.
* @param {String} $cli_success_msg
*/
function response_complete($httpcode, $result, $cli_success_msg='') {
global $cli, $verbose;
$mutatedResult = is_array($result) ? json_encode($result) : $result;
if ($cli) {
if ($verbose) verbose_output($httpcode, $result);
$json = @json_decode($mutatedResult,true);
if (!empty($json['error'])) {
echo 'Error: '.$json['error'].PHP_EOL;
exit(1);
}
exit($cli_success_msg.PHP_EOL);
}
header('Content-Type: application/json');
http_response_code($httpcode);
exit((string)$mutatedResult);
}
// This is a stub, does nothing but return success
$cli = php_sapi_name()=='cli';
$verbose = $anon = false;
if ($cli && ($argc > 1) && $argv[1] == "-v") {
$verbose = true;
$anon = true;
}
if ($cli && ($argc > 1) && $argv[1] == "-vv") {
$verbose = true;
}
$var = parse_ini_file('/var/local/emhttp/var.ini');
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
$is69 = version_compare($var['version'],"6.9.9","<");
$reloadNginx = false;
$dnserr = false;
$icon_warn = "⚠️ ";
$icon_ok = "✅ ";
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
// ensure some vars are defined here so we don't have to test them later
if (empty($myservers['remote']['apikey'])) {
$myservers['remote']['apikey'] = "";
}
if (empty($myservers['remote']['wanaccess'])) {
$myservers['remote']['wanaccess'] = "no";
}
if (empty($myservers['remote']['wanport'])) {
$myservers['remote']['wanport'] = 443;
}
// remoteaccess, externalport
if ($cli) {
$remoteaccess = (empty($nginx['NGINX_WANFQDN'])) ? 'no' : 'yes';
$externalport = $myservers['remote']['wanport'];
} else {
$remoteaccess = $_POST['remoteaccess']??'no';
$externalport = intval($_POST['externalport']??443);
if ($remoteaccess != 'yes') {
$remoteaccess = 'no';
}
if ($externalport < 1 || $externalport > 65535) {
$externalport = 443;
}
if ($myservers['remote']['wanaccess'] != $remoteaccess) {
// update the wanaccess ini value
$orig = file_exists($myservers_flash_cfg_path) ? parse_ini_file($myservers_flash_cfg_path,true) : [];
if (!$orig) {
$orig = ['remote' => $myservers['remote']];
}
$orig['remote']['wanaccess'] = $remoteaccess;
$text = '';
foreach ($orig as $section => $block) {
$pairs = "";
foreach ($block as $key => $value) if (strlen($value)) $pairs .= "$key=\"$value\"\n";
if ($pairs) $text .= "[$section]\n".$pairs;
}
if ($text) file_put_contents($myservers_flash_cfg_path, $text);
// need nginx reload
$reloadNginx = true;
}
exit("success".PHP_EOL);
}
$isRegistered = !empty($myservers['remote']['username']);
// protocols, hostnames, ports
$internalprotocol = 'http';
$internalport = $nginx['NGINX_PORT'];
$internalhostname = $nginx['NGINX_LANMDNS'];
$externalprotocol = 'https';
// keyserver will expand *.hash.myunraid.net or add www to hash.unraid.net as needed
$externalhostname = $nginx['NGINX_CERTNAME'];
$isLegacyCert = preg_match('/.*\.unraid\.net$/', $nginx['NGINX_CERTNAME']);
$isWildcardCert = preg_match('/.*\.myunraid\.net$/', $nginx['NGINX_CERTNAME']);
$internalip = $nginx['NGINX_LANIP'];
if ($nginx['NGINX_USESSL']=='yes') {
// When NGINX_USESSL is 'yes' in 6.9, it could be using either Server_unraid_bundle.pem or certificate_bundle.pem
// When NGINX_USESSL is 'yes' in 6.10, it is is using Server_unraid_bundle.pem
$internalprotocol = 'https';
$internalport = $nginx['NGINX_PORTSSL'];
if ($is69 && $nginx['NGINX_CERTNAME']) {
// this is from certificate_bundle.pem
$internalhostname = $nginx['NGINX_CERTNAME'];
}
}
if ($nginx['NGINX_USESSL']=='auto') {
// NGINX_USESSL cannot be 'auto' in 6.9, it is either 'yes' or 'no'
// When NGINX_USESSL is 'auto' in 6.10, it is using certificate_bundle.pem
$internalprotocol = 'https';
$internalport = $nginx['NGINX_PORTSSL'];
// keyserver will expand *.hash.myunraid.net as needed
$internalhostname = $nginx['NGINX_CERTNAME'];
}
// My Servers version
$plgversion = file_exists("/var/log/plugins/dynamix.unraid.net.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
: ( file_exists("/var/log/plugins/dynamix.unraid.net.staging.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
: 'base-'.$var['version'] );
// only proceed when when signed in or when legacy unraid.net SSL certificate exists
if (!$isRegistered && !$isLegacyCert) {
response_complete(406, array('error' => _('Nothing to do')));
}
// keyfile
$keyfile = empty($var['regFILE']) ? false : @file_get_contents($var['regFILE']);
if ($keyfile === false) {
response_complete(406, array('error' => _('Registration key required')));
}
$keyfile = @base64_encode($keyfile);
// build post array
$post = [
'keyfile' => $keyfile,
'plgversion' => $plgversion
];
if ($isLegacyCert) {
// sign in not required to maintain local ddns for unraid.net cert
// enable local ddns regardless of use_ssl value
$post['internalip'] = $internalip;
// if host.unraid.net does not resolve to the internalip and DNS Rebind Protection is disabled, disable caching
if (host_lookup_ip(generate_internal_host($nginx['NGINX_CERTNAME'], $post['internalip'])) != $post['internalip'] && rebindDisabled()) $dnserr = true;
}
if ($isRegistered) {
// if signed in, send data needed to maintain My Servers Dashboard
$post['internalhostname'] = $internalhostname;
$post['internalport'] = $internalport;
$post['internalprotocol'] = $internalprotocol;
$post['remoteaccess'] = $remoteaccess;
$post['servercomment'] = $var['COMMENT'];
$post['servername'] = $var['NAME'];
if ($isWildcardCert) {
// keyserver needs the internalip to generate the local access url
$post['internalip'] = $internalip;
}
if ($remoteaccess == 'yes') {
// include wanip in the cache file so we can track if it changes
$post['_wanip'] = trim(@file_get_contents("https://wanip4.unraid.net/"));
$post['externalhostname'] = $externalhostname;
$post['externalport'] = $externalport;
$post['externalprotocol'] = $externalprotocol;
// if wanip.hash.myunraid.net or www.hash.unraid.net does not resolve to the wanip, disable caching
if (host_lookup_ip(generate_external_host($post['externalhostname'], $post['_wanip'])) != $post['_wanip']) $dnserr = true;
}
}
// Include unraid-api report
$unraidreport = [];
if (file_exists('/usr/local/sbin/unraid-api')) {
$jsonString = trim(@exec("/usr/local/sbin/unraid-api report --json 2>/dev/null"));
$unraidreport = @json_decode($jsonString, true);
if ($unraidreport === false) {
$post['unraidreport'] = $jsonString;
} else {
// remove fields we don't need to submit
unset($unraidreport['servers']);
}
} elseif (strpos($plgversion, "base-") === false) {
// The plugin is installed but the api doesn't exist. This is a failed install. Generate basic troubleshooting data.
if (file_exists('/boot/config/plugins/dynamix.my.servers/env')) {
@extract(parse_ini_file('/boot/config/plugins/dynamix.my.servers/env',true));
}
if (empty($env)) {
$env = "production";
}
$unraidreport['os']['version'] = $var['version'];
$unraidreport['api']['version'] = "failed install";
$unraidreport['api']['status'] = "missing";
$unraidreport['api']['environment'] = $env;
$unraidreport['relay']['status'] = "disconnected";
$unraidreport['minigraph']['status'] = "disconnected";
if ($isRegistered) {
$unraidreport['myServers']['status'] = "authenticated";
$unraidreport['myServers']['myServersUsername'] = $myservers['remote']['username'];
} else {
$unraidreport['myServers']['status'] = "signed out";
}
$unraidreport['apiKey'] = (empty($myservers['remote']['apikey'])) ? "invalid" : "exists";
}
if (!empty($unraidreport)) {
// include unraid-api crash logs
$crashLog = '/var/log/unraid-api/crash.json';
$crashAge = 0;
if (file_exists($crashLog)) {
$crashTime = filemtime($crashLog);
$crashAge = time() - $crashTime; // age of crashLog in seconds
$crashDetails = @json_decode(@file_get_contents($crashLog), true);
if (empty($crashDetails['apiVersion']) && $crashAge < 30*60) {
// found a recent crash log without an apiVersion, assume was created by current version of api
$crashDetails['apiVersion'] = $unraidreport['api']['version'];
// overwrite the crash log so it will always have the apiVersion
file_put_contents($crashLog, json_encode($crashDetails, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// reset to original timestamp so crashAge remains accurate
touch($crashLog, $crashTime);
}
$unraidreport['crashAge'] = $crashAge;
$unraidreport['crashLogs'] = $crashDetails;
}
// add flash backup status
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
if (empty($flashbackup_status['activated'])) {
$flashbackup_status['activated'] = "";
}
if (empty($flashbackup_status['error'])) {
$flashbackup_status['error'] = "";
}
$unraidreport['flashbackup']['activated'] = ($flashbackup_status['activated']) ? "yes" : "no";
$unraidreport['flashbackup']['error'] = ($flashbackup_status['error']) ? $flashbackup_status['error'] : "no";
// add unraidreport to payload
$post['unraidreport'] = json_encode($unraidreport);
// if the api is stopped and there are no crashLogs, or any crashLogs are more than maxCrashAge, start the api
$maxCrashAge = 1*60*60; // 1 hour
if ($unraidreport['api']['status'] == 'stopped' && (empty($unraidreport['crashLogs']) || $crashAge > $maxCrashAge)) {
exec("echo \"/usr/local/sbin/unraid-api start\" | at -M now >/dev/null 2>&1");
}
}
// if remoteaccess is enabled in 6.10.0-rc3+ and WANIP has changed since nginx started, reload nginx
if (isset($post['_wanip']) && ($post['_wanip'] != $nginx['NGINX_WANIP']) && version_compare($var['version'],"6.10.0-rc2",">")) $reloadNginx = true;
// if remoteaccess is currently disabled (perhaps because a wanip was not available when nginx was started)
// BUT the system is configured to have it enabled AND a wanip is now available
// then reload nginx
if ($remoteaccess == 'no' && $nginx['NGINX_WANACCESS'] == 'yes' && !empty(trim(@file_get_contents("https://wanip4.unraid.net/")))) $reloadNginx = true;
if ($reloadNginx) {
exec("/etc/rc.d/rc.nginx reload &>/dev/null");
}
// maxage is 36 hours
$maxage = 36*60*60;
if ($dnserr || $verbose) $maxage = 0;
$datafile = "/tmp/UpdateDNS.txt";
$datafiletmp = "/tmp/UpdateDNS.txt.new";
$dataprev = @file_get_contents($datafile) ?: '';
$datanew = implode("\n",$post)."\n";
if ($datanew == $dataprev && (time()-filemtime($datafile) < $maxage)) {
response_complete(204, null, _('No change to report'));
}
file_put_contents($datafiletmp,$datanew);
rename($datafiletmp, $datafile);
// do not submit the wanip, it will be captured from the submission if needed for remote access
unset($post['_wanip']);
// report necessary server details to limetech for DNS updates
$ch = curl_init('https://keys.lime-technology.com/account/server/register');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ( ($result === false) || ($httpcode != "200") ) {
// delete cache file to retry submission on next run
@unlink($datafile);
response_complete($httpcode ?? "500", array('error' => $error));
}
response_complete($httpcode, $result, _('success'));
header('Content-Type: application/json');
http_response_code(204);
exit(0);
?>

View File

@@ -2,4 +2,5 @@ VITE_ACCOUNT=https://localhost:8008
VITE_CONNECT=https://connect.myunraid.net
VITE_UNRAID_NET=https://unraid.ddev.site
VITE_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams
VITE_ALLOW_CONSOLE_LOGS=false
VITE_ALLOW_CONSOLE_LOGS=false
VITE_WEBGUI=http://localhost

View File

@@ -1,26 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { readFileSync } = require('fs');
const { parse } = require('dotenv');
const envConfig = parse(readFileSync('.env'));
for (const k in envConfig) {
process.env[k] = envConfig[k];
}
module.exports = {
extends: ['@nuxtjs/eslint-config-typescript'],
ignorePatterns: ['composables/gql/'],
rules: {
'comma-dangle': ['warn', 'only-multiline'],
semi: ['error', 'always'],
quotes: ['warn', 'single'],
'no-console': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
'no-debugger': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
'@typescript-eslint/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'max-len': 'off',
'vue/multi-word-component-names': 'off',
'vue/v-on-event-hyphenation': 'off',
'vue/no-v-html': 'off',
'no-fallthrough': 'off',
}
};

View File

@@ -1 +1 @@
18.17.1
18.19.1

View File

@@ -10,7 +10,7 @@ import type {
Server,
ServerState,
// ServerUpdateOsResponse,
} from '~/types/server';
} from "~/types/server";
// dayjs plugins
// extend(customParseFormat);
@@ -24,28 +24,14 @@ import type {
// return result;
// }
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
const staticGuid = '1111-1111-5GDB-123412341234';
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
// const regWizTime = `1616711990500_${randomGuid}`;
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
const twentyDaysAgo = Date.now() - 20 * 24 * 60 * 60 * 1000; // 20 days ago
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000; // 2 days ago
// const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day ago
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
// const oneDayFromNow = Date.now() + 24 * 60 * 60 * 1000; // 1 day from now
let expireTime = 0;
let regExp: number | undefined;
// ENOKEYFILE
// TRIAL
// BASIC
// PLUS
// PRO
// STARTER
// UNLEASHED
// LIFETIME
// EEXPIRED
// EGUID
// EGUID1
@@ -57,54 +43,74 @@ let regExp: number | undefined;
// EBLACKLISTED1
// EBLACKLISTED2
// ENOCONN
const state: ServerState = 'STARTER';
let regDev = 0;
let regTy = '';
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
const state: ServerState = "BASIC" as ServerState;
const currentFlashGuid = "1111-1111-CFXF-TEST1234ZACK"; // this is the flash drive that's been booted from
const regGuid = "1111-1111-CFXF-TEST1234ZACK"; // this guid is registered in key server
const keyfileBase64 = "asdf"; // @todo raycast download key to base64
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
// const regWizTime = `1616711990500_${randomGuid}`;
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
// const twentyDaysAgo = Date.now() - 20 * 24 * 60 * 60 * 1000; // 20 days ago
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000; // 90 days ago
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000; // 2 days ago
// const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day ago
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
// const oneDayFromNow = Date.now() + 24 * 60 * 60 * 1000; // 1 day from now
let expireTime = 0;
let regExp: number | undefined;
let regDevs = 0;
let regTy = "";
switch (state) {
// @ts-ignore
case 'EEXPIRED':
case "EEXPIRED":
expireTime = uptime; // 1 hour ago
// @ts-ignore
case 'ENOCONN':
// @ts-ignore
case 'TRIAL':
break;
case "ENOCONN":
break;
case "TRIAL":
expireTime = oneHourFromNow; // in 1 hour
regTy = 'Trial';
// @ts-ignore
case 'BASIC':
regDev = 6;
// @ts-ignore
case 'PLUS':
regDev = 12;
// @ts-ignore
case 'PRO':
// @ts-ignore
case 'STARTER':
regDev = 4;
// regExp = oneHourFromNow;
// regExp = oneDayFromNow;
regExp = twentyDaysAgo;
// regExp = uptime;
// regExp = 1696363920000; // nori.local's expiration
// @ts-ignore
case 'UNLEASHED':
// regExp = oneHourFromNow;
// regExp = oneDayFromNow;
// regExp = oneDayAgo;
// regExp = uptime;
// regExp = 1696363920000; // nori.local's expiration
// @ts-ignore
case 'LIFETIME':
if (regDev === 0) { regDev = 99999; }
if (regTy === '') { regTy = state.charAt(0).toUpperCase() + state.substring(1).toLowerCase(); } // title case
regTy = "Trial";
break;
case "BASIC":
regDevs = 6;
regTy = "Basic";
break;
case "PLUS":
regDevs = 12;
regTy = "Plus";
break;
case "PRO":
regDevs = -1;
regTy = "Pro";
break;
case "STARTER":
regDevs = 6;
regExp = ninetyDaysAgo;
regTy = "Starter";
break;
case "UNLEASHED":
regDevs = -1;
regExp = ninetyDaysAgo;
regTy = "Unleashed";
break;
case "LIFETIME":
regDevs = -1;
regTy = "Lifetime";
break;
}
const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
// const connectPluginInstalled = '';
// const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
const connectPluginInstalled = "";
const osVersion = '6.13.0-beta0.22';
const osVersionBranch = 'preview';
const osVersion = "6.12.8";
const osVersionBranch = "stable";
// const parsedRegExp = regExp ? dayjs(regExp).format('YYYY-MM-DD') : undefined;
// const mimicWebguiUnraidCheck = async (): Promise<ServerUpdateOsResponse | undefined> => {
@@ -129,56 +135,60 @@ const osVersionBranch = 'preview';
// };
export const serverState: Server = {
apiKey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
avatar: 'https://source.unsplash.com/300x300/?portrait',
apiKey: "unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810",
avatar: "https://source.unsplash.com/300x300/?portrait",
config: {
// error: 'INVALID',
valid: true,
error: null,
valid: false,
},
connectPluginInstalled,
description: 'DevServer9000',
description: "DevServer9000",
deviceCount: 3,
expireTime,
flashBackupActivated: !!connectPluginInstalled,
flashProduct: 'SanDisk_3.2Gen1',
flashVendor: 'USB',
guid: staticGuid,
flashProduct: "SanDisk_3.2Gen1",
flashVendor: "USB",
guid: currentFlashGuid,
// "guid": "0781-5583-8355-81071A2B0211",
inIframe: false,
// keyfile: 'DUMMY_KEYFILE',
keyfile: 'TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg',
lanIp: '192.168.254.36',
license: '',
locale: 'en_US', // en_US, ja
name: 'dev-static',
keyfile: keyfileBase64,
lanIp: "192.168.254.36",
license: "",
locale: "en_US", // en_US, ja
name: "dev-static",
osVersion,
osVersionBranch,
// registered: connectPluginInstalled ? true : false,
registered: false,
regGen: 0,
regTm: twoDaysAgo,
regTo: 'Zack Spear',
regTo: "Zack Spear",
regTy,
regDevs,
regExp,
// "regGuid": "0781-5583-8355-81071A2B0211",
site: 'http://localhost:4321',
regGuid,
site: "http://localhost:4321",
state,
theme: {
banner: false,
bannerGradient: false,
bgColor: '',
bgColor: "",
descriptionShow: true,
metaColor: '',
name: 'white',
textColor: ''
},
updateOsResponse: {
version: '6.13.0-beta0.27',
name: 'Unraid 6.13.0-beta0.27',
date: '2023-12-13',
isNewer: true,
metaColor: "",
name: "white",
textColor: "",
},
// updateOsResponse: {
// version: '6.12.6',
// name: 'Unraid 6.12.6',
// date: '2023-12-13',
// isNewer: true,
// isEligible: false,
// changelog: 'https://docs.unraid.net/unraid-os/release-notes/6.12.6/',
// sha256: '2f5debaf80549029cf6dfab0db59180e7e3391c059e6521aace7971419c9c4bf',
// },
uptime,
username: 'zspearmint',
wanFQDN: ''
username: "zspearmint",
wanFQDN: "",
};

View File

@@ -7,6 +7,8 @@ Tag="globe"
/**
* @todo create web component env switcher liker upcEnv(). If we utilize manifest.json then we'll be switching its path.
*/
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
// print_r($mystatus);
@@ -142,7 +144,7 @@ if ($display['theme'] === 'black' || $display['theme'] === 'azure') {
<unraid-key-actions></connect-key-actions>
</div>
<div class="ComponentWrapper">
<unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
<unraid-wan-ip-check php-wan-ip="<?=http_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
</div>
<script>

View File

@@ -1,8 +1,7 @@
<script lang="ts" setup>
const nuxtApp = useNuxtApp();
const { registerEntry } = useCustomElements();
onBeforeMount(() => {
// @ts-ignore
nuxtApp.$customElements.registerEntry('UnraidComponents');
registerEntry('UnraidComponents');
});
</script>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
// eslint-disable vue/no-v-html
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
@@ -25,7 +26,7 @@ const { authAction, stateData } = storeToRefs(serverStore);
size="12px"
:text="t(authAction.text)"
:title="authAction?.title ? t(authAction?.title) : undefined"
@click="authAction.click()"
@click="authAction.click?.()"
/>
</span>
</div>

View File

@@ -1,35 +1,22 @@
<script setup lang="ts">
import {
computed,
type Component,
} from 'vue';
import { computed } from 'vue';
import type { ButtonProps } from '~/types/ui/button';
export interface ButtonProps {
btnStyle?: 'black' | 'fill' | 'gray' | 'outline' | 'outline-black' | 'outline-white' | 'underline' | 'white';
btnType?: 'button' | 'submit' | 'reset';
click?: () => void;
disabled?: boolean;
download?: boolean;
external?: boolean;
href?: string;
icon?: Component;
iconRight?: Component;
iconRightHoverDisplay?: boolean;
// iconRightHoverAnimate?: boolean;
size?: '12px' | '14px' | '16px' | '18px' | '20px' | '24px';
text?: string;
}
const props = withDefaults(defineProps<ButtonProps>(), {
btnStyle: 'fill',
btnType: 'button',
class: undefined,
click: undefined,
href: undefined,
icon: undefined,
iconRight: undefined,
iconRightHoverDisplay: false,
// iconRightHoverAnimate: true,
noPadding: false,
size: '16px',
text: '',
title: '',
});
defineEmits(['click']);
@@ -63,6 +50,9 @@ const classes = computed(() => {
case 'underline':
buttonColors = 'opacity-75 underline border-transparent transition hover:text-alpha hover:bg-beta hover:border-beta focus:text-alpha focus:bg-beta focus:border-beta hover:opacity-100 focus:opacity-100';
break;
case 'underline-hover-red':
buttonColors = 'opacity-75 underline border-transparent transition hover:text-white hover:bg-unraid-red hover:border-unraid-red focus:text-white focus:bg-unraid-red focus:border-unraid-red hover:opacity-100 focus:opacity-100';
break;
case 'white':
buttonColors = 'text-black bg-white transition hover:bg-grey focus:bg-grey';
break;
@@ -70,33 +60,35 @@ const classes = computed(() => {
switch (props.size) {
case '12px':
buttonSize = 'text-12px p-8px gap-4px';
buttonSize = `text-12px ${props.noPadding ? 'p-0' : 'p-8px'} gap-4px`;
iconSize = 'w-12px';
break;
case '14px':
buttonSize = 'text-14px p-8px gap-8px';
buttonSize = `text-14px ${props.noPadding ? 'p-0' : 'p-8px'} gap-8px`;
iconSize = 'w-14px';
break;
case '16px':
buttonSize = 'text-16px p-12px gap-8px';
buttonSize = `text-16px ${props.noPadding ? 'p-0' : 'p-12px'} gap-8px`;
iconSize = 'w-16px';
break;
case '18px':
buttonSize = 'text-18px p-12px gap-8px';
buttonSize = `text-18px ${props.noPadding ? 'p-0' : 'p-12px'} gap-8px`;
iconSize = 'w-18px';
break;
case '20px':
buttonSize = 'text-20px p-16px gap-8px';
buttonSize = `text-20px ${props.noPadding ? 'p-0' : 'p-16px'} gap-8px`;
iconSize = 'w-20px';
break;
case '24px':
buttonSize = 'text-24px p-16px gap-8px';
buttonSize = `text-24px ${props.noPadding ? 'p-0' : 'p-16px'} gap-8px`;
iconSize = 'w-24px';
break;
}
return {
button: `${buttonSize} ${buttonColors} ${buttonDefaults}`,
button: props.btnStyle === 'none'
? `${buttonSize} ${props.class}`
: `${buttonSize} ${buttonColors} ${buttonDefaults} ${props.class}`,
icon: `${iconSize} fill-current flex-shrink-0`,
};
});
@@ -111,6 +103,7 @@ const classes = computed(() => {
:target="external ? '_blank' : ''"
:type="!href ? btnType : ''"
:class="classes.button"
:title="title"
@click="click ?? $emit('click')"
>
<div

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
const apiSettingsStore = useUnraidApiSettingsStore();
const originsText = ref<string>('');
const errors = ref<string[]>([]);
onMounted(async () => {
const allowedOriginsSettings = await apiSettingsStore.getAllowedOrigins();
originsText.value = allowedOriginsSettings.join(', ');
});
const origins = computed<string[]>(() => {
console.log('originsText.value: ' + originsText.value);
const newOrigins: string[] = [];
if (originsText.value) {
originsText.value.split(',').forEach((origin) => {
try {
const newUrl = new URL(origin.trim());
newOrigins.push(newUrl.toString());
} catch (e) {
errors.value.push(`Invalid origin: ${origin}`);
}
});
}
return newOrigins;
});
const setAllowedOrigins = () => {
apiSettingsStore.setAllowedOrigins(origins.value);
};
</script>
<template>
<div class="flex flex-col">
<h2>Setup Allowed Origins</h2>
<input v-model="originsText" type="text" placeholder="Input Comma Separated List of URLs">
<button type="button" @click="setAllowedOrigins()">
Set Allowed Origins
</button>
<div v-for="(error, index) of errors" :key="index">
<p>{{ error }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
// import { useI18n } from 'vue-i18n';
// const { t } = useI18n();
</script>
<template>
<AuthCe />
<!-- @todo: flashback up -->
<WanIpCheckCe />
<ConnectSettingsRemoteAccess />
<ConnectSettingsAllowedOrigins />
<DownloadApiLogsCe />
</template>

View File

@@ -0,0 +1,60 @@
<script lang="ts" setup>
import { WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from '~/composables/gql/graphql';
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
const apiSettingsStore = useUnraidApiSettingsStore();
const accessType = ref<WAN_ACCESS_TYPE>(WAN_ACCESS_TYPE.Disabled);
const forwardType = ref<WAN_FORWARD_TYPE | null>(null);
const port = ref<number | null>(null);
onMounted(async () => {
const remoteAccessSettings = await apiSettingsStore.getRemoteAccess();
accessType.value =
remoteAccessSettings?.accessType ?? WAN_ACCESS_TYPE.Disabled;
forwardType.value = remoteAccessSettings?.forwardType ?? null;
port.value = remoteAccessSettings?.port ?? null;
});
const setRemoteAccess = () => {
apiSettingsStore.setupRemoteAccess({
accessType: accessType.value,
...(forwardType.value ? { forwardType: forwardType.value } : {}),
...(port.value ? { port: port.value } : {}),
});
};
watch(accessType, (newVal) => {
if (newVal !== WAN_ACCESS_TYPE.Disabled) {
forwardType.value = WAN_FORWARD_TYPE.Static;
}
});
</script>
<template>
<div class="flex flex-col">
<h2>Setup Remote Access</h2>
<label for="forwardType">Forward Type</label>
<select id="forwardType" v-model="accessType">
<option v-for="(val, index) in Object.values(WAN_ACCESS_TYPE)" :key="index" :value="val">
{{ val }}
</option>
</select>
<template v-if="accessType !== WAN_ACCESS_TYPE.Disabled">
<label for="forwardType">Forward Type</label>
<select id="forwardType" v-model="forwardType">
<option v-for="(val, index) in Object.values(WAN_FORWARD_TYPE)" :key="index" :value="val">
{{ val }}
</option>
</select>
</template>
<template v-if="forwardType === WAN_FORWARD_TYPE.Static && accessType !== WAN_ACCESS_TYPE.Disabled">
<label for="port">Port</label>
<input id="port" v-model="port" type="number">
</template>
<button @click="setRemoteAccess">
Save
</button>
</div>
</template>

View File

@@ -39,7 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
const serverStore = useServerStore();
const { rebootType } = storeToRefs(serverStore);
const { rebootType, osVersionBranch } = storeToRefs(serverStore);
const subtitle = computed(() => {
if (rebootType.value === 'update') {
@@ -48,6 +48,8 @@ const subtitle = computed(() => {
return '';
});
const showExternalDowngrade = computed(() => osVersionBranch.value !== 'stable');
onBeforeMount(() => {
serverStore.setRebootVersion(props.rebootVersion);
});
@@ -59,6 +61,7 @@ onBeforeMount(() => {
:title="t('Downgrade Unraid OS')"
:subtitle="subtitle"
:downgrade-not-available="restoreVersion === '' && rebootType === ''"
:show-external-downgrade="showExternalDowngrade"
:t="t"
/>
<UpdateOsDowngrade

View File

@@ -14,6 +14,8 @@ import { WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
import type { UiBadgeProps, UiBadgePropsColor } from '~/types/ui/badge';
const { t } = useI18n();
@@ -21,20 +23,48 @@ const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const updateOsActionsStore = useUpdateOsActionsStore();
const { osVersion, rebootType } = storeToRefs(serverStore);
const { available } = storeToRefs(updateOsStore);
const { ineligibleText, rebootTypeText } = storeToRefs(updateOsActionsStore);
const { osVersion, rebootType, stateDataError } = storeToRefs(serverStore);
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
const { rebootTypeText } = storeToRefs(updateOsActionsStore);
const showUpdateAvailable = computed(() => !ineligibleText.value && available.value && rebootType.value === '');
export interface UpdateOsStatus extends UserProfileLink {
badge: UiBadgeProps;
}
const updateOsStatus = computed(() => {
if (stateDataError.value) { // only allowed to update when server is does not have a state error
return null;
}
const rebootRequiredLink = computed(() => {
if (rebootType.value === 'downgrade') {
return WEBGUI_TOOLS_DOWNGRADE.toString();
if (rebootTypeText.value) {
return {
badge: {
color: 'yellow' as UiBadgePropsColor,
icon: ExclamationTriangleIcon,
},
href: rebootType.value === 'downgrade'
? WEBGUI_TOOLS_DOWNGRADE.toString()
: WEBGUI_TOOLS_UPDATE.toString(),
text: t(rebootTypeText.value),
};
}
if (rebootType.value === 'thirdPartyDriversDownloading' || rebootType.value === 'update') {
return WEBGUI_TOOLS_UPDATE.toString();
if (availableWithRenewal.value || available.value) {
return {
badge: {
color: 'orange' as UiBadgePropsColor,
icon: BellAlertIcon,
},
click: () => { updateOsStore.setModalOpen(true); },
text: availableWithRenewal.value
? t('Update Released')
: t('Update Available'),
title: availableWithRenewal.value
? t('Unraid OS {0} Released', [availableWithRenewal.value])
: t('Unraid OS {0} Update Available', [available.value]),
};
}
return '';
return null;
});
</script>
@@ -56,33 +86,26 @@ const rebootRequiredLink = computed(() => {
</UiBadge>
</button>
<a
v-if="showUpdateAvailable"
:href="WEBGUI_TOOLS_UPDATE.toString()"
<component
:is="updateOsStatus.href ? 'a' : 'button'"
v-if="updateOsStatus"
:href="updateOsStatus.href ?? undefined"
:title="updateOsStatus.title ?? undefined"
class="group"
:title="t('Unraid OS {0} Update Available', [available])"
@click="updateOsStatus.click?.()"
>
<UiBadge
color="orange"
:icon="BellAlertIcon"
v-if="updateOsStatus.badge"
:color="updateOsStatus.badge.color"
:icon="updateOsStatus.badge.icon"
size="12px"
>
{{ t('Update Available') }}
{{ updateOsStatus.text }}
</UiBadge>
</a>
<a
v-else-if="rebootRequiredLink"
:href="rebootRequiredLink"
class="group"
>
<UiBadge
:color="'yellow'"
:icon="ExclamationTriangleIcon"
size="12px"
>
{{ t(rebootTypeText) }}
</UiBadge>
</a>
<template v-else>
{{ updateOsStatus.text }}
</template>
</component>
</div>
</template>

View File

@@ -4,7 +4,7 @@ import { createI18n, I18nInjectionKey } from 'vue-i18n';
import { disableProductionConsoleLogs } from '~/helpers/functions';
import en_US from '~/locales/en_US.json'; // eslint-disable-line camelcase
import en_US from '~/locales/en_US.json';
disableProductionConsoleLogs();
// import ja from '~/locales/ja.json';
@@ -17,6 +17,7 @@ let nonDefaultLocale = false;
* Unfortunately, this was the only way I could get the data from PHP to vue-i18n :(
* I tried using i18n.setLocaleMessage() but it didn't work no matter what I tried.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const windowLocaleData = (window as any).LOCALE_DATA || null;
if (windowLocaleData) {
try {
@@ -33,7 +34,7 @@ const i18n = createI18n<false>({
locale: nonDefaultLocale ? parsedLocale : defaultLocale,
fallbackLocale: defaultLocale,
messages: {
en_US, // eslint-disable-line camelcase
en_US,
// ja,
...(nonDefaultLocale ? parsedMessages : {}),
}

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import type { ServerStateDataAction } from '~/types/server';
@@ -10,7 +11,7 @@ const props = withDefaults(defineProps<{
filterBy?: string[] | undefined;
filterOut?: string[] | undefined;
maxWidth?: boolean;
t: any;
t: ComposerTranslation;
}>(), {
actions: undefined,
filterBy: undefined,
@@ -49,7 +50,7 @@ const filteredKeyActions = computed((): ServerStateDataAction[] | undefined => {
:icon-right-hover-display="true"
:text="t(action.text)"
:title="action.title ? t(action.title) : undefined"
@click="action.click()"
@click="action.click?.()"
/>
</li>
</ul>

View File

@@ -1,32 +1,38 @@
<script setup lang="ts">
import { TransitionChild, TransitionRoot } from '@headlessui/vue';
import { XMarkIcon } from '@heroicons/vue/24/outline';
import useFocusTrap from '~/composables/useFocusTrap';
import type { ComposerTranslation } from 'vue-i18n';
export interface Props {
centerContent?: boolean;
description?: string;
error?: boolean;
maxWidth?: string;
open?: boolean;
showCloseX?: boolean;
success?: boolean;
t: any;
t: ComposerTranslation;
tallContent?: boolean;
title?: string;
}
const props = withDefaults(defineProps<Props>(), {
centerContent: true,
description: '',
error: false,
maxWidth: 'sm:max-w-lg',
open: false,
showCloseX: false,
success: false,
tallContent: false,
title: '',
});
watchEffect(() => {
// toggle body scrollability
return props.open
? document.body.style.setProperty('overflow', 'hidden')
: document.body.style.removeProperty('overflow');
if (props.open) {
document.body.style.setProperty('overflow', 'hidden')
} else {
document.body.style.removeProperty('overflow');
}
});
const emit = defineEmits(['close']);
@@ -34,8 +40,6 @@ const closeModal = () => {
emit('close');
};
const { trapRef } = useFocusTrap();
const ariaLablledById = computed((): string|undefined => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
/**
@@ -46,7 +50,6 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
<template>
<TransitionRoot appear :show="open" as="template">
<div
ref="trapRef"
class="fixed inset-0 z-10 overflow-y-auto"
role="dialog"
aria-dialog="true"
@@ -54,23 +57,29 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
tabindex="-1"
@keyup.esc="closeModal"
>
<TransitionChild
appear
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
<div
class="fixed inset-0 flex min-h-screen w-screen justify-center p-8px sm:p-16px overflow-y-auto"
:class="{
'items-start sm:items-center': tallContent,
'items-center': !tallContent,
}"
>
<div
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
:title="t('Click to close modal')"
@click="closeModal"
/>
</TransitionChild>
<div class="text-center flex min-h-full items-center justify-center p-4 md:p-0">
<TransitionChild
appear
as="template"
enter="duration-300 ease-out"
enter-from="opacity-0"
enter-to="opacity-100"
leave="duration-200 ease-in"
leave-from="opacity-100"
leave-to="opacity-0"
>
<div
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
:title="t('Click to close modal')"
@click="closeModal"
/>
</TransitionChild>
<TransitionChild
appear
as="template"
@@ -88,30 +97,49 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
success ? 'shadow-green-600/30 border-green-600/10' : '',
!error && !success ? 'shadow-orange/10 border-white/10' : '',
]"
class="text-16px text-beta bg-alpha text-left relative flex flex-col justify-around p-16px my-24px sm:p-24px border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
class="text-16px text-beta bg-alpha text-left relative z-10 flex flex-col justify-around border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
>
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
<button type="button" class="rounded-md text-beta bg-alpha p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="closeModal">
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block">
<button
class="rounded-md text-beta bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
type="button"
@click="closeModal"
>
<span class="sr-only">{{ t('Close') }}</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
<header class="text-center">
<header
class="relative z-0 grid items-start gap-2 p-16px md:p-24px rounded-t"
:class="{
'sm:pr-40px': showCloseX,
'justify-between': $slots['header'],
'justify-center': !$slots['header'],
}"
>
<div class="absolute -z-10 inset-0 opacity-10 bg-beta" />
<template v-if="!$slots['header']">
<h1 v-if="title" :id="ariaLablledById" class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
<h1 v-if="title" :id="ariaLablledById" class="text-center text-20px sm:text-24px font-semibold flex flex-wrap justify-center gap-x-4px">
{{ title }}
<slot name="headerTitle" />
</h1>
<h2 v-if="description" class="text-20px opacity-75">
{{ description }}
</h2>
</template>
<slot name="header" />
</header>
<slot name="main" />
<footer v-if="$slots['footer']" class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px p-4 sm:p-6">
<div
v-if="$slots['main'] || description"
class="relative max-h-[65vh] tall:max-h-[75vh] flex flex-col gap-y-16px sm:gap-y-24px p-16px md:p-24px overflow-y-auto shadow-inner"
:class="centerContent && 'text-center'"
>
<h2 v-if="description" class="text-18px sm:text-20px opacity-75" v-html="description" />
<div v-if="$slots['main']">
<slot name="main" />
</div>
</div>
<footer v-if="$slots['footer']" class="text-14px relative p-16px md:p-24px">
<div class="absolute z-0 inset-0 opacity-10 bg-beta" />
<div class="relative z-10">
<slot name="footer" />

View File

@@ -7,11 +7,15 @@ import '~/assets/main.css';
import { useCallbackActionsStore } from '~/store/callbackActions';
import { useTrialStore } from '~/store/trial';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
const { t } = useI18n();
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
const { trialModalVisible } = storeToRefs(useTrialStore());
const { modalOpen: updateOsModalVisible } = storeToRefs(useUpdateOsStore());
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdateOsChangelogStore());
// import { usePromoStore } from '~/store/promo';
// const { promoVisible } = storeToRefs(usePromoStore());
// <UpcPromo :t="t" :open="promoVisible" />
@@ -21,6 +25,8 @@ const { trialModalVisible } = storeToRefs(useTrialStore());
<div class="relative z-[99999]">
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
<UpcTrial :t="t" :open="trialModalVisible" />
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
</div>
</template>

View File

@@ -25,18 +25,25 @@ import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
import type { RegistrationItemProps } from '~/types/registration';
import KeyActions from '~/components/KeyActions.vue';
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
import RegistrationKeyLinkedStatus from '~/components/Registration/KeyLinkedStatus.vue';
import RegistrationUpdateExpirationAction from '~/components/Registration/UpdateExpirationAction.vue';
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
const { t } = useI18n();
const replaceRenewCheckStore = useReplaceRenewStore();
const serverStore = useServerStore();
const {
computedArray,
arrayWarning,
authAction,
dateTimeFormat,
deviceCount,
@@ -44,46 +51,80 @@ const {
flashVendor,
flashProduct,
keyActions,
keyfile,
computedRegDevs,
regGuid,
regTm,
regTo,
regTy,
regExp,
regUpdatesExpired,
serverErrors,
state,
stateData,
stateDataError,
tooManyDevices,
} = storeToRefs(serverStore);
const { outputDateTimeFormatted: formattedRegTm } = useDateTimeHelper(dateTimeFormat.value, t, false, regTm.value);
const formattedRegTm = ref<string>();
/**
* regTm may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
*/
const setFormattedRegTm = () => {
if (!regTm.value) { return; }
const devicesAvailable = computed((): number => {
switch (regTy.value) {
case 'Starter':
return 4;
case 'Basic':
return 6;
case 'Plus':
return 12;
case 'Unleashed':
case 'Lifetime':
case 'Pro':
case 'Trial':
return 9999;
default:
return 0;
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, t, true, regTm.value);
formattedRegTm.value = outputDateTimeFormatted.value;
};
watch(regTm, (_newV) => {
setFormattedRegTm();
});
onBeforeMount(() => {
setFormattedRegTm();
/** automatically check for replacement and renewal eligibility…will prompt user if eligible for a renewal / key re-roll for legacy keys */
if (guid.value && keyfile.value) {
replaceRenewCheckStore.check();
}
});
const headingIcon = computed(() => serverErrors.value.length ? ShieldExclamationIcon : ShieldCheckIcon);
const heading = computed(() => {
if (serverErrors.value.length) { // It's rare to have multiple errors but for the time being only show the first error
return serverErrors.value[0]?.heading;
}
return stateData.value.heading;
});
const subheading = computed(() => {
if (serverErrors.value.length) { // It's rare to have multiple errors but for the time being only show the first error
return serverErrors.value[0]?.message;
}
return stateData.value.message;
});
const showTrialExpiration = computed((): boolean => state.value === 'TRIAL' || state.value === 'EEXPIRED');
const showUpdateEligibility = computed((): boolean => !!(regExp.value));
const keyInstalled = computed((): boolean => !!(!stateDataError.value && state.value !== 'ENOKEYFILE'));
const showLinkedAndTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
const showFilteredKeyActions = computed((): boolean => !!(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0));
const items = computed((): RegistrationItemProps[] => {
return [
...(computedArray.value
? [{
label: t('Array status'),
text: computedArray.value,
warning: arrayWarning.value,
}]
: []),
...(regTy.value
? [{
label: t('License key type'),
text: regTy.value,
}]
: []),
...(state.value === 'TRIAL' || state.value === 'EEXPIRED'
...(showTrialExpiration.value
? [{
error: state.value === 'EEXPIRED',
label: t('Trial expiration'),
@@ -102,13 +143,13 @@ const items = computed((): RegistrationItemProps[] => {
text: regTo.value,
}]
: []),
...(regTo.value && regTm.value
...(regTo.value && regTm.value && formattedRegTm.value
? [{
label: t('Registered on'),
text: formattedRegTm.value,
}]
: []),
...(regExp.value && (state.value === 'STARTER' || state.value === 'UNLEASHED')
...(showUpdateEligibility.value
? [{
label: t('OS Update Eligibility'),
warning: regUpdatesExpired.value,
@@ -141,26 +182,32 @@ const items = computed((): RegistrationItemProps[] => {
text: flashProduct.value,
}]
: []),
...(!stateDataError.value
...(keyInstalled.value
? [{
error: deviceCount.value > devicesAvailable.value,
error: tooManyDevices.value,
label: t('Attached Storage Devices'),
text: deviceCount.value > devicesAvailable.value
? t('{0} out of {1} allowed devices upgrade your key to support more devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value])
: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value]),
text: tooManyDevices.value
? t('{0} out of {1} allowed devices upgrade your key to support more devices', [deviceCount.value, computedRegDevs.value])
: t('{0} out of {1} devices', [deviceCount.value, computedRegDevs.value === -1 ? t('unlimited') : computedRegDevs.value]),
}]
: []),
...(!stateDataError.value && guid.value
...(showLinkedAndTransferStatus.value
? [{
label: t('Transfer License to New Flash'),
component: RegistrationReplaceCheck,
componentProps: { t },
}]
: []),
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
...(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0
...(regTo.value && showLinkedAndTransferStatus.value
? [{
label: t('Linked to Unraid.net account'),
component: RegistrationKeyLinkedStatus,
componentProps: { t },
}]
: []),
...(showFilteredKeyActions.value
? [{
label: t('License key actions'),
component: KeyActions,
componentProps: {
filterOut: ['renew'],
@@ -179,17 +226,17 @@ const items = computed((): RegistrationItemProps[] => {
<header class="flex flex-col gap-y-16px">
<h3
class="text-20px md:text-24px font-semibold leading-normal flex flex-row items-center gap-8px"
:class="stateDataError ? 'text-unraid-red' : 'text-green-500'"
:class="serverErrors.length ? 'text-unraid-red' : 'text-green-500'"
>
<component :is="stateDataError ? ShieldExclamationIcon : ShieldCheckIcon" class="w-24px h-24px" />
<component :is="headingIcon" class="w-24px h-24px" />
<span>
{{ stateData.heading }}
{{ heading }}
</span>
</h3>
<div
v-if="stateData.message"
v-if="subheading"
class="prose text-16px leading-relaxed whitespace-normal opacity-75"
v-html="stateData.message"
v-html="subheading"
/>
<span v-if="authAction" class="grow-0">
<BrandButton
@@ -197,7 +244,7 @@ const items = computed((): RegistrationItemProps[] => {
:icon="authAction.icon"
:text="t(authAction.text)"
:title="authAction.title ? t(authAction.title) : undefined"
@click="authAction.click()"
@click="authAction.click?.()"
/>
</span>
</header>

View File

@@ -25,14 +25,23 @@ const evenBgColor = computed(() => {
error && 'text-white bg-unraid-red',
warning && 'text-black bg-yellow-100',
]"
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-3 sm:gap-16px items-start rounded"
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-5 sm:gap-16px items-baseline rounded"
>
<dt class="font-semibold flex flex-row justify-start items-center gap-x-8px">
<dt v-if="label" class="font-semibold leading-normal sm:col-span-2 flex flex-row sm:justify-end sm:text-right items-center gap-x-8px">
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
<span>{{ label }}</span>
<span v-html="label" />
</dt>
<dd class="leading-normal sm:col-span-2">
<span v-if="text" class="select-all" :class="!error ? 'opacity-75' : ''">
<dd
class="leading-normal sm:col-span-3"
:class="!label && 'sm:col-start-2'"
>
<span
v-if="text"
class="select-all"
:class="{
'opacity-75': !error,
}"
>
{{ text }}
</span>
<template v-if="$slots['right']">

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
ArrowPathIcon,
LinkIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useAccountStore } from '~/store/account';
import { useReplaceRenewStore } from '~/store/replaceRenew';
const accountStore = useAccountStore();
const replaceRenewStore = useReplaceRenewStore();
const { keyLinkedStatus, keyLinkedOutput } = storeToRefs(replaceRenewStore);
defineProps<{
t: ComposerTranslation;
}>();
</script>
<template>
<div class="flex flex-wrap items-center justify-between gap-8px">
<BrandButton
v-if="keyLinkedStatus !== 'linked' && keyLinkedStatus !== 'checking'"
btn-style="none"
:no-padding="true"
:title="t('Refresh')"
class="group"
@click="replaceRenewStore.check(true)"
>
<UiBadge
v-if="keyLinkedOutput"
:color="keyLinkedOutput.color"
:icon="keyLinkedOutput.icon"
:icon-right="ArrowPathIcon"
size="16px"
>
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
</UiBadge>
</BrandButton>
<UiBadge
v-else
:color="keyLinkedOutput.color"
:icon="keyLinkedOutput.icon"
size="16px"
>
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
</UiBadge>
<span class="inline-flex flex-wrap-items-start gap-8px">
<BrandButton
v-if="keyLinkedStatus === 'notLinked'"
btn-style="underline"
:external="true"
:icon="LinkIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Link Key')"
:title="t('Learn more and link your key to your account')"
class="text-14px"
@click="accountStore.linkKey"
/>
<BrandButton
v-else
btn-style="underline"
:external="true"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
@click="accountStore.myKeys"
/>
</span>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import {
KeyIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { DOCS_REGISTRATION_REPLACE_KEY } from '~/helpers/urls';
import { useReplaceRenewStore } from '~/store/replaceRenew';
@@ -12,12 +13,12 @@ const replaceRenewStore = useReplaceRenewStore();
const { replaceStatusOutput } = storeToRefs(replaceRenewStore);
defineProps<{
t: any;
t: ComposerTranslation;
}>();
</script>
<template>
<div class="flex flex-wrap items-start justify-between gap-8px">
<div class="flex flex-wrap items-center justify-between gap-8px">
<BrandButton
v-if="!replaceStatusOutput"
:icon="KeyIcon"
@@ -32,16 +33,18 @@ defineProps<{
:icon="replaceStatusOutput.icon"
size="16px"
>
{{ t(replaceStatusOutput.text) }}
{{ t(replaceStatusOutput.text ?? 'Unknown') }}
</UiBadge>
<BrandButton
btn-style="underline"
:external="true"
:href="DOCS_REGISTRATION_REPLACE_KEY.toString()"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
/>
<span class="inline-flex flex-wrap items-center justify-end gap-8px">
<BrandButton
btn-style="underline"
:external="true"
:href="DOCS_REGISTRATION_REPLACE_KEY.toString()"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
/>
</span>
</div>
</template>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useServerStore } from '~/store/server';
export interface Props {
componentIs?: string;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
@@ -27,11 +28,11 @@ const output = computed(() => {
}
return {
text: regUpdatesExpired.value
? props.t('Ineligible for updates released after {0}', [outputDateTimeFormatted.value])
: props.t('Eligible for updates until {0}', [outputDateTimeFormatted.value]),
? props.t('Ineligible for feature updates released after {0}', [outputDateTimeFormatted.value])
: props.t('Eligible for free feature updates until {0}', [outputDateTimeFormatted.value]),
title: regUpdatesExpired.value
? props.t('Ineligible as of {0}', [outputDateTimeReadableDiff.value])
: props.t('Eligible for updates for {0}', [outputDateTimeReadableDiff.value]),
: props.t('Eligible for free feature updates for {0}', [outputDateTimeReadableDiff.value]),
};
});
</script>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { DOCS_REGISTRATION_LICENSING } from '~/helpers/urls';
@@ -8,7 +9,7 @@ import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
export interface Props {
t: any;
t: ComposerTranslation;
}
const props = defineProps<Props>();
@@ -39,11 +40,11 @@ const output = computed(() => {
}
return {
text: regUpdatesExpired.value
? props.t('Ineligible for updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for updates until {0}', [formattedRegExp.value]),
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]),
title: regUpdatesExpired.value
? props.t('Ineligible as of {0}', [readableDiffRegExp.value])
: props.t('Eligible for updates for {0}', [readableDiffRegExp.value]),
: props.t('Eligible for free feature updates for {0}', [readableDiffRegExp.value]),
};
});
</script>
@@ -75,7 +76,7 @@ const output = computed(() => {
:text="t('Extend License')"
:title="t('Pay your annual fee to continue receiving OS updates.')"
class="flex-grow"
@click="renewAction.click()"
@click="renewAction.click?.()"
/>
<BrandButton

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
export interface Props {
label?: string;
// propChecked?: boolean;
}
withDefaults(defineProps<Props>(), {
label: '',
// propChecked: false,
});
const checked = ref(false);
</script>
<template>
<SwitchGroup>
<div class="flex items-center gap-8px p-8px rounded">
<Switch
v-model="checked"
:class="checked ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
>
<span v-show="!checked" class="absolute z-0 inset-0 opacity-10 bg-beta" />
<span
:class="checked ? 'translate-x-[26px]' : 'translate-x-[2px]'"
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
/>
</Switch>
<SwitchLabel>{{ label }}</SwitchLabel>
</div>
</SwitchGroup>
</template>

View File

@@ -19,8 +19,8 @@ import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useServerStore } from '~/store/server';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
@@ -34,8 +34,8 @@ const props = withDefaults(defineProps<Props>(), {
rebootVersion: '',
});
const accountStore = useAccountStore();
const serverStore = useServerStore();
const updateOsActionsStore = useUpdateOsActionsStore();
const { rebootType } = storeToRefs(serverStore);
const subtitle = computed(() => {
@@ -50,7 +50,7 @@ const showLoader = computed(() => window.location.pathname === WEBGUI_TOOLS_UPDA
onBeforeMount(() => {
if (showLoader.value) {
updateOsActionsStore.executeUpdateOsCallback(true);
accountStore.updateOs(true);
}
serverStore.setRebootVersion(props.rebootVersion);
});

View File

@@ -1,23 +1,27 @@
<script setup lang="ts">
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import { useAccountStore } from '~/store/account';
import type { ButtonStyle } from '~/types/ui/button';
defineProps<{
t: any;
btnStyle?: ButtonStyle;
t: ComposerTranslation;
}>();
const updateOsActionsStore = useUpdateOsActionsStore();
const accountStore = useAccountStore();
</script>
<template>
<div class="flex flex-col sm:flex-shrink-0 sm:flex-grow-0 items-center">
<BrandButton
:btn-style="btnStyle"
:icon="ArrowPathIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Check for OS Updates')"
class="flex-0"
@click="updateOsActionsStore.executeUpdateOsCallback()"
@click="accountStore.updateOs()"
/>
</div>
</template>

View File

@@ -0,0 +1,140 @@
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
ArrowSmallRightIcon,
EyeIcon,
KeyIcon,
ServerStackIcon,
XMarkIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import { usePurchaseStore } from '~/store/purchase';
import { useUpdateOsStore } from '~/store/updateOs';
// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
export interface Props {
open?: boolean;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
const purchaseStore = usePurchaseStore();
const updateOsStore = useUpdateOsStore();
// const updateOsActionsStore = useUpdateOsActionsStore();
const updateOsChangelogStore = useUpdateOsChangelogStore();
const { availableWithRenewal } = storeToRefs(updateOsStore);
const {
releaseForUpdate,
mutatedParsedChangelog,
parseChangelogFailed,
parsedChangelogTitle,
} = storeToRefs(updateOsChangelogStore);
const showExtendKeyButton = computed(() => {
return availableWithRenewal.value;
});
</script>
<template>
<Modal
:center-content="false"
:error="!!parseChangelogFailed"
max-width="max-w-800px"
:open="!!releaseForUpdate"
:show-close-x="true"
:t="t"
:tall-content="true"
:title="parsedChangelogTitle ?? undefined"
@close="updateOsChangelogStore.setReleaseForUpdate(null)"
>
<template #main>
<div
v-if="mutatedParsedChangelog"
class="text-16px sm:text-18px prose prose-a:text-unraid-red hover:prose-a:no-underline hover:prose-a:text-unraid-red/60 dark:prose-a:text-orange hover:dark:prose-a:text-orange/60"
v-html="mutatedParsedChangelog"
/>
<div
v-else-if="parseChangelogFailed"
class="text-center flex flex-col gap-4 prose"
>
<h2 class="text-lg text-unraid-red italic font-semibold">
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
</h2>
<p>
{{ props.t(`It's highly recommended to review the changelog before continuing your update`) }}
</p>
<div
v-if="releaseForUpdate?.changelogPretty"
class="flex self-center"
>
<BrandButton
:href="releaseForUpdate?.changelogPretty"
btn-style="underline"
:external="true"
:icon-right="ArrowTopRightOnSquareIcon"
>
{{ props.t("View Changelog on Docs") }}
</BrandButton>
</div>
</div>
<div
v-else
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
>
<BrandLoading class="w-[150px] mx-auto mt-24px" />
<p>{{ props.t("Fetching & parsing changelog…") }}</p>
</div>
</template>
<template #footer>
<div class="flex flex-col-reverse xs:flex-row justify-between gap-12px md:gap-16px">
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-12px md:gap-16px">
<BrandButton
btn-style="underline-hover-red"
:icon="XMarkIcon"
@click="updateOsChangelogStore.setReleaseForUpdate(null)"
>
{{ props.t("Close") }}
</BrandButton>
<BrandButton
v-if="releaseForUpdate?.changelogPretty"
btn-style="underline"
:external="true"
:href="releaseForUpdate?.changelogPretty"
:icon="EyeIcon"
:icon-right="ArrowTopRightOnSquareIcon"
>
{{ props.t("View on Docs") }}
</BrandButton>
</div>
<BrandButton
v-if="showExtendKeyButton"
btn-style="fill"
:icon="KeyIcon"
:icon-right="ArrowTopRightOnSquareIcon"
@click="purchaseStore.renew()"
>
{{ props.t("Extend License to Update") }}
</BrandButton>
<BrandButton
v-else-if="releaseForUpdate?.sha256"
:icon="ServerStackIcon"
:icon-right="ArrowSmallRightIcon"
@click="updateOsChangelogStore.fetchAndConfirmInstall(releaseForUpdate.sha256)"
>
{{ props.t('Continue') }}
</BrandButton>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,335 @@
<script lang="ts" setup>
import {
ArrowTopRightOnSquareIcon,
CogIcon,
EyeIcon,
IdentificationIcon,
KeyIcon,
XMarkIcon,
} from '@heroicons/vue/24/solid';
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useAccountStore } from '~/store/account';
import { usePurchaseStore } from '~/store/purchase';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
import type { ButtonProps } from '~/types/ui/button';
export interface Props {
open?: boolean;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
const accountStore = useAccountStore();
const purchaseStore = usePurchaseStore();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const updateOsChangelogStore = useUpdateOsChangelogStore();
const {
regExp,
regUpdatesExpired,
dateTimeFormat,
updateOsIgnoredReleases,
updateOsNotificationsEnabled,
updateOsResponse,
} = storeToRefs(serverStore);
const {
available,
availableWithRenewal,
availableReleaseDate,
availableRequiresAuth,
checkForUpdatesLoading,
} = storeToRefs(updateOsStore);
/**
* regExp may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
*/
const formattedRegExp = ref<string>();
const setFormattedRegExp = () => { // ran in watch on regExp and onBeforeMount
if (!regExp.value) { return; }
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
formattedRegExp.value = outputDateTimeFormatted.value;
};
watch(regExp, (_newV) => {
setFormattedRegExp();
});
const ignoreThisRelease = ref(false);
// if we had a release ignored and now we don't set ignoreThisRelease to false
watch(updateOsIgnoredReleases, (newVal, oldVal) => {
if (oldVal.length > 0 && newVal.length === 0) {
ignoreThisRelease.value = false;
}
});
const notificationsSettings = computed(() => {
return !updateOsNotificationsEnabled.value
? props.t('Go to Settings > Notifications to enable automatic OS update notifications for future releases.')
: undefined;
});
interface ModalCopy {
title: string;
description?: string;
}
const modalCopy = computed((): ModalCopy | null => {
if (checkForUpdatesLoading.value) {
return {
title: props.t('Checking for OS updates...'),
};
}
// Use the release date
let formattedReleaseDate = '';
if (availableReleaseDate.value) {
// build string with prefix
formattedReleaseDate = props.t('Release date {0}', [userFormattedReleaseDate.value]);
}
if (availableWithRenewal.value) {
const description = regUpdatesExpired.value
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]);
return {
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
};
} else if (available.value) {
const description = availableRequiresAuth.value
? props.t('Release requires verification to update')
: undefined;
return {
title: props.t('Unraid OS {0} Update Available', [available.value]),
description: description ? `<p>${formattedReleaseDate}</p><p>${description}</p>` : formattedReleaseDate,
};
} else if (!available.value && !availableWithRenewal.value) {
return {
title: props.t('Unraid OS is up-to-date'),
description: notificationsSettings.value ?? undefined,
};
}
return null;
});
const showNotificationsSettingsLink = computed(() => {
return !updateOsNotificationsEnabled.value && !available.value && !availableWithRenewal.value;
});
const extraLinks = computed((): ButtonProps[] => {
const buttons: ButtonProps[] = [];
if (showNotificationsSettingsLink.value) {
buttons.push({
btnStyle: 'outline',
href: '/Settings/Notifications',
icon: CogIcon,
text: props.t('Enable update notifications'),
});
}
return buttons;
});
const actionButtons = computed((): ButtonProps[] | null => {
// update not available or no action buttons default closing
if (!available.value || ignoreThisRelease.value) { return null; }
const buttons: ButtonProps[] = [];
// update available but not stable branch - should link out to account update callback
// if availableWithRenewal.value is true, then we need to renew the license before we can update so don't show the verify button
if (availableRequiresAuth.value && !availableWithRenewal.value) {
buttons.push({
click: async () => await accountStore.updateOs(),
icon: IdentificationIcon,
text: props.t('Verify to Update'),
});
return buttons;
}
// update available - open changelog to commence update
if (available.value && updateOsResponse.value?.changelog) {
buttons.push({
btnStyle: availableWithRenewal.value
? 'outline'
: undefined,
click: async () => await updateOsChangelogStore.setReleaseForUpdate(updateOsResponse.value ?? null),
icon: EyeIcon,
text: availableWithRenewal.value
? props.t('View Changelog')
: props.t('View Changelog to Start Update'),
});
}
// update available with renewal - open changelog and Extend License options
if (availableWithRenewal.value) {
buttons.push({
click: async () => await purchaseStore.renew(),
icon: KeyIcon,
iconRight: ArrowTopRightOnSquareIcon,
iconRightHoverDisplay: false,
text: props.t('Extend License'),
title: props.t('Pay your annual fee to continue receiving OS updates.'),
});
}
return buttons;
});
const close = () => {
// close it
updateOsStore.setModalOpen(false);
// then ignore the release if applicable
if (ignoreThisRelease.value && (availableWithRenewal.value || available.value)) {
setTimeout(() => {
serverStore.updateOsIgnoreRelease(availableWithRenewal.value ?? available.value ?? '');
}, 500);
}
};
const renderMainSlot = computed(() => {
return !!(checkForUpdatesLoading.value || available.value || availableWithRenewal.value || extraLinks.value?.length > 0 || updateOsIgnoredReleases.value.length > 0);
});
const userFormattedReleaseDate = ref<string>();
/**
* availableReleaseDate may not have a value until we get a release in the update os check response.
* So we need to watch for this value to be able to format it based on the user's date time preferences.
*/
const setUserFormattedReleaseDate = () => {
if (!availableReleaseDate.value) { return; }
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, availableReleaseDate.value.valueOf());
userFormattedReleaseDate.value = outputDateTimeFormatted.value;
};
watch(availableReleaseDate, (_newV) => {
setUserFormattedReleaseDate();
});
onBeforeMount(() => {
if (availableReleaseDate.value) {
setUserFormattedReleaseDate();
}
setFormattedRegExp();
});
const modalWidth = computed(() => {
if (availableWithRenewal.value) { // wider since we'll have four buttons
return 'max-w-800px';
}
return 'max-w-640px';
});
</script>
<template>
<Modal
:t="t"
:open="open"
:title="modalCopy?.title"
:description="modalCopy?.description"
:show-close-x="!checkForUpdatesLoading"
:max-width="modalWidth"
@close="close"
>
<template v-if="renderMainSlot" #main>
<BrandLoading v-if="checkForUpdatesLoading" class="w-[150px] mx-auto" />
<div v-else class="flex flex-col gap-y-16px">
<div v-if="extraLinks.length > 0" class="flex flex-col xs:flex-row justify-center gap-8px">
<BrandButton
v-for="item in extraLinks"
:key="item.text"
:btn-style="item.btnStyle ?? undefined"
:href="item.href ?? undefined"
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="t(item.text ?? '')"
:title="item.title ? t(item.title) : undefined"
@click="item.click?.()"
/>
</div>
<div v-if="available || availableWithRenewal" class="mx-auto">
<SwitchGroup>
<div class="flex justify-center items-center gap-8px p-8px rounded">
<Switch
v-model="ignoreThisRelease"
:class="ignoreThisRelease ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
>
<span v-show="!ignoreThisRelease" class="absolute z-0 inset-0 opacity-10 bg-beta" />
<span
:class="ignoreThisRelease ? 'translate-x-[26px]' : 'translate-x-[2px]'"
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
/>
</Switch>
<SwitchLabel class="text-16px">
{{ t('Ignore this release until next reboot') }}
</SwitchLabel>
</div>
</SwitchGroup>
</div>
<div v-else-if="updateOsIgnoredReleases.length > 0" class="w-full max-w-640px mx-auto flex flex-col gap-8px">
<h3 class="text-left text-16px font-semibold italic">
{{ t('Ignored Releases') }}
</h3>
<UpdateOsIgnoredRelease
v-for="ignoredRelease in updateOsIgnoredReleases"
:key="ignoredRelease"
:label="ignoredRelease"
:t="t"
/>
</div>
</div>
</template>
<template v-if="!checkForUpdatesLoading" #footer>
<div
class="w-full flex gap-8px mx-auto"
:class="{
'flex-col-reverse xs:flex-row justify-between': actionButtons,
'justify-center': !actionButtons,
}"
>
<div class="flex flex-col-reverse xs:flex-row justify-start gap-8px">
<BrandButton
btn-style="underline-hover-red"
:icon="XMarkIcon"
:text="t('Close')"
@click="close"
/>
<BrandButton
btn-style="underline"
:icon="ArrowTopRightOnSquareIcon"
:text="t('More options')"
@click="accountStore.updateOs()"
/>
</div>
<div v-if="actionButtons" class="flex flex-col xs:flex-row justify-end gap-8px">
<BrandButton
v-for="item in actionButtons"
:key="item.text"
:btn-style="item.btnStyle ?? undefined"
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="t(item.text ?? '')"
:title="item.title ? t(item.title) : undefined"
@click="item.click?.()"
/>
</div>
</div>
</template>
</Modal>
</template>

View File

@@ -9,6 +9,7 @@ import {
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
@@ -20,7 +21,7 @@ import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{
t: any;
t: ComposerTranslation;
releaseDate: string;
version: string;
}>();
@@ -35,7 +36,7 @@ const {
const diagnosticsButton = ref<UserProfileLink | undefined>({
click: () => {
// @ts-ignore global function provided by the webgui on the update page
// @ts-expect-error global function provided by the webgui on the update page
downloadDiagnostics();
},
icon: FolderArrowDownIcon,
@@ -45,7 +46,7 @@ const diagnosticsButton = ref<UserProfileLink | undefined>({
const downgradeButton = ref<UserProfileLink>({
click: () => {
// @ts-ignore global function provided by the webgui on the update page
// @ts-expect-error global function provided by the webgui on the update page
confirmDowngrade();
},
name: 'downgrade',

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