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" version="3.11.0"
extraOrigins="https://google.com,https://test.com" extraOrigins="https://google.com,https://test.com"
[local] [local]
sandbox=""
[remote] [remote]
wanaccess="yes" wanaccess="yes"
wanport="8443" wanport="8443"

View File

@@ -2,6 +2,7 @@
version="3.11.0" version="3.11.0"
extraOrigins="https://google.com,https://test.com" extraOrigins="https://google.com,https://test.com"
[local] [local]
sandbox=""
[remote] [remote]
wanaccess="yes" wanaccess="yes"
wanport="8443" wanport="8443"
@@ -19,5 +20,5 @@ dynamicRemoteAccessType="DISABLED"
ssoSubIds="" 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" 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] [connectionStatus]
minigraph="PRE_INIT" minigraph="ERROR_RETRYING"
upnpStatus="" upnpStatus=""

View File

@@ -1,4 +1,5 @@
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import merge from 'lodash/merge';
import { getAllowedOrigins } from '@app/common/allowed-origins'; import { getAllowedOrigins } from '@app/common/allowed-origins';
import { initialState } from '@app/store/modules/config'; import { initialState } from '@app/store/modules/config';
@@ -23,14 +24,10 @@ export const getWriteableConfig = <T extends ConfigType>(
const defaultConfig = schema.parse(initialState); const defaultConfig = schema.parse(initialState);
// Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory` // Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory`
const mergedConfig = { const mergedConfig = merge<
...defaultConfig, MyServersConfig,
...config, T extends 'memory' ? MyServersConfigMemory : MyServersConfig
remote: { >(defaultConfig, config);
...defaultConfig.remote,
...config.remote,
},
} as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
if (mode === 'memory') { if (mode === 'memory') {
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', '); (mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');

View File

@@ -49,7 +49,9 @@ export const initialState: SliceState = {
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED, dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
ssoSubIds: '', ssoSubIds: '',
}, },
local: {}, local: {
sandbox: 'no'
},
api: { api: {
extraOrigins: '', extraOrigins: '',
version: '', 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 // Base config schema
export const MyServersConfigSchema = z 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 { ListSSOUserCommand } from '@app/unraid-api/cli/sso/list-sso-user.command';
import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions'; import { AddApiKeyQuestionSet } from '@app/unraid-api/cli/apikey/add-api-key.questions';
import { ApiKeyService } from '@app/unraid-api/auth/api-key.service'; 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({ @Module({
providers: [ providers: [
@@ -43,6 +45,8 @@ import { ApiKeyService } from '@app/unraid-api/auth/api-key.service';
ValidateTokenCommand, ValidateTokenCommand,
LogsCommand, LogsCommand,
ConfigCommand, ConfigCommand,
DeveloperCommand,
DeveloperQuestions
], ],
}) })
export class CliModule {} 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, UUIDResolver,
} from 'graphql-scalars'; } from 'graphql-scalars';
import { GRAPHQL_INTROSPECTION } from '@app/environment';
import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long'; import { GraphQLLong } from '@app/graphql/resolvers/graphql-type-long';
import { typeDefs } from '@app/graphql/schema/index'; import { typeDefs } from '@app/graphql/schema/index';
import { idPrefixPlugin } from '@app/unraid-api/graph/id-prefix-plugin'; 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 { sandboxPlugin } from './sandbox-plugin';
import { ServicesResolver } from './services/services.resolver'; import { ServicesResolver } from './services/services.resolver';
import { SharesResolver } from './shares/shares.resolver'; import { SharesResolver } from './shares/shares.resolver';
import { getters } from '@app/store/index';
@Module({ @Module({
imports: [ imports: [
ResolversModule, ResolversModule,
GraphQLModule.forRoot<ApolloDriverConfig>({ GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver, driver: ApolloDriver,
introspection: GRAPHQL_INTROSPECTION ? true : false, introspection: getters.config().local?.sandbox === 'yes' ? true : false,
playground: false,
context: ({ req, connectionParams, extra }) => ({ context: ({ req, connectionParams, extra }) => ({
req, req,
connectionParams, connectionParams,
extra, extra,
}), }),
playground: false, plugins: [sandboxPlugin, idPrefixPlugin],
plugins: GRAPHQL_INTROSPECTION ? [sandboxPlugin, idPrefixPlugin] : [idPrefixPlugin],
subscriptions: { subscriptions: {
'graphql-ws': { 'graphql-ws': {
path: '/graphql', path: '/graphql',

View File

@@ -28,13 +28,43 @@ const preconditionFailed = (preconditionName: string) => {
throw new HttpException(`Precondition failed: ${preconditionName} `, HttpStatus.PRECONDITION_FAILED); 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. * Renders the sandbox page for the GraphQL server with Apollo Server landing page configuration.
* *
* @param service - The GraphQL server context object * @param service - The GraphQL server context object
* @returns Promise that resolves to an Apollo `LandingPage`, or throws a precondition failed error * @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. * @throws {Error} When downstream plugin components from apollo are unavailable. This should never happen.
* *
* @remarks * @remarks
* This function configures and renders the Apollo Server landing page with: * This function configures and renders the Apollo Server landing page with:
* - Disabled footer * - Disabled footer
@@ -44,33 +74,22 @@ const preconditionFailed = (preconditionName: string) => {
*/ */
async function renderSandboxPage(service: GraphQLServerContext) { async function renderSandboxPage(service: GraphQLServerContext) {
const { getters } = await import('@app/store'); const { getters } = await import('@app/store');
const { ApolloServerPluginLandingPageLocalDefault } = await import( const sandbox = getters.config().local.sandbox === 'yes';
'@apollo/server/plugin/landingPage/default' const csrfToken = getters.emhttp().var.csrfToken;
); const plugin = await getPluginBasedOnSandbox(sandbox, csrfToken);
const plugin = ApolloServerPluginLandingPageLocalDefault({
footer: false,
includeCookies: true,
document: initialDocument,
embed: {
initialState: {
sharedHeaders: {
'x-csrf-token': getters.emhttp().var.csrfToken,
},
},
},
});
if (!plugin.serverWillStart) return preconditionFailed('serverWillStart'); if (!plugin.serverWillStart) return preconditionFailed('serverWillStart');
const serverListener = await plugin.serverWillStart(service); const serverListener = await plugin.serverWillStart(service);
if (!serverListener) return preconditionFailed('serverListener'); if (!serverListener) return preconditionFailed('serverListener');
if (!serverListener.renderLandingPage) return preconditionFailed('renderLandingPage'); if (!serverListener.renderLandingPage) return preconditionFailed('renderLandingPage');
return serverListener.renderLandingPage(); return serverListener.renderLandingPage();
} }
/** /**
* Apollo plugin to render the GraphQL Sandbox page on-demand based on current server state. * Apollo plugin to render the GraphQL Sandbox page on-demand based on current server state.
* *
* Usually, the `ApolloServerPluginLandingPageLocalDefault` plugin configures its * Usually, the `ApolloServerPluginLandingPageLocalDefault` plugin configures its
* parameters once, during server startup. This plugin defers the configuration * parameters once, during server startup. This plugin defers the configuration
* and rendering to request-time instead of server startup. * and rendering to request-time instead of server startup.