mirror of
https://github.com/unraid/api.git
synced 2026-02-05 15:39:05 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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';
|
||||
@@ -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 }
|
||||
);
|
||||
});
|
||||
@@ -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', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
}),
|
||||
@@ -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 {
|
||||
@@ -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(
|
||||
@@ -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>;
|
||||
@@ -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 {
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Field } from '@nestjs/graphql';
|
||||
|
||||
export class ConnectDemoConfig {
|
||||
@Field(() => String)
|
||||
demo!: string;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
@@ -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')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -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;
|
||||
@@ -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 {
|
||||
@@ -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 {}
|
||||
@@ -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 {}
|
||||
@@ -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 {
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
@@ -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.
|
||||
@@ -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 {
|
||||
@@ -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.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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 {
|
||||
@@ -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],
|
||||
@@ -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 {
|
||||
Reference in New Issue
Block a user