feat: enable sandbox with developer command

This commit is contained in:
Eli Bosley
2025-01-29 14:41:03 -05:00
parent c4d731401c
commit 9c5e418872
10 changed files with 126 additions and 34 deletions

View File

@@ -2,6 +2,7 @@
version="3.11.0"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox=""
[remote]
wanaccess="yes"
wanport="8443"

View File

@@ -2,6 +2,7 @@
version="3.11.0"
extraOrigins="https://google.com,https://test.com"
[local]
sandbox=""
[remote]
wanaccess="yes"
wanport="8443"
@@ -19,5 +20,5 @@ dynamicRemoteAccessType="DISABLED"
ssoSubIds=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
[connectionStatus]
minigraph="PRE_INIT"
minigraph="ERROR_RETRYING"
upnpStatus=""

View File

@@ -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';
@@ -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(', ');

View File

@@ -49,7 +49,9 @@ export const initialState: SliceState = {
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
ssoSubIds: '',
},
local: {},
local: {
sandbox: 'no'
},
api: {
extraOrigins: '',
version: '',

View File

@@ -41,7 +41,9 @@ const RemoteConfigSchema = z.object({
),
});
const LocalConfigSchema = z.object({});
const LocalConfigSchema = z.object({
sandbox: z.string()
});
// Base config schema
export const MyServersConfigSchema = z

View File

@@ -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: [
@@ -43,6 +45,8 @@ import { ApiKeyService } from '@app/unraid-api/auth/api-key.service';
ValidateTokenCommand,
LogsCommand,
ConfigCommand,
DeveloperCommand,
DeveloperQuestions
],
})
export class CliModule {}

View File

@@ -0,0 +1,40 @@
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';
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
) {
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' } }));
console.log(store.getState().config.local.sandbox);
writeConfigSync('flash');
this.logger.info('Updated Developer Configuration');
}
}

View File

@@ -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;
}
}

View File

@@ -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';
@@ -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',

View File

@@ -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
@@ -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.