mirror of
https://github.com/unraid/api.git
synced 2026-01-02 06:30:02 -06:00
Compare commits
41 Commits
feat/separ
...
v4.6.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0827df60e | ||
|
|
9568aabd17 | ||
|
|
5a61aec841 | ||
|
|
ed67af9568 | ||
|
|
5ba3fa67a2 | ||
|
|
1f10b63c8b | ||
|
|
df168224ea | ||
|
|
3911be38bc | ||
|
|
15153855f7 | ||
|
|
65b7747c0e | ||
|
|
5c1b4352cf | ||
|
|
9004313ff8 | ||
|
|
7a421ce4ec | ||
|
|
3c6683c814 | ||
|
|
6bed63805f | ||
|
|
f7f40d7906 | ||
|
|
e2d00dc346 | ||
|
|
48c6ad7afa | ||
|
|
12eddf894e | ||
|
|
61fe6966ca | ||
|
|
b4c8efa483 | ||
|
|
dc9d5ad661 | ||
|
|
4964cf557b | ||
|
|
e54f189630 | ||
|
|
9f7218da79 | ||
|
|
d5a3d0dfac | ||
|
|
2266139742 | ||
|
|
7fb78494cb | ||
|
|
2f09445f2e | ||
|
|
fa6a5c56b6 | ||
|
|
234017a828 | ||
|
|
0210b9e62e | ||
|
|
4b2763c7f9 | ||
|
|
ed44796d5d | ||
|
|
d23a89f0b3 | ||
|
|
d0d2ff65ed | ||
|
|
9f492bf217 | ||
|
|
c4b4d26af0 | ||
|
|
0bcfb47bbc | ||
|
|
b0099421f3 | ||
|
|
49f636541b |
2
.github/ISSUE_TEMPLATE/work_intent.md
vendored
2
.github/ISSUE_TEMPLATE/work_intent.md
vendored
@@ -3,7 +3,7 @@ name: Work Intent
|
||||
about: Request approval for planned development work (must be approved before starting)
|
||||
title: 'Work Intent: '
|
||||
labels: work-intent, unapproved
|
||||
assignees: ''
|
||||
assignees: 'elibosley'
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
44
.github/workflows/main.yml
vendored
44
.github/workflows/main.yml
vendored
@@ -152,6 +152,11 @@ jobs:
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
|
||||
- name: Upload PNPM Store to Github artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: packed-pnpm-store
|
||||
path: ${{ github.workspace }}/api/deploy/packed-pnpm-store.txz
|
||||
|
||||
build-unraid-ui-webcomponents:
|
||||
name: Build Unraid UI Library (Webcomponent Version)
|
||||
@@ -316,6 +321,15 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get API Version
|
||||
id: vars
|
||||
run: |
|
||||
GIT_SHA=$(git rev-parse --short HEAD)
|
||||
IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '')
|
||||
PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json)
|
||||
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
|
||||
echo "API_VERSION=${API_VERSION}" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
@@ -347,6 +361,11 @@ jobs:
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/plugin/api/
|
||||
- name: Download PNPM Store
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: packed-pnpm-store
|
||||
path: ${{ github.workspace }}/plugin/
|
||||
- name: Extract Unraid API
|
||||
run: |
|
||||
mkdir -p ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
|
||||
@@ -355,6 +374,7 @@ jobs:
|
||||
id: build-plugin
|
||||
run: |
|
||||
cd ${{ github.workspace }}/plugin
|
||||
ls -al
|
||||
pnpm run build:txz
|
||||
|
||||
if [ -n "${{ github.event.pull_request.number }}" ]; then
|
||||
@@ -365,19 +385,20 @@ jobs:
|
||||
BUCKET_PATH="unraid-api"
|
||||
fi
|
||||
|
||||
# On release, build both prod and preview plugins
|
||||
if [ "${{ needs.release-please.outputs.releases_created }}" == 'true' ]; then
|
||||
BASE_URL="https://stable.dl.unraid.net/unraid-api"
|
||||
else
|
||||
BASE_URL="https://preview.dl.unraid.net/unraid-api"
|
||||
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
|
||||
cp -r ./deploy ./deploy-prod
|
||||
fi
|
||||
|
||||
BASE_URL="https://preview.dl.unraid.net/unraid-api"
|
||||
echo "BUCKET_PATH=${BUCKET_PATH}" >> $GITHUB_OUTPUT
|
||||
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
|
||||
|
||||
pnpm run build:plugin --tag="${TAG}" --base-url="${BASE_URL}"
|
||||
|
||||
- name: Ensure Plugin Files Exist
|
||||
run: |
|
||||
ls -al ./deploy
|
||||
if [ ! -f ./deploy/*.plg ]; then
|
||||
echo "Error: .plg file not found in plugin/deploy/"
|
||||
exit 1
|
||||
@@ -387,6 +408,19 @@ jobs:
|
||||
echo "Error: .txz file not found in plugin/deploy/"
|
||||
exit 1
|
||||
fi
|
||||
- name: Ensure Production Plugin Files Exist
|
||||
if: needs.release-please.outputs.releases_created == 'true'
|
||||
run: |
|
||||
ls -al ./deploy-prod
|
||||
if [ ! -f ./deploy-prod/*.plg ]; then
|
||||
echo "Error: .plg file not found in plugin/deploy-prod/"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f ./deploy-prod/*.txz ]; then
|
||||
echo "Error: .txz file not found in plugin/deploy-prod/"
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload to GHA
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
@@ -414,7 +448,7 @@ jobs:
|
||||
run: |
|
||||
release_name=$(gh release list --repo ${{ github.repository }} --json name,isDraft --jq '.[] | select(.isDraft == true) | .name' | head -n 1)
|
||||
# For each file in release directory
|
||||
for file in deploy/*; do
|
||||
for file in deploy-prod/*; do
|
||||
echo "Uploading $file to release..."
|
||||
gh release upload "${release_name}" "$file" --clobber
|
||||
done
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -88,7 +88,6 @@ deploy/*
|
||||
.nitro
|
||||
.cache
|
||||
.output
|
||||
.env*
|
||||
!.env.example
|
||||
|
||||
fb_keepalive
|
||||
@@ -101,3 +100,9 @@ result
|
||||
result-*
|
||||
.direnv/
|
||||
.envrc
|
||||
|
||||
# Webgui sync script helpers
|
||||
web/scripts/.sync-webgui-repo-*
|
||||
|
||||
# Activation code data
|
||||
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/data/activation-data.php
|
||||
@@ -1 +1 @@
|
||||
{".":"4.4.0"}
|
||||
{".":"4.6.1"}
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
Project License Notice
|
||||
----------------------
|
||||
|
||||
This project is licensed under the terms of the GNU General Public License version 2,
|
||||
**or (at your option) any later version** published by the Free Software Foundation.
|
||||
|
||||
The full text of the GNU GPL v2.0 is provided below for reference.
|
||||
|
||||
----------------------
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
4
api/.env.staging
Normal file
4
api/.env.staging
Normal file
@@ -0,0 +1,4 @@
|
||||
ENVIRONMENT="staging"
|
||||
NODE_ENV="production"
|
||||
PORT="/var/run/unraid-api.sock"
|
||||
MOTHERSHIP_GRAPHQL_LINK="https://staging.mothership.unraid.net/ws"
|
||||
5
api/.gqlconfig
Normal file
5
api/.gqlconfig
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
schema: {
|
||||
files: 'src/graphql/schema/types/**/*.graphql'
|
||||
}
|
||||
}
|
||||
11
api/.vscode/extensions.json
vendored
Normal file
11
api/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"mikestead.dotenv",
|
||||
"eamodio.gitlens",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"antfu.goto-alias",
|
||||
"bierner.markdown-mermaid",
|
||||
"github.vscode-pull-request-github",
|
||||
"bierner.markdown-preview-github-styles"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,57 @@
|
||||
# Changelog
|
||||
|
||||
## [4.6.1](https://github.com/unraid/api/compare/v4.6.0...v4.6.1) (2025-04-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* don't mv deploy on prod release ([9568aab](https://github.com/unraid/api/commit/9568aabd17fbab9e7e2f06f723ee57dc2026583c))
|
||||
|
||||
## [4.6.0](https://github.com/unraid/api/compare/v4.5.0...v4.6.0) (2025-04-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add gui settings field for sso users ([#1310](https://github.com/unraid/api/issues/1310)) ([5ba3fa6](https://github.com/unraid/api/commit/5ba3fa67a26828f29e1e234c6838e7beaa3fdff3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build ([ed67af9](https://github.com/unraid/api/commit/ed67af956802eec95845519997bc15b32c84c6ee))
|
||||
* **plugin:** flaky masking of benign warning during pnpm install ([#1313](https://github.com/unraid/api/issues/1313)) ([1f10b63](https://github.com/unraid/api/commit/1f10b63c8b015e9a2527f79e15a7042feb2d2aca))
|
||||
|
||||
## [4.5.0](https://github.com/unraid/api/compare/v4.4.1...v4.5.0) (2025-04-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add webgui theme switcher component ([#1304](https://github.com/unraid/api/issues/1304)) ([e2d00dc](https://github.com/unraid/api/commit/e2d00dc3464f9663062ac759a8aad85e61804b91))
|
||||
* api plugin system & offline versioned dependency vendoring ([#1252](https://github.com/unraid/api/issues/1252)) ([9f492bf](https://github.com/unraid/api/commit/9f492bf2175b1b909d3bec079ce901ba34765eb7))
|
||||
* **api:** add `unraid-api --delete` command ([#1289](https://github.com/unraid/api/issues/1289)) ([2f09445](https://github.com/unraid/api/commit/2f09445f2ed6b23cd851ca64ac5b84cfde3cbd50))
|
||||
* basic array controls ([#1291](https://github.com/unraid/api/issues/1291)) ([61fe696](https://github.com/unraid/api/commit/61fe6966caf973eec3d74c67741302dd4b507134))
|
||||
* basic docker controls ([#1292](https://github.com/unraid/api/issues/1292)) ([12eddf8](https://github.com/unraid/api/commit/12eddf894e1808b61f5d4e007f3a7a39a3f2e4d6))
|
||||
* copy to webgui repo script docs + wc build options ([#1285](https://github.com/unraid/api/issues/1285)) ([e54f189](https://github.com/unraid/api/commit/e54f189630f70aeff5af6bdef4271f0a01fedb74))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* additional url fixes ([4b2763c](https://github.com/unraid/api/commit/4b2763c7f9d8b85d5b0ce066dfc9a9a80a115658))
|
||||
* **api:** redirect benign pnpm postinstall warning to log file ([#1290](https://github.com/unraid/api/issues/1290)) ([7fb7849](https://github.com/unraid/api/commit/7fb78494cb23630f60a889e6252fc06754e14ef9))
|
||||
* **deps:** update dependency chalk to v5 ([#1296](https://github.com/unraid/api/issues/1296)) ([6bed638](https://github.com/unraid/api/commit/6bed63805ff026be98a8e20c4d8a37cd47048357))
|
||||
* **deps:** update dependency diff to v7 ([#1297](https://github.com/unraid/api/issues/1297)) ([3c6683c](https://github.com/unraid/api/commit/3c6683c81422a088c13e9545aaecececd78b8628))
|
||||
* disable all config watchers ([#1306](https://github.com/unraid/api/issues/1306)) ([5c1b435](https://github.com/unraid/api/commit/5c1b4352cf71d8525f667822f8ca202e2934f463))
|
||||
* extract callbacks to library ([#1280](https://github.com/unraid/api/issues/1280)) ([2266139](https://github.com/unraid/api/commit/226613974258f15d39932de94316a54aec2e29d2))
|
||||
* OEM plugin issues ([#1288](https://github.com/unraid/api/issues/1288)) ([d5a3d0d](https://github.com/unraid/api/commit/d5a3d0dfac214fc433c2c0aec578de564a990dd4))
|
||||
* replace files lost during pruning ([d0d2ff6](https://github.com/unraid/api/commit/d0d2ff65ed2d51223414e50bb1c2ecf82e32a071))
|
||||
|
||||
## [4.4.1](https://github.com/unraid/api/compare/v4.4.0...v4.4.1) (2025-03-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* .env.production from allowing console logs on build ([#1273](https://github.com/unraid/api/issues/1273)) ([32acc2d](https://github.com/unraid/api/commit/32acc2d27c8bb565b38a66d8233030de3711ea12))
|
||||
* patch version override logic incorrect ([#1275](https://github.com/unraid/api/issues/1275)) ([6a59756](https://github.com/unraid/api/commit/6a597561a3e21c27fff8d4530cf59cf382eaa015))
|
||||
|
||||
## [4.4.0](https://github.com/unraid/api/compare/v4.3.1...v4.4.0) (2025-03-25)
|
||||
|
||||
|
||||
|
||||
1
api/dev/Unraid.net/Pro.key
Normal file
1
api/dev/Unraid.net/Pro.key
Normal file
@@ -0,0 +1 @@
|
||||
┘[5╢╦Ояb┴ю└;R╛леЩ²ДА├y÷шd│яя╛Еlя▓ё"Hи╜ь;QДs≈@Вы▄╠╩1·Qy╓к|й╔+╨фM)X9jя▄тГО⌠1а2WHщ'│.ЕJё-╨MPгS╜╧:Ю▓]o9^ЮО0┴$"░ l^`╪>3к:╦я ЯО┤q~ёш≈└с ш5ёЗ=р╟─]╗IWf╥и ⌡?:У2ВоE5[р╨Ш(÷╤Е}з+о│ШIмAч²%╞╓дq:ё╤эb╣┼
|
||||
1
api/dev/data/machine-id
Normal file
1
api/dev/data/machine-id
Normal file
@@ -0,0 +1 @@
|
||||
d0b5433294c110f1eed72bdb63910a9a
|
||||
1
api/dev/data/unraid-version
Normal file
1
api/dev/data/unraid-version
Normal file
@@ -0,0 +1 @@
|
||||
version="6.12.0-beta5"
|
||||
BIN
api/dev/dynamix/banner.png
Normal file
BIN
api/dev/dynamix/banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
1
api/dev/dynamix/case-model.cfg
Normal file
1
api/dev/dynamix/case-model.cfg
Normal file
@@ -0,0 +1 @@
|
||||
case-model.png
|
||||
BIN
api/dev/dynamix/case-model.png
Normal file
BIN
api/dev/dynamix/case-model.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
0
api/dev/states/devs.ini
Normal file
0
api/dev/states/devs.ini
Normal file
@@ -1,5 +1,5 @@
|
||||
[api]
|
||||
version="4.1.3"
|
||||
version="4.4.1"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox="yes"
|
||||
@@ -20,5 +20,5 @@ dynamicRemoteAccessType="DISABLED"
|
||||
ssoSubIds=""
|
||||
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
|
||||
[connectionStatus]
|
||||
minigraph="PRE_INIT"
|
||||
minigraph="ERROR_RETRYING"
|
||||
upnpStatus=""
|
||||
|
||||
30
api/dev/states/network.ini
Normal file
30
api/dev/states/network.ini
Normal file
@@ -0,0 +1,30 @@
|
||||
[eth0]
|
||||
DHCP_KEEPRESOLV="no"
|
||||
DNS_SERVER1="1.1.1.1"
|
||||
DNS_SERVER2="8.8.8.8"
|
||||
DHCP6_KEEPRESOLV="no"
|
||||
BONDING="yes"
|
||||
BONDNAME=""
|
||||
BONDNICS="eth0,eth1,eth2,eth3"
|
||||
BONDING_MODE="1"
|
||||
BONDING_MIIMON="100"
|
||||
BRIDGING="yes"
|
||||
BRNAME=""
|
||||
BRNICS="bond0"
|
||||
BRSTP="0"
|
||||
BRFD="0"
|
||||
DESCRIPTION:0=""
|
||||
PROTOCOL:0=""
|
||||
USE_DHCP:0="yes"
|
||||
IPADDR:0="192.168.1.150"
|
||||
NETMASK:0="255.255.255.0"
|
||||
GATEWAY:0="192.168.1.1"
|
||||
METRIC:0=""
|
||||
USE_DHCP6:0=""
|
||||
IPADDR6:0=""
|
||||
NETMASK6:0=""
|
||||
GATEWAY6:0=""
|
||||
METRIC6:0=""
|
||||
PRIVACY6:0=""
|
||||
MTU=""
|
||||
TYPE="access"
|
||||
190
api/dev/states/sec.ini
Normal file
190
api/dev/states/sec.ini
Normal file
@@ -0,0 +1,190 @@
|
||||
["disk1"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk2"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk3"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk4"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk5"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk6"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk7"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk8"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk9"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk10"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk11"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk12"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk13"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk14"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk15"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk16"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk17"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk18"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk19"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk20"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk21"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["disk22"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["abc"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
caseSensitive="auto"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
volsizelimit=""
|
||||
["flash"]
|
||||
export="e"
|
||||
fruit="no"
|
||||
security="public"
|
||||
readList=""
|
||||
writeList=""
|
||||
92
api/dev/states/sec_nfs.ini
Normal file
92
api/dev/states/sec_nfs.ini
Normal file
@@ -0,0 +1,92 @@
|
||||
["disk1"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk2"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk3"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk4"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk5"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk6"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk7"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk8"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk9"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk10"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk11"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk12"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk13"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk14"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk15"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk16"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk17"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk18"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk19"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk20"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk21"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["disk22"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
["abc"]
|
||||
export="-"
|
||||
security="public"
|
||||
hostList=""
|
||||
68
api/dev/states/shares.ini
Normal file
68
api/dev/states/shares.ini
Normal file
@@ -0,0 +1,68 @@
|
||||
["appdata"]
|
||||
name="appdata"
|
||||
nameOrig="appdata"
|
||||
comment=""
|
||||
allocator="highwater"
|
||||
splitLevel=""
|
||||
floor="0"
|
||||
include=""
|
||||
exclude=""
|
||||
useCache="no"
|
||||
cachePool="cache"
|
||||
cow="auto"
|
||||
color="yellow-on"
|
||||
size="0"
|
||||
free="9091184"
|
||||
used="32831348"
|
||||
luksStatus="0"
|
||||
["domains"]
|
||||
name="domains"
|
||||
nameOrig="domains"
|
||||
comment="saved VM instances"
|
||||
allocator="highwater"
|
||||
splitLevel="1"
|
||||
floor="0"
|
||||
include=""
|
||||
exclude=""
|
||||
useCache="prefer"
|
||||
cachePool="cache"
|
||||
cow="auto"
|
||||
color="yellow-on"
|
||||
size="0"
|
||||
free="9091184"
|
||||
used="32831348"
|
||||
luksStatus="0"
|
||||
["isos"]
|
||||
name="isos"
|
||||
nameOrig="isos"
|
||||
comment="ISO images"
|
||||
allocator="highwater"
|
||||
splitLevel=""
|
||||
floor="0"
|
||||
include=""
|
||||
exclude=""
|
||||
useCache="yes"
|
||||
cachePool="cache"
|
||||
cow="auto"
|
||||
color="yellow-on"
|
||||
size="0"
|
||||
free="9091184"
|
||||
used="32831348"
|
||||
luksStatus="0"
|
||||
["system"]
|
||||
name="system"
|
||||
nameOrig="system"
|
||||
comment="system data"
|
||||
allocator="highwater"
|
||||
splitLevel="1"
|
||||
floor="0"
|
||||
include=""
|
||||
exclude=""
|
||||
useCache="prefer"
|
||||
cachePool="cache"
|
||||
cow="auto"
|
||||
color="yellow-on"
|
||||
size="0"
|
||||
free="9091184"
|
||||
used="32831348"
|
||||
luksStatus="0"
|
||||
15
api/dev/states/users.ini
Normal file
15
api/dev/states/users.ini
Normal file
@@ -0,0 +1,15 @@
|
||||
["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"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "4.4.0",
|
||||
"version": "4.6.1",
|
||||
"main": "src/cli/index.ts",
|
||||
"type": "module",
|
||||
"corepack": {
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"repository": "git@github.com:unraid/api.git",
|
||||
"author": "Lime Technology, Inc. <unraid.net>",
|
||||
"license": "GPL-2.0-only",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"engines": {
|
||||
"pnpm": ">=8.0.0"
|
||||
},
|
||||
@@ -17,6 +17,7 @@
|
||||
"start": "node dist/main.js",
|
||||
"dev": "vite",
|
||||
"command": "pnpm run build && clear && ./dist/cli.js",
|
||||
"command:raw": "./dist/cli.js",
|
||||
"// Build and Deploy": "",
|
||||
"build": "vite build --mode=production",
|
||||
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
|
||||
@@ -77,6 +78,7 @@
|
||||
"cacheable-lookup": "^7.0.0",
|
||||
"camelcase-keys": "^9.1.3",
|
||||
"casbin": "^5.32.0",
|
||||
"change-case": "^5.4.4",
|
||||
"chokidar": "^4.0.1",
|
||||
"cli-table": "^0.3.11",
|
||||
"command-exists": "^1.2.9",
|
||||
@@ -143,7 +145,7 @@
|
||||
"@graphql-codegen/typed-document-node": "^5.0.11",
|
||||
"@graphql-codegen/typescript": "^4.1.1",
|
||||
"@graphql-codegen/typescript-operations": "^4.3.1",
|
||||
"@graphql-codegen/typescript-resolvers": "4.4.4",
|
||||
"@graphql-codegen/typescript-resolvers": "4.5.0",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
|
||||
"@nestjs/testing": "^11.0.11",
|
||||
@@ -186,6 +188,7 @@
|
||||
"rollup-plugin-node-externals": "^8.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"tsx": "^4.19.2",
|
||||
"type-fest": "^4.37.0",
|
||||
"typescript": "^5.6.3",
|
||||
"typescript-eslint": "^8.13.0",
|
||||
"unplugin-swc": "^1.5.1",
|
||||
@@ -201,5 +204,5 @@
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.6.5"
|
||||
"packageManager": "pnpm@10.7.1"
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ try {
|
||||
|
||||
// Update the package.json version to the deployment version
|
||||
parsedPackageJson.version = deploymentVersion;
|
||||
// omit dev dependencies from release build
|
||||
parsedPackageJson.devDependencies = {};
|
||||
|
||||
// Create a temporary directory for packaging
|
||||
await mkdir('./deploy/pack/', { recursive: true });
|
||||
@@ -36,9 +38,18 @@ try {
|
||||
// Change to the pack directory and install dependencies
|
||||
cd('./deploy/pack');
|
||||
|
||||
console.log('Installing production dependencies...');
|
||||
console.log('Building production pnpm store...');
|
||||
$.verbose = true;
|
||||
await $`pnpm install --prod --ignore-workspace --node-linker hoisted`;
|
||||
await $`pnpm install --prod --ignore-workspace --store-dir=../.pnpm-store`;
|
||||
|
||||
await $`rm -rf node_modules`; // Don't include node_modules in final package
|
||||
|
||||
const sudoCheck = await $`command -v sudo`.nothrow();
|
||||
const SUDO = sudoCheck.exitCode === 0 ? 'sudo' : '';
|
||||
await $`${SUDO} chown -R 0:0 ../.pnpm-store`;
|
||||
|
||||
await $`XZ_OPT=-5 tar -cJf ../packed-pnpm-store.txz ../.pnpm-store`;
|
||||
await $`${SUDO} rm -rf ../.pnpm-store`;
|
||||
|
||||
// chmod the cli
|
||||
await $`chmod +x ./dist/cli.js`;
|
||||
|
||||
3
api/src/__test__/common/nginx/nginx-state.test.ts
Normal file
3
api/src/__test__/common/nginx/nginx-state.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns generated data');
|
||||
@@ -0,0 +1,5 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Adds a disk to the array');
|
||||
|
||||
test.todo('Fails to add the disk if the array is started');
|
||||
@@ -0,0 +1,5 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Removes a disk from the array');
|
||||
|
||||
test.todo('Fails to remove the disk if the array is started');
|
||||
5
api/src/__test__/core/modules/array/update-array.test.ts
Normal file
5
api/src/__test__/core/modules/array/update-array.test.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Starts the array');
|
||||
|
||||
test.todo('Stops the array');
|
||||
@@ -0,0 +1,7 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Can start a parity check');
|
||||
|
||||
test.todo('Can pause a parity check');
|
||||
|
||||
test.todo('Can start a parity check');
|
||||
3
api/src/__test__/core/modules/debug/get-context.test.ts
Normal file
3
api/src/__test__/core/modules/debug/get-context.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns the current content');
|
||||
5
api/src/__test__/core/modules/disks/id/get-disk.test.ts
Normal file
5
api/src/__test__/core/modules/disks/id/get-disk.test.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns a single disk by ID');
|
||||
|
||||
test.todo('Returns nothing if no disk matches the ID');
|
||||
@@ -0,0 +1,5 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns all the Docker containers');
|
||||
|
||||
test.todo('Returns running Docker containers');
|
||||
@@ -0,0 +1,7 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns all USB devices');
|
||||
|
||||
test.todo('Returns all PCI-e devices');
|
||||
|
||||
test.todo('Returns all audio devices');
|
||||
3
api/src/__test__/core/modules/info/get-app-count.test.ts
Normal file
3
api/src/__test__/core/modules/info/get-app-count.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Gets total count of Docker containers installed/running');
|
||||
3
api/src/__test__/core/modules/info/get-baseboard.test.ts
Normal file
3
api/src/__test__/core/modules/info/get-baseboard.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns baseboard info');
|
||||
3
api/src/__test__/core/modules/info/get-cpu.test.ts
Normal file
3
api/src/__test__/core/modules/info/get-cpu.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Gets CPU info');
|
||||
@@ -1,23 +0,0 @@
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { SliceState } from '@app/store/modules/emhttp.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
|
||||
test('Returns true if the array is started', async () => {
|
||||
vi.spyOn(getters, 'emhttp').mockImplementation(
|
||||
() => ({ var: { mdState: 'STARTED' } }) as unknown as SliceState
|
||||
);
|
||||
|
||||
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running.js');
|
||||
expect(arrayIsRunning()).toBe(true);
|
||||
vi.spyOn(getters, 'emhttp').mockReset();
|
||||
});
|
||||
|
||||
test('Returns false if the array is stopped', async () => {
|
||||
vi.spyOn(getters, 'emhttp').mockImplementation(
|
||||
() => ({ var: { mdState: 'Stopped' } }) as unknown as SliceState
|
||||
);
|
||||
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running.js');
|
||||
expect(arrayIsRunning()).toBe(false);
|
||||
vi.spyOn(getters, 'emhttp').mockReset();
|
||||
});
|
||||
3
api/src/__test__/core/utils/misc/atomic-sleep.test.ts
Normal file
3
api/src/__test__/core/utils/misc/atomic-sleep.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Sleeps atomically for n milliseconds');
|
||||
3
api/src/__test__/core/utils/misc/get-machine-id.test.ts
Normal file
3
api/src/__test__/core/utils/misc/get-machine-id.test.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { test } from 'vitest';
|
||||
|
||||
test.todo('Returns machine-id');
|
||||
@@ -20,7 +20,16 @@ const getUnraidApiLocation = async () => {
|
||||
};
|
||||
|
||||
try {
|
||||
await CommandFactory.run(CliModule, {
|
||||
// Register plugins and create a dynamic module configuration
|
||||
const dynamicModule = await CliModule.registerWithPlugins();
|
||||
|
||||
// Create a new class that extends CliModule with the dynamic configuration
|
||||
const DynamicCliModule = class extends CliModule {
|
||||
static module = dynamicModule.module;
|
||||
static imports = dynamicModule.imports;
|
||||
static providers = dynamicModule.providers;
|
||||
};
|
||||
await CommandFactory.run(DynamicCliModule, {
|
||||
cliName: 'unraid-api',
|
||||
logger: LOG_LEVEL === 'TRACE' ? new LogService() : false, // - enable this to see nest initialization issues
|
||||
completion: {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { GraphQLError } from 'graphql';
|
||||
import { sum } from 'lodash-es';
|
||||
|
||||
import type { ArrayCapacity, ArrayType } from '@app/graphql/generated/api/types.js';
|
||||
import { getServerIdentifier } from '@app/core/utils/server-identifier.js';
|
||||
import { ArrayDiskType } from '@app/graphql/generated/api/types.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import fs from 'fs';
|
||||
|
||||
import camelCaseKeys from 'camelcase-keys';
|
||||
import { ContainerInfo } from 'dockerode';
|
||||
|
||||
import type { ContainerPort, Docker, DockerContainer } from '@app/graphql/generated/api/types.js';
|
||||
import { dockerLogger } from '@app/core/log.js';
|
||||
@@ -11,13 +10,16 @@ import { ContainerPortType, ContainerState } from '@app/graphql/generated/api/ty
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { updateDockerState } from '@app/store/modules/docker.js';
|
||||
|
||||
export interface ContainerListingOptions {
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Docker containers.
|
||||
* @returns All the in/active Docker containers on the system.
|
||||
*/
|
||||
|
||||
export const getDockerContainers = async (
|
||||
{ useCache } = { useCache: true }
|
||||
{ useCache }: ContainerListingOptions = { useCache: true }
|
||||
): Promise<Array<DockerContainer>> => {
|
||||
const dockerState = getters.docker();
|
||||
if (useCache && dockerState.containers) {
|
||||
|
||||
2
api/src/core/types/states/devices.ts
Normal file
2
api/src/core/types/states/devices.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type Device = Record<string, unknown>;
|
||||
export type Devices = Array<Record<string, unknown>>;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { got } from 'got';
|
||||
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { logger } from '@app/core/log.js';
|
||||
import { type LooseObject } from '@app/core/types/index.js';
|
||||
import { catchHandlers } from '@app/core/utils/misc/catch-handlers.js';
|
||||
import { DRY_RUN } from '@app/environment.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
|
||||
@@ -27,10 +27,15 @@ export const emcmd = async (commands: LooseObject) => {
|
||||
// Ensure we only log on dry-run
|
||||
return;
|
||||
}
|
||||
// Untested, this code is unused right now so going to assume it's probably not working well anyway, swapped
|
||||
// to got to remove this request-promise dependency
|
||||
return got
|
||||
.get(url, { searchParams: { ...commands, csrf_token: csrfToken } })
|
||||
.catch(catchHandlers.emhttpd);
|
||||
// return request.get(url, options).catch(catchHandlers.emhttpd);
|
||||
.get(url, {
|
||||
enableUnixSockets: true,
|
||||
searchParams: { ...commands, csrf_token: csrfToken },
|
||||
})
|
||||
.catch((error: NodeJS.ErrnoException) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new AppError('emhttpd socket unavailable.');
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
3
api/src/core/utils/files/get-extension-from-path.ts
Normal file
3
api/src/core/utils/files/get-extension-from-path.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { extname } from 'path';
|
||||
|
||||
export const getExtensionFromPath = (filePath: string): string => extname(filePath);
|
||||
@@ -3,36 +3,57 @@ import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const getPackageJsonVersion = () => {
|
||||
import type { PackageJson, SetRequired } from 'type-fest';
|
||||
|
||||
/**
|
||||
* Tries to get the package.json at the given location.
|
||||
* @param location - The location of the package.json file, relative to the current file
|
||||
* @returns The package.json object or undefined if unable to read
|
||||
*/
|
||||
function readPackageJson(location: string): PackageJson | undefined {
|
||||
try {
|
||||
// Try different possible locations for package.json
|
||||
const possibleLocations = ['../package.json', '../../package.json'];
|
||||
|
||||
for (const location of possibleLocations) {
|
||||
try {
|
||||
const packageJsonUrl = import.meta.resolve(location);
|
||||
const packageJsonPath = fileURLToPath(packageJsonUrl);
|
||||
const packageJson = readFileSync(packageJsonPath, 'utf-8');
|
||||
const packageJsonObject = JSON.parse(packageJson);
|
||||
if (packageJsonObject.version) {
|
||||
return packageJsonObject.version;
|
||||
}
|
||||
} catch {
|
||||
// Continue to next location if this one fails
|
||||
}
|
||||
let packageJsonPath: string;
|
||||
try {
|
||||
const packageJsonUrl = import.meta.resolve(location);
|
||||
packageJsonPath = fileURLToPath(packageJsonUrl);
|
||||
} catch {
|
||||
// Fallback (e.g. for local development): resolve the path relative to this module
|
||||
packageJsonPath = fileURLToPath(new URL(location, import.meta.url));
|
||||
}
|
||||
|
||||
// If we get here, we couldn't find a valid package.json in any location
|
||||
console.error('Could not find package.json in any of the expected locations');
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
console.error('Failed to load package.json:', error);
|
||||
const packageJsonRaw = readFileSync(packageJsonPath, 'utf-8');
|
||||
return JSON.parse(packageJsonRaw) as PackageJson;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Unraid API package.json. Throws if unable to find.
|
||||
* This should be considered a fatal error.
|
||||
*
|
||||
* @returns The package.json object
|
||||
*/
|
||||
export const getPackageJson = () => {
|
||||
const packageJson = readPackageJson('../package.json') || readPackageJson('../../package.json');
|
||||
if (!packageJson) {
|
||||
throw new Error('Could not find package.json in any of the expected locations');
|
||||
}
|
||||
return packageJson as SetRequired<PackageJson, 'version' | 'dependencies'>;
|
||||
};
|
||||
|
||||
export const API_VERSION =
|
||||
process.env.npm_package_version ?? getPackageJsonVersion() ?? new Error('API_VERSION not set');
|
||||
/**
|
||||
* Returns list of runtime dependencies from the Unraid-API package.json. Returns undefined if
|
||||
* the package.json or its dependency object cannot be found or read.
|
||||
*
|
||||
* Does not log or produce side effects.
|
||||
* @returns The names of all runtime dependencies. Undefined if failed.
|
||||
*/
|
||||
export const getPackageJsonDependencies = (): string[] | undefined => {
|
||||
const { dependencies } = getPackageJson();
|
||||
return Object.keys(dependencies);
|
||||
};
|
||||
|
||||
export const API_VERSION = process.env.npm_package_version ?? getPackageJson().version;
|
||||
|
||||
export const NODE_ENV =
|
||||
(process.env.NODE_ENV as 'development' | 'test' | 'staging' | 'production') ?? 'production';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import * as Types from '@app/graphql/generated/api/types.js';
|
||||
|
||||
import { z } from 'zod'
|
||||
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, arrayDiskInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
|
||||
import { AccessUrl, AccessUrlInput, AddPermissionInput, AddRoleForApiKeyInput, AddRoleForUserInput, AllowedOriginInput, ApiKey, ApiKeyResponse, ApiKeyWithSecret, ApiSettingsInput, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskInput, ArrayDiskStatus, ArrayDiskType, ArrayMutations, ArrayMutationsaddDiskToArrayArgs, ArrayMutationsclearArrayDiskStatisticsArgs, ArrayMutationsmountArrayDiskArgs, ArrayMutationsremoveDiskFromArrayArgs, ArrayMutationssetStateArgs, ArrayMutationsunmountArrayDiskArgs, ArrayPendingState, ArrayState, ArrayStateInput, ArrayStateInputState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, Connect, ConnectSettings, ConnectSettingsValues, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, CreateApiKeyInput, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, Docker, DockerContainer, DockerMutations, DockerMutationsstartContainerArgs, DockerMutationsstopContainerArgs, DockerNetwork, DynamicRemoteAccessStatus, DynamicRemoteAccessType, EnableDynamicRemoteAccessInput, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, LogFile, LogFileContent, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Node, Notification, NotificationCounts, NotificationData, NotificationFilter, NotificationOverview, NotificationType, Notifications, NotificationslistArgs, Os, Owner, ParityCheck, Partition, Pci, Permission, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, RemoveRoleFromApiKeyInput, Resource, Role, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, URL_TYPE, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addUserInput, deleteUserInput, mdState, registrationType, usersInput } from '@app/graphql/generated/api/types.js'
|
||||
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
|
||||
|
||||
type Properties<T> = Required<{
|
||||
@@ -25,6 +25,8 @@ export const ArrayPendingStateSchema = z.nativeEnum(ArrayPendingState);
|
||||
|
||||
export const ArrayStateSchema = z.nativeEnum(ArrayState);
|
||||
|
||||
export const ArrayStateInputStateSchema = z.nativeEnum(ArrayStateInputState);
|
||||
|
||||
export const ConfigErrorStateSchema = z.nativeEnum(ConfigErrorState);
|
||||
|
||||
export const ContainerPortTypeSchema = z.nativeEnum(ContainerPortType);
|
||||
@@ -158,7 +160,8 @@ export function ApiSettingsInputSchema(): z.ZodObject<Properties<ApiSettingsInpu
|
||||
extraOrigins: z.array(z.string()).nullish(),
|
||||
forwardType: WAN_FORWARD_TYPESchema.nullish(),
|
||||
port: z.number().nullish(),
|
||||
sandbox: z.boolean().nullish()
|
||||
sandbox: z.boolean().nullish(),
|
||||
ssoUserIds: z.array(z.string()).nullish()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +216,67 @@ export function ArrayDiskSchema(): z.ZodObject<Properties<ArrayDisk>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayDiskInputSchema(): z.ZodObject<Properties<ArrayDiskInput>> {
|
||||
return z.object({
|
||||
id: z.string(),
|
||||
slot: z.number().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsSchema(): z.ZodObject<Properties<ArrayMutations>> {
|
||||
return z.object({
|
||||
__typename: z.literal('ArrayMutations').optional(),
|
||||
addDiskToArray: ArrayTypeSchema().nullish(),
|
||||
clearArrayDiskStatistics: z.record(z.string(), z.any()).nullish(),
|
||||
mountArrayDisk: DiskSchema().nullish(),
|
||||
removeDiskFromArray: ArrayTypeSchema().nullish(),
|
||||
setState: ArrayTypeSchema().nullish(),
|
||||
unmountArrayDisk: DiskSchema().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsaddDiskToArrayArgsSchema(): z.ZodObject<Properties<ArrayMutationsaddDiskToArrayArgs>> {
|
||||
return z.object({
|
||||
input: z.lazy(() => ArrayDiskInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsclearArrayDiskStatisticsArgsSchema(): z.ZodObject<Properties<ArrayMutationsclearArrayDiskStatisticsArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsmountArrayDiskArgsSchema(): z.ZodObject<Properties<ArrayMutationsmountArrayDiskArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsremoveDiskFromArrayArgsSchema(): z.ZodObject<Properties<ArrayMutationsremoveDiskFromArrayArgs>> {
|
||||
return z.object({
|
||||
input: z.lazy(() => ArrayDiskInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationssetStateArgsSchema(): z.ZodObject<Properties<ArrayMutationssetStateArgs>> {
|
||||
return z.object({
|
||||
input: z.lazy(() => ArrayStateInputSchema().nullish())
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayMutationsunmountArrayDiskArgsSchema(): z.ZodObject<Properties<ArrayMutationsunmountArrayDiskArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function ArrayStateInputSchema(): z.ZodObject<Properties<ArrayStateInput>> {
|
||||
return z.object({
|
||||
desiredState: z.lazy(() => ArrayStateInputStateSchema)
|
||||
})
|
||||
}
|
||||
|
||||
export function BaseboardSchema(): z.ZodObject<Properties<Baseboard>> {
|
||||
return z.object({
|
||||
__typename: z.literal('Baseboard').optional(),
|
||||
@@ -299,7 +363,8 @@ export function ConnectSettingsValuesSchema(): z.ZodObject<Properties<ConnectSet
|
||||
extraOrigins: z.array(z.string()),
|
||||
forwardType: WAN_FORWARD_TYPESchema.nullish(),
|
||||
port: z.number().nullish(),
|
||||
sandbox: z.boolean()
|
||||
sandbox: z.boolean(),
|
||||
ssoUserIds: z.array(z.string())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -438,6 +503,7 @@ export function DockerSchema(): z.ZodObject<Properties<Docker>> {
|
||||
__typename: z.literal('Docker').optional(),
|
||||
containers: z.array(DockerContainerSchema()).nullish(),
|
||||
id: z.string(),
|
||||
mutations: DockerMutationsSchema(),
|
||||
networks: z.array(DockerNetworkSchema()).nullish()
|
||||
})
|
||||
}
|
||||
@@ -463,6 +529,26 @@ export function DockerContainerSchema(): z.ZodObject<Properties<DockerContainer>
|
||||
})
|
||||
}
|
||||
|
||||
export function DockerMutationsSchema(): z.ZodObject<Properties<DockerMutations>> {
|
||||
return z.object({
|
||||
__typename: z.literal('DockerMutations').optional(),
|
||||
startContainer: DockerContainerSchema(),
|
||||
stopContainer: DockerContainerSchema()
|
||||
})
|
||||
}
|
||||
|
||||
export function DockerMutationsstartContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstartContainerArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function DockerMutationsstopContainerArgsSchema(): z.ZodObject<Properties<DockerMutationsstopContainerArgs>> {
|
||||
return z.object({
|
||||
id: z.string()
|
||||
})
|
||||
}
|
||||
|
||||
export function DockerNetworkSchema(): z.ZodObject<Properties<DockerNetwork>> {
|
||||
return z.object({
|
||||
__typename: z.literal('DockerNetwork').optional(),
|
||||
@@ -1303,13 +1389,6 @@ export function addUserInputSchema(): z.ZodObject<Properties<addUserInput>> {
|
||||
})
|
||||
}
|
||||
|
||||
export function arrayDiskInputSchema(): z.ZodObject<Properties<arrayDiskInput>> {
|
||||
return z.object({
|
||||
id: z.string(),
|
||||
slot: z.number().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteUserInputSchema(): z.ZodObject<Properties<deleteUserInput>> {
|
||||
return z.object({
|
||||
name: z.string()
|
||||
|
||||
@@ -107,6 +107,8 @@ export type ApiSettingsInput = {
|
||||
* If false, the GraphQL sandbox will be disabled and only the production API will be available.
|
||||
*/
|
||||
sandbox?: InputMaybe<Scalars['Boolean']['input']>;
|
||||
/** A list of Unique Unraid Account ID's. */
|
||||
ssoUserIds?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
};
|
||||
|
||||
export type ArrayType = Node & {
|
||||
@@ -191,6 +193,13 @@ export enum ArrayDiskFsColor {
|
||||
YELLOW_ON = 'yellow_on'
|
||||
}
|
||||
|
||||
export type ArrayDiskInput = {
|
||||
/** Disk ID */
|
||||
id: Scalars['ID']['input'];
|
||||
/** The slot for the disk */
|
||||
slot?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export enum ArrayDiskStatus {
|
||||
/** disabled, old disk still present */
|
||||
DISK_DSBL = 'DISK_DSBL',
|
||||
@@ -223,6 +232,49 @@ export enum ArrayDiskType {
|
||||
PARITY = 'Parity'
|
||||
}
|
||||
|
||||
export type ArrayMutations = {
|
||||
__typename?: 'ArrayMutations';
|
||||
/** Add new disk to array */
|
||||
addDiskToArray?: Maybe<ArrayType>;
|
||||
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
|
||||
mountArrayDisk?: Maybe<Disk>;
|
||||
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
|
||||
removeDiskFromArray?: Maybe<ArrayType>;
|
||||
/** Set array state */
|
||||
setState?: Maybe<ArrayType>;
|
||||
unmountArrayDisk?: Maybe<Disk>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsaddDiskToArrayArgs = {
|
||||
input?: InputMaybe<ArrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsclearArrayDiskStatisticsArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsremoveDiskFromArrayArgs = {
|
||||
input?: InputMaybe<ArrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationssetStateArgs = {
|
||||
input?: InputMaybe<ArrayStateInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ArrayMutationsunmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export enum ArrayPendingState {
|
||||
/** Array has no data disks */
|
||||
NO_DATA_DISKS = 'no_data_disks',
|
||||
@@ -259,6 +311,18 @@ export enum ArrayState {
|
||||
TOO_MANY_MISSING_DISKS = 'TOO_MANY_MISSING_DISKS'
|
||||
}
|
||||
|
||||
export type ArrayStateInput = {
|
||||
/** Array state */
|
||||
desiredState: ArrayStateInputState;
|
||||
};
|
||||
|
||||
export enum ArrayStateInputState {
|
||||
/** Start array */
|
||||
START = 'START',
|
||||
/** Stop array */
|
||||
STOP = 'STOP'
|
||||
}
|
||||
|
||||
export type Baseboard = {
|
||||
__typename?: 'Baseboard';
|
||||
assetTag?: Maybe<Scalars['String']['output']>;
|
||||
@@ -346,6 +410,8 @@ export type ConnectSettingsValues = {
|
||||
* If false, the GraphQL sandbox is disabled and only the production API will be available.
|
||||
*/
|
||||
sandbox: Scalars['Boolean']['output'];
|
||||
/** A list of Unique Unraid Account ID's. */
|
||||
ssoUserIds: Array<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export type ConnectSignInInput = {
|
||||
@@ -493,6 +559,7 @@ export type Docker = Node & {
|
||||
__typename?: 'Docker';
|
||||
containers?: Maybe<Array<DockerContainer>>;
|
||||
id: Scalars['ID']['output'];
|
||||
mutations: DockerMutations;
|
||||
networks?: Maybe<Array<DockerNetwork>>;
|
||||
};
|
||||
|
||||
@@ -516,6 +583,22 @@ export type DockerContainer = {
|
||||
status: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type DockerMutations = {
|
||||
__typename?: 'DockerMutations';
|
||||
startContainer: DockerContainer;
|
||||
stopContainer: DockerContainer;
|
||||
};
|
||||
|
||||
|
||||
export type DockerMutationsstartContainerArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type DockerMutationsstopContainerArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
export type DockerNetwork = {
|
||||
__typename?: 'DockerNetwork';
|
||||
attachable: Scalars['Boolean']['output'];
|
||||
@@ -731,8 +814,6 @@ export type Mount = {
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
/** Add new disk to array */
|
||||
addDiskToArray?: Maybe<ArrayType>;
|
||||
addPermission: Scalars['Boolean']['output'];
|
||||
addRoleForApiKey: Scalars['Boolean']['output'];
|
||||
addRoleForUser: Scalars['Boolean']['output'];
|
||||
@@ -742,9 +823,9 @@ export type Mutation = {
|
||||
/** Marks a notification as archived. */
|
||||
archiveNotification: Notification;
|
||||
archiveNotifications: NotificationOverview;
|
||||
array?: Maybe<ArrayMutations>;
|
||||
/** Cancel parity check */
|
||||
cancelParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
clearArrayDiskStatistics?: Maybe<Scalars['JSON']['output']>;
|
||||
connectSignIn: Scalars['Boolean']['output'];
|
||||
connectSignOut: Scalars['Boolean']['output'];
|
||||
createApiKey: ApiKeyWithSecret;
|
||||
@@ -756,29 +837,21 @@ export type Mutation = {
|
||||
deleteUser?: Maybe<User>;
|
||||
enableDynamicRemoteAccess: Scalars['Boolean']['output'];
|
||||
login?: Maybe<Scalars['String']['output']>;
|
||||
mountArrayDisk?: Maybe<Disk>;
|
||||
/** Pause parity check */
|
||||
pauseParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
reboot?: Maybe<Scalars['String']['output']>;
|
||||
/** Reads each notification to recompute & update the overview. */
|
||||
recalculateOverview: NotificationOverview;
|
||||
/** Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error. */
|
||||
removeDiskFromArray?: Maybe<ArrayType>;
|
||||
removeRoleFromApiKey: Scalars['Boolean']['output'];
|
||||
/** Resume parity check */
|
||||
resumeParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
setAdditionalAllowedOrigins: Array<Scalars['String']['output']>;
|
||||
setupRemoteAccess: Scalars['Boolean']['output'];
|
||||
shutdown?: Maybe<Scalars['String']['output']>;
|
||||
/** Start array */
|
||||
startArray?: Maybe<ArrayType>;
|
||||
/** Start parity check */
|
||||
startParityCheck?: Maybe<Scalars['JSON']['output']>;
|
||||
/** Stop array */
|
||||
stopArray?: Maybe<ArrayType>;
|
||||
unarchiveAll: NotificationOverview;
|
||||
unarchiveNotifications: NotificationOverview;
|
||||
unmountArrayDisk?: Maybe<Disk>;
|
||||
/** Marks a notification as unread. */
|
||||
unreadNotification: Notification;
|
||||
/**
|
||||
@@ -789,11 +862,6 @@ export type Mutation = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationaddDiskToArrayArgs = {
|
||||
input?: InputMaybe<arrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationaddPermissionArgs = {
|
||||
input: AddPermissionInput;
|
||||
};
|
||||
@@ -829,11 +897,6 @@ export type MutationarchiveNotificationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationclearArrayDiskStatisticsArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationconnectSignInArgs = {
|
||||
input: ConnectSignInInput;
|
||||
};
|
||||
@@ -871,16 +934,6 @@ export type MutationloginArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoveDiskFromArrayArgs = {
|
||||
input?: InputMaybe<arrayDiskInput>;
|
||||
};
|
||||
|
||||
|
||||
export type MutationremoveRoleFromApiKeyArgs = {
|
||||
input: RemoveRoleFromApiKeyInput;
|
||||
};
|
||||
@@ -911,11 +964,6 @@ export type MutationunarchiveNotificationsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationunmountArrayDiskArgs = {
|
||||
id: Scalars['ID']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationunreadNotificationArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -1823,13 +1871,6 @@ export type addUserInput = {
|
||||
password: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type arrayDiskInput = {
|
||||
/** Disk ID */
|
||||
id: Scalars['ID']['input'];
|
||||
/** The slot for the disk */
|
||||
slot?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type deleteUserInput = {
|
||||
name: Scalars['String']['input'];
|
||||
};
|
||||
@@ -1945,10 +1986,14 @@ export type ResolversTypes = ResolversObject<{
|
||||
ArrayCapacity: ResolverTypeWrapper<ArrayCapacity>;
|
||||
ArrayDisk: ResolverTypeWrapper<ArrayDisk>;
|
||||
ArrayDiskFsColor: ArrayDiskFsColor;
|
||||
ArrayDiskInput: ArrayDiskInput;
|
||||
ArrayDiskStatus: ArrayDiskStatus;
|
||||
ArrayDiskType: ArrayDiskType;
|
||||
ArrayMutations: ResolverTypeWrapper<ArrayMutations>;
|
||||
ArrayPendingState: ArrayPendingState;
|
||||
ArrayState: ArrayState;
|
||||
ArrayStateInput: ArrayStateInput;
|
||||
ArrayStateInputState: ArrayStateInputState;
|
||||
Baseboard: ResolverTypeWrapper<Baseboard>;
|
||||
Boolean: ResolverTypeWrapper<Scalars['Boolean']['output']>;
|
||||
Capacity: ResolverTypeWrapper<Capacity>;
|
||||
@@ -1978,6 +2023,7 @@ export type ResolversTypes = ResolversObject<{
|
||||
Display: ResolverTypeWrapper<Display>;
|
||||
Docker: ResolverTypeWrapper<Docker>;
|
||||
DockerContainer: ResolverTypeWrapper<DockerContainer>;
|
||||
DockerMutations: ResolverTypeWrapper<DockerMutations>;
|
||||
DockerNetwork: ResolverTypeWrapper<DockerNetwork>;
|
||||
DynamicRemoteAccessStatus: ResolverTypeWrapper<DynamicRemoteAccessStatus>;
|
||||
DynamicRemoteAccessType: DynamicRemoteAccessType;
|
||||
@@ -2057,7 +2103,6 @@ export type ResolversTypes = ResolversObject<{
|
||||
WAN_FORWARD_TYPE: WAN_FORWARD_TYPE;
|
||||
Welcome: ResolverTypeWrapper<Welcome>;
|
||||
addUserInput: addUserInput;
|
||||
arrayDiskInput: arrayDiskInput;
|
||||
deleteUserInput: deleteUserInput;
|
||||
mdState: mdState;
|
||||
registrationType: registrationType;
|
||||
@@ -2079,6 +2124,9 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Array: ArrayType;
|
||||
ArrayCapacity: ArrayCapacity;
|
||||
ArrayDisk: ArrayDisk;
|
||||
ArrayDiskInput: ArrayDiskInput;
|
||||
ArrayMutations: ArrayMutations;
|
||||
ArrayStateInput: ArrayStateInput;
|
||||
Baseboard: Baseboard;
|
||||
Boolean: Scalars['Boolean']['output'];
|
||||
Capacity: Capacity;
|
||||
@@ -2102,6 +2150,7 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Display: Display;
|
||||
Docker: Docker;
|
||||
DockerContainer: DockerContainer;
|
||||
DockerMutations: DockerMutations;
|
||||
DockerNetwork: DockerNetwork;
|
||||
DynamicRemoteAccessStatus: DynamicRemoteAccessStatus;
|
||||
EnableDynamicRemoteAccessInput: EnableDynamicRemoteAccessInput;
|
||||
@@ -2165,7 +2214,6 @@ export type ResolversParentTypes = ResolversObject<{
|
||||
Vms: Vms;
|
||||
Welcome: Welcome;
|
||||
addUserInput: addUserInput;
|
||||
arrayDiskInput: arrayDiskInput;
|
||||
deleteUserInput: deleteUserInput;
|
||||
usersInput: usersInput;
|
||||
}>;
|
||||
@@ -2250,6 +2298,16 @@ export type ArrayDiskResolvers<ContextType = Context, ParentType extends Resolve
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type ArrayMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['ArrayMutations'] = ResolversParentTypes['ArrayMutations']> = ResolversObject<{
|
||||
addDiskToArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationsaddDiskToArrayArgs>>;
|
||||
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<ArrayMutationsclearArrayDiskStatisticsArgs, 'id'>>;
|
||||
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<ArrayMutationsmountArrayDiskArgs, 'id'>>;
|
||||
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationsremoveDiskFromArrayArgs>>;
|
||||
setState?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<ArrayMutationssetStateArgs>>;
|
||||
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<ArrayMutationsunmountArrayDiskArgs, 'id'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type BaseboardResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Baseboard'] = ResolversParentTypes['Baseboard']> = ResolversObject<{
|
||||
assetTag?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
manufacturer?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
@@ -2319,6 +2377,7 @@ export type ConnectSettingsValuesResolvers<ContextType = Context, ParentType ext
|
||||
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
|
||||
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
|
||||
sandbox?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
ssoUserIds?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
@@ -2417,6 +2476,7 @@ export type DisplayResolvers<ContextType = Context, ParentType extends Resolvers
|
||||
export type DockerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Docker'] = ResolversParentTypes['Docker']> = ResolversObject<{
|
||||
containers?: Resolver<Maybe<Array<ResolversTypes['DockerContainer']>>, ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
mutations?: Resolver<ResolversTypes['DockerMutations'], ParentType, ContextType>;
|
||||
networks?: Resolver<Maybe<Array<ResolversTypes['DockerNetwork']>>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
@@ -2440,6 +2500,12 @@ export type DockerContainerResolvers<ContextType = Context, ParentType extends R
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type DockerMutationsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerMutations'] = ResolversParentTypes['DockerMutations']> = ResolversObject<{
|
||||
startContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstartContainerArgs, 'id'>>;
|
||||
stopContainer?: Resolver<ResolversTypes['DockerContainer'], ParentType, ContextType, RequireFields<DockerMutationsstopContainerArgs, 'id'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
export type DockerNetworkResolvers<ContextType = Context, ParentType extends ResolversParentTypes['DockerNetwork'] = ResolversParentTypes['DockerNetwork']> = ResolversObject<{
|
||||
attachable?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
configFrom?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
@@ -2612,7 +2678,6 @@ export type MountResolvers<ContextType = Context, ParentType extends ResolversPa
|
||||
}>;
|
||||
|
||||
export type MutationResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation']> = ResolversObject<{
|
||||
addDiskToArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationaddDiskToArrayArgs>>;
|
||||
addPermission?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddPermissionArgs, 'input'>>;
|
||||
addRoleForApiKey?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddRoleForApiKeyArgs, 'input'>>;
|
||||
addRoleForUser?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationaddRoleForUserArgs, 'input'>>;
|
||||
@@ -2620,8 +2685,8 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
|
||||
archiveAll?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationarchiveAllArgs>>;
|
||||
archiveNotification?: Resolver<ResolversTypes['Notification'], ParentType, ContextType, RequireFields<MutationarchiveNotificationArgs, 'id'>>;
|
||||
archiveNotifications?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationarchiveNotificationsArgs>>;
|
||||
array?: Resolver<Maybe<ResolversTypes['ArrayMutations']>, ParentType, ContextType>;
|
||||
cancelParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
clearArrayDiskStatistics?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, RequireFields<MutationclearArrayDiskStatisticsArgs, 'id'>>;
|
||||
connectSignIn?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationconnectSignInArgs, 'input'>>;
|
||||
connectSignOut?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
createApiKey?: Resolver<ResolversTypes['ApiKeyWithSecret'], ParentType, ContextType, RequireFields<MutationcreateApiKeyArgs, 'input'>>;
|
||||
@@ -2631,22 +2696,17 @@ export type MutationResolvers<ContextType = Context, ParentType extends Resolver
|
||||
deleteUser?: Resolver<Maybe<ResolversTypes['User']>, ParentType, ContextType, RequireFields<MutationdeleteUserArgs, 'input'>>;
|
||||
enableDynamicRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationenableDynamicRemoteAccessArgs, 'input'>>;
|
||||
login?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationloginArgs, 'password' | 'username'>>;
|
||||
mountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationmountArrayDiskArgs, 'id'>>;
|
||||
pauseParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
reboot?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
recalculateOverview?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType>;
|
||||
removeDiskFromArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType, Partial<MutationremoveDiskFromArrayArgs>>;
|
||||
removeRoleFromApiKey?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationremoveRoleFromApiKeyArgs, 'input'>>;
|
||||
resumeParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
setAdditionalAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType, RequireFields<MutationsetAdditionalAllowedOriginsArgs, 'input'>>;
|
||||
setupRemoteAccess?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType, RequireFields<MutationsetupRemoteAccessArgs, 'input'>>;
|
||||
shutdown?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
startArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
|
||||
startParityCheck?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType, Partial<MutationstartParityCheckArgs>>;
|
||||
stopArray?: Resolver<Maybe<ResolversTypes['Array']>, ParentType, ContextType>;
|
||||
unarchiveAll?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationunarchiveAllArgs>>;
|
||||
unarchiveNotifications?: Resolver<ResolversTypes['NotificationOverview'], ParentType, ContextType, Partial<MutationunarchiveNotificationsArgs>>;
|
||||
unmountArrayDisk?: Resolver<Maybe<ResolversTypes['Disk']>, ParentType, ContextType, RequireFields<MutationunmountArrayDiskArgs, 'id'>>;
|
||||
unreadNotification?: Resolver<ResolversTypes['Notification'], ParentType, ContextType, RequireFields<MutationunreadNotificationArgs, 'id'>>;
|
||||
updateApiSettings?: Resolver<ResolversTypes['ConnectSettingsValues'], ParentType, ContextType, RequireFields<MutationupdateApiSettingsArgs, 'input'>>;
|
||||
}>;
|
||||
@@ -3273,6 +3333,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
|
||||
Array?: ArrayResolvers<ContextType>;
|
||||
ArrayCapacity?: ArrayCapacityResolvers<ContextType>;
|
||||
ArrayDisk?: ArrayDiskResolvers<ContextType>;
|
||||
ArrayMutations?: ArrayMutationsResolvers<ContextType>;
|
||||
Baseboard?: BaseboardResolvers<ContextType>;
|
||||
Capacity?: CapacityResolvers<ContextType>;
|
||||
Case?: CaseResolvers<ContextType>;
|
||||
@@ -3292,6 +3353,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
|
||||
Display?: DisplayResolvers<ContextType>;
|
||||
Docker?: DockerResolvers<ContextType>;
|
||||
DockerContainer?: DockerContainerResolvers<ContextType>;
|
||||
DockerMutations?: DockerMutationsResolvers<ContextType>;
|
||||
DockerNetwork?: DockerNetworkResolvers<ContextType>;
|
||||
DynamicRemoteAccessStatus?: DynamicRemoteAccessStatusResolvers<ContextType>;
|
||||
Flash?: FlashResolvers<ContextType>;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mergeTypeDefs } from '@graphql-tools/merge';
|
||||
|
||||
import { logger } from '@app/core/log.js';
|
||||
|
||||
export const loadTypeDefs = async () => {
|
||||
export const loadTypeDefs = async (additionalTypeDefs: string[] = []) => {
|
||||
// TypeScript now knows this returns Record<string, () => Promise<string>>
|
||||
const typeModules = import.meta.glob('./types/**/*.graphql', { query: '?raw', import: 'default' });
|
||||
|
||||
@@ -19,6 +19,7 @@ export const loadTypeDefs = async () => {
|
||||
if (!files.length) {
|
||||
throw new Error('No GraphQL type definitions found');
|
||||
}
|
||||
files.push(...additionalTypeDefs);
|
||||
return mergeTypeDefs(files);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load GraphQL type definitions:', error);
|
||||
|
||||
@@ -3,16 +3,26 @@ type Query {
|
||||
array: Array!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
enum ArrayStateInputState {
|
||||
"""Start array"""
|
||||
startArray: Array
|
||||
START
|
||||
"""Stop array"""
|
||||
stopArray: Array
|
||||
STOP
|
||||
}
|
||||
|
||||
input ArrayStateInput {
|
||||
"""Array state"""
|
||||
desiredState: ArrayStateInputState!
|
||||
}
|
||||
|
||||
type ArrayMutations {
|
||||
"""Set array state"""
|
||||
setState(input: ArrayStateInput): Array
|
||||
|
||||
"""Add new disk to array"""
|
||||
addDiskToArray(input: arrayDiskInput): Array
|
||||
addDiskToArray(input: ArrayDiskInput): Array
|
||||
"""Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error."""
|
||||
removeDiskFromArray(input: arrayDiskInput): Array
|
||||
removeDiskFromArray(input: ArrayDiskInput): Array
|
||||
|
||||
mountArrayDisk(id: ID!): Disk
|
||||
unmountArrayDisk(id: ID!): Disk
|
||||
@@ -20,11 +30,15 @@ type Mutation {
|
||||
clearArrayDiskStatistics(id: ID!): JSON
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
array: ArrayMutations
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
array: Array!
|
||||
}
|
||||
|
||||
input arrayDiskInput {
|
||||
input ArrayDiskInput {
|
||||
"""Disk ID"""
|
||||
id: ID!
|
||||
"""The slot for the disk"""
|
||||
|
||||
43
api/src/graphql/schema/types/cloud/cloud.graphql
Normal file
43
api/src/graphql/schema/types/cloud/cloud.graphql
Normal file
@@ -0,0 +1,43 @@
|
||||
type ApiKeyResponse {
|
||||
valid: Boolean!
|
||||
error: String
|
||||
}
|
||||
|
||||
enum MinigraphStatus {
|
||||
PRE_INIT
|
||||
CONNECTING
|
||||
CONNECTED
|
||||
PING_FAILURE
|
||||
ERROR_RETRYING
|
||||
}
|
||||
|
||||
type MinigraphqlResponse {
|
||||
status: MinigraphStatus!
|
||||
timeout: Int
|
||||
error: String
|
||||
}
|
||||
|
||||
type CloudResponse {
|
||||
status: String!
|
||||
ip: String
|
||||
error: String
|
||||
}
|
||||
|
||||
type RelayResponse {
|
||||
status: String!
|
||||
timeout: String
|
||||
error: String
|
||||
}
|
||||
|
||||
type Cloud {
|
||||
error: String
|
||||
apiKey: ApiKeyResponse!
|
||||
relay: RelayResponse
|
||||
minigraphql: MinigraphqlResponse!
|
||||
cloud: CloudResponse!
|
||||
allowedOrigins: [String!]!
|
||||
}
|
||||
|
||||
type Query {
|
||||
cloud: Cloud
|
||||
}
|
||||
@@ -39,8 +39,6 @@ input SetupRemoteAccessInput {
|
||||
port: Port
|
||||
}
|
||||
|
||||
|
||||
|
||||
input EnableDynamicRemoteAccessInput {
|
||||
url: AccessUrlInput!
|
||||
enabled: Boolean!
|
||||
@@ -59,59 +57,67 @@ type DynamicRemoteAccessStatus {
|
||||
}
|
||||
|
||||
"""
|
||||
Intersection type of ApiSettings and RemoteAccess
|
||||
Intersection type of ApiSettings and RemoteAccess
|
||||
"""
|
||||
type ConnectSettingsValues {
|
||||
"""
|
||||
If true, the GraphQL sandbox is enabled and available at /graphql.
|
||||
If false, the GraphQL sandbox is disabled and only the production API will be available.
|
||||
If true, the GraphQL sandbox is enabled and available at /graphql.
|
||||
If false, the GraphQL sandbox is disabled and only the production API will be available.
|
||||
"""
|
||||
sandbox: Boolean!
|
||||
"""
|
||||
A list of origins allowed to interact with the API.
|
||||
A list of origins allowed to interact with the API.
|
||||
"""
|
||||
extraOrigins: [String!]!
|
||||
"""
|
||||
The type of WAN access used for Remote Access.
|
||||
The type of WAN access used for Remote Access.
|
||||
"""
|
||||
accessType: WAN_ACCESS_TYPE!
|
||||
"""
|
||||
The type of port forwarding used for Remote Access.
|
||||
The type of port forwarding used for Remote Access.
|
||||
"""
|
||||
forwardType: WAN_FORWARD_TYPE
|
||||
"""
|
||||
The port used for Remote Access.
|
||||
The port used for Remote Access.
|
||||
"""
|
||||
port: Port
|
||||
"""
|
||||
A list of Unique Unraid Account ID's.
|
||||
"""
|
||||
ssoUserIds: [String!]!
|
||||
}
|
||||
|
||||
"""
|
||||
Input should be a subset of ApiSettings that can be updated.
|
||||
Some field combinations may be required or disallowed. Please refer to each field for more information.
|
||||
Input should be a subset of ApiSettings that can be updated.
|
||||
Some field combinations may be required or disallowed. Please refer to each field for more information.
|
||||
"""
|
||||
input ApiSettingsInput {
|
||||
"""
|
||||
If true, the GraphQL sandbox will be enabled and available at /graphql.
|
||||
If false, the GraphQL sandbox will be disabled and only the production API will be available.
|
||||
If true, the GraphQL sandbox will be enabled and available at /graphql.
|
||||
If false, the GraphQL sandbox will be disabled and only the production API will be available.
|
||||
"""
|
||||
sandbox: Boolean
|
||||
"""
|
||||
A list of origins allowed to interact with the API.
|
||||
A list of origins allowed to interact with the API.
|
||||
"""
|
||||
extraOrigins: [String!]
|
||||
"""
|
||||
The type of WAN access to use for Remote Access.
|
||||
The type of WAN access to use for Remote Access.
|
||||
"""
|
||||
accessType: WAN_ACCESS_TYPE
|
||||
"""
|
||||
The type of port forwarding to use for Remote Access.
|
||||
The type of port forwarding to use for Remote Access.
|
||||
"""
|
||||
forwardType: WAN_FORWARD_TYPE
|
||||
"""
|
||||
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
|
||||
Ignored if accessType is DISABLED or forwardType is UPNP.
|
||||
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType.
|
||||
Ignored if accessType is DISABLED or forwardType is UPNP.
|
||||
"""
|
||||
port: Port
|
||||
"""
|
||||
A list of Unique Unraid Account ID's.
|
||||
"""
|
||||
ssoUserIds: [String!]
|
||||
}
|
||||
|
||||
type ConnectSettings implements Node {
|
||||
@@ -140,8 +146,8 @@ type Mutation {
|
||||
setAdditionalAllowedOrigins(input: AllowedOriginInput!): [String!]!
|
||||
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
|
||||
"""
|
||||
Update the API settings.
|
||||
Some setting combinations may be required or disallowed. Please refer to each setting for more information.
|
||||
Update the API settings.
|
||||
Some setting combinations may be required or disallowed. Please refer to each setting for more information.
|
||||
"""
|
||||
updateApiSettings(input: ApiSettingsInput!): ConnectSettingsValues!
|
||||
}
|
||||
}
|
||||
|
||||
60
api/src/graphql/schema/types/docker/container.graphql
Normal file
60
api/src/graphql/schema/types/docker/container.graphql
Normal file
@@ -0,0 +1,60 @@
|
||||
type Query {
|
||||
"""All Docker containers"""
|
||||
dockerContainers(all: Boolean): [DockerContainer!]!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
dockerContainer(id: ID!): DockerContainer!
|
||||
dockerContainers: [DockerContainer]
|
||||
}
|
||||
|
||||
enum ContainerPortType {
|
||||
TCP
|
||||
UDP
|
||||
}
|
||||
|
||||
type ContainerPort {
|
||||
ip: String
|
||||
privatePort: Int
|
||||
publicPort: Int
|
||||
type: ContainerPortType
|
||||
}
|
||||
|
||||
enum ContainerState {
|
||||
RUNNING
|
||||
EXITED
|
||||
}
|
||||
|
||||
type ContainerHostConfig {
|
||||
networkMode: String!
|
||||
}
|
||||
|
||||
type ContainerMount {
|
||||
type: String!
|
||||
name: String!
|
||||
source: String!
|
||||
destination: String!
|
||||
driver: String!
|
||||
mode: String!
|
||||
rw: Boolean!
|
||||
propagation: String!
|
||||
}
|
||||
|
||||
type DockerContainer {
|
||||
id: ID!
|
||||
names: [String!]
|
||||
image: String!
|
||||
imageId: String!
|
||||
command: String!
|
||||
created: Int!
|
||||
ports: [ContainerPort!]!
|
||||
""" (B) Total size of all the files in the container """
|
||||
sizeRootFs: Long
|
||||
labels: JSON
|
||||
state: ContainerState!
|
||||
status: String!
|
||||
hostConfig: ContainerHostConfig
|
||||
networkSettings: JSON
|
||||
mounts: [JSON]
|
||||
autoStart: Boolean!
|
||||
}
|
||||
@@ -6,4 +6,13 @@ type Docker implements Node {
|
||||
|
||||
type Query {
|
||||
docker: Docker!
|
||||
}
|
||||
|
||||
type DockerMutations {
|
||||
startContainer(id: ID!): DockerContainer!
|
||||
stopContainer(id: ID!): DockerContainer!
|
||||
}
|
||||
|
||||
extend type Docker {
|
||||
mutations: DockerMutations!
|
||||
}
|
||||
13
api/src/graphql/schema/types/flash/flash.graphql
Normal file
13
api/src/graphql/schema/types/flash/flash.graphql
Normal file
@@ -0,0 +1,13 @@
|
||||
type Query {
|
||||
flash: Flash
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
flash: Flash!
|
||||
}
|
||||
|
||||
type Flash {
|
||||
guid: String
|
||||
vendor: String
|
||||
product: String
|
||||
}
|
||||
11
api/src/graphql/schema/types/info/apps.graphql
Normal file
11
api/src/graphql/schema/types/info/apps.graphql
Normal file
@@ -0,0 +1,11 @@
|
||||
type Info {
|
||||
"""Count of docker containers"""
|
||||
apps: InfoApps
|
||||
}
|
||||
|
||||
type InfoApps {
|
||||
"""How many docker containers are installed"""
|
||||
installed: Int
|
||||
"""How many docker containers are running"""
|
||||
started: Int
|
||||
}
|
||||
14
api/src/graphql/schema/types/info/baseboard.graphql
Normal file
14
api/src/graphql/schema/types/info/baseboard.graphql
Normal file
@@ -0,0 +1,14 @@
|
||||
type Info {
|
||||
baseboard: Baseboard
|
||||
}
|
||||
|
||||
type Baseboard {
|
||||
# Dell Inc.
|
||||
manufacturer: String!
|
||||
# 0MD99X
|
||||
model: String
|
||||
# A07
|
||||
version: String
|
||||
serial: String
|
||||
assetTag: String
|
||||
}
|
||||
39
api/src/graphql/schema/types/info/cpu.graphql
Normal file
39
api/src/graphql/schema/types/info/cpu.graphql
Normal file
@@ -0,0 +1,39 @@
|
||||
type Info {
|
||||
cpu: InfoCpu
|
||||
}
|
||||
|
||||
type InfoCpu {
|
||||
# 'Intel®'
|
||||
manufacturer: String!
|
||||
# 'Xeon® L5640'
|
||||
brand: String!
|
||||
# 'GenuineIntel'
|
||||
vendor: String!
|
||||
# '6'
|
||||
family: String!
|
||||
# '44'
|
||||
model: String!
|
||||
# '2'
|
||||
stepping: Int!
|
||||
# ''
|
||||
revision: String!
|
||||
# ''
|
||||
voltage: String
|
||||
# '2.27'
|
||||
speed: Float!
|
||||
# '1.60'
|
||||
speedmin: Float!
|
||||
# '2.26'
|
||||
speedmax: Float!
|
||||
# 12
|
||||
threads: Int!
|
||||
# 6
|
||||
cores: Int!
|
||||
# 1
|
||||
processors: Long!
|
||||
# 'LGA1366'
|
||||
socket: String!
|
||||
# { l1d: 196608, l1i: 196608, l2: 1, l3: 12 }
|
||||
cache: JSON!
|
||||
flags: [String!]
|
||||
}
|
||||
52
api/src/graphql/schema/types/info/devices.graphql
Normal file
52
api/src/graphql/schema/types/info/devices.graphql
Normal file
@@ -0,0 +1,52 @@
|
||||
type Info {
|
||||
devices: Devices
|
||||
}
|
||||
|
||||
type Devices {
|
||||
gpu: [Gpu]
|
||||
network: [Network]
|
||||
pci: [Pci]
|
||||
usb: [Usb]
|
||||
}
|
||||
|
||||
type Gpu {
|
||||
id: ID!
|
||||
type: String!
|
||||
typeid: String!
|
||||
vendorname: String!
|
||||
productid: String!
|
||||
blacklisted: Boolean!
|
||||
class: String!
|
||||
}
|
||||
|
||||
type Network {
|
||||
iface: String
|
||||
ifaceName: String
|
||||
ipv4: String
|
||||
ipv6: String
|
||||
mac: String
|
||||
internal: String
|
||||
operstate: String
|
||||
type: String
|
||||
duplex: String
|
||||
mtu: String
|
||||
speed: String
|
||||
carrierChanges: String
|
||||
}
|
||||
|
||||
type Pci {
|
||||
id: ID!
|
||||
type: String
|
||||
typeid: String
|
||||
vendorname: String
|
||||
vendorid: String
|
||||
productname: String
|
||||
productid: String
|
||||
blacklisted: String
|
||||
class: String
|
||||
}
|
||||
|
||||
type Usb {
|
||||
id: ID!
|
||||
name: String
|
||||
}
|
||||
34
api/src/graphql/schema/types/info/display.graphql
Normal file
34
api/src/graphql/schema/types/info/display.graphql
Normal file
@@ -0,0 +1,34 @@
|
||||
type Info {
|
||||
display: Display
|
||||
}
|
||||
|
||||
type Display {
|
||||
date: String
|
||||
number: String
|
||||
scale: Boolean
|
||||
tabs: Boolean
|
||||
users: String
|
||||
resize: Boolean
|
||||
wwn: Boolean
|
||||
total: Boolean
|
||||
usage: Boolean
|
||||
banner: String
|
||||
dashapps: String
|
||||
theme: Theme
|
||||
text: Boolean
|
||||
unit: Temperature
|
||||
warning: Int
|
||||
critical: Int
|
||||
hot: Int
|
||||
max: Int
|
||||
locale: String
|
||||
}
|
||||
|
||||
enum Temperature {
|
||||
C
|
||||
F
|
||||
}
|
||||
|
||||
enum Theme {
|
||||
white
|
||||
}
|
||||
4
api/src/graphql/schema/types/info/machine-id.graphql
Normal file
4
api/src/graphql/schema/types/info/machine-id.graphql
Normal file
@@ -0,0 +1,4 @@
|
||||
type Info {
|
||||
"""Machine ID"""
|
||||
machineId: ID
|
||||
}
|
||||
41
api/src/graphql/schema/types/info/memory.graphql
Normal file
41
api/src/graphql/schema/types/info/memory.graphql
Normal file
@@ -0,0 +1,41 @@
|
||||
type Info {
|
||||
memory: InfoMemory
|
||||
}
|
||||
|
||||
type InfoMemory {
|
||||
max: Long!
|
||||
total: Long!
|
||||
free: Long!
|
||||
used: Long!
|
||||
active: Long!
|
||||
available: Long!
|
||||
buffcache: Long!
|
||||
swaptotal: Long!
|
||||
swapused: Long!
|
||||
swapfree: Long!
|
||||
layout: [MemoryLayout!]
|
||||
}
|
||||
|
||||
type MemoryLayout {
|
||||
size: Long!
|
||||
bank: String
|
||||
type: MemoryType
|
||||
clockSpeed: Long
|
||||
formFactor: MemoryFormFactor
|
||||
manufacturer: String
|
||||
partNum: String
|
||||
serialNum: String
|
||||
voltageConfigured: Long
|
||||
voltageMin: Long
|
||||
voltageMax: Long
|
||||
}
|
||||
|
||||
enum MemoryType {
|
||||
DDR2
|
||||
DDR3
|
||||
DDR4
|
||||
}
|
||||
|
||||
enum MemoryFormFactor {
|
||||
DIMM
|
||||
}
|
||||
18
api/src/graphql/schema/types/info/os.graphql
Normal file
18
api/src/graphql/schema/types/info/os.graphql
Normal file
@@ -0,0 +1,18 @@
|
||||
type Info {
|
||||
os: Os
|
||||
}
|
||||
|
||||
type Os {
|
||||
platform: String
|
||||
distro: String
|
||||
release: String
|
||||
codename: String
|
||||
kernel: String
|
||||
arch: String
|
||||
hostname: String
|
||||
codepage: String
|
||||
logofile: String
|
||||
serial: String
|
||||
build: String
|
||||
uptime: DateTime
|
||||
}
|
||||
12
api/src/graphql/schema/types/info/system.graphql
Normal file
12
api/src/graphql/schema/types/info/system.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
type Info {
|
||||
system: System
|
||||
}
|
||||
|
||||
type System {
|
||||
manufacturer: String
|
||||
model: String
|
||||
version: String
|
||||
serial: String
|
||||
uuid: String
|
||||
sku: String
|
||||
}
|
||||
32
api/src/graphql/schema/types/info/versions.graphql
Normal file
32
api/src/graphql/schema/types/info/versions.graphql
Normal file
@@ -0,0 +1,32 @@
|
||||
type Info {
|
||||
versions: Versions
|
||||
}
|
||||
|
||||
type Versions {
|
||||
kernel: String
|
||||
openssl: String
|
||||
systemOpenssl: String
|
||||
systemOpensslLib: String
|
||||
node: String
|
||||
v8: String
|
||||
npm: String
|
||||
yarn: String
|
||||
pm2: String
|
||||
gulp: String
|
||||
grunt: String
|
||||
git: String
|
||||
tsc: String
|
||||
mysql: String
|
||||
redis: String
|
||||
mongodb: String
|
||||
apache: String
|
||||
nginx: String
|
||||
php: String
|
||||
docker: String
|
||||
postfix: String
|
||||
postgresql: String
|
||||
perl: String
|
||||
python: String
|
||||
gcc: String
|
||||
unraid: String
|
||||
}
|
||||
13
api/src/graphql/schema/types/owner/owner.graphql
Normal file
13
api/src/graphql/schema/types/owner/owner.graphql
Normal file
@@ -0,0 +1,13 @@
|
||||
type Query {
|
||||
owner: Owner
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
owner: Owner!
|
||||
}
|
||||
|
||||
type Owner {
|
||||
username: String
|
||||
url: String
|
||||
avatar: String
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
type Mount {
|
||||
name: String
|
||||
directory: String
|
||||
type: String
|
||||
permissions: String
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
type Partition {
|
||||
devlinks: String
|
||||
devname: String
|
||||
devpath: String
|
||||
devtype: String
|
||||
idAta: String
|
||||
idAtaDownloadMicrocode: String
|
||||
idAtaFeatureSetAam: String
|
||||
idAtaFeatureSetAamCurrentValue: String
|
||||
idAtaFeatureSetAamEnabled: String
|
||||
idAtaFeatureSetAamVendorRecommendedValue: String
|
||||
idAtaFeatureSetApm: String
|
||||
idAtaFeatureSetApmCurrentValue: String
|
||||
idAtaFeatureSetApmEnabled: String
|
||||
idAtaFeatureSetHpa: String
|
||||
idAtaFeatureSetHpaEnabled: String
|
||||
idAtaFeatureSetPm: String
|
||||
idAtaFeatureSetPmEnabled: String
|
||||
idAtaFeatureSetPuis: String
|
||||
idAtaFeatureSetPuisEnabled: String
|
||||
idAtaFeatureSetSecurity: String
|
||||
idAtaFeatureSetSecurityEnabled: String
|
||||
idAtaFeatureSetSecurityEnhancedEraseUnitMin: String
|
||||
idAtaFeatureSetSecurityEraseUnitMin: String
|
||||
idAtaFeatureSetSmart: String
|
||||
idAtaFeatureSetSmartEnabled: String
|
||||
idAtaRotationRateRpm: String
|
||||
idAtaSata: String
|
||||
idAtaSataSignalRateGen1: String
|
||||
idAtaSataSignalRateGen2: String
|
||||
idAtaWriteCache: String
|
||||
idAtaWriteCacheEnabled: String
|
||||
idBus: String
|
||||
idFsType: String
|
||||
idFsUsage: String
|
||||
idFsUuid: String
|
||||
idFsUuidEnc: String
|
||||
idModel: String
|
||||
idModelEnc: String
|
||||
idPartEntryDisk: String
|
||||
idPartEntryNumber: String
|
||||
idPartEntryOffset: String
|
||||
idPartEntryScheme: String
|
||||
idPartEntrySize: String
|
||||
idPartEntryType: String
|
||||
idPartTableType: String
|
||||
idPath: String
|
||||
idPathTag: String
|
||||
idRevision: String
|
||||
idSerial: String
|
||||
idSerialShort: String
|
||||
idType: String
|
||||
idWwn: String
|
||||
idWwnWithExtension: String
|
||||
major: String
|
||||
minor: String
|
||||
partn: String
|
||||
subsystem: String
|
||||
usecInitialized: String
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export const store = configureStore({
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type ApiStore = typeof store;
|
||||
|
||||
export const getters = {
|
||||
cache: () => store.getState().cache,
|
||||
|
||||
0
api/src/store/initial-state/initial-config-state.ts
Normal file
0
api/src/store/initial-state/initial-config-state.ts
Normal file
@@ -219,6 +219,9 @@ export const config = createSlice({
|
||||
stateAsArray.push(action.payload);
|
||||
state.remote.ssoSubIds = stateAsArray.join(',');
|
||||
},
|
||||
setSsoUsers(state, action: PayloadAction<string[]>) {
|
||||
state.remote.ssoSubIds = action.payload.filter((id) => id).join(',');
|
||||
},
|
||||
removeSsoUser(state, action: PayloadAction<string | null>) {
|
||||
if (action.payload === null) {
|
||||
state.remote.ssoSubIds = '';
|
||||
@@ -309,6 +312,7 @@ const { actions, reducer } = config;
|
||||
|
||||
export const {
|
||||
addSsoUser,
|
||||
setSsoUsers,
|
||||
updateUserConfig,
|
||||
updateAccessTokens,
|
||||
updateAllowedOrigins,
|
||||
@@ -324,6 +328,7 @@ export const {
|
||||
*/
|
||||
export const configUpdateActionsFlash = isAnyOf(
|
||||
addSsoUser,
|
||||
setSsoUsers,
|
||||
updateUserConfig,
|
||||
updateAccessTokens,
|
||||
updateAllowedOrigins,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { store } from '@app/store/index.js';
|
||||
import { syncInfoApps } from '@app/store/sync/info-apps-sync.js';
|
||||
import { syncRegistration } from '@app/store/sync/registration-sync.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
import { setupConfigPathWatch } from '@app/store/watch/config-watch.js';
|
||||
|
||||
export const startStoreSync = async () => {
|
||||
// The last state is stored so we don't end up in a loop of writing -> reading -> writing
|
||||
@@ -45,6 +44,4 @@ export const startStoreSync = async () => {
|
||||
|
||||
lastState = state;
|
||||
});
|
||||
|
||||
setupConfigPathWatch();
|
||||
};
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { existsSync, writeFileSync } from 'fs';
|
||||
|
||||
import { watch } from 'chokidar';
|
||||
|
||||
import { logger } from '@app/core/log.js';
|
||||
import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer.js';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer.js';
|
||||
import { CHOKIDAR_USEPOLLING, ENVIRONMENT } from '@app/environment.js';
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { initialState, loadConfigFile, logoutUser } from '@app/store/modules/config.js';
|
||||
|
||||
export const setupConfigPathWatch = () => {
|
||||
const myServersConfigPath = getters.paths()?.['myservers-config'];
|
||||
if (myServersConfigPath) {
|
||||
logger.info('Watch Setup on Config Path: %s', myServersConfigPath);
|
||||
if (!existsSync(myServersConfigPath)) {
|
||||
const config = safelySerializeObjectToIni(getWriteableConfig(initialState, 'flash'));
|
||||
writeFileSync(myServersConfigPath, config, 'utf-8');
|
||||
}
|
||||
const watcher = watch(myServersConfigPath, {
|
||||
persistent: true,
|
||||
ignoreInitial: false,
|
||||
usePolling: CHOKIDAR_USEPOLLING === true,
|
||||
})
|
||||
.on('change', async (change) => {
|
||||
logger.trace('Config File Changed, Reloading Config %s', change);
|
||||
await store.dispatch(loadConfigFile());
|
||||
})
|
||||
.on('unlink', async () => {
|
||||
const config = safelySerializeObjectToIni(getWriteableConfig(initialState, 'flash'));
|
||||
await writeFileSync(myServersConfigPath, config, 'utf-8');
|
||||
watcher.close();
|
||||
setupConfigPathWatch();
|
||||
store.dispatch(logoutUser({ reason: 'Config File was Deleted' }));
|
||||
});
|
||||
} else {
|
||||
logger.error('[FATAL] Failed to setup watch on My Servers Config (Could Not Read Config Path)');
|
||||
}
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import { GraphqlAuthGuard } from '@app/unraid-api/auth/auth.guard.js';
|
||||
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
|
||||
import { CronModule } from '@app/unraid-api/cron/cron.module.js';
|
||||
import { GraphModule } from '@app/unraid-api/graph/graph.module.js';
|
||||
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
|
||||
import { RestModule } from '@app/unraid-api/rest/rest.module.js';
|
||||
import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/unraid-file-modifier.module.js';
|
||||
|
||||
@@ -46,6 +47,7 @@ import { UnraidFileModifierModule } from '@app/unraid-api/unraid-file-modifier/u
|
||||
},
|
||||
]),
|
||||
UnraidFileModifierModule,
|
||||
PluginModule.registerPlugins(),
|
||||
],
|
||||
controllers: [],
|
||||
providers: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import crypto from 'crypto';
|
||||
import { readdir, readFile, writeFile } from 'fs/promises';
|
||||
import { readdir, readFile, unlink, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
|
||||
import { watch } from 'chokidar';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { setLocalApiKey } from '@app/store/modules/config.js';
|
||||
import { FileLoadStatus } from '@app/store/types.js';
|
||||
import { batchProcess } from '@app/utils.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyService implements OnModuleInit {
|
||||
@@ -312,4 +313,36 @@ export class ApiKeyService implements OnModuleInit {
|
||||
basePath: this.basePath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes API keys from the disk and updates the in-memory store.
|
||||
*
|
||||
* This method first verifies that all the provided API key IDs exist in the in-memory store.
|
||||
* If any keys are missing, it throws an Error detailing the missing keys.
|
||||
* It then deletes the corresponding JSON files concurrently using batch processing.
|
||||
* If any errors occur during the file deletion process, an array of errors is thrown.
|
||||
*
|
||||
* @param ids An array of API key identifiers to delete.
|
||||
* @throws Error if one or more API keys are not found.
|
||||
* @throws Array<Error> if errors occur during the file deletion.
|
||||
*/
|
||||
public async deleteApiKeys(ids: string[]): Promise<void> {
|
||||
// First verify all keys exist
|
||||
const missingKeys = ids.filter((id) => !this.findByField('id', id));
|
||||
if (missingKeys.length > 0) {
|
||||
throw new Error(`API keys not found: ${missingKeys.join(', ')}`);
|
||||
}
|
||||
|
||||
// Delete all files in parallel
|
||||
const { errors, data: deletedIds } = await batchProcess(ids, async (id) => {
|
||||
await unlink(join(this.basePath, `${id}.json`));
|
||||
return id;
|
||||
});
|
||||
|
||||
const deletedSet = new Set(deletedIds);
|
||||
this.memoryApiKeys = this.memoryApiKeys.filter((key) => !deletedSet.has(key.id));
|
||||
if (errors.length > 0) {
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ import { AuthActionVerb } from 'nest-authz';
|
||||
import { Command, CommandRunner, InquirerService, Option } from 'nest-commander';
|
||||
|
||||
import type { Permission } from '@app/graphql/generated/api/types.js';
|
||||
import type { DeleteApiKeyAnswers } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
|
||||
import { Resource, Role } from '@app/graphql/generated/api/types.js';
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
|
||||
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
|
||||
interface KeyOptions {
|
||||
name: string;
|
||||
create: boolean;
|
||||
delete?: boolean;
|
||||
description?: string;
|
||||
roles?: Role[];
|
||||
permissions?: Permission[];
|
||||
@@ -17,7 +20,7 @@ interface KeyOptions {
|
||||
|
||||
@Command({
|
||||
name: 'apikey',
|
||||
description: `Create / Fetch Connect API Keys - use --create with no arguments for a creation wizard`,
|
||||
description: `Create / Fetch / Delete Connect API Keys - use --create with no arguments for a creation wizard, or --delete to remove keys`,
|
||||
})
|
||||
export class ApiKeyCommand extends CommandRunner {
|
||||
constructor(
|
||||
@@ -88,8 +91,50 @@ ACTIONS: ${Object.values(AuthActionVerb).join(', ')}`,
|
||||
return description;
|
||||
}
|
||||
|
||||
async run(_: string[], options: KeyOptions = { create: false, name: '' }): Promise<void> {
|
||||
@Option({
|
||||
flags: '--delete',
|
||||
description: 'Delete selected API keys',
|
||||
})
|
||||
parseDelete(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Prompt the user to select API keys to delete. Then, delete the selected keys. */
|
||||
private async deleteKeys() {
|
||||
const allKeys = this.apiKeyService.findAll();
|
||||
if (allKeys.length === 0) {
|
||||
this.logger.log('No API keys found to delete');
|
||||
return;
|
||||
}
|
||||
|
||||
const answers = await this.inquirerService.prompt<DeleteApiKeyAnswers>(
|
||||
DeleteApiKeyQuestionSet.name,
|
||||
{}
|
||||
);
|
||||
if (!answers.selectedKeys || answers.selectedKeys.length === 0) {
|
||||
this.logger.log('No keys selected for deletion');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiKeyService.deleteApiKeys(answers.selectedKeys);
|
||||
this.logger.log(`Successfully deleted ${answers.selectedKeys.length} API keys`);
|
||||
} catch (error) {
|
||||
this.logger.error(error as any);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async run(
|
||||
_: string[],
|
||||
options: KeyOptions = { create: false, name: '', delete: false }
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (options.delete) {
|
||||
await this.deleteKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
const key = this.apiKeyService.findByField('name', options.name);
|
||||
if (key) {
|
||||
this.logger.log(key.key);
|
||||
|
||||
35
api/src/unraid-api/cli/apikey/delete-api-key.questions.ts
Normal file
35
api/src/unraid-api/cli/apikey/delete-api-key.questions.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { ChoicesFor, Question, QuestionSet } from 'nest-commander';
|
||||
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
|
||||
export interface DeleteApiKeyAnswers {
|
||||
selectedKeys: string[];
|
||||
}
|
||||
|
||||
@QuestionSet({ name: 'delete-api-key' })
|
||||
export class DeleteApiKeyQuestionSet {
|
||||
constructor(
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
private readonly logger: LogService
|
||||
) {}
|
||||
|
||||
static name = 'delete-api-key';
|
||||
|
||||
@Question({
|
||||
name: 'selectedKeys',
|
||||
type: 'checkbox',
|
||||
message: 'Select API keys to delete:',
|
||||
})
|
||||
parseSelectedKeys(keyIds: string[]): string[] {
|
||||
return keyIds;
|
||||
}
|
||||
|
||||
@ChoicesFor({ name: 'selectedKeys' })
|
||||
async getKeys() {
|
||||
return this.apiKeyService.findAll().map((key) => ({
|
||||
name: `${key.name} (${key.description ?? ''}) [${key.id}]`,
|
||||
value: key.id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
|
||||
|
||||
import { CommandRunner } from 'nest-commander';
|
||||
|
||||
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service.js';
|
||||
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions.js';
|
||||
import { ApiKeyCommand } from '@app/unraid-api/cli/apikey/api-key.command.js';
|
||||
import { DeleteApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/delete-api-key.questions.js';
|
||||
import { ConfigCommand } from '@app/unraid-api/cli/config.command.js';
|
||||
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command.js';
|
||||
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
|
||||
@@ -23,32 +26,77 @@ import { StatusCommand } from '@app/unraid-api/cli/status.command.js';
|
||||
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
|
||||
import { SwitchEnvCommand } from '@app/unraid-api/cli/switch-env.command.js';
|
||||
import { VersionCommand } from '@app/unraid-api/cli/version.command.js';
|
||||
import { ApiPluginDefinition } from '@app/unraid-api/plugin/plugin.interface.js';
|
||||
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
|
||||
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
|
||||
|
||||
const DEFAULT_COMMANDS = [
|
||||
ApiKeyCommand,
|
||||
ConfigCommand,
|
||||
DeveloperCommand,
|
||||
LogsCommand,
|
||||
ReportCommand,
|
||||
RestartCommand,
|
||||
StartCommand,
|
||||
StatusCommand,
|
||||
StopCommand,
|
||||
SwitchEnvCommand,
|
||||
VersionCommand,
|
||||
SSOCommand,
|
||||
ValidateTokenCommand,
|
||||
AddSSOUserCommand,
|
||||
RemoveSSOUserCommand,
|
||||
ListSSOUserCommand,
|
||||
] as const;
|
||||
|
||||
const DEFAULT_PROVIDERS = [
|
||||
AddApiKeyQuestionSet,
|
||||
DeleteApiKeyQuestionSet,
|
||||
AddSSOUserQuestionSet,
|
||||
RemoveSSOUserQuestionSet,
|
||||
DeveloperQuestions,
|
||||
LogService,
|
||||
PM2Service,
|
||||
ApiKeyService,
|
||||
] as const;
|
||||
|
||||
type PluginProvider = Provider & {
|
||||
provide: string | symbol | Type<any>;
|
||||
useValue?: ApiPluginDefinition;
|
||||
};
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
AddSSOUserCommand,
|
||||
AddSSOUserQuestionSet,
|
||||
RemoveSSOUserCommand,
|
||||
RemoveSSOUserQuestionSet,
|
||||
ListSSOUserCommand,
|
||||
LogService,
|
||||
PM2Service,
|
||||
StartCommand,
|
||||
StopCommand,
|
||||
RestartCommand,
|
||||
ReportCommand,
|
||||
ApiKeyService,
|
||||
ApiKeyCommand,
|
||||
AddApiKeyQuestionSet,
|
||||
SwitchEnvCommand,
|
||||
VersionCommand,
|
||||
StatusCommand,
|
||||
SSOCommand,
|
||||
ValidateTokenCommand,
|
||||
LogsCommand,
|
||||
ConfigCommand,
|
||||
DeveloperCommand,
|
||||
DeveloperQuestions,
|
||||
],
|
||||
imports: [PluginModule],
|
||||
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS],
|
||||
})
|
||||
export class CliModule {}
|
||||
export class CliModule {
|
||||
/**
|
||||
* Get all registered commands
|
||||
* @returns Array of registered command classes
|
||||
*/
|
||||
static getCommands(): Type<CommandRunner>[] {
|
||||
return [...DEFAULT_COMMANDS];
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the module with plugin support
|
||||
* @returns DynamicModule configuration including plugin commands
|
||||
*/
|
||||
static async registerWithPlugins(): Promise<DynamicModule> {
|
||||
const pluginModule = await PluginModule.registerPlugins();
|
||||
|
||||
// Get commands from plugins
|
||||
const pluginCommands: Type<CommandRunner>[] = [];
|
||||
for (const provider of (pluginModule.providers || []) as PluginProvider[]) {
|
||||
if (provider.provide !== PluginService && provider.useValue?.commands) {
|
||||
pluginCommands.push(...provider.useValue.commands);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
module: CliModule,
|
||||
imports: [pluginModule],
|
||||
providers: [...DEFAULT_COMMANDS, ...DEFAULT_PROVIDERS, ...pluginCommands],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import { loadConfigFile, updateUserConfig } from '@app/store/modules/config.js';
|
||||
import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
|
||||
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
|
||||
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
|
||||
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
|
||||
|
||||
interface DeveloperOptions {
|
||||
disclaimer: boolean;
|
||||
@@ -21,7 +22,8 @@ export class DeveloperCommand extends CommandRunner {
|
||||
constructor(
|
||||
private logger: LogService,
|
||||
private readonly inquirerService: InquirerService,
|
||||
private readonly restartCommand: RestartCommand
|
||||
private readonly startCommand: StartCommand,
|
||||
private readonly stopCommand: StopCommand
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -33,6 +35,7 @@ export class DeveloperCommand extends CommandRunner {
|
||||
}
|
||||
const { store } = await import('@app/store/index.js');
|
||||
await store.dispatch(loadConfigFile());
|
||||
await this.stopCommand.run([]);
|
||||
store.dispatch(updateUserConfig({ local: { sandbox: options.sandbox ? 'yes' : 'no' } }));
|
||||
writeConfigSync('flash');
|
||||
|
||||
@@ -40,6 +43,6 @@ export class DeveloperCommand extends CommandRunner {
|
||||
'Updated Developer Configuration - restart the API in 5 seconds to apply them...'
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
await this.restartCommand.run([]);
|
||||
await this.startCommand.run([], {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
|
||||
import { AddSSOUserQuestionSet } from '@app/unraid-api/cli/sso/add-sso-user.questions.js';
|
||||
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
|
||||
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
|
||||
|
||||
interface AddSSOUserCommandOptions {
|
||||
disclaimer: string;
|
||||
@@ -25,7 +27,8 @@ export class AddSSOUserCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly logger: LogService,
|
||||
private readonly inquirerService: InquirerService,
|
||||
private readonly restartCommand: RestartCommand
|
||||
private readonly startCommand: StartCommand,
|
||||
private readonly stopCommand: StopCommand
|
||||
) {
|
||||
super();
|
||||
}
|
||||
@@ -34,16 +37,12 @@ export class AddSSOUserCommand extends CommandRunner {
|
||||
try {
|
||||
options = await this.inquirerService.prompt(AddSSOUserQuestionSet.name, options);
|
||||
if (options.disclaimer === 'y' && options.username) {
|
||||
await this.stopCommand.run([]);
|
||||
await store.dispatch(loadConfigFile());
|
||||
const shouldRestart = store.getState().config.remote.ssoSubIds.length === 0;
|
||||
store.dispatch(addSsoUser(options.username));
|
||||
writeConfigSync('flash');
|
||||
this.logger.info(`User added ${options.username}`);
|
||||
if (shouldRestart) {
|
||||
this.logger.info('Restarting the Unraid API in 5 seconds to enable the SSO button');
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
await this.restartCommand.run([]);
|
||||
}
|
||||
this.logger.info(`User added ${options.username}, starting the API`);
|
||||
await this.startCommand.run([], {});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CommandRunner, InquirerService, Option, OptionChoiceFor, SubCommand } from 'nest-commander';
|
||||
import { CommandRunner, InquirerService, Option, SubCommand } from 'nest-commander';
|
||||
|
||||
import { store } from '@app/store/index.js';
|
||||
import { loadConfigFile, removeSsoUser } from '@app/store/modules/config.js';
|
||||
import { writeConfigSync } from '@app/store/sync/config-disk-sync.js';
|
||||
import { LogService } from '@app/unraid-api/cli/log.service.js';
|
||||
import { RemoveSSOUserQuestionSet } from '@app/unraid-api/cli/sso/remove-sso-user.questions.js';
|
||||
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
|
||||
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
|
||||
|
||||
interface RemoveSSOUserCommandOptions {
|
||||
username: string;
|
||||
@@ -21,13 +23,17 @@ interface RemoveSSOUserCommandOptions {
|
||||
export class RemoveSSOUserCommand extends CommandRunner {
|
||||
constructor(
|
||||
private readonly logger: LogService,
|
||||
private readonly inquirerService: InquirerService
|
||||
private readonly inquirerService: InquirerService,
|
||||
private readonly stopCommand: StopCommand,
|
||||
private readonly startCommand: StartCommand
|
||||
) {
|
||||
super();
|
||||
}
|
||||
public async run(_input: string[], options: RemoveSSOUserCommandOptions): Promise<void> {
|
||||
await store.dispatch(loadConfigFile());
|
||||
options = await this.inquirerService.prompt(RemoveSSOUserQuestionSet.name, options);
|
||||
|
||||
await this.stopCommand.run([]);
|
||||
store.dispatch(removeSsoUser(options.username === 'all' ? null : options.username));
|
||||
if (options.username === 'all') {
|
||||
this.logger.info('All users removed from SSO');
|
||||
@@ -35,6 +41,7 @@ export class RemoveSSOUserCommand extends CommandRunner {
|
||||
this.logger.info('User removed: ' + options.username);
|
||||
}
|
||||
writeConfigSync('flash');
|
||||
await this.startCommand.run([], {});
|
||||
}
|
||||
|
||||
@Option({
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
WAN_FORWARD_TYPE,
|
||||
} from '@app/graphql/generated/api/types.js';
|
||||
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access.js';
|
||||
import { updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js';
|
||||
import { setSsoUsers, updateAllowedOrigins, updateUserConfig } from '@app/store/modules/config.js';
|
||||
import { mergeSettingSlices } from '@app/unraid-api/types/json-forms.js';
|
||||
import { csvStringToArray } from '@app/utils.js';
|
||||
|
||||
@@ -50,11 +50,12 @@ export class ConnectSettingsService {
|
||||
|
||||
async getCurrentSettings(): Promise<ConnectSettingsValues> {
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
const { local, api } = getters.config();
|
||||
const { local, api, remote } = getters.config();
|
||||
return {
|
||||
...(await this.dynamicRemoteAccessSettings()),
|
||||
sandbox: local.sandbox === 'yes',
|
||||
extraOrigins: csvStringToArray(api.extraOrigins),
|
||||
ssoUserIds: csvStringToArray(remote.ssoSubIds),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ export class ConnectSettingsService {
|
||||
* @param settings - The settings to sync
|
||||
* @returns true if a restart is required, false otherwise
|
||||
*/
|
||||
async syncSettings(settings: Partial<ApiSettingsInput>) {
|
||||
async syncSettings(settings: Partial<ApiSettingsInput>): Promise<boolean> {
|
||||
let restartRequired = false;
|
||||
const { getters } = await import('@app/store/index.js');
|
||||
const { nginx } = getters.emhttp();
|
||||
@@ -86,13 +87,15 @@ export class ConnectSettingsService {
|
||||
port: settings.port,
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.extraOrigins) {
|
||||
await this.updateAllowedOrigins(settings.extraOrigins);
|
||||
}
|
||||
if (typeof settings.sandbox === 'boolean') {
|
||||
restartRequired ||= await this.setSandboxMode(settings.sandbox);
|
||||
}
|
||||
if (settings.ssoUserIds) {
|
||||
restartRequired ||= await this.updateSSOUsers(settings.ssoUserIds);
|
||||
}
|
||||
const { writeConfigSync } = await import('@app/store/sync/config-disk-sync.js');
|
||||
writeConfigSync('flash');
|
||||
return restartRequired;
|
||||
@@ -117,6 +120,32 @@ export class ConnectSettingsService {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the SSO users and returns true if a restart is required
|
||||
* @param userIds - The list of SSO user IDs
|
||||
* @returns true if a restart is required, false otherwise
|
||||
*/
|
||||
private async updateSSOUsers(userIds: string[]): Promise<boolean> {
|
||||
const { ssoUserIds } = await this.getCurrentSettings();
|
||||
const currentUserSet = new Set(ssoUserIds);
|
||||
const newUserSet = new Set(userIds);
|
||||
if (newUserSet.symmetricDifference(currentUserSet).size === 0) {
|
||||
// there's no change, so no need to update
|
||||
return false;
|
||||
}
|
||||
// make sure we aren't adding invalid user ids
|
||||
const uuidRegex =
|
||||
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||
const invalidUserIds = userIds.filter((id) => !uuidRegex.test(id));
|
||||
if (invalidUserIds.length > 0) {
|
||||
throw new GraphQLError(`Invalid SSO user ID's: ${invalidUserIds.join(', ')}`);
|
||||
}
|
||||
const { store } = await import('@app/store/index.js');
|
||||
store.dispatch(setSsoUsers(userIds));
|
||||
// request a restart if we're there were no sso users before
|
||||
return currentUserSet.size === 0;
|
||||
}
|
||||
|
||||
private async updateRemoteAccess(input: SetupRemoteAccessInput): Promise<boolean> {
|
||||
const { store } = await import('@app/store/index.js');
|
||||
await store.dispatch(setupRemoteAccessThunk(input)).unwrap();
|
||||
@@ -151,6 +180,7 @@ export class ConnectSettingsService {
|
||||
await this.remoteAccessSlice(),
|
||||
await this.sandboxSlice(),
|
||||
this.flashBackupSlice(),
|
||||
this.ssoUsersSlice(),
|
||||
// Because CORS is effectively disabled, this setting is no longer necessary
|
||||
// keeping it here for in case it needs to be re-enabled
|
||||
//
|
||||
@@ -344,4 +374,32 @@ export class ConnectSettingsService {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra origins settings slice
|
||||
*/
|
||||
ssoUsersSlice(): SettingSlice {
|
||||
return {
|
||||
properties: {
|
||||
ssoUserIds: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
title: 'Unraid API SSO Users',
|
||||
description: `Provide a list of Unique Unraid Account ID's. Find yours at <a href="https://account.unraid.net/settings" target="_blank">account.unraid.net/settings</a>`,
|
||||
},
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
type: 'Control',
|
||||
scope: '#/properties/ssoUserIds',
|
||||
options: {
|
||||
inputType: 'text',
|
||||
placeholder: 'UUID',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ export class ConnectResolver implements ConnectResolvers {
|
||||
const restartRequired = await this.connectSettingsService.syncSettings(settings);
|
||||
const currentSettings = await this.connectSettingsService.getCurrentSettings();
|
||||
if (restartRequired) {
|
||||
const restartDelayMs = 3_000;
|
||||
setTimeout(async () => {
|
||||
// Send restart out of band to avoid blocking the return of this resolver
|
||||
this.logger.log('Restarting API');
|
||||
await this.connectService.restartApi();
|
||||
}, restartDelayMs);
|
||||
}, 300);
|
||||
}
|
||||
return currentSettings;
|
||||
}
|
||||
|
||||
@@ -24,38 +24,46 @@ import { ResolversModule } from '@app/unraid-api/graph/resolvers/resolvers.modul
|
||||
import { sandboxPlugin } from '@app/unraid-api/graph/sandbox-plugin.js';
|
||||
import { ServicesResolver } from '@app/unraid-api/graph/services/services.resolver.js';
|
||||
import { SharesResolver } from '@app/unraid-api/graph/shares/shares.resolver.js';
|
||||
import { PluginModule } from '@app/unraid-api/plugin/plugin.module.js';
|
||||
import { PluginService } from '@app/unraid-api/plugin/plugin.service.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ResolversModule,
|
||||
GraphQLModule.forRootAsync<ApolloDriverConfig>({
|
||||
driver: ApolloDriver,
|
||||
useFactory: async () => ({
|
||||
introspection: getters.config()?.local?.sandbox === 'yes',
|
||||
playground: false,
|
||||
context: ({ req, connectionParams, extra }: any) => ({
|
||||
req,
|
||||
connectionParams,
|
||||
extra,
|
||||
}),
|
||||
plugins: [sandboxPlugin, idPrefixPlugin] as any[],
|
||||
subscriptions: {
|
||||
'graphql-ws': {
|
||||
path: '/graphql',
|
||||
imports: [PluginModule],
|
||||
inject: [PluginService],
|
||||
useFactory: async (pluginService: PluginService) => {
|
||||
const plugins = await pluginService.getGraphQLConfiguration();
|
||||
return {
|
||||
introspection: getters.config()?.local?.sandbox === 'yes',
|
||||
playground: false,
|
||||
context: ({ req, connectionParams, extra }: any) => ({
|
||||
req,
|
||||
connectionParams,
|
||||
extra,
|
||||
}),
|
||||
plugins: [sandboxPlugin, idPrefixPlugin] as any[],
|
||||
subscriptions: {
|
||||
'graphql-ws': {
|
||||
path: '/graphql',
|
||||
},
|
||||
},
|
||||
},
|
||||
path: '/graphql',
|
||||
typeDefs: print(await loadTypeDefs()),
|
||||
resolvers: {
|
||||
JSON: JSONResolver,
|
||||
Long: GraphQLLong,
|
||||
UUID: UUIDResolver,
|
||||
DateTime: DateTimeResolver,
|
||||
Port: PortResolver,
|
||||
URL: URLResolver,
|
||||
},
|
||||
validationRules: [NoUnusedVariablesRule],
|
||||
}),
|
||||
path: '/graphql',
|
||||
typeDefs: [print(await loadTypeDefs([plugins.typeDefs]))],
|
||||
resolvers: {
|
||||
JSON: JSONResolver,
|
||||
Long: GraphQLLong,
|
||||
UUID: UUIDResolver,
|
||||
DateTime: DateTimeResolver,
|
||||
Port: PortResolver,
|
||||
URL: URLResolver,
|
||||
...plugins.resolvers,
|
||||
},
|
||||
validationRules: [NoUnusedVariablesRule],
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js';
|
||||
import { Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
@Resolver('ArrayMutations')
|
||||
export class ArrayMutationsResolver {
|
||||
constructor(private readonly arrayService: ArrayService) {}
|
||||
|
||||
@ResolveField('setState')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async setState(@Args('input') input: ArrayStateInput) {
|
||||
return this.arrayService.updateArrayState(input);
|
||||
}
|
||||
|
||||
@ResolveField('addDiskToArray')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async addDiskToArray(@Args('input') input: ArrayDiskInput) {
|
||||
return this.arrayService.addDiskToArray(input);
|
||||
}
|
||||
|
||||
@ResolveField('removeDiskFromArray')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async removeDiskFromArray(@Args('input') input: ArrayDiskInput) {
|
||||
return this.arrayService.removeDiskFromArray(input);
|
||||
}
|
||||
|
||||
@ResolveField('mountArrayDisk')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async mountArrayDisk(@Args('id') id: string) {
|
||||
return this.arrayService.mountArrayDisk(id);
|
||||
}
|
||||
|
||||
@ResolveField('unmountArrayDisk')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async unmountArrayDisk(@Args('id') id: string) {
|
||||
return this.arrayService.unmountArrayDisk(id);
|
||||
}
|
||||
|
||||
@ResolveField('clearArrayDiskStatistics')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.ARRAY,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async clearArrayDiskStatistics(@Args('id') id: string) {
|
||||
return this.arrayService.clearArrayDiskStatistics(id);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,35 @@
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
describe('ArrayResolver', () => {
|
||||
let resolver: ArrayResolver;
|
||||
let arrayService: ArrayService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ArrayResolver],
|
||||
providers: [
|
||||
ArrayResolver,
|
||||
{
|
||||
provide: ArrayService,
|
||||
useValue: {
|
||||
updateArrayState: vi.fn(),
|
||||
addDiskToArray: vi.fn(),
|
||||
removeDiskFromArray: vi.fn(),
|
||||
mountArrayDisk: vi.fn(),
|
||||
unmountArrayDisk: vi.fn(),
|
||||
clearArrayDiskStatistics: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<ArrayResolver>(ArrayResolver);
|
||||
arrayService = module.get<ArrayService>(ArrayService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
||||
@@ -6,9 +6,12 @@ import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
||||
import { Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { store } from '@app/store/index.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
@Resolver('Array')
|
||||
export class ArrayResolver {
|
||||
constructor(private readonly arrayService: ArrayService) {}
|
||||
|
||||
@Query()
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.READ,
|
||||
|
||||
210
api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
Normal file
210
api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ArrayDiskInput, ArrayStateInput } from '@app/graphql/generated/api/types.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { ArrayState, ArrayStateInputState } from '@app/graphql/generated/api/types.js';
|
||||
import { getters } from '@app/store/index.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
|
||||
vi.mock('@app/core/utils/clients/emcmd.js', () => ({
|
||||
emcmd: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@app/store/index.js', () => ({
|
||||
getters: {
|
||||
emhttp: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@app/core/modules/array/get-array-data.js', () => ({
|
||||
getArrayData: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ArrayService', () => {
|
||||
let service: ArrayService;
|
||||
let mockArrayData: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ArrayService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ArrayService>(ArrayService);
|
||||
|
||||
// Mock getters.emhttp()
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STOPPED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
// Mock getArrayData
|
||||
mockArrayData = {
|
||||
id: 'array',
|
||||
state: ArrayState.STOPPED,
|
||||
capacity: {
|
||||
kilobytes: {
|
||||
free: '1000',
|
||||
used: '1000',
|
||||
total: '2000',
|
||||
},
|
||||
disks: {
|
||||
free: '10',
|
||||
used: '5',
|
||||
total: '15',
|
||||
},
|
||||
},
|
||||
boot: null,
|
||||
parities: [],
|
||||
disks: [],
|
||||
caches: [],
|
||||
};
|
||||
vi.mocked(getArrayData).mockReturnValue(mockArrayData);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should update array state', async () => {
|
||||
const input: ArrayStateInput = {
|
||||
desiredState: ArrayStateInputState.START,
|
||||
};
|
||||
const result = await service.updateArrayState(input);
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
cmdStart: 'Start',
|
||||
startState: 'STOPPED',
|
||||
});
|
||||
});
|
||||
|
||||
it('should add disk to array', async () => {
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
const result = await service.addDiskToArray(input);
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
changeDevice: 'apply',
|
||||
'slotId.1': 'test-disk',
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove disk from array', async () => {
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
const result = await service.removeDiskFromArray(input);
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
changeDevice: 'apply',
|
||||
'slotId.1': '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should mount array disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await service.mountArrayDisk('test-disk');
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
mount: 'apply',
|
||||
'diskId.test-disk': '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should unmount array disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await service.unmountArrayDisk('test-disk');
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
unmount: 'apply',
|
||||
'diskId.test-disk': '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear array disk statistics', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await service.clearArrayDiskStatistics('test-disk');
|
||||
expect(result).toEqual(mockArrayData);
|
||||
expect(emcmd).toHaveBeenCalledWith({
|
||||
clearStats: 'apply',
|
||||
'diskId.test-disk': '1',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when array is running for add disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
await expect(service.addDiskToArray(input)).rejects.toThrow(
|
||||
'Array needs to be stopped before any changes can occur.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is running for remove disk', async () => {
|
||||
// Mock array as running
|
||||
vi.mocked(getters.emhttp).mockReturnValue({
|
||||
var: {
|
||||
mdState: ArrayState.STARTED,
|
||||
},
|
||||
} as any);
|
||||
|
||||
const input: ArrayDiskInput = {
|
||||
id: 'test-disk',
|
||||
slot: 1,
|
||||
};
|
||||
await expect(service.removeDiskFromArray(input)).rejects.toThrow(
|
||||
'Array needs to be stopped before any changes can occur.'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is not running for mount disk', async () => {
|
||||
await expect(service.mountArrayDisk('test-disk')).rejects.toThrow(
|
||||
'Array must be running to mount disks'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is not running for unmount disk', async () => {
|
||||
await expect(service.unmountArrayDisk('test-disk')).rejects.toThrow(
|
||||
'Array must be running to unmount disks'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when array is not running for clear disk statistics', async () => {
|
||||
await expect(service.clearArrayDiskStatistics('test-disk')).rejects.toThrow(
|
||||
'Array must be running to clear disk statistics'
|
||||
);
|
||||
});
|
||||
});
|
||||
145
api/src/unraid-api/graph/resolvers/array/array.service.ts
Normal file
145
api/src/unraid-api/graph/resolvers/array/array.service.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
|
||||
import { capitalCase, constantCase } from 'change-case';
|
||||
|
||||
import type { ArrayDiskInput, ArrayStateInput, ArrayType } from '@app/graphql/generated/api/types.js';
|
||||
import { AppError } from '@app/core/errors/app-error.js';
|
||||
import { ArrayRunningError } from '@app/core/errors/array-running-error.js';
|
||||
import { getArrayData } from '@app/core/modules/array/get-array-data.js';
|
||||
import { emcmd } from '@app/core/utils/clients/emcmd.js';
|
||||
import { arrayIsRunning as arrayIsRunningUtil } from '@app/core/utils/index.js';
|
||||
import {
|
||||
ArrayPendingState,
|
||||
ArrayState,
|
||||
ArrayStateInputState,
|
||||
} from '@app/graphql/generated/api/types.js';
|
||||
|
||||
@Injectable()
|
||||
export class ArrayService {
|
||||
private pendingState: ArrayPendingState | null = null;
|
||||
|
||||
/**
|
||||
* Is the array running?
|
||||
* @todo Refactor this to include this util in the service directly
|
||||
*/
|
||||
private arrayIsRunning() {
|
||||
return arrayIsRunningUtil();
|
||||
}
|
||||
|
||||
async updateArrayState({ desiredState }: ArrayStateInput): Promise<ArrayType> {
|
||||
const startState = this.arrayIsRunning() ? ArrayState.STARTED : ArrayState.STOPPED;
|
||||
const pendingState =
|
||||
desiredState === ArrayStateInputState.STOP
|
||||
? ArrayPendingState.STOPPING
|
||||
: ArrayPendingState.STARTING;
|
||||
|
||||
// Prevent this running multiple times at once
|
||||
if (this.pendingState) {
|
||||
throw new BadRequestException(
|
||||
new AppError(`Array state is still being updated. Changing to ${pendingState}`)
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent starting/stopping array when it's already in the same state
|
||||
if (
|
||||
(this.arrayIsRunning() && desiredState === ArrayStateInputState.START) ||
|
||||
(!this.arrayIsRunning() && desiredState === ArrayStateInputState.STOP)
|
||||
) {
|
||||
throw new BadRequestException(new AppError(`The array is already ${startState}`));
|
||||
}
|
||||
|
||||
// Set lock then start/stop array
|
||||
this.pendingState = pendingState;
|
||||
const command = {
|
||||
[`cmd${capitalCase(desiredState)}`]: capitalCase(desiredState),
|
||||
startState: constantCase(startState),
|
||||
};
|
||||
|
||||
try {
|
||||
await emcmd(command);
|
||||
} finally {
|
||||
this.pendingState = null;
|
||||
}
|
||||
|
||||
// Get new array JSON
|
||||
const array = getArrayData();
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
async addDiskToArray(input: ArrayDiskInput): Promise<ArrayType> {
|
||||
if (this.arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { id: diskId, slot: preferredSlot } = input;
|
||||
const slot = preferredSlot?.toString() ?? '';
|
||||
|
||||
// Add disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slot}`]: diskId,
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async removeDiskFromArray(input: ArrayDiskInput): Promise<ArrayType> {
|
||||
if (this.arrayIsRunning()) {
|
||||
throw new ArrayRunningError();
|
||||
}
|
||||
|
||||
const { slot } = input;
|
||||
const slotStr = slot?.toString() ?? '';
|
||||
|
||||
// Remove disk
|
||||
await emcmd({
|
||||
changeDevice: 'apply',
|
||||
[`slotId.${slotStr}`]: '',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async mountArrayDisk(id: string): Promise<ArrayType> {
|
||||
if (!this.arrayIsRunning()) {
|
||||
throw new BadRequestException('Array must be running to mount disks');
|
||||
}
|
||||
|
||||
// Mount disk
|
||||
await emcmd({
|
||||
mount: 'apply',
|
||||
[`diskId.${id}`]: '1',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async unmountArrayDisk(id: string): Promise<ArrayType> {
|
||||
if (!this.arrayIsRunning()) {
|
||||
throw new BadRequestException('Array must be running to unmount disks');
|
||||
}
|
||||
|
||||
// Unmount disk
|
||||
await emcmd({
|
||||
unmount: 'apply',
|
||||
[`diskId.${id}`]: '1',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
|
||||
async clearArrayDiskStatistics(id: string): Promise<ArrayType> {
|
||||
if (!this.arrayIsRunning()) {
|
||||
throw new BadRequestException('Array must be running to clear disk statistics');
|
||||
}
|
||||
|
||||
// Clear disk statistics
|
||||
await emcmd({
|
||||
clearStats: 'apply',
|
||||
[`diskId.${id}`]: '1',
|
||||
});
|
||||
|
||||
return getArrayData();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import type { AllowedOriginInput } from '@app/graphql/generated/api/types.js';
|
||||
import { getAllowedOrigins } from '@app/common/allowed-origins.js';
|
||||
import { Config, ConfigErrorState, Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { Config, Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { getters, store } from '@app/store/index.js';
|
||||
import { updateAllowedOrigins } from '@app/store/modules/config.js';
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
|
||||
import { ContainerState } from '@app/graphql/generated/api/types.js';
|
||||
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
|
||||
describe('DockerMutationsResolver', () => {
|
||||
let resolver: DockerMutationsResolver;
|
||||
let dockerService: DockerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DockerMutationsResolver,
|
||||
{
|
||||
provide: DockerService,
|
||||
useValue: {
|
||||
startContainer: vi.fn(),
|
||||
stopContainer: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<DockerMutationsResolver>(DockerMutationsResolver);
|
||||
dockerService = module.get<DockerService>(DockerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(resolver).toBeDefined();
|
||||
});
|
||||
|
||||
it('should start container', async () => {
|
||||
const mockContainer: DockerContainer = {
|
||||
id: '1',
|
||||
autoStart: false,
|
||||
command: 'test',
|
||||
created: 1234567890,
|
||||
image: 'test-image',
|
||||
imageId: 'test-image-id',
|
||||
ports: [],
|
||||
state: ContainerState.RUNNING,
|
||||
status: 'Up 2 hours',
|
||||
};
|
||||
vi.mocked(dockerService.startContainer).mockResolvedValue(mockContainer);
|
||||
|
||||
const result = await resolver.startContainer('1');
|
||||
expect(result).toEqual(mockContainer);
|
||||
expect(dockerService.startContainer).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('should stop container', async () => {
|
||||
const mockContainer: DockerContainer = {
|
||||
id: '1',
|
||||
autoStart: false,
|
||||
command: 'test',
|
||||
created: 1234567890,
|
||||
image: 'test-image',
|
||||
imageId: 'test-image-id',
|
||||
ports: [],
|
||||
state: ContainerState.EXITED,
|
||||
status: 'Exited',
|
||||
};
|
||||
vi.mocked(dockerService.stopContainer).mockResolvedValue(mockContainer);
|
||||
|
||||
const result = await resolver.stopContainer('1');
|
||||
expect(result).toEqual(mockContainer);
|
||||
expect(dockerService.stopContainer).toHaveBeenCalledWith('1');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Args, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
|
||||
@Resolver('DockerMutations')
|
||||
export class DockerMutationsResolver {
|
||||
constructor(private readonly dockerService: DockerService) {}
|
||||
|
||||
@ResolveField('startContainer')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.DOCKER,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async startContainer(@Args('id') id: string) {
|
||||
return this.dockerService.startContainer(id);
|
||||
}
|
||||
|
||||
@ResolveField('stopContainer')
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.UPDATE,
|
||||
resource: Resource.DOCKER,
|
||||
possession: AuthPossession.ANY,
|
||||
})
|
||||
public async stopContainer(@Args('id') id: string) {
|
||||
return this.dockerService.stopContainer(id);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,77 @@
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
|
||||
import { ContainerState } from '@app/graphql/generated/api/types.js';
|
||||
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
|
||||
describe('DockerResolver', () => {
|
||||
let resolver: DockerResolver;
|
||||
let dockerService: DockerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [DockerResolver],
|
||||
providers: [
|
||||
DockerResolver,
|
||||
{
|
||||
provide: DockerService,
|
||||
useValue: {
|
||||
getContainers: vi.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
resolver = module.get<DockerResolver>(DockerResolver);
|
||||
dockerService = module.get<DockerService>(DockerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(resolver).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return docker object with id', () => {
|
||||
const result = resolver.docker();
|
||||
expect(result).toEqual({ id: 'docker' });
|
||||
});
|
||||
|
||||
it('should return containers from service', async () => {
|
||||
const mockContainers: DockerContainer[] = [
|
||||
{
|
||||
id: '1',
|
||||
autoStart: false,
|
||||
command: 'test',
|
||||
created: 1234567890,
|
||||
image: 'test-image',
|
||||
imageId: 'test-image-id',
|
||||
ports: [],
|
||||
state: ContainerState.EXITED,
|
||||
status: 'Exited',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
autoStart: true,
|
||||
command: 'test2',
|
||||
created: 1234567891,
|
||||
image: 'test-image2',
|
||||
imageId: 'test-image-id2',
|
||||
ports: [],
|
||||
state: ContainerState.RUNNING,
|
||||
status: 'Up 2 hours',
|
||||
},
|
||||
];
|
||||
vi.mocked(dockerService.getContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const result = await resolver.containers();
|
||||
expect(result).toEqual(mockContainers);
|
||||
expect(dockerService.getContainers).toHaveBeenCalledWith({ useCache: false });
|
||||
});
|
||||
|
||||
it('should return mutations object with id', () => {
|
||||
const result = resolver.mutations();
|
||||
expect(result).toEqual({ id: 'docker-mutations' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,9 +3,12 @@ import { Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
|
||||
|
||||
import { Resource } from '@app/graphql/generated/api/types.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
|
||||
@Resolver('Docker')
|
||||
export class DockerResolver {
|
||||
constructor(private readonly dockerService: DockerService) {}
|
||||
|
||||
@UsePermissions({
|
||||
action: AuthActionVerb.READ,
|
||||
resource: Resource.DOCKER,
|
||||
@@ -25,10 +28,13 @@ export class DockerResolver {
|
||||
})
|
||||
@ResolveField()
|
||||
public async containers() {
|
||||
const { getDockerContainers } = await import(
|
||||
'@app/core/modules/docker/get-docker-containers.js'
|
||||
);
|
||||
return this.dockerService.getContainers({ useCache: false });
|
||||
}
|
||||
|
||||
return getDockerContainers({ useCache: false });
|
||||
@ResolveField()
|
||||
public mutations() {
|
||||
return {
|
||||
id: 'docker-mutations',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
125
api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
Normal file
125
api/src/unraid-api/graph/resolvers/docker/docker.service.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
|
||||
import { getDockerContainers } from '@app/core/modules/docker/get-docker-containers.js';
|
||||
import { docker } from '@app/core/utils/clients/docker.js';
|
||||
import { ContainerState } from '@app/graphql/generated/api/types.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
|
||||
vi.mock('@app/core/utils/clients/docker.js', () => ({
|
||||
docker: {
|
||||
getContainer: vi.fn(),
|
||||
listContainers: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@app/core/modules/docker/get-docker-containers.js', () => ({
|
||||
getDockerContainers: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('DockerService', () => {
|
||||
let service: DockerService;
|
||||
let mockContainer: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [DockerService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DockerService>(DockerService);
|
||||
|
||||
mockContainer = {
|
||||
start: vi.fn(),
|
||||
stop: vi.fn(),
|
||||
};
|
||||
vi.mocked(docker.getContainer).mockReturnValue(mockContainer as any);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should get containers', async () => {
|
||||
const mockContainers: DockerContainer[] = [
|
||||
{
|
||||
id: '1',
|
||||
autoStart: false,
|
||||
command: 'test',
|
||||
created: 1234567890,
|
||||
image: 'test-image',
|
||||
imageId: 'test-image-id',
|
||||
ports: [],
|
||||
state: ContainerState.EXITED,
|
||||
status: 'Exited',
|
||||
},
|
||||
];
|
||||
vi.mocked(getDockerContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const result = await service.getContainers({ useCache: false });
|
||||
expect(result).toEqual(mockContainers);
|
||||
expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false });
|
||||
});
|
||||
|
||||
it('should start container', async () => {
|
||||
const mockContainers: DockerContainer[] = [
|
||||
{
|
||||
id: '1',
|
||||
autoStart: false,
|
||||
command: 'test',
|
||||
created: 1234567890,
|
||||
image: 'test-image',
|
||||
imageId: 'test-image-id',
|
||||
ports: [],
|
||||
state: ContainerState.RUNNING,
|
||||
status: 'Up 2 hours',
|
||||
},
|
||||
];
|
||||
vi.mocked(getDockerContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const result = await service.startContainer('1');
|
||||
expect(result).toEqual(mockContainers[0]);
|
||||
expect(docker.getContainer).toHaveBeenCalledWith('1');
|
||||
expect(mockContainer.start).toHaveBeenCalled();
|
||||
expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false });
|
||||
});
|
||||
|
||||
it('should stop container', async () => {
|
||||
const mockContainers: DockerContainer[] = [
|
||||
{
|
||||
id: '1',
|
||||
autoStart: false,
|
||||
command: 'test',
|
||||
created: 1234567890,
|
||||
image: 'test-image',
|
||||
imageId: 'test-image-id',
|
||||
ports: [],
|
||||
state: ContainerState.EXITED,
|
||||
status: 'Exited',
|
||||
},
|
||||
];
|
||||
vi.mocked(getDockerContainers).mockResolvedValue(mockContainers);
|
||||
|
||||
const result = await service.stopContainer('1');
|
||||
expect(result).toEqual(mockContainers[0]);
|
||||
expect(docker.getContainer).toHaveBeenCalledWith('1');
|
||||
expect(mockContainer.stop).toHaveBeenCalled();
|
||||
expect(getDockerContainers).toHaveBeenCalledWith({ useCache: false });
|
||||
});
|
||||
|
||||
it('should throw error if container not found after start', async () => {
|
||||
vi.mocked(getDockerContainers).mockResolvedValue([]);
|
||||
|
||||
await expect(service.startContainer('1')).rejects.toThrow(
|
||||
'Container 1 not found after starting'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if container not found after stop', async () => {
|
||||
vi.mocked(getDockerContainers).mockResolvedValue([]);
|
||||
|
||||
await expect(service.stopContainer('1')).rejects.toThrow('Container 1 not found after stopping');
|
||||
});
|
||||
});
|
||||
37
api/src/unraid-api/graph/resolvers/docker/docker.service.ts
Normal file
37
api/src/unraid-api/graph/resolvers/docker/docker.service.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import type { DockerContainer } from '@app/graphql/generated/api/types.js';
|
||||
import {
|
||||
ContainerListingOptions,
|
||||
getDockerContainers,
|
||||
} from '@app/core/modules/docker/get-docker-containers.js';
|
||||
import { docker } from '@app/core/utils/clients/docker.js';
|
||||
|
||||
@Injectable()
|
||||
export class DockerService {
|
||||
public async getContainers({ useCache }: ContainerListingOptions): Promise<DockerContainer[]> {
|
||||
return getDockerContainers({ useCache });
|
||||
}
|
||||
|
||||
public async startContainer(id: string): Promise<DockerContainer> {
|
||||
const container = docker.getContainer(id);
|
||||
await container.start();
|
||||
const containers = await this.getContainers({ useCache: false });
|
||||
const updatedContainer = containers.find((c) => c.id === id);
|
||||
if (!updatedContainer) {
|
||||
throw new Error(`Container ${id} not found after starting`);
|
||||
}
|
||||
return updatedContainer;
|
||||
}
|
||||
|
||||
public async stopContainer(id: string): Promise<DockerContainer> {
|
||||
const container = docker.getContainer(id);
|
||||
await container.stop();
|
||||
const containers = await this.getContainers({ useCache: false });
|
||||
const updatedContainer = containers.find((c) => c.id === id);
|
||||
if (!updatedContainer) {
|
||||
throw new Error(`Container ${id} not found after stopping`);
|
||||
}
|
||||
return updatedContainer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ResolveField, Resolver } from '@nestjs/graphql';
|
||||
|
||||
@Resolver('Mutation')
|
||||
export class MutationResolver {
|
||||
@ResolveField()
|
||||
public async array() {
|
||||
return {
|
||||
__typename: 'ArrayMutations',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,22 @@ import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '@app/unraid-api/auth/auth.module.js';
|
||||
import { ConnectSettingsService } from '@app/unraid-api/graph/connect/connect-settings.service.js';
|
||||
import { ApiKeyResolver } from '@app/unraid-api/graph/resolvers/api-key/api-key.resolver.js';
|
||||
import { ArrayMutationsResolver } from '@app/unraid-api/graph/resolvers/array/array.mutations.resolver.js';
|
||||
import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver.js';
|
||||
import { ArrayService } from '@app/unraid-api/graph/resolvers/array/array.service.js';
|
||||
import { CloudResolver } from '@app/unraid-api/graph/resolvers/cloud/cloud.resolver.js';
|
||||
import { ConfigResolver } from '@app/unraid-api/graph/resolvers/config/config.resolver.js';
|
||||
import { DisksResolver } from '@app/unraid-api/graph/resolvers/disks/disks.resolver.js';
|
||||
import { DisplayResolver } from '@app/unraid-api/graph/resolvers/display/display.resolver.js';
|
||||
import { DockerMutationsResolver } from '@app/unraid-api/graph/resolvers/docker/docker.mutations.resolver.js';
|
||||
import { DockerResolver } from '@app/unraid-api/graph/resolvers/docker/docker.resolver.js';
|
||||
import { DockerService } from '@app/unraid-api/graph/resolvers/docker/docker.service.js';
|
||||
import { FlashResolver } from '@app/unraid-api/graph/resolvers/flash/flash.resolver.js';
|
||||
import { InfoResolver } from '@app/unraid-api/graph/resolvers/info/info.resolver.js';
|
||||
import { LogsResolver } from '@app/unraid-api/graph/resolvers/logs/logs.resolver.js';
|
||||
import { LogsService } from '@app/unraid-api/graph/resolvers/logs/logs.service.js';
|
||||
import { MeResolver } from '@app/unraid-api/graph/resolvers/me/me.resolver.js';
|
||||
import { MutationResolver } from '@app/unraid-api/graph/resolvers/mutation/mutation.resolver.js';
|
||||
import { NotificationsResolver } from '@app/unraid-api/graph/resolvers/notifications/notifications.resolver.js';
|
||||
import { NotificationsService } from '@app/unraid-api/graph/resolvers/notifications/notifications.service.js';
|
||||
import { OnlineResolver } from '@app/unraid-api/graph/resolvers/online/online.resolver.js';
|
||||
@@ -27,13 +32,18 @@ import { VmsResolver } from '@app/unraid-api/graph/resolvers/vms/vms.resolver.js
|
||||
imports: [AuthModule],
|
||||
providers: [
|
||||
ArrayResolver,
|
||||
ArrayMutationsResolver,
|
||||
ArrayService,
|
||||
ApiKeyResolver,
|
||||
CloudResolver,
|
||||
ConfigResolver,
|
||||
DisksResolver,
|
||||
DisplayResolver,
|
||||
DockerResolver,
|
||||
DockerMutationsResolver,
|
||||
DockerService,
|
||||
FlashResolver,
|
||||
MutationResolver,
|
||||
InfoResolver,
|
||||
NotificationsResolver,
|
||||
OnlineResolver,
|
||||
|
||||
@@ -52,7 +52,13 @@ export async function bootstrapNestServer(): Promise<NestFastifyApplication> {
|
||||
origin: true, // Allows all origins
|
||||
credentials: true,
|
||||
methods: ['GET', 'PUT', 'POST', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
||||
allowedHeaders: [
|
||||
'Content-Type',
|
||||
'Authorization',
|
||||
'X-Requested-With',
|
||||
'X-CSRF-TOKEN',
|
||||
'X-API-KEY',
|
||||
],
|
||||
});
|
||||
|
||||
// Setup Nestjs Pino Logger
|
||||
|
||||
61
api/src/unraid-api/plugin/plugin.interface.ts
Normal file
61
api/src/unraid-api/plugin/plugin.interface.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Logger, Type } from '@nestjs/common';
|
||||
|
||||
import { CommandRunner } from 'nest-commander';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ApiStore } from '@app/store/index.js';
|
||||
|
||||
export interface PluginMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const asyncArray = () => z.function().returns(z.promise(z.array(z.any())));
|
||||
const asyncString = () => z.function().returns(z.promise(z.string()));
|
||||
const asyncVoid = () => z.function().returns(z.promise(z.void()));
|
||||
|
||||
// GraphQL resolver type definitions
|
||||
const resolverFunction = z
|
||||
.function()
|
||||
.args(
|
||||
z.any().optional(), // parent
|
||||
z.any().optional(), // args
|
||||
z.any().optional(), // context
|
||||
z.any().optional() // info
|
||||
)
|
||||
.returns(z.any());
|
||||
|
||||
const resolverFieldMap = z.record(z.string(), resolverFunction);
|
||||
const resolverTypeMap = z.record(
|
||||
z.enum(['Query', 'Mutation', 'Subscription']).or(z.string()),
|
||||
resolverFieldMap
|
||||
);
|
||||
const asyncResolver = () => z.function().returns(z.promise(resolverTypeMap));
|
||||
|
||||
/** Warning: unstable API. The config mechanism and API may soon change. */
|
||||
export const apiPluginSchema = z.object({
|
||||
_type: z.literal('UnraidApiPlugin'),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
commands: z.array(z.custom<Type<CommandRunner>>()),
|
||||
registerGraphQLResolvers: asyncResolver().optional(),
|
||||
registerGraphQLTypeDefs: asyncString().optional(),
|
||||
registerRESTControllers: asyncArray().optional(),
|
||||
registerRESTRoutes: asyncArray().optional(),
|
||||
registerServices: asyncArray().optional(),
|
||||
registerCronJobs: asyncArray().optional(),
|
||||
// These schema definitions are picked up as nest modules as well.
|
||||
onModuleInit: asyncVoid().optional(),
|
||||
onModuleDestroy: asyncVoid().optional(),
|
||||
});
|
||||
|
||||
/** Warning: unstable API. The config mechanism and API may soon change. */
|
||||
export type ApiPluginDefinition = z.infer<typeof apiPluginSchema>;
|
||||
|
||||
// todo: the blocker to publishing this type is the 'ApiStore' type.
|
||||
// It pulls in a lot of irrelevant types (e.g. graphql types) and triggers js transpilation of everything related to the store.
|
||||
// If we can isolate the type, we can publish it to npm and developers can use it as a dev dependency.
|
||||
/**
|
||||
* Represents a subclass of UnraidAPIPlugin that can be instantiated.
|
||||
*/
|
||||
export type ConstructablePlugin = (options: { store: ApiStore; logger: Logger }) => ApiPluginDefinition;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user