refactor(connect): organize src by concern instead of artifact (#1457)

Reorganizes files, but keeps logic (and Nest dependency relationships)
unchanged.

* **Documentation**
  * Update README to reflect the new directory structure.

* **Refactor**
* Update import paths to match the new feature-based directory
structure.
* Rename `SystemModule` to `NetworkModule` for smaller scope and
increased clarity.
  * Reorganize source code into directories of feature-related concerns.
 
* **Chores**
* Delete unused demo configuration and related code for a cleaner
codebase.

* **Other**
  * No functional changes to end-user features or behaviors.
This commit is contained in:
Pujit Mehrotra
2025-07-03 09:47:31 -04:00
committed by GitHub
parent 27b33f0f95
commit 0d443de20e
43 changed files with 272 additions and 256 deletions

View File

@@ -1497,6 +1497,47 @@ type AccessUrlObject {
name: String
}
type ApiKeyResponse {
valid: Boolean!
error: String
}
type MinigraphqlResponse {
status: MinigraphStatus!
timeout: Int
error: String
}
"""The status of the minigraph"""
enum MinigraphStatus {
PRE_INIT
CONNECTING
CONNECTED
PING_FAILURE
ERROR_RETRYING
}
type CloudResponse {
status: String!
ip: String
error: String
}
type RelayResponse {
status: String!
timeout: String
error: String
}
type Cloud {
error: String
apiKey: ApiKeyResponse!
relay: RelayResponse
minigraphql: MinigraphqlResponse!
cloud: CloudResponse!
allowedOrigins: [String!]!
}
type RemoteAccess {
"""The type of WAN access used for Remote Access"""
accessType: WAN_ACCESS_TYPE!
@@ -1575,47 +1616,6 @@ type Network implements Node {
accessUrls: [AccessUrl!]
}
type ApiKeyResponse {
valid: Boolean!
error: String
}
type MinigraphqlResponse {
status: MinigraphStatus!
timeout: Int
error: String
}
"""The status of the minigraph"""
enum MinigraphStatus {
PRE_INIT
CONNECTING
CONNECTED
PING_FAILURE
ERROR_RETRYING
}
type CloudResponse {
status: String!
ip: String
error: String
}
type RelayResponse {
status: String!
timeout: String
error: String
}
type Cloud {
error: String
apiKey: ApiKeyResponse!
relay: RelayResponse
minigraphql: MinigraphqlResponse!
cloud: CloudResponse!
allowedOrigins: [String!]!
}
input AccessUrlObjectInput {
ipv4: String
ipv6: String

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { CloudService } from '../service/cloud.service.js';
import { CloudService } from '../connection-status/cloud.service.js';
const MOTHERSHIP_GRAPHQL_LINK = 'https://mothership.unraid.net/ws';
const API_VERSION = 'TEST_VERSION';

View File

@@ -1,11 +1,11 @@
import { ConfigService } from '@nestjs/config';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { faker } from '@faker-js/faker';
import * as fc from 'fast-check';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ConfigType, DynamicRemoteAccessType } from '../model/connect-config.model.js';
import { ConnectConfigPersister } from '../service/config.persistence.js';
import { ConnectConfigPersister } from '../config/config.persistence.js';
import { ConfigType, DynamicRemoteAccessType } from '../config/connect.config.js';
describe('ConnectConfigPersister', () => {
let service: ConnectConfigPersister;
@@ -430,37 +430,34 @@ ssoSubIds="sub1,sub2"
it('should handle edge cases in port conversion', () => {
fc.assert(
fc.asyncProperty(
fc.integer({ min: 0, max: 65535 }),
async (port) => {
const legacyConfig = {
api: { version: '6.12.0', extraOrigins: '' },
local: { sandbox: 'no' },
remote: {
wanaccess: 'no',
wanport: port.toString(),
upnpEnabled: 'no',
apikey: 'unraid_test',
localApiKey: 'test_local',
email: 'test@example.com',
username: faker.internet.username(),
avatar: '',
regWizTime: '',
accesstoken: '',
idtoken: '',
refreshtoken: '',
dynamicRemoteAccessType: 'DISABLED',
ssoSubIds: '',
},
} as any;
fc.asyncProperty(fc.integer({ min: 0, max: 65535 }), async (port) => {
const legacyConfig = {
api: { version: '6.12.0', extraOrigins: '' },
local: { sandbox: 'no' },
remote: {
wanaccess: 'no',
wanport: port.toString(),
upnpEnabled: 'no',
apikey: 'unraid_test',
localApiKey: 'test_local',
email: 'test@example.com',
username: faker.internet.username(),
avatar: '',
regWizTime: '',
accesstoken: '',
idtoken: '',
refreshtoken: '',
dynamicRemoteAccessType: 'DISABLED',
ssoSubIds: '',
},
} as any;
const result = await service.convertLegacyConfig(legacyConfig);
const result = await service.convertLegacyConfig(legacyConfig);
// Test port conversion logic
expect(result.wanport).toBe(port);
expect(typeof result.wanport).toBe('number');
}
),
// Test port conversion logic
expect(result.wanport).toBe(port);
expect(typeof result.wanport).toBe('number');
}),
{ numRuns: 15 }
);
});

View File

@@ -1,10 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ConfigService } from '@nestjs/config';
import { faker } from '@faker-js/faker';
import * as fc from 'fast-check';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MyServersConfig, DynamicRemoteAccessType } from '../model/connect-config.model.js';
import { ConnectConfigPersister } from '../service/config.persistence.js';
import { ConnectConfigPersister } from '../config/config.persistence.js';
import { DynamicRemoteAccessType, MyServersConfig } from '../config/connect.config.js';
describe('MyServersConfig Validation', () => {
let persister: ConnectConfigPersister;
@@ -23,7 +24,7 @@ describe('MyServersConfig Validation', () => {
} as any;
persister = new ConnectConfigPersister(configService as any);
validConfig = {
wanaccess: false,
wanport: 0,
@@ -157,31 +158,24 @@ describe('MyServersConfig Validation', () => {
it('should handle various boolean combinations', () => {
fc.assert(
fc.asyncProperty(
fc.boolean(),
fc.boolean(),
async (wanaccess, upnpEnabled) => {
const config = { ...validConfig, wanaccess, upnpEnabled };
const result = await persister.validate(config);
expect(result.wanaccess).toBe(wanaccess);
expect(result.upnpEnabled).toBe(upnpEnabled);
}
),
fc.asyncProperty(fc.boolean(), fc.boolean(), async (wanaccess, upnpEnabled) => {
const config = { ...validConfig, wanaccess, upnpEnabled };
const result = await persister.validate(config);
expect(result.wanaccess).toBe(wanaccess);
expect(result.upnpEnabled).toBe(upnpEnabled);
}),
{ numRuns: 10 }
);
});
it('should handle valid port numbers', () => {
fc.assert(
fc.asyncProperty(
fc.integer({ min: 0, max: 65535 }),
async (port) => {
const config = { ...validConfig, wanport: port };
const result = await persister.validate(config);
expect(result.wanport).toBe(port);
expect(typeof result.wanport).toBe('number');
}
),
fc.asyncProperty(fc.integer({ min: 0, max: 65535 }), async (port) => {
const config = { ...validConfig, wanport: port };
const result = await persister.validate(config);
expect(result.wanport).toBe(port);
expect(typeof result.wanport).toBe('number');
}),
{ numRuns: 20 }
);
});
@@ -225,9 +219,9 @@ describe('MyServersConfig Validation', () => {
it('should reject invalid enum values', () => {
fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1 }).filter(s =>
!Object.values(DynamicRemoteAccessType).includes(s as any)
),
fc
.string({ minLength: 1 })
.filter((s) => !Object.values(DynamicRemoteAccessType).includes(s as any)),
async (invalidEnumValue) => {
const config = { ...validConfig, dynamicRemoteAccessType: invalidEnumValue };
await expect(persister.validate(config)).rejects.toThrow();
@@ -240,9 +234,9 @@ describe('MyServersConfig Validation', () => {
it('should reject invalid email formats using fuzzing', () => {
fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1 }).filter(s =>
!s.includes('@') || s.startsWith('@') || s.endsWith('@')
),
fc
.string({ minLength: 1 })
.filter((s) => !s.includes('@') || s.startsWith('@') || s.endsWith('@')),
async (invalidEmail) => {
const config = { ...validConfig, email: invalidEmail };
await expect(persister.validate(config)).rejects.toThrow();
@@ -254,15 +248,12 @@ describe('MyServersConfig Validation', () => {
it('should accept any number values for wanport (range validation is done at form level)', () => {
fc.assert(
fc.asyncProperty(
fc.integer({ min: -100000, max: 100000 }),
async (port) => {
const config = { ...validConfig, wanport: port };
const result = await persister.validate(config);
expect(result.wanport).toBe(port);
expect(typeof result.wanport).toBe('number');
}
),
fc.asyncProperty(fc.integer({ min: -100000, max: 100000 }), async (port) => {
const config = { ...validConfig, wanport: port };
const result = await persister.validate(config);
expect(result.wanport).toBe(port);
expect(typeof result.wanport).toBe('number');
}),
{ numRuns: 10 }
);
});
@@ -310,4 +301,4 @@ describe('MyServersConfig Validation', () => {
);
});
});
});
});

View File

@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { MothershipGraphqlClientService } from '../service/graphql.client.js';
import { MinigraphStatus } from '../model/connect-config.model.js';
import { MinigraphStatus } from '../config/connect.config.js';
import { MothershipGraphqlClientService } from '../mothership-proxy/graphql.client.js';
// Mock only the WebSocket client creation, not the Apollo Client error handling
vi.mock('graphql-ws', () => ({
@@ -87,7 +87,8 @@ describe('MothershipGraphqlClientService', () => {
{
description: 'malformed GraphQL error with API key message',
error: {
message: '"error" message expects the \'payload\' property to be an array of GraphQL errors, but got "API Key Invalid with error No user found"',
message:
'"error" message expects the \'payload\' property to be an array of GraphQL errors, but got "API Key Invalid with error No user found"',
},
expected: true,
},
@@ -127,7 +128,7 @@ describe('MothershipGraphqlClientService', () => {
// Since we're not mocking Apollo Client, this will create a real client
// We just want to verify the state check works
const client = service.getClient();
// The client should either be null (if not created yet) or an Apollo client instance
// The key is that it doesn't throw an error when state is valid
expect(() => service.getClient()).not.toThrow();
@@ -157,4 +158,4 @@ describe('MothershipGraphqlClientService', () => {
expect(service.mothershipGraphqlLink).toBe('https://mothership.unraid.net/ws');
});
});
});
});

View File

@@ -4,8 +4,8 @@ import type { Mock } from 'vitest';
import { URL_TYPE } from '@unraid/shared/network.model.js';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ConfigType } from '../model/connect-config.model.js';
import { UrlResolverService } from '../service/url-resolver.service.js';
import { ConfigType } from '../config/connect.config.js';
import { UrlResolverService } from '../network/url-resolver.service.js';
interface PortTestParams {
httpPort: number;

View File

@@ -10,8 +10,8 @@ import { parse as parseIni } from 'ini';
import { isEqual } from 'lodash-es';
import { bufferTime } from 'rxjs/operators';
import type { MyServersConfig as LegacyConfig } from '../model/my-servers-config.model.js';
import { ConfigType, MyServersConfig } from '../model/connect-config.model.js';
import type { MyServersConfig as LegacyConfig } from './my-servers.config.js';
import { ConfigType, MyServersConfig } from './connect.config.js';
@Injectable()
export class ConnectConfigPersister implements OnModuleInit, OnModuleDestroy {

View File

@@ -3,7 +3,7 @@ import { ConfigService } from '@nestjs/config';
import { OnEvent } from '@nestjs/event-emitter';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConfigType, emptyMyServersConfig, MyServersConfig } from '../model/connect-config.model.js';
import { ConfigType, emptyMyServersConfig, MyServersConfig } from './connect.config.js';
@Injectable()
export class ConnectConfigService {

View File

@@ -1,7 +1,6 @@
import { UsePipes, ValidationPipe } from '@nestjs/common';
import { registerAs } from '@nestjs/config';
import { Field, InputType, ObjectType } from '@nestjs/graphql';
import { ValidateIf } from 'class-validator';
import { URL_TYPE } from '@unraid/shared/network.model.js';
import { plainToInstance } from 'class-transformer';
@@ -14,10 +13,9 @@ import {
IsOptional,
IsString,
Matches,
ValidateIf,
} from 'class-validator';
import { ConnectDemoConfig } from './config.demo.js';
export enum MinigraphStatus {
PRE_INIT = 'PRE_INIT',
CONNECTING = 'CONNECTING',
@@ -180,7 +178,7 @@ export const makeDisabledDynamicRemoteAccessState = (): DynamicRemoteAccessState
allowedUrl: null,
});
export type ConnectConfig = ConnectDemoConfig & {
export type ConnectConfig = {
mothership: ConnectionMetadata;
dynamicRemoteAccess: DynamicRemoteAccessState;
config: MyServersConfig;
@@ -204,7 +202,6 @@ export const emptyMyServersConfig = (): MyServersConfig => ({
});
export const configFeature = registerAs<ConnectConfig>('connect', () => ({
demo: 'hello.unraider',
mothership: plainToInstance(ConnectionMetadata, {
status: MinigraphStatus.PRE_INIT,
}),

View File

@@ -1,6 +1,6 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { MinigraphStatus } from './my-servers-config.model.js';
import { MinigraphStatus } from '../config/my-servers.config.js';
@ObjectType()
export class ApiKeyResponse {

View File

@@ -7,10 +7,13 @@ import {
UsePermissions,
} from '@unraid/shared/use-permissions.directive.js';
import { Cloud } from '../model/cloud.model.js';
import { CloudService } from '../service/cloud.service.js';
import { NetworkService } from '../service/network.service.js';
import { NetworkService } from '../network/network.service.js';
import { Cloud } from './cloud.model.js';
import { CloudService } from './cloud.service.js';
/**
* Exposes details about the connection to the Unraid Connect cloud.
*/
@Resolver(() => Cloud)
export class CloudResolver {
constructor(

View File

@@ -7,11 +7,11 @@ import { got, HTTPError, TimeoutError } from 'got';
import ip from 'ip';
import NodeCache from 'node-cache';
import { ConfigType, MinigraphStatus } from '../config/connect.config.js';
import { ConnectConfigService } from '../config/connect.config.service.js';
import { ONE_HOUR_SECS, ONE_MINUTE_SECS } from '../helper/generic-consts.js';
import { CloudResponse, MinigraphqlResponse } from '../model/cloud.model.js';
import { ConfigType, MinigraphStatus } from '../model/connect-config.model.js';
import { ConnectConfigService } from './connect-config.service.js';
import { MothershipConnectionService } from './connection.service.js';
import { MothershipConnectionService } from '../mothership-proxy/connection.service.js';
import { CloudResponse, MinigraphqlResponse } from './cloud.model.js';
interface CacheSchema {
cloudIp: string;
@@ -38,6 +38,11 @@ const createGotOptions = (apiVersion: string, apiKey: string) => ({
});
const isHttpError = (error: unknown): error is HTTPError => error instanceof HTTPError;
/**
* Cloud connection service.
*
* Checks connection status to the cloud infrastructure supporting Unraid Connect.
*/
@Injectable()
export class CloudService {
static cache = new NodeCache() as TypedCache<CacheSchema>;

View File

@@ -3,11 +3,11 @@ import { SchedulerRegistry } from '@nestjs/schedule';
import { isDefined } from 'class-validator';
import { MinigraphStatus } from '../config/connect.config.js';
import { ONE_MINUTE_MS, THREE_MINUTES_MS } from '../helper/generic-consts.js';
import { MinigraphStatus } from '../model/connect-config.model.js';
import { MothershipConnectionService } from '../service/connection.service.js';
import { DynamicRemoteAccessService } from '../service/dynamic-remote-access.service.js';
import { MothershipSubscriptionHandler } from '../service/mothership-subscription.handler.js';
import { MothershipConnectionService } from '../mothership-proxy/connection.service.js';
import { MothershipSubscriptionHandler } from '../mothership-proxy/mothership-subscription.handler.js';
import { DynamicRemoteAccessService } from '../remote-access/dynamic-remote-access.service.js';
@Injectable()
export class TimeoutCheckerJob {

View File

@@ -1,10 +1,10 @@
import { Inject, Logger, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { configFeature } from './model/connect-config.model.js';
import { ConnectModule } from './module/connect.module.js';
import { MothershipModule } from './module/mothership.module.js';
import { ConnectConfigPersister } from './service/config.persistence.js';
import { ConnectConfigPersister } from './config/config.persistence.js';
import { configFeature } from './config/connect.config.js';
import { MothershipModule } from './mothership-proxy/mothership.module.js';
import { ConnectModule } from './unraid-connect/connect.module.js';
export const adapter = 'nestjs';

View File

@@ -9,8 +9,19 @@ import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';
import { getMainDefinition } from '@apollo/client/utilities/index.js';
import { createClient } from 'graphql-ws';
import { ConnectApiKeyService } from './connect-api-key.service.js';
import { ConnectApiKeyService } from '../authn/connect-api-key.service.js';
/**
* Internal GraphQL "RPC" client.
*
* Unfortunately, there's no simple way to make perform internal gql operations that go through
* all of the validations, filters, authorization, etc. in our setup.
*
* The simplest and most maintainable solution, unfortunately, is to maintain an actual graphql client
* that queries our own graphql server.
*
* This service handles the lifecycle and construction of that client.
*/
@Injectable()
export class InternalClientService {
constructor(

View File

@@ -1,6 +0,0 @@
import { Field } from '@nestjs/graphql';
export class ConnectDemoConfig {
@Field(() => String)
demo!: string;
}

View File

@@ -1,29 +0,0 @@
import { Module } from '@nestjs/common';
import { MothershipHandler } from '../event-handler/mothership.handler.js';
import { TimeoutCheckerJob } from '../job/timeout-checker.job.js';
import { CloudResolver } from '../resolver/cloud.resolver.js';
import { CloudService } from '../service/cloud.service.js';
import { ConnectApiKeyService } from '../service/connect-api-key.service.js';
import { MothershipConnectionService } from '../service/connection.service.js';
import { MothershipGraphqlClientService } from '../service/graphql.client.js';
import { InternalClientService } from '../service/internal.client.js';
import { MothershipSubscriptionHandler } from '../service/mothership-subscription.handler.js';
import { RemoteAccessModule } from './remote-access.module.js';
@Module({
imports: [RemoteAccessModule],
providers: [
ConnectApiKeyService,
MothershipConnectionService,
MothershipGraphqlClientService,
InternalClientService,
MothershipHandler,
MothershipSubscriptionHandler,
TimeoutCheckerJob,
CloudService,
CloudResolver,
],
exports: [],
})
export class MothershipModule {}

View File

@@ -1,19 +0,0 @@
import { Module } from '@nestjs/common';
import { WanAccessEventHandler } from '../event-handler/wan-access.handler.js';
import { DynamicRemoteAccessService } from '../service/dynamic-remote-access.service.js';
import { StaticRemoteAccessService } from '../service/static-remote-access.service.js';
import { UpnpRemoteAccessService } from '../service/upnp-remote-access.service.js';
import { SystemModule } from './system.module.js';
@Module({
imports: [SystemModule],
providers: [
DynamicRemoteAccessService,
StaticRemoteAccessService,
UpnpRemoteAccessService,
WanAccessEventHandler,
],
exports: [DynamicRemoteAccessService, SystemModule],
})
export class RemoteAccessModule {}

View File

@@ -6,8 +6,8 @@ import type { OutgoingHttpHeaders } from 'node:http2';
import { Subscription } from 'rxjs';
import { bufferTime, filter } from 'rxjs/operators';
import { ConnectionMetadata, MinigraphStatus, MyServersConfig } from '../config/connect.config.js';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConnectionMetadata, MinigraphStatus, MyServersConfig } from '../model/connect-config.model.js';
interface MothershipWebsocketHeaders extends OutgoingHttpHeaders {
'x-api-key': string;

View File

@@ -15,11 +15,11 @@ import { GraphQLWsLink } from '@apollo/client/link/subscriptions/index.js';
import { Client, createClient } from 'graphql-ws';
import { WebSocket } from 'ws';
import { MinigraphStatus } from '../config/connect.config.js';
import { RemoteGraphQlEventType } from '../graphql/generated/client/graphql.js';
import { SEND_REMOTE_QUERY_RESPONSE } from '../graphql/remote-response.js';
import { buildDelayFunction } from '../helper/delay-function.js';
import { EVENTS } from '../helper/nest-tokens.js';
import { MinigraphStatus } from '../model/connect-config.model.js';
import { MothershipConnectionService } from './connection.service.js';
const FIVE_MINUTES_MS = 5 * 60 * 1000;
@@ -184,7 +184,13 @@ export class MothershipGraphqlClientService implements OnModuleInit, OnModuleDes
* Check if an error is an invalid API key error
*/
private isInvalidApiKeyError(error: unknown): boolean {
return typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string' && error.message.includes('API Key Invalid');
return (
typeof error === 'object' &&
error !== null &&
'message' in error &&
typeof error.message === 'string' &&
error.message.includes('API Key Invalid')
);
}
/**

View File

@@ -12,9 +12,9 @@ import {
import { useFragment } from '../graphql/generated/client/index.js';
import { SEND_REMOTE_QUERY_RESPONSE } from '../graphql/remote-response.js';
import { parseGraphQLQuery } from '../helper/parse-graphql.js';
import { InternalClientService } from '../internal-rpc/internal.client.js';
import { MothershipConnectionService } from './connection.service.js';
import { MothershipGraphqlClientService } from './graphql.client.js';
import { InternalClientService } from './internal.client.js';
type SubscriptionProxy = {
sha256: string;

View File

@@ -3,12 +3,12 @@ import { OnEvent } from '@nestjs/event-emitter';
import { PubSub } from 'graphql-subscriptions';
import { MinigraphStatus } from '../config/connect.config.js';
import { TimeoutCheckerJob } from '../connection-status/timeout-checker.job.js';
import { EVENTS, GRAPHQL_PUBSUB_CHANNEL, GRAPHQL_PUBSUB_TOKEN } from '../helper/nest-tokens.js';
import { TimeoutCheckerJob } from '../job/timeout-checker.job.js';
import { MinigraphStatus } from '../model/connect-config.model.js';
import { MothershipConnectionService } from '../service/connection.service.js';
import { MothershipGraphqlClientService } from '../service/graphql.client.js';
import { MothershipSubscriptionHandler } from '../service/mothership-subscription.handler.js';
import { MothershipConnectionService } from './connection.service.js';
import { MothershipGraphqlClientService } from './graphql.client.js';
import { MothershipSubscriptionHandler } from './mothership-subscription.handler.js';
@Injectable()
export class MothershipHandler implements OnModuleDestroy {

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { ConnectApiKeyService } from '../authn/connect-api-key.service.js';
import { CloudResolver } from '../connection-status/cloud.resolver.js';
import { CloudService } from '../connection-status/cloud.service.js';
import { TimeoutCheckerJob } from '../connection-status/timeout-checker.job.js';
import { InternalClientService } from '../internal-rpc/internal.client.js';
import { RemoteAccessModule } from '../remote-access/remote-access.module.js';
import { MothershipConnectionService } from './connection.service.js';
import { MothershipGraphqlClientService } from './graphql.client.js';
import { MothershipSubscriptionHandler } from './mothership-subscription.handler.js';
import { MothershipHandler } from './mothership.events.js';
@Module({
imports: [RemoteAccessModule],
providers: [
ConnectApiKeyService,
MothershipConnectionService,
MothershipGraphqlClientService,
InternalClientService,
MothershipHandler,
MothershipSubscriptionHandler,
TimeoutCheckerJob,
CloudService,
CloudResolver,
],
exports: [],
})
export class MothershipModule {}

View File

@@ -1,12 +1,12 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { NetworkResolver } from '../resolver/network.resolver.js';
import { ConnectConfigService } from '../service/connect-config.service.js';
import { DnsService } from '../service/dns.service.js';
import { NetworkService } from '../service/network.service.js';
import { UpnpService } from '../service/upnp.service.js';
import { UrlResolverService } from '../service/url-resolver.service.js';
import { ConnectConfigService } from '../config/connect.config.service.js';
import { DnsService } from './dns.service.js';
import { NetworkResolver } from './network.resolver.js';
import { NetworkService } from './network.service.js';
import { UpnpService } from './upnp.service.js';
import { UrlResolverService } from './url-resolver.service.js';
@Module({
imports: [ConfigModule],
@@ -27,4 +27,4 @@ import { UrlResolverService } from '../service/url-resolver.service.js';
ConnectConfigService,
],
})
export class SystemModule {}
export class NetworkModule {}

View File

@@ -8,8 +8,8 @@ import {
UsePermissions,
} from '@unraid/shared/use-permissions.directive.js';
import { Network } from '../model/connect.model.js';
import { UrlResolverService } from '../service/url-resolver.service.js';
import { Network } from '../unraid-connect/connect.model.js';
import { UrlResolverService } from './url-resolver.service.js';
@Resolver(() => Network)
export class NetworkResolver {

View File

@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { NginxService } from '@unraid/shared/services/nginx.js';
import { NGINX_SERVICE_TOKEN } from '@unraid/shared/tokens.js';
import { ConnectConfigService } from './connect-config.service.js';
import { ConnectConfigService } from '../config/connect.config.service.js';
import { DnsService } from './dns.service.js';
import { UrlResolverService } from './url-resolver.service.js';

View File

@@ -6,9 +6,9 @@ import { Client, Mapping } from '@runonflux/nat-upnp';
import { UPNP_CLIENT_TOKEN } from '@unraid/shared/tokens.js';
import { isDefined } from 'class-validator';
import { ConfigType } from '../config/connect.config.js';
import { ONE_HOUR_SECS } from '../helper/generic-consts.js';
import { UPNP_RENEWAL_JOB_TOKEN } from '../helper/nest-tokens.js';
import { ConfigType } from '../model/connect-config.model.js';
@Injectable()
export class UpnpService implements OnModuleDestroy {

View File

@@ -4,7 +4,7 @@ import { ConfigService } from '@nestjs/config';
import { URL_TYPE } from '@unraid/shared/network.model.js';
import { makeSafeRunner } from '@unraid/shared/util/processing.js';
import { ConfigType } from '../model/connect-config.model.js';
import { ConfigType } from '../config/connect.config.js';
/**
* Represents a Fully Qualified Domain Name (FQDN) entry in the nginx configuration.

View File

@@ -2,9 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { OnEvent } from '@nestjs/event-emitter';
import { ConfigType } from '../config/connect.config.js';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConfigType } from '../model/connect-config.model.js';
import { NetworkService } from '../service/network.service.js';
import { NetworkService } from './network.service.js';
@Injectable()
export class WanAccessEventHandler {

View File

@@ -4,15 +4,25 @@ This directory contains the core source code for the Unraid Connect API plugin,
## Structure
- **index.ts**: Main entry, conforming to the `nestjs` API plugin schema.
- **module/**: NestJS modules. Organizes concerns. Also configures the dependency injection contexts.
- **service/**: Business logic & implementation.
- **model/**: TypeScript and GraphQL models, dto's, and types.
- **resolver/**: GraphQL resolvers.
- **event-handler/**: Event-driven handlers.
- **job/**: Background jobs (e.g., connection timeout checker).
- **helper/**: Utility functions and constants.
- **authn/**: Authentication services.
- **config/**: Configuration management, persistence, and settings.
- **connection-status/**: Connection state monitoring and status tracking.
- **graphql/**: GraphQL request definitions and generated client code.
- **test/**: Vitest-based unit and integration tests for services.
- **helper/**: Utility functions and constants.
- **internal-rpc/**: Internal RPC communication services.
- **mothership-proxy/**: Mothership server proxy and communication.
- **network/**: Network services including UPnP, DNS, URL resolution, and WAN access.
- **remote-access/**: Remote access services (static, dynamic, UPnP).
- **unraid-connect/**: Core Unraid Connect functionality and settings.
- **\_\_test\_\_/**: Vitest-based unit and integration tests.
Each feature directory follows a consistent pattern:
- `*.module.ts`: NestJS module definition
- `*.service.ts`: Business logic implementation
- `*.resolver.ts`: GraphQL resolvers
- `*.model.ts`: TypeScript and GraphQL models, DTOs, and types
- `*.events.ts`: Event handlers for event-driven operations
- `*.config.ts`: Configuration definitions
## Usage
This package is intended to be used as a NestJS plugin/module. Import `ApiModule` from `index.ts` and add it to your NestJS app's module imports.
@@ -29,8 +39,9 @@ export class AppModule {}
## Development
- Install dependencies from the monorepo root: `pnpm install`
- Build: `pnpm run build` (from the package root)
- Codegen (GraphQL): `npm run codegen`
- Tests: `vitest` (see `test/` for examples)
- Codegen (GraphQL): `pnpm run codegen`
- Tests: `vitest` (see `__test__/` for examples)
- Format: `pnpm run format` to format all files in project
## Notes
- Designed for Unraid server environments.

View File

@@ -3,14 +3,14 @@ import { ConfigService } from '@nestjs/config';
import { URL_TYPE } from '@unraid/shared/network.model.js';
import { ONE_MINUTE_MS } from '../helper/generic-consts.js';
import {
AccessUrlObject,
ConfigType,
DynamicRemoteAccessState,
DynamicRemoteAccessType,
makeDisabledDynamicRemoteAccessState,
} from '../model/connect-config.model.js';
} from '../config/connect.config.js';
import { ONE_MINUTE_MS } from '../helper/generic-consts.js';
import { StaticRemoteAccessService } from './static-remote-access.service.js';
import { UpnpRemoteAccessService } from './upnp-remote-access.service.js';

View File

@@ -0,0 +1,19 @@
import { Module } from '@nestjs/common';
import { NetworkModule } from '../network/network.module.js';
import { WanAccessEventHandler } from '../network/wan-access.events.js';
import { DynamicRemoteAccessService } from './dynamic-remote-access.service.js';
import { StaticRemoteAccessService } from './static-remote-access.service.js';
import { UpnpRemoteAccessService } from './upnp-remote-access.service.js';
@Module({
imports: [NetworkModule],
providers: [
DynamicRemoteAccessService,
StaticRemoteAccessService,
UpnpRemoteAccessService,
WanAccessEventHandler,
],
exports: [DynamicRemoteAccessService, NetworkModule],
})
export class RemoteAccessModule {}

View File

@@ -2,9 +2,9 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ConfigType, DynamicRemoteAccessType, MyServersConfig } from '../config/connect.config.js';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConfigType, DynamicRemoteAccessType, MyServersConfig } from '../model/connect-config.model.js';
import { AccessUrl, UrlResolverService } from './url-resolver.service.js';
import { AccessUrl, UrlResolverService } from '../network/url-resolver.service.js';
@Injectable()
export class StaticRemoteAccessService {

View File

@@ -2,10 +2,10 @@ import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { ConfigType } from '../config/connect.config.js';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConfigType } from '../model/connect-config.model.js';
import { UpnpService } from './upnp.service.js';
import { UrlResolverService } from './url-resolver.service.js';
import { UpnpService } from '../network/upnp.service.js';
import { UrlResolverService } from '../network/url-resolver.service.js';
@Injectable()
export class UpnpRemoteAccessService {

View File

@@ -11,6 +11,7 @@ import { GraphQLJSON } from 'graphql-scalars';
import { AuthActionVerb, AuthPossession } from 'nest-authz';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConnectSettingsService } from './connect-settings.service.js';
import {
AllowedOriginInput,
ConnectSettings,
@@ -20,8 +21,7 @@ import {
EnableDynamicRemoteAccessInput,
RemoteAccess,
SetupRemoteAccessInput,
} from '../model/connect.model.js';
import { ConnectSettingsService } from '../service/connect-settings.service.js';
} from './connect.model.js';
@Resolver(() => ConnectSettings)
export class ConnectSettingsResolver {

View File

@@ -20,13 +20,13 @@ import type {
EnableDynamicRemoteAccessInput,
RemoteAccess,
SetupRemoteAccessInput,
} from '../model/connect.model.js';
} from './connect.model.js';
import { ConnectApiKeyService } from '../authn/connect-api-key.service.js';
import { ConfigType, MyServersConfig } from '../config/connect.config.js';
import { EVENTS } from '../helper/nest-tokens.js';
import { ConfigType, MyServersConfig } from '../model/connect-config.model.js';
import { DynamicRemoteAccessType, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from '../model/connect.model.js';
import { ConnectApiKeyService } from './connect-api-key.service.js';
import { DynamicRemoteAccessService } from './dynamic-remote-access.service.js';
import { NetworkService } from './network.service.js';
import { NetworkService } from '../network/network.service.js';
import { DynamicRemoteAccessService } from '../remote-access/dynamic-remote-access.service.js';
import { DynamicRemoteAccessType, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from './connect.model.js';
declare module '@unraid/shared/services/user-settings.js' {
interface UserSettings {

View File

@@ -3,13 +3,13 @@ import { ConfigModule } from '@nestjs/config';
import { UserSettingsModule } from '@unraid/shared/services/user-settings.js';
import { ConnectLoginHandler } from '../event-handler/connect-login.handler.js';
import { ConnectSettingsResolver } from '../resolver/connect-settings.resolver.js';
import { ConnectResolver } from '../resolver/connect.resolver.js';
import { ConnectApiKeyService } from '../service/connect-api-key.service.js';
import { ConnectConfigService } from '../service/connect-config.service.js';
import { ConnectSettingsService } from '../service/connect-settings.service.js';
import { RemoteAccessModule } from './remote-access.module.js';
import { ConnectApiKeyService } from '../authn/connect-api-key.service.js';
import { ConnectLoginHandler } from '../authn/connect-login.events.js';
import { ConnectConfigService } from '../config/connect.config.service.js';
import { RemoteAccessModule } from '../remote-access/remote-access.module.js';
import { ConnectSettingsResolver } from './connect-settings.resolver.js';
import { ConnectSettingsService } from './connect-settings.service.js';
import { ConnectResolver } from './connect.resolver.js';
@Module({
imports: [RemoteAccessModule, ConfigModule, UserSettingsModule],

View File

@@ -9,8 +9,8 @@ import {
UsePermissions,
} from '@unraid/shared/use-permissions.directive.js';
import { ConfigType, ConnectConfig, DynamicRemoteAccessType } from '../model/connect-config.model.js';
import { Connect, ConnectSettings, DynamicRemoteAccessStatus } from '../model/connect.model.js';
import { ConfigType, ConnectConfig, DynamicRemoteAccessType } from '../config/connect.config.js';
import { Connect, ConnectSettings, DynamicRemoteAccessStatus } from './connect.model.js';
@Resolver(() => Connect)
export class ConnectResolver {