Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/dev/Unraid.net/myservers.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
version="3.11.0"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"
[remote]
wanaccess="yes"
wanport="8443"
Expand Down
1 change: 1 addition & 0 deletions api/dev/states/myservers.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
version="3.11.0"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox="yes"
[remote]
wanaccess="yes"
wanport="8443"
Expand Down
16 changes: 12 additions & 4 deletions api/src/__test__/core/utils/files/config-file-normalizer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
"extraOrigins": "",
"version": "",
},
"local": {},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"apikey": "",
Expand Down Expand Up @@ -49,7 +51,9 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
Expand Down Expand Up @@ -88,7 +92,9 @@ test('it creates a FLASH config with OPTIONAL values', () => {
"extraOrigins": "myextra.origins",
"version": "",
},
"local": {},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"apikey": "",
Expand Down Expand Up @@ -129,7 +135,9 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
"minigraph": "PRE_INIT",
"upnpStatus": "Turned On",
},
"local": {},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ exports[`Before init returns default values for all fields 1`] = `
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {},
"local": {
"sandbox": "no",
},
"nodeEnv": "test",
"remote": {
"accesstoken": "",
Expand Down
8 changes: 6 additions & 2 deletions api/src/__test__/store/modules/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ test('After init returns values from cfg file for all fields', async () => {
minigraph: 'PRE_INIT',
upnpStatus: '',
},
local: {},
local: {
sandbox: expect.any(String)
},
nodeEnv: 'test',
remote: {
accesstoken: '',
Expand Down Expand Up @@ -74,7 +76,9 @@ test('updateUserConfig merges in changes to current state', async () => {
minigraph: 'PRE_INIT',
upnpStatus: '',
},
local: {},
local: {
sandbox: expect.any(String)
},
nodeEnv: 'test',
remote: {
accesstoken: '',
Expand Down
13 changes: 5 additions & 8 deletions api/src/core/utils/files/config-file-normalizer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isEqual } from 'lodash-es';
import merge from 'lodash/merge';

import { getAllowedOrigins } from '@app/common/allowed-origins';
import { initialState } from '@app/store/modules/config';
Expand All @@ -23,14 +24,10 @@ export const getWriteableConfig = <T extends ConfigType>(

const defaultConfig = schema.parse(initialState);
// Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory`
const mergedConfig = {
...defaultConfig,
...config,
remote: {
...defaultConfig.remote,
...config.remote,
},
} as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
const mergedConfig = merge<
MyServersConfig,
T extends 'memory' ? MyServersConfigMemory : MyServersConfig
>(defaultConfig, config);

if (mode === 'memory') {
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');
Expand Down
4 changes: 3 additions & 1 deletion api/src/store/modules/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ export const initialState: SliceState = {
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
ssoSubIds: '',
},
local: {},
local: {
sandbox: 'no'
},
api: {
extraOrigins: '',
version: '',
Expand Down
4 changes: 3 additions & 1 deletion api/src/types/my-servers-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ const RemoteConfigSchema = z.object({
),
});

const LocalConfigSchema = z.object({});
const LocalConfigSchema = z.object({
sandbox: z.enum(['yes', 'no']).default('no'),
});

// Base config schema
export const MyServersConfigSchema = z
Expand Down
4 changes: 4 additions & 0 deletions api/src/unraid-api/cli/cli.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { RemoveSSOUserQuestionSet } from '@app/unraid-api/cli/sso/remove-sso-use
import { ListSSOUserCommand } from '@app/unraid-api/cli/sso/list-sso-user.command';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service';
import { DeveloperCommand } from '@app/unraid-api/cli/developer/developer.command';
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions';

@Module({
providers: [
Expand All @@ -43,6 +45,8 @@ import { ApiKeyService } from '@app/unraid-api/auth/api-key.service';
ValidateTokenCommand,
LogsCommand,
ConfigCommand,
DeveloperCommand,
DeveloperQuestions
],
})
export class CliModule {}
45 changes: 45 additions & 0 deletions api/src/unraid-api/cli/developer/developer.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';

import { Command, CommandRunner, InquirerService } from 'nest-commander';

import { loadConfigFile, updateUserConfig } from '@app/store/modules/config';
import { writeConfigSync } from '@app/store/sync/config-disk-sync';
import { DeveloperQuestions } from '@app/unraid-api/cli/developer/developer.questions';
import { LogService } from '@app/unraid-api/cli/log.service';
import { RestartCommand } from '@app/unraid-api/cli/restart.command';

interface DeveloperOptions {
disclaimer: boolean;
sandbox: boolean;
}
@Injectable()
@Command({
name: 'developer',
description: 'Configure Developer Features for the API',
})
export class DeveloperCommand extends CommandRunner {
constructor(
private logger: LogService,
private readonly inquirerService: InquirerService,
private readonly restartCommand: RestartCommand
) {
super();
}
async run(_, options?: DeveloperOptions): Promise<void> {
options = await this.inquirerService.prompt(DeveloperQuestions.name, options);
if (!options.disclaimer) {
this.logger.warn('No changes made, disclaimer not accepted.');
process.exit(1);
}
const { store } = await import('@app/store');
await store.dispatch(loadConfigFile());
store.dispatch(updateUserConfig({ local: { sandbox: options.sandbox ? 'yes' : 'no' } }));
writeConfigSync('flash');

this.logger.info(
'Updated Developer Configuration - restart the API in 5 seconds to apply them...'
);
await new Promise((resolve) => setTimeout(resolve, 5000));
await this.restartCommand.run([]);
}
}
26 changes: 26 additions & 0 deletions api/src/unraid-api/cli/developer/developer.questions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Question, QuestionSet } from 'nest-commander';

@QuestionSet({ name: 'developer' })
export class DeveloperQuestions {
static name = 'developer';

@Question({
message: `Are you sure you wish to enable developer mode?
Currently this allows enabling the GraphQL sandbox on SERVER_URL/graphql.
`,
type: 'confirm',
name: 'disclaimer',
})
parseDisclaimer(val: boolean) {
return val;
}

@Question({
message: 'Do you wish to enable the sandbox?',
type: 'confirm',
name: 'sandbox',
})
parseSandbox(val: boolean) {
return val;
}
}
1 change: 1 addition & 0 deletions api/src/unraid-api/cli/sso/validate-token.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export class ValidateTokenCommand extends CommandRunner {
}
const possibleUserIds = configFile.remote.ssoSubIds.split(',');
if (possibleUserIds.includes(username)) {
this.logger.clear();
this.logger.info(JSON.stringify({ error: null, valid: true, username }));
process.exit(0);
} else {
Expand Down
8 changes: 4 additions & 4 deletions api/src/unraid-api/graph/graph.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
UUIDResolver,
} from 'graphql-scalars';

import { GRAPHQL_INTROSPECTION } from '@app/environment';
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long';
import { typeDefs } from '@app/graphql/schema/index';
import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin';
Expand All @@ -24,20 +23,21 @@ import { ResolversModule } from './resolvers/resolvers.module';
import { sandboxPlugin } from './sandbox-plugin';
import { ServicesResolver } from './services/services.resolver';
import { SharesResolver } from './shares/shares.resolver';
import { getters } from '@app/store/index';

@Module({
imports: [
ResolversModule,
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
introspection: GRAPHQL_INTROSPECTION ? true : false,
introspection: getters.config().local?.sandbox === 'yes' ? true : false,
playground: false,
context: ({ req, connectionParams, extra }) => ({
req,
connectionParams,
extra,
}),
playground: false,
plugins: GRAPHQL_INTROSPECTION ? [sandboxPlugin, idPrefixPlugin] : [idPrefixPlugin],
plugins: [sandboxPlugin, idPrefixPlugin],
subscriptions: {
'graphql-ws': {
path: '/graphql',
Expand Down
57 changes: 38 additions & 19 deletions api/src/unraid-api/graph/sandbox-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,43 @@ const preconditionFailed = (preconditionName: string) => {
throw new HttpException(`Precondition failed: ${preconditionName} `, HttpStatus.PRECONDITION_FAILED);
};

export const getPluginBasedOnSandbox = async (sandbox: boolean, csrfToken: string) => {
if (sandbox) {
const { ApolloServerPluginLandingPageLocalDefault } = await import(
'@apollo/server/plugin/landingPage/default'
);
const plugin = ApolloServerPluginLandingPageLocalDefault({
footer: false,
includeCookies: true,
document: initialDocument,
embed: {
initialState: {
sharedHeaders: {
'x-csrf-token': csrfToken,
},
},
},
});
return plugin;
} else {
const { ApolloServerPluginLandingPageProductionDefault } = await import(
'@apollo/server/plugin/landingPage/default'
);

const plugin = ApolloServerPluginLandingPageProductionDefault({
footer: false
});
return plugin;
}
};

/**
* Renders the sandbox page for the GraphQL server with Apollo Server landing page configuration.
*
*
* @param service - The GraphQL server context object
* @returns Promise that resolves to an Apollo `LandingPage`, or throws a precondition failed error
* @throws {Error} When downstream plugin components from apollo are unavailable. This should never happen.
*
*
* @remarks
* This function configures and renders the Apollo Server landing page with:
* - Disabled footer
Expand All @@ -44,33 +74,22 @@ const preconditionFailed = (preconditionName: string) => {
*/
async function renderSandboxPage(service: GraphQLServerContext) {
const { getters } = await import('@app/store');
const { ApolloServerPluginLandingPageLocalDefault } = await import(
'@apollo/server/plugin/landingPage/default'
);
const plugin = ApolloServerPluginLandingPageLocalDefault({
footer: false,
includeCookies: true,
document: initialDocument,
embed: {
initialState: {
sharedHeaders: {
'x-csrf-token': getters.emhttp().var.csrfToken,
},
},
},
});
const sandbox = getters.config().local.sandbox === 'yes';
const csrfToken = getters.emhttp().var.csrfToken;
const plugin = await getPluginBasedOnSandbox(sandbox, csrfToken);

if (!plugin.serverWillStart) return preconditionFailed('serverWillStart');
const serverListener = await plugin.serverWillStart(service);

if (!serverListener) return preconditionFailed('serverListener');
if (!serverListener.renderLandingPage) return preconditionFailed('renderLandingPage');

return serverListener.renderLandingPage();
}

/**
* Apollo plugin to render the GraphQL Sandbox page on-demand based on current server state.
*
*
* Usually, the `ApolloServerPluginLandingPageLocalDefault` plugin configures its
* parameters once, during server startup. This plugin defers the configuration
* and rendering to request-time instead of server startup.
Expand Down

This file was deleted.

Loading