Compare commits

..

1 Commits

Author SHA1 Message Date
snyk-bot
db95ba9336 fix: upgrade vue-i18n from 9.2.2 to 9.4.0
Snyk has created this PR to upgrade vue-i18n from 9.2.2 to 9.4.0.

See this package in npm:
https://www.npmjs.com/package/vue-i18n

See this project in Snyk:
https://app.snyk.io/org/ljm42/project/1e6cd09c-8b19-4c1a-82a7-745b77b939c6?utm_source=github&utm_medium=referral&page=upgrade-pr
2023-10-14 01:49:09 +00:00
257 changed files with 11434 additions and 21793 deletions

View File

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

3
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

6869
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -26,11 +26,11 @@
"compile": "tsup --config ./tsup.config.ts",
"bundle": "pkg . --public",
"build": "npm run compile && npm run bundle",
"build:docker": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder",
"build:docker": "docker-compose run --rm builder",
"build-pkg": "./scripts/build.mjs",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config",
"codegen:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 MOTHERSHIP_GRAPHQL_LINK='https://mothership.localhost/ws' graphql-codegen-esm --config codegen.yml --watch",
"codegen:local": "MOTHERSHIP_GRAPHQL_LINK='http://localhost:3000/graphql' graphql-codegen-esm --config codegen.yml --watch",
"tsc": "tsc --noEmit",
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
@@ -41,16 +41,14 @@
"release": "standard-version",
"typesync": "typesync",
"install:unraid": "./scripts/install-in-unraid.sh",
"start:plugin": "INTROSPECTION=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
"start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs start --debug'",
"restart:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs restart --debug'",
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
"start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'",
"start:docker": "docker compose run --rm builder-interactive",
"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"
"docker:dev": "docker-compose run --rm --service-ports dev",
"docker:test": "docker-compose run --rm builder npm run test"
},
"files": [
".env.staging",
@@ -61,17 +59,11 @@
"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-codegen/client-preset": "^3.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",
@@ -83,9 +75,8 @@
"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",
@@ -96,14 +87,14 @@
"dotenv": "^16.0.3",
"express": "^4.18.2",
"find-process": "^1.4.7",
"graphql": "^16.8.1",
"graphql": "^16.6.0",
"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.14.2",
"graphql-ws": "^5.12.1",
"htpasswd-js": "^1.0.2",
"ini": "^4.1.0",
"ip": "^1.1.8",
@@ -112,22 +103,17 @@
"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",
@@ -138,16 +124,15 @@
},
"devDependencies": {
"@babel/runtime": "^7.21.0",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typed-document-node": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.0",
"@graphql-codegen/typescript-operations": "^4.0.0",
"@graphql-codegen/typescript-resolvers": "4.0.1",
"@graphql-codegen/add": "^4.0.1",
"@graphql-codegen/cli": "^3.3.0",
"@graphql-codegen/fragment-matcher": "^4.0.1",
"@graphql-codegen/import-types-preset": "^2.2.6",
"@graphql-codegen/typed-document-node": "^4.0.0",
"@graphql-codegen/typescript": "^3.0.3",
"@graphql-codegen/typescript-operations": "^3.0.3",
"@graphql-codegen/typescript-resolvers": "3.2.1",
"@graphql-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",
@@ -189,6 +174,7 @@
"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",
@@ -196,6 +182,7 @@
"pkg": "^5.8.1",
"pretty-bytes": "^6.1.0",
"pretty-ms": "^8.0.0",
"serialize-error": "^11.0.2",
"standard-version": "^9.5.0",
"tsup": "^7.0.0",
"typescript": "^4.9.4",

View File

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

View File

@@ -16,21 +16,34 @@ 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'),
}));
@@ -64,7 +77,7 @@ test('Returns generated data', async () => {
"case": {
"base64": "",
"error": "",
"icon": "",
"icon": "case-model.png",
"url": "",
},
},
@@ -94,8 +107,6 @@ test('Returns generated data', async () => {
"flashGuid": "0000-0000-0000-000000000000",
"regState": "PRO",
"regTy": "PRO",
"serverDescription": "Dev Server",
"serverName": "Tower",
},
"versions": {
"unraid": "6.11.2",

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,48 +0,0 @@
import { beforeEach, expect, test, vi } from 'vitest';
// Preloading imports for faster tests
import '@app/mothership/utils/convert-to-fuzzy-time';
vi.mock('fs', () => ({
default: {
readFileSync: vi.fn().mockReturnValue('my-file'),
writeFileSync: vi.fn(),
existsSync: vi.fn(),
},
readFileSync: vi.fn().mockReturnValue('my-file'),
existsSync: vi.fn(),
}));
vi.mock('@graphql-tools/schema', () => ({
makeExecutableSchema: vi.fn(),
}));
vi.mock('@app/core/log', () => ({
default: { relayLogger: { trace: vi.fn() } },
relayLogger: { trace: vi.fn() },
logger: { trace: vi.fn() },
}));
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
const generateTestCases = () => {
const cases: Array<{ min: number; max: number }> = [];
for (let i = 0; i < 15; i += 1) {
const min = Math.round(Math.random() * 100);
const max = min + (Math.round(Math.random() * 20));
cases.push({ min, max });
}
return cases;
};
test.each(generateTestCases())('Successfully converts to fuzzy time %o', async ({ min, max }) => {
const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time');
const res = convertToFuzzyTime(min, max);
expect(res).toBeGreaterThanOrEqual(min);
expect(res).toBeLessThanOrEqual(max);
});

View File

@@ -75,10 +75,15 @@ 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;
}
@@ -117,10 +122,12 @@ export const getServersData = async ({
);
return foundServers;
} catch (error: unknown) {
cliLogger.addContext('error', error);
cliLogger.trace(
'Failed fetching servers from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
cliLogger.removeContext('error');
return {
online: [],
offline: [],

View File

@@ -1,12 +0,0 @@
import { setEnv } from '@app/cli/set-env';
import { start } from '@app/cli/commands/start';
import { stop } from '@app/cli/commands/stop';
/**
* Stop a running API process and then start it again.
*/
export const restart = async () => {
setEnv('LOG_TRANSPORT', 'stdout');
await stop();
await start();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,111 +1,137 @@
import { pino } from 'pino';
import { LOG_TRANSPORT, LOG_TYPE } from '@app/environment';
import chalk from 'chalk';
import { configure, getLogger } from 'log4js';
import { serializeError } from 'serialize-error';
import pretty from 'pino-pretty';
export const levels = ['ALL', 'TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL', 'MARK', 'OFF'] as const;
export const levels = [
'trace',
'debug',
'info',
'warn',
'error',
'fatal',
] as const;
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 '';
}
const level =
levels[
levels.indexOf(
process.env.LOG_LEVEL?.toLowerCase() as (typeof levels)[number]
)
] ?? 'info';
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}`;
}
},
},
};
export const logDestination = pino.destination({
dest: LOG_TRANSPORT === 'file' ? '/var/log/unraid-api/stdout.log' : 1,
minLength: 1_024,
sync: false
});
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 stream =
LOG_TYPE === 'pretty'
? pretty({
singleLine: true,
hideObject: false,
colorize: true,
ignore: 'time,hostname,pid',
destination: logDestination,
})
: logDestination;
export const logger = pino(
{
level,
timestamp: () => `,"time":"${new Date().toISOString()}"`,
formatters: {
level: (label: string) => ({ level: label }),
},
},
stream
);
export const internalLogger = logger.child({ logger: 'internal' });
export const appLogger = logger.child({ logger: 'app' });
export const mothershipLogger = logger.child({ logger: 'mothership' });
export const dashboardLogger = logger.child({ logger: 'dashboard' });
export const emhttpLogger = logger.child({ logger: 'emhttp' });
export const libvirtLogger = logger.child({ logger: 'libvirt' });
export const graphqlLogger = logger.child({ logger: 'graphql' });
export const dockerLogger = logger.child({ logger: 'docker' });
export const cliLogger = logger.child({ logger: 'cli' });
export const minigraphLogger = logger.child({ logger: 'minigraph' });
export const cloudConnectorLogger = logger.child({ logger: 'cloud-connector' });
export const upnpLogger = logger.child({ logger: 'upnp' });
export const keyServerLogger = logger.child({ logger: 'key-server' });
export const remoteAccessLogger = logger.child({ logger: 'remote-access' });
export const remoteQueryLogger = logger.child({ logger: 'remote-query' });
export const apiLogger = logger.child({ logger: 'api' });
export const 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');
export const loggers = [
internalLogger,
appLogger,
mothershipLogger,
dashboardLogger,
emhttpLogger,
libvirtLogger,
graphqlLogger,
dockerLogger,
cliLogger,
minigraphLogger,
cloudConnectorLogger,
upnpLogger,
keyServerLogger,
remoteAccessLogger,
remoteQueryLogger,
apiLogger,
logger,
mothershipLogger,
dashboardLogger,
emhttpLogger,
libvirtLogger,
graphqlLogger,
dockerLogger,
cliLogger,
minigraphLogger,
cloudConnectorLogger,
upnpLogger,
keyServerLogger,
remoteAccessLogger,
remoteQueryLogger,
];
// Send SIGUSR1 to increase log level
process.on('SIGUSR1', () => {
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}`,
});
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);
});
// Send SIGUSR1 to decrease log level
process.on('SIGUSR2', () => {
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}`,
});
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);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
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 } from '@app/core/utils';
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
import { getShares, ensurePermission } from '@app/core/utils';
interface Context extends CoreContext {
params: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,4 @@ export const GRAPHQL_INTROSPECTION = Boolean(
export const PORT = process.env.PORT ?? '/var/run/unraid-api.sock';
export const DRY_RUN = process.env.DRY_RUN === 'true';
export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === 'true';
export const LOG_CORS = process.env.LOG_CORS === 'true';
export const LOG_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';
export const LOG_CORS = process.env.LOG_CORS === 'true';

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
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)) {
@@ -8,3 +10,16 @@ export const getCoreModule = (moduleName: string) => {
return modules[moduleName];
};
export const apiKeyToUser = async (apiKey: string) => {
try {
const config = getters.config();
if (apiKey === config.remote.apikey) return { id: -1, description: 'My servers service account', name: 'my_servers', role: 'my_servers' };
if (apiKey === config.upc.apikey) return { id: -1, description: 'UPC service account', name: 'upc', role: 'upc' };
if (apiKey === config.notifier.apikey) return { id: -1, description: 'Notifier service account', name: 'notifier', role: 'notifier' };
} catch (error: unknown) {
graphqlLogger.debug('Failed looking up API key with "%s"', (error as Error).message);
}
return { id: -1, description: 'A guest user', name: 'guest', role: 'guest' };
};

View File

@@ -1,7 +1,6 @@
import { ensurePermission } from '@app/core/utils/index';
import { NODE_ENV } from '@app/environment';
import {
type ConnectSignInInput,
} from '@app/graphql/generated/api/types';
import { type MutationResolvers } 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';
@@ -10,26 +9,31 @@ import { FileLoadStatus } from '@app/store/types';
import { GraphQLError } from 'graphql';
import { decodeJwt } from 'jose';
export const connectSignIn = async (
input: ConnectSignInInput
): Promise<boolean> => {
export const connectSignIn: MutationResolvers['connectSignIn'] = async (
_,
args,
context
) => {
ensurePermission(context.user, {
resource: 'connect',
possession: 'own',
action: 'update',
});
if (getters.emhttp().status === FileLoadStatus.LOADED) {
const result =
NODE_ENV === 'development'
? API_KEY_STATUS.API_KEY_VALID
: await validateApiKeyWithKeyServer({
apiKey: input.apiKey,
flashGuid: getters.emhttp().var.flashGuid,
});
const result = NODE_ENV === 'development' ? API_KEY_STATUS.API_KEY_VALID : await validateApiKeyWithKeyServer({
apiKey: args.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 = input.idToken
? decodeJwt(input.idToken)
: input.userInfo ?? null;
const userInfo = args.input.idToken
? decodeJwt(args.input.idToken)
: args.input.userInfo ?? null;
if (
!userInfo ||
!userInfo.preferred_username ||
@@ -43,11 +47,10 @@ export const 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: input.apiKey,
apikey: args.input.apiKey,
})
);
return true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,65 +0,0 @@
type Query {
"""Single disk"""
disk(id: ID!): Disk
"""Mulitiple disks"""
disks: [Disk]!
}
type Disk {
# /dev/sdb
device: String!
# SSD
type: String!
# Samsung_SSD_860_QVO_1TB
name: String!
# Samsung
vendor: String!
# 1000204886016
size: Long!
# -1
bytesPerSector: Long!
# -1
totalCylinders: Long!
# -1
totalHeads: Long!
# -1
totalSectors: Long!
# -1
totalTracks: Long!
# -1
tracksPerCylinder: Long!
# -1
sectorsPerTrack: Long!
# 1B6Q
firmwareRevision: String!
# S4CZNF0M807232N
serialNum: String!
interfaceType: DiskInterfaceType!
smartStatus: DiskSmartStatus!
temperature: Long!
partitions: [DiskPartition!]
}
type DiskPartition {
name: String!
fsType: DiskFsType!
size: Long!
}
enum DiskFsType {
xfs
btrfs
vfat
}
enum DiskInterfaceType {
SAS
SATA
USB
PCIe
UNKNOWN
}
enum DiskSmartStatus {
OK
UNKNOWN
}

View File

@@ -1,29 +0,0 @@
type Query {
"""Docker network"""
dockerNetwork(id: ID!): DockerNetwork!
"""All Docker networks"""
dockerNetworks(all: Boolean): [DockerNetwork]!
}
type Subscription {
dockerNetwork(id: ID!): DockerNetwork!
dockerNetworks: [DockerNetwork]!
}
type DockerNetwork {
name: String
id: ID
created: String
scope: String
driver: String
enableIPv6: Boolean!
ipam: JSON
internal: Boolean!
attachable: Boolean!
ingress: Boolean!
configFrom: JSON
configOnly: Boolean!
containers: JSON
options: JSON
labels: JSON
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

46
api/src/graphql/types.ts Normal file
View File

@@ -0,0 +1,46 @@
import { mergeTypeDefs } from '@graphql-tools/merge';
import { gql } from 'graphql-tag';
import { typeDefs } from '@app/graphql/schema/index';
export const baseTypes = [
gql`
scalar JSON
scalar Long
scalar UUID
scalar DateTime
scalar Port
directive @subscription(channel: String!) on FIELD_DEFINITION
type Welcome {
message: String!
}
type Query {
# This should always be available even for guest users
welcome: Welcome @func(module: "getWelcome")
online: Boolean
info: Info
}
type Mutation {
login(username: String!, password: String!): String
sendNotification(notification: NotificationInput!): Notification
shutdown: String
reboot: String
}
type Subscription {
ping: String!
info: Info!
online: Boolean!
}
`,
];
export const types = mergeTypeDefs([
...baseTypes,
typeDefs,
]);
export default types;

View File

@@ -12,23 +12,20 @@ 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: NestFastifyApplication<RawServerDefault>;
let server: ApolloServer<BaseContext>;
const unlinkUnixPort = () => {
if (isNaN(parseInt(PORT, 10))) {
@@ -39,9 +36,6 @@ 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') });
@@ -53,8 +47,6 @@ 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());
@@ -86,7 +78,8 @@ void am(
unlinkUnixPort();
// Start webserver
server = await bootstrapNestServer();
server = await createApolloExpressServer();
PingTimeoutJobs.init();
startMiddlewareListeners();
@@ -95,7 +88,6 @@ 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();
@@ -104,11 +96,16 @@ void am(
});
},
async (error: NodeJS.ErrnoException) => {
logger.error('API-GLOBAL-ERROR %s %s', error.message, error.stack);
if (server) {
await server?.close?.();
}
// Log error to syslog
logger.error('API-GLOBAL-ERROR', error);
shutdownApiEvent();
// Stop server
logger.debug('Stopping HTTP server');
if (server) {
await server.stop();
}
// Kill application
process.exitCode = 1;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,76 @@
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();
};

355
api/src/server.ts Normal file
View File

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

View File

@@ -1,62 +0,0 @@
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',
};
});

View File

@@ -1,24 +0,0 @@
import { logDestination, logger } from '@app/core/log';
import { MinigraphStatus } from '@app/graphql/generated/api/types';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { setGraphqlConnectionStatus } from '@app/store/actions/set-minigraph-status';
import { store } from '@app/store/index';
import { stopListeners } from '@app/store/listeners/stop-listeners';
import { setWanAccess } from '@app/store/modules/config';
import { writeConfigSync } from '@app/store/sync/config-disk-sync';
export const shutdownApiEvent = () => {
logger.debug('Running shutdown');
stopListeners();
store.dispatch(setGraphqlConnectionStatus({ status: MinigraphStatus.PRE_INIT, error: null }));
if (store.getState().config.remote.dynamicRemoteAccessType !== DynamicRemoteAccessType.DISABLED) {
store.dispatch(setWanAccess('no'));
}
logger.debug('Writing final configs');
writeConfigSync('flash');
writeConfigSync('memory');
logDestination.flushSync();
logDestination.destroy();
};

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