mirror of
https://github.com/unraid/api.git
synced 2026-01-01 14:10:10 -06:00
feat: enable sandbox with developer command
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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=""
|
||||||
|
|||||||
@@ -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(', ');
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
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,
|
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',
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user