mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
feat: use zod to parse config
This commit is contained in:
@@ -10,72 +10,75 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
|
||||
const basicConfig = initialState;
|
||||
const config = getWriteableConfig(basicConfig, 'flash');
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
{
|
||||
"api": {
|
||||
"extraOrigins": "",
|
||||
"version": "",
|
||||
},
|
||||
"local": {},
|
||||
"notifier": {
|
||||
"apikey": "",
|
||||
},
|
||||
"remote": {
|
||||
"accesstoken": "",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
"email": "",
|
||||
"idtoken": "",
|
||||
"localApiKey": "",
|
||||
"refreshtoken": "",
|
||||
"regWizTime": "",
|
||||
"username": "",
|
||||
"wanaccess": "",
|
||||
"wanport": "",
|
||||
},
|
||||
"upc": {
|
||||
"apikey": "",
|
||||
},
|
||||
}
|
||||
`);
|
||||
{
|
||||
"api": {
|
||||
"extraOrigins": "",
|
||||
"version": "",
|
||||
},
|
||||
"local": {},
|
||||
"notifier": {
|
||||
"apikey": "",
|
||||
},
|
||||
"remote": {
|
||||
"accesstoken": "",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
"email": "",
|
||||
"idtoken": "",
|
||||
"localApiKey": "",
|
||||
"refreshtoken": "",
|
||||
"regWizTime": "",
|
||||
"upnpEnabled": "",
|
||||
"username": "",
|
||||
"wanaccess": "",
|
||||
"wanport": "",
|
||||
},
|
||||
"upc": {
|
||||
"apikey": "",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it creates a MEMORY config with NO OPTIONAL values', () => {
|
||||
const basicConfig = initialState;
|
||||
const config = getWriteableConfig(basicConfig, 'memory');
|
||||
expect(config).toMatchInlineSnapshot(`
|
||||
{
|
||||
"api": {
|
||||
"extraOrigins": "",
|
||||
"version": "",
|
||||
},
|
||||
"connectionStatus": {
|
||||
"minigraph": "PRE_INIT",
|
||||
},
|
||||
"local": {},
|
||||
"notifier": {
|
||||
"apikey": "",
|
||||
},
|
||||
"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",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
"email": "",
|
||||
"idtoken": "",
|
||||
"localApiKey": "",
|
||||
"refreshtoken": "",
|
||||
"regWizTime": "",
|
||||
"username": "",
|
||||
"wanaccess": "",
|
||||
"wanport": "",
|
||||
},
|
||||
"upc": {
|
||||
"apikey": "",
|
||||
},
|
||||
}
|
||||
`);
|
||||
{
|
||||
"api": {
|
||||
"extraOrigins": "",
|
||||
"version": "",
|
||||
},
|
||||
"connectionStatus": {
|
||||
"minigraph": "PRE_INIT",
|
||||
"upnpStatus": "",
|
||||
},
|
||||
"local": {},
|
||||
"notifier": {
|
||||
"apikey": "",
|
||||
},
|
||||
"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",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
"email": "",
|
||||
"idtoken": "",
|
||||
"localApiKey": "",
|
||||
"refreshtoken": "",
|
||||
"regWizTime": "",
|
||||
"upnpEnabled": "",
|
||||
"username": "",
|
||||
"wanaccess": "",
|
||||
"wanport": "",
|
||||
},
|
||||
"upc": {
|
||||
"apikey": "",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('it creates a FLASH config with OPTIONAL values', () => {
|
||||
|
||||
@@ -68,6 +68,7 @@ export const JWKS_LOCAL_PAYLOAD: JSONWebKeySet = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const OAUTH_BASE_URL =
|
||||
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
|
||||
export const OAUTH_CLIENT_ID = '53ci4o48gac8vq5jepubkjmo36';
|
||||
|
||||
@@ -1,95 +1,64 @@
|
||||
import { getAllowedOrigins } from '@app/common/allowed-origins';
|
||||
import { DynamicRemoteAccessType } from '@app/graphql/generated/api/types';
|
||||
import {
|
||||
type SliceState as ConfigSliceState,
|
||||
initialState,
|
||||
} from '@app/store/modules/config';
|
||||
import { type RecursivePartial } from '@app/types';
|
||||
import type {
|
||||
MyServersConfig,
|
||||
MyServersConfigMemory,
|
||||
} from '@app/types/my-servers-config';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export type ConfigType = 'flash' | 'memory';
|
||||
type ConfigObject<T> = T extends 'flash'
|
||||
? MyServersConfig
|
||||
: T extends 'memory'
|
||||
? MyServersConfigMemory
|
||||
: never;
|
||||
/**
|
||||
*
|
||||
* @param config Config to read from to create a new formatted server config to write
|
||||
* @param mode 'flash' or 'memory', changes what fields are included in the writeable payload
|
||||
* @returns
|
||||
*/
|
||||
|
||||
export const getWriteableConfig = <T extends ConfigType>(
|
||||
config: ConfigSliceState,
|
||||
mode: T
|
||||
): ConfigObject<T> => {
|
||||
// Get current state
|
||||
const { api, local, notifier, remote, upc, connectionStatus } = config;
|
||||
import { getAllowedOrigins } from '@app/common/allowed-origins';
|
||||
import { initialState } from '@app/store/modules/config';
|
||||
import {
|
||||
MyServersConfig,
|
||||
MyServersConfigMemory,
|
||||
MyServersConfigMemorySchema,
|
||||
MyServersConfigSchema,
|
||||
} from '@app/types/my-servers-config';
|
||||
|
||||
// Create new state
|
||||
|
||||
const newState: ConfigObject<T> = {
|
||||
api: {
|
||||
version: api?.version ?? initialState.api.version,
|
||||
extraOrigins: api?.extraOrigins ?? initialState.api.extraOrigins,
|
||||
},
|
||||
local: {},
|
||||
notifier: {
|
||||
apikey: notifier.apikey ?? initialState.notifier.apikey,
|
||||
},
|
||||
// Define ConfigType and ConfigObject
|
||||
export type ConfigType = 'flash' | 'memory';
|
||||
|
||||
/**
|
||||
* Get a writeable configuration based on the mode ('flash' or 'memory').
|
||||
*/
|
||||
export const getWriteableConfig = <T extends ConfigType>(
|
||||
config: T extends 'memory' ? MyServersConfigMemory : MyServersConfig,
|
||||
mode: T
|
||||
): T extends 'memory' ? MyServersConfigMemory : MyServersConfig => {
|
||||
const schema = mode === 'memory' ? MyServersConfigMemorySchema : MyServersConfigSchema;
|
||||
|
||||
const defaultConfig = schema.parse(initialState);
|
||||
// Use a type assertion for the mergedConfig to include `connectionStatus` only if `mode === 'memory`
|
||||
const mergedConfig = {
|
||||
...defaultConfig,
|
||||
...config,
|
||||
remote: {
|
||||
wanaccess: remote.wanaccess ?? initialState.remote.wanaccess,
|
||||
wanport: remote.wanport ?? initialState.remote.wanport,
|
||||
...(remote.upnpEnabled ? { upnpEnabled: remote.upnpEnabled } : {}),
|
||||
apikey: remote.apikey ?? initialState.remote.apikey,
|
||||
localApiKey: remote.localApiKey ?? initialState.remote.localApiKey,
|
||||
email: remote.email ?? initialState.remote.email,
|
||||
username: remote.username ?? initialState.remote.username,
|
||||
avatar: remote.avatar ?? initialState.remote.avatar,
|
||||
regWizTime: remote.regWizTime ?? initialState.remote.regWizTime,
|
||||
idtoken: remote.idtoken ?? initialState.remote.idtoken,
|
||||
accesstoken: remote.accesstoken ?? initialState.remote.accesstoken,
|
||||
refreshtoken:
|
||||
remote.refreshtoken ?? initialState.remote.refreshtoken,
|
||||
...(mode === 'memory'
|
||||
? {
|
||||
allowedOrigins:
|
||||
getAllowedOrigins().join(', ')
|
||||
}
|
||||
: {}),
|
||||
dynamicRemoteAccessType: remote.dynamicRemoteAccessType ?? DynamicRemoteAccessType.DISABLED,
|
||||
...defaultConfig.remote,
|
||||
...config.remote,
|
||||
},
|
||||
upc: {
|
||||
apikey: upc.apikey ?? initialState.upc.apikey,
|
||||
},
|
||||
...(mode === 'memory'
|
||||
? {
|
||||
connectionStatus: {
|
||||
minigraph:
|
||||
connectionStatus.minigraph ??
|
||||
initialState.connectionStatus.minigraph,
|
||||
...(connectionStatus.upnpStatus
|
||||
? { upnpStatus: connectionStatus.upnpStatus }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
} as ConfigObject<T>;
|
||||
return newState;
|
||||
} as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
|
||||
|
||||
if (mode === 'memory') {
|
||||
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');
|
||||
(mergedConfig as MyServersConfigMemory).connectionStatus = {
|
||||
...(defaultConfig as MyServersConfigMemory).connectionStatus,
|
||||
...(config as MyServersConfigMemory).connectionStatus,
|
||||
};
|
||||
}
|
||||
|
||||
return schema.parse(mergedConfig) as any; // Narrowing ensures correct typing
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to convert an object into a normalized config file.
|
||||
* This is used for loading config files and ensure changes have been made before the state is merged.
|
||||
* Check if two configurations are equivalent by normalizing them through the Zod schema.
|
||||
*/
|
||||
export const areConfigsEquivalent = (
|
||||
newConfigFile: RecursivePartial<MyServersConfig>,
|
||||
currentConfig: ConfigSliceState
|
||||
): boolean =>
|
||||
// Enable to view config diffs: logger.debug(getDiff(getWriteableConfig(currentConfig, 'flash'), newConfigFile));
|
||||
isEqual(newConfigFile, getWriteableConfig(currentConfig, 'flash'));
|
||||
newConfigFile: Partial<MyServersConfigMemory>, // Use Partial here for flexibility
|
||||
currentConfig: MyServersConfig
|
||||
): boolean => {
|
||||
// Parse and validate the new config file using the schema (with default values applied)
|
||||
const normalizedNewConfig = MyServersConfigSchema.parse({
|
||||
...currentConfig, // Use currentConfig as a baseline to fill missing fields
|
||||
...newConfigFile,
|
||||
});
|
||||
|
||||
// Get the writeable configuration for the current config
|
||||
const normalizedCurrentConfig = getWriteableConfig(currentConfig, 'flash');
|
||||
|
||||
// Compare the normalized configurations
|
||||
return isEqual(normalizedNewConfig, normalizedCurrentConfig);
|
||||
};
|
||||
|
||||
58
api/src/types/my-servers-config.d.ts
vendored
58
api/src/types/my-servers-config.d.ts
vendored
@@ -1,58 +0,0 @@
|
||||
import { type DynamicRemoteAccessType, type MinigraphStatus } from '@app/graphql/generated/api/types';
|
||||
|
||||
interface MyServersConfig extends Record<string, unknown> {
|
||||
api: {
|
||||
version: string;
|
||||
extraOrigins: string;
|
||||
};
|
||||
local: {};
|
||||
notifier: {
|
||||
apikey: string;
|
||||
};
|
||||
remote: {
|
||||
wanaccess: string;
|
||||
wanport: string;
|
||||
upnpEnabled?: string;
|
||||
apikey: string;
|
||||
localApiKey?: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar: string;
|
||||
regWizTime: string;
|
||||
accesstoken: string;
|
||||
idtoken: string;
|
||||
refreshtoken: string;
|
||||
allowedOrigins?: string;
|
||||
dynamicRemoteAccessType?: DynamicRemoteAccessType;
|
||||
};
|
||||
upc: {
|
||||
apikey: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MyServersConfigWithMandatoryHiddenFields extends MyServersConfig {
|
||||
api: {
|
||||
extraOrigins: string;
|
||||
};
|
||||
remote: MyServersConfig['remote'] & {
|
||||
upnpEnabled: string;
|
||||
dynamicRemoteAccessType: DynamicRemoteAccessType;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MyServersConfigMemory extends MyServersConfig {
|
||||
connectionStatus: {
|
||||
minigraph: MinigraphStatus;
|
||||
upnpStatus?: null | string;
|
||||
};
|
||||
remote: MyServersConfig['remote'] & {
|
||||
allowedOrigins: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MyServersConfigMemoryWithMandatoryHiddenFields extends MyServersConfigMemory {
|
||||
connectionStatus: {
|
||||
minigraph: MinigraphStatus;
|
||||
upnpStatus?: null | string;
|
||||
};
|
||||
}
|
||||
67
api/src/types/my-servers-config.ts
Normal file
67
api/src/types/my-servers-config.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DynamicRemoteAccessType, MinigraphStatus } from '@app/graphql/generated/api/types';
|
||||
|
||||
// Define Zod schemas
|
||||
const ApiConfigSchema = z.object({
|
||||
version: z.string(),
|
||||
extraOrigins: z.string(),
|
||||
});
|
||||
|
||||
const NotifierConfigSchema = z.object({
|
||||
apikey: z.string(),
|
||||
});
|
||||
|
||||
const RemoteConfigSchema = z.object({
|
||||
wanaccess: z.string(),
|
||||
wanport: z.string(),
|
||||
upnpEnabled: z.string(),
|
||||
apikey: z.string(),
|
||||
localApiKey: z.string(),
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
avatar: z.string(),
|
||||
regWizTime: z.string(),
|
||||
accesstoken: z.string(),
|
||||
idtoken: z.string(),
|
||||
refreshtoken: z.string(),
|
||||
dynamicRemoteAccessType: z.nativeEnum(DynamicRemoteAccessType),
|
||||
});
|
||||
|
||||
const UpcConfigSchema = z.object({
|
||||
apikey: z.string(),
|
||||
});
|
||||
|
||||
// Base config schema
|
||||
export const MyServersConfigSchema = z.object({
|
||||
api: ApiConfigSchema,
|
||||
local: z.object({}), // Empty object
|
||||
notifier: NotifierConfigSchema,
|
||||
remote: RemoteConfigSchema,
|
||||
upc: UpcConfigSchema,
|
||||
});
|
||||
|
||||
// Memory config schema
|
||||
export const ConnectionStatusSchema = z.object({
|
||||
minigraph: z.nativeEnum(MinigraphStatus),
|
||||
upnpStatus: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
export const MyServersConfigMemorySchema = MyServersConfigSchema.extend({
|
||||
connectionStatus: ConnectionStatusSchema,
|
||||
remote: RemoteConfigSchema.extend({
|
||||
allowedOrigins: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Memory config with mandatory hidden fields schema
|
||||
export const MyServersConfigMemoryWithMandatoryHiddenFieldsSchema = MyServersConfigMemorySchema.extend({
|
||||
connectionStatus: ConnectionStatusSchema,
|
||||
});
|
||||
|
||||
// Infer and export types from Zod schemas
|
||||
export type MyServersConfig = z.infer<typeof MyServersConfigSchema>;
|
||||
export type MyServersConfigMemory = z.infer<typeof MyServersConfigMemorySchema>;
|
||||
export type MyServersConfigMemoryWithMandatoryHiddenFields = z.infer<
|
||||
typeof MyServersConfigMemoryWithMandatoryHiddenFieldsSchema
|
||||
>;
|
||||
@@ -28,6 +28,7 @@ export class StartCommand extends CommandRunner {
|
||||
);
|
||||
if (stdout) {
|
||||
this.logger.log(stdout);
|
||||
process.exit(0);
|
||||
}
|
||||
if (stderr) {
|
||||
this.logger.error(stderr);
|
||||
|
||||
@@ -29,6 +29,7 @@ export class ValidateTokenCommand extends CommandRunner {
|
||||
async run(passedParams: string[]): Promise<void> {
|
||||
if (passedParams.length !== 1) {
|
||||
this.logger.error('Please pass token argument only');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const token = passedParams[0];
|
||||
|
||||
Reference in New Issue
Block a user