mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: enable sandbox with developer command
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
version="3.11.0"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
sandbox=""
|
||||
[remote]
|
||||
wanaccess="yes"
|
||||
wanport="8443"
|
||||
|
||||
@@ -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=""
|
||||
|
||||
@@ -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(', ');
|
||||
|
||||
@@ -49,7 +49,9 @@ export const initialState: SliceState = {
|
||||
dynamicRemoteAccessType: DynamicRemoteAccessType.DISABLED,
|
||||
ssoSubIds: '',
|
||||
},
|
||||
local: {},
|
||||
local: {
|
||||
sandbox: 'no'
|
||||
},
|
||||
api: {
|
||||
extraOrigins: '',
|
||||
version: '',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
40
api/src/unraid-api/cli/developer/developer.command.ts
Normal file
40
api/src/unraid-api/cli/developer/developer.command.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
26
api/src/unraid-api/cli/developer/developer.questions.ts
Normal file
26
api/src/unraid-api/cli/developer/developer.questions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user