From 6f0673f428b77d7ec37e705b3532bfcee4b5613b Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Tue, 12 Dec 2023 13:59:59 -0500 Subject: [PATCH] feat: nestjs initial query implementation (#748) * feat: nestjs initial query implementation * feat: more permissions and resolver cleanup * fix: back to ubuntu to remain compatible with pkg docker building * feat: listen on socket as well as ports * feat: swap to bookworm instead of ubuntu --- api/.dockerignore | 1 + api/Dockerfile | 11 +- api/dev/Unraid.net/myservers.cfg | 3 +- api/dev/states/myservers.cfg | 5 +- api/package-lock.json | 2128 +++++++++++++++-- api/package.json | 30 +- .../common/dashboard/generate-data.test.ts | 17 +- .../default-permissions.test.ts.snap | 360 --- .../__snapshots__/permissions.test.ts.snap | 403 ++++ api/src/__test__/core/permissions.test.ts | 6 + .../files/config-file-normalizer.test.ts | 167 ++ api/src/__test__/mothership/index.test.ts | 48 + api/src/cli/commands/report.ts | 7 - api/src/cli/index.ts | 118 +- api/src/common/allowed-origins.ts | 4 +- api/src/common/dashboard/generate-data.ts | 196 +- api/src/core/default-permissions.ts | 122 - api/src/core/log.ts | 213 +- api/src/core/logrotate/setup-logrotate.ts | 20 + api/src/core/modules/add-license-key.ts | 126 - api/src/core/modules/add-user.ts | 1 - .../modules/docker/get-docker-networks.ts | 3 +- api/src/core/modules/get-array.ts | 25 - api/src/core/modules/get-disks.ts | 10 - .../core/modules/get-unassigned-devices.ts | 28 - api/src/core/modules/get-vars.ts | 26 - api/src/core/modules/index.ts | 23 + api/src/core/modules/services/get-emhttpd.ts | 2 +- api/src/core/modules/shares/name/get-share.ts | 3 +- api/src/core/modules/vms/get-domains.ts | 17 +- api/src/core/permissions.ts | 217 +- api/src/core/pubsub.ts | 13 +- api/src/core/utils/index.ts | 10 + api/src/core/utils/misc/load-state.ts | 4 +- .../core/utils/misc/send-form-to-keyserver.ts | 35 + api/src/environment.ts | 5 +- api/src/graphql/express/get-logs.ts | 52 - api/src/graphql/func-directive.ts | 94 - api/src/graphql/generated/api/operations.ts | 85 +- api/src/graphql/generated/api/types.ts | 237 +- api/src/graphql/generated/client/graphql.ts | 2 +- api/src/graphql/index.ts | 15 - .../mutation/connect/connect-sign-in.ts | 41 +- .../mutation/connect/connect-sign-out.ts | 19 - .../connect/set-additional-allowed-origins.ts | 20 - api/src/graphql/resolvers/mutation/index.ts | 12 - api/src/graphql/resolvers/query/config.ts | 23 - api/src/graphql/resolvers/query/docker.ts | 16 - api/src/graphql/resolvers/query/index.ts | 40 - api/src/graphql/resolvers/query/info.ts | 96 +- .../graphql/resolvers/query/notifications.ts | 42 - api/src/graphql/resolvers/query/online.ts | 1 - .../graphql/resolvers/query/registration.ts | 40 - api/src/graphql/resolvers/query/server.ts | 16 - api/src/graphql/resolvers/query/servers.ts | 29 - api/src/graphql/resolvers/query/vms.ts | 1 - api/src/graphql/resolvers/resolvers.ts | 28 - .../resolvers/subscription/dashboard.ts | 8 +- .../graphql/resolvers/subscription/index.ts | 69 - .../graphql/resolvers/subscription/network.ts | 591 +++-- .../remote-graphql/remote-query.ts | 14 +- api/src/graphql/resolvers/user-account.ts | 6 - api/src/graphql/schema.ts | 10 + .../schema/types/apikeys/apikey.graphql | 42 + .../graphql/schema/types/array/array.graphql | 8 +- .../graphql/schema/types/array/parity.graphql | 26 + api/src/graphql/schema/types/base.graphql | 28 + .../graphql/schema/types/disks/disk.graphql | 65 + .../schema/types/docker/network.graphql | 29 + .../schema/types/servers/server.graphql | 34 + .../graphql/schema/types/shares/share.graphql | 2 +- .../unassigned-device.graphql | 62 + api/src/graphql/schema/types/users/me.graphql | 17 + .../graphql/schema/types/users/user.graphql | 50 + .../graphql/schema/types/vars/vars.graphql | 2 +- api/src/graphql/schema/utils.ts | 4 +- api/src/graphql/types.ts | 46 - api/src/index.ts | 26 +- .../validate-api-key-with-keyserver.ts | 6 +- api/src/mothership/graphql-client.ts | 8 +- api/src/mothership/jobs/token-refresh-jobs.ts | 56 + api/src/mothership/subscribe-to-mothership.ts | 135 ++ api/src/mothership/utils/delay-function.ts | 25 + api/src/originMiddleware.ts | 76 - api/src/server.ts | 355 --- api/src/store/actions/setup-remote-access.ts | 62 + .../store/listeners/array-event-listener.ts | 51 + api/src/store/listeners/config-listener.ts | 77 + .../store/listeners/server-state-listener.ts | 40 + api/src/store/listeners/upnp-listener.ts | 45 + api/src/store/modules/config.ts | 17 +- api/src/store/store-sync.ts | 3 - api/src/store/sync/info-apps-sync.ts | 44 + api/src/store/sync/registration-sync.ts | 62 + api/src/store/watch/docker-watch.ts | 2 - api/src/unraid-api/app/app.module.ts | 40 + api/src/unraid-api/auth/auth.guard.ts | 80 + api/src/unraid-api/auth/auth.module.ts | 11 + api/src/unraid-api/auth/auth.service.spec.ts | 18 + api/src/unraid-api/auth/auth.service.ts | 17 + api/src/unraid-api/auth/header.strategy.ts | 26 + api/src/unraid-api/auth/public.decorator.ts | 4 + .../exceptions/graphql-exceptions.filter.ts | 28 + .../exceptions/http-exceptions.filter.ts | 40 + api/src/unraid-api/graph/graph.module.ts | 51 + .../resolvers/array/array.resolver.spec.ts | 18 + .../graph/resolvers/array/array.resolver.ts | 28 + .../resolvers/cloud/cloud.resolver.spec.ts | 18 + .../graph/resolvers/cloud/cloud.resolver.ts | 88 + .../resolvers/config/config.resolver.spec.ts | 18 + .../graph/resolvers/config/config.resolver.ts | 37 + .../resolvers/disks/disks.resolver.spec.ts | 18 + .../graph/resolvers/disks/disks.resolver.ts | 19 + .../display/display.resolver.spec.ts | 18 + .../resolvers/display/display.resolver.ts | 114 + .../docker-containers.resolver.spec.ts | 18 + .../docker-containers.resolver.ts | 16 + .../resolvers/flash/flash.resolver.spec.ts | 18 + .../graph/resolvers/flash/flash.resolver.ts | 22 + .../resolvers/info/info.resolver.spec.ts | 18 + .../graph/resolvers/info/info.resolver.ts | 86 + .../notifications.resolver.spec.ts | 18 + .../notifications/notifications.resolver.ts | 99 + .../resolvers/online/online.resolver.spec.ts | 18 + .../graph/resolvers/online/online.resolver.ts | 15 + .../resolvers/owner/owner.resolver.spec.ts | 18 + .../graph/resolvers/owner/owner.resolver.ts | 40 + .../registration.resolver.spec.ts | 18 + .../registration/registration.resolver.ts | 54 + .../graph/resolvers/resolvers.module.ts | 37 + .../resolvers/servers/server.resolver.spec.ts | 18 + .../resolvers/servers/server.resolver.ts | 39 + .../resolvers/vars/vars.resolver.spec.ts | 18 + .../graph/resolvers/vars/vars.resolver.ts | 16 + .../graph/resolvers/vms/vms.resolver.spec.ts | 18 + .../graph/resolvers/vms/vms.resolver.ts | 30 + api/src/unraid-api/main.ts | 72 + .../unraid-api/observers/shutdown.observer.ts | 37 + api/src/unraid-api/rest/rest.controller.ts | 56 + api/src/unraid-api/rest/rest.module.ts | 10 + api/src/unraid-api/rest/rest.service.spec.ts | 18 + api/src/unraid-api/rest/rest.service.ts | 78 + api/src/unraid-api/users/users.module.ts | 8 + api/src/unraid-api/users/users.service.ts | 49 + api/src/ws.ts | 34 + api/tsconfig.json | 42 + api/vite.config.ts | 26 + 147 files changed, 6332 insertions(+), 2942 deletions(-) create mode 100644 api/.dockerignore delete mode 100644 api/src/__test__/core/__snapshots__/default-permissions.test.ts.snap create mode 100644 api/src/__test__/core/__snapshots__/permissions.test.ts.snap create mode 100644 api/src/__test__/core/permissions.test.ts create mode 100644 api/src/__test__/core/utils/files/config-file-normalizer.test.ts create mode 100644 api/src/__test__/mothership/index.test.ts delete mode 100644 api/src/core/default-permissions.ts create mode 100644 api/src/core/logrotate/setup-logrotate.ts delete mode 100644 api/src/core/modules/add-license-key.ts delete mode 100644 api/src/core/modules/get-array.ts delete mode 100644 api/src/core/modules/get-unassigned-devices.ts delete mode 100644 api/src/core/modules/get-vars.ts create mode 100644 api/src/core/modules/index.ts create mode 100644 api/src/core/utils/index.ts create mode 100644 api/src/core/utils/misc/send-form-to-keyserver.ts delete mode 100644 api/src/graphql/express/get-logs.ts delete mode 100644 api/src/graphql/func-directive.ts delete mode 100644 api/src/graphql/resolvers/mutation/connect/connect-sign-out.ts delete mode 100644 api/src/graphql/resolvers/mutation/connect/set-additional-allowed-origins.ts delete mode 100644 api/src/graphql/resolvers/mutation/index.ts delete mode 100644 api/src/graphql/resolvers/query/config.ts delete mode 100644 api/src/graphql/resolvers/query/docker.ts delete mode 100644 api/src/graphql/resolvers/query/index.ts delete mode 100644 api/src/graphql/resolvers/query/notifications.ts delete mode 100644 api/src/graphql/resolvers/query/online.ts delete mode 100644 api/src/graphql/resolvers/query/registration.ts delete mode 100644 api/src/graphql/resolvers/query/server.ts delete mode 100644 api/src/graphql/resolvers/query/servers.ts delete mode 100644 api/src/graphql/resolvers/query/vms.ts delete mode 100644 api/src/graphql/resolvers/resolvers.ts delete mode 100644 api/src/graphql/resolvers/subscription/index.ts delete mode 100644 api/src/graphql/resolvers/user-account.ts create mode 100644 api/src/graphql/schema.ts create mode 100644 api/src/graphql/schema/types/apikeys/apikey.graphql create mode 100644 api/src/graphql/schema/types/array/parity.graphql create mode 100644 api/src/graphql/schema/types/base.graphql create mode 100644 api/src/graphql/schema/types/disks/disk.graphql create mode 100644 api/src/graphql/schema/types/docker/network.graphql create mode 100644 api/src/graphql/schema/types/servers/server.graphql create mode 100644 api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql create mode 100644 api/src/graphql/schema/types/users/me.graphql create mode 100644 api/src/graphql/schema/types/users/user.graphql delete mode 100644 api/src/graphql/types.ts create mode 100644 api/src/mothership/jobs/token-refresh-jobs.ts create mode 100644 api/src/mothership/subscribe-to-mothership.ts create mode 100644 api/src/mothership/utils/delay-function.ts delete mode 100644 api/src/originMiddleware.ts delete mode 100644 api/src/server.ts create mode 100644 api/src/store/actions/setup-remote-access.ts create mode 100644 api/src/store/listeners/array-event-listener.ts create mode 100644 api/src/store/listeners/config-listener.ts create mode 100644 api/src/store/listeners/server-state-listener.ts create mode 100644 api/src/store/listeners/upnp-listener.ts create mode 100644 api/src/store/sync/info-apps-sync.ts create mode 100644 api/src/store/sync/registration-sync.ts create mode 100644 api/src/unraid-api/app/app.module.ts create mode 100644 api/src/unraid-api/auth/auth.guard.ts create mode 100644 api/src/unraid-api/auth/auth.module.ts create mode 100644 api/src/unraid-api/auth/auth.service.spec.ts create mode 100644 api/src/unraid-api/auth/auth.service.ts create mode 100644 api/src/unraid-api/auth/header.strategy.ts create mode 100644 api/src/unraid-api/auth/public.decorator.ts create mode 100644 api/src/unraid-api/exceptions/graphql-exceptions.filter.ts create mode 100644 api/src/unraid-api/exceptions/http-exceptions.filter.ts create mode 100644 api/src/unraid-api/graph/graph.module.ts create mode 100644 api/src/unraid-api/graph/resolvers/array/array.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/array/array.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/config/config.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/config/config.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/display/display.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/display/display.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/flash/flash.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/info/info.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/info/info.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/online/online.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/online/online.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/owner/owner.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/registration/registration.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/resolvers.module.ts create mode 100644 api/src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/servers/server.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts create mode 100644 api/src/unraid-api/graph/resolvers/vms/vms.resolver.spec.ts create mode 100644 api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts create mode 100644 api/src/unraid-api/main.ts create mode 100644 api/src/unraid-api/observers/shutdown.observer.ts create mode 100644 api/src/unraid-api/rest/rest.controller.ts create mode 100644 api/src/unraid-api/rest/rest.module.ts create mode 100644 api/src/unraid-api/rest/rest.service.spec.ts create mode 100644 api/src/unraid-api/rest/rest.service.ts create mode 100644 api/src/unraid-api/users/users.module.ts create mode 100644 api/src/unraid-api/users/users.service.ts create mode 100644 api/src/ws.ts create mode 100644 api/tsconfig.json create mode 100644 api/vite.config.ts diff --git a/api/.dockerignore b/api/.dockerignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/api/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/api/Dockerfile b/api/Dockerfile index 3f065e7c3..399bc1691 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,18 +1,19 @@ ########################################################### # Development/Build Image ########################################################### -FROM node:18.17.1-alpine As development +FROM node:18.17.1-bookworm-slim As development # Install build tools and dependencies -RUN apk add --no-cache \ +RUN apt-get update -y && apt-get install -y \ bash \ # Real PS Command (needed for some dependencies) procps \ - alpine-sdk \ python3 \ libvirt-dev \ jq \ - zstd + zstd \ + git \ + build-essential RUN mkdir /var/log/unraid-api/ @@ -33,7 +34,7 @@ COPY package.json package-lock.json ./ RUN npm i -g pkg zx # Install deps -RUN npm ci +RUN npm i EXPOSE 4000 diff --git a/api/dev/Unraid.net/myservers.cfg b/api/dev/Unraid.net/myservers.cfg index e71f8ec52..cf2076855 100644 --- a/api/dev/Unraid.net/myservers.cfg +++ b/api/dev/Unraid.net/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="3.2.3+da53f636" +version="3.2.3+30d451e0" extraOrigins="" [local] [notifier] @@ -16,5 +16,6 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0" idtoken="" accesstoken="" refreshtoken="" +dynamicRemoteAccessType="DISABLED" [upc] apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810" diff --git a/api/dev/states/myservers.cfg b/api/dev/states/myservers.cfg index a42f7b25d..973905dbb 100644 --- a/api/dev/states/myservers.cfg +++ b/api/dev/states/myservers.cfg @@ -1,5 +1,5 @@ [api] -version="3.2.3+da53f636" +version="3.2.3+30d451e0" extraOrigins="" [local] [notifier] @@ -17,7 +17,8 @@ idtoken="" accesstoken="" refreshtoken="" allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com" +dynamicRemoteAccessType="DISABLED" [upc] apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810" [connectionStatus] -minigraph="ERROR_RETRYING" +minigraph="PRE_INIT" diff --git a/api/package-lock.json b/api/package-lock.json index 28cc041e9..c28f71140 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -11,11 +11,17 @@ "dependencies": { "@apollo/client": "^3.7.12", "@apollo/server": "^4.6.0", + "@as-integrations/fastify": "^2.1.1", "@graphql-codegen/client-preset": "^4.0.0", "@graphql-tools/load-files": "^6.6.1", "@graphql-tools/merge": "^8.4.0", "@graphql-tools/schema": "^9.0.17", "@graphql-tools/utils": "^9.2.1", + "@nestjs/apollo": "^12.0.11", + "@nestjs/core": "^10.2.9", + "@nestjs/graphql": "^12.0.11", + "@nestjs/passport": "^10.0.2", + "@nestjs/platform-fastify": "^10.2.9", "@reduxjs/toolkit": "^1.9.5", "@reflet/cron": "^1.3.1", "@runonflux/nat-upnp": "^1.0.2", @@ -27,8 +33,9 @@ "bytes": "^3.1.2", "cacheable-lookup": "^6.1.0", "catch-exit": "^1.2.2", - "chalk": "^4.1.2", "chokidar": "^3.5.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "cli-table": "^0.3.11", "command-exists": "^1.2.9", "convert": "^4.10.0", @@ -39,14 +46,14 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "find-process": "^1.4.7", - "graphql": "^16.6.0", + "graphql": "^16.8.1", "graphql-fields": "^2.0.3", "graphql-scalars": "^1.21.3", "graphql-subscriptions": "^2.0.0", "graphql-tag": "^2.12.6", "graphql-type-json": "^0.3.2", "graphql-type-uuid": "^0.2.0", - "graphql-ws": "^5.12.1", + "graphql-ws": "^5.14.2", "htpasswd-js": "^1.0.2", "ini": "^4.1.0", "ip": "^1.1.8", @@ -55,17 +62,22 @@ "multi-ini": "^2.2.0", "mustache": "^4.2.0", "nanobus": "^4.5.0", + "nest-access-control": "^3.1.0", + "nestjs-pino": "^3.5.0", "node-cache": "^5.1.2", "node-window-polyfill": "^1.0.2", "openid-client": "^5.4.0", "p-iteration": "^1.1.8", "p-retry": "^4.6.2", + "passport-http-header-strategy": "^1.1.0", "pidusage": "^3.0.2", + "pino": "^8.16.2", + "pino-http": "^8.5.1", + "pino-pretty": "^10.2.3", "reflect-metadata": "^0.1.13", "request": "^2.88.2", "semver": "^7.4.0", "stoppable": "^1.1.0", - "subscriptions-transport-ws": "^0.11.0", "systeminformation": "^5.21.2", "ts-command-line-args": "^2.5.0", "uuid": "^9.0.0", @@ -88,6 +100,7 @@ "@graphql-codegen/typescript-operations": "^4.0.0", "@graphql-codegen/typescript-resolvers": "4.0.1", "@graphql-typed-document-node/core": "^3.2.0", + "@nestjs/testing": "^10.2.10", "@swc/core": "^1.3.81", "@types/async-exit-hook": "^2.0.0", "@types/btoa": "^1.2.3", @@ -129,7 +142,6 @@ "graphql-codegen-typescript-validation-schema": "^0.11.0", "ip-regex": "^5.0.0", "json-difference": "^1.9.1", - "log4js": "^6.9.1", "map-obj": "^5.0.2", "p-props": "^5.0.0", "path-exists": "^5.0.0", @@ -137,7 +149,6 @@ "pkg": "^5.8.1", "pretty-bytes": "^6.1.0", "pretty-ms": "^8.0.0", - "serialize-error": "^11.0.2", "standard-version": "^9.5.0", "tsup": "^7.0.0", "typescript": "^4.9.4", @@ -293,6 +304,21 @@ "graphql": "14.x || 15.x || 16.x" } }, + "node_modules/@apollo/server-plugin-landing-page-graphql-playground": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@apollo/server-plugin-landing-page-graphql-playground/-/server-plugin-landing-page-graphql-playground-4.0.0.tgz", + "integrity": "sha512-PBDtKI/chJ+hHeoJUUH9Kuqu58txQl00vUGuxqiC9XcReulIg7RjsyD0G1u3drX4V709bxkL5S0nTeXfRHD0qA==", + "deprecated": "The use of GraphQL Playground in Apollo Server was supported in previous versions, but this is no longer the case as of December 31, 2022. This package exists for v4 migration purposes only. We do not intend to resolve security issues or other bugs with this package if they arise, so please migrate away from this to [Apollo Server's default Explorer](https://www.apollographql.com/docs/apollo-server/api/plugin/landing-pages) as soon as possible.", + "dependencies": { + "@apollographql/graphql-playground-html": "1.6.29" + }, + "engines": { + "node": ">=14.0" + }, + "peerDependencies": { + "@apollo/server": "^4.0.0" + } + }, "node_modules/@apollo/usage-reporting-protobuf": { "version": "4.1.1", "license": "MIT", @@ -421,6 +447,14 @@ "node": ">=14" } }, + "node_modules/@apollographql/graphql-playground-html": { + "version": "1.6.29", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", + "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", + "dependencies": { + "xss": "^1.0.8" + } + }, "node_modules/@ardatan/relay-compiler": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", @@ -587,6 +621,21 @@ "node": ">=14" } }, + "node_modules/@as-integrations/fastify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@as-integrations/fastify/-/fastify-2.1.1.tgz", + "integrity": "sha512-iupYSQyRrlBAdsDcCYzuKjJlJAKiQpRDLd33A6yAF3Ip/rGrSDDDrxfJWp6FqyVlivSL+MQkGIphmUI/ckfZkg==", + "dependencies": { + "fastify-plugin": "^4.4.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@apollo/server": "^4.0.0", + "fastify": "^4.4.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -1740,6 +1789,68 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "dependencies": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.4.1.tgz", + "integrity": "sha512-iYQJtrY3pFiDS5mo5zRaudzg2OcUdJ96PD6xfkKOOEilly5nnrFZx/W6Sce2T79xxlEn2qpU3t5+qS2phS369w==", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.5" + } + }, + "node_modules/@fastify/deepmerge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", + "integrity": "sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==" + }, + "node_modules/@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "dependencies": { + "fast-json-stringify": "^5.7.0" + } + }, + "node_modules/@fastify/formbody": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-7.4.0.tgz", + "integrity": "sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==", + "dependencies": { + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0" + } + }, + "node_modules/@fastify/middie": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-8.3.0.tgz", + "integrity": "sha512-h+zBxCzMlkEkh4fM7pZaSGzqS7P9M0Z6rXnWPdUEPfe7x1BCj++wEk/pQ5jpyYY4pF8AknFqb77n7uwh8HdxEA==", + "dependencies": { + "@fastify/error": "^3.2.0", + "fastify-plugin": "^4.0.0", + "path-to-regexp": "^6.1.0", + "reusify": "^1.0.4" + } + }, + "node_modules/@fastify/middie/node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, "node_modules/@graphql-codegen/add": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-5.0.0.tgz", @@ -3695,6 +3806,335 @@ "version": "1.4.14", "license": "MIT" }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nestjs/apollo": { + "version": "12.0.11", + "resolved": "https://registry.npmjs.org/@nestjs/apollo/-/apollo-12.0.11.tgz", + "integrity": "sha512-E8kBOyGBZ8Zx4qMLnK3+ECZgmLKqNHyYbtkOi0fXWr8ackosLMkRqGgtDVffXRlVA3eo6G3RgnL0Qyu3VvfD5A==", + "dependencies": { + "@apollo/server-plugin-landing-page-graphql-playground": "4.0.0", + "iterall": "1.3.0", + "lodash.omit": "4.5.0", + "tslib": "2.6.2" + }, + "peerDependencies": { + "@apollo/gateway": "^2.0.0", + "@apollo/server": "^4.3.2", + "@apollo/subgraph": "^2.0.0", + "@as-integrations/fastify": "^1.3.0 || ^2.0.0", + "@nestjs/common": "^9.3.8 || ^10.0.0", + "@nestjs/core": "^9.3.8 || ^10.0.0", + "@nestjs/graphql": "^12.0.0", + "graphql": "^16.6.0" + }, + "peerDependenciesMeta": { + "@apollo/gateway": { + "optional": true + }, + "@apollo/subgraph": { + "optional": true + }, + "@as-integrations/fastify": { + "optional": true + } + } + }, + "node_modules/@nestjs/apollo/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@nestjs/common": { + "version": "10.2.9", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.9.tgz", + "integrity": "sha512-i7vb2zMLJUDIPqjfBhMkgIITK1AnKDkFYSsM+aaRHpNa9xv/CwsiQuINaXfzStMpnwjkq5FDE3aoF0wkTfD2cQ==", + "peer": true, + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/common/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "peer": true + }, + "node_modules/@nestjs/core": { + "version": "10.2.9", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.9.tgz", + "integrity": "sha512-Hl6HC9hR7JD3YmzwcveBKeydaq9cguEsMdEghzLuVH3VEH0M+bTFHjCIKhsxMez4/O7/K6n3EhNx1Et4Z+BqWg==", + "hasInstallScript": true, + "dependencies": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + } + } + }, + "node_modules/@nestjs/core/node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, + "node_modules/@nestjs/core/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@nestjs/graphql": { + "version": "12.0.11", + "resolved": "https://registry.npmjs.org/@nestjs/graphql/-/graphql-12.0.11.tgz", + "integrity": "sha512-iCyVs9+utCQt9ehMhUjQcEdjRN/MrcTBINd7P44O1fzGENuWMbt1Z8RCoZbeGi5iVPBY63HgYik+BnnICqmxZw==", + "dependencies": { + "@graphql-tools/merge": "9.0.0", + "@graphql-tools/schema": "10.0.0", + "@graphql-tools/utils": "10.0.8", + "@nestjs/mapped-types": "2.0.2", + "chokidar": "3.5.3", + "fast-glob": "3.3.2", + "graphql-tag": "2.12.6", + "graphql-ws": "5.14.2", + "lodash": "4.17.21", + "normalize-path": "3.0.0", + "subscriptions-transport-ws": "0.11.0", + "tslib": "2.6.2", + "uuid": "9.0.1", + "ws": "8.14.2" + }, + "peerDependencies": { + "@apollo/subgraph": "^2.0.0", + "@nestjs/common": "^9.3.8 || ^10.0.0", + "@nestjs/core": "^9.3.8 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "graphql": "^16.6.0", + "reflect-metadata": "^0.1.13", + "ts-morph": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0" + }, + "peerDependenciesMeta": { + "@apollo/subgraph": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "ts-morph": { + "optional": true + } + } + }, + "node_modules/@nestjs/graphql/node_modules/@graphql-tools/merge": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.0.tgz", + "integrity": "sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==", + "dependencies": { + "@graphql-tools/utils": "^10.0.0", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@nestjs/graphql/node_modules/@graphql-tools/schema": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.0.tgz", + "integrity": "sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==", + "dependencies": { + "@graphql-tools/merge": "^9.0.0", + "@graphql-tools/utils": "^10.0.0", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@nestjs/graphql/node_modules/@graphql-tools/utils": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.0.8.tgz", + "integrity": "sha512-yjyA8ycSa1WRlJqyX/aLqXeE5DvF/H02+zXMUFnCzIDrj0UvLMUrxhmVFnMK0Q2n3bh4uuTeY3621m5za9ovXw==", + "dependencies": { + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.0", + "dset": "^3.1.2", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@nestjs/graphql/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.2.tgz", + "integrity": "sha512-od31vfB2z3y05IDB5dWSbCGE2+pAf2k2WCBinNuTTOxN0O0+wtO1L3kawj/aCW3YR9uxsTOVbTDwtwgpNNsnjQ==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "passport": "^0.4.0 || ^0.5.0 || ^0.6.0" + } + }, + "node_modules/@nestjs/platform-fastify": { + "version": "10.2.9", + "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-10.2.9.tgz", + "integrity": "sha512-kSkNivpAIwgt5Xnawwe8hfcafo9XMmnjNWcNCSXcjkic8pLPJvU4FD2D1+jmOWEx7uqFKKvg1Cf0Y6HenYfaMA==", + "dependencies": { + "@fastify/cors": "8.4.1", + "@fastify/formbody": "7.4.0", + "@fastify/middie": "8.3.0", + "fastify": "4.24.3", + "light-my-request": "5.11.0", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0", + "@fastify/view": "^7.0.0 || ^8.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "@fastify/view": { + "optional": true + } + } + }, + "node_modules/@nestjs/platform-fastify/node_modules/path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, + "node_modules/@nestjs/platform-fastify/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@nestjs/testing": { + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz", + "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==", + "dev": true, + "dependencies": { + "tslib": "2.6.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/microservices": { + "optional": true + }, + "@nestjs/platform-express": { + "optional": true + } + } + }, + "node_modules/@nestjs/testing/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -3735,6 +4175,23 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "dependencies": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + }, + "bin": { + "opencollective": "bin/opencollective.js" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, "node_modules/@parcel/watcher": { "version": "2.1.0", "dev": true, @@ -4477,6 +4934,11 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.11.6", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz", + "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==" + }, "node_modules/@types/which": { "version": "3.0.0", "dev": true, @@ -4936,6 +5398,22 @@ "node": ">=8" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, "node_modules/accepts": { "version": "1.3.8", "license": "MIT", @@ -5022,9 +5500,7 @@ }, "node_modules/ajv": { "version": "8.12.0", - "dev": true, "license": "MIT", - "optional": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -5036,6 +5512,22 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/am": { "version": "2.0.0", "resolved": "git+ssh://git@github.com/unraid/am.git#12653ef112eb0a9e75a9628b21509864229205f6", @@ -5119,6 +5611,11 @@ "node": ">=8" } }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, "node_modules/arg": { "version": "4.1.3", "dev": true, @@ -5318,6 +5815,14 @@ "node": ">= 4.0.0" } }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/auto-bind": { "version": "4.0.0", "license": "MIT", @@ -5339,6 +5844,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avvio": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", + "integrity": "sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==", + "dependencies": { + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.1" + } + }, "node_modules/awilix": { "version": "8.0.1", "dev": true, @@ -6056,6 +6571,21 @@ "node": ">=8" } }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "node_modules/clean-regexp": { "version": "1.0.0", "dev": true, @@ -6173,7 +6703,6 @@ }, "node_modules/colorette": { "version": "2.0.19", - "dev": true, "license": "MIT" }, "node_modules/colors": { @@ -6369,6 +6898,11 @@ "dev": true, "license": "MIT" }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" + }, "node_modules/constant-case": { "version": "3.0.4", "license": "MIT", @@ -6956,6 +7490,11 @@ "node": ">= 8" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" + }, "node_modules/cz-conventional-changelog": { "version": "3.3.0", "dev": true, @@ -7071,14 +7610,6 @@ "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", "dev": true }, - "node_modules/date-format": { - "version": "4.0.14", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/dateformat": { "version": "3.0.3", "dev": true, @@ -8306,10 +8837,26 @@ "node": "*" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "3.1.2", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "7.2.0", "dev": true, @@ -8481,9 +9028,18 @@ ], "license": "MIT" }, + "node_modules/fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "node_modules/fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -8496,8 +9052,9 @@ "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.1", - "license": "MIT", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -8513,6 +9070,20 @@ "version": "2.1.0", "license": "MIT" }, + "node_modules/fast-json-stringify": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.9.1.tgz", + "integrity": "sha512-NMrf+uU9UJnTzfxaumMDXK1NWqtPCfGoM9DYIE+ESlaTQqjlANFBy0VAbsm6FB88Mx0nceyi18zTo5kIEUlzxg==", + "dependencies": { + "@fastify/deepmerge": "^1.0.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, "node_modules/fast-levenshtein": { "version": "2.0.6", "dev": true, @@ -8520,12 +9091,29 @@ }, "node_modules/fast-querystring": { "version": "1.1.1", - "dev": true, "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "node_modules/fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, "node_modules/fast-url-parser": { "version": "1.1.3", "dev": true, @@ -8548,6 +9136,34 @@ "url": "https://paypal.me/naturalintelligence" } }, + "node_modules/fastify": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.24.3.tgz", + "integrity": "sha512-6HHJ+R2x2LS3y1PqxnwEIjOTZxFl+8h4kSC/TuDPXtA+v2JnV9yEtOsNSKK1RMD7sIR2y1ZsA4BEFaid/cK5pg==", + "dependencies": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.2.1", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^7.7.0", + "light-my-request": "^5.11.0", + "pino": "^8.16.0", + "process-warning": "^2.2.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/fastq": { "version": "1.15.0", "license": "ISC", @@ -8703,6 +9319,19 @@ "version": "2.0.0", "license": "MIT" }, + "node_modules/find-my-way": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", + "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/find-node-modules": { "version": "2.1.3", "dev": true, @@ -9627,11 +10256,9 @@ } }, "node_modules/graphql-ws": { - "version": "5.14.0", - "license": "MIT", - "workspaces": [ - "website" - ], + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.14.2.tgz", + "integrity": "sha512-LycmCwhZ+Op2GlHz4BZDsUYHKRiiUz+3r9wbhBATMETNlORQJAaFlAgTFoeRh6xQoQegwYwIylVD1Qns9/DA3w==", "engines": { "node": ">=10" }, @@ -9781,6 +10408,52 @@ "tslib": "^2.0.3" } }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/help-me/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "license": "BSD-3-Clause", @@ -10646,6 +11319,14 @@ "version": "1.3.0", "license": "MIT" }, + "node_modules/iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/jackspeak": { "version": "2.2.3", "dev": true, @@ -10680,7 +11361,6 @@ }, "node_modules/joycon": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -10742,11 +11422,17 @@ "version": "0.4.0", "license": "(AFL-2.1 OR BSD-3-Clause)" }, + "node_modules/json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", - "dev": true, - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/json-stable-stringify": { "version": "1.0.2", @@ -10880,6 +11566,21 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.49", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz", + "integrity": "sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ==" + }, + "node_modules/light-my-request": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", + "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "dependencies": { + "cookie": "^0.5.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, "node_modules/lilconfig": { "version": "2.1.0", "dev": true, @@ -11029,6 +11730,11 @@ "license": "MIT", "optional": true }, + "node_modules/lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "node_modules/lodash.sortby": { "version": "4.7.0", "license": "MIT" @@ -11100,21 +11806,6 @@ "node": ">=8" } }, - "node_modules/log4js": { - "version": "6.9.1", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/loglevel": { "version": "1.8.1", "license": "MIT", @@ -11518,7 +12209,6 @@ }, "node_modules/minimist": { "version": "1.2.7", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11749,6 +12439,14 @@ "ufo": "^1.3.0" } }, + "node_modules/mnemonist": { + "version": "0.39.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", + "integrity": "sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, "node_modules/modify-values": { "version": "1.0.1", "dev": true, @@ -11918,6 +12616,41 @@ "dev": true, "license": "MIT" }, + "node_modules/nest-access-control": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nest-access-control/-/nest-access-control-3.1.0.tgz", + "integrity": "sha512-rg8OWIcvA2gGiSjnl141RDg2B+R7YgyYgCl3D67NcK7/1TXoTJCdqSyFysYXzkiHiXZOvANKYkvke//p6Yzl8g==", + "dependencies": { + "accesscontrol": "^2.2.1", + "tslib": "^2.6.2" + }, + "peerDependencies": { + "@nestjs/graphql": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0" + }, + "peerDependenciesMeta": { + "@nestjs/graphql": { + "optional": true + } + } + }, + "node_modules/nest-access-control/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/nestjs-pino": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-3.5.0.tgz", + "integrity": "sha512-IWJ3dzLVjg5istcd3Cz3rVO+gmvabfVAT1YmQgzL1HnC2hkc0H6qA6k6SZ7OIwQfewuRejYfPu3TlkxwRrqxHQ==", + "hasInstallScript": true, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "pino-http": "^6.4.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/no-case": { "version": "3.0.4", "license": "MIT", @@ -12249,6 +12982,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, "node_modules/oidc-token-hash": { "version": "5.0.3", "license": "MIT", @@ -12256,6 +12994,14 @@ "node": "^10.13.0 || >=12.0.0" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "license": "MIT", @@ -12631,6 +13377,40 @@ "tslib": "^2.0.3" } }, + "node_modules/passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-http-header-strategy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", + "integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==", + "dependencies": { + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-case": { "version": "3.0.4", "license": "MIT", @@ -12736,6 +13516,12 @@ "node": "*" } }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "node_modules/pause-stream": { "version": "0.0.11", "dev": true, @@ -12783,6 +13569,168 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.2.tgz", + "integrity": "sha512-2advCDGVEvkKu9TTVSa/kWW7Z3htI/sBKEZpqiHk6ive0i/7f5b1rsU8jn0aimxqfnSz5bj/nOYkwhBUn5xxvg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/pino-http": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.5.1.tgz", + "integrity": "sha512-T/3d9YHKBYpv/QHjNy73P5BNYYkRrC2/D6CxKMecG4fKFLN+B2iC6LsKYzGRTRV+Ld3fjxFC1ca4TUGbPdzk+Q==", + "dependencies": { + "get-caller-file": "^2.0.5", + "pino": "^8.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", + "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/pino-pretty/node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "engines": { + "node": "*" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, "node_modules/pirates": { "version": "4.0.5", "dev": true, @@ -13122,11 +14070,24 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "dev": true, "license": "MIT" }, + "node_modules/process-warning": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", + "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" + }, "node_modules/progress": { "version": "2.0.3", "dev": true, @@ -13270,6 +14231,11 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "node_modules/quick-lru": { "version": "6.1.1", "dev": true, @@ -13456,6 +14422,14 @@ "node": ">=8.10.0" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redent": { "version": "3.0.0", "dev": true, @@ -13638,9 +14612,7 @@ }, "node_modules/require-from-string": { "version": "2.0.2", - "dev": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -13762,6 +14734,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/retry": { "version": "0.13.1", "license": "MIT", @@ -13779,7 +14759,6 @@ }, "node_modules/rfdc": { "version": "1.3.0", - "dev": true, "license": "MIT" }, "node_modules/rimraf": { @@ -13937,7 +14916,6 @@ }, "node_modules/rxjs": { "version": "7.8.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -13982,6 +14960,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "dependencies": { + "ret": "~0.2.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "license": "MIT" @@ -13996,6 +14990,11 @@ "dev": true, "license": "MIT" }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/semver": { "version": "7.5.4", "license": "ISC", @@ -14069,21 +15068,6 @@ "upper-case-first": "^2.0.2" } }, - "node_modules/serialize-error": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.2.tgz", - "integrity": "sha512-o43i0jLcA0LXA5Uu+gI1Vj+lF66KR9IAcy0ThbGq1bAMPN+k5IgSHsulfnqf/ddKAz6dWf+k8PD5hAr9oCSHEQ==", - "dev": true, - "dependencies": { - "type-fest": "^2.12.2" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/serve-static": { "version": "1.15.0", "license": "MIT", @@ -14102,6 +15086,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -14300,6 +15289,14 @@ "dev": true, "license": "MIT" }, + "node_modules/sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, @@ -14609,48 +15606,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/streamroller": { - "version": "3.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/streamroller/node_modules/fs-extra": { - "version": "8.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/streamroller/node_modules/jsonfile": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/streamroller/node_modules/universalify": { - "version": "0.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, "node_modules/streamsearch": { "version": "1.1.0", "dev": true, @@ -14801,7 +15756,6 @@ }, "node_modules/strip-json-comments": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15144,6 +16098,14 @@ "node": ">=0.8" } }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/through": { "version": "2.3.8", "dev": true, @@ -15226,6 +16188,14 @@ "node": ">=8.0" } }, + "node_modules/toad-cache": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.3.0.tgz", + "integrity": "sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==", + "engines": { + "node": ">=12" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", @@ -15829,6 +16799,17 @@ "node": ">=0.8.0" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "dev": true, @@ -15997,8 +16978,13 @@ } }, "node_modules/uuid": { - "version": "9.0.0", - "license": "MIT", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "bin": { "uuid": "dist/bin/uuid" } @@ -16042,6 +17028,14 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-promise": { "version": "1.0.12", "license": "MIT", @@ -16483,6 +17477,26 @@ "node": ">=4.0" } }, + "node_modules/xss": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", + "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, "node_modules/xtend": { "version": "4.0.2", "dev": true, @@ -16802,6 +17816,14 @@ "@apollo/utils.logger": "^2.0.0" } }, + "@apollo/server-plugin-landing-page-graphql-playground": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@apollo/server-plugin-landing-page-graphql-playground/-/server-plugin-landing-page-graphql-playground-4.0.0.tgz", + "integrity": "sha512-PBDtKI/chJ+hHeoJUUH9Kuqu58txQl00vUGuxqiC9XcReulIg7RjsyD0G1u3drX4V709bxkL5S0nTeXfRHD0qA==", + "requires": { + "@apollographql/graphql-playground-html": "1.6.29" + } + }, "@apollo/usage-reporting-protobuf": { "version": "4.1.1", "requires": { @@ -16867,6 +17889,14 @@ "@apollo/utils.withrequired": { "version": "2.0.1" }, + "@apollographql/graphql-playground-html": { + "version": "1.6.29", + "resolved": "https://registry.npmjs.org/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.29.tgz", + "integrity": "sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==", + "requires": { + "xss": "^1.0.8" + } + }, "@ardatan/relay-compiler": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/@ardatan/relay-compiler/-/relay-compiler-12.0.0.tgz", @@ -16995,6 +18025,14 @@ "node-fetch": "^2.6.1" } }, + "@as-integrations/fastify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@as-integrations/fastify/-/fastify-2.1.1.tgz", + "integrity": "sha512-iupYSQyRrlBAdsDcCYzuKjJlJAKiQpRDLd33A6yAF3Ip/rGrSDDDrxfJWp6FqyVlivSL+MQkGIphmUI/ckfZkg==", + "requires": { + "fastify-plugin": "^4.4.0" + } + }, "@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -17784,6 +18822,70 @@ "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", "dev": true }, + "@fastify/ajv-compiler": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz", + "integrity": "sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==", + "requires": { + "ajv": "^8.11.0", + "ajv-formats": "^2.1.1", + "fast-uri": "^2.0.0" + } + }, + "@fastify/cors": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.4.1.tgz", + "integrity": "sha512-iYQJtrY3pFiDS5mo5zRaudzg2OcUdJ96PD6xfkKOOEilly5nnrFZx/W6Sce2T79xxlEn2qpU3t5+qS2phS369w==", + "requires": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.5" + } + }, + "@fastify/deepmerge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", + "integrity": "sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==" + }, + "@fastify/error": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-3.4.1.tgz", + "integrity": "sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==" + }, + "@fastify/fast-json-stringify-compiler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.3.0.tgz", + "integrity": "sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==", + "requires": { + "fast-json-stringify": "^5.7.0" + } + }, + "@fastify/formbody": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@fastify/formbody/-/formbody-7.4.0.tgz", + "integrity": "sha512-H3C6h1GN56/SMrZS8N2vCT2cZr7mIHzBHzOBa5OPpjfB/D6FzP9mMpE02ZzrFX0ANeh0BAJdoXKOF2e7IbV+Og==", + "requires": { + "fast-querystring": "^1.0.0", + "fastify-plugin": "^4.0.0" + } + }, + "@fastify/middie": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@fastify/middie/-/middie-8.3.0.tgz", + "integrity": "sha512-h+zBxCzMlkEkh4fM7pZaSGzqS7P9M0Z6rXnWPdUEPfe7x1BCj++wEk/pQ5jpyYY4pF8AknFqb77n7uwh8HdxEA==", + "requires": { + "@fastify/error": "^3.2.0", + "fastify-plugin": "^4.0.0", + "path-to-regexp": "^6.1.0", + "reusify": "^1.0.4" + }, + "dependencies": { + "path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + } + } + }, "@graphql-codegen/add": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/add/-/add-5.0.0.tgz", @@ -19237,6 +20339,187 @@ } } }, + "@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" + }, + "@nestjs/apollo": { + "version": "12.0.11", + "resolved": "https://registry.npmjs.org/@nestjs/apollo/-/apollo-12.0.11.tgz", + "integrity": "sha512-E8kBOyGBZ8Zx4qMLnK3+ECZgmLKqNHyYbtkOi0fXWr8ackosLMkRqGgtDVffXRlVA3eo6G3RgnL0Qyu3VvfD5A==", + "requires": { + "@apollo/server-plugin-landing-page-graphql-playground": "4.0.0", + "iterall": "1.3.0", + "lodash.omit": "4.5.0", + "tslib": "2.6.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@nestjs/common": { + "version": "10.2.9", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.9.tgz", + "integrity": "sha512-i7vb2zMLJUDIPqjfBhMkgIITK1AnKDkFYSsM+aaRHpNa9xv/CwsiQuINaXfzStMpnwjkq5FDE3aoF0wkTfD2cQ==", + "peer": true, + "requires": { + "iterare": "1.2.1", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "peer": true + } + } + }, + "@nestjs/core": { + "version": "10.2.9", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.9.tgz", + "integrity": "sha512-Hl6HC9hR7JD3YmzwcveBKeydaq9cguEsMdEghzLuVH3VEH0M+bTFHjCIKhsxMez4/O7/K6n3EhNx1Et4Z+BqWg==", + "requires": { + "@nuxtjs/opencollective": "0.3.2", + "fast-safe-stringify": "2.1.1", + "iterare": "1.2.1", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2", + "uid": "2.0.2" + }, + "dependencies": { + "path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@nestjs/graphql": { + "version": "12.0.11", + "resolved": "https://registry.npmjs.org/@nestjs/graphql/-/graphql-12.0.11.tgz", + "integrity": "sha512-iCyVs9+utCQt9ehMhUjQcEdjRN/MrcTBINd7P44O1fzGENuWMbt1Z8RCoZbeGi5iVPBY63HgYik+BnnICqmxZw==", + "requires": { + "@graphql-tools/merge": "9.0.0", + "@graphql-tools/schema": "10.0.0", + "@graphql-tools/utils": "10.0.8", + "@nestjs/mapped-types": "2.0.2", + "chokidar": "3.5.3", + "fast-glob": "3.3.2", + "graphql-tag": "2.12.6", + "graphql-ws": "5.14.2", + "lodash": "4.17.21", + "normalize-path": "3.0.0", + "subscriptions-transport-ws": "0.11.0", + "tslib": "2.6.2", + "uuid": "9.0.1", + "ws": "8.14.2" + }, + "dependencies": { + "@graphql-tools/merge": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-9.0.0.tgz", + "integrity": "sha512-J7/xqjkGTTwOJmaJQJ2C+VDBDOWJL3lKrHJN4yMaRLAJH3PosB7GiPRaSDZdErs0+F77sH2MKs2haMMkywzx7Q==", + "requires": { + "@graphql-tools/utils": "^10.0.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-10.0.0.tgz", + "integrity": "sha512-kf3qOXMFcMs2f/S8Y3A8fm/2w+GaHAkfr3Gnhh2LOug/JgpY/ywgFVxO3jOeSpSEdoYcDKLcXVjMigNbY4AdQg==", + "requires": { + "@graphql-tools/merge": "^9.0.0", + "@graphql-tools/utils": "^10.0.0", + "tslib": "^2.4.0", + "value-or-promise": "^1.0.12" + } + }, + "@graphql-tools/utils": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.0.8.tgz", + "integrity": "sha512-yjyA8ycSa1WRlJqyX/aLqXeE5DvF/H02+zXMUFnCzIDrj0UvLMUrxhmVFnMK0Q2n3bh4uuTeY3621m5za9ovXw==", + "requires": { + "@graphql-typed-document-node/core": "^3.1.1", + "cross-inspect": "1.0.0", + "dset": "^3.1.2", + "tslib": "^2.4.0" + } + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@nestjs/mapped-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "requires": {} + }, + "@nestjs/passport": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.2.tgz", + "integrity": "sha512-od31vfB2z3y05IDB5dWSbCGE2+pAf2k2WCBinNuTTOxN0O0+wtO1L3kawj/aCW3YR9uxsTOVbTDwtwgpNNsnjQ==", + "requires": {} + }, + "@nestjs/platform-fastify": { + "version": "10.2.9", + "resolved": "https://registry.npmjs.org/@nestjs/platform-fastify/-/platform-fastify-10.2.9.tgz", + "integrity": "sha512-kSkNivpAIwgt5Xnawwe8hfcafo9XMmnjNWcNCSXcjkic8pLPJvU4FD2D1+jmOWEx7uqFKKvg1Cf0Y6HenYfaMA==", + "requires": { + "@fastify/cors": "8.4.1", + "@fastify/formbody": "7.4.0", + "@fastify/middie": "8.3.0", + "fastify": "4.24.3", + "light-my-request": "5.11.0", + "path-to-regexp": "3.2.0", + "tslib": "2.6.2" + }, + "dependencies": { + "path-to-regexp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", + "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + }, + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "@nestjs/testing": { + "version": "10.2.10", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.10.tgz", + "integrity": "sha512-IVLUnPz/+fkBtPATYfqTIP+phN9yjkXejmj+JyhmcfPJZpxBmD1i9VSMqa4u54l37j0xkGPscQ0IXpbhqMYUKw==", + "dev": true, + "requires": { + "tslib": "2.6.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "requires": { @@ -19261,6 +20544,16 @@ "semver": "^7.3.5" } }, + "@nuxtjs/opencollective": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", + "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", + "requires": { + "chalk": "^4.1.0", + "consola": "^2.15.0", + "node-fetch": "^2.6.1" + } + }, "@parcel/watcher": { "version": "2.1.0", "dev": true, @@ -19771,6 +21064,11 @@ "version": "9.0.2", "dev": true }, + "@types/validator": { + "version": "13.11.6", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.6.tgz", + "integrity": "sha512-HUgHujPhKuNzgNXBRZKYexwoG+gHKU+tnfPqjWXFghZAnn73JElicMkuSKJyLGr9JgyA8IgK7fj88IyA9rwYeQ==" + }, "@types/which": { "version": "3.0.0", "dev": true @@ -20068,6 +21366,19 @@ "tslib": "^2.3.0" } }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, "accepts": { "version": "1.3.8", "requires": { @@ -20122,8 +21433,6 @@ }, "ajv": { "version": "8.12.0", - "dev": true, - "optional": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -20131,6 +21440,14 @@ "uri-js": "^4.2.2" } }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "requires": { + "ajv": "^8.0.0" + } + }, "am": { "version": "git+ssh://git@github.com/unraid/am.git#12653ef112eb0a9e75a9628b21509864229205f6", "from": "am@github:unraid/am" @@ -20177,6 +21494,11 @@ "apache-md5": { "version": "1.1.8" }, + "archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" + }, "arg": { "version": "4.1.3", "dev": true, @@ -20302,6 +21624,11 @@ "version": "1.0.0", "dev": true }, + "atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==" + }, "auto-bind": { "version": "4.0.0" }, @@ -20309,6 +21636,16 @@ "version": "1.0.5", "dev": true }, + "avvio": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.2.1.tgz", + "integrity": "sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==", + "requires": { + "archy": "^1.0.0", + "debug": "^4.0.0", + "fastq": "^1.6.1" + } + }, "awilix": { "version": "8.0.1", "dev": true, @@ -20764,6 +22101,21 @@ "version": "3.8.0", "dev": true }, + "class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "requires": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "clean-regexp": { "version": "1.0.0", "dev": true, @@ -20832,8 +22184,7 @@ "version": "1.1.4" }, "colorette": { - "version": "2.0.19", - "dev": true + "version": "2.0.19" }, "colors": { "version": "1.0.3" @@ -20959,6 +22310,11 @@ "version": "1.0.11", "dev": true }, + "consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" + }, "constant-case": { "version": "3.0.4", "requires": { @@ -21354,6 +22710,11 @@ "which": "^2.0.1" } }, + "cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" + }, "cz-conventional-changelog": { "version": "3.3.0", "dev": true, @@ -21431,10 +22792,6 @@ "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==", "dev": true }, - "date-format": { - "version": "4.0.14", - "dev": true - }, "dateformat": { "version": "3.0.3", "dev": true @@ -22224,9 +23581,19 @@ } } }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "eventemitter3": { "version": "3.1.2" }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, "execa": { "version": "7.2.0", "dev": true, @@ -22353,9 +23720,18 @@ "extsprintf": { "version": "1.3.0" }, + "fast-content-type-parse": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" + }, + "fast-copy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.1.tgz", + "integrity": "sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==" + }, "fast-decode-uri-component": { - "version": "1.0.1", - "dev": true + "version": "1.0.1" }, "fast-deep-equal": { "version": "3.1.3" @@ -22365,7 +23741,9 @@ "dev": true }, "fast-glob": { - "version": "3.3.1", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "requires": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -22377,17 +23755,45 @@ "fast-json-stable-stringify": { "version": "2.1.0" }, + "fast-json-stringify": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.9.1.tgz", + "integrity": "sha512-NMrf+uU9UJnTzfxaumMDXK1NWqtPCfGoM9DYIE+ESlaTQqjlANFBy0VAbsm6FB88Mx0nceyi18zTo5kIEUlzxg==", + "requires": { + "@fastify/deepmerge": "^1.0.0", + "ajv": "^8.10.0", + "ajv-formats": "^2.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^2.1.0", + "json-schema-ref-resolver": "^1.0.1", + "rfdc": "^1.2.0" + } + }, "fast-levenshtein": { "version": "2.0.6", "dev": true }, "fast-querystring": { "version": "1.1.1", - "dev": true, "requires": { "fast-decode-uri-component": "^1.0.1" } }, + "fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==" + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "fast-uri": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.3.0.tgz", + "integrity": "sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==" + }, "fast-url-parser": { "version": "1.1.3", "dev": true, @@ -22401,6 +23807,34 @@ "strnum": "^1.0.5" } }, + "fastify": { + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-4.24.3.tgz", + "integrity": "sha512-6HHJ+R2x2LS3y1PqxnwEIjOTZxFl+8h4kSC/TuDPXtA+v2JnV9yEtOsNSKK1RMD7sIR2y1ZsA4BEFaid/cK5pg==", + "requires": { + "@fastify/ajv-compiler": "^3.5.0", + "@fastify/error": "^3.4.0", + "@fastify/fast-json-stringify-compiler": "^4.3.0", + "abstract-logging": "^2.0.1", + "avvio": "^8.2.1", + "fast-content-type-parse": "^1.1.0", + "fast-json-stringify": "^5.8.0", + "find-my-way": "^7.7.0", + "light-my-request": "^5.11.0", + "pino": "^8.16.0", + "process-warning": "^2.2.0", + "proxy-addr": "^2.0.7", + "rfdc": "^1.3.0", + "secure-json-parse": "^2.7.0", + "semver": "^7.5.4", + "toad-cache": "^3.3.0" + } + }, + "fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "fastq": { "version": "1.15.0", "requires": { @@ -22513,6 +23947,16 @@ } } }, + "find-my-way": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-7.7.0.tgz", + "integrity": "sha512-+SrHpvQ52Q6W9f3wJoJBbAQULJuNEEQwBvlvYwACDhBTLOTMiQ0HYWh4+vC3OivGP2ENcTI1oKlFA2OepJNjhQ==", + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^2.0.0" + } + }, "find-node-modules": { "version": "2.1.3", "dev": true, @@ -23141,7 +24585,9 @@ "requires": {} }, "graphql-ws": { - "version": "5.14.0", + "version": "5.14.2", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.14.2.tgz", + "integrity": "sha512-LycmCwhZ+Op2GlHz4BZDsUYHKRiiUz+3r9wbhBATMETNlORQJAaFlAgTFoeRh6xQoQegwYwIylVD1Qns9/DA3w==", "requires": {} }, "handlebars": { @@ -23223,6 +24669,45 @@ "tslib": "^2.0.3" } }, + "help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "requires": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "hoist-non-react-statics": { "version": "3.3.2", "requires": { @@ -23739,6 +25224,11 @@ "iterall": { "version": "1.3.0" }, + "iterare": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", + "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==" + }, "jackspeak": { "version": "2.2.3", "dev": true, @@ -23755,8 +25245,7 @@ "version": "4.14.4" }, "joycon": { - "version": "3.1.1", - "dev": true + "version": "3.1.1" }, "js-tokens": { "version": "4.0.0" @@ -23793,10 +25282,16 @@ "json-schema": { "version": "0.4.0" }, + "json-schema-ref-resolver": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz", + "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==", + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, "json-schema-traverse": { - "version": "1.0.0", - "dev": true, - "optional": true + "version": "1.0.0" }, "json-stable-stringify": { "version": "1.0.2", @@ -23883,6 +25378,21 @@ "type-check": "~0.4.0" } }, + "libphonenumber-js": { + "version": "1.10.49", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.49.tgz", + "integrity": "sha512-gvLtyC3tIuqfPzjvYLH9BmVdqzGDiSi4VjtWe2fAgSdBf0yt8yPmbNnRIHNbR5IdtVkm0ayGuzwQKTWmU0hdjQ==" + }, + "light-my-request": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-5.11.0.tgz", + "integrity": "sha512-qkFCeloXCOMpmEdZ/MV91P8AT4fjwFXWaAFz3lUeStM8RcoM1ks4J/F8r1b3r6y/H4u3ACEJ1T+Gv5bopj7oDA==", + "requires": { + "cookie": "^0.5.0", + "process-warning": "^2.0.0", + "set-cookie-parser": "^2.4.1" + } + }, "lilconfig": { "version": "2.1.0", "dev": true @@ -23976,6 +25486,11 @@ "dev": true, "optional": true }, + "lodash.omit": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz", + "integrity": "sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==" + }, "lodash.sortby": { "version": "4.7.0" }, @@ -24022,17 +25537,6 @@ } } }, - "log4js": { - "version": "6.9.1", - "dev": true, - "requires": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "flatted": "^3.2.7", - "rfdc": "^1.3.0", - "streamroller": "^3.1.5" - } - }, "loglevel": { "version": "1.8.1" }, @@ -24265,8 +25769,7 @@ } }, "minimist": { - "version": "1.2.7", - "dev": true + "version": "1.2.7" }, "minimist-options": { "version": "4.1.0", @@ -24432,6 +25935,14 @@ "ufo": "^1.3.0" } }, + "mnemonist": { + "version": "0.39.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", + "integrity": "sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==", + "requires": { + "obliterator": "^2.0.1" + } + }, "modify-values": { "version": "1.0.1", "dev": true @@ -24536,6 +26047,28 @@ "version": "2.6.2", "dev": true }, + "nest-access-control": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nest-access-control/-/nest-access-control-3.1.0.tgz", + "integrity": "sha512-rg8OWIcvA2gGiSjnl141RDg2B+R7YgyYgCl3D67NcK7/1TXoTJCdqSyFysYXzkiHiXZOvANKYkvke//p6Yzl8g==", + "requires": { + "accesscontrol": "^2.2.1", + "tslib": "^2.6.2" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, + "nestjs-pino": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/nestjs-pino/-/nestjs-pino-3.5.0.tgz", + "integrity": "sha512-IWJ3dzLVjg5istcd3Cz3rVO+gmvabfVAT1YmQgzL1HnC2hkc0H6qA6k6SZ7OIwQfewuRejYfPu3TlkxwRrqxHQ==", + "requires": {} + }, "no-case": { "version": "3.0.4", "requires": { @@ -24731,9 +26264,19 @@ "es-abstract": "^1.20.4" } }, + "obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, "oidc-token-hash": { "version": "5.0.3" }, + "on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==" + }, "on-finished": { "version": "2.4.1", "requires": { @@ -24955,6 +26498,30 @@ "tslib": "^2.0.3" } }, + "passport": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz", + "integrity": "sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug==", + "peer": true, + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + } + }, + "passport-http-header-strategy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz", + "integrity": "sha512-Gn60rR55UE1wXbVhnnfG3yyeRSz5pzz3n6rppxa6xiOo4gGPh/onuw29HuGjpk9DSzXRFkJn95+8RT11kXHeWA==", + "requires": { + "passport-strategy": "^1.0.0" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==" + }, "path-case": { "version": "3.0.4", "requires": { @@ -25017,6 +26584,12 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "pause-stream": { "version": "0.0.11", "dev": true, @@ -25043,6 +26616,126 @@ "version": "2.3.0", "dev": true }, + "pino": { + "version": "8.16.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.2.tgz", + "integrity": "sha512-2advCDGVEvkKu9TTVSa/kWW7Z3htI/sBKEZpqiHk6ive0i/7f5b1rsU8jn0aimxqfnSz5bj/nOYkwhBUn5xxvg==", + "requires": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + } + }, + "pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "requires": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + }, + "split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" + } + } + }, + "pino-http": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/pino-http/-/pino-http-8.5.1.tgz", + "integrity": "sha512-T/3d9YHKBYpv/QHjNy73P5BNYYkRrC2/D6CxKMecG4fKFLN+B2iC6LsKYzGRTRV+Ld3fjxFC1ca4TUGbPdzk+Q==", + "requires": { + "get-caller-file": "^2.0.5", + "pino": "^8.0.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0" + } + }, + "pino-pretty": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.2.3.tgz", + "integrity": "sha512-4jfIUc8TC1GPUfDyMSlW1STeORqkoxec71yhxIpLDQapUu8WOuoz2TTCoidrIssyz78LZC69whBMPIKCMbi3cw==", + "requires": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==" + }, + "readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } + } + }, + "pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, "pirates": { "version": "4.0.5", "dev": true @@ -25254,10 +26947,20 @@ "version": "3.0.0", "dev": true }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-nextick-args": { "version": "2.0.1", "dev": true }, + "process-warning": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", + "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" + }, "progress": { "version": "2.0.3", "dev": true @@ -25344,6 +27047,11 @@ "queue-microtask": { "version": "1.2.3" }, + "quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, "quick-lru": { "version": "6.1.1", "dev": true @@ -25461,6 +27169,11 @@ "picomatch": "^2.2.1" } }, + "real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==" + }, "redent": { "version": "3.0.0", "dev": true, @@ -25589,9 +27302,7 @@ "version": "2.1.1" }, "require-from-string": { - "version": "2.0.2", - "dev": true, - "optional": true + "version": "2.0.2" }, "require-main-filename": { "version": "2.0.0", @@ -25665,6 +27376,11 @@ } } }, + "ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==" + }, "retry": { "version": "0.13.1" }, @@ -25672,8 +27388,7 @@ "version": "1.0.4" }, "rfdc": { - "version": "1.3.0", - "dev": true + "version": "1.3.0" }, "rimraf": { "version": "3.0.2", @@ -25765,7 +27480,6 @@ }, "rxjs": { "version": "7.8.0", - "dev": true, "requires": { "tslib": "^2.1.0" } @@ -25789,6 +27503,19 @@ "is-regex": "^1.1.4" } }, + "safe-regex2": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", + "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "requires": { + "ret": "~0.2.0" + } + }, + "safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" + }, "safer-buffer": { "version": "2.1.2" }, @@ -25800,6 +27527,11 @@ "version": "1.1.0", "dev": true }, + "secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "semver": { "version": "7.5.4", "requires": { @@ -25859,15 +27591,6 @@ "upper-case-first": "^2.0.2" } }, - "serialize-error": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-11.0.2.tgz", - "integrity": "sha512-o43i0jLcA0LXA5Uu+gI1Vj+lF66KR9IAcy0ThbGq1bAMPN+k5IgSHsulfnqf/ddKAz6dWf+k8PD5hAr9oCSHEQ==", - "dev": true, - "requires": { - "type-fest": "^2.12.2" - } - }, "serve-static": { "version": "1.15.0", "requires": { @@ -25882,6 +27605,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "set-cookie-parser": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", + "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -26001,6 +27729,14 @@ "socks": "^2.6.2" } }, + "sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "requires": { + "atomic-sleep": "^1.0.0" + } + }, "source-map": { "version": "0.6.1", "dev": true @@ -26226,37 +27962,6 @@ } } }, - "streamroller": { - "version": "3.1.5", - "dev": true, - "requires": { - "date-format": "^4.0.14", - "debug": "^4.3.4", - "fs-extra": "^8.1.0" - }, - "dependencies": { - "fs-extra": { - "version": "8.1.0", - "dev": true, - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "4.0.0", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "universalify": { - "version": "0.1.2", - "dev": true - } - } - }, "streamsearch": { "version": "1.1.0", "dev": true @@ -26351,8 +28056,7 @@ } }, "strip-json-comments": { - "version": "3.1.1", - "dev": true + "version": "3.1.1" }, "strip-literal": { "version": "1.0.1", @@ -26557,6 +28261,14 @@ "thenify": ">= 3.1.0 < 4" } }, + "thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "requires": { + "real-require": "^0.2.0" + } + }, "through": { "version": "2.3.8", "dev": true @@ -26610,6 +28322,11 @@ "is-number": "^7.0.0" } }, + "toad-cache": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.3.0.tgz", + "integrity": "sha512-3oDzcogWGHZdkwrHyvJVpPjA7oNzY6ENOV3PsWJY9XYPZ6INo94Yd47s5may1U+nleBPwDhrRiTPMIvKaa3MQg==" + }, "toidentifier": { "version": "1.0.1" }, @@ -26964,6 +28681,14 @@ "dev": true, "optional": true }, + "uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "requires": { + "@lukeed/csprng": "^1.0.0" + } + }, "unbox-primitive": { "version": "1.0.2", "dev": true, @@ -27067,7 +28792,9 @@ "version": "1.0.1" }, "uuid": { - "version": "9.0.0" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" }, "v8-compile-cache-lib": { "version": "3.0.1", @@ -27098,6 +28825,11 @@ "builtins": "^5.0.0" } }, + "validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" + }, "value-or-promise": { "version": "1.0.12" }, @@ -27328,6 +29060,22 @@ "version": "11.0.1", "optional": true }, + "xss": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz", + "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==", + "requires": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, "xtend": { "version": "4.0.2", "dev": true diff --git a/api/package.json b/api/package.json index 516c5a711..055c2177c 100644 --- a/api/package.json +++ b/api/package.json @@ -41,14 +41,15 @@ "release": "standard-version", "typesync": "typesync", "install:unraid": "./scripts/install-in-unraid.sh", - "start:plugin": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug", + "start:plugin": "INTROSPECTION=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug", "start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug", "start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs start --debug'", "stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'", "start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'", "start:docker": "docker compose run --rm builder-interactive", - "docker:dev": "docker-compose run --rm --service-ports dev", - "docker:test": "docker-compose run --rm builder npm run test" + "build:dev": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build dev", + "docker:dev": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm --service-ports dev", + "docker:test": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder npm run test" }, "files": [ ".env.staging", @@ -59,11 +60,17 @@ "dependencies": { "@apollo/client": "^3.7.12", "@apollo/server": "^4.6.0", + "@as-integrations/fastify": "^2.1.1", "@graphql-codegen/client-preset": "^4.0.0", "@graphql-tools/load-files": "^6.6.1", "@graphql-tools/merge": "^8.4.0", "@graphql-tools/schema": "^9.0.17", "@graphql-tools/utils": "^9.2.1", + "@nestjs/apollo": "^12.0.11", + "@nestjs/core": "^10.2.9", + "@nestjs/graphql": "^12.0.11", + "@nestjs/passport": "^10.0.2", + "@nestjs/platform-fastify": "^10.2.9", "@reduxjs/toolkit": "^1.9.5", "@reflet/cron": "^1.3.1", "@runonflux/nat-upnp": "^1.0.2", @@ -75,8 +82,9 @@ "bytes": "^3.1.2", "cacheable-lookup": "^6.1.0", "catch-exit": "^1.2.2", - "chalk": "^4.1.2", "chokidar": "^3.5.3", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "cli-table": "^0.3.11", "command-exists": "^1.2.9", "convert": "^4.10.0", @@ -87,14 +95,14 @@ "dotenv": "^16.0.3", "express": "^4.18.2", "find-process": "^1.4.7", - "graphql": "^16.6.0", + "graphql": "^16.8.1", "graphql-fields": "^2.0.3", "graphql-scalars": "^1.21.3", "graphql-subscriptions": "^2.0.0", "graphql-tag": "^2.12.6", "graphql-type-json": "^0.3.2", "graphql-type-uuid": "^0.2.0", - "graphql-ws": "^5.12.1", + "graphql-ws": "^5.14.2", "htpasswd-js": "^1.0.2", "ini": "^4.1.0", "ip": "^1.1.8", @@ -103,17 +111,22 @@ "multi-ini": "^2.2.0", "mustache": "^4.2.0", "nanobus": "^4.5.0", + "nest-access-control": "^3.1.0", + "nestjs-pino": "^3.5.0", "node-cache": "^5.1.2", "node-window-polyfill": "^1.0.2", "openid-client": "^5.4.0", "p-iteration": "^1.1.8", "p-retry": "^4.6.2", + "passport-http-header-strategy": "^1.1.0", "pidusage": "^3.0.2", + "pino": "^8.16.2", + "pino-http": "^8.5.1", + "pino-pretty": "^10.2.3", "reflect-metadata": "^0.1.13", "request": "^2.88.2", "semver": "^7.4.0", "stoppable": "^1.1.0", - "subscriptions-transport-ws": "^0.11.0", "systeminformation": "^5.21.2", "ts-command-line-args": "^2.5.0", "uuid": "^9.0.0", @@ -133,6 +146,7 @@ "@graphql-codegen/typescript-operations": "^4.0.0", "@graphql-codegen/typescript-resolvers": "4.0.1", "@graphql-typed-document-node/core": "^3.2.0", + "@nestjs/testing": "^10.2.10", "@swc/core": "^1.3.81", "@types/async-exit-hook": "^2.0.0", "@types/btoa": "^1.2.3", @@ -174,7 +188,6 @@ "graphql-codegen-typescript-validation-schema": "^0.11.0", "ip-regex": "^5.0.0", "json-difference": "^1.9.1", - "log4js": "^6.9.1", "map-obj": "^5.0.2", "p-props": "^5.0.0", "path-exists": "^5.0.0", @@ -182,7 +195,6 @@ "pkg": "^5.8.1", "pretty-bytes": "^6.1.0", "pretty-ms": "^8.0.0", - "serialize-error": "^11.0.2", "standard-version": "^9.5.0", "tsup": "^7.0.0", "typescript": "^4.9.4", diff --git a/api/src/__test__/common/dashboard/generate-data.test.ts b/api/src/__test__/common/dashboard/generate-data.test.ts index 460dffe91..365103ee3 100644 --- a/api/src/__test__/common/dashboard/generate-data.test.ts +++ b/api/src/__test__/common/dashboard/generate-data.test.ts @@ -16,34 +16,21 @@ vi.mock('@app/core/log', () => ({ error: vi.fn(), debug: vi.fn(), trace: vi.fn(), - addContext: vi.fn(), - removeContext: vi.fn(), }, dashboardLogger: { info: vi.fn(), error: vi.fn((...input) => console.log(input)), debug: vi.fn(), trace: vi.fn(), - addContext: vi.fn(), - removeContext: vi.fn(), }, emhttpLogger: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), trace: vi.fn(), - addContext: vi.fn(), - removeContext: vi.fn(), }, })); -vi.mock('@app/common/two-factor', () => ({ - checkTwoFactorEnabled: vi.fn(() => ({ - isRemoteEnabled: false, - isLocalEnabled: false, - })), -})); - vi.mock('@app/common/dashboard/boot-timestamp', () => ({ bootTimestamp: new Date('2022-06-10T04:35:58.276Z'), })); @@ -77,7 +64,7 @@ test('Returns generated data', async () => { "case": { "base64": "", "error": "", - "icon": "case-model.png", + "icon": "", "url": "", }, }, @@ -107,6 +94,8 @@ test('Returns generated data', async () => { "flashGuid": "0000-0000-0000-000000000000", "regState": "PRO", "regTy": "PRO", + "serverDescription": "Dev Server", + "serverName": "Tower", }, "versions": { "unraid": "6.11.2", diff --git a/api/src/__test__/core/__snapshots__/default-permissions.test.ts.snap b/api/src/__test__/core/__snapshots__/default-permissions.test.ts.snap deleted file mode 100644 index 19621784c..000000000 --- a/api/src/__test__/core/__snapshots__/default-permissions.test.ts.snap +++ /dev/null @@ -1,360 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`Returns default permissions 1`] = ` -{ - "admin": { - "extends": "user", - "permissions": [ - { - "action": "read:any", - "attributes": "*", - "resource": "apikey", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "array", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "cpu", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "crash-reporting-enabled", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "device", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "device/unassigned", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "disk", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "disk/settings", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "display", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "docker/container", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "docker/network", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "flash", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "info", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "license-key", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "machine-id", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "memory", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "notifications", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "online", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "os", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "owner", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "parity-history", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "permission", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "registration", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "servers", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "service", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "service/emhttpd", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "service/unraid-api", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "services", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "share", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "software-versions", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "unraid-version", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "uptime", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "user", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "vars", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "vms", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "vms/domain", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "vms/network", - }, - ], - }, - "guest": { - "permissions": [ - { - "action": "read:any", - "attributes": "*", - "resource": "me", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "welcome", - }, - ], - }, - "my_servers": { - "extends": "guest", - "permissions": [ - { - "action": "read:any", - "attributes": "*", - "resource": "dashboard", - }, - { - "action": "read:own", - "attributes": "*", - "resource": "two-factor", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "array", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "docker/container", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "docker/network", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "notifications", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "vms/domain", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "unraid-version", - }, - ], - }, - "notifier": { - "extends": "guest", - "permissions": [ - { - "action": "create:own", - "attributes": "*", - "resource": "notifications", - }, - ], - }, - "upc": { - "extends": "guest", - "permissions": [ - { - "action": "read:own", - "attributes": "*", - "resource": "apikey", - }, - { - "action": "read:own", - "attributes": "*", - "resource": "cloud", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "config", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "crash-reporting-enabled", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "disk", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "display", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "flash", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "os", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "owner", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "permission", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "registration", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "servers", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "vars", - }, - { - "action": "read:own", - "attributes": "*", - "resource": "connect", - }, - { - "action": "update:own", - "attributes": "*", - "resource": "connect", - }, - ], - }, - "user": { - "extends": "guest", - "permissions": [ - { - "action": "read:own", - "attributes": "*", - "resource": "apikey", - }, - { - "action": "read:any", - "attributes": "*", - "resource": "permission", - }, - ], - }, -} -`; diff --git a/api/src/__test__/core/__snapshots__/permissions.test.ts.snap b/api/src/__test__/core/__snapshots__/permissions.test.ts.snap new file mode 100644 index 000000000..d5f8af658 --- /dev/null +++ b/api/src/__test__/core/__snapshots__/permissions.test.ts.snap @@ -0,0 +1,403 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Returns default permissions 1`] = ` +RolesBuilder { + "_grants": { + "admin": { + "$extend": [ + "guest", + ], + "apikey": { + "read:any": [ + "*", + ], + }, + "array": { + "read:any": [ + "*", + ], + }, + "cloud": { + "read:own": [ + "*", + ], + }, + "config": { + "update:own": [ + "*", + ], + }, + "connect": { + "read:own": [ + "*", + ], + "update:own": [ + "*", + ], + }, + "cpu": { + "read:any": [ + "*", + ], + }, + "crash-reporting-enabled": { + "read:any": [ + "*", + ], + }, + "customizations": { + "read:any": [ + "*", + ], + }, + "device": { + "read:any": [ + "*", + ], + }, + "device/unassigned": { + "read:any": [ + "*", + ], + }, + "disk": { + "read:any": [ + "*", + ], + }, + "disk/settings": { + "read:any": [ + "*", + ], + }, + "display": { + "read:any": [ + "*", + ], + }, + "docker/container": { + "read:any": [ + "*", + ], + }, + "docker/network": { + "read:any": [ + "*", + ], + }, + "flash": { + "read:any": [ + "*", + ], + }, + "info": { + "read:any": [ + "*", + ], + }, + "license-key": { + "read:any": [ + "*", + ], + }, + "logs": { + "read:any": [ + "*", + ], + }, + "machine-id": { + "read:any": [ + "*", + ], + }, + "memory": { + "read:any": [ + "*", + ], + }, + "notifications": { + "create:any": [ + "*", + ], + "read:any": [ + "*", + ], + }, + "online": { + "read:any": [ + "*", + ], + }, + "os": { + "read:any": [ + "*", + ], + }, + "owner": { + "read:any": [ + "*", + ], + }, + "parity-history": { + "read:any": [ + "*", + ], + }, + "permission": { + "read:any": [ + "*", + ], + }, + "registration": { + "read:any": [ + "*", + ], + }, + "servers": { + "read:any": [ + "*", + ], + }, + "service": { + "read:any": [ + "*", + ], + }, + "service/emhttpd": { + "read:any": [ + "*", + ], + }, + "service/unraid-api": { + "read:any": [ + "*", + ], + }, + "services": { + "read:any": [ + "*", + ], + }, + "share": { + "read:any": [ + "*", + ], + }, + "software-versions": { + "read:any": [ + "*", + ], + }, + "unraid-version": { + "read:any": [ + "*", + ], + }, + "uptime": { + "read:any": [ + "*", + ], + }, + "user": { + "read:any": [ + "*", + ], + }, + "vars": { + "read:any": [ + "*", + ], + }, + "vms": { + "read:any": [ + "*", + ], + }, + "vms/domain": { + "read:any": [ + "*", + ], + }, + "vms/network": { + "read:any": [ + "*", + ], + }, + }, + "guest": { + "me": { + "read:any": [ + "*", + ], + }, + "welcome": { + "read:any": [ + "*", + ], + }, + }, + "my_servers": { + "$extend": [ + "guest", + ], + "array": { + "read:any": [ + "*", + ], + }, + "customizations": { + "read:any": [ + "*", + ], + }, + "dashboard": { + "read:any": [ + "*", + ], + }, + "docker/container": { + "read:any": [ + "*", + ], + }, + "docker/network": { + "read:any": [ + "*", + ], + }, + "logs": { + "read:any": [ + "*", + ], + }, + "notifications": { + "read:any": [ + "*", + ], + }, + "unraid-version": { + "read:any": [ + "*", + ], + }, + "vms": { + "read:any": [ + "*", + ], + }, + "vms/domain": { + "read:any": [ + "*", + ], + }, + }, + "notifier": { + "$extend": [ + "guest", + ], + "notifications": { + "create:own": [ + "*", + ], + }, + }, + "upc": { + "$extend": [ + "guest", + ], + "apikey": { + "read:own": [ + "*", + ], + }, + "cloud": { + "read:own": [ + "*", + ], + }, + "config": { + "read:any": [ + "*", + ], + "update:own": [ + "*", + ], + }, + "connect": { + "read:own": [ + "*", + ], + "update:own": [ + "*", + ], + }, + "crash-reporting-enabled": { + "read:any": [ + "*", + ], + }, + "customizations": { + "read:any": [ + "*", + ], + }, + "disk": { + "read:any": [ + "*", + ], + }, + "display": { + "read:any": [ + "*", + ], + }, + "flash": { + "read:any": [ + "*", + ], + }, + "info": { + "read:any": [ + "*", + ], + }, + "logs": { + "read:any": [ + "*", + ], + }, + "os": { + "read:any": [ + "*", + ], + }, + "owner": { + "read:any": [ + "*", + ], + }, + "permission": { + "read:any": [ + "*", + ], + }, + "registration": { + "read:any": [ + "*", + ], + }, + "servers": { + "read:any": [ + "*", + ], + }, + "vars": { + "read:any": [ + "*", + ], + }, + }, + }, + "_isLocked": false, +} +`; diff --git a/api/src/__test__/core/permissions.test.ts b/api/src/__test__/core/permissions.test.ts new file mode 100644 index 000000000..7b923c593 --- /dev/null +++ b/api/src/__test__/core/permissions.test.ts @@ -0,0 +1,6 @@ +import { expect, test } from 'vitest'; +import { setupPermissions } from '@app/core/permissions'; + +test('Returns default permissions', () => { + expect(setupPermissions()).toMatchSnapshot(); +}); diff --git a/api/src/__test__/core/utils/files/config-file-normalizer.test.ts b/api/src/__test__/core/utils/files/config-file-normalizer.test.ts new file mode 100644 index 000000000..fc3b3d2c5 --- /dev/null +++ b/api/src/__test__/core/utils/files/config-file-normalizer.test.ts @@ -0,0 +1,167 @@ +import { test, expect } from 'vitest'; +import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer'; +import { initialState } from '@app/store/modules/config'; +import { cloneDeep } from 'lodash'; + +test('it creates a FLASH config with NO OPTIONAL values', () => { + const basicConfig = initialState; + const config = getWriteableConfig(basicConfig, 'flash'); + expect(config).toMatchInlineSnapshot(` + { + "api": { + "extraOrigins": "", + "version": "", + }, + "local": {}, + "notifier": { + "apikey": "", + }, + "remote": { + "accesstoken": "", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "refreshtoken": "", + "regWizTime": "", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "upc": { + "apikey": "", + }, + } + `); +}); + +test('it creates a MEMORY config with NO OPTIONAL values', () => { + const basicConfig = initialState; + const config = getWriteableConfig(basicConfig, 'memory'); + expect(config).toMatchInlineSnapshot(` + { + "api": { + "extraOrigins": "", + "version": "", + }, + "connectionStatus": { + "minigraph": "PRE_INIT", + }, + "local": {}, + "notifier": { + "apikey": "", + }, + "remote": { + "accesstoken": "", + "allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "refreshtoken": "", + "regWizTime": "", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "upc": { + "apikey": "", + }, + } + `); +}); + +test('it creates a FLASH config with OPTIONAL values', () => { + const basicConfig = cloneDeep(initialState); + basicConfig.remote['2Fa'] = 'yes'; + basicConfig.local['2Fa'] = 'yes'; + basicConfig.local.showT2Fa = 'yes'; + basicConfig.api.extraOrigins = 'myextra.origins'; + basicConfig.remote.upnpEnabled = 'yes'; + basicConfig.connectionStatus.upnpStatus = 'Turned On'; + const config = getWriteableConfig(basicConfig, 'flash'); + expect(config).toMatchInlineSnapshot(` + { + "api": { + "extraOrigins": "myextra.origins", + "version": "", + }, + "local": { + "2Fa": "yes", + "showT2Fa": "yes", + }, + "notifier": { + "apikey": "", + }, + "remote": { + "2Fa": "yes", + "accesstoken": "", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "refreshtoken": "", + "regWizTime": "", + "upnpEnabled": "yes", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "upc": { + "apikey": "", + }, + } + `); +}); + +test('it creates a MEMORY config with OPTIONAL values', () => { + const basicConfig = cloneDeep(initialState); + basicConfig.remote['2Fa'] = 'yes'; + basicConfig.local['2Fa'] = 'yes'; + basicConfig.local.showT2Fa = 'yes'; + basicConfig.api.extraOrigins = 'myextra.origins'; + basicConfig.remote.upnpEnabled = 'yes'; + basicConfig.connectionStatus.upnpStatus = 'Turned On'; + const config = getWriteableConfig(basicConfig, 'memory'); + expect(config).toMatchInlineSnapshot(` + { + "api": { + "extraOrigins": "myextra.origins", + "version": "", + }, + "connectionStatus": { + "minigraph": "PRE_INIT", + "upnpStatus": "Turned On", + }, + "local": { + "2Fa": "yes", + "showT2Fa": "yes", + }, + "notifier": { + "apikey": "", + }, + "remote": { + "2Fa": "yes", + "accesstoken": "", + "allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000", + "apikey": "", + "avatar": "", + "dynamicRemoteAccessType": "DISABLED", + "email": "", + "idtoken": "", + "refreshtoken": "", + "regWizTime": "", + "upnpEnabled": "yes", + "username": "", + "wanaccess": "", + "wanport": "", + }, + "upc": { + "apikey": "", + }, + } + `); +}); diff --git a/api/src/__test__/mothership/index.test.ts b/api/src/__test__/mothership/index.test.ts new file mode 100644 index 000000000..a56b09454 --- /dev/null +++ b/api/src/__test__/mothership/index.test.ts @@ -0,0 +1,48 @@ +import { beforeEach, expect, test, vi } from 'vitest'; + +// Preloading imports for faster tests +import '@app/mothership/utils/convert-to-fuzzy-time'; + +vi.mock('fs', () => ({ + default: { + readFileSync: vi.fn().mockReturnValue('my-file'), + writeFileSync: vi.fn(), + existsSync: vi.fn(), + }, + readFileSync: vi.fn().mockReturnValue('my-file'), + existsSync: vi.fn(), +})); + +vi.mock('@graphql-tools/schema', () => ({ + makeExecutableSchema: vi.fn(), +})); + +vi.mock('@app/core/log', () => ({ + default: { relayLogger: { trace: vi.fn() } }, + relayLogger: { trace: vi.fn() }, + logger: { trace: vi.fn() }, +})); + +beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); +}); + +const generateTestCases = () => { + const cases: Array<{ min: number; max: number }> = []; + for (let i = 0; i < 15; i += 1) { + const min = Math.round(Math.random() * 100); + const max = min + (Math.round(Math.random() * 20)); + cases.push({ min, max }); + } + + return cases; +}; + +test.each(generateTestCases())('Successfully converts to fuzzy time %o', async ({ min, max }) => { + const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time'); + + const res = convertToFuzzyTime(min, max); + expect(res).toBeGreaterThanOrEqual(min); + expect(res).toBeLessThanOrEqual(max); +}); diff --git a/api/src/cli/commands/report.ts b/api/src/cli/commands/report.ts index 2dfe54179..eca2a848f 100644 --- a/api/src/cli/commands/report.ts +++ b/api/src/cli/commands/report.ts @@ -75,15 +75,10 @@ export const getCloudData = async ( const cloud = await client.query({ query: getCloudDocument }); return cloud.data.cloud ?? null; } catch (error: unknown) { - cliLogger.addContext( - 'error-stack', - error instanceof Error ? error.stack : error - ); cliLogger.trace( 'Failed fetching cloud from local graphql with "%s"', error instanceof Error ? error.message : 'Unknown Error' ); - cliLogger.removeContext('error-stack'); return null; } @@ -122,12 +117,10 @@ export const getServersData = async ({ ); return foundServers; } catch (error: unknown) { - cliLogger.addContext('error', error); cliLogger.trace( 'Failed fetching servers from local graphql with "%s"', error instanceof Error ? error.message : 'Unknown Error' ); - cliLogger.removeContext('error'); return { online: [], offline: [], diff --git a/api/src/cli/index.ts b/api/src/cli/index.ts index bdf47ae78..b801257e2 100644 --- a/api/src/cli/index.ts +++ b/api/src/cli/index.ts @@ -8,65 +8,77 @@ import { getters } from '@app/store'; const command = mainOptions.command as unknown as string; export const main = async (...argv: string[]) => { - cliLogger.addContext('envs', env); - cliLogger.debug('Loading env file'); - cliLogger.removeContext('envs'); + cliLogger.debug(env, 'Loading env file'); - // Set envs - setEnv('LOG_TYPE', process.env.LOG_TYPE ?? (command === 'start' || mainOptions.debug ? 'pretty' : 'raw')); - cliLogger.addContext('paths', getters.paths()); - cliLogger.debug('Starting CLI'); - cliLogger.removeContext('paths'); + // Set envs + setEnv( + 'LOG_TYPE', + process.env.LOG_TYPE ?? + (command === 'start' || mainOptions.debug ? 'pretty' : 'raw') + ); + cliLogger.debug({ paths: getters.paths() }, 'Starting CLI'); - setEnv('DEBUG', mainOptions.debug ?? false); - setEnv('ENVIRONMENT', process.env.ENVIRONMENT ?? 'production'); - setEnv('PORT', process.env.PORT ?? mainOptions.port ?? '9000'); - setEnv('LOG_LEVEL', process.env.LOG_LEVEL ?? mainOptions['log-level'] ?? 'INFO'); - if (!process.env.LOG_TRANSPORT) { - if (process.env.ENVIRONMENT === 'production' && !mainOptions.debug) { - setEnv('LOG_TRANSPORT', 'file,errors'); - setEnv('LOG_LEVEL', 'DEBUG'); - } else if (!mainOptions.debug) { - // Staging Environment, backgrounded plugin - setEnv('LOG_TRANSPORT', 'file,errors'); - setEnv('LOG_LEVEL', 'TRACE'); - } else { - cliLogger.debug('In Debug Mode - Log Level Defaulting to: stdout'); - } - } + setEnv('DEBUG', mainOptions.debug ?? false); + setEnv('ENVIRONMENT', process.env.ENVIRONMENT ?? 'production'); + setEnv('PORT', process.env.PORT ?? mainOptions.port ?? '9000'); + setEnv( + 'LOG_LEVEL', + process.env.LOG_LEVEL ?? mainOptions['log-level'] ?? 'INFO' + ); + if (!process.env.LOG_TRANSPORT) { + if (process.env.ENVIRONMENT === 'production' && !mainOptions.debug) { + setEnv('LOG_TRANSPORT', 'file'); + setEnv('LOG_LEVEL', 'DEBUG'); + } else if (!mainOptions.debug) { + // Staging Environment, backgrounded plugin + setEnv('LOG_TRANSPORT', 'file'); + setEnv('LOG_LEVEL', 'TRACE'); + } else { + cliLogger.debug('In Debug Mode - Log Level Defaulting to: stdout'); + } + } - if (!command) { - // Run help command - parse(args, { ...options, partial: true, stopAtFirstUnknown: true, argv: ['-h'] }); - } + if (!command) { + // Run help command + parse(args, { + ...options, + partial: true, + stopAtFirstUnknown: true, + argv: ['-h'], + }); + } - // Only import the command we need when we use it - const commands = { - start: import('@app/cli/commands/start').then(pkg => pkg.start), - stop: import('@app/cli/commands/stop').then(pkg => pkg.stop), - restart: import('@app/cli/commands/restart').then(pkg => pkg.restart), - 'switch-env': import('@app/cli/commands/switch-env').then(pkg => pkg.switchEnv), - version: import('@app/cli/commands/version').then(pkg => pkg.version), - status: import('@app/cli/commands/status').then(pkg => pkg.status), - report: import('@app/cli/commands/report').then(pkg => pkg.report), - 'validate-token': import('@app/cli/commands/validate-token').then(pkg => pkg.validateToken), - }; + // Only import the command we need when we use it + const commands = { + start: import('@app/cli/commands/start').then((pkg) => pkg.start), + stop: import('@app/cli/commands/stop').then((pkg) => pkg.stop), + restart: import('@app/cli/commands/restart').then((pkg) => pkg.restart), + 'switch-env': import('@app/cli/commands/switch-env').then( + (pkg) => pkg.switchEnv + ), + version: import('@app/cli/commands/version').then((pkg) => pkg.version), + status: import('@app/cli/commands/status').then((pkg) => pkg.status), + report: import('@app/cli/commands/report').then((pkg) => pkg.report), + 'validate-token': import('@app/cli/commands/validate-token').then( + (pkg) => pkg.validateToken + ), + }; - // Unknown command - if (!Object.keys(commands).includes(command)) { - throw new Error(`Invalid command "${command}"`); - } + // Unknown command + if (!Object.keys(commands).includes(command)) { + throw new Error(`Invalid command "${command}"`); + } - // Resolve the command import - const commandMethod = await commands[command]; + // Resolve the command import + const commandMethod = await commands[command]; - // Run the command - await commandMethod(...argv); + // Run the command + await commandMethod(...argv); - // Allow the process to exit - // Don't exit when we start though - if (!['start', 'restart'].includes(command)) { - // Ensure process is exited - process.exit(0); - } + // Allow the process to exit + // Don't exit when we start though + if (!['start', 'restart'].includes(command)) { + // Ensure process is exited + process.exit(0); + } }; diff --git a/api/src/common/allowed-origins.ts b/api/src/common/allowed-origins.ts index a2b0f9a01..4f0c6f3d9 100644 --- a/api/src/common/allowed-origins.ts +++ b/api/src/common/allowed-origins.ts @@ -3,7 +3,7 @@ import { uniq } from 'lodash'; import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network'; import { FileLoadStatus } from '@app/store/types'; import { logger } from '../core'; -import { ENVIRONMENT, INTROSPECTION } from '@app/environment'; +import { GRAPHQL_INTROSPECTION } from '@app/environment'; const getAllowedSocks = (): string[] => [ // Notifier bridge @@ -76,7 +76,7 @@ const getConnectOrigins = () : string[] => { } const getApolloSandbox = (): string[] => { - if (INTROSPECTION || ENVIRONMENT === 'development') { + if (GRAPHQL_INTROSPECTION) { return ['https://studio.apollographql.com']; } return []; diff --git a/api/src/common/dashboard/generate-data.ts b/api/src/common/dashboard/generate-data.ts index 20f6a64e6..7ec9da35e 100644 --- a/api/src/common/dashboard/generate-data.ts +++ b/api/src/common/dashboard/generate-data.ts @@ -1,58 +1,64 @@ import { ConnectListAllDomainsFlags } from '@vmngr/libvirt'; import { getHypervisor } from '@app/core/utils/vms/get-hypervisor'; -import display from '@app/graphql/resolvers/query/display'; import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version'; import { getArray } from '@app/common/dashboard/get-array'; import { bootTimestamp } from '@app/common/dashboard/boot-timestamp'; import { dashboardLogger } from '@app/core/log'; import { getters, store } from '@app/store'; -import { type DashboardServiceInput, type DashboardInput } from '@app/graphql/generated/client/graphql'; +import { + type DashboardServiceInput, + type DashboardInput, +} from '@app/graphql/generated/client/graphql'; import { API_VERSION } from '@app/environment'; import { DynamicRemoteAccessType } from '@app/remoteAccess/types'; import { DashboardInputSchema } from '@app/graphql/generated/client/validators'; import { ZodError } from 'zod'; const getVmSummary = async (): Promise => { - try { - const hypervisor = await getHypervisor(); - if (!hypervisor) { - return { - installed: 0, - started: 0, - }; - } + try { + const hypervisor = await getHypervisor(); + if (!hypervisor) { + return { + installed: 0, + started: 0, + }; + } - const activeDomains = await hypervisor.connectListAllDomains(ConnectListAllDomainsFlags.ACTIVE) as unknown[]; - const inactiveDomains = await hypervisor.connectListAllDomains(ConnectListAllDomainsFlags.INACTIVE) as unknown[]; - return { - installed: activeDomains.length + inactiveDomains.length, - started: activeDomains.length, - }; - } catch { - return { - installed: 0, - started: 0, - }; - } + const activeDomains = (await hypervisor.connectListAllDomains( + ConnectListAllDomainsFlags.ACTIVE + )) as unknown[]; + const inactiveDomains = (await hypervisor.connectListAllDomains( + ConnectListAllDomainsFlags.INACTIVE + )) as unknown[]; + return { + installed: activeDomains.length + inactiveDomains.length, + started: activeDomains.length, + }; + } catch { + return { + installed: 0, + started: 0, + }; + } }; const getDynamicRemoteAccessService = (): DashboardServiceInput | null => { - const { config, dynamicRemoteAccess } = store.getState(); - const enabledStatus = config.remote.dynamicRemoteAccessType; + const { config, dynamicRemoteAccess } = store.getState(); + const enabledStatus = config.remote.dynamicRemoteAccessType; - return { - name: 'dynamic-remote-access', - online: enabledStatus !== DynamicRemoteAccessType.DISABLED, - version: dynamicRemoteAccess.runningType, - uptime: { - timestamp: bootTimestamp.toISOString(), - }, - }; + return { + name: 'dynamic-remote-access', + online: enabledStatus !== DynamicRemoteAccessType.DISABLED, + version: dynamicRemoteAccess.runningType, + uptime: { + timestamp: bootTimestamp.toISOString(), + }, + }; }; const services = (): DashboardInput['services'] => { - const dynamicRemoteAccess = getDynamicRemoteAccessService(); - return [ + const dynamicRemoteAccess = getDynamicRemoteAccessService(); + return [ { name: 'unraid-api', online: true, @@ -66,63 +72,81 @@ const services = (): DashboardInput['services'] => { }; const getData = async (): Promise => { - const emhttp = getters.emhttp(); - const docker = getters.docker(); + const emhttp = getters.emhttp(); + const docker = getters.docker(); - return { - vars: { - regState: emhttp.var.regState, - regTy: emhttp.var.regTy, - flashGuid: emhttp.var.flashGuid, - serverName: emhttp.var.name, - serverDescription: emhttp.var.comment, - }, - apps: { - installed: docker.installed ?? 0, - started: docker.running ?? 0 - }, - versions: { - unraid: await getUnraidVersion(), - }, - os: { - hostname: emhttp.var.name, - uptime: bootTimestamp.toISOString() - }, - vms: await getVmSummary(), - array: getArray(), - services: services(), - display: await display(), - config: emhttp.var.configValid ? { valid: true } : { - valid: false, - error: { - error: 'UNKNOWN_ERROR', - invalid: 'INVALID', - nokeyserver: 'NO_KEY_SERVER', - withdrawn: 'WITHDRAWN', - }[emhttp.var.configState] ?? 'UNKNOWN_ERROR', - }, - }; + return { + vars: { + regState: emhttp.var.regState, + regTy: emhttp.var.regTy, + flashGuid: emhttp.var.flashGuid, + serverName: emhttp.var.name, + serverDescription: emhttp.var.comment, + }, + apps: { + installed: docker.installed ?? 0, + started: docker.running ?? 0, + }, + versions: { + unraid: await getUnraidVersion(), + }, + os: { + hostname: emhttp.var.name, + uptime: bootTimestamp.toISOString(), + }, + vms: await getVmSummary(), + array: getArray(), + services: services(), + display: { + case: { + url: '', + icon: '', + error: '', + base64: '', + }, + }, + config: emhttp.var.configValid + ? { valid: true } + : { + valid: false, + error: + { + error: 'UNKNOWN_ERROR', + invalid: 'INVALID', + nokeyserver: 'NO_KEY_SERVER', + withdrawn: 'WITHDRAWN', + }[emhttp.var.configState] ?? 'UNKNOWN_ERROR', + }, + }; }; export const generateData = async (): Promise => { - const data = await getData(); + const data = await getData(); - try { - // Validate generated data - // @TODO: Fix this runtype to use generated types from the Zod validators (as seen in mothership Codegen) - const result = DashboardInputSchema().parse(data) + try { + // Validate generated data + // @TODO: Fix this runtype to use generated types from the Zod validators (as seen in mothership Codegen) + const result = DashboardInputSchema().parse(data); - return result + return result; + } catch (error: unknown) { + // Log error for user + if (error instanceof ZodError) { + dashboardLogger.error( + 'Failed validation with issues: ', + error.issues.map((issue) => ({ + message: issue.message, + path: issue.path.join(','), + })) + ); + } else { + dashboardLogger.error( + 'Failed validating dashboard object: ', + error, + data + ); + } + } - } catch (error: unknown) { - // Log error for user - if (error instanceof ZodError) { - dashboardLogger.error('Failed validation with issues: ' , error.issues.map(issue => ({ message: issue.message, path: issue.path.join(',') }))) - } else { - dashboardLogger.error('Failed validating dashboard object: ', error, data); - } - } - - return null; + return null; }; - diff --git a/api/src/core/default-permissions.ts b/api/src/core/default-permissions.ts deleted file mode 100644 index 67a2fcbcb..000000000 --- a/api/src/core/default-permissions.ts +++ /dev/null @@ -1,122 +0,0 @@ -export interface Permission { resource: string, action: string, attributes: string } -export interface Role { - permissions: Array - extends?: string; -} - -export const admin: Role = { - extends: 'user', - permissions: [ - // @NOTE: Uncomment the first line to enable creation of api keys. - // See the README.md for more information. - // @WARNING: This is currently unsupported, please be careful. - // { resource: 'apikey', action: 'create:any', attributes: '*' }, - { resource: 'apikey', action: 'read:any', attributes: '*' }, - { resource: 'array', action: 'read:any', attributes: '*' }, - { resource: 'cpu', action: 'read:any', attributes: '*' }, - { - resource: 'crash-reporting-enabled', - action: 'read:any', - attributes: '*', - }, - { resource: 'device', action: 'read:any', attributes: '*' }, - { resource: 'device/unassigned', action: 'read:any', attributes: '*' }, - { resource: 'disk', action: 'read:any', attributes: '*' }, - { resource: 'disk/settings', action: 'read:any', attributes: '*' }, - { resource: 'display', action: 'read:any', attributes: '*' }, - { resource: 'docker/container', action: 'read:any', attributes: '*' }, - { resource: 'docker/network', action: 'read:any', attributes: '*' }, - { resource: 'flash', action: 'read:any', attributes: '*' }, - { resource: 'info', action: 'read:any', attributes: '*' }, - { resource: 'license-key', action: 'read:any', attributes: '*' }, - { resource: 'machine-id', action: 'read:any', attributes: '*' }, - { resource: 'memory', action: 'read:any', attributes: '*' }, - { resource: 'notifications', action: 'read:any', attributes: '*' }, - { resource: 'online', action: 'read:any', attributes: '*' }, - { resource: 'os', action: 'read:any', attributes: '*' }, - { resource: 'owner', action: 'read:any', attributes: '*' }, - { resource: 'parity-history', action: 'read:any', attributes: '*' }, - { resource: 'permission', action: 'read:any', attributes: '*' }, - { resource: 'registration', action: 'read:any', attributes: '*' }, - { resource: 'servers', action: 'read:any', attributes: '*' }, - { resource: 'service', action: 'read:any', attributes: '*' }, - { resource: 'service/emhttpd', action: 'read:any', attributes: '*' }, - { resource: 'service/unraid-api', action: 'read:any', attributes: '*' }, - { resource: 'services', action: 'read:any', attributes: '*' }, - { resource: 'share', action: 'read:any', attributes: '*' }, - { resource: 'software-versions', action: 'read:any', attributes: '*' }, - { resource: 'unraid-version', action: 'read:any', attributes: '*' }, - { resource: 'uptime', action: 'read:any', attributes: '*' }, - { resource: 'user', action: 'read:any', attributes: '*' }, - { resource: 'vars', action: 'read:any', attributes: '*' }, - { resource: 'vms', action: 'read:any', attributes: '*' }, - { resource: 'vms/domain', action: 'read:any', attributes: '*' }, - { resource: 'vms/network', action: 'read:any', attributes: '*' }, - ], -}; - -export const user: Role = { - extends: 'guest', - permissions: [ - { resource: 'apikey', action: 'read:own', attributes: '*' }, - { resource: 'permission', action: 'read:any', attributes: '*' }, - ], -}; - -export const upc: Role = { - extends: 'guest', - permissions: [ - { resource: 'apikey', action: 'read:own', attributes: '*' }, - { resource: 'cloud', action: 'read:own', attributes: '*' }, - { resource: 'config', action: 'read:any', attributes: '*' }, - { resource: 'crash-reporting-enabled', action: 'read:any', attributes: '*' }, - { resource: 'disk', action: 'read:any', attributes: '*' }, - { resource: 'display', action: 'read:any', attributes: '*' }, - { resource: 'flash', action: 'read:any', attributes: '*' }, - { resource: 'os', action: 'read:any', attributes: '*' }, - { resource: 'owner', action: 'read:any', attributes: '*' }, - { resource: 'permission', action: 'read:any', attributes: '*' }, - { resource: 'registration', action: 'read:any', attributes: '*' }, - { resource: 'servers', action: 'read:any', attributes: '*' }, - { resource: 'vars', action: 'read:any', attributes: '*' }, - { resource: 'connect', action: 'read:own', attributes: '*' }, - { resource: 'connect', action: 'update:own', attributes: '*' } - ], -}; - -export const my_servers: Role = { - extends: 'guest', - permissions: [ - { resource: 'dashboard', action: 'read:any', attributes: '*' }, - { resource: 'two-factor', action: 'read:own', attributes: '*' }, - { resource: 'array', action: 'read:any', attributes: '*' }, - { resource: 'docker/container', action: 'read:any', attributes: '*' }, - { resource: 'docker/network', action: 'read:any', attributes: '*' }, - { resource: 'notifications', action: 'read:any', attributes: '*' }, - { resource: 'vms/domain', action: 'read:any', attributes: '*' }, - { resource: 'unraid-version', action: 'read:any', attributes: '*' }, - ], -}; - -export const notifier: Role = { - extends: 'guest', - permissions: [ - { resource: 'notifications', action: 'create:own', attributes: '*' }, - ], -}; - -export const guest: Role = { - permissions: [ - { resource: 'me', action: 'read:any', attributes: '*' }, - { resource: 'welcome', action: 'read:any', attributes: '*' }, - ], -}; - -export const permissions: Record = { - guest, - user, - admin, - upc, - my_servers, - notifier, -}; diff --git a/api/src/core/log.ts b/api/src/core/log.ts index 4cf9636c5..3307e1110 100644 --- a/api/src/core/log.ts +++ b/api/src/core/log.ts @@ -1,137 +1,108 @@ -import chalk from 'chalk'; -import { configure, getLogger } from 'log4js'; -import { serializeError } from 'serialize-error'; +import { pino } from 'pino'; +import { LOG_TRANSPORT, LOG_TYPE } from '@app/environment'; -export const levels = ['ALL', 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'MARK', 'OFF'] as const; +import pretty from 'pino-pretty'; -const contextEnabled = Boolean(process.env.LOG_CONTEXT); -const stackEnabled = Boolean(process.env.LOG_STACKTRACE); -const tracingEnabled = Boolean(process.env.LOG_TRACING); -const fullLoggingPattern = chalk`{gray [%d]} %x\{id\} %[[%p]%] %[[%c]%] %m{gray %x\{context\}}${tracingEnabled ? ' %[%f:%l%]' : ''}`; -const minimumLoggingPattern = '%m'; -const appenders = process.env.LOG_TRANSPORT?.split(',').map(transport => transport.trim()) ?? ['out']; -const level = levels[levels.indexOf(process.env.LOG_LEVEL?.toUpperCase() as typeof levels[number])] ?? 'INFO'; -const logLayout = { - type: 'pattern', - // Depending on what this env is set to we'll either get raw or pretty logs - // The reason we do this is to allow the app to change this value - // This way pretty logs can be turned off programmatically - pattern: process.env.LOG_TYPE === 'pretty' ? fullLoggingPattern : minimumLoggingPattern, - tokens: { - id() { - return chalk`{gray [${process.pid}]}`; - }, - context({ context }: { context?: any }) { - if (!contextEnabled || !context) { - return ''; - } +export const levels = [ + 'trace', + 'debug', + 'info', + 'warn', + 'error', + 'fatal', +] as const; - try { - const contextEntries = Object.entries(context) - .map(([key, value]) => [key, value instanceof Error ? (stackEnabled ? serializeError(value) : value) : value]) - .filter(([key]) => key !== 'pid'); - const cleanContext = Object.fromEntries(contextEntries); - return `\n${Object.entries(cleanContext).map(([key, value]) => `${key}=${JSON.stringify(value, null, 2)}`).join(' ')}`; - } catch (error: unknown) { - const errorInfo = error instanceof Error ? `${error.message}: ${error.stack ?? 'no stack'}` : 'Error not instance of error'; - return `Error generating context: ${errorInfo}`; - } - }, - }, -}; +const level = + levels[ + levels.indexOf( + process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number] + ) + ] ?? 'info'; -if (process.env.NODE_ENV !== 'test') { - // We log to both the stdout and log file - // The log file should be changed to errors only unless in debug mode - configure({ - appenders: { - file: { - type: 'file', - filename: '/var/log/unraid-api/stdout.log', - maxLogSize: 10_000_000, - backups: 0, - layout: { - ...logLayout, - // File logs should always be pretty - pattern: fullLoggingPattern, - }, - }, - errorFile: { - type: 'file', - filename: '/var/log/unraid-api/stderr.log', - maxLogSize: 2_500_000, - backups: 0, - layout: { - ...logLayout, - // File logs should always be pretty - pattern: fullLoggingPattern, - }, - }, - out: { - type: 'stdout', - layout: logLayout, - }, - errors: { type: 'logLevelFilter', appender: 'errorFile', level: 'error' }, - }, - categories: { - default: { - appenders, - level, - enableCallStack: tracingEnabled, - }, - }, - }); -} +const logDestination = + LOG_TRANSPORT === 'file' ? pino.destination('/var/log/unraid-api/stdout.log') : 1; -export const internalLogger = getLogger('internal'); -export const logger = getLogger('app'); -export const mothershipLogger = getLogger('mothership'); -export const dashboardLogger = getLogger('dashboard'); -export const emhttpLogger = getLogger('emhttp'); -export const libvirtLogger = getLogger('libvirt'); -export const graphqlLogger = getLogger('graphql'); -export const dockerLogger = getLogger('docker'); -export const cliLogger = getLogger('cli'); -export const minigraphLogger = getLogger('minigraph'); -export const cloudConnectorLogger = getLogger('cloud-connector'); -export const upnpLogger = getLogger('upnp'); -export const keyServerLogger = getLogger('key-server'); -export const remoteAccessLogger = getLogger('remote-access'); -export const remoteQueryLogger = getLogger('remote-query'); +const stream = + LOG_TYPE === 'pretty' + ? pretty({ + singleLine: true, + hideObject: false, + colorize: true, + ignore: 'time,hostname,pid', + destination: logDestination, + }) + : pino.destination(logDestination); + +export const logger = pino( + { + level, + timestamp: () => `,"time":"${new Date().toISOString()}"`, + formatters: { + level: (label: string) => ({ level: label }), + }, + }, + stream +); + +export const internalLogger = logger.child({ logger: 'internal' }); +export const appLogger = logger.child({ logger: 'app' }); +export const mothershipLogger = logger.child({ logger: 'mothership' }); +export const dashboardLogger = logger.child({ logger: 'dashboard' }); +export const emhttpLogger = logger.child({ logger: 'emhttp' }); +export const libvirtLogger = logger.child({ logger: 'libvirt' }); +export const graphqlLogger = logger.child({ logger: 'graphql' }); +export const dockerLogger = logger.child({ logger: 'docker' }); +export const cliLogger = logger.child({ logger: 'cli' }); +export const minigraphLogger = logger.child({ logger: 'minigraph' }); +export const cloudConnectorLogger = logger.child({ logger: 'cloud-connector' }); +export const upnpLogger = logger.child({ logger: 'upnp' }); +export const keyServerLogger = logger.child({ logger: 'key-server' }); +export const remoteAccessLogger = logger.child({ logger: 'remote-access' }); +export const remoteQueryLogger = logger.child({ logger: 'remote-query' }); +export const apiLogger = logger.child({ logger: 'api' }); export const loggers = [ - logger, - mothershipLogger, - dashboardLogger, - emhttpLogger, - libvirtLogger, - graphqlLogger, - dockerLogger, - cliLogger, - minigraphLogger, - cloudConnectorLogger, - upnpLogger, - keyServerLogger, - remoteAccessLogger, - remoteQueryLogger, + internalLogger, + appLogger, + mothershipLogger, + dashboardLogger, + emhttpLogger, + libvirtLogger, + graphqlLogger, + dockerLogger, + cliLogger, + minigraphLogger, + cloudConnectorLogger, + upnpLogger, + keyServerLogger, + remoteAccessLogger, + remoteQueryLogger, + apiLogger ]; // Send SIGUSR1 to increase log level process.on('SIGUSR1', () => { - const level = typeof logger.level === 'string' ? logger.level : logger.level.levelStr; - const nextLevel = levels[levels.findIndex(_level => _level === level) + 1] ?? levels[0]; - loggers.forEach(logger => { - logger.level = nextLevel; - }); - internalLogger.mark('Log level changed from %s to %s', level, nextLevel); + const level = logger.level; + const nextLevel = + levels[levels.findIndex((_level) => _level === level) + 1] ?? levels[0]; + loggers.forEach((logger) => { + logger.level = nextLevel; + }); + internalLogger.info({ + message: `Log level changed from ${level} to ${nextLevel}`, + }); }); // Send SIGUSR1 to decrease log level process.on('SIGUSR2', () => { - const level = typeof logger.level === 'string' ? logger.level : logger.level.levelStr; - const nextLevel = levels[levels.findIndex(_level => _level === level) - 1] ?? levels[levels.length - 1]; - loggers.forEach(logger => { - logger.level = nextLevel; - }); - internalLogger.mark('Log level changed from %s to %s', level, nextLevel); + const level = logger.level; + const nextLevel = + levels[levels.findIndex((_level) => _level === level) - 1] ?? + levels[levels.length - 1]; + loggers.forEach((logger) => { + logger.level = nextLevel; + }); + internalLogger.info({ + message: `Log level changed from ${level} to ${nextLevel}`, + }); }); diff --git a/api/src/core/logrotate/setup-logrotate.ts b/api/src/core/logrotate/setup-logrotate.ts new file mode 100644 index 000000000..26d485f54 --- /dev/null +++ b/api/src/core/logrotate/setup-logrotate.ts @@ -0,0 +1,20 @@ +import { writeFile } from 'fs/promises'; +import { fileExists } from '@app/core/utils/files/file-exists'; + +export const setupLogRotation = async () => { + if (await fileExists('/etc/logrotate.d/unraid-api')) { + return; + } else { + await writeFile( + '/etc/logrotate.d/unraid-api', + ` + /var/log/unraid-api/*.log { + rotate 2 + missingok + size 5M + } + `, + { mode: '644' } + ); + } +}; diff --git a/api/src/core/modules/add-license-key.ts b/api/src/core/modules/add-license-key.ts deleted file mode 100644 index bcbaa52ec..000000000 --- a/api/src/core/modules/add-license-key.ts +++ /dev/null @@ -1,126 +0,0 @@ -// import fs from 'fs'; -// import { log } from '../log'; -import type { CoreContext, CoreResult } from '@app/core/types'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { NotImplementedError } from '@app/core/errors/not-implemented-error'; -import { AppError } from '@app/core/errors/app-error'; -import { getters } from '@app/store'; - -interface Context extends CoreContext { - data: { - keyUri?: string; - trial?: boolean; - replacement?: boolean; - email?: string; - keyFile?: string; - }; -} - -interface Result extends CoreResult { - json: { - key?: string; - type?: string; - }; -} - -/** - * Register a license key. - */ -export const addLicenseKey = async (context: Context): Promise => { - ensurePermission(context.user, { - resource: 'license-key', - action: 'create', - possession: 'any', - }); - - // Const { data } = context; - const emhttp = getters.emhttp(); - const guid = emhttp.var.regGuid; - // Const timestamp = new Date(); - - if (!guid) { - throw new AppError('guid missing'); - } - - throw new NotImplementedError(); - - // // Connect to unraid.net to request a trial key - // if (data?.trial) { - // const body = new FormData(); - // body.append('guid', guid); - // body.append('timestamp', timestamp.getTime().toString()); - - // const key = await got('https://keys.lime-technology.com/account/trial', { method: 'POST', body }) - // .then(response => JSON.parse(response.body)) - // .catch(error => { - // log.error(error); - // throw new AppError(`Sorry, a HTTP ${error.status} error occurred while registering USB Flash GUID ${guid}`); - // }); - - // // Update the trial key file - // await fs.promises.writeFile('/boot/config/Trial.key', Buffer.from(key, 'base64')); - - // return { - // text: 'Thank you for registering, your trial key has been accepted.', - // json: { - // key - // } - // }; - // } - - // // Connect to unraid.net to request a new replacement key - // if (data?.replacement) { - // const { email, keyFile } = data; - - // if (!email || !keyFile) { - // throw new AppError('email or keyFile is missing'); - // } - - // const body = new FormData(); - // body.append('guid', guid); - // body.append('timestamp', timestamp.getTime().toString()); - // body.append('email', email); - // body.append('keyfile', keyFile); - - // const { body: key } = await got('https://keys.lime-technology.com/account/license/transfer', { method: 'POST', body }) - // .then(response => JSON.parse(response.body)) - // .catch(error => { - // log.error(error); - // throw new AppError(`Sorry, a HTTP ${error.status} error occurred while issuing a replacement for USB Flash GUID ${guid}`); - // }); - - // // Update the trial key file - // await fs.promises.writeFile('/boot/config/Trial.key', Buffer.from(key, 'base64')); - - // return { - // text: 'Thank you for registering, your trial key has been registered.', - // json: { - // key - // } - // }; - // } - - // // Register a new server - // if (data?.keyUri) { - // const parts = data.keyUri.split('.key')[0].split('/'); - // const { [parts.length - 1]: keyType } = parts; - - // // Download key blob - // const { body: key } = await got(data.keyUri) - // .then(response => JSON.parse(response.body)) - // .catch(error => { - // log.error(error); - // throw new AppError(`Sorry, a HTTP ${error.status} error occurred while registering your key for USB Flash GUID ${guid}`); - // }); - - // // Save key file - // await fs.promises.writeFile(`/boot/config/${keyType}.key`, Buffer.from(key, 'base64')); - - // return { - // text: `Thank you for registering, your ${keyType} key has been accepted.`, - // json: { - // type: keyType - // } - // }; - // } -}; diff --git a/api/src/core/modules/add-user.ts b/api/src/core/modules/add-user.ts index d691b71ba..7a506c96b 100644 --- a/api/src/core/modules/add-user.ts +++ b/api/src/core/modules/add-user.ts @@ -23,7 +23,6 @@ interface Context extends CoreContext { */ export const addUser = async (context: Context): Promise => { const { data } = context; - // Check permissions ensurePermission(context.user, { resource: 'user', diff --git a/api/src/core/modules/docker/get-docker-networks.ts b/api/src/core/modules/docker/get-docker-networks.ts index fa82ac6a4..6dc82330d 100644 --- a/api/src/core/modules/docker/get-docker-networks.ts +++ b/api/src/core/modules/docker/get-docker-networks.ts @@ -1,7 +1,8 @@ import camelCaseKeys from 'camelcase-keys'; -import { docker, ensurePermission } from '@app/core/utils'; +import { docker } from '@app/core/utils'; import { type CoreContext, type CoreResult } from '@app/core/types'; import { catchHandlers } from '@app/core/utils/misc/catch-handlers'; +import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; export const getDockerNetworks = async (context: CoreContext): Promise => { const { user } = context; diff --git a/api/src/core/modules/get-array.ts b/api/src/core/modules/get-array.ts deleted file mode 100644 index 5ffe777b9..000000000 --- a/api/src/core/modules/get-array.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { store } from '@app/store'; -import { type QueryResolvers } from '@app/graphql/generated/api/types'; -import { getArrayData } from '@app/core/modules/array/get-array-data'; - -/** - * Get array info. - * @returns Array state and array/disk capacity. - */ -export const getArray: QueryResolvers['array'] = ( - _, - __, - context -) => { - const { user } = context; - - // Check permissions - ensurePermission(user, { - resource: 'array', - action: 'read', - possession: 'any', - }); - - return getArrayData(store.getState); -}; diff --git a/api/src/core/modules/get-disks.ts b/api/src/core/modules/get-disks.ts index 55c3ee6e4..be2f4a5a4 100644 --- a/api/src/core/modules/get-disks.ts +++ b/api/src/core/modules/get-disks.ts @@ -86,18 +86,8 @@ const parseDisk = async ( * Get all disks. */ export const getDisks = async ( - context: Context, options?: { temperature: boolean } ): Promise => { - const { user } = context; - - // Check permissions - ensurePermission(user, { - resource: 'disk', - action: 'read', - possession: 'any', - }); - // Return all fields but temperature if (options?.temperature === false) { const partitions = await blockDevices().then((devices) => diff --git a/api/src/core/modules/get-unassigned-devices.ts b/api/src/core/modules/get-unassigned-devices.ts deleted file mode 100644 index 325c7ff3a..000000000 --- a/api/src/core/modules/get-unassigned-devices.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { AppError } from '@app/core/errors/app-error'; -import type { CoreResult, CoreContext } from '@app/core/types'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; - -/** - * Get all unassigned devices. - */ -export const getUnassignedDevices = async (context: CoreContext): Promise => { - const { user } = context; - - // Bail if the user doesn't have permission - ensurePermission(user, { - resource: 'devices/unassigned', - action: 'read', - possession: 'any', - }); - - const devices = []; - - if (devices.length === 0) { - throw new AppError('No devices found.', 404); - } - - return { - text: `Unassigned devices: ${JSON.stringify(devices, null, 2)}`, - json: devices, - }; -}; diff --git a/api/src/core/modules/get-vars.ts b/api/src/core/modules/get-vars.ts deleted file mode 100644 index 18f01d9b3..000000000 --- a/api/src/core/modules/get-vars.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { CoreContext, CoreResult } from '@app/core/types'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { getters } from '@app/store'; - -/** - * Get all system vars. - */ -export const getVars = async (context: CoreContext): Promise => { - const { user } = context; - - // Bail if the user doesn't have permission - ensurePermission(user, { - resource: 'vars', - action: 'read', - possession: 'any', - }); - - const emhttp = getters.emhttp(); - - return { - text: `Vars: ${JSON.stringify(emhttp.var, null, 2)}`, - json: { - ...emhttp.var, - }, - }; -}; diff --git a/api/src/core/modules/index.ts b/api/src/core/modules/index.ts new file mode 100644 index 000000000..d013db1f3 --- /dev/null +++ b/api/src/core/modules/index.ts @@ -0,0 +1,23 @@ +// Created from 'create-ts-index' + +export * from './array'; +export * from './debug'; +export * from './disks'; +export * from './docker'; +export * from './services'; +export * from './settings'; +export * from './shares'; +export * from './users'; +export * from './vms'; +export * from './add-share'; +export * from './add-user'; +export * from './get-all-shares'; +export * from './get-apps'; +export * from './get-devices'; +export * from './get-disks'; +export * from './get-me'; +export * from './get-parity-history'; +export * from './get-permissions'; +export * from './get-services'; +export * from './get-users'; +export * from './get-welcome'; diff --git a/api/src/core/modules/services/get-emhttpd.ts b/api/src/core/modules/services/get-emhttpd.ts index f91578c0e..63d812d91 100644 --- a/api/src/core/modules/services/get-emhttpd.ts +++ b/api/src/core/modules/services/get-emhttpd.ts @@ -1,7 +1,7 @@ import { execa } from 'execa'; -import { ensurePermission } from '@app/core/utils'; import { type CoreContext, type CoreResult } from '@app/core/types'; import { cleanStdout } from '@app/core/utils/misc/clean-stdout'; +import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; interface Result extends CoreResult { json: { diff --git a/api/src/core/modules/shares/name/get-share.ts b/api/src/core/modules/shares/name/get-share.ts index 11fbcf761..d563164d9 100644 --- a/api/src/core/modules/shares/name/get-share.ts +++ b/api/src/core/modules/shares/name/get-share.ts @@ -1,7 +1,8 @@ import type { CoreContext, CoreResult } from '@app/core/types/global'; import type { UserShare, DiskShare } from '@app/core/types/states/share'; import { AppError } from '@app/core/errors/app-error'; -import { getShares, ensurePermission } from '@app/core/utils'; +import { getShares } from '@app/core/utils'; +import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; interface Context extends CoreContext { params: { diff --git a/api/src/core/modules/vms/get-domains.ts b/api/src/core/modules/vms/get-domains.ts index 397ea53f2..425431f40 100644 --- a/api/src/core/modules/vms/get-domains.ts +++ b/api/src/core/modules/vms/get-domains.ts @@ -1,7 +1,6 @@ import { ConnectListAllDomainsFlags } from '@vmngr/libvirt'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; import { getHypervisor } from '@app/core/utils/vms/get-hypervisor'; -import { VmState, type VmDomain, type VmsResolvers } from '@app/graphql/generated/api/types'; +import { VmState, type VmDomain } from '@app/graphql/generated/api/types'; import { GraphQLError } from 'graphql'; const states = { @@ -18,19 +17,7 @@ const states = { /** * Get vm domains. */ -export const domainResolver: VmsResolvers['domain'] = async ( - _, - __, - context -) => { - const { user } = context; - - // Check permissions - ensurePermission(user, { - resource: 'vms/domain', - action: 'read', - possession: 'any', - }); +export const getDomains =async () => { try { const hypervisor = await getHypervisor(); diff --git a/api/src/core/permissions.ts b/api/src/core/permissions.ts index 3b1257349..d7c10949d 100644 --- a/api/src/core/permissions.ts +++ b/api/src/core/permissions.ts @@ -1,33 +1,190 @@ -import { logger } from '@app/core/log'; -import { permissions as defaultPermissions } from '@app/core/default-permissions'; -import { AccessControl } from 'accesscontrol'; +import { apiLogger } from '@app/core/log'; +import { RolesBuilder } from 'nest-access-control'; + +export interface Permission { + role?: string; + resource: string; + action: string; + attributes: string; +} +export interface Role { + permissions: Array; + extends?: string; +} // Use built in permissions -const getPermissions = () => defaultPermissions; - -// Build permissions array -const roles = getPermissions(); -const permissions = Object.entries(roles).flatMap(([roleName, role]) => [ - ...(role?.permissions ?? []).map(permission => ({ - ...permission, - role: roleName, - })), -]); - -// Grant permissions -const ac = new AccessControl(permissions); - -// Extend roles -Object.entries(getPermissions()).forEach(([roleName, role]) => { - if (role.extends) { - ac.extendRole(roleName, role.extends); - } -}); - -logger.addContext('permissions', permissions); -logger.trace('Loaded permissions'); -logger.removeContext('permissions'); - -export { - ac, +const roles: Record = { + admin: { + extends: 'guest', + permissions: [ + { resource: 'apikey', action: 'read:any', attributes: '*' }, + { resource: 'cloud', action: 'read:own', attributes: '*' }, + { resource: 'config', action: 'update:own', attributes: '*' }, + { resource: 'connect', action: 'read:own', attributes: '*' }, + { resource: 'connect', action: 'update:own', attributes: '*' }, + { resource: 'customizations', action: 'read:any', attributes: '*' }, + { resource: 'array', action: 'read:any', attributes: '*' }, + { resource: 'cpu', action: 'read:any', attributes: '*' }, + { + resource: 'crash-reporting-enabled', + action: 'read:any', + attributes: '*', + }, + { resource: 'device', action: 'read:any', attributes: '*' }, + { + resource: 'device/unassigned', + action: 'read:any', + attributes: '*', + }, + { resource: 'disk', action: 'read:any', attributes: '*' }, + { resource: 'disk/settings', action: 'read:any', attributes: '*' }, + { resource: 'display', action: 'read:any', attributes: '*' }, + { + resource: 'docker/container', + action: 'read:any', + attributes: '*', + }, + { resource: 'docker/network', action: 'read:any', attributes: '*' }, + { resource: 'flash', action: 'read:any', attributes: '*' }, + { resource: 'info', action: 'read:any', attributes: '*' }, + { resource: 'license-key', action: 'read:any', attributes: '*' }, + { resource: 'logs', action: 'read:any', attributes: '*' }, + { resource: 'machine-id', action: 'read:any', attributes: '*' }, + { resource: 'memory', action: 'read:any', attributes: '*' }, + { resource: 'notifications', action: 'read:any', attributes: '*' }, + { + resource: 'notifications', + action: 'create:any', + attributes: '*', + }, + { resource: 'online', action: 'read:any', attributes: '*' }, + { resource: 'os', action: 'read:any', attributes: '*' }, + { resource: 'owner', action: 'read:any', attributes: '*' }, + { resource: 'parity-history', action: 'read:any', attributes: '*' }, + { resource: 'permission', action: 'read:any', attributes: '*' }, + { resource: 'registration', action: 'read:any', attributes: '*' }, + { resource: 'servers', action: 'read:any', attributes: '*' }, + { resource: 'service', action: 'read:any', attributes: '*' }, + { + resource: 'service/emhttpd', + action: 'read:any', + attributes: '*', + }, + { + resource: 'service/unraid-api', + action: 'read:any', + attributes: '*', + }, + { resource: 'services', action: 'read:any', attributes: '*' }, + { resource: 'share', action: 'read:any', attributes: '*' }, + { + resource: 'software-versions', + action: 'read:any', + attributes: '*', + }, + { resource: 'unraid-version', action: 'read:any', attributes: '*' }, + { resource: 'uptime', action: 'read:any', attributes: '*' }, + { resource: 'user', action: 'read:any', attributes: '*' }, + { resource: 'vars', action: 'read:any', attributes: '*' }, + { resource: 'vms', action: 'read:any', attributes: '*' }, + { resource: 'vms/domain', action: 'read:any', attributes: '*' }, + { resource: 'vms/network', action: 'read:any', attributes: '*' }, + ], + }, + upc: { + extends: 'guest', + permissions: [ + { resource: 'apikey', action: 'read:own', attributes: '*' }, + { resource: 'cloud', action: 'read:own', attributes: '*' }, + { resource: 'config', action: 'read:any', attributes: '*' }, + { + resource: 'crash-reporting-enabled', + action: 'read:any', + attributes: '*', + }, + { resource: 'customizations', action: 'read:any', attributes: '*' }, + { resource: 'disk', action: 'read:any', attributes: '*' }, + { resource: 'display', action: 'read:any', attributes: '*' }, + { resource: 'flash', action: 'read:any', attributes: '*' }, + { resource: 'info', action: 'read:any', attributes: '*' }, + { resource: 'logs', action: 'read:any', attributes: '*' }, + { resource: 'os', action: 'read:any', attributes: '*' }, + { resource: 'owner', action: 'read:any', attributes: '*' }, + { resource: 'permission', action: 'read:any', attributes: '*' }, + { resource: 'registration', action: 'read:any', attributes: '*' }, + { resource: 'servers', action: 'read:any', attributes: '*' }, + { resource: 'vars', action: 'read:any', attributes: '*' }, + { resource: 'config', action: 'update:own', attributes: '*' }, + { resource: 'connect', action: 'read:own', attributes: '*' }, + { resource: 'connect', action: 'update:own', attributes: '*' }, + ], + }, + my_servers: { + extends: 'guest', + permissions: [ + { resource: 'array', action: 'read:any', attributes: '*' }, + { resource: 'customizations', action: 'read:any', attributes: '*' }, + { resource: 'dashboard', action: 'read:any', attributes: '*' }, + { + resource: 'docker/container', + action: 'read:any', + attributes: '*', + }, + { resource: 'logs', action: 'read:any', attributes: '*' }, + { resource: 'docker/network', action: 'read:any', attributes: '*' }, + { resource: 'notifications', action: 'read:any', attributes: '*' }, + { resource: 'vms', action: 'read:any', attributes: '*' }, + { resource: 'vms/domain', action: 'read:any', attributes: '*' }, + { resource: 'unraid-version', action: 'read:any', attributes: '*' }, + ], + }, + notifier: { + extends: 'guest', + permissions: [ + { + resource: 'notifications', + action: 'create:own', + attributes: '*', + }, + ], + }, + guest: { + permissions: [ + { resource: 'me', action: 'read:any', attributes: '*' }, + { resource: 'welcome', action: 'read:any', attributes: '*' }, + ], + }, }; + +export const setupPermissions = (): RolesBuilder => { + // First create an array of permissions that will be used as the base permission set for the app + const grantList = Object.entries(roles).reduce>( + (acc, [roleName, role]) => { + if (role.permissions) { + role.permissions.forEach((permission) => { + acc.push({ + ...permission, + role: roleName, + }); + }); + } + return acc; + }, + [] + ); + + const ac = new RolesBuilder(grantList); + + // Next, Extend roles + Object.entries(roles).forEach(([roleName, role]) => { + if (role.extends) { + ac.extendRole(roleName, role.extends); + } + }); + + apiLogger.debug('Possible Roles: %o', ac.getRoles()); + + return ac; +}; + +export const ac = null; diff --git a/api/src/core/pubsub.ts b/api/src/core/pubsub.ts index 35c5eef6f..a9a7899dd 100644 --- a/api/src/core/pubsub.ts +++ b/api/src/core/pubsub.ts @@ -6,12 +6,23 @@ const eventEmitter = new EventEmitter(); eventEmitter.setMaxListeners(30); export enum PUBSUB_CHANNEL { + ARRAY = 'ARRAY', + DASHBOARD = 'DASHBOARD', DISPLAY = 'DISPLAY', INFO = 'INFO', NOTIFICATION = 'NOTIFICATION', OWNER = 'OWNER', SERVERS = 'SERVERS', - + VMS = 'VMS', + REGISTRATION = 'REGISTRATION', } export const pubsub = new PubSub({ eventEmitter }); + +/** + * Create a pubsub subscription. + * @param channel The pubsub channel to subscribe to. + */ +export const createSubscription = (channel: PUBSUB_CHANNEL) => { + return pubsub.asyncIterator(channel); +}; diff --git a/api/src/core/utils/index.ts b/api/src/core/utils/index.ts new file mode 100644 index 000000000..23ea6b7b9 --- /dev/null +++ b/api/src/core/utils/index.ts @@ -0,0 +1,10 @@ +// Created from 'create-ts-index' + +export * from './array'; +export * from './authentication'; +export * from './clients'; +export * from './plugins'; +export * from './shares'; +export * from './validation'; +export * from './vms'; +export * from './casting'; diff --git a/api/src/core/utils/misc/load-state.ts b/api/src/core/utils/misc/load-state.ts index fef316adb..8d7da8a5b 100644 --- a/api/src/core/utils/misc/load-state.ts +++ b/api/src/core/utils/misc/load-state.ts @@ -15,9 +15,7 @@ export const loadState = >(filePath: string): deep: true, }) as T; - logger.addContext('config', config); - logger.trace('"%s" was loaded', filePath); - logger.removeContext('config'); + logger.trace({ config }, '"%s" was loaded', filePath); return config; } catch (error: unknown) { diff --git a/api/src/core/utils/misc/send-form-to-keyserver.ts b/api/src/core/utils/misc/send-form-to-keyserver.ts new file mode 100644 index 000000000..02f2944d6 --- /dev/null +++ b/api/src/core/utils/misc/send-form-to-keyserver.ts @@ -0,0 +1,35 @@ +import { AppError } from '@app/core/errors/app-error'; +import { logger } from '@app/core/log'; +import { type CancelableRequest, got, type Response } from 'got'; + +export const sendFormToKeyServer = async (url: string, data: Record): Promise>> => { + if (!data) { + throw new AppError('Missing data field.'); + } + + // Create form + const form = new URLSearchParams(); + Object.entries(data).forEach(([key, value]) => { + if (value !== undefined) { + form.append(key, String(value)); + } + }); + + // Convert form to string + const body = form.toString(); + + logger.trace({form: body }, 'Sending form to key-server'); + + // Send form + return got(url, { + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + timeout: { + request: 5_000, + }, + throwHttpErrors: true, + body, + }); +}; diff --git a/api/src/environment.ts b/api/src/environment.ts index 7050b271e..881d8494a 100644 --- a/api/src/environment.ts +++ b/api/src/environment.ts @@ -14,4 +14,7 @@ export const GRAPHQL_INTROSPECTION = Boolean( export const PORT = process.env.PORT ?? '/var/run/unraid-api.sock'; export const DRY_RUN = process.env.DRY_RUN === 'true'; export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === 'true'; -export const LOG_CORS = process.env.LOG_CORS === 'true'; \ No newline at end of file +export const LOG_CORS = process.env.LOG_CORS === 'true'; +export const LOG_TYPE = process.env.LOG_TYPE as 'pretty' | 'raw'; +export const LOG_LEVEL = process.env.LOG_LEVEL as 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL'; +export const LOG_TRANSPORT = process.env.LOG_TRANSPORT as 'file' | 'stdout'; \ No newline at end of file diff --git a/api/src/graphql/express/get-logs.ts b/api/src/graphql/express/get-logs.ts deleted file mode 100644 index 9b969cef0..000000000 --- a/api/src/graphql/express/get-logs.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { report } from '@app/cli/commands/report'; -import { logger } from '@app/core/log'; -import { apiKeyToUser } from '@app/graphql/index'; -import { getters } from '@app/store/index'; -import { execa } from 'execa'; -import { type Response, type Request } from 'express'; -import { stat, writeFile } from 'fs/promises'; -import { join } from 'path'; - -const saveApiReport = async (pathToReport: string) => { - try { - const apiReport = await report('-vv', '--json'); - logger.debug('Report object %o', apiReport); - await writeFile( - pathToReport, - JSON.stringify(apiReport, null, 2), - 'utf-8' - ); - } catch (error) { - logger.warn('Could not generate report for zip with error %o', error); - } -}; - -export const getLogs = async (req: Request, res: Response) => { - const apiKey = req.headers['x-api-key'] || req.query.apiKey; - const logPath = getters.paths()['log-base']; - try { - await saveApiReport(join(logPath, 'report.json')); - } catch (error) { - logger.warn('Could not generate report for zip with error %o', error); - } - const zipToWrite = join(logPath, '../unraid-api.tar.gz'); - if ( - apiKey && - typeof apiKey === 'string' && - (await apiKeyToUser(apiKey)).role !== 'guest' - ) { - const exists = Boolean(await stat(logPath).catch(() => null)); - if (exists) { - try { - await execa('tar', ['-czf', zipToWrite, logPath]); - return res.status(200).sendFile(zipToWrite); - } catch (error) { - return res.status(503).send(`Failed: ${error}`); - } - } else { - return res.status(404).send('No Logs Available'); - } - } - - return res.status(403).send('unauthorized'); -}; diff --git a/api/src/graphql/func-directive.ts b/api/src/graphql/func-directive.ts deleted file mode 100644 index 483c9317e..000000000 --- a/api/src/graphql/func-directive.ts +++ /dev/null @@ -1,94 +0,0 @@ -import get from 'lodash/get'; -import * as core from '@app/core'; -import { graphqlLogger } from '@app/core/log'; -import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils'; -import { getCoreModule } from '@app/graphql/index'; -import type { GraphQLFieldResolver, GraphQLSchema } from 'graphql'; -import type { User } from '@app/core/types/states/user'; - -interface FuncDirective { - module: string; - data: object; - query: any; - extractFromResponse: string; -} - -const funcDirectiveResolver: (directiveArgs: FuncDirective) => GraphQLFieldResolver | undefined = ({ - module: coreModule, - data, - query, - extractFromResponse, -}) => async (_, args, context) => { - const func = getCoreModule(coreModule); - - const functionContext = { - query, - data, - user: context.user, - }; - - // Run function - const [error, coreMethodResult] = await Promise.resolve(func(functionContext, core)) - .then(result => [undefined, result]) - .catch(error_ => { - // Ensure we aren't leaking anything in production - if (process.env.NODE_ENV === 'production') { - graphqlLogger.error('Module:', coreModule, 'Error:', error_.message); - return [new Error(error_.message)]; - } - - return [error_]; - }); - - // Bail if we can't get the method to run - if (error) { - return error; - } - - // Get wanted result type or fallback to json - const result = coreMethodResult[args.result || 'json']; - - // Allow fields to be extracted - if (extractFromResponse) { - return get(result, extractFromResponse); - } - - return result; -}; - -/** - * Get the func directive - this is used to resolve @func directives in the graphql schema - * @returns Type definition and schema interceptor to create resolvers for @func directives - */ -export function getFuncDirective() { - const directiveName = 'func'; - return { - funcDirectiveTypeDefs: /* GraphQL */` - directive @func( - module: String! - data: JSON - query: JSON - result: String - extractFromResponse: String - ) on FIELD_DEFINITION - `, - funcDirectiveTransformer: (schema: GraphQLSchema): GraphQLSchema => mapSchema(schema, { - [MapperKind.MUTATION_ROOT_FIELD](fieldConfig) { - const funcDirective = getDirective(schema, fieldConfig, directiveName)?.[0] as FuncDirective | undefined; - if (funcDirective?.module) { - fieldConfig.resolve = funcDirectiveResolver(funcDirective); - } - - return fieldConfig; - }, - [MapperKind.QUERY_ROOT_FIELD](fieldConfig) { - const funcDirective = getDirective(schema, fieldConfig, directiveName)?.[0] as FuncDirective | undefined; - if (funcDirective?.module) { - fieldConfig.resolve = funcDirectiveResolver(funcDirective); - } - - return fieldConfig; - }, - }), - }; -} diff --git a/api/src/graphql/generated/api/operations.ts b/api/src/graphql/generated/api/operations.ts index eacae7074..77ed1ccb1 100755 --- a/api/src/graphql/generated/api/operations.ts +++ b/api/src/graphql/generated/api/operations.ts @@ -2,7 +2,7 @@ import * as Types from '@app/graphql/generated/api/types'; import { z } from 'zod' -import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Device, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, Permissions, ProfileModel, Registration, RegistrationState, RelayResponse, Scope, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, TwoFactorLocal, TwoFactorRemote, TwoFactorWithToken, TwoFactorWithoutToken, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmNetwork, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addScopeInput, addScopeToApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types' +import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types' import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; type Properties = Required<{ @@ -248,17 +248,6 @@ export function ContainerPortSchema(): z.ZodObject> { }) } -export function DeviceSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Device').optional(), - device: z.string().nullish(), - id: z.string(), - sectorSize: z.string().nullish(), - sectors: z.string().nullish(), - tag: z.string().nullish() - }) -} - export function DevicesSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Devices').optional(), @@ -471,7 +460,7 @@ export function MeSchema(): z.ZodObject> { id: z.string(), name: z.string(), permissions: definedNonNullAnySchema.nullish(), - role: z.string() + roles: z.string() }) } @@ -682,14 +671,6 @@ export function PciSchema(): z.ZodObject> { }) } -export function PermissionsSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Permissions').optional(), - grants: definedNonNullAnySchema.nullish(), - scopes: definedNonNullAnySchema.nullish() - }) -} - export function ProfileModelSchema(): z.ZodObject> { return z.object({ __typename: z.literal('ProfileModel').optional(), @@ -720,14 +701,6 @@ export function RelayResponseSchema(): z.ZodObject> { }) } -export function ScopeSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('Scope').optional(), - description: z.string().nullish(), - name: z.string().nullish() - }) -} - export function ServerSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Server').optional(), @@ -794,37 +767,6 @@ export function SystemSchema(): z.ZodObject> { }) } -export function TwoFactorLocalSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('TwoFactorLocal').optional(), - enabled: z.boolean().nullish() - }) -} - -export function TwoFactorRemoteSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('TwoFactorRemote').optional(), - enabled: z.boolean().nullish() - }) -} - -export function TwoFactorWithTokenSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('TwoFactorWithToken').optional(), - local: TwoFactorLocalSchema().nullish(), - remote: TwoFactorRemoteSchema().nullish(), - token: z.string().nullish() - }) -} - -export function TwoFactorWithoutTokenSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('TwoFactorWithoutToken').optional(), - local: TwoFactorLocalSchema().nullish(), - remote: TwoFactorRemoteSchema().nullish() - }) -} - export function UnassignedDeviceSchema(): z.ZodObject> { return z.object({ __typename: z.literal('UnassignedDevice').optional(), @@ -905,7 +847,7 @@ export function UserSchema(): z.ZodObject> { id: z.string(), name: z.string(), password: z.boolean().nullish(), - role: z.string() + roles: z.string() }) } @@ -1098,13 +1040,6 @@ export function VmDomainSchema(): z.ZodObject> { }) } -export function VmNetworkSchema(): z.ZodObject> { - return z.object({ - __typename: z.literal('VmNetwork').optional(), - _placeholderType: z.string().nullish() - }) -} - export function VmsSchema(): z.ZodObject> { return z.object({ __typename: z.literal('Vms').optional(), @@ -1127,20 +1062,6 @@ export function addApiKeyInputSchema(): z.ZodObject> }) } -export function addScopeInputSchema(): z.ZodObject> { - return z.object({ - description: z.string().nullish(), - name: z.string() - }) -} - -export function addScopeToApiKeyInputSchema(): z.ZodObject> { - return z.object({ - apiKey: z.string(), - name: z.string() - }) -} - export function addUserInputSchema(): z.ZodObject> { return z.object({ description: z.string().nullish(), diff --git a/api/src/graphql/generated/api/types.ts b/api/src/graphql/generated/api/types.ts index 704c5841f..895312df9 100644 --- a/api/src/graphql/generated/api/types.ts +++ b/api/src/graphql/generated/api/types.ts @@ -294,15 +294,6 @@ export enum ContainerState { RUNNING = 'RUNNING' } -export type Device = { - __typename?: 'Device'; - device?: Maybe; - id: Scalars['ID']['output']; - sectorSize?: Maybe; - sectors?: Maybe; - tag?: Maybe; -}; - export type Devices = { __typename?: 'Devices'; gpu?: Maybe>>; @@ -519,7 +510,7 @@ export type Me = UserAccount & { id: Scalars['ID']['output']; name: Scalars['String']['output']; permissions?: Maybe; - role: Scalars['String']['output']; + roles: Scalars['String']['output']; }; export enum MemoryFormFactor { @@ -576,10 +567,6 @@ export type Mutation = { addApikey?: Maybe; /** Add new disk to array */ addDiskToArray?: Maybe; - /** Add a new permission scope */ - addScope?: Maybe; - /** Add a new permission scope to apiKey */ - addScopeToApiKey?: Maybe; /** Add a new user */ addUser?: Maybe; /** Cancel parity check */ @@ -627,16 +614,6 @@ export type MutationaddDiskToArrayArgs = { }; -export type MutationaddScopeArgs = { - input: addScopeInput; -}; - - -export type MutationaddScopeToApiKeyArgs = { - input: addScopeToApiKeyInput; -}; - - export type MutationaddUserArgs = { input: addUserInput; }; @@ -869,12 +846,6 @@ export type Pci = { vendorname?: Maybe; }; -export type Permissions = { - __typename?: 'Permissions'; - grants?: Maybe; - scopes?: Maybe; -}; - export type ProfileModel = { __typename?: 'ProfileModel'; avatar?: Maybe; @@ -891,9 +862,6 @@ export type Query = { array: ArrayType; cloud?: Maybe; config: Config; - crashReportingEnabled?: Maybe; - device?: Maybe; - devices: Array>; /** Single disk */ disk?: Maybe; /** Mulitiple disks */ @@ -913,29 +881,19 @@ export type Query = { online?: Maybe; owner?: Maybe; parityHistory?: Maybe>>; - permissions?: Maybe; registration?: Maybe; server?: Maybe; servers: Array; /** Network Shares */ shares?: Maybe>>; - twoFactor?: Maybe; unassignedDevices?: Maybe>>; /** User account */ user?: Maybe; /** User accounts */ users: Array; vars?: Maybe; - /** Virtual network for vms */ - vmNetwork?: Maybe; /** Virtual machines */ vms?: Maybe; - welcome?: Maybe; -}; - - -export type QuerydeviceArgs = { - id: Scalars['ID']['input']; }; @@ -964,11 +922,6 @@ export type QuerynotificationsArgs = { }; -export type QueryserverArgs = { - name: Scalars['String']['input']; -}; - - export type QueryuserArgs = { id: Scalars['ID']['input']; }; @@ -978,11 +931,6 @@ export type QueryusersArgs = { input?: InputMaybe; }; - -export type QueryvmNetworkArgs = { - name: Scalars['String']['input']; -}; - export type Registration = { __typename?: 'Registration'; expiration?: Maybe; @@ -1040,15 +988,6 @@ export type RelayResponse = { timeout?: Maybe; }; -/** A permission scope */ -export type Scope = { - __typename?: 'Scope'; - /** A user friendly description */ - description?: Maybe; - /** A unique name for the scope */ - name?: Maybe; -}; - export type Server = { __typename?: 'Server'; apikey: Scalars['String']['output']; @@ -1114,9 +1053,6 @@ export type Subscription = { apikeys?: Maybe>>; array: ArrayType; config: Config; - crashReportingEnabled: Scalars['Boolean']['output']; - device: Device; - devices?: Maybe>; display?: Maybe; dockerContainer: DockerContainer; dockerContainers?: Maybe>>; @@ -1131,26 +1067,18 @@ export type Subscription = { parityHistory: ParityCheck; ping: Scalars['String']['output']; registration: Registration; - server: Server; - servers?: Maybe>; + server?: Maybe; service?: Maybe>; share: Share; shares?: Maybe>; - twoFactor?: Maybe; unassignedDevices?: Maybe>; user: User; users: Array>; vars: Vars; - vmNetworks?: Maybe>; vms?: Maybe; }; -export type SubscriptiondeviceArgs = { - id: Scalars['ID']['input']; -}; - - export type SubscriptiondockerContainerArgs = { id: Scalars['ID']['input']; }; @@ -1161,11 +1089,6 @@ export type SubscriptiondockerNetworkArgs = { }; -export type SubscriptionserverArgs = { - name: Scalars['String']['input']; -}; - - export type SubscriptionserviceArgs = { name: Scalars['String']['input']; }; @@ -1199,29 +1122,6 @@ export enum Theme { WHITE = 'white' } -export type TwoFactorLocal = { - __typename?: 'TwoFactorLocal'; - enabled?: Maybe; -}; - -export type TwoFactorRemote = { - __typename?: 'TwoFactorRemote'; - enabled?: Maybe; -}; - -export type TwoFactorWithToken = { - __typename?: 'TwoFactorWithToken'; - local?: Maybe; - remote?: Maybe; - token?: Maybe; -}; - -export type TwoFactorWithoutToken = { - __typename?: 'TwoFactorWithoutToken'; - local?: Maybe; - remote?: Maybe; -}; - export type UnassignedDevice = { __typename?: 'UnassignedDevice'; devlinks?: Maybe; @@ -1298,14 +1198,14 @@ export type User = UserAccount & { name: Scalars['String']['output']; /** If the account has a password set */ password?: Maybe; - role: Scalars['String']['output']; + roles: Scalars['String']['output']; }; export type UserAccount = { description: Scalars['String']['output']; id: Scalars['ID']['output']; name: Scalars['String']['output']; - role: Scalars['String']['output']; + roles: Scalars['String']['output']; }; export type Vars = { @@ -1511,11 +1411,6 @@ export type VmDomain = { uuid: Scalars['ID']['output']; }; -export type VmNetwork = { - __typename?: 'VmNetwork'; - _placeholderType?: Maybe; -}; - export enum VmState { CRASHED = 'CRASHED', IDLE = 'IDLE', @@ -1554,19 +1449,6 @@ export type addApiKeyInput = { userId?: InputMaybe; }; -export type addScopeInput = { - /** Scope description */ - description?: InputMaybe; - /** Scope name */ - name: Scalars['String']['input']; -}; - -export type addScopeToApiKeyInput = { - apiKey: Scalars['String']['input']; - /** Scope name */ - name: Scalars['String']['input']; -}; - export type addUserInput = { description?: InputMaybe; name: Scalars['String']['input']; @@ -1716,7 +1598,6 @@ export type ResolversTypes = ResolversObject<{ ContainerPortType: ContainerPortType; ContainerState: ContainerState; DateTime: ResolverTypeWrapper; - Device: ResolverTypeWrapper; Devices: ResolverTypeWrapper; Disk: ResolverTypeWrapper; DiskFsType: DiskFsType; @@ -1757,14 +1638,12 @@ export type ResolversTypes = ResolversObject<{ ParityCheck: ResolverTypeWrapper; Partition: ResolverTypeWrapper; Pci: ResolverTypeWrapper; - Permissions: ResolverTypeWrapper; Port: ResolverTypeWrapper; ProfileModel: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; Registration: ResolverTypeWrapper; RegistrationState: RegistrationState; RelayResponse: ResolverTypeWrapper; - Scope: ResolverTypeWrapper; Server: ResolverTypeWrapper; ServerStatus: ServerStatus; Service: ResolverTypeWrapper; @@ -1775,10 +1654,6 @@ export type ResolversTypes = ResolversObject<{ System: ResolverTypeWrapper; Temperature: Temperature; Theme: Theme; - TwoFactorLocal: ResolverTypeWrapper; - TwoFactorRemote: ResolverTypeWrapper; - TwoFactorWithToken: ResolverTypeWrapper; - TwoFactorWithoutToken: ResolverTypeWrapper; UUID: ResolverTypeWrapper; UnassignedDevice: ResolverTypeWrapper; Uptime: ResolverTypeWrapper; @@ -1788,15 +1663,12 @@ export type ResolversTypes = ResolversObject<{ Vars: ResolverTypeWrapper; Versions: ResolverTypeWrapper; VmDomain: ResolverTypeWrapper; - VmNetwork: ResolverTypeWrapper; VmState: VmState; Vms: ResolverTypeWrapper; WAN_ACCESS_TYPE: WAN_ACCESS_TYPE; WAN_FORWARD_TYPE: WAN_FORWARD_TYPE; Welcome: ResolverTypeWrapper; addApiKeyInput: addApiKeyInput; - addScopeInput: addScopeInput; - addScopeToApiKeyInput: addScopeToApiKeyInput; addUserInput: addUserInput; arrayDiskInput: arrayDiskInput; authenticateInput: authenticateInput; @@ -1828,7 +1700,6 @@ export type ResolversParentTypes = ResolversObject<{ ContainerMount: ContainerMount; ContainerPort: ContainerPort; DateTime: Scalars['DateTime']['output']; - Device: Device; Devices: Devices; Disk: Disk; DiskPartition: DiskPartition; @@ -1861,13 +1732,11 @@ export type ResolversParentTypes = ResolversObject<{ ParityCheck: ParityCheck; Partition: Partition; Pci: Pci; - Permissions: Permissions; Port: Scalars['Port']['output']; ProfileModel: ProfileModel; Query: {}; Registration: Registration; RelayResponse: RelayResponse; - Scope: Scope; Server: Server; Service: Service; SetupRemoteAccessInput: SetupRemoteAccessInput; @@ -1875,10 +1744,6 @@ export type ResolversParentTypes = ResolversObject<{ String: Scalars['String']['output']; Subscription: {}; System: System; - TwoFactorLocal: TwoFactorLocal; - TwoFactorRemote: TwoFactorRemote; - TwoFactorWithToken: TwoFactorWithToken; - TwoFactorWithoutToken: TwoFactorWithoutToken; UUID: Scalars['UUID']['output']; UnassignedDevice: UnassignedDevice; Uptime: Uptime; @@ -1888,12 +1753,9 @@ export type ResolversParentTypes = ResolversObject<{ Vars: Vars; Versions: Versions; VmDomain: VmDomain; - VmNetwork: VmNetwork; Vms: Vms; Welcome: Welcome; addApiKeyInput: addApiKeyInput; - addScopeInput: addScopeInput; - addScopeToApiKeyInput: addScopeToApiKeyInput; addUserInput: addUserInput; arrayDiskInput: arrayDiskInput; authenticateInput: authenticateInput; @@ -1902,12 +1764,6 @@ export type ResolversParentTypes = ResolversObject<{ usersInput: usersInput; }>; -export type subscriptionDirectiveArgs = { - channel: Scalars['String']['input']; -}; - -export type subscriptionDirectiveResolver = DirectiveResolverFn; - export type ApiKeyResolvers = ResolversObject<{ description?: Resolver, ParentType, ContextType>; expiresAt?: Resolver; @@ -2043,15 +1899,6 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig = ResolversObject<{ - device?: Resolver, ParentType, ContextType>; - id?: Resolver; - sectorSize?: Resolver, ParentType, ContextType>; - sectors?: Resolver, ParentType, ContextType>; - tag?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export type DevicesResolvers = ResolversObject<{ gpu?: Resolver>>, ParentType, ContextType>; network?: Resolver>>, ParentType, ContextType>; @@ -2244,7 +2091,7 @@ export type MeResolvers; name?: Resolver; permissions?: Resolver, ParentType, ContextType>; - role?: Resolver; + roles?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -2281,8 +2128,6 @@ export type MountResolvers = ResolversObject<{ addApikey?: Resolver, ParentType, ContextType, RequireFields>; addDiskToArray?: Resolver, ParentType, ContextType, Partial>; - addScope?: Resolver, ParentType, ContextType, RequireFields>; - addScopeToApiKey?: Resolver, ParentType, ContextType, RequireFields>; addUser?: Resolver, ParentType, ContextType, RequireFields>; cancelParityCheck?: Resolver, ParentType, ContextType>; clearArrayDiskStatistics?: Resolver, ParentType, ContextType, RequireFields>; @@ -2442,12 +2287,6 @@ export type PciResolvers; }>; -export type PermissionsResolvers = ResolversObject<{ - grants?: Resolver, ParentType, ContextType>; - scopes?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export interface PortScalarConfig extends GraphQLScalarTypeConfig { name: 'Port'; } @@ -2465,9 +2304,6 @@ export type QueryResolvers; cloud?: Resolver, ParentType, ContextType>; config?: Resolver; - crashReportingEnabled?: Resolver, ParentType, ContextType>; - device?: Resolver, ParentType, ContextType, RequireFields>; - devices?: Resolver>, ParentType, ContextType>; disk?: Resolver, ParentType, ContextType, RequireFields>; disks?: Resolver>, ParentType, ContextType>; display?: Resolver, ParentType, ContextType>; @@ -2481,19 +2317,15 @@ export type QueryResolvers, ParentType, ContextType>; owner?: Resolver, ParentType, ContextType>; parityHistory?: Resolver>>, ParentType, ContextType>; - permissions?: Resolver, ParentType, ContextType>; registration?: Resolver, ParentType, ContextType>; - server?: Resolver, ParentType, ContextType, RequireFields>; + server?: Resolver, ParentType, ContextType>; servers?: Resolver, ParentType, ContextType>; shares?: Resolver>>, ParentType, ContextType>; - twoFactor?: Resolver, ParentType, ContextType>; unassignedDevices?: Resolver>>, ParentType, ContextType>; user?: Resolver, ParentType, ContextType, RequireFields>; users?: Resolver, ParentType, ContextType, Partial>; vars?: Resolver, ParentType, ContextType>; - vmNetwork?: Resolver, ParentType, ContextType, RequireFields>; vms?: Resolver, ParentType, ContextType>; - welcome?: Resolver, ParentType, ContextType>; }>; export type RegistrationResolvers = ResolversObject<{ @@ -2512,12 +2344,6 @@ export type RelayResponseResolvers; }>; -export type ScopeResolvers = ResolversObject<{ - description?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export type ServerResolvers = ResolversObject<{ apikey?: Resolver; guid?: Resolver; @@ -2562,9 +2388,6 @@ export type SubscriptionResolvers>>, "apikeys", ParentType, ContextType>; array?: SubscriptionResolver; config?: SubscriptionResolver; - crashReportingEnabled?: SubscriptionResolver; - device?: SubscriptionResolver>; - devices?: SubscriptionResolver>, "devices", ParentType, ContextType>; display?: SubscriptionResolver, "display", ParentType, ContextType>; dockerContainer?: SubscriptionResolver>; dockerContainers?: SubscriptionResolver>>, "dockerContainers", ParentType, ContextType>; @@ -2579,17 +2402,14 @@ export type SubscriptionResolvers; ping?: SubscriptionResolver; registration?: SubscriptionResolver; - server?: SubscriptionResolver>; - servers?: SubscriptionResolver>, "servers", ParentType, ContextType>; + server?: SubscriptionResolver, "server", ParentType, ContextType>; service?: SubscriptionResolver>, "service", ParentType, ContextType, RequireFields>; share?: SubscriptionResolver>; shares?: SubscriptionResolver>, "shares", ParentType, ContextType>; - twoFactor?: SubscriptionResolver, "twoFactor", ParentType, ContextType>; unassignedDevices?: SubscriptionResolver>, "unassignedDevices", ParentType, ContextType>; user?: SubscriptionResolver>; users?: SubscriptionResolver>, "users", ParentType, ContextType>; vars?: SubscriptionResolver; - vmNetworks?: SubscriptionResolver>, "vmNetworks", ParentType, ContextType>; vms?: SubscriptionResolver, "vms", ParentType, ContextType>; }>; @@ -2603,29 +2423,6 @@ export type SystemResolvers; }>; -export type TwoFactorLocalResolvers = ResolversObject<{ - enabled?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type TwoFactorRemoteResolvers = ResolversObject<{ - enabled?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type TwoFactorWithTokenResolvers = ResolversObject<{ - local?: Resolver, ParentType, ContextType>; - remote?: Resolver, ParentType, ContextType>; - token?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - -export type TwoFactorWithoutTokenResolvers = ResolversObject<{ - local?: Resolver, ParentType, ContextType>; - remote?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export interface UUIDScalarConfig extends GraphQLScalarTypeConfig { name: 'UUID'; } @@ -2702,7 +2499,7 @@ export type UserResolvers; name?: Resolver; password?: Resolver, ParentType, ContextType>; - role?: Resolver; + roles?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -2711,7 +2508,7 @@ export type UserAccountResolvers; id?: Resolver; name?: Resolver; - role?: Resolver; + roles?: Resolver; }>; export type VarsResolvers = ResolversObject<{ @@ -2897,11 +2694,6 @@ export type VmDomainResolvers; }>; -export type VmNetworkResolvers = ResolversObject<{ - _placeholderType?: Resolver, ParentType, ContextType>; - __isTypeOf?: IsTypeOfResolverFn; -}>; - export type VmsResolvers = ResolversObject<{ domain?: Resolver>, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -2928,7 +2720,6 @@ export type Resolvers = ResolversObject<{ ContainerMount?: ContainerMountResolvers; ContainerPort?: ContainerPortResolvers; DateTime?: GraphQLScalarType; - Device?: DeviceResolvers; Devices?: DevicesResolvers; Disk?: DiskResolvers; DiskPartition?: DiskPartitionResolvers; @@ -2956,22 +2747,16 @@ export type Resolvers = ResolversObject<{ ParityCheck?: ParityCheckResolvers; Partition?: PartitionResolvers; Pci?: PciResolvers; - Permissions?: PermissionsResolvers; Port?: GraphQLScalarType; ProfileModel?: ProfileModelResolvers; Query?: QueryResolvers; Registration?: RegistrationResolvers; RelayResponse?: RelayResponseResolvers; - Scope?: ScopeResolvers; Server?: ServerResolvers; Service?: ServiceResolvers; Share?: ShareResolvers; Subscription?: SubscriptionResolvers; System?: SystemResolvers; - TwoFactorLocal?: TwoFactorLocalResolvers; - TwoFactorRemote?: TwoFactorRemoteResolvers; - TwoFactorWithToken?: TwoFactorWithTokenResolvers; - TwoFactorWithoutToken?: TwoFactorWithoutTokenResolvers; UUID?: GraphQLScalarType; UnassignedDevice?: UnassignedDeviceResolvers; Uptime?: UptimeResolvers; @@ -2981,11 +2766,7 @@ export type Resolvers = ResolversObject<{ Vars?: VarsResolvers; Versions?: VersionsResolvers; VmDomain?: VmDomainResolvers; - VmNetwork?: VmNetworkResolvers; Vms?: VmsResolvers; Welcome?: WelcomeResolvers; }>; -export type DirectiveResolvers = ResolversObject<{ - subscription?: subscriptionDirectiveResolver; -}>; diff --git a/api/src/graphql/generated/client/graphql.ts b/api/src/graphql/generated/client/graphql.ts index 522e03e5e..a17fed8c2 100644 --- a/api/src/graphql/generated/client/graphql.ts +++ b/api/src/graphql/generated/client/graphql.ts @@ -345,7 +345,7 @@ export type KsServerDetails = { flashProduct: Scalars['String']['output']; flashVendor: Scalars['String']['output']; guid: Scalars['String']['output']; - ipsId: Scalars['String']['output']; + ipsId?: Maybe; keyType?: Maybe; licenseKey: Scalars['String']['output']; name: Scalars['String']['output']; diff --git a/api/src/graphql/index.ts b/api/src/graphql/index.ts index b6c3d7ad2..e93e5342d 100644 --- a/api/src/graphql/index.ts +++ b/api/src/graphql/index.ts @@ -1,7 +1,5 @@ import { FatalAppError } from '@app/core/errors/fatal-error'; -import { graphqlLogger } from '@app/core/log'; import { modules } from '@app/core'; -import { getters } from '@app/store'; export const getCoreModule = (moduleName: string) => { if (!Object.keys(modules).includes(moduleName)) { @@ -10,16 +8,3 @@ export const getCoreModule = (moduleName: string) => { return modules[moduleName]; }; - -export const apiKeyToUser = async (apiKey: string) => { - try { - const config = getters.config(); - if (apiKey === config.remote.apikey) return { id: -1, description: 'My servers service account', name: 'my_servers', role: 'my_servers' }; - if (apiKey === config.upc.apikey) return { id: -1, description: 'UPC service account', name: 'upc', role: 'upc' }; - if (apiKey === config.notifier.apikey) return { id: -1, description: 'Notifier service account', name: 'notifier', role: 'notifier' }; - } catch (error: unknown) { - graphqlLogger.debug('Failed looking up API key with "%s"', (error as Error).message); - } - - return { id: -1, description: 'A guest user', name: 'guest', role: 'guest' }; -}; diff --git a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts index 4180d5a7e..3157de991 100644 --- a/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts +++ b/api/src/graphql/resolvers/mutation/connect/connect-sign-in.ts @@ -1,6 +1,7 @@ -import { ensurePermission } from '@app/core/utils/index'; import { NODE_ENV } from '@app/environment'; -import { type MutationResolvers } from '@app/graphql/generated/api/types'; +import { + type ConnectSignInInput, +} from '@app/graphql/generated/api/types'; import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types'; import { validateApiKeyWithKeyServer } from '@app/mothership/api-key/validate-api-key-with-keyserver'; import { getters, store } from '@app/store/index'; @@ -9,31 +10,26 @@ import { FileLoadStatus } from '@app/store/types'; import { GraphQLError } from 'graphql'; import { decodeJwt } from 'jose'; -export const connectSignIn: MutationResolvers['connectSignIn'] = async ( - _, - args, - context -) => { - ensurePermission(context.user, { - resource: 'connect', - possession: 'own', - action: 'update', - }); - +export const connectSignIn = async ( + input: ConnectSignInInput +): Promise => { if (getters.emhttp().status === FileLoadStatus.LOADED) { - const result = NODE_ENV === 'development' ? API_KEY_STATUS.API_KEY_VALID : await validateApiKeyWithKeyServer({ - apiKey: args.input.apiKey, - flashGuid: getters.emhttp().var.flashGuid, - }); + const result = + NODE_ENV === 'development' + ? API_KEY_STATUS.API_KEY_VALID + : await validateApiKeyWithKeyServer({ + apiKey: input.apiKey, + flashGuid: getters.emhttp().var.flashGuid, + }); if (result !== API_KEY_STATUS.API_KEY_VALID) { throw new GraphQLError( `Validating API Key Failed with Error: ${result}` ); } - const userInfo = args.input.idToken - ? decodeJwt(args.input.idToken) - : args.input.userInfo ?? null; + const userInfo = input.idToken + ? decodeJwt(input.idToken) + : input.userInfo ?? null; if ( !userInfo || !userInfo.preferred_username || @@ -47,10 +43,11 @@ export const connectSignIn: MutationResolvers['connectSignIn'] = async ( // @TODO once we deprecate old sign in method, switch this to do all validation requests await store.dispatch( loginUser({ - avatar: typeof userInfo.avatar === 'string' ? userInfo.avatar : '', + avatar: + typeof userInfo.avatar === 'string' ? userInfo.avatar : '', username: userInfo.preferred_username, email: userInfo.email, - apikey: args.input.apiKey, + apikey: input.apiKey, }) ); return true; diff --git a/api/src/graphql/resolvers/mutation/connect/connect-sign-out.ts b/api/src/graphql/resolvers/mutation/connect/connect-sign-out.ts deleted file mode 100644 index 2ad685ac3..000000000 --- a/api/src/graphql/resolvers/mutation/connect/connect-sign-out.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { type MutationResolvers } from '@app/graphql/generated/api/types'; -import { store } from '@app/store/index'; -import { logoutUser } from '@app/store/modules/config'; - -export const connectSignOut: MutationResolvers['connectSignOut'] = async ( - _, - __, - context -) => { - ensurePermission(context.user, { - resource: 'connect', - possession: 'own', - action: 'update', - }); - - await store.dispatch(logoutUser({ reason: 'Manual Sign Out With API' })); - return true; -}; diff --git a/api/src/graphql/resolvers/mutation/connect/set-additional-allowed-origins.ts b/api/src/graphql/resolvers/mutation/connect/set-additional-allowed-origins.ts deleted file mode 100644 index c5c0d7bf9..000000000 --- a/api/src/graphql/resolvers/mutation/connect/set-additional-allowed-origins.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getAllowedOrigins } from '@app/common/allowed-origins'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { type MutationResolvers } from '@app/graphql/generated/api/types'; -import { store } from '@app/store/index'; -import { updateAllowedOrigins } from '@app/store/modules/config'; - -export const setAdditionalAllowedOrigins: MutationResolvers['setAdditionalAllowedOrigins'] = - async (_, args, context) => { - ensurePermission(context.user, { - resource: 'connect', - possession: 'own', - action: 'update', - }); - - await store.dispatch( - updateAllowedOrigins(args.input.origins) - ); - - return getAllowedOrigins(); - }; diff --git a/api/src/graphql/resolvers/mutation/index.ts b/api/src/graphql/resolvers/mutation/index.ts deleted file mode 100644 index 5b7b9c0d2..000000000 --- a/api/src/graphql/resolvers/mutation/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type Resolvers } from '@app/graphql/generated/api/types'; -import { sendNotification } from './notifications'; -import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in'; -import { connectSignOut } from '@app/graphql/resolvers/mutation/connect/connect-sign-out'; -import { setAdditionalAllowedOrigins } from '@app/graphql/resolvers/mutation/connect/set-additional-allowed-origins'; - -export const Mutation: Resolvers['Mutation'] = { - sendNotification, - connectSignIn, - connectSignOut, - setAdditionalAllowedOrigins, -}; diff --git a/api/src/graphql/resolvers/query/config.ts b/api/src/graphql/resolvers/query/config.ts deleted file mode 100644 index 8b8176bdb..000000000 --- a/api/src/graphql/resolvers/query/config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*! - * Copyright 2021 Lime Technology Inc. All rights reserved. - * Written by: Alexis Tyler - */ - -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { ConfigErrorState, type QueryResolvers } from '@app/graphql/generated/api/types'; -import { getters } from '@app/store'; - -export const config: QueryResolvers['config'] = async (_, __, context) => { - ensurePermission(context.user, { - resource: 'config', - action: 'read', - possession: 'any', - }); - - const emhttp = getters.emhttp(); - - return { - valid: emhttp.var.configValid, - error: emhttp.var.configValid ? null : ConfigErrorState[emhttp.var.configState] ?? ConfigErrorState.UNKNOWN_ERROR - }; -}; diff --git a/api/src/graphql/resolvers/query/docker.ts b/api/src/graphql/resolvers/query/docker.ts deleted file mode 100644 index 2f3f57139..000000000 --- a/api/src/graphql/resolvers/query/docker.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getDockerContainers } from "@app/core/modules/index"; -import { ensurePermission } from "@app/core/utils/permissions/ensure-permission"; -import { type QueryResolvers } from "@app/graphql/generated/api/types"; - -export const dockerContainersResolver: QueryResolvers['dockerContainers'] = async (_, __, context) => { - const { user } = context; - - // Check permissions - ensurePermission(user, { - resource: 'docker/container', - action: 'read', - possession: 'any', - }); - - return getDockerContainers(); -} \ No newline at end of file diff --git a/api/src/graphql/resolvers/query/index.ts b/api/src/graphql/resolvers/query/index.ts deleted file mode 100644 index d9e33366e..000000000 --- a/api/src/graphql/resolvers/query/index.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getArray } from '@app/core/modules/get-array'; -import { type QueryResolvers } from '@app/graphql/generated/api/types'; -import cloud from '@app/graphql/resolvers/query/cloud'; -import { config } from '@app/graphql/resolvers/query/config'; -import crashReportingEnabled from '@app/graphql/resolvers/query/crash-reporting-enabled'; -import { disksResolver } from '@app/graphql/resolvers/query/disks'; -import display from '@app/graphql/resolvers/query/display'; -import { dockerContainersResolver } from '@app/graphql/resolvers/query/docker'; -import flash from '@app/graphql/resolvers/query/flash'; -import { notificationsResolver } from '@app/graphql/resolvers/query/notifications'; -import online from '@app/graphql/resolvers/query/online'; -import owner from '@app/graphql/resolvers/query/owner'; -import { registration } from '@app/graphql/resolvers/query/registration'; -import { server } from '@app/graphql/resolvers/query/server'; -import { servers } from '@app/graphql/resolvers/query/servers'; -import twoFactor from '@app/graphql/resolvers/query/two-factor'; -import { vmsResolver } from '@app/graphql/resolvers/query/vms'; - -export const Query: QueryResolvers = { - array: getArray, - cloud, - config, - crashReportingEnabled, - disks: disksResolver, - dockerContainers: dockerContainersResolver, - display, - flash, - notifications: notificationsResolver, - online, - owner, - registration, - server, - servers, - twoFactor, - vms: vmsResolver, - info() { - // Returns an empty object because the subfield resolvers live at the root (allows for partial fetching) - return {}; - }, -}; diff --git a/api/src/graphql/resolvers/query/info.ts b/api/src/graphql/resolvers/query/info.ts index 48de8afda..0b36b0595 100644 --- a/api/src/graphql/resolvers/query/info.ts +++ b/api/src/graphql/resolvers/query/info.ts @@ -1,11 +1,9 @@ import { - baseboard, cpu, cpuFlags, mem, memLayout, osInfo, - system, versions, } from 'systeminformation'; import { docker } from '@app/core/utils/clients/docker'; @@ -16,13 +14,10 @@ import { type Display, type Theme, type Temperature, - type Baseboard, type Versions, type InfoMemory, type MemoryLayout, - type System, type Devices, - type InfoResolvers, type Gpu, } from '@app/graphql/generated/api/types'; import { getters } from '@app/store'; @@ -33,7 +28,6 @@ import toBytes from 'bytes'; import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version'; import { AppError } from '@app/core/errors/app-error'; import { cleanStdout } from '@app/core/utils/misc/clean-stdout'; -import { getMachineId } from '@app/core/utils/misc/get-machine-id'; import { execaCommandSync, execa } from 'execa'; import { pathExists } from 'path-exists'; import { filter as asyncFilter } from 'p-iteration'; @@ -58,19 +52,22 @@ export const generateApps = async (): Promise => { return { installed, started }; }; -const generateOs = async (): Promise => { +export const generateOs = async (): Promise => { const os = await osInfo(); return { ...os, + hostname: getters.emhttp().var.name, uptime: bootTimestamp.toISOString(), }; }; -const generateCpu = async (): Promise => { +export const generateCpu = async (): Promise => { const { cores, physicalCores, speedMin, speedMax, stepping, ...rest } = await cpu(); - const flags = await cpuFlags().then((flags) => flags.split(' ')); + const flags = await cpuFlags() + .then((flags) => flags.split(' ')) + .catch(() => []); return { ...rest, @@ -84,7 +81,7 @@ const generateCpu = async (): Promise => { }; }; -const generateDisplay = async (): Promise => { +export const generateDisplay = async (): Promise => { const filePath = getters.paths()['dynamix-config']; const state = loadState(filePath); if (!state) { @@ -110,9 +107,7 @@ const generateDisplay = async (): Promise => { }; }; -const generateBaseboard = async (): Promise => baseboard(); - -const generateVersions = async (): Promise => { +export const generateVersions = async (): Promise => { const unraid = await getUnraidVersion(); const softwareVersions = await versions(); @@ -122,10 +117,10 @@ const generateVersions = async (): Promise => { }; }; -const generateMemory = async (): Promise => { +export const generateMemory = async (): Promise => { const layout = await memLayout().then((dims) => dims.map((dim) => dim as MemoryLayout) - ); + ).catch(() => []); const info = await mem(); let max = info.total; @@ -175,7 +170,7 @@ const generateMemory = async (): Promise => { }; }; -const generateDevices = async (): Promise => { +export const generateDevices = async (): Promise => { /** * Set device class to device. * @param device The device to modify. @@ -277,24 +272,24 @@ const generateDevices = async (): Promise => { * @ignore * @private */ - const systemGPUDevices: Promise = systemPciDevices().then( - (devices) => { - return devices.filter( - (device) => device.class === 'vga' && !device.allowed - ).map(entry => { - const gpu: Gpu = { - blacklisted: entry.allowed, - class: entry.class, - id: entry.id, - productid: entry.product, - typeid: entry.typeid, - type: entry.manufacturer, - vendorname: entry.vendorname - } - return gpu; - }); - } - ).catch(() => []); + const systemGPUDevices: Promise = systemPciDevices() + .then((devices) => { + return devices + .filter((device) => device.class === 'vga' && !device.allowed) + .map((entry) => { + const gpu: Gpu = { + blacklisted: entry.allowed, + class: entry.class, + id: entry.id, + productid: entry.product, + typeid: entry.typeid, + type: entry.manufacturer, + vendorname: entry.vendorname, + }; + return gpu; + }); + }) + .catch(() => []); /** * System usb devices. @@ -422,13 +417,15 @@ const generateDevices = async (): Promise => { }) ?? []; // Get all usb devices - const usbDevices = await execa('lsusb').then(async ({ stdout }) => - parseUsbDevices(stdout) - .map(parseDevice) - .filter(filterBootDrive) - .filter(filterUsbHubs) - .map(sanitizeVendorName) - ); + const usbDevices = await execa('lsusb') + .then(async ({ stdout }) => + parseUsbDevices(stdout) + .map(parseDevice) + .filter(filterBootDrive) + .filter(filterUsbHubs) + .map(sanitizeVendorName) + ) + .catch(() => []); return usbDevices; } catch (error: unknown) { @@ -445,20 +442,3 @@ const generateDevices = async (): Promise => { usb: await getSystemUSBDevices(), }; }; - -const generateMachineId = async (): Promise => getMachineId(); - -const generateSystem = async (): Promise => system(); - -export const infoSubResolvers: InfoResolvers = { - apps: async () => generateApps(), - baseboard: async () => generateBaseboard(), - cpu: async () => generateCpu(), - devices: async () => generateDevices(), - display: async () => generateDisplay(), - machineId: async () => generateMachineId(), - memory: async () => generateMemory(), - os: async () => generateOs(), - system: async () => generateSystem(), - versions: async () => generateVersions(), -}; diff --git a/api/src/graphql/resolvers/query/notifications.ts b/api/src/graphql/resolvers/query/notifications.ts deleted file mode 100644 index a5a4471f4..000000000 --- a/api/src/graphql/resolvers/query/notifications.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { type QueryResolvers } from '@app/graphql/generated/api/types'; -import { getters } from '@app/store/index'; - -export const notificationsResolver: QueryResolvers['notifications'] = async ( - _, - { filter: { offset, limit, importance, type } }, - context -) => { - ensurePermission(context.user, { - possession: 'any', - resource: 'notifications', - action: 'read', - }); - - if (limit > 50) { - throw new Error('Limit must be less than 50'); - } - return Object.values(getters.notifications().notifications) - .filter((notification) => { - if ( - importance && - importance !== notification.importance - ) { - return false; - } - if (type && type !== notification.type) { - return false; - } - - return true; - }) - .sort( - (a, b) => - new Date(b.timestamp ?? 0).getTime() - - new Date(a.timestamp ?? 0).getTime() - ) - .slice( - offset, - limit + offset - ); -}; diff --git a/api/src/graphql/resolvers/query/online.ts b/api/src/graphql/resolvers/query/online.ts deleted file mode 100644 index 3c72b2d38..000000000 --- a/api/src/graphql/resolvers/query/online.ts +++ /dev/null @@ -1 +0,0 @@ -export default () => true; diff --git a/api/src/graphql/resolvers/query/registration.ts b/api/src/graphql/resolvers/query/registration.ts deleted file mode 100644 index 3025782c1..000000000 --- a/api/src/graphql/resolvers/query/registration.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*! - * Copyright 2021 Lime Technology Inc. All rights reserved. - * Written by: Alexis Tyler - */ - -import { getKeyFile } from '@app/core/utils/misc/get-key-file'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { type Registration, type QueryResolvers } from '@app/graphql/generated/api/types'; -import { getters } from '@app/store'; -import { FileLoadStatus } from '@app/store/types'; - -export const registration: QueryResolvers['registration'] = async (_, __, context) => { - ensurePermission(context.user, { - resource: 'registration', - action: 'read', - possession: 'any', - }); - - const emhttp = getters.emhttp(); - if (emhttp.status !== FileLoadStatus.LOADED || !emhttp.var?.regTy) { - return null; - } - - const isTrial = emhttp.var.regTy?.toLowerCase() === 'trial'; - const isExpired = emhttp.var.regTy.includes('expired'); - - const registration: Registration = { - guid: emhttp.var.regGuid, - type: emhttp.var.regTy, - state: emhttp.var.regState, - // Based on https://github.com/unraid/dynamix.unraid.net/blob/c565217fa8b2acf23943dc5c22a12d526cdf70a1/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php#L64 - expiration: - (1_000 * (isTrial || isExpired ? Number(emhttp.var.regTm2) : 0)).toString(), - keyFile: { - location: emhttp.var.regFile, - contents: await getKeyFile(), - }, - }; - return registration; -}; diff --git a/api/src/graphql/resolvers/query/server.ts b/api/src/graphql/resolvers/query/server.ts deleted file mode 100644 index bbb6d7170..000000000 --- a/api/src/graphql/resolvers/query/server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getServers } from '@app/graphql/schema/utils'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { type QueryResolvers } from '@app/graphql/generated/api/types'; - -export const server: QueryResolvers['server'] = async (_: unknown, { name }, context) => { - ensurePermission(context.user, { - resource: 'servers', - action: 'read', - possession: 'any', - }); - - const servers = getServers(); - - // Single server - return servers.find(server => server.name === name) ?? undefined; -}; diff --git a/api/src/graphql/resolvers/query/servers.ts b/api/src/graphql/resolvers/query/servers.ts deleted file mode 100644 index 6746a5a10..000000000 --- a/api/src/graphql/resolvers/query/servers.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getServers } from '@app/graphql/schema/utils'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { ServerStatus, type Resolvers } from '../../generated/api/types'; - -export const servers: NonNullable['servers'] = async (_, __, context) => { - ensurePermission(context.user, { - resource: 'servers', - action: 'read', - possession: 'any', - }); - - // All servers - const servers = getServers().map(server => ({ - ...server, - apikey: server.apikey ?? '', - guid: server.guid ?? '', - lanip: server.lanip ?? '', - localurl: server.localurl ?? '', - wanip: server.wanip ?? '', - name: server.name ?? '', - owner: { - ...server.owner, - username: server.owner?.username ?? '' - }, - remoteurl: server.remoteurl ?? '', - status: server.status ?? ServerStatus.OFFLINE - })) - return servers; -}; diff --git a/api/src/graphql/resolvers/query/vms.ts b/api/src/graphql/resolvers/query/vms.ts deleted file mode 100644 index a5da75931..000000000 --- a/api/src/graphql/resolvers/query/vms.ts +++ /dev/null @@ -1 +0,0 @@ -export const vmsResolver = () => ({}); diff --git a/api/src/graphql/resolvers/resolvers.ts b/api/src/graphql/resolvers/resolvers.ts deleted file mode 100644 index 26c33432b..000000000 --- a/api/src/graphql/resolvers/resolvers.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DateTimeResolver, JSONResolver, PortResolver, UUIDResolver } from 'graphql-scalars'; - -import { Query } from '@app/graphql/resolvers/query'; -import { Mutation } from '@app/graphql/resolvers/mutation'; -import { Subscription } from '@app/graphql/resolvers/subscription'; -import { UserAccount } from '@app/graphql/resolvers/user-account'; -import { type Resolvers } from '../generated/api/types'; -import { infoSubResolvers } from './query/info'; -import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long'; -import { domainResolver } from '@app/core/modules/index'; - -export const resolvers: Resolvers = { - JSON: JSONResolver, - Long: GraphQLLong, - UUID: UUIDResolver, - DateTime: DateTimeResolver, - Port: PortResolver, - Query, - Mutation, - Subscription, - UserAccount, - Info: { - ...infoSubResolvers, - }, - Vms: { - domain: domainResolver, - }, -}; diff --git a/api/src/graphql/resolvers/subscription/dashboard.ts b/api/src/graphql/resolvers/subscription/dashboard.ts index c0f80e2f8..aae1772c2 100644 --- a/api/src/graphql/resolvers/subscription/dashboard.ts +++ b/api/src/graphql/resolvers/subscription/dashboard.ts @@ -1,6 +1,6 @@ import { dashboardLogger } from '@app/core/log'; import { generateData } from '@app/common/dashboard/generate-data'; -import { pubsub } from '@app/core/pubsub'; +import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; import { getters, store } from '@app/store'; import { saveDataPacket } from '@app/store/modules/dashboard'; import { isEqual } from 'lodash'; @@ -63,12 +63,10 @@ export const publishToDashboard = async () => { store.dispatch(saveDataPacket({ lastDataPacket: dataPacket })); // Publish the updated data - dashboardLogger.addContext('update', dataPacket); - dashboardLogger.trace('Publishing update'); - dashboardLogger.removeContext('update'); + dashboardLogger.trace({ dataPacket } , 'Publishing update'); // Update local clients - await pubsub.publish('dashboard', { + await pubsub.publish(PUBSUB_CHANNEL.DASHBOARD, { dashboard: dataPacket, }); if (dataPacket) { diff --git a/api/src/graphql/resolvers/subscription/index.ts b/api/src/graphql/resolvers/subscription/index.ts deleted file mode 100644 index 25f9280ae..000000000 --- a/api/src/graphql/resolvers/subscription/index.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; -import { ensurePermission } from '@app/core/utils/permissions/ensure-permission'; -import { type Resolvers } from '@app/graphql/generated/api/types'; -import { createSubscription } from '@app/graphql/schema/utils'; - -export const Subscription: Resolvers['Subscription'] = { - display: { - ...createSubscription('display'), - }, - apikeys: { - // Not sure how we're going to secure this - // ...createSubscription('apikeys') - }, - config: { - ...createSubscription('config'), - }, - array: { - ...createSubscription('array'), - }, - dockerContainers: { - ...createSubscription('docker/container'), - }, - dockerNetworks: { - ...createSubscription('docker/network'), - }, - notificationAdded: { - subscribe: (_parent, _args, context) => { - ensurePermission(context.user, { - possession: 'any', - resource: 'notifications', - action: 'read', - }); - return { - [Symbol.asyncIterator]: () => - pubsub.asyncIterator(PUBSUB_CHANNEL.NOTIFICATION), - }; - }, - }, - info: { - ...createSubscription('info'), - }, - servers: { - ...createSubscription('servers'), - }, - shares: { - ...createSubscription('shares'), - }, - unassignedDevices: { - ...createSubscription('devices/unassigned'), - }, - users: { - ...createSubscription('users'), - }, - vars: { - ...createSubscription('vars'), - }, - vms: { - ...createSubscription('vms'), - }, - registration: { - ...createSubscription('registration'), - }, - online: { - ...createSubscription('online'), - }, - owner: { - ...createSubscription('owner'), - }, -}; diff --git a/api/src/graphql/resolvers/subscription/network.ts b/api/src/graphql/resolvers/subscription/network.ts index dbb8fd1ab..f32540f61 100644 --- a/api/src/graphql/resolvers/subscription/network.ts +++ b/api/src/graphql/resolvers/subscription/network.ts @@ -1,60 +1,82 @@ import { GraphQLClient } from '@app/mothership/graphql-client'; import { type Nginx } from '@app/core/types/states/nginx'; import { type RootState, store, getters } from '@app/store'; -import { type NetworkInput, URL_TYPE, type AccessUrlInput } from '@app/graphql/generated/client/graphql'; +import { + type NetworkInput, + URL_TYPE, + type AccessUrlInput, +} from '@app/graphql/generated/client/graphql'; import { dashboardLogger, logger } from '@app/core'; import { isEqual } from 'lodash'; import { SEND_NETWORK_MUTATION } from '@app/graphql/mothership/mutations'; import { saveNetworkPacket } from '@app/store/modules/dashboard'; import { ApolloError } from '@apollo/client/core/core.cjs'; -import { AccessUrlInputSchema, NetworkInputSchema } from '@app/graphql/generated/client/validators'; +import { + AccessUrlInputSchema, + NetworkInputSchema, +} from '@app/graphql/generated/client/validators'; import { ZodError } from 'zod'; interface UrlForFieldInput { - url: string; - port?: number; - portSsl?: number; + url: string; + port?: number; + portSsl?: number; } interface UrlForFieldInputSecure extends UrlForFieldInput { - url: string; - portSsl: number; + url: string; + portSsl: number; } interface UrlForFieldInputInsecure extends UrlForFieldInput { - url: string; - port: number; + url: string; + port: number; } -export const getUrlForField = ({ url, port, portSsl }: UrlForFieldInputInsecure | UrlForFieldInputSecure) => { - let portToUse = ''; - let httpMode = 'https://'; +export const getUrlForField = ({ + url, + port, + portSsl, +}: UrlForFieldInputInsecure | UrlForFieldInputSecure) => { + let portToUse = ''; + let httpMode = 'https://'; - if (!url || url === '') { - throw new Error('No URL Provided'); - } + if (!url || url === '') { + throw new Error('No URL Provided'); + } - if (port) { - portToUse = port === 80 ? '' : `:${port}`; - httpMode = 'http://'; - } else if (portSsl) { - portToUse = portSsl === 443 ? '' : `:${portSsl}`; - httpMode = 'https://'; - } else { - throw new Error(`No ports specified for URL: ${url}`); - } + if (port) { + portToUse = port === 80 ? '' : `:${port}`; + httpMode = 'http://'; + } else if (portSsl) { + portToUse = portSsl === 443 ? '' : `:${portSsl}`; + httpMode = 'https://'; + } else { + throw new Error(`No ports specified for URL: ${url}`); + } - const urlString = `${httpMode}${url}${portToUse}`; + const urlString = `${httpMode}${url}${portToUse}`; - try { - return new URL(urlString); - } catch (error: unknown) { - throw new Error(`Failed to parse URL: ${urlString}`); - } + try { + return new URL(urlString); + } catch (error: unknown) { + throw new Error(`Failed to parse URL: ${urlString}`); + } }; -const fieldIsFqdn = (field: keyof Nginx) => field?.toLowerCase().includes('fqdn'); +const fieldIsFqdn = (field: keyof Nginx) => + field?.toLowerCase().includes('fqdn'); -export type NginxUrlFields = Extract; +export type NginxUrlFields = Extract< + keyof Nginx, + | 'lanIp' + | 'lanIp6' + | 'lanName' + | 'lanMdns' + | 'lanFqdn' + | 'lanFqdn6' + | 'wanFqdn' + | 'wanFqdn6' +>; /** * @@ -63,254 +85,307 @@ export type NginxUrlFields = Extract { - if (nginx[field]) { - if (fieldIsFqdn(field)) { - return getUrlForField({ url: nginx[field], portSsl: nginx.httpsPort }); - } +export const getUrlForServer = ({ + nginx, + field, +}: { + nginx: Nginx; + field: NginxUrlFields; +}): URL => { + if (nginx[field]) { + if (fieldIsFqdn(field)) { + return getUrlForField({ + url: nginx[field], + portSsl: nginx.httpsPort, + }); + } - if (!nginx.sslEnabled) {// Use SSL = no - return getUrlForField({ url: nginx[field], port: nginx.httpPort }); - } + if (!nginx.sslEnabled) { + // Use SSL = no + return getUrlForField({ url: nginx[field], port: nginx.httpPort }); + } - if (nginx.sslMode === 'yes') { - return getUrlForField({ url: nginx[field], portSsl: nginx.httpsPort }); - } + if (nginx.sslMode === 'yes') { + return getUrlForField({ + url: nginx[field], + portSsl: nginx.httpsPort, + }); + } - if (nginx.sslMode === 'auto') { - throw new Error(`Cannot get IP Based URL for field: "${field}" SSL mode auto`); - } - } + if (nginx.sslMode === 'auto') { + throw new Error( + `Cannot get IP Based URL for field: "${field}" SSL mode auto` + ); + } + } - throw new Error(`IP URL Resolver: Could not resolve any access URL for field: "${field}", is FQDN?: ${fieldIsFqdn(field)}`); + throw new Error( + `IP URL Resolver: Could not resolve any access URL for field: "${field}", is FQDN?: ${fieldIsFqdn( + field + )}` + ); }; // eslint-disable-next-line complexity -export const getServerIps = (state: RootState = store.getState()): { urls: AccessUrlInput[]; errors: Error[] } => { - const { nginx } = state.emhttp; - const { remote: { wanport } } = state.config; - if (!nginx || Object.keys(nginx).length === 0) { - return { urls: [], errors: [new Error('Nginx Not Loaded')] }; - } +export const getServerIps = ( + state: RootState = store.getState() +): { urls: AccessUrlInput[]; errors: Error[] } => { + const { nginx } = state.emhttp; + const { + remote: { wanport }, + } = state.config; + if (!nginx || Object.keys(nginx).length === 0) { + return { urls: [], errors: [new Error('Nginx Not Loaded')] }; + } - const errors: Error[] = []; - const urls: AccessUrlInput[] = []; + const errors: Error[] = []; + const urls: AccessUrlInput[] = []; - try { - // Default URL - const defaultUrl = new URL(nginx.defaultUrl); - urls.push({ - name: 'Default', - type: URL_TYPE.DEFAULT, - ipv4: defaultUrl, - ipv6: defaultUrl, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Default URL + const defaultUrl = new URL(nginx.defaultUrl); + urls.push({ + name: 'Default', + type: URL_TYPE.DEFAULT, + ipv4: defaultUrl, + ipv6: defaultUrl, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // Lan IP URL - const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' }); - urls.push({ - name: 'LAN IPv4', - type: URL_TYPE.LAN, - ipv4: lanIp4Url, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Lan IP URL + const lanIp4Url = getUrlForServer({ nginx, field: 'lanIp' }); + urls.push({ + name: 'LAN IPv4', + type: URL_TYPE.LAN, + ipv4: lanIp4Url, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // Lan IP6 URL - const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' }); - urls.push({ - name: 'LAN IPv6', - type: URL_TYPE.LAN, - ipv4: lanIp6Url, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Lan IP6 URL + const lanIp6Url = getUrlForServer({ nginx, field: 'lanIp6' }); + urls.push({ + name: 'LAN IPv6', + type: URL_TYPE.LAN, + ipv4: lanIp6Url, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // Lan Name URL - const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' }); - urls.push({ - name: 'LAN Name', - type: URL_TYPE.MDNS, - ipv4: lanNameUrl, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Lan Name URL + const lanNameUrl = getUrlForServer({ nginx, field: 'lanName' }); + urls.push({ + name: 'LAN Name', + type: URL_TYPE.MDNS, + ipv4: lanNameUrl, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // Lan MDNS URL - const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' }); - urls.push({ - name: 'LAN MDNS', - type: URL_TYPE.MDNS, - ipv4: lanMdnsUrl, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Lan MDNS URL + const lanMdnsUrl = getUrlForServer({ nginx, field: 'lanMdns' }); + urls.push({ + name: 'LAN MDNS', + type: URL_TYPE.MDNS, + ipv4: lanMdnsUrl, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // Lan FQDN URL - const lanFqdnUrl = getUrlForServer({ nginx, field: 'lanFqdn' }); - urls.push({ - name: 'LAN FQDN', - type: URL_TYPE.LAN, - ipv4: lanFqdnUrl, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Lan FQDN URL + const lanFqdnUrl = getUrlForServer({ nginx, field: 'lanFqdn' }); + urls.push({ + name: 'LAN FQDN', + type: URL_TYPE.LAN, + ipv4: lanFqdnUrl, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // Lan FQDN6 URL - const lanFqdn6Url = getUrlForServer({ nginx, field: 'lanFqdn6' }); - urls.push({ - name: 'LAN FQDNv6', - type: URL_TYPE.LAN, - ipv6: lanFqdn6Url, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // Lan FQDN6 URL + const lanFqdn6Url = getUrlForServer({ nginx, field: 'lanFqdn6' }); + urls.push({ + name: 'LAN FQDNv6', + type: URL_TYPE.LAN, + ipv6: lanFqdn6Url, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // WAN FQDN URL - const wanFqdnUrl = getUrlForField({ url: nginx.wanFqdn, portSsl: Number(wanport || 443) }); - urls.push({ - name: 'WAN FQDN', - type: URL_TYPE.WAN, - ipv4: wanFqdnUrl, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // WAN FQDN URL + const wanFqdnUrl = getUrlForField({ + url: nginx.wanFqdn, + portSsl: Number(wanport || 443), + }); + urls.push({ + name: 'WAN FQDN', + type: URL_TYPE.WAN, + ipv4: wanFqdnUrl, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - try { - // WAN FQDN6 URL - const wanFqdn6Url = getUrlForField({ url: nginx.wanFqdn6, portSsl: Number(wanport) }); - urls.push({ - name: 'WAN FQDNv6', - type: URL_TYPE.WAN, - ipv6: wanFqdn6Url, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } + try { + // WAN FQDN6 URL + const wanFqdn6Url = getUrlForField({ + url: nginx.wanFqdn6, + portSsl: Number(wanport), + }); + urls.push({ + name: 'WAN FQDNv6', + type: URL_TYPE.WAN, + ipv6: wanFqdn6Url, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } - for (const wgFqdn of nginx.wgFqdns) { - try { - // WG FQDN URL - const wgFqdnUrl = getUrlForField({ url: wgFqdn.fqdn, portSsl: nginx.httpsPort }); - urls.push({ - name: `WG FQDN ${wgFqdn.id}`, - type: URL_TYPE.WIREGUARD, - ipv4: wgFqdnUrl, - }); - } catch (error: unknown) { - if (error instanceof Error) { - errors.push(error); - } else { - logger.warn('Uncaught error in network resolver', error); - } - } - } + for (const wgFqdn of nginx.wgFqdns) { + try { + // WG FQDN URL + const wgFqdnUrl = getUrlForField({ + url: wgFqdn.fqdn, + portSsl: nginx.httpsPort, + }); + urls.push({ + name: `WG FQDN ${wgFqdn.id}`, + type: URL_TYPE.WIREGUARD, + ipv4: wgFqdnUrl, + }); + } catch (error: unknown) { + if (error instanceof Error) { + errors.push(error); + } else { + logger.warn('Uncaught error in network resolver', error); + } + } + } - const safeUrls = urls.map((url) => AccessUrlInputSchema().safeParse(url)).reduce((acc, curr) => { - if (curr.success) { - acc.push(curr.data) - } else { - errors.push(curr.error) - } - return acc; - }, []); + const safeUrls = urls + .map((url) => AccessUrlInputSchema().safeParse(url)) + .reduce((acc, curr) => { + if (curr.success) { + acc.push(curr.data); + } else { + errors.push(curr.error); + } + return acc; + }, []); - return { urls: safeUrls, errors }; + return { urls: safeUrls, errors }; }; export const publishNetwork = async () => { - try { - const client = GraphQLClient.getInstance(); + try { + const client = GraphQLClient.getInstance(); - const datapacket = getServerIps(); - if (datapacket.errors ) { - const zodErrors = datapacket.errors.filter(error => error instanceof ZodError) - if (zodErrors.length) { - dashboardLogger.warn('Validation Errors Encountered with Network Payload: %s', zodErrors.map(error => error.message).join(',')) - } - } - const networkPacket: NetworkInput = { accessUrls: datapacket.urls } - const validatedNetwork = NetworkInputSchema().parse(networkPacket); - - const { lastNetworkPacket } = getters.dashboard(); - const { apikey: apiKey } = getters.config().remote; - if (isEqual(JSON.stringify(lastNetworkPacket), JSON.stringify(validatedNetwork))) { - dashboardLogger.trace('[DASHBOARD] Skipping Update'); - } else if (client) { - dashboardLogger.addContext('data', validatedNetwork); - dashboardLogger.info('Sending data packet for network'); - dashboardLogger.removeContext('data'); - const result = await client.mutate({ - mutation: SEND_NETWORK_MUTATION, - variables: { - apiKey, - data: validatedNetwork, - }, - }); - dashboardLogger.addContext('sendNetworkResult', result); - dashboardLogger.debug('Sent network mutation with %s urls', datapacket.urls.length); - dashboardLogger.removeContext('sendNetworkResult'); - store.dispatch(saveNetworkPacket({ lastNetworkPacket: validatedNetwork })); - } - } catch (error: unknown) { - dashboardLogger.trace('ERROR', error); - if (error instanceof ApolloError) { - dashboardLogger.error('Failed publishing with GQL Errors: %s, \nClient Errors: %s', error.graphQLErrors.map(error => error.message).join(','), error.clientErrors.join(', ')); - } else { - dashboardLogger.error(error); - } - } + const datapacket = getServerIps(); + if (datapacket.errors) { + const zodErrors = datapacket.errors.filter( + (error) => error instanceof ZodError + ); + if (zodErrors.length) { + dashboardLogger.warn( + 'Validation Errors Encountered with Network Payload: %s', + zodErrors.map((error) => error.message).join(',') + ); + } + } + const networkPacket: NetworkInput = { accessUrls: datapacket.urls }; + const validatedNetwork = NetworkInputSchema().parse(networkPacket); + + const { lastNetworkPacket } = getters.dashboard(); + const { apikey: apiKey } = getters.config().remote; + if ( + isEqual( + JSON.stringify(lastNetworkPacket), + JSON.stringify(validatedNetwork) + ) + ) { + dashboardLogger.trace('[DASHBOARD] Skipping Update'); + } else if (client) { + dashboardLogger.info( + { validatedNetwork }, + 'Sending data packet for network' + ); + const result = await client.mutate({ + mutation: SEND_NETWORK_MUTATION, + variables: { + apiKey, + data: validatedNetwork, + }, + }); + dashboardLogger.debug( + { result }, + 'Sent network mutation with %s urls', + datapacket.urls.length + ); + store.dispatch( + saveNetworkPacket({ lastNetworkPacket: validatedNetwork }) + ); + } + } catch (error: unknown) { + dashboardLogger.trace('ERROR', error); + if (error instanceof ApolloError) { + dashboardLogger.error( + 'Failed publishing with GQL Errors: %s, \nClient Errors: %s', + error.graphQLErrors.map((error) => error.message).join(','), + error.clientErrors.join(', ') + ); + } else { + dashboardLogger.error(error); + } + } }; diff --git a/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts b/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts index fa1ae470e..0df2c56b0 100644 --- a/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts +++ b/api/src/graphql/resolvers/subscription/remote-graphql/remote-query.ts @@ -13,9 +13,7 @@ import { getters } from '@app/store/index'; export const executeRemoteGraphQLQuery = async ( data: RemoteGraphQLEventFragmentFragment['remoteGraphQLEventData'] ) => { - remoteQueryLogger.addContext('data', data); - remoteQueryLogger.debug('Executing remote query'); - remoteQueryLogger.removeContext('data'); + remoteQueryLogger.debug({ query: data }, 'Executing remote query'); const client = GraphQLClient.getInstance(); const apiKey = getters.config().remote.apikey; const originalBody = data.body; @@ -25,18 +23,14 @@ export const executeRemoteGraphQLQuery = async ( upcApiKey: apiKey }); if (ENVIRONMENT === 'development') { - remoteQueryLogger.addContext('query', parsedQuery.query); - remoteQueryLogger.debug('[DEVONLY] Running query'); - remoteQueryLogger.removeContext('query'); + remoteQueryLogger.debug({ query: parsedQuery.query }, '[DEVONLY] Running query'); } const localResult = await localClient.query({ query: parsedQuery.query, variables: parsedQuery.variables, }); if (localResult.data) { - remoteQueryLogger.addContext('data', localResult.data); - remoteQueryLogger.trace('Got data from remoteQuery request', data.sha256); - remoteQueryLogger.removeContext('data') + remoteQueryLogger.trace({ data: localResult.data }, 'Got data from remoteQuery request', data.sha256); await client?.mutate({ mutation: SEND_REMOTE_QUERY_RESPONSE, @@ -77,8 +71,6 @@ export const executeRemoteGraphQLQuery = async ( } catch (error) { remoteQueryLogger.warn('Could not respond %o', error); } - remoteQueryLogger.addContext('error', err); remoteQueryLogger.error('Error executing remote query %s', err instanceof Error ? err.message: 'Unknown Error'); - remoteQueryLogger.removeContext('error'); } }; diff --git a/api/src/graphql/resolvers/user-account.ts b/api/src/graphql/resolvers/user-account.ts deleted file mode 100644 index 1bda6a9d5..000000000 --- a/api/src/graphql/resolvers/user-account.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const UserAccount = { - __resolveType(obj: Record) { - // Only a user has a password field, the current user aka "me" doesn't. - return obj.password ? 'User' : 'Me'; - }, -}; diff --git a/api/src/graphql/schema.ts b/api/src/graphql/schema.ts new file mode 100644 index 000000000..dcf88f7a8 --- /dev/null +++ b/api/src/graphql/schema.ts @@ -0,0 +1,10 @@ +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { resolvers } from '@app/graphql/resolvers/resolvers'; +import { typeDefs } from '@app/graphql/schema/index'; + +const baseSchema = makeExecutableSchema({ + typeDefs: typeDefs, + resolvers, +}); + +export const schema = (baseSchema); diff --git a/api/src/graphql/schema/types/apikeys/apikey.graphql b/api/src/graphql/schema/types/apikeys/apikey.graphql new file mode 100644 index 000000000..58ba4b6f2 --- /dev/null +++ b/api/src/graphql/schema/types/apikeys/apikey.graphql @@ -0,0 +1,42 @@ +input authenticateInput { + password: String! +} + +input addApiKeyInput { + name: String + key: String + userId: String +} + +input updateApikeyInput { + description: String + expiresAt: Long! +} + +type Query { + """Get all API keys""" + apiKeys: [ApiKey] +} + +type Mutation { + """Get an existing API key""" + getApiKey(name: String!, input: authenticateInput): ApiKey + + """Create a new API key""" + addApikey(name: String!, input: updateApikeyInput): ApiKey + + """Update an existing API key""" + updateApikey(name: String!, input: updateApikeyInput): ApiKey +} + +type Subscription { + apikeys: [ApiKey] +} + +type ApiKey { + name: String! + key: String! + description: String + scopes: JSON! + expiresAt: Long! +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/array/array.graphql b/api/src/graphql/schema/types/array/array.graphql index be70c2b6d..dd8b1ebea 100644 --- a/api/src/graphql/schema/types/array/array.graphql +++ b/api/src/graphql/schema/types/array/array.graphql @@ -5,14 +5,14 @@ type Query { type Mutation { """Start array""" - startArray: Array @func(module: "updateArray", data: { state: "start" }) + startArray: Array """Stop array""" - stopArray: Array @func(module: "updateArray", data: { state: "stop" }) + stopArray: Array """Add new disk to array""" - addDiskToArray(input: arrayDiskInput): Array @func(module: "addDiskToArray") + addDiskToArray(input: arrayDiskInput): Array """Remove existing disk from array. NOTE: The array must be stopped before running this otherwise it'll throw an error.""" - removeDiskFromArray(input: arrayDiskInput): Array @func(module: "removeDiskFromArray") + removeDiskFromArray(input: arrayDiskInput): Array mountArrayDisk(id: ID!): Disk unmountArrayDisk(id: ID!): Disk diff --git a/api/src/graphql/schema/types/array/parity.graphql b/api/src/graphql/schema/types/array/parity.graphql new file mode 100644 index 000000000..4430c2a39 --- /dev/null +++ b/api/src/graphql/schema/types/array/parity.graphql @@ -0,0 +1,26 @@ +type Query { + parityHistory: [ParityCheck] +} + +type Mutation { + """Start parity check""" + startParityCheck(correct: Boolean): JSON + """Pause parity check""" + pauseParityCheck: JSON + """Resume parity check""" + resumeParityCheck: JSON + """Cancel parity check""" + cancelParityCheck: JSON +} + +type Subscription { + parityHistory: ParityCheck! +} + +type ParityCheck { + date: String! + duration: Int! + speed: String! + status: String! + errors: String! +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/base.graphql b/api/src/graphql/schema/types/base.graphql new file mode 100644 index 000000000..581da86f5 --- /dev/null +++ b/api/src/graphql/schema/types/base.graphql @@ -0,0 +1,28 @@ +scalar JSON +scalar Long +scalar UUID +scalar DateTime +scalar Port + +type Welcome { + message: String! +} + +type Query { + # This should always be available even for guest users + online: Boolean + info: Info +} + +type Mutation { + login(username: String!, password: String!): String + sendNotification(notification: NotificationInput!): Notification + shutdown: String + reboot: String +} + +type Subscription { + ping: String! + info: Info! + online: Boolean! +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/disks/disk.graphql b/api/src/graphql/schema/types/disks/disk.graphql new file mode 100644 index 000000000..3017fab14 --- /dev/null +++ b/api/src/graphql/schema/types/disks/disk.graphql @@ -0,0 +1,65 @@ +type Query { + """Single disk""" + disk(id: ID!): Disk + """Mulitiple disks""" + disks: [Disk]! +} +type Disk { + # /dev/sdb + device: String! + # SSD + type: String! + # Samsung_SSD_860_QVO_1TB + name: String! + # Samsung + vendor: String! + # 1000204886016 + size: Long! + # -1 + bytesPerSector: Long! + # -1 + totalCylinders: Long! + # -1 + totalHeads: Long! + # -1 + totalSectors: Long! + # -1 + totalTracks: Long! + # -1 + tracksPerCylinder: Long! + # -1 + sectorsPerTrack: Long! + # 1B6Q + firmwareRevision: String! + # S4CZNF0M807232N + serialNum: String! + interfaceType: DiskInterfaceType! + smartStatus: DiskSmartStatus! + temperature: Long! + partitions: [DiskPartition!] +} + +type DiskPartition { + name: String! + fsType: DiskFsType! + size: Long! +} + +enum DiskFsType { + xfs + btrfs + vfat +} + +enum DiskInterfaceType { + SAS + SATA + USB + PCIe + UNKNOWN +} + +enum DiskSmartStatus { + OK + UNKNOWN +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/docker/network.graphql b/api/src/graphql/schema/types/docker/network.graphql new file mode 100644 index 000000000..5bb566cfd --- /dev/null +++ b/api/src/graphql/schema/types/docker/network.graphql @@ -0,0 +1,29 @@ +type Query { + """Docker network""" + dockerNetwork(id: ID!): DockerNetwork! + """All Docker networks""" + dockerNetworks(all: Boolean): [DockerNetwork]! +} + +type Subscription { + dockerNetwork(id: ID!): DockerNetwork! + dockerNetworks: [DockerNetwork]! +} + +type DockerNetwork { + name: String + id: ID + created: String + scope: String + driver: String + enableIPv6: Boolean! + ipam: JSON + internal: Boolean! + attachable: Boolean! + ingress: Boolean! + configFrom: JSON + configOnly: Boolean! + containers: JSON + options: JSON + labels: JSON +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/servers/server.graphql b/api/src/graphql/schema/types/servers/server.graphql new file mode 100644 index 000000000..574ff556d --- /dev/null +++ b/api/src/graphql/schema/types/servers/server.graphql @@ -0,0 +1,34 @@ +type Query { + server: Server + servers: [Server!]! +} + +type Subscription { + server: Server +} + +enum ServerStatus { + online + offline + never_connected +} + + +type ProfileModel { + userId: ID + username: String + url: String + avatar: String +} + +type Server { + owner: ProfileModel! + guid: String! + apikey: String! + name: String! + status: ServerStatus! + wanip: String! + lanip: String! + localurl: String! + remoteurl: String! +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/shares/share.graphql b/api/src/graphql/schema/types/shares/share.graphql index ae8ecc4c4..244302a06 100644 --- a/api/src/graphql/schema/types/shares/share.graphql +++ b/api/src/graphql/schema/types/shares/share.graphql @@ -1,6 +1,6 @@ type Query { """Network Shares""" - shares: [Share] @func(module: "getAllShares") + shares: [Share] } type Subscription { diff --git a/api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql b/api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql new file mode 100644 index 000000000..b3d3f733c --- /dev/null +++ b/api/src/graphql/schema/types/unassigned-devices/unassigned-device.graphql @@ -0,0 +1,62 @@ +type Query { + unassignedDevices: [UnassignedDevice] +} + +type Subscription { + unassignedDevices: [UnassignedDevice!] +} + +type UnassignedDevice { + devlinks: String + devname: String + devpath: String + devtype: String + idAta: String + idAtaDownloadMicrocode: String + idAtaFeatureSetAam: String + idAtaFeatureSetAamCurrentValue: String + idAtaFeatureSetAamEnabled: String + idAtaFeatureSetAamVendorRecommendedValue: String + idAtaFeatureSetApm: String + idAtaFeatureSetApmCurrentValue: String + idAtaFeatureSetApmEnabled: String + idAtaFeatureSetHpa: String + idAtaFeatureSetHpaEnabled: String + idAtaFeatureSetPm: String + idAtaFeatureSetPmEnabled: String + idAtaFeatureSetPuis: String + idAtaFeatureSetPuisEnabled: String + idAtaFeatureSetSecurity: String + idAtaFeatureSetSecurityEnabled: String + idAtaFeatureSetSecurityEnhancedEraseUnitMin: String + idAtaFeatureSetSecurityEraseUnitMin: String + idAtaFeatureSetSmart: String + idAtaFeatureSetSmartEnabled: String + idAtaRotationRateRpm: String + idAtaSata: String + idAtaSataSignalRateGen1: String + idAtaSataSignalRateGen2: String + idAtaWriteCache: String + idAtaWriteCacheEnabled: String + idBus: String + idModel: String + idModelEnc: String + idPartTableType: String + idPath: String + idPathTag: String + idRevision: String + idSerial: String + idSerialShort: String + idType: String + idWwn: String + idWwnWithExtension: String + major: String + minor: String + subsystem: String + usecInitialized: String + partitions: [Partition] + temp: Int + name: String + mounted: Boolean + mount: Mount +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/users/me.graphql b/api/src/graphql/schema/types/users/me.graphql new file mode 100644 index 000000000..23affdfc1 --- /dev/null +++ b/api/src/graphql/schema/types/users/me.graphql @@ -0,0 +1,17 @@ +type Query { + """Current user account""" + me: Me +} + +"""The current user""" +type Me implements UserAccount { + id: ID! + name: String! + description: String! + roles: String! + permissions: JSON +} + +type Subscription { + me: Me +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/users/user.graphql b/api/src/graphql/schema/types/users/user.graphql new file mode 100644 index 000000000..3389b1419 --- /dev/null +++ b/api/src/graphql/schema/types/users/user.graphql @@ -0,0 +1,50 @@ +interface UserAccount { + id: ID! + name: String! + description: String! + roles: String! +} + +input usersInput { + slim: Boolean +} + +type Query { + """User account""" + user(id: ID!): User + """User accounts""" + users(input: usersInput): [User!]! +} + +input addUserInput { + name: String! + password: String! + description: String +} + +input deleteUserInput { + name: String! +} + +type Mutation { + """Add a new user""" + addUser(input: addUserInput!): User + """Delete a user""" + deleteUser(input: deleteUserInput!): User +} + +type Subscription { + user(id: ID!): User! + users: [User]! +} + +"""A local user account""" +type User implements UserAccount { + id: ID! + """A unique name for the user""" + name: String! + description: String! + roles: String! + """If the account has a password set""" + password: Boolean +} \ No newline at end of file diff --git a/api/src/graphql/schema/types/vars/vars.graphql b/api/src/graphql/schema/types/vars/vars.graphql index 1a3ba9241..00a723e94 100644 --- a/api/src/graphql/schema/types/vars/vars.graphql +++ b/api/src/graphql/schema/types/vars/vars.graphql @@ -1,5 +1,5 @@ type Query { - vars: Vars @func(module: "getVars") + vars: Vars } type Subscription { diff --git a/api/src/graphql/schema/utils.ts b/api/src/graphql/schema/utils.ts index 6568cd6ed..e77ae8314 100644 --- a/api/src/graphql/schema/utils.ts +++ b/api/src/graphql/schema/utils.ts @@ -39,7 +39,7 @@ export const createSubscription = (channel: string, resource?: string) => ({ }); // eslint-disable-next-line @typescript-eslint/no-unused-vars -const getLocalServer = (getState = store.getState): Array => { +export const getLocalServer = (getState = store.getState): Array => { const { emhttp, config, minigraph } = getState(); const guid = emhttp.var.regGuid; const { name } = emhttp.var; @@ -58,7 +58,7 @@ const getLocalServer = (getState = store.getState): Array => { }, guid, apikey: config.remote.apikey ?? '', - name, + name: name ?? 'Local Server', status: minigraph.status === MinigraphStatus.CONNECTED ? ServerStatus.ONLINE diff --git a/api/src/graphql/types.ts b/api/src/graphql/types.ts deleted file mode 100644 index 0b23ad8d7..000000000 --- a/api/src/graphql/types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { mergeTypeDefs } from '@graphql-tools/merge'; -import { gql } from 'graphql-tag'; -import { typeDefs } from '@app/graphql/schema/index'; - -export const baseTypes = [ - gql` - scalar JSON - scalar Long - scalar UUID - scalar DateTime - scalar Port - - directive @subscription(channel: String!) on FIELD_DEFINITION - - type Welcome { - message: String! - } - - type Query { - # This should always be available even for guest users - welcome: Welcome @func(module: "getWelcome") - online: Boolean - info: Info - } - - type Mutation { - login(username: String!, password: String!): String - sendNotification(notification: NotificationInput!): Notification - shutdown: String - reboot: String - } - - type Subscription { - ping: String! - info: Info! - online: Boolean! - } - `, -]; - -export const types = mergeTypeDefs([ - ...baseTypes, - typeDefs, -]); - -export default types; diff --git a/api/src/index.ts b/api/src/index.ts index e83ed0ac8..f8cb8dc60 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -12,20 +12,23 @@ import { loadStateFiles } from '@app/store/modules/emhttp'; import { StateManager } from '@app/store/watch/state-watch'; import { setupRegistrationKeyWatch } from '@app/store/watch/registration-watch'; import { loadRegistrationKey } from '@app/store/modules/registration'; -import { createApolloExpressServer } from '@app/server'; import { unlinkSync } from 'fs'; import { fileExistsSync } from '@app/core/utils/files/file-exists'; import { PORT, environment } from '@app/environment'; import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event'; import { PingTimeoutJobs } from '@app/mothership/jobs/ping-timeout-jobs'; -import { type BaseContext, type ApolloServer } from '@apollo/server'; import { setupDynamixConfigWatch } from '@app/store/watch/dynamix-config-watch'; import { setupVarRunWatch } from '@app/store/watch/var-run-watch'; import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file'; import { startMiddlewareListeners } from '@app/store/listeners/listener-middleware'; import { validateApiKeyIfPresent } from '@app/store/listeners/api-key-listener'; +import { bootstrapNestServer } from '@app/unraid-api/main'; +import { type NestFastifyApplication } from '@nestjs/platform-fastify'; +import { type RawServerDefault } from 'fastify'; +import { setupLogRotation } from '@app/core/logrotate/setup-logrotate'; +import * as env from '@app/environment'; -let server: ApolloServer; +let server: NestFastifyApplication; const unlinkUnixPort = () => { if (isNaN(parseInt(PORT, 10))) { @@ -36,6 +39,9 @@ const unlinkUnixPort = () => { void am( async () => { environment.IS_MAIN_PROCESS = true; + + logger.debug('ENV %o', env); + const cacheable = new CacheableLookup(); Object.assign(global, { WebSocket: require('ws') }); @@ -47,6 +53,8 @@ void am( // Must occur before config is loaded to ensure that the handler can fix broken configs await startStoreSync(); + await setupLogRotation(); + // Load my servers config file into store await store.dispatch(loadConfigFile()); @@ -78,8 +86,7 @@ void am( unlinkUnixPort(); // Start webserver - server = await createApolloExpressServer(); - + server = await bootstrapNestServer(); PingTimeoutJobs.init(); startMiddlewareListeners(); @@ -88,6 +95,7 @@ void am( // On process exit stop HTTP server - this says it supports async but it doesnt seem to exitHook(() => { + server?.close?.(); // If port is unix socket, delete socket before exiting unlinkUnixPort(); @@ -96,14 +104,10 @@ void am( }); }, async (error: NodeJS.ErrnoException) => { - // Log error to syslog - logger.error('API-GLOBAL-ERROR', error); + logger.error('API-GLOBAL-ERROR %s %s', error.message, error.stack); shutdownApiEvent(); - - // Stop server - logger.debug('Stopping HTTP server'); if (server) { - await server.stop(); + await server?.close?.(); } // Kill application diff --git a/api/src/mothership/api-key/validate-api-key-with-keyserver.ts b/api/src/mothership/api-key/validate-api-key-with-keyserver.ts index b6d8895c4..383cfa360 100644 --- a/api/src/mothership/api-key/validate-api-key-with-keyserver.ts +++ b/api/src/mothership/api-key/validate-api-key-with-keyserver.ts @@ -12,7 +12,7 @@ import { type Response } from 'got'; export const validateApiKeyWithKeyServer = async ({ flashGuid, apiKey }: { flashGuid: string; apiKey: string }): Promise => { // If we're still loading config state, just return the config is loading - ksLog.log('Validating API Key with KeyServer'); + ksLog.info('Validating API Key with KeyServer'); // Send apiKey, etc. to key-server for verification let response: Response; @@ -22,9 +22,7 @@ export const validateApiKeyWithKeyServer = async ({ flashGuid, apiKey }: { flash apikey: apiKey, }); } catch (error: unknown) { - ksLog.addContext('networkError', error); - ksLog.error('Caught error reaching Key Server'); - ksLog.removeContext('networkError'); + ksLog.error({ error }, 'Caught error reaching Key Server'); return API_KEY_STATUS.NETWORK_ERROR; } diff --git a/api/src/mothership/graphql-client.ts b/api/src/mothership/graphql-client.ts index 6aeef0c69..1ce2388c6 100644 --- a/api/src/mothership/graphql-client.ts +++ b/api/src/mothership/graphql-client.ts @@ -33,7 +33,7 @@ const getWebsocketWithMothershipHeaders = () => { headers: getMothershipWebsocketHeaders(), }); } - } + }; }; const delayFn = buildDelayFunction({ @@ -89,7 +89,7 @@ export class GraphQLClient { const isStateValid = isAPIStateDataFullyLoaded() && isApiKeyValid(); if (!GraphQLClient.instance && isStateValid) { - minigraphLogger.debug("Creating a new Apollo Client Instance"); + minigraphLogger.debug('Creating a new Apollo Client Instance'); GraphQLClient.instance = GraphQLClient.createGraphqlClient(); } @@ -128,10 +128,10 @@ export class GraphQLClient { logoutUser({ reason: 'Invalid API Key on Mothership' }) ); } - + const getDelay = delayFn(count); store.dispatch(setMothershipTimeout(getDelay)); - minigraphLogger.info('Delay currently is', getDelay); + minigraphLogger.info('Delay currently is: %i', getDelay); return getDelay; }, attempts: { max: Infinity }, diff --git a/api/src/mothership/jobs/token-refresh-jobs.ts b/api/src/mothership/jobs/token-refresh-jobs.ts new file mode 100644 index 000000000..0937c5132 --- /dev/null +++ b/api/src/mothership/jobs/token-refresh-jobs.ts @@ -0,0 +1,56 @@ +import { OAUTH_CLIENT_ID, OAUTH_OPENID_CONFIGURATION_URL } from '@app/consts'; +import { mothershipLogger } from '@app/core'; +import { getters, store } from '@app/store'; +import { updateAccessTokens } from '@app/store/modules/config'; +import { Cron, Expression, Initializer } from '@reflet/cron'; +import { Issuer } from 'openid-client'; + +export class TokenRefresh extends Initializer { + private issuer: Issuer | null = null; + + @Cron.PreventOverlap + @Cron(Expression.EVERY_DAY_AT_NOON) + @Cron.RunOnInit + async getNewTokens() { + const { + remote: { refreshtoken }, + } = getters.config(); + + if (!refreshtoken) { + mothershipLogger.debug('No JWT refresh token configured'); + return; + } + + if (!this.issuer) { + try { + this.issuer = await Issuer.discover( + OAUTH_OPENID_CONFIGURATION_URL + ); + + mothershipLogger.trace( + 'Discovered Issuer %s', + this.issuer.issuer + ); + } catch (error: unknown) { + mothershipLogger.error({ error }, 'Failed to discover issuer'); + return; + } + } + + const client = new this.issuer.Client({ + client_id: OAUTH_CLIENT_ID, + token_endpoint_auth_method: 'none', + }); + + const newTokens = await client.refresh(refreshtoken); + mothershipLogger.debug('tokens %o', newTokens); + if (newTokens.access_token && newTokens.id_token) { + store.dispatch( + updateAccessTokens({ + accesstoken: newTokens.access_token, + idtoken: newTokens.id_token, + }) + ); + } + } +} diff --git a/api/src/mothership/subscribe-to-mothership.ts b/api/src/mothership/subscribe-to-mothership.ts new file mode 100644 index 000000000..178e74f13 --- /dev/null +++ b/api/src/mothership/subscribe-to-mothership.ts @@ -0,0 +1,135 @@ +/* eslint-disable max-depth */ +import { minigraphLogger, mothershipLogger } from '@app/core/log'; +import { GraphQLClient } from './graphql-client'; +import { store } from '@app/store'; +import { + startDashboardProducer, + stopDashboardProducer, +} from '@app/store/modules/dashboard'; + +import { + EVENTS_SUBSCRIPTION, + RemoteAccess_Fragment, + RemoteGraphQL_Fragment, +} from '@app/graphql/mothership/subscriptions'; + +import { ClientType } from '@app/graphql/generated/client/graphql'; +import { notNull } from '@app/utils'; +import { handleRemoteAccessEvent } from '@app/store/actions/handle-remote-access-event'; +import { useFragment } from '@app/graphql/generated/client/fragment-masking'; +import { handleRemoteGraphQLEvent } from '@app/store/actions/handle-remote-graphql-event'; +import { + setSelfDisconnected, + setSelfReconnected, +} from '@app/store/modules/minigraph'; + +export const subscribeToEvents = async (apiKey: string) => { + minigraphLogger.info('Subscribing to Events'); + const client = GraphQLClient.getInstance(); + if (!client) { + throw new Error('Unable to use client - state must not be loaded'); + } + + const eventsSub = client.subscribe({ + query: EVENTS_SUBSCRIPTION, + fetchPolicy: 'no-cache', + }); + eventsSub.subscribe(async ({ data, errors }) => { + if (errors) { + mothershipLogger.error( + 'GraphQL Error with events subscription: %s', + errors.join(',') + ); + } else if (data) { + mothershipLogger.trace({ events: data.events }, 'Got events from mothership'); + + for (const event of data.events?.filter(notNull) ?? []) { + switch (event.__typename) { + case 'ClientConnectedEvent': { + const { + connectedData: { type, apiKey: eventApiKey }, + } = event; + // Another server connected to Mothership + if (type === ClientType.API) { + if (eventApiKey === apiKey) { + // We are online, clear timeout waiting if it's set + store.dispatch(setSelfReconnected()); + } + } + + // Dashboard Connected to Mothership + + if ( + type === ClientType.DASHBOARD && + apiKey === eventApiKey + ) { + store.dispatch(startDashboardProducer()); + } + + break; + } + + case 'ClientDisconnectedEvent': { + const { + disconnectedData: { type, apiKey: eventApiKey }, + } = event; + // Server Disconnected From Mothership + if (type === ClientType.API) { + if (eventApiKey === apiKey) { + store.dispatch(setSelfDisconnected()); + } + } + + // The dashboard was closed or went idle + + if ( + type === ClientType.DASHBOARD && + apiKey === eventApiKey + ) { + store.dispatch(stopDashboardProducer()); + } + + break; + } + + case 'RemoteAccessEvent': { + const eventAsRemoteAccessEvent = useFragment( + RemoteAccess_Fragment, + event + ); + + if (eventAsRemoteAccessEvent.data.apiKey === apiKey) { + void store.dispatch( + handleRemoteAccessEvent( + eventAsRemoteAccessEvent + ) + ); + } + + break; + } + + case 'RemoteGraphQLEvent': { + const eventAsRemoteGraphQLEvent = useFragment( + RemoteGraphQL_Fragment, + event + ); + // No need to check API key here anymore + + void store.dispatch( + handleRemoteGraphQLEvent(eventAsRemoteGraphQLEvent) + ); + break; + } + + case 'UpdateEvent': { + break; + } + + default: + break; + } + } + } + }); +}; diff --git a/api/src/mothership/utils/delay-function.ts b/api/src/mothership/utils/delay-function.ts new file mode 100644 index 000000000..62f6373fa --- /dev/null +++ b/api/src/mothership/utils/delay-function.ts @@ -0,0 +1,25 @@ +import { type DelayFunctionOptions } from '@apollo/client/link/retry/delayFunction'; + +export function buildDelayFunction( + delayOptions?: DelayFunctionOptions, +): (count: number) => number { + const { initial = 10_000, jitter = true, max = Infinity } = delayOptions ?? {}; + // If we're jittering, baseDelay is half of the maximum delay for that + // attempt (and is, on average, the delay we will encounter). + // If we're not jittering, adjust baseDelay so that the first attempt + // lines up with initialDelay, for everyone's sanity. + const baseDelay = jitter ? initial : initial / 2; + + return (count: number) => { + // eslint-disable-next-line no-mixed-operators + let delay = Math.min(max, baseDelay * 2 ** count); + if (jitter) { + // We opt for a full jitter approach for a mostly uniform distribution, + // but bound it within initialDelay and delay for everyone's sanity. + // eslint-disable-next-line operator-assignment + delay = Math.random() * delay; + } + + return Math.round(delay); + }; +} diff --git a/api/src/originMiddleware.ts b/api/src/originMiddleware.ts deleted file mode 100644 index a3c117e56..000000000 --- a/api/src/originMiddleware.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { type NextFunction, type Request, type Response } from 'express'; -import { logger } from '@app/core'; -import { getAllowedOrigins } from '@app/common/allowed-origins'; -import { LOG_CORS } from '@app/environment'; - -const getOriginGraphqlError = () => ({ - data: null, - errors: [ - { - message: - 'The CORS policy for this site does not allow access from the specified Origin.', - }, - ], -}); - -/** - * Middleware to check a users origin and send a GraphQL error if they are not using a valid one - * @param req Express Request - * @param res Express Response - * @param next Express NextFunction - * @returns void - */ -export const originMiddleware = ( - req: Request, - res: Response, - next: NextFunction -): void => { - if (req.method === 'GET' && req.query.apiKey && !req.headers.origin) { - // Bypass GET request headers on requests to the log endpoint - return next(); - } - // Dev Mode Bypass - const origin = req.get('Origin')?.toLowerCase() ?? ''; - const allowedOrigins = getAllowedOrigins(); - - if (process.env.BYPASS_CORS_CHECKS === 'true') { - logger.addContext('cors', allowedOrigins); - logger.warn(`BYPASSING_CORS_CHECK: %o`, req.headers); - logger.removeContext('cors'); - next(); - return; - } else { - if (LOG_CORS) { - logger.addContext('origins', allowedOrigins.join(', ')); - logger.trace(`Current Origin: ${origin ?? 'undefined'}`); - logger.removeContext('origins'); - } - } - - // Disallow requests with no origin - // (like mobile apps, curl requests or viewing /graphql directly) - if (!origin) { - logger.debug('No origin provided, denying CORS!'); - res.status(403).send(getOriginGraphqlError()); - return; - } - - if (LOG_CORS) { - logger.trace(`📒 Checking "${origin}" for CORS access.`); - } - - // Only allow known origins - if (!allowedOrigins.includes(origin)) { - logger.error( - '❌ %s is not in the allowed origins list, denying CORS!', - origin - ); - res.status(403).send(getOriginGraphqlError()); - return; - } - - if (LOG_CORS) { - logger.trace('✔️ Origin check passed, granting CORS!'); - } - next(); -}; diff --git a/api/src/server.ts b/api/src/server.ts deleted file mode 100644 index 176af5841..000000000 --- a/api/src/server.ts +++ /dev/null @@ -1,355 +0,0 @@ -import path from 'path'; -import cors from 'cors'; -import { watch } from 'chokidar'; -import express, { json, type Request, type Response } from 'express'; -import http from 'http'; -import { ApolloServer } from '@apollo/server'; -import { expressMiddleware } from '@apollo/server/express4'; -import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; -import { logger, pubsub, graphqlLogger } from '@app/core'; -import { verifyTwoFactorToken } from '@app/common/two-factor'; -import display from '@app/graphql/resolvers/query/display'; -import { getters } from '@app/store'; -import { schema } from '@app/graphql/schema'; -import { execute, subscribe } from 'graphql'; -import { GRAPHQL_WS, SubscriptionServer } from 'subscriptions-transport-ws'; -import { wsHasConnected, wsHasDisconnected } from '@app/ws'; -import { apiKeyToUser } from '@app/graphql'; -import { randomUUID } from 'crypto'; -import { getServerAddress } from '@app/common/get-server-address'; -import { originMiddleware } from '@app/originMiddleware'; -import { API_VERSION, GRAPHQL_INTROSPECTION, PORT } from '@app/environment'; -import { - getBannerPathIfPresent, - getCasePathIfPresent, -} from '@app/core/utils/images/image-file-helpers'; -import { WebSocketServer } from 'ws'; -import { useServer } from 'graphql-ws/lib/use/ws'; -import { GRAPHQL_TRANSPORT_WS_PROTOCOL } from 'graphql-ws'; -import { getLogs } from '@app/graphql/express/get-logs'; - -const configFilePath = path.join( - getters.paths()['dynamix-base'], - 'case-model.cfg' -); -const customImageFilePath = path.join( - getters.paths()['dynamix-base'], - 'case-model.png' -); - -const updatePubsub = async () => { - await pubsub.publish('display', { - display: await display(), - }); -}; - -// Update pub/sub when config/image file is added/updated/removed -watch(configFilePath).on('all', updatePubsub); -watch(customImageFilePath).on('all', updatePubsub); - -export const createApolloExpressServer = async () => { - // Try and load the HTTP server - graphqlLogger.debug('Starting HTTP server'); - const app = express(); - const httpServer = http.createServer(app); - - app.use(json()); - - // Cors - app.use(cors()); - app.use(originMiddleware); - - // Add Unraid API version header - app.use(async (_req, res, next) => { - // Only get the machine ID on first request - // We do this to avoid using async in the main server function - if (!app.get('x-unraid-api-version')) { - app.set('x-unraid-api-version', API_VERSION); - } - - // Update header with unraid API version - res.set('x-unraid-api-version', app.get('x-unraid-api-version')); - - next(); - }); - - // Log only if the server actually binds to the port - httpServer.on('listening', () => { - logger.info('Server is up! %s', getServerAddress(httpServer)); - }); - - // graphql-ws - const graphqlWs = new WebSocketServer({ noServer: true }); - - // subscriptions-transport-ws - const subTransWs = new WebSocketServer({ - noServer: true, - }); - - // graphql-ws setup - const graphqlWsServer = useServer< - { 'x-api-key': string }, - { context: { user: any; websocketId: string } } - >( - { - schema, - onError(ctx, message, errors) { - logger.debug('%o %o %o', ctx, message, errors); - }, - async onConnect(ctx) { - logger.debug( - 'Connecting new client with params: %o', - ctx.connectionParams - ); - const params: unknown = ctx.connectionParams?.['x-api-key']; - if (params && typeof params === 'string') { - const apiKey = params; - const user = await apiKeyToUser(apiKey); - const websocketId = randomUUID(); - logger.debug('User is %o', user); - ctx.extra.context = { user, websocketId }; - return true; - } - return {}; - }, - context: (ctx) => { - return ctx.extra.context; - }, - }, - graphqlWs - ); - - // subscriptions-transport-ws setup - const subscriptionsTransportServer = SubscriptionServer.create( - { - // This is the `schema` we just created. - schema, - // These are imported from `graphql`. - execute, - subscribe, - // Ensure keep-alive packets are sent - keepAlive: 10_000, - // Providing `onConnect` is the `SubscriptionServer` equivalent to the - // `context` function in `ApolloServer`. Please [see the docs](https://github.com/apollographql/subscriptions-transport-ws#constructoroptions-socketoptions--socketserver) - // for more information on this hook. - async onConnect(connectionParams: { 'x-api-key': string }) { - const apiKey = connectionParams['x-api-key']; - const user = await apiKeyToUser(apiKey); - const websocketId = randomUUID(); - - graphqlLogger.addContext('websocketId', websocketId); - graphqlLogger.debug('%s connected', user.name); - graphqlLogger.removeContext('websocketId'); - - // Update ws connection count and other needed values - wsHasConnected(websocketId); - - return { - user, - websocketId, - }; - }, - async onDisconnect( - _, - websocketContext: { - initPromise: Promise< - | boolean - | { - user: { - name: string; - }; - websocketId: string; - } - >; - } - ) { - const context = await websocketContext.initPromise; - - // The websocket has disconnected before init event has resolved - // @see: https://github.com/apollographql/subscriptions-transport-ws/issues/349 - if (context === true || context === false) { - // This seems to also happen if a tab is left open and then a server starts up - // The tab hits the server over and over again without sending init - graphqlLogger.debug('unknown disconnected'); - return; - } - - const { user, websocketId } = context; - - graphqlLogger.addContext('websocketId', websocketId); - graphqlLogger.debug('%s disconnected.', user.name); - graphqlLogger.removeContext('websocketId'); - - // Update ws connection count and other needed values - wsHasDisconnected(websocketId); - }, - }, - subTransWs - ); - - const apolloServerPluginOnExit = { - async serverWillStart() { - return { - /** - * When the app exits this will be run. - */ - async drainServer() { - // Close all connections to subscriptions server - subscriptionsTransportServer.close(); - graphqlWsServer.dispose(); - }, - }; - }, - }; - - // Create graphql instance - const apolloServer = new ApolloServer({ - schema, - plugins: [ - apolloServerPluginOnExit, - ApolloServerPluginDrainHttpServer({ httpServer }), - ], - introspection: GRAPHQL_INTROSPECTION, - }); - - await apolloServer.start(); - - app.get('/graphql/api/logs', getLogs); - - app.get( - '/graphql/api/customizations/:type', - async (req: Request, res: Response) => { - // @TODO - Clean up this function - const apiKey = req.headers['x-api-key']; - if ( - apiKey && - typeof apiKey === 'string' && - (await apiKeyToUser(apiKey)).role !== 'guest' - ) { - if (req.params.type === 'banner') { - const path = await getBannerPathIfPresent(); - if (path) { - res.sendFile(path); - return; - } - } else if (req.params.type === 'case') { - const path = await getCasePathIfPresent(); - if (path) { - res.sendFile(path); - return; - } - } - - return res - .status(404) - .send('no customization of this type found'); - } - - return res.status(403).send('unauthorized'); - } - ); - - app.use( - '/graphql', - cors(), - json(), - expressMiddleware(apolloServer, { - context: async ({ req }) => { - // Normal Websocket connection - /* if (connection && Object.keys(connection.context).length >= 1) { - // Check connection for metadata - return { - ...connection.context, - }; - } */ - - // Normal HTTP connection - if ( - req && - req.headers['x-api-key'] && - typeof req.headers['x-api-key'] === 'string' - ) { - const apiKey = req.headers['x-api-key']; - const user = await apiKeyToUser(apiKey); - - return { - user, - }; - } - - throw new Error('Invalid API key'); - }, - }) - ); - - httpServer.on('upgrade', (req, socket, head) => { - // extract websocket subprotocol from header - const protocol = req.headers['sec-websocket-protocol']; - const protocols = Array.isArray(protocol) - ? protocol - : protocol?.split(',').map((p) => p.trim()); - - // decide which websocket server to use - const wss = - protocols?.includes(GRAPHQL_WS) && // subscriptions-transport-ws subprotocol - !protocols.includes(GRAPHQL_TRANSPORT_WS_PROTOCOL) // graphql-ws subprotocol - ? subTransWs - : // graphql-ws will welcome its own subprotocol and - // gracefully reject invalid ones. if the client supports - // both transports, graphql-ws will prevail - graphqlWs; - wss.handleUpgrade(req, socket, head, (ws) => { - wss.emit('connection', ws, req); - }); - }); - - // List all endpoints at start of server - app.get('/', (_, res: Response) => res.status(200).send('OK')); - - app.post('/verify', async (req, res) => { - try { - // Check two-factor token is valid - verifyTwoFactorToken(req.body?.username, req.body?.token); - - // Success - logger.debug('2FA token valid, allowing login.'); - - // Allow the user to pass - res.sendStatus(204); - return; - } catch (error: unknown) { - logger.addContext('error', error); - logger.error('Failed validating 2FA token.'); - logger.removeContext('error'); - - // User failed verification - res.status(401); - res.send((error as Error).message); - } - }); - - // Handle errors by logging them and returning a 500. - app.use( - ( - error: Error & { stackTrace?: string; status?: number }, - _, - res: Response, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - __ - ) => { - // Don't log CORS errors - if (error.message.includes('CORS')) return; - - logger.error(error); - - if (error.stack) { - error.stackTrace = error.stack; - } - - res.status(error.status ?? 500).send(error); - } - ); - - httpServer.listen(PORT); - return apolloServer; -}; diff --git a/api/src/store/actions/setup-remote-access.ts b/api/src/store/actions/setup-remote-access.ts new file mode 100644 index 000000000..c6c64db62 --- /dev/null +++ b/api/src/store/actions/setup-remote-access.ts @@ -0,0 +1,62 @@ +import { + type SetupRemoteAccessInput, + WAN_ACCESS_TYPE, + WAN_FORWARD_TYPE, +} from '@app/graphql/generated/api/types'; +import { DynamicRemoteAccessType } from '@app/remoteAccess/types'; +import { type AppDispatch, type RootState } from '@app/store/index'; +import { type MyServersConfig } from '@app/types/my-servers-config'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + +const getDynamicRemoteAccessType = ( + accessType: WAN_ACCESS_TYPE, + forwardType?: WAN_FORWARD_TYPE | undefined | null +): DynamicRemoteAccessType => { + // If access is disabled or always, DRA is disabled + if ( + accessType === WAN_ACCESS_TYPE.DISABLED || + accessType === WAN_ACCESS_TYPE.ALWAYS + ) { + return DynamicRemoteAccessType.DISABLED; + } + // if access is enabled and forward type is UPNP, DRA is UPNP, otherwise it is static + return forwardType === WAN_FORWARD_TYPE.UPNP + ? DynamicRemoteAccessType.UPNP + : DynamicRemoteAccessType.STATIC; +}; + +export const setupRemoteAccessThunk = createAsyncThunk< + Pick< + MyServersConfig['remote'], + 'wanaccess' | 'wanport' | 'dynamicRemoteAccessType' | 'upnpEnabled' + >, + SetupRemoteAccessInput, + { state: RootState; dispatch: AppDispatch } +>('config/setupRemoteAccess', async (payload) => { + + if (payload.accessType === WAN_ACCESS_TYPE.DISABLED) { + return { + wanaccess: 'no', + wanport: '', + dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED, + upnpEnabled: 'no', + } + } + + if (payload.forwardType === WAN_FORWARD_TYPE.STATIC && !payload.port) { + throw new Error('Missing port for WAN forward type STATIC'); + } + + return { + wanaccess: payload.accessType === WAN_ACCESS_TYPE.ALWAYS ? 'yes' : 'no', + wanport: + payload.forwardType === WAN_FORWARD_TYPE.STATIC + ? String(payload.port) + : '', + dynamicRemoteAccessType: getDynamicRemoteAccessType( + payload.accessType, + payload.forwardType + ), + upnpEnabled: payload.forwardType === WAN_FORWARD_TYPE.UPNP ? 'yes' : 'no', + }; +}); diff --git a/api/src/store/listeners/array-event-listener.ts b/api/src/store/listeners/array-event-listener.ts new file mode 100644 index 000000000..039f6827b --- /dev/null +++ b/api/src/store/listeners/array-event-listener.ts @@ -0,0 +1,51 @@ +import { logger } from '@app/core/log'; +import { getArrayData } from '@app/core/modules/array/get-array-data'; +import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; +import { startAppListening } from '@app/store/listeners/listener-middleware'; +import { loadSingleStateFile } from '@app/store/modules/emhttp'; +import { StateFileKey } from '@app/store/types'; +import { isAnyOf } from '@reduxjs/toolkit'; +import { isEqual } from 'lodash'; + +export const enableArrayEventListener = () => + startAppListening({ + matcher: isAnyOf(loadSingleStateFile.fulfilled), + async effect( + action, + { getState, getOriginalState, delay, unsubscribe, subscribe } + ) { + if (loadSingleStateFile.fulfilled.match(action)) { + if (action.meta.arg === StateFileKey.disks) { + unsubscribe(); + // getOriginalState must be called BEFORE the awaited delay in this function + const oldArrayData = getArrayData(getOriginalState); + await delay(5_000); + const array = getArrayData(getState); + if (!isEqual(oldArrayData, array)) { + pubsub.publish(PUBSUB_CHANNEL.ARRAY, { array }); + logger.debug( + { event: array }, + 'Array was updated, publishing event' + ); + } + + subscribe(); + } else if (action.meta.arg === StateFileKey.var) { + if ( + !isEqual( + getOriginalState().emhttp.var?.name, + getState().emhttp.var?.name + ) + ) { + await pubsub.publish(PUBSUB_CHANNEL.INFO, { + info: { + os: { + hostname: getState().emhttp.var?.name, + }, + }, + }); + } + } + } + }, + }); diff --git a/api/src/store/listeners/config-listener.ts b/api/src/store/listeners/config-listener.ts new file mode 100644 index 000000000..234b667c5 --- /dev/null +++ b/api/src/store/listeners/config-listener.ts @@ -0,0 +1,77 @@ +import { startAppListening } from '@app/store/listeners/listener-middleware'; +import { getDiff } from 'json-difference'; +import { isEqual } from 'lodash'; +import { logger } from '@app/core/log'; +import { + type ConfigType, + getWriteableConfig, +} from '@app/core/utils/files/config-file-normalizer'; +import { + loadConfigFile, + loginUser, + logoutUser, +} from '@app/store/modules/config'; +import { FileLoadStatus } from '@app/store/types'; +import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer'; +import { isFulfilled } from '@reduxjs/toolkit'; +import { environment } from '@app/environment'; +import { writeFileSync } from 'fs'; + +const actionIsLoginOrLogout = isFulfilled(logoutUser, loginUser); + +export const enableConfigFileListener = (mode: ConfigType) => () => + startAppListening({ + predicate(action, currentState, previousState) { + if (!environment.IS_MAIN_PROCESS) { + return false; + } + + if (currentState.config.status === FileLoadStatus.LOADED) { + const oldFlashConfig = previousState?.config.api.version + ? getWriteableConfig(previousState.config, mode) + : null; + const newFlashConfig = getWriteableConfig( + currentState.config, + mode + ); + + if ( + !isEqual(oldFlashConfig, newFlashConfig) && + action.type !== loadConfigFile.fulfilled.type && + action.type !== loadConfigFile.rejected.type + ) { + logger.trace( + { + diff: getDiff(oldFlashConfig ?? {}, newFlashConfig), + }, + `${mode} Config Changed!`, + 'Action:', + action.type + ); + + return true; + } + + if (actionIsLoginOrLogout(action) && mode === 'memory') { + logger.trace( + 'Logout / Login Action Encountered, writing memory config' + ); + return true; + } + } + + return false; + }, + async effect(_, { getState }) { + const { paths, config } = getState(); + const pathToWrite = + mode === 'flash' + ? paths['myservers-config'] + : paths['myservers-config-states']; + const writeableConfig = getWriteableConfig(config, mode); + const serializedConfig = + safelySerializeObjectToIni(writeableConfig); + logger.debug('Writing updated config to %s', pathToWrite); + writeFileSync(pathToWrite, serializedConfig); + }, + }); diff --git a/api/src/store/listeners/server-state-listener.ts b/api/src/store/listeners/server-state-listener.ts new file mode 100644 index 000000000..ae2388ed5 --- /dev/null +++ b/api/src/store/listeners/server-state-listener.ts @@ -0,0 +1,40 @@ +import { mothershipLogger } from '@app/core/log'; +import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; +import { getServers } from '@app/graphql/schema/utils'; +import { isAPIStateDataFullyLoaded } from '@app/mothership/graphql-client'; +import { startAppListening } from '@app/store/listeners/listener-middleware'; +import { FileLoadStatus } from '@app/store/types'; + +import isEqual from 'lodash/isEqual'; + +export const enableServerStateListener = () => + startAppListening({ + predicate: (_, currState, prevState) => { + if (currState.config.status === FileLoadStatus.LOADED && currState.emhttp.status === FileLoadStatus.LOADED ) { + if (prevState.minigraph.status !== currState.minigraph.status || !isEqual(prevState.config.remote, currState.config.remote)) { + return true; + } + } + return false; + }, + async effect(_, { getState }) { + if (isAPIStateDataFullyLoaded(getState())) { + const servers = getServers(getState); + mothershipLogger.trace( + 'Got local server state', + servers + ); + if (servers.length > 0) { + // Publish owner event + await pubsub.publish(PUBSUB_CHANNEL.OWNER, { + owner: servers[0].owner, + }); + + // Publish servers event + await pubsub.publish(PUBSUB_CHANNEL.SERVERS, { + servers: servers, + }); + } + } + }, + }); diff --git a/api/src/store/listeners/upnp-listener.ts b/api/src/store/listeners/upnp-listener.ts new file mode 100644 index 000000000..08bab99ab --- /dev/null +++ b/api/src/store/listeners/upnp-listener.ts @@ -0,0 +1,45 @@ +import { startAppListening } from '@app/store/listeners/listener-middleware'; +import { type RootState } from '@app/store'; +import { disableUpnp, enableUpnp } from '@app/store/modules/upnp'; +import { upnpLogger } from '@app/core/log'; +import { loadConfigFile } from '@app/store/modules/config'; +import { loadSingleStateFile, loadStateFiles } from '@app/store/modules/emhttp'; +import { FileLoadStatus } from '@app/store/types'; +import { isAnyOf } from '@reduxjs/toolkit'; +import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access'; + +const shouldUpnpBeEnabled = (state: RootState | null): boolean => { + if (state?.config.status !== FileLoadStatus.LOADED || state?.emhttp.status !== FileLoadStatus.LOADED) { + return false; + } + + const { useUpnp } = state.emhttp.var; + const { upnpEnabled, wanaccess } = state.config.remote; + + return useUpnp && upnpEnabled === 'yes' && wanaccess === 'yes'; +}; + +const isStateOrConfigUpdate = isAnyOf(loadConfigFile.fulfilled, loadSingleStateFile.fulfilled, loadStateFiles.fulfilled, setupRemoteAccessThunk.fulfilled); + +export const enableUpnpListener = () => startAppListening({ + predicate(action, currentState, previousState) { + // @TODO: One of our actions is incorrectly configured. Sometimes the action is an anonymous function. We need to fix this. + if ((isStateOrConfigUpdate(action) || !action?.type) + && (shouldUpnpBeEnabled(currentState) !== shouldUpnpBeEnabled(previousState))) { + return true; + } + + return false; + }, async effect(_, { getState, dispatch }) { + const state = getState(); + const { config: { remote: { wanport } }, emhttp: { var: { portssl } } } = getState(); + upnpLogger.info('UPNP Enabled: (%s) Wan Port: [%s]', shouldUpnpBeEnabled(state), wanport === '' ? 'Will Generate New WAN Port' : wanport); + + if (shouldUpnpBeEnabled(state)) { + await dispatch(enableUpnp({ wanport, portssl })); + } else { + await dispatch(disableUpnp()); + } + }, +}); + diff --git a/api/src/store/modules/config.ts b/api/src/store/modules/config.ts index 7216beef5..878f09937 100644 --- a/api/src/store/modules/config.ts +++ b/api/src/store/modules/config.ts @@ -21,9 +21,10 @@ import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-sta import { getWriteableConfig } from '@app/core/utils/files/config-file-normalizer'; import { writeFileSync } from 'fs'; import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer'; -import { pubsub } from '@app/core/pubsub'; +import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; import { DynamicRemoteAccessType } from '@app/remoteAccess/types'; import { isEqual } from 'lodash'; +import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access'; export type SliceState = { status: FileLoadStatus; @@ -79,7 +80,7 @@ export const loginUser = createAsyncThunk< username: userInfo.username, avatar: userInfo.avatar, }; - await pubsub.publish('owner', { owner }); + await pubsub.publish(PUBSUB_CHANNEL.OWNER, { owner }); return userInfo; }); @@ -92,7 +93,7 @@ export const logoutUser = createAsyncThunk< const { pubsub } = await import('@app/core/pubsub'); // Publish to servers endpoint - await pubsub.publish('servers', { + await pubsub.publish(PUBSUB_CHANNEL.SERVERS, { servers: [], }); @@ -102,7 +103,7 @@ export const logoutUser = createAsyncThunk< avatar: '', }; // Publish to owner endpoint - await pubsub.publish('owner', { owner }); + await pubsub.publish(PUBSUB_CHANNEL.OWNER, { owner }); }); /** @@ -325,6 +326,14 @@ export const config = createSlice({ builder.addCase(setGraphqlConnectionStatus, (state, action) => { state.connectionStatus.minigraph = action.payload.status; }); + + builder.addCase(setupRemoteAccessThunk.fulfilled, (state, action) => { + state.remote.wanaccess = action.payload.wanaccess; + state.remote.dynamicRemoteAccessType = + action.payload.dynamicRemoteAccessType; + state.remote.wanport = action.payload.wanport; + state.remote.upnpEnabled = action.payload.upnpEnabled; + }); }, }); const { actions, reducer } = config; diff --git a/api/src/store/store-sync.ts b/api/src/store/store-sync.ts index f3b28cef6..f869179ab 100644 --- a/api/src/store/store-sync.ts +++ b/api/src/store/store-sync.ts @@ -18,9 +18,6 @@ export const startStoreSync = async () => { const state = store.getState(); // Config dependent options, wait until config loads to execute if (state.config.status === FileLoadStatus.LOADED) { - // Update 2FA - // await sync2FA(); - // Update registration await syncRegistration(lastState); diff --git a/api/src/store/sync/info-apps-sync.ts b/api/src/store/sync/info-apps-sync.ts new file mode 100644 index 000000000..79203266c --- /dev/null +++ b/api/src/store/sync/info-apps-sync.ts @@ -0,0 +1,44 @@ +import { logger } from '@app/core/log'; +import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; +import { store } from '@app/store'; +import { DaemonConnectionStatus, type StoreSubscriptionHandler } from '@app/store/types'; +import { isEqual } from 'lodash'; + +type InfoAppsEvent = { + info: { + apps: { + installed: number | null; + running: number | null; + }; + }; +}; + +export const createInfoAppsEvent = (state: Parameters[0]): InfoAppsEvent | null => { + // Docker state isn't loaded + if (state === null || state.docker.status === DaemonConnectionStatus.DISCONNECTED) return null; + + return { + info: { + apps: { + installed: state?.docker.installed, + running: state?.docker.running, + }, + }, + }; +}; + +export const syncInfoApps: StoreSubscriptionHandler = async lastState => { + const lastEvent = createInfoAppsEvent(lastState); + const currentEvent = createInfoAppsEvent(store.getState()); + + // Skip if either event resolved to null + if (lastEvent === null || currentEvent === null) return; + + // Skip this if it's the same as the last one + if (isEqual(lastEvent, currentEvent)) return; + + logger.debug('Docker container count was updated, publishing event'); + + // Publish to graphql + await pubsub.publish(PUBSUB_CHANNEL.INFO, currentEvent); +}; diff --git a/api/src/store/sync/registration-sync.ts b/api/src/store/sync/registration-sync.ts new file mode 100644 index 000000000..d695ff852 --- /dev/null +++ b/api/src/store/sync/registration-sync.ts @@ -0,0 +1,62 @@ +import { logger } from '@app/core/log'; +import { PUBSUB_CHANNEL, pubsub } from '@app/core/pubsub'; +import { store } from '@app/store'; +import { FileLoadStatus, type StoreSubscriptionHandler } from '@app/store/types'; +import isEqual from 'lodash/isEqual'; + +export type RegistrationEvent = { + registration: { + guid: string; + type: string; + state: string; + keyFile: { + location: string; + contents: null; + }; + }; +}; + +export const createRegistrationEvent = (state: Parameters[0]): RegistrationEvent | null => { + // Var state isn't loaded + if (state === null || Object.keys(state.emhttp.var).length === 0) return null; + + const event = { + registration: { + guid: state.emhttp.var.regGuid, + type: state.emhttp.var.regTy.toUpperCase(), + state: state.emhttp.var.regState, + keyFile: { + location: state.emhttp.var.regFile, + contents: state.registration.keyFile, + }, + }, + }; + + return event; +}; + +export const syncRegistration: StoreSubscriptionHandler = async lastState => { + try { + // Skip until we have the key and emhttp states loaded + const { registration, emhttp } = store.getState(); + if (registration.status !== FileLoadStatus.LOADED) return; + if (emhttp.status !== FileLoadStatus.LOADED) return; + + const lastEvent = createRegistrationEvent(lastState); + const currentEvent = createRegistrationEvent(store.getState()); + + // Skip if either event resolved to null + if (lastEvent === null || currentEvent === null) return; + + // Skip this if it's the same as the last one + if (isEqual(lastEvent, currentEvent)) return; + + logger.debug('Registration was updated, publishing event'); + + // Publish to graphql + await pubsub.publish(PUBSUB_CHANNEL.REGISTRATION, currentEvent); + } catch (error: unknown) { + if (!(error instanceof Error)) throw new Error(`Failed publishing registration event with unknown error "${String(error)}"`); + logger.error('Failed publishing registration event with "%s"', error.message); + } +}; diff --git a/api/src/store/watch/docker-watch.ts b/api/src/store/watch/docker-watch.ts index 5721e4e1e..ab1dcdc77 100644 --- a/api/src/store/watch/docker-watch.ts +++ b/api/src/store/watch/docker-watch.ts @@ -52,9 +52,7 @@ export const setupDockerWatch = async (): Promise => { if (!watchedActions.includes(data.Action)) { return; } - dockerLogger.addContext('data', data); dockerLogger.debug(`[${data.from}] ${data.Type}->${data.Action}`); - dockerLogger.removeContext('data'); await debouncedContainerCacheUpdate(); } ); diff --git a/api/src/unraid-api/app/app.module.ts b/api/src/unraid-api/app/app.module.ts new file mode 100644 index 000000000..9daebd24c --- /dev/null +++ b/api/src/unraid-api/app/app.module.ts @@ -0,0 +1,40 @@ +import { apiLogger } from '@app/core/log'; +import { setupPermissions } from '@app/core/permissions'; +import { GraphqlAuthGuard } from '@app/unraid-api/auth/auth.guard'; +import { AuthModule } from '@app/unraid-api/auth/auth.module'; +import { GraphModule } from '@app/unraid-api/graph/graph.module'; +import { RestModule } from '@app/unraid-api/rest/rest.module'; +import { Module } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { ACGuard, AccessControlModule } from 'nest-access-control'; +import { LoggerModule } from 'nestjs-pino'; + +@Module({ + imports: [ + LoggerModule.forRoot({ + pinoHttp: { + logger: apiLogger, + autoLogging: false + }, + }), + AccessControlModule.forRoles(setupPermissions()), + AuthModule, + GraphModule, + RestModule, + ], + controllers: [], + providers: [ + { + provide: 'APP_GUARD', + useFactory: () => + new GraphqlAuthGuard( + new Reflector(), + ), + }, + { + provide: 'APP_GUARD', + useClass: ACGuard, + }, + ], +}) +export class AppModule {} diff --git a/api/src/unraid-api/auth/auth.guard.ts b/api/src/unraid-api/auth/auth.guard.ts new file mode 100644 index 000000000..ade1b1774 --- /dev/null +++ b/api/src/unraid-api/auth/auth.guard.ts @@ -0,0 +1,80 @@ +import { apiLogger } from '@app/core/log'; +import { BYPASS_PERMISSION_CHECKS } from '@app/environment'; +import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy'; +import { IS_PUBLIC_KEY } from '@app/unraid-api/auth/public.decorator'; +import { + type ExecutionContext, + Injectable, + type CanActivate, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { GqlExecutionContext, type GqlContextType } from '@nestjs/graphql'; +import { AuthGuard } from '@nestjs/passport'; +import { type Observable } from 'rxjs'; + +@Injectable() +export class GraphqlAuthGuard + extends AuthGuard([ServerHeaderStrategy.key]) + implements CanActivate +{ + constructor(private readonly reflector: Reflector) { + super(); + } + + handleRequest(err, user: UserAccount | null, info, context) { + if (!user) { + if (context) { + const ctx = GqlExecutionContext.create(context); + const fullContext = ctx.getContext(); + apiLogger.error( + 'No user found in request - connection params: %o', + fullContext.connectionParams ?? {} + ); + } + + throw new UnauthorizedException('User not found'); + } + + return user; + } + + /** + * Helper to determine if this middleware should run activate. If the route is marked as public, then it will not run. + * @param context + * @returns + */ + canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { + const isPublic = this.reflector.getAllAndOverride( + IS_PUBLIC_KEY, + [context.getHandler(), context.getClass()] + ); + + if (isPublic) { + return true; + } + + return super.canActivate(context); + } + + getRequest(context: ExecutionContext) { + if (context.getType() === 'graphql') { + // headers are either inside context.getContext().connectionParams or in the request, which is in context.getContext().req (see context.ts) + const ctx = GqlExecutionContext.create(context); + const fullContext = ctx.getContext(); + const request = fullContext.req ?? {}; + const additionalConnectionParamHeaders = + fullContext.connectionParams ?? {}; + request.headers = { + ...(request.headers ?? {}), + ...additionalConnectionParamHeaders, + }; + + return request; + } else { + return context.switchToHttp().getRequest(); + } + } +} diff --git a/api/src/unraid-api/auth/auth.module.ts b/api/src/unraid-api/auth/auth.module.ts new file mode 100644 index 000000000..6e5ee3ba4 --- /dev/null +++ b/api/src/unraid-api/auth/auth.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; +import { UsersModule } from '@app/unraid-api/users/users.module'; +import { PassportModule } from '@nestjs/passport'; +import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy'; + +@Module({ + imports: [UsersModule, PassportModule], + providers: [AuthService, ServerHeaderStrategy], +}) +export class AuthModule {} diff --git a/api/src/unraid-api/auth/auth.service.spec.ts b/api/src/unraid-api/auth/auth.service.spec.ts new file mode 100644 index 000000000..800ab6626 --- /dev/null +++ b/api/src/unraid-api/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/auth/auth.service.ts b/api/src/unraid-api/auth/auth.service.ts new file mode 100644 index 000000000..72a3a4622 --- /dev/null +++ b/api/src/unraid-api/auth/auth.service.ts @@ -0,0 +1,17 @@ +import { type UserAccount } from '@app/graphql/generated/api/types'; +import { UsersService } from '@app/unraid-api/users/users.service'; +import { Injectable, UnauthorizedException } from '@nestjs/common'; + +@Injectable() +export class AuthService { + constructor(private usersService: UsersService) {} + + async validateUser(apiKey: string): Promise { + + const user = this.usersService.findOne(apiKey); + if (user) { + return user; + } + throw new UnauthorizedException('Invalid API key'); + } +} diff --git a/api/src/unraid-api/auth/header.strategy.ts b/api/src/unraid-api/auth/header.strategy.ts new file mode 100644 index 000000000..e425a57b0 --- /dev/null +++ b/api/src/unraid-api/auth/header.strategy.ts @@ -0,0 +1,26 @@ +import { PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-http-header-strategy'; +import { Injectable, Logger } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Injectable() +export class ServerHeaderStrategy extends PassportStrategy( + Strategy, + 'server-http-header' +) { + static key = 'server-http-header'; + private readonly logger = new Logger(ServerHeaderStrategy.name); + + constructor(private readonly authService: AuthService) { + super({ header: 'x-api-key', passReqToCallback: false }); + } + + public validate = async ( + apiKey: string + ): Promise => { + this.logger.debug('Validating API key'); + const user = await this.authService.validateUser(apiKey); + + return user; + }; +} diff --git a/api/src/unraid-api/auth/public.decorator.ts b/api/src/unraid-api/auth/public.decorator.ts new file mode 100644 index 000000000..95b7a2981 --- /dev/null +++ b/api/src/unraid-api/auth/public.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'IS_PUBLIC'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); diff --git a/api/src/unraid-api/exceptions/graphql-exceptions.filter.ts b/api/src/unraid-api/exceptions/graphql-exceptions.filter.ts new file mode 100644 index 000000000..d1f17f905 --- /dev/null +++ b/api/src/unraid-api/exceptions/graphql-exceptions.filter.ts @@ -0,0 +1,28 @@ +import { + Catch, + type ArgumentsHost, + type ExceptionFilter, +} from '@nestjs/common'; +import { GraphQLError } from 'graphql'; +import { type FastifyReply } from 'fastify'; + +@Catch(GraphQLError) +export class GraphQLExceptionsFilter + implements ExceptionFilter +{ + catch(exception: T, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response: FastifyReply = ctx.getResponse(); + + response.code(200).send({ + data: null, + errors: [ + { + message: exception.message, + locations: exception.locations, + path: exception.path, + }, + ], + }); + } +} diff --git a/api/src/unraid-api/exceptions/http-exceptions.filter.ts b/api/src/unraid-api/exceptions/http-exceptions.filter.ts new file mode 100644 index 000000000..13a4f7b8c --- /dev/null +++ b/api/src/unraid-api/exceptions/http-exceptions.filter.ts @@ -0,0 +1,40 @@ +import { + Catch, + HttpException, + type ArgumentsHost, + type ExceptionFilter, + Logger, +} from '@nestjs/common'; +import { type FastifyReply } from 'fastify'; + +@Catch(HttpException) +export class HttpExceptionFilter + implements ExceptionFilter +{ + protected logger = new Logger('HttpExceptionFilter'); + + catch(exception: T, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response: FastifyReply = ctx.getResponse(); + // if response is empty object, rethrow + if ( + Object.keys(response).length === 0 || + !response.status || + typeof response.status !== 'function' + ) { + throw exception; + } + + const status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + const error = + typeof exceptionResponse === 'string' + ? { message: exceptionResponse } + : (exceptionResponse as object); + + this.logger.error(`HttpException: ${JSON.stringify(error)}`); + return response + .status(status) + .send({ error, timestamp: new Date().toISOString() }); + } +} diff --git a/api/src/unraid-api/graph/graph.module.ts b/api/src/unraid-api/graph/graph.module.ts new file mode 100644 index 000000000..84c8b121b --- /dev/null +++ b/api/src/unraid-api/graph/graph.module.ts @@ -0,0 +1,51 @@ +import { + DateTimeResolver, + JSONResolver, + PortResolver, + UUIDResolver, +} from 'graphql-scalars'; +import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long'; +import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin/landingPage/default'; +import { ApolloDriver, type ApolloDriverConfig } from '@nestjs/apollo'; +import { Module } from '@nestjs/common'; +import { GraphQLModule } from '@nestjs/graphql'; +import { ResolversModule } from './resolvers/resolvers.module'; +import { GRAPHQL_INTROSPECTION } from '@app/environment'; +import { typeDefs } from '@app/graphql/schema/index'; +import { print } from 'graphql'; + +@Module({ + imports: [ + ResolversModule, + GraphQLModule.forRoot({ + driver: ApolloDriver, + introspection: GRAPHQL_INTROSPECTION ? true : false, + context: ({ req, connectionParams, extra }) => ({ + req, + connectionParams, + extra, + }), + playground: false, + plugins: GRAPHQL_INTROSPECTION + ? [ApolloServerPluginLandingPageLocalDefault()] + : [], + subscriptions: { + 'graphql-ws': { + path: '/graphql', + } + }, + path: '/graphql', + typeDefs: print(typeDefs), + resolvers: { + JSON: JSONResolver, + Long: GraphQLLong, + UUID: UUIDResolver, + DateTime: DateTimeResolver, + Port: PortResolver, + }, + // schema: schema + }), + ], + providers: [], +}) +export class GraphModule {} diff --git a/api/src/unraid-api/graph/resolvers/array/array.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/array/array.resolver.spec.ts new file mode 100644 index 000000000..d7c8bb3da --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/array.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ArrayResolver } from './array.resolver'; + +describe('ArrayResolver', () => { + let resolver: ArrayResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ArrayResolver], + }).compile(); + + resolver = module.get(ArrayResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/array/array.resolver.ts b/api/src/unraid-api/graph/resolvers/array/array.resolver.ts new file mode 100644 index 000000000..35286117b --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/array/array.resolver.ts @@ -0,0 +1,28 @@ +import { getArrayData } from '@app/core/modules/array/get-array-data'; +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; +import { store } from '@app/store/index'; +import { Resolver, Query, Subscription } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver('Array') +export class ArrayResolver { + @Query() + @UseRoles({ + resource: 'array', + action: 'read', + possession: 'own' + }) + public async array() { + return getArrayData(store.getState); + } + + @Subscription('array') + @UseRoles({ + resource: 'array', + action: 'read', + possession: 'own' + }) + public async arraySubscription() { + return createSubscription(PUBSUB_CHANNEL.ARRAY); + } +} diff --git a/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts new file mode 100644 index 000000000..166e7bc2d --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CloudResolver } from './cloud.resolver'; + +describe('CloudResolver', () => { + let resolver: CloudResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CloudResolver], + }).compile(); + + resolver = module.get(CloudResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts b/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts new file mode 100644 index 000000000..cdb4b3652 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/cloud/cloud.resolver.ts @@ -0,0 +1,88 @@ +import { getAllowedOrigins } from '@app/common/allowed-origins'; +import { + type ConnectSignInInput, + type SetupRemoteAccessInput, +} from '@app/graphql/generated/api/types'; +import type { Cloud } from '@app/graphql/generated/api/types'; +import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in'; +import { checkApi } from '@app/graphql/resolvers/query/cloud/check-api'; +import { checkCloud } from '@app/graphql/resolvers/query/cloud/check-cloud'; +import { checkMinigraphql } from '@app/graphql/resolvers/query/cloud/check-minigraphql'; +import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access'; +import { store } from '@app/store/index'; +import { logoutUser } from '@app/store/modules/config'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver('Cloud') +export class CloudResolver { + @Query() + @UseRoles({ + resource: 'cloud', + action: 'read', + possession: 'own', + }) + public async cloud(): Promise { + const minigraphql = checkMinigraphql(); + const [apiKey, cloud] = await Promise.all([checkApi(), checkCloud()]); + + return { + relay: { + // Left in for UPC backwards compat. + error: undefined, + status: 'connected', + timeout: null, + }, + apiKey, + minigraphql, + cloud, + allowedOrigins: getAllowedOrigins(), + error: + `${apiKey.error ? `API KEY: ${apiKey.error}` : ''}${ + cloud.error ? `NETWORK: ${cloud.error}` : '' + }${minigraphql.error ? `CLOUD: ${minigraphql.error}` : ''}` || + null, + }; + } + + @Mutation() + @UseRoles({ + resource: 'connect', + action: 'update', + possession: 'own', + }) + public async connectSignIn( + @Args('input') input: ConnectSignInInput + ): Promise { + /** + * @todo Move to service + */ + return connectSignIn(input); + } + + @Mutation() + @UseRoles({ + resource: 'connect', + action: 'update', + possession: 'own', + }) + public async connectSignOut() { + await store.dispatch( + logoutUser({ reason: 'Manual Sign Out Using API' }) + ); + return true; + } + + @Mutation() + @UseRoles({ + resource: 'connect', + action: 'update', + possession: 'own', + }) + public async setupRemoteAccess( + @Args('input') input: SetupRemoteAccessInput + ): Promise { + await store.dispatch(setupRemoteAccessThunk(input)).unwrap(); + return true; + } +} diff --git a/api/src/unraid-api/graph/resolvers/config/config.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/config/config.resolver.spec.ts new file mode 100644 index 000000000..24eb3364d --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/config/config.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigResolver } from './config.resolver'; + +describe('ConfigResolver', () => { + let resolver: ConfigResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ConfigResolver], + }).compile(); + + resolver = module.get(ConfigResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/config/config.resolver.ts b/api/src/unraid-api/graph/resolvers/config/config.resolver.ts new file mode 100644 index 000000000..cc7ff92f2 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/config/config.resolver.ts @@ -0,0 +1,37 @@ +import { getAllowedOrigins } from '@app/common/allowed-origins'; +import { type AllowedOriginInput, ConfigErrorState } from '@app/graphql/generated/api/types'; +import { getters, store } from '@app/store/index'; +import { updateAllowedOrigins } from '@app/store/modules/config'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver('Config') +export class ConfigResolver { + @Query() + @UseRoles({ + resource: 'config', + action: 'read', + possession: 'any', + }) + public async config() { + const emhttp = getters.emhttp(); + return { + valid: emhttp.var.configValid, + error: emhttp.var.configValid + ? null + : ConfigErrorState[emhttp.var.configState] ?? + ConfigErrorState.UNKNOWN_ERROR, + }; + } + + @Mutation('setAdditionalAllowedOrigins') + @UseRoles({ + resource: 'config', + action: 'update', + possession: 'own', + }) + public async setAdditionalAllowedOrigins(@Args('input') input: AllowedOriginInput) { + await store.dispatch(updateAllowedOrigins(input.origins)); + return getAllowedOrigins(); + } +} diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts new file mode 100644 index 000000000..628fdc85b --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DisksResolver } from './disks.resolver'; + +describe('DisksResolver', () => { + let resolver: DisksResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DisksResolver], + }).compile(); + + resolver = module.get(DisksResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts new file mode 100644 index 000000000..f06f5b007 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/disks/disks.resolver.ts @@ -0,0 +1,19 @@ +import { getDisks } from '@app/core/modules/get-disks'; +import { Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver('Disks') +export class DisksResolver { + @Query() + @UseRoles({ + resource: 'disks', + action: 'read', + possession: 'own', + }) + public async disks() { + const disks = await getDisks({ + temperature: true, + }); + return disks; + } +} diff --git a/api/src/unraid-api/graph/resolvers/display/display.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/display/display.resolver.spec.ts new file mode 100644 index 000000000..ff5f021b5 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/display/display.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DisplayResolver } from './display.resolver'; + +describe('DisplayResolver', () => { + let resolver: DisplayResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DisplayResolver], + }).compile(); + + resolver = module.get(DisplayResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/display/display.resolver.ts b/api/src/unraid-api/graph/resolvers/display/display.resolver.ts new file mode 100644 index 000000000..9f306ad56 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/display/display.resolver.ts @@ -0,0 +1,114 @@ +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; +import { type Display } from '@app/graphql/generated/api/types'; +import { getters } from '@app/store/index'; +import { Query, Resolver, Subscription } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; +import { existsSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +const states = { + // Success + custom: { + url: '', + icon: 'custom', + error: '', + base64: '', + }, + default: { + url: '', + icon: 'default', + error: '', + base64: '', + }, + + // Errors + couldNotReadConfigFile: { + url: '', + icon: 'custom', + error: 'could-not-read-config-file', + base64: '', + }, + couldNotReadImage: { + url: '', + icon: 'custom', + error: 'could-not-read-image', + base64: '', + }, + imageMissing: { + url: '', + icon: 'custom', + error: 'image-missing', + base64: '', + }, + imageTooBig: { + url: '', + icon: 'custom', + error: 'image-too-big', + base64: '', + }, + imageCorrupt: { + url: '', + icon: 'custom', + error: 'image-corrupt', + base64: '', + }, +}; + +@Resolver() +export class DisplayResolver { + @Query() + @UseRoles({ + resource: 'display', + action: 'read', + possession: 'any', + }) + public async display(): Promise { + /** + * This is deprecated, remove it eventually + */ + const dynamixBasePath = getters.paths()['dynamix-base']; + const configFilePath = join(dynamixBasePath, 'case-model.cfg'); + + // If the config file doesn't exist then it's a new OS install + // Default to "default" + if (!existsSync(configFilePath)) { + return { case: states.default }; + } + + // Attempt to get case from file + const serverCase = await readFile(configFilePath) + .then((buffer) => buffer.toString().split('\n')[0]) + .catch(() => 'error_reading_config_file'); + + // Config file can't be read, maybe a permissions issue? + if (serverCase === 'error_reading_config_file') { + return { case: states.couldNotReadConfigFile }; + } + + // Blank cfg file? + if (serverCase.trim().length === 0) { + return { + case: states.default, + }; + } + + // Non-custom icon + return { + case: { + ...states.default, + icon: serverCase, + }, + }; + } + + @Subscription('display') + @UseRoles({ + resource: 'display', + action: 'read', + possession: 'any', + }) + public async displaySubscription() { + return createSubscription(PUBSUB_CHANNEL.DISPLAY); + } +} diff --git a/api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.spec.ts new file mode 100644 index 000000000..c9db2cd83 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DockerContainersResolver } from './docker-containers.resolver'; + +describe('DockerContainersResolver', () => { + let resolver: DockerContainersResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [DockerContainersResolver], + }).compile(); + + resolver = module.get(DockerContainersResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.ts b/api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.ts new file mode 100644 index 000000000..c0946e09f --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/docker-containers/docker-containers.resolver.ts @@ -0,0 +1,16 @@ +import { getDockerContainers } from '@app/core/modules/index'; +import { Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver('DockerContainers') +export class DockerContainersResolver { + @Query() + @UseRoles({ + resource: 'docker/container', + action: 'read', + possession: 'any', + }) + public async dockerContainers() { + return getDockerContainers(); + } +} diff --git a/api/src/unraid-api/graph/resolvers/flash/flash.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.spec.ts new file mode 100644 index 000000000..41714cc49 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { FlashResolver } from './flash.resolver'; + +describe('FlashResolver', () => { + let resolver: FlashResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [FlashResolver], + }).compile(); + + resolver = module.get(FlashResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts new file mode 100644 index 000000000..c8f864f64 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/flash/flash.resolver.ts @@ -0,0 +1,22 @@ +import { getters } from '@app/store/index'; +import { Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver() +export class FlashResolver { + @Query() + @UseRoles({ + resource: 'flash', + action: 'read', + possession: 'own', + }) + public async flash() { + const emhttp = getters.emhttp(); + + return { + guid: emhttp.var.flashGuid, + vendor: emhttp.var.flashVendor, + product: emhttp.var.flashProduct, + }; + } +} diff --git a/api/src/unraid-api/graph/resolvers/info/info.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/info/info.resolver.spec.ts new file mode 100644 index 000000000..9675825e9 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/info.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { InfoResolver } from './info.resolver'; + +describe('InfoResolver', () => { + let resolver: InfoResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [InfoResolver], + }).compile(); + + resolver = module.get(InfoResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/info/info.resolver.ts b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts new file mode 100644 index 000000000..f1e7e8b57 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/info/info.resolver.ts @@ -0,0 +1,86 @@ +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; +import { getMachineId } from '@app/core/utils/misc/get-machine-id'; +import { + generateApps, + generateCpu, + generateDevices, + generateDisplay, + generateMemory, + generateOs, + generateVersions, +} from '@app/graphql/resolvers/query/info'; +import { Query, ResolveField, Resolver, Subscription } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; +import { baseboard, system } from 'systeminformation'; + +@Resolver('Info') +export class InfoResolver { + @Query() + @UseRoles({ + resource: 'info', + action: 'read', + possession: 'any', + }) + public async info() { + return {}; + } + + @ResolveField('apps') + public async apps() { + return generateApps(); + } + + @ResolveField('baseboard') + public async baseboard() { + return baseboard(); + } + + @ResolveField('cpu') + public async cpu() { + return generateCpu(); + } + + @ResolveField('devices') + public async devices() { + return generateDevices(); + } + + @ResolveField('display') + public async display() { + return generateDisplay(); + } + + @ResolveField('machineId') + public async machineId() { + return getMachineId(); + } + + @ResolveField('memory') + public async memory() { + return generateMemory(); + } + + @ResolveField('os') + public async os() { + return generateOs(); + } + + @ResolveField('system') + public async system() { + return system(); + } + @ResolveField('versions') + public async versions() { + return generateVersions(); + } + + @Subscription('info') + @UseRoles({ + resource: 'info', + action: 'read', + possession: 'any', + }) + public async infoSubscription() { + return createSubscription(PUBSUB_CHANNEL.INFO); + } +} diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.spec.ts new file mode 100644 index 000000000..577290b8d --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotificationsResolver } from './notifications.resolver'; + +describe('NotificationsResolver', () => { + let resolver: NotificationsResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [NotificationsResolver], + }).compile(); + + resolver = module.get(NotificationsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts new file mode 100644 index 000000000..a338b23fc --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/notifications/notifications.resolver.ts @@ -0,0 +1,99 @@ +import { type NotificationFilter } from '@app/graphql/generated/api/types'; +import { getters } from '@app/store/index'; +import { Query, Resolver, Args, Mutation, Subscription } from '@nestjs/graphql'; +import { GraphQLError } from 'graphql'; +import { UseRoles } from 'nest-access-control'; +import { Logger } from '@nestjs/common'; +import { type NotificationInput } from '@app/graphql/generated/client/graphql'; +import { GraphQLClient } from '@app/mothership/graphql-client'; +import { SEND_NOTIFICATION_MUTATION } from '@app/graphql/mothership/mutations'; +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; + +@Resolver() +export class NotificationsResolver { + private logger = new Logger(NotificationsResolver.name); + @Query() + @UseRoles({ + resource: 'notifications', + action: 'read', + possession: 'any', + }) + public async notifications( + @Args('filter') + { limit, importance, type, offset }: NotificationFilter + ) { + if (limit > 50) { + throw new GraphQLError('Limit must be less than 50'); + } + return Object.values(getters.notifications().notifications) + .filter((notification) => { + if (importance && importance !== notification.importance) { + return false; + } + if (type && type !== notification.type) { + return false; + } + + return true; + }) + .sort( + (a, b) => + new Date(b.timestamp ?? 0).getTime() - + new Date(a.timestamp ?? 0).getTime() + ) + .slice(offset, limit + offset); + } + + @Mutation('sendNotification') + @UseRoles({ + resource: 'notifications', + action: 'create', + possession: 'own', + }) + public async sendNotification( + @Args('notification') notification: NotificationInput + ) { + this.logger.log('Sending notification', JSON.stringify(notification)); + const promise = new Promise((res, rej) => { + setTimeout(async () => { + rej(new GraphQLError('Sending Notification Timeout')); + }, 5_000); + const client = GraphQLClient.getInstance(); + // If there's no mothership connection then bail + if (!client) { + this.logger.error('Mothership is not working'); + throw new GraphQLError('Mothership is down'); + } + client + .query({ + query: SEND_NOTIFICATION_MUTATION, + variables: { + notification: notification, + apiKey: getters.config().remote.apikey, + }, + }) + .then((result) => { + this.logger.debug( + 'Query Result from Notifications.ts', + result + ); + res(notification); + }) + .catch((err) => { + rej(err); + }); + }); + + return promise; + } + + @Subscription('notificationAdded') + @UseRoles({ + resource: 'notifications', + action: 'read', + possession: 'any', + }) + async notificationAdded() { + return createSubscription(PUBSUB_CHANNEL.NOTIFICATION); + } +} diff --git a/api/src/unraid-api/graph/resolvers/online/online.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/online/online.resolver.spec.ts new file mode 100644 index 000000000..1242952d0 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/online/online.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { OnlineResolver } from './online.resolver'; + +describe('OnlineResolver', () => { + let resolver: OnlineResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [OnlineResolver], + }).compile(); + + resolver = module.get(OnlineResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/online/online.resolver.ts b/api/src/unraid-api/graph/resolvers/online/online.resolver.ts new file mode 100644 index 000000000..14538bac5 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/online/online.resolver.ts @@ -0,0 +1,15 @@ +import { Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver() +export class OnlineResolver { + @Query() + @UseRoles({ + resource: 'online', + action: 'read', + possession: 'any', + }) + public async online() { + return true; + } +} diff --git a/api/src/unraid-api/graph/resolvers/owner/owner.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/owner/owner.resolver.spec.ts new file mode 100644 index 000000000..2f7362304 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/owner/owner.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { OwnerResolver } from './owner.resolver'; + +describe('OwnerResolver', () => { + let resolver: OwnerResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [OwnerResolver], + }).compile(); + + resolver = module.get(OwnerResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts b/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts new file mode 100644 index 000000000..87a90c928 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/owner/owner.resolver.ts @@ -0,0 +1,40 @@ +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; +import { getters } from '@app/store/index'; +import { Query, Resolver, Subscription } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver() +export class OwnerResolver { + @Query() + @UseRoles({ + resource: 'owner', + action: 'read', + possession: 'own', + }) + public async owner() { + const { remote } = getters.config(); + + if (!remote.username) { + return { + username: 'root', + avatar: '', + url: '', + }; + } + + return { + username: remote.username, + avatar: remote.avatar, + }; + } + + @Subscription('owner') + @UseRoles({ + resource: 'owner', + action: 'read', + possession: 'own', + }) + public ownerSubscription() { + return createSubscription(PUBSUB_CHANNEL.OWNER); + } +} diff --git a/api/src/unraid-api/graph/resolvers/registration/registration.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.spec.ts new file mode 100644 index 000000000..6c105c659 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RegistrationResolver } from './registration.resolver'; + +describe('RegistrationResolver', () => { + let resolver: RegistrationResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RegistrationResolver], + }).compile(); + + resolver = module.get(RegistrationResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts new file mode 100644 index 000000000..9e9514554 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/registration/registration.resolver.ts @@ -0,0 +1,54 @@ +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; +import { getKeyFile } from '@app/core/utils/misc/get-key-file'; +import { + registrationType, + type Registration, +} from '@app/graphql/generated/api/types'; +import { getters } from '@app/store/index'; +import { FileLoadStatus } from '@app/store/types'; +import { Query, Resolver, Subscription } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver() +export class RegistrationResolver { + @Query() + @UseRoles({ + resource: 'registration', + action: 'read', + possession: 'any', + }) + public async registration() { + const emhttp = getters.emhttp(); + if (emhttp.status !== FileLoadStatus.LOADED || !emhttp.var?.regTy) { + return null; + } + + const isTrial = emhttp.var.regTy === registrationType.TRIAL; + const isExpired = emhttp.var.regTy.includes('expired'); + + const registration: Registration = { + guid: emhttp.var.regGuid, + type: emhttp.var.regTy, + state: emhttp.var.regState, + // Based on https://github.com/unraid/dynamix.unraid.net/blob/c565217fa8b2acf23943dc5c22a12d526cdf70a1/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php#L64 + expiration: ( + 1_000 * (isTrial || isExpired ? Number(emhttp.var.regTm2) : 0) + ).toString(), + keyFile: { + location: emhttp.var.regFile, + contents: await getKeyFile(), + }, + }; + return registration; + } + + @Subscription('registration') + @UseRoles({ + resource: 'registration', + action: 'read', + possession: 'any', + }) + public registrationSubscription() { + return createSubscription(PUBSUB_CHANNEL.REGISTRATION); + } +} diff --git a/api/src/unraid-api/graph/resolvers/resolvers.module.ts b/api/src/unraid-api/graph/resolvers/resolvers.module.ts new file mode 100644 index 000000000..87a134a14 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/resolvers.module.ts @@ -0,0 +1,37 @@ +import { ArrayResolver } from '@app/unraid-api/graph/resolvers/array/array.resolver'; +import { Module } from '@nestjs/common'; +import { CloudResolver } from './cloud/cloud.resolver'; +import { ConfigResolver } from './config/config.resolver'; +import { DisksResolver } from './disks/disks.resolver'; +import { DockerContainersResolver } from './docker-containers/docker-containers.resolver'; +import { DisplayResolver } from './display/display.resolver'; +import { NotificationsResolver } from './notifications/notifications.resolver'; +import { OnlineResolver } from './online/online.resolver'; +import { InfoResolver } from './info/info.resolver'; +import { VmsResolver } from './vms/vms.resolver'; +import { FlashResolver } from './flash/flash.resolver'; +import { OwnerResolver } from './owner/owner.resolver'; +import { RegistrationResolver } from './registration/registration.resolver'; +import { ServerResolver } from './servers/server.resolver'; +import { VarsResolver } from './vars/vars.resolver'; + +@Module({ + providers: [ + ArrayResolver, + CloudResolver, + ConfigResolver, + DisksResolver, + DockerContainersResolver, + DisplayResolver, + NotificationsResolver, + OnlineResolver, + InfoResolver, + VmsResolver, + FlashResolver, + OwnerResolver, + RegistrationResolver, + ServerResolver, + VarsResolver, + ], +}) +export class ResolversModule {} diff --git a/api/src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts new file mode 100644 index 000000000..22edb0e89 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/servers/server.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, type TestingModule } from '@nestjs/testing'; +import { ServerResolver } from './server.resolver'; + +describe('ServersResolver', () => { + let resolver: ServerResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ServerResolver], + }).compile(); + + resolver = module.get(ServerResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts b/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts new file mode 100644 index 000000000..ef8fcbb05 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/servers/server.resolver.ts @@ -0,0 +1,39 @@ +import { Query, Resolver, Subscription } from '@nestjs/graphql'; +import { getLocalServer } from '@app/graphql/schema/utils'; +import { type Server } from '@app/graphql/generated/client/graphql'; +import { UseRoles } from 'nest-access-control'; +import { PUBSUB_CHANNEL, createSubscription } from '@app/core/pubsub'; + +@Resolver() +export class ServerResolver { + @Query() + @UseRoles({ + resource: 'server', + action: 'read', + possession: 'any', + }) + public async server(): Promise { + return getLocalServer()[0]; + } + + @Resolver('servers') + @Query() + @UseRoles({ + resource: 'server', + action: 'read', + possession: 'any', + }) + public async servers(): Promise { + return getLocalServer(); + } + + @Subscription('server') + @UseRoles({ + resource: 'server', + action: 'read', + possession: 'any', + }) + public async serversSubscription() { + return createSubscription(PUBSUB_CHANNEL.SERVERS); + } +} diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts new file mode 100644 index 000000000..7d9ac9e1f --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { VarsResolver } from './vars.resolver'; + +describe('VarsResolver', () => { + let resolver: VarsResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [VarsResolver], + }).compile(); + + resolver = module.get(VarsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts new file mode 100644 index 000000000..d8499f7c5 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts @@ -0,0 +1,16 @@ +import { getters } from '@app/store/index'; +import { Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver() +export class VarsResolver { + @Query() + @UseRoles({ + resource: 'vars', + action: 'read', + possession: 'any', + }) + public async vars() { + return getters.emhttp().var ?? {}; + } +} diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.resolver.spec.ts b/api/src/unraid-api/graph/resolvers/vms/vms.resolver.spec.ts new file mode 100644 index 000000000..6ed339285 --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/vms/vms.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { VmsResolver } from './vms.resolver'; + +describe('VmsResolver', () => { + let resolver: VmsResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [VmsResolver], + }).compile(); + + resolver = module.get(VmsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts b/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts new file mode 100644 index 000000000..33d72b03a --- /dev/null +++ b/api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts @@ -0,0 +1,30 @@ +import { getDomains } from '@app/core/modules/vms/get-domains'; +import { Query, Resolver } from '@nestjs/graphql'; +import { UseRoles } from 'nest-access-control'; + +@Resolver() +export class VmsResolver { + @Query() + @UseRoles({ + resource: 'vms', + action: 'read', + possession: 'any', + }) + public async vms() { + /** + * @todo Method implemntation + */ + return {}; + } + + @Resolver('domain') + @Query() + @UseRoles({ + resource: 'vms/domain', + action: 'read', + possession: 'any', + }) + public async domain() { + return getDomains(); + } +} diff --git a/api/src/unraid-api/main.ts b/api/src/unraid-api/main.ts new file mode 100644 index 000000000..ca4898855 --- /dev/null +++ b/api/src/unraid-api/main.ts @@ -0,0 +1,72 @@ +import { NestFactory } from '@nestjs/core'; +import { LoggerErrorInterceptor, Logger as PinoLogger } from 'nestjs-pino'; +import { AppModule } from './app/app.module'; +import Fastify from 'fastify'; +import { + FastifyAdapter, + type NestFastifyApplication, +} from '@nestjs/platform-fastify'; +import { type CorsOptionsDelegate } from 'cors'; + +import { getAllowedOrigins } from '@app/common/allowed-origins'; +import { HttpExceptionFilter } from '@app/unraid-api/exceptions/http-exceptions.filter'; +import { GraphQLError } from 'graphql'; +import { GraphQLExceptionsFilter } from '@app/unraid-api/exceptions/graphql-exceptions.filter'; +import { BYPASS_PERMISSION_CHECKS, PORT } from '@app/environment'; +import { type FastifyInstance } from 'fastify'; +import { type Server, type IncomingMessage, type ServerResponse } from 'http'; +import { apiLogger } from '@app/core/log'; +export const corsOptionsDelegate: CorsOptionsDelegate = async ( + origin: string | undefined +) => { + const allowedOrigins = getAllowedOrigins(); + if (origin && allowedOrigins.includes(origin)) { + return true; + } else { + apiLogger.debug(`Origin not in allowed origins: ${origin}`); + + if (BYPASS_PERMISSION_CHECKS) { + return true; + } + + throw new GraphQLError( + 'The CORS policy for this site does not allow access from the specified Origin.' + ); + } +}; + +export async function bootstrapNestServer(): Promise { + const server: FastifyInstance = + Fastify({ + logger: false, + }); + + const app = await NestFactory.create( + AppModule, + new FastifyAdapter(server), + { cors: { origin: corsOptionsDelegate }, bufferLogs: true } + ); + + // Setup Nestjs Pino Logger + app.useLogger(app.get(PinoLogger)); + app.useGlobalInterceptors(new LoggerErrorInterceptor()); + app.flushLogs(); + + apiLogger.debug('Starting Nest Server on Port / Path: %s', PORT); + app.useGlobalFilters( + new GraphQLExceptionsFilter(), + new HttpExceptionFilter() + ); + + await app.init(); + if (Number.isNaN(parseInt(PORT))) { + server.listen({ path: '/var/run/unraid-api.sock' }); + } else { + server.listen({ port: parseInt(PORT), host: '0.0.0.0' }); + } + + //await app.getHttpAdapter().listen(PORT); + apiLogger.debug('Nest Server is now listening'); + + return app; +} diff --git a/api/src/unraid-api/observers/shutdown.observer.ts b/api/src/unraid-api/observers/shutdown.observer.ts new file mode 100644 index 000000000..9965e7326 --- /dev/null +++ b/api/src/unraid-api/observers/shutdown.observer.ts @@ -0,0 +1,37 @@ +import { Injectable, type OnApplicationShutdown } from '@nestjs/common'; +import { type FastifyInstance } from 'fastify'; +import { Logger } from '@nestjs/common'; +import { type Server, type IncomingMessage, type ServerResponse } from 'http'; + +/** + * @todo Swap to this globally. This is a better way to handle shutdowns (right now they're handled in index.ts) + */ +@Injectable() +export class ShutdownObserver implements OnApplicationShutdown { + private httpServers: FastifyInstance< + Server, + IncomingMessage, + ServerResponse + >[] = []; + private logger = new Logger(ShutdownObserver.name); + + public addFastifyServer( + server: FastifyInstance + ): void { + this.logger.debug('Adding Fastify Server to Shutdown Observers'); + this.httpServers.push(server); + } + + public async onApplicationShutdown(signal): Promise { + console.log('Application Shutdown detected with signal: ' + signal); + this.logger.debug('Shutting down NestJS application'); + for (const server of this.httpServers) { + try { + this.logger.debug('Shut down server'); + await server?.close?.(); + } catch (error) { + this.logger.error(`Error Shutting Down Server: ${error}`); + } + } + } +} diff --git a/api/src/unraid-api/rest/rest.controller.ts b/api/src/unraid-api/rest/rest.controller.ts new file mode 100644 index 000000000..fe2cda17e --- /dev/null +++ b/api/src/unraid-api/rest/rest.controller.ts @@ -0,0 +1,56 @@ +import { Public } from '@app/unraid-api/auth/public.decorator'; +import { RestService } from '@app/unraid-api/rest/rest.service'; +import { Controller, Get, Res, Logger, Param } from '@nestjs/common'; +import { FastifyReply } from 'fastify'; +import { UseRoles } from 'nest-access-control'; + +@Controller() +export class RestController { + protected logger = new Logger(RestController.name); + constructor(private readonly restService: RestService) {} + + @Get('/') + @Public() + async getRoot() { + return 'OK'; + } + + @Get('/graphql/api/logs') + @UseRoles({ + resource: 'logs', + action: 'read', + possession: 'any', + }) + async getLogs(@Res() res: FastifyReply) { + try { + const logStream = await this.restService.getLogs(); + return res.status(200).type('application/x-gtar').send(logStream); + } catch (error: unknown) { + this.logger.error(error); + return res.status(500).send(`Error: Failed to get logs`); + } + } + + @Get('/graphql/api/customizations/:type') + @UseRoles({ + resource: 'customizations', + action: 'read', + possession: 'any', + }) + async getCustomizations( + @Param('type') type: string, + @Res() res: FastifyReply + ) { + if (type !== 'banner' && type !== 'case') { + throw new Error('Invalid Customization Type'); + } + + try { + const customizationStream = await this.restService.getCustomizationStream(type); + return res.status(200).type('image/png').send(customizationStream); + } catch (error: unknown) { + this.logger.error(error); + return res.status(500).send(`Error: Failed to get customizations`); + } + } +} diff --git a/api/src/unraid-api/rest/rest.module.ts b/api/src/unraid-api/rest/rest.module.ts new file mode 100644 index 000000000..ea2651913 --- /dev/null +++ b/api/src/unraid-api/rest/rest.module.ts @@ -0,0 +1,10 @@ +import { RestController } from '@app/unraid-api/rest/rest.controller'; +import { Module } from '@nestjs/common'; +import { RestService } from './rest.service'; + +@Module({ + imports: [], + controllers: [RestController], + providers: [RestService], +}) +export class RestModule {} diff --git a/api/src/unraid-api/rest/rest.service.spec.ts b/api/src/unraid-api/rest/rest.service.spec.ts new file mode 100644 index 000000000..ab16a8db4 --- /dev/null +++ b/api/src/unraid-api/rest/rest.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { RestService } from './rest.service'; + +describe('RestService', () => { + let service: RestService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [RestService], + }).compile(); + + service = module.get(RestService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/api/src/unraid-api/rest/rest.service.ts b/api/src/unraid-api/rest/rest.service.ts new file mode 100644 index 000000000..2b0f615c8 --- /dev/null +++ b/api/src/unraid-api/rest/rest.service.ts @@ -0,0 +1,78 @@ +import { report } from '@app/cli/commands/report'; +import { getBannerPathIfPresent, getCasePathIfPresent } from '@app/core/utils/images/image-file-helpers'; +import { getters } from '@app/store/index'; +import { Injectable, Logger } from '@nestjs/common'; +import { execa } from 'execa'; +import { type ReadStream, createReadStream } from 'node:fs'; +import { stat, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +@Injectable() +export class RestService { + protected logger = new Logger(RestService.name); + + async saveApiReport (pathToReport: string) { + try { + const apiReport = await report('-vv', '--json'); + this.logger.debug('Report object %o', apiReport); + await writeFile( + pathToReport, + JSON.stringify(apiReport, null, 2), + 'utf-8' + ); + } catch (error) { + this.logger.warn( + 'Could not generate report for zip with error %o', + error + ); + } + } + + async getLogs(): Promise { + const logPath = getters.paths()['log-base']; + try { + await this.saveApiReport(join(logPath, 'report.json')); + } catch (error) { + this.logger.warn( + 'Could not generate report for zip with error %o', + error + ); + } + const zipToWrite = join(logPath, '../unraid-api.tar.gz'); + + const logPathExists = Boolean(await stat(logPath).catch(() => null)); + if (logPathExists) { + try { + await execa('tar', ['-czf', zipToWrite, logPath]); + const tarFileExists = Boolean(await stat(zipToWrite).catch(() => null)); + + if (tarFileExists) { + return createReadStream(zipToWrite); + } else { + throw new Error('Failed to create log zip'); + } + } catch (error) { + throw new Error('Failed to create logs'); + } + } else { + throw new Error('No logs to download'); + } + } + + async getCustomizationPath(type: 'banner' | 'case'): Promise { + switch (type) { + case 'banner': + return getBannerPathIfPresent(); + case 'case': + return getCasePathIfPresent(); + } + } + + async getCustomizationStream(type: 'banner' | 'case'): Promise { + const path = await this.getCustomizationPath(type); + if (!path) { + throw new Error(`No ${type} found`); + } + return createReadStream(path); + } +} diff --git a/api/src/unraid-api/users/users.module.ts b/api/src/unraid-api/users/users.module.ts new file mode 100644 index 000000000..40981b941 --- /dev/null +++ b/api/src/unraid-api/users/users.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { UsersService } from './users.service'; + +@Module({ + providers: [UsersService], + exports: [UsersService], +}) +export class UsersModule {} diff --git a/api/src/unraid-api/users/users.service.ts b/api/src/unraid-api/users/users.service.ts new file mode 100644 index 000000000..497d40419 --- /dev/null +++ b/api/src/unraid-api/users/users.service.ts @@ -0,0 +1,49 @@ +import { BYPASS_PERMISSION_CHECKS } from '@app/environment'; +import { type UserAccount } from '@app/graphql/generated/api/types'; +import { getters } from '@app/store/index'; +import { Injectable } from '@nestjs/common'; +import { Logger } from '@nestjs/common'; + +@Injectable() +export class UsersService { + private logger = new Logger(UsersService.name); + apiKeyToUser(apiKey: string): UserAccount | null { + const config = getters.config(); + if (BYPASS_PERMISSION_CHECKS === true) { + this.logger.warn(`BYPASSING_PERMISSION_CHECK`); + return { + id: '-1', + description: 'BYPASS_PERMISSION_CHECK', + name: 'BYPASS_PERMISSION_CHECK', + roles: 'admin', + }; + } + if (apiKey === config.remote.apikey) + return { + id: '-1', + description: 'My servers service account', + name: 'my_servers', + roles: 'my_servers', + }; + if (apiKey === config.upc.apikey) + return { + id: '-1', + description: 'UPC service account', + name: 'upc', + roles: 'upc', + }; + if (apiKey === config.notifier.apikey) + return { + id: '-1', + description: 'Notifier service account', + name: 'notifier', + roles: 'notifier', + }; + + return null; + } + + findOne(apiKey: string): UserAccount | null { + return this.apiKeyToUser(apiKey); + } +} diff --git a/api/src/ws.ts b/api/src/ws.ts new file mode 100644 index 000000000..f92352163 --- /dev/null +++ b/api/src/ws.ts @@ -0,0 +1,34 @@ +import { graphqlLogger } from '@app/core/log'; + +type Subscription = { + total: number; + channels: string[]; +}; + +const subscriptions: Record = {}; + +/** + * Return current ws connection count. + */ +export const getWsConnectionCount = () => Object.values(subscriptions).filter(subscription => subscription.total >= 1).length; + +/** + * Return current ws connection count in channel. + */ +export const getWsConnectionCountInChannel = (channel: string) => Object.values(subscriptions).filter(subscription => subscription.channels.includes(channel)).length; + +export const hasSubscribedToChannel = (id: string, channel: string) => { + graphqlLogger.debug('Subscribing to %s', channel); + + // Setup initial object + if (subscriptions[id] === undefined) { + subscriptions[id] = { + total: 1, + channels: [channel], + }; + return; + } + + subscriptions[id].total++; + subscriptions[id].channels.push(channel); +}; diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 000000000..f671e9243 --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,42 @@ +{ + "include": [ + "src/**/*", + ".eslintrc.cjs", + "vite.config.ts" + ], + "exclude": [ + "node_modules", + "vite.config.ts" + ], + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "paths": { + "@app/*": [ + "./src/*" + ] + }, + "skipLibCheck": true, + "target": "ESNext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + "allowJs": false, /* Allow javascript files to be compiled. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + "removeComments": true, /* Do not emit comments to output. */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true, /* Enable strict null checks. */ + "noUnusedLocals": false, /* Report errors on unused locals. */ + "noUnusedParameters": false, /* Report errors on unused parameters. */ + "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + "typeRoots": [ + "node_modules/@types/", + "./src/types/" + ], /* List of folders to include type definitions from. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, + "resolveJsonModule": false + } +} \ No newline at end of file diff --git a/api/vite.config.ts b/api/vite.config.ts new file mode 100644 index 000000000..bf98d326e --- /dev/null +++ b/api/vite.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config'; +import tsconfigPaths from 'vite-tsconfig-paths'; +import { config } from 'dotenv'; + +export default defineConfig(() => { + config({ path: './.env.test' }); + // Manually set NODE_ENV to make sure we always run tests in test mode + process.env.NODE_ENV = 'test'; + return { + + plugins: [tsconfigPaths()], + test: { + globals: true, + coverage: { + all: true, + include: ['src/**/*'], + reporter: ['text', 'json', 'html'], + }, + clearMocks: true, + setupFiles: [ + 'src/__test__/setup/keyserver-mock.ts' + ] + }, + }; +}); +