Skip to content

Commit 4dd3718

Browse files
committed
feat: add remote access and connection management to GraphQL API
- Introduced new types and enums for managing remote access configurations, including AccessUrl, RemoteAccess, and Connect settings. - Added mutations for updating API settings and managing remote access. - Updated the API configuration to include the new connect plugin. - Enhanced the pnpm lock file with the addition of the pify package. - Implemented logic to skip file modifications in development mode.
1 parent 5caeb09 commit 4dd3718

File tree

7 files changed

+256
-1
lines changed

7 files changed

+256
-1
lines changed

api/dev/configs/api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
"extraOrigins": [],
44
"sandbox": false,
55
"ssoSubIds": [],
6-
"plugins": []
6+
"plugins": ["unraid-api-plugin-connect"]
77
}

api/dev/states/connectStatus.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"connectionStatus": "PRE_INIT",
3+
"error": null,
4+
"lastPing": null,
5+
"allowedOrigins": "",
6+
"timestamp": 1753974976746
7+
}

api/generated-schema.graphql

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1599,6 +1599,160 @@ type Plugin {
15991599
hasCliModule: Boolean
16001600
}
16011601

1602+
type AccessUrl {
1603+
type: URL_TYPE!
1604+
name: String
1605+
ipv4: URL
1606+
ipv6: URL
1607+
}
1608+
1609+
enum URL_TYPE {
1610+
LAN
1611+
WIREGUARD
1612+
WAN
1613+
MDNS
1614+
OTHER
1615+
DEFAULT
1616+
}
1617+
1618+
"""
1619+
A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt.
1620+
"""
1621+
scalar URL
1622+
1623+
type AccessUrlObject {
1624+
ipv4: String
1625+
ipv6: String
1626+
type: URL_TYPE!
1627+
name: String
1628+
}
1629+
1630+
type ApiKeyResponse {
1631+
valid: Boolean!
1632+
error: String
1633+
}
1634+
1635+
type MinigraphqlResponse {
1636+
status: MinigraphStatus!
1637+
timeout: Int
1638+
error: String
1639+
}
1640+
1641+
"""The status of the minigraph"""
1642+
enum MinigraphStatus {
1643+
PRE_INIT
1644+
CONNECTING
1645+
CONNECTED
1646+
PING_FAILURE
1647+
ERROR_RETRYING
1648+
}
1649+
1650+
type CloudResponse {
1651+
status: String!
1652+
ip: String
1653+
error: String
1654+
}
1655+
1656+
type RelayResponse {
1657+
status: String!
1658+
timeout: String
1659+
error: String
1660+
}
1661+
1662+
type Cloud {
1663+
error: String
1664+
apiKey: ApiKeyResponse!
1665+
relay: RelayResponse
1666+
minigraphql: MinigraphqlResponse!
1667+
cloud: CloudResponse!
1668+
allowedOrigins: [String!]!
1669+
}
1670+
1671+
type RemoteAccess {
1672+
"""The type of WAN access used for Remote Access"""
1673+
accessType: WAN_ACCESS_TYPE!
1674+
1675+
"""The type of port forwarding used for Remote Access"""
1676+
forwardType: WAN_FORWARD_TYPE
1677+
1678+
"""The port used for Remote Access"""
1679+
port: Int
1680+
}
1681+
1682+
enum WAN_ACCESS_TYPE {
1683+
DYNAMIC
1684+
ALWAYS
1685+
DISABLED
1686+
}
1687+
1688+
enum WAN_FORWARD_TYPE {
1689+
UPNP
1690+
STATIC
1691+
}
1692+
1693+
type DynamicRemoteAccessStatus {
1694+
"""The type of dynamic remote access that is enabled"""
1695+
enabledType: DynamicRemoteAccessType!
1696+
1697+
"""The type of dynamic remote access that is currently running"""
1698+
runningType: DynamicRemoteAccessType!
1699+
1700+
"""Any error message associated with the dynamic remote access"""
1701+
error: String
1702+
}
1703+
1704+
enum DynamicRemoteAccessType {
1705+
STATIC
1706+
UPNP
1707+
DISABLED
1708+
}
1709+
1710+
type ConnectSettingsValues {
1711+
"""The type of WAN access used for Remote Access"""
1712+
accessType: WAN_ACCESS_TYPE!
1713+
1714+
"""The type of port forwarding used for Remote Access"""
1715+
forwardType: WAN_FORWARD_TYPE
1716+
1717+
"""The port used for Remote Access"""
1718+
port: Int
1719+
}
1720+
1721+
type ConnectSettings implements Node {
1722+
id: PrefixedID!
1723+
1724+
"""The data schema for the Connect settings"""
1725+
dataSchema: JSON!
1726+
1727+
"""The UI schema for the Connect settings"""
1728+
uiSchema: JSON!
1729+
1730+
"""The values for the Connect settings"""
1731+
values: ConnectSettingsValues!
1732+
}
1733+
1734+
type Connect implements Node {
1735+
id: PrefixedID!
1736+
1737+
"""The status of dynamic remote access"""
1738+
dynamicRemoteAccess: DynamicRemoteAccessStatus!
1739+
1740+
"""The settings for the Connect instance"""
1741+
settings: ConnectSettings!
1742+
}
1743+
1744+
type Network implements Node {
1745+
id: PrefixedID!
1746+
accessUrls: [AccessUrl!]
1747+
}
1748+
1749+
input AccessUrlObjectInput {
1750+
ipv4: String
1751+
ipv6: String
1752+
type: URL_TYPE!
1753+
name: String
1754+
}
1755+
16021756
"\n### Description:\n\nID scalar type that prefixes the underlying ID with the server identifier on output and strips it on input.\n\nWe use this scalar type to ensure that the ID is unique across all servers, allowing the same underlying resource ID to be used across different server instances.\n\n#### Input Behavior:\n\nWhen providing an ID as input (e.g., in arguments or input objects), the server identifier prefix ('<serverId>:') is optional.\n\n- If the prefix is present (e.g., '123:456'), it will be automatically stripped, and only the underlying ID ('456') will be used internally.\n- If the prefix is absent (e.g., '456'), the ID will be used as-is.\n\nThis makes it flexible for clients, as they don't strictly need to know or provide the server ID.\n\n#### Output Behavior:\n\nWhen an ID is returned in the response (output), it will *always* be prefixed with the current server's unique identifier (e.g., '123:456').\n\n#### Example:\n\nNote: The server identifier is '123' in this example.\n\n##### Input (Prefix Optional):\n```graphql\n# Both of these are valid inputs resolving to internal ID '456'\n{\n someQuery(id: \"123:456\") { ... }\n anotherQuery(id: \"456\") { ... }\n}\n```\n\n##### Output (Prefix Always Added):\n```graphql\n# Assuming internal ID is '456'\n{\n \"data\": {\n \"someResource\": {\n \"id\": \"123:456\" \n }\n }\n}\n```\n "
16031757
scalar PrefixedID
16041758

@@ -1650,6 +1804,10 @@ type Query {
16501804

16511805
"""List all installed plugins with their metadata"""
16521806
plugins: [Plugin!]!
1807+
remoteAccess: RemoteAccess!
1808+
connect: Connect!
1809+
network: Network!
1810+
cloud: Cloud!
16531811
}
16541812

16551813
type Mutation {
@@ -1693,6 +1851,11 @@ type Mutation {
16931851
Remove one or more plugins from the API. Returns false if restart was triggered automatically, true if manual restart is required.
16941852
"""
16951853
removePlugin(input: PluginManagementInput!): Boolean!
1854+
updateApiSettings(input: ConnectSettingsInput!): ConnectSettingsValues!
1855+
connectSignIn(input: ConnectSignInInput!): Boolean!
1856+
connectSignOut: Boolean!
1857+
setupRemoteAccess(input: SetupRemoteAccessInput!): Boolean!
1858+
enableDynamicRemoteAccess(input: EnableDynamicRemoteAccessInput!): Boolean!
16961859
}
16971860

16981861
input NotificationData {
@@ -1810,6 +1973,66 @@ input PluginManagementInput {
18101973
restart: Boolean! = true
18111974
}
18121975

1976+
input ConnectSettingsInput {
1977+
"""The type of WAN access to use for Remote Access"""
1978+
accessType: WAN_ACCESS_TYPE
1979+
1980+
"""The type of port forwarding to use for Remote Access"""
1981+
forwardType: WAN_FORWARD_TYPE
1982+
1983+
"""
1984+
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP.
1985+
"""
1986+
port: Int
1987+
}
1988+
1989+
input ConnectSignInInput {
1990+
"""The API key for authentication"""
1991+
apiKey: String!
1992+
1993+
"""User information for the sign-in"""
1994+
userInfo: ConnectUserInfoInput
1995+
}
1996+
1997+
input ConnectUserInfoInput {
1998+
"""The preferred username of the user"""
1999+
preferred_username: String!
2000+
2001+
"""The email address of the user"""
2002+
email: String!
2003+
2004+
"""The avatar URL of the user"""
2005+
avatar: String
2006+
}
2007+
2008+
input SetupRemoteAccessInput {
2009+
"""The type of WAN access to use for Remote Access"""
2010+
accessType: WAN_ACCESS_TYPE!
2011+
2012+
"""The type of port forwarding to use for Remote Access"""
2013+
forwardType: WAN_FORWARD_TYPE
2014+
2015+
"""
2016+
The port to use for Remote Access. Not required for UPNP forwardType. Required for STATIC forwardType. Ignored if accessType is DISABLED or forwardType is UPNP.
2017+
"""
2018+
port: Int
2019+
}
2020+
2021+
input EnableDynamicRemoteAccessInput {
2022+
"""The AccessURL Input for dynamic remote access"""
2023+
url: AccessUrlInput!
2024+
2025+
"""Whether to enable or disable dynamic remote access"""
2026+
enabled: Boolean!
2027+
}
2028+
2029+
input AccessUrlInput {
2030+
type: URL_TYPE!
2031+
name: String
2032+
ipv4: URL
2033+
ipv6: URL
2034+
}
2035+
18132036
type Subscription {
18142037
displaySubscription: Display!
18152038
infoSubscription: Info!

api/src/unraid-api/unraid-file-modifier/file-modification.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { applyPatch, createPatch, parsePatch, reversePatch } from 'diff';
88
import { coerce, compare, gte } from 'semver';
99

1010
import { getUnraidVersion } from '@app/common/dashboard/get-unraid-version.js';
11+
import { NODE_ENV } from '@app/environment.js';
1112

1213
export type ModificationEffect = 'nginx:reload';
1314

@@ -218,6 +219,14 @@ export abstract class FileModification {
218219
throw new Error('Invalid file modification configuration');
219220
}
220221

222+
// Skip file modifications in development mode
223+
if (NODE_ENV === 'development') {
224+
return {
225+
shouldApply: false,
226+
reason: 'File modifications are disabled in development mode',
227+
};
228+
}
229+
221230
const fileExists = await access(this.filePath, constants.R_OK | constants.W_OK)
222231
.then(() => true)
223232
.catch(() => false);

api/src/unraid-api/unraid-file-modifier/unraid-file-modifier.service.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { ConfigService } from '@nestjs/config';
99

1010
import type { ModificationEffect } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
11+
import { NODE_ENV } from '@app/environment.js';
1112
import { FileModificationEffectService } from '@app/unraid-api/unraid-file-modifier/file-modification-effect.service.js';
1213
import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
1314

@@ -29,6 +30,11 @@ export class UnraidFileModificationService
2930
*/
3031
async onModuleInit() {
3132
try {
33+
if (NODE_ENV === 'development') {
34+
this.logger.log('Skipping file modifications in development mode');
35+
return;
36+
}
37+
3238
this.logger.log('Loading file modifications...');
3339
const mods = await this.loadModifications();
3440
await this.applyModifications(mods);

packages/unraid-api-plugin-connect/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"jose": "6.0.12",
5858
"lodash-es": "4.17.21",
5959
"nest-authz": "2.17.0",
60+
"pify": "^6.1.0",
6061
"prettier": "3.6.2",
6162
"rimraf": "6.0.1",
6263
"rxjs": "7.8.2",

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)