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 basicConfig = initialState;
|
||||||
const config = getWriteableConfig(basicConfig, 'flash');
|
const config = getWriteableConfig(basicConfig, 'flash');
|
||||||
expect(config).toMatchInlineSnapshot(`
|
expect(config).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"extraOrigins": "",
|
"extraOrigins": "",
|
||||||
"version": "",
|
"version": "",
|
||||||
},
|
},
|
||||||
"local": {},
|
"local": {},
|
||||||
"notifier": {
|
"notifier": {
|
||||||
"apikey": "",
|
"apikey": "",
|
||||||
},
|
},
|
||||||
"remote": {
|
"remote": {
|
||||||
"accesstoken": "",
|
"accesstoken": "",
|
||||||
"apikey": "",
|
"apikey": "",
|
||||||
"avatar": "",
|
"avatar": "",
|
||||||
"dynamicRemoteAccessType": "DISABLED",
|
"dynamicRemoteAccessType": "DISABLED",
|
||||||
"email": "",
|
"email": "",
|
||||||
"idtoken": "",
|
"idtoken": "",
|
||||||
"localApiKey": "",
|
"localApiKey": "",
|
||||||
"refreshtoken": "",
|
"refreshtoken": "",
|
||||||
"regWizTime": "",
|
"regWizTime": "",
|
||||||
"username": "",
|
"upnpEnabled": "",
|
||||||
"wanaccess": "",
|
"username": "",
|
||||||
"wanport": "",
|
"wanaccess": "",
|
||||||
},
|
"wanport": "",
|
||||||
"upc": {
|
},
|
||||||
"apikey": "",
|
"upc": {
|
||||||
},
|
"apikey": "",
|
||||||
}
|
},
|
||||||
`);
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it creates a MEMORY config with NO OPTIONAL values', () => {
|
test('it creates a MEMORY config with NO OPTIONAL values', () => {
|
||||||
const basicConfig = initialState;
|
const basicConfig = initialState;
|
||||||
const config = getWriteableConfig(basicConfig, 'memory');
|
const config = getWriteableConfig(basicConfig, 'memory');
|
||||||
expect(config).toMatchInlineSnapshot(`
|
expect(config).toMatchInlineSnapshot(`
|
||||||
{
|
{
|
||||||
"api": {
|
"api": {
|
||||||
"extraOrigins": "",
|
"extraOrigins": "",
|
||||||
"version": "",
|
"version": "",
|
||||||
},
|
},
|
||||||
"connectionStatus": {
|
"connectionStatus": {
|
||||||
"minigraph": "PRE_INIT",
|
"minigraph": "PRE_INIT",
|
||||||
},
|
"upnpStatus": "",
|
||||||
"local": {},
|
},
|
||||||
"notifier": {
|
"local": {},
|
||||||
"apikey": "",
|
"notifier": {
|
||||||
},
|
"apikey": "",
|
||||||
"remote": {
|
},
|
||||||
"accesstoken": "",
|
"remote": {
|
||||||
"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",
|
"accesstoken": "",
|
||||||
"apikey": "",
|
"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",
|
||||||
"avatar": "",
|
"apikey": "",
|
||||||
"dynamicRemoteAccessType": "DISABLED",
|
"avatar": "",
|
||||||
"email": "",
|
"dynamicRemoteAccessType": "DISABLED",
|
||||||
"idtoken": "",
|
"email": "",
|
||||||
"localApiKey": "",
|
"idtoken": "",
|
||||||
"refreshtoken": "",
|
"localApiKey": "",
|
||||||
"regWizTime": "",
|
"refreshtoken": "",
|
||||||
"username": "",
|
"regWizTime": "",
|
||||||
"wanaccess": "",
|
"upnpEnabled": "",
|
||||||
"wanport": "",
|
"username": "",
|
||||||
},
|
"wanaccess": "",
|
||||||
"upc": {
|
"wanport": "",
|
||||||
"apikey": "",
|
},
|
||||||
},
|
"upc": {
|
||||||
}
|
"apikey": "",
|
||||||
`);
|
},
|
||||||
|
}
|
||||||
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it creates a FLASH config with OPTIONAL values', () => {
|
test('it creates a FLASH config with OPTIONAL values', () => {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export const JWKS_LOCAL_PAYLOAD: JSONWebKeySet = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OAUTH_BASE_URL =
|
export const OAUTH_BASE_URL =
|
||||||
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
|
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
|
||||||
export const OAUTH_CLIENT_ID = '53ci4o48gac8vq5jepubkjmo36';
|
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';
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
export type ConfigType = 'flash' | 'memory';
|
import { getAllowedOrigins } from '@app/common/allowed-origins';
|
||||||
type ConfigObject<T> = T extends 'flash'
|
import { initialState } from '@app/store/modules/config';
|
||||||
? MyServersConfig
|
import {
|
||||||
: T extends 'memory'
|
MyServersConfig,
|
||||||
? MyServersConfigMemory
|
MyServersConfigMemory,
|
||||||
: never;
|
MyServersConfigMemorySchema,
|
||||||
/**
|
MyServersConfigSchema,
|
||||||
*
|
} from '@app/types/my-servers-config';
|
||||||
* @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;
|
|
||||||
|
|
||||||
// Create new state
|
// Define ConfigType and ConfigObject
|
||||||
|
export type ConfigType = 'flash' | 'memory';
|
||||||
const newState: ConfigObject<T> = {
|
|
||||||
api: {
|
/**
|
||||||
version: api?.version ?? initialState.api.version,
|
* Get a writeable configuration based on the mode ('flash' or 'memory').
|
||||||
extraOrigins: api?.extraOrigins ?? initialState.api.extraOrigins,
|
*/
|
||||||
},
|
export const getWriteableConfig = <T extends ConfigType>(
|
||||||
local: {},
|
config: T extends 'memory' ? MyServersConfigMemory : MyServersConfig,
|
||||||
notifier: {
|
mode: T
|
||||||
apikey: notifier.apikey ?? initialState.notifier.apikey,
|
): 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: {
|
remote: {
|
||||||
wanaccess: remote.wanaccess ?? initialState.remote.wanaccess,
|
...defaultConfig.remote,
|
||||||
wanport: remote.wanport ?? initialState.remote.wanport,
|
...config.remote,
|
||||||
...(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,
|
|
||||||
},
|
},
|
||||||
upc: {
|
} as T extends 'memory' ? MyServersConfigMemory : MyServersConfig;
|
||||||
apikey: upc.apikey ?? initialState.upc.apikey,
|
|
||||||
},
|
if (mode === 'memory') {
|
||||||
...(mode === 'memory'
|
(mergedConfig as MyServersConfigMemory).remote.allowedOrigins = getAllowedOrigins().join(', ');
|
||||||
? {
|
(mergedConfig as MyServersConfigMemory).connectionStatus = {
|
||||||
connectionStatus: {
|
...(defaultConfig as MyServersConfigMemory).connectionStatus,
|
||||||
minigraph:
|
...(config as MyServersConfigMemory).connectionStatus,
|
||||||
connectionStatus.minigraph ??
|
};
|
||||||
initialState.connectionStatus.minigraph,
|
}
|
||||||
...(connectionStatus.upnpStatus
|
|
||||||
? { upnpStatus: connectionStatus.upnpStatus }
|
return schema.parse(mergedConfig) as any; // Narrowing ensures correct typing
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
} as ConfigObject<T>;
|
|
||||||
return newState;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to convert an object into a normalized config file.
|
* Check if two configurations are equivalent by normalizing them through the Zod schema.
|
||||||
* This is used for loading config files and ensure changes have been made before the state is merged.
|
|
||||||
*/
|
*/
|
||||||
export const areConfigsEquivalent = (
|
export const areConfigsEquivalent = (
|
||||||
newConfigFile: RecursivePartial<MyServersConfig>,
|
newConfigFile: Partial<MyServersConfigMemory>, // Use Partial here for flexibility
|
||||||
currentConfig: ConfigSliceState
|
currentConfig: MyServersConfig
|
||||||
): boolean =>
|
): boolean => {
|
||||||
// Enable to view config diffs: logger.debug(getDiff(getWriteableConfig(currentConfig, 'flash'), newConfigFile));
|
// Parse and validate the new config file using the schema (with default values applied)
|
||||||
isEqual(newConfigFile, getWriteableConfig(currentConfig, 'flash'));
|
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) {
|
if (stdout) {
|
||||||
this.logger.log(stdout);
|
this.logger.log(stdout);
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
if (stderr) {
|
if (stderr) {
|
||||||
this.logger.error(stderr);
|
this.logger.error(stderr);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export class ValidateTokenCommand extends CommandRunner {
|
|||||||
async run(passedParams: string[]): Promise<void> {
|
async run(passedParams: string[]): Promise<void> {
|
||||||
if (passedParams.length !== 1) {
|
if (passedParams.length !== 1) {
|
||||||
this.logger.error('Please pass token argument only');
|
this.logger.error('Please pass token argument only');
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = passedParams[0];
|
const token = passedParams[0];
|
||||||
|
|||||||
Reference in New Issue
Block a user