fix: update handling of default/forced values

This commit is contained in:
Viren070
2025-06-13 16:24:51 +01:00
parent 091394b837
commit c60ef6fde9
6 changed files with 207 additions and 212 deletions
+3 -3
View File
@@ -41,9 +41,9 @@ const Formatter = z.object({
const StreamProxyConfig = z.object({
enabled: z.boolean().optional(),
id: z.enum(constants.PROXY_SERVICES),
url: z.string().url(),
credentials: z.string().min(1),
id: z.enum(constants.PROXY_SERVICES).optional(),
url: z.string().optional(),
credentials: z.string().min(1).optional(),
publicIp: z.string().ip().optional(),
proxiedAddons: z.array(z.string().min(1)).optional(),
proxiedServices: z.array(z.string().min(1)).optional(),
+12 -2
View File
@@ -13,14 +13,24 @@ export interface ProxyStream {
};
}
type ValidatedStreamProxyConfig = StreamProxyConfig & {
id: 'mediaflow' | 'stremthru';
url: string;
credentials: string;
};
export abstract class BaseProxy {
protected readonly config: StreamProxyConfig;
protected readonly config: ValidatedStreamProxyConfig;
private readonly PRIVATE_CIDR =
/^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)/;
constructor(config: StreamProxyConfig) {
if (!config.id || !config.credentials || !config.url) {
throw new Error('Proxy configuration is missing');
}
this.config = {
enabled: config.enabled,
enabled: config.enabled ?? false,
id: config.id,
url: config.url,
credentials: config.credentials,
+42 -22
View File
@@ -13,7 +13,7 @@ import { createProxy } from '../proxy';
import { constants } from '.';
import { isEncrypted, decryptString, encryptString } from './crypto';
import { Env } from './env';
import { createLogger } from './logger';
import { createLogger, maskSensitiveInfo } from './logger';
import { ZodError } from 'zod';
import { ConditionParser } from '../parser/conditions';
import { RPDB } from './rpdb';
@@ -273,11 +273,15 @@ export async function validateConfig(
}
if (config.proxy) {
config.proxy = await validateProxy(
config.proxy,
skipErrorsFromAddonsOrProxies,
decryptValues
);
const decryptedProxy = ensureDecrypted(config).proxy;
if (decryptedProxy) {
config.proxy = await validateProxy(
config.proxy,
decryptedProxy,
skipErrorsFromAddonsOrProxies,
decryptValues
);
}
}
if (config.rpdbApiKey) {
@@ -351,7 +355,7 @@ async function validateRegexes(config: UserData) {
);
}
function ensureDecrypted(config: UserData) {
function ensureDecrypted(config: UserData): UserData {
const decryptedConfig = { ...config };
// Helper function to decrypt a value if needed
@@ -369,7 +373,7 @@ function ensureDecrypted(config: UserData) {
if (!service.credentials) continue;
for (const [credential, value] of Object.entries(service.credentials)) {
service.credentials[credential] = tryDecrypt(
value,
decodeURIComponent(value),
`credential ${credential}`
);
}
@@ -378,8 +382,14 @@ function ensureDecrypted(config: UserData) {
// Decrypt proxy config
if (decryptedConfig.proxy) {
const proxy = decryptedConfig.proxy;
proxy.credentials = tryDecrypt(proxy.credentials, 'proxy credentials');
proxy.url = tryDecrypt(proxy.url, 'proxy URL');
proxy.credentials = tryDecrypt(
proxy.credentials ? decodeURIComponent(proxy.credentials) : undefined,
'proxy credentials'
);
proxy.url = tryDecrypt(
proxy.url ? decodeURIComponent(proxy.url) : undefined,
'proxy URL'
);
}
return decryptedConfig;
@@ -519,6 +529,7 @@ function validateOption(
if (option.forced) {
value = option.forced;
}
value = decodeURIComponent(value);
if (isEncrypted(value) && decryptValues) {
const { success, data, error } = decryptString(value);
if (!success) {
@@ -547,6 +558,7 @@ function validateOption(
async function validateProxy(
proxy: StreamProxyConfig,
decryptedProxy: StreamProxyConfig,
skipProxyErrors: boolean = false,
decryptCredentials: boolean = false
): Promise<StreamProxyConfig> {
@@ -572,18 +584,10 @@ async function validateProxy(
throw new Error('Proxy credentials are required');
}
const ProxyService = createProxy(proxy);
try {
proxy.publicIp || (await ProxyService.getPublicIp());
} catch (error) {
if (!skipProxyErrors) {
throw new Error(
`Failed to get the public IP of the proxy service ${proxy.id}: ${error}`
);
}
}
proxy.credentials = decodeURIComponent(proxy.credentials);
proxy.url = proxy.url.startsWith('aioEncrypt')
? decodeURIComponent(proxy.url)
: proxy.url;
if (isEncrypted(proxy.credentials) && decryptCredentials) {
const { success, data, error } = decryptString(proxy.credentials);
if (!success) {
@@ -602,6 +606,22 @@ async function validateProxy(
}
proxy.url = data;
}
// use decrypted proxy config for validation.
const ProxyService = createProxy(decryptedProxy);
try {
proxy.publicIp || (await ProxyService.getPublicIp());
} catch (error) {
if (!skipProxyErrors) {
logger.error(
`Failed to get the public IP of the proxy service ${proxy.id} (${maskSensitiveInfo(proxy.url)}): ${error}`
);
throw new Error(
`Failed to get the public IP of the proxy service ${proxy.id}: ${error}`
);
}
}
}
return proxy;
}
+50 -141
View File
@@ -35,68 +35,12 @@ export function ProxyMenu() {
);
}
// provides a page to configure a proxy
// use constants.PROXY_DETAILS to get the list of proxies
// and use status.settings.defaults.proxy to load default values if current userData doesn't have a value
// use status.settings.forced.proxy to always load the forced values
// should look like this.
// a switch to enable/disable the proxy. should be enabled on left and then switch on right, with descriptin of setting below.
// when disabled, hide the rest of the settings
// a select menu to choose a proxy, mapped to the proxy.id option (Get name/label from details)
// shows the description below the select.
// then a password input to provide the credential
// then a multi select menu to choose services that proxy is used for
// then a multi select menu to choose addons that proxy is used for
// addon labels should use the addon name, and value should be the ID, calculated using same method as getPresetUniqueKey in addons.tsx
function Content() {
const { status } = useStatus();
const { userData, setUserData } = useUserData();
const details = constants.PROXY_SERVICE_DETAILS;
// Initialize proxy configuration from userData, defaults, or forced values
const [proxyEnabled, setProxyEnabled] = useState(false);
const [selectedProxyId, setSelectedProxyId] =
useState<ProxyServiceId>('mediaflow');
const [proxyUrl, setProxyUrl] = useState('');
const [proxyCredentials, setProxyCredentials] = useState('');
const [proxiedServices, setProxiedServices] = useState<string[] | undefined>(
undefined
);
const [proxiedAddons, setProxiedAddons] = useState<string[] | undefined>(
undefined
);
const [hasChanges, setHasChanges] = useState(false);
// Effect to initialize values from userData/defaults/forced
useEffect(() => {
if (!status) return;
const forced = status.settings.forced.proxy;
const defaults = status.settings.defaults.proxy;
const current = userData.proxy;
// Apply forced values first, then current values, then defaults
setProxyEnabled(
forced.enabled ?? current?.enabled ?? defaults.enabled ?? false
);
setSelectedProxyId(
(forced.id ?? current?.id ?? defaults.id ?? '') as ProxyServiceId
);
setProxyCredentials(
forced.credentials ?? current?.credentials ?? defaults.credentials ?? ''
);
setProxyUrl(forced.url ?? current?.url ?? defaults.url ?? '');
setProxiedServices(
forced.proxiedServices ??
current?.proxiedServices ??
defaults.proxiedServices ??
[]
);
setProxiedAddons(current?.proxiedAddons);
setHasChanges(false);
}, [status]);
// Generate options for proxy service select
const proxyOptions = Object.entries(details).map(([id, detail]) => ({
@@ -126,61 +70,7 @@ function Content() {
};
});
// Handle changes
const handleProxyChange = (enabled: boolean) => {
setProxyEnabled(enabled);
if (!enabled && !selectedProxyId && userData.proxy === undefined) {
setHasChanges(false);
} else {
setHasChanges(true);
}
};
const handleUrlChange = (value: string) => {
setProxyUrl(value);
setHasChanges(true);
};
const handleProxyServiceChange = (value: string) => {
setSelectedProxyId(value as ProxyServiceId);
setHasChanges(true);
};
const handleCredentialsChange = (value: string) => {
setProxyCredentials(value);
setHasChanges(true);
};
const handleProxiedServicesChange = (values: string[]) => {
setProxiedServices(values);
setHasChanges(true);
};
const handleProxiedAddonsChange = (values: string[]) => {
setProxiedAddons(values);
setHasChanges(true);
};
const handleSubmit = () => {
const proxyConfig: ProxyConfig = {
enabled: proxyEnabled,
id: selectedProxyId,
url: proxyUrl,
credentials: proxyCredentials,
proxiedServices: proxiedServices,
proxiedAddons: proxiedAddons,
};
setUserData((prev) => ({
...prev,
proxy: proxyConfig,
}));
setHasChanges(false);
toast.success('Proxy settings saved');
};
// Check if values are forced
// lues are forced
const isForced = status?.settings.forced.proxy;
const isProxyForced = isForced?.enabled !== null;
const isUrlForced = isForced?.url !== null;
@@ -189,8 +79,8 @@ function Content() {
const isServicesForced = isForced?.proxiedServices !== null;
const isProxiedAddonsDisabled = isForced?.disableProxiedAddons;
const selectedProxyDetails = selectedProxyId
? details[selectedProxyId]
const selectedProxyDetails = userData.proxy?.id
? details[userData.proxy.id]
: undefined;
return (
@@ -213,8 +103,13 @@ function Content() {
<Switch
side="right"
label="Enable"
value={proxyEnabled}
onValueChange={handleProxyChange}
value={userData.proxy?.enabled ?? false}
onValueChange={(v) => {
setUserData((prev) => ({
...prev,
proxy: { ...prev.proxy, enabled: v },
}));
}}
disabled={isProxyForced}
/>
</SettingsCard>
@@ -223,10 +118,15 @@ function Content() {
<div className="space-y-2">
<Select
label="Proxy Service"
value={selectedProxyId}
onValueChange={handleProxyServiceChange}
value={userData.proxy?.id ?? 'mediaflow'}
onValueChange={(v) => {
setUserData((prev) => ({
...prev,
proxy: { ...prev.proxy, id: v as ProxyServiceId },
}));
}}
options={proxyOptions}
disabled={isIdForced || !proxyEnabled}
disabled={isIdForced || !userData.proxy?.enabled}
/>
{selectedProxyDetails && (
<p className="text-[--muted] text-sm">
@@ -238,11 +138,16 @@ function Content() {
<div className="space-y-2">
<TextInput
label="URL"
value={proxyUrl}
value={userData.proxy?.url ?? ''}
type="password"
onValueChange={handleUrlChange}
onValueChange={(v) => {
setUserData((prev) => ({
...prev,
proxy: { ...prev.proxy, url: v },
}));
}}
placeholder="Enter proxy URL"
disabled={isUrlForced || !proxyEnabled}
disabled={isUrlForced || !userData.proxy?.enabled}
/>
<p className="text-[--muted] text-sm">
The URL of your hosted proxy service.
@@ -253,10 +158,15 @@ function Content() {
<TextInput
label="Credentials"
type="password"
value={proxyCredentials}
onValueChange={handleCredentialsChange}
value={userData.proxy?.credentials ?? ''}
onValueChange={(v) => {
setUserData((prev) => ({
...prev,
proxy: { ...prev.proxy, credentials: v },
}));
}}
placeholder="Enter proxy credentials"
disabled={isCredentialsForced || !proxyEnabled}
disabled={isCredentialsForced || !userData.proxy?.enabled}
/>
{selectedProxyDetails && (
<p className="text-[--muted] text-sm">
@@ -275,12 +185,17 @@ function Content() {
<div className="space-y-2">
<Combobox
label="Proxied Services"
value={proxiedServices}
onValueChange={handleProxiedServicesChange}
value={userData.proxy?.proxiedServices ?? []}
onValueChange={(v) => {
setUserData((prev) => ({
...prev,
proxy: { ...prev.proxy, proxiedServices: v },
}));
}}
options={serviceOptions}
placeholder="Select services to proxy"
multiple={true}
disabled={isServicesForced || !proxyEnabled}
disabled={isServicesForced || !userData.proxy?.enabled}
emptyMessage="No services available"
/>
<p className="text-[--muted] text-sm">
@@ -293,12 +208,17 @@ function Content() {
<div className="space-y-2">
<Combobox
label="Proxied Addons"
value={proxiedAddons}
onValueChange={handleProxiedAddonsChange}
value={userData.proxy?.proxiedAddons ?? []}
onValueChange={(v) => {
setUserData((prev) => ({
...prev,
proxy: { ...prev.proxy, proxiedAddons: v },
}));
}}
options={addonOptions}
placeholder="Select addons to proxy"
multiple={true}
disabled={isProxiedAddonsDisabled || !proxyEnabled}
disabled={isProxiedAddonsDisabled || !userData.proxy?.enabled}
emptyMessage="No addons available"
/>
<p className="text-[--muted] text-sm">
@@ -307,17 +227,6 @@ function Content() {
</div>
</SettingsCard>
</div>
<div className="flex justify-start mt-4">
<Button
intent="white"
rounded
onClick={handleSubmit}
disabled={!hasChanges}
>
Save Changes
</Button>
</div>
</>
);
}
@@ -127,36 +127,7 @@ function Content() {
const currentServices = userData.services ?? [];
// Remove any services not in SERVICE_DETAILS and apply forced/default credentials
let filtered = currentServices
.filter((s) => allServiceIds.includes(s.id))
.map((service) => {
const svcMeta = status.settings.services[service.id]!;
const updatedCredentials = { ...service.credentials };
let hasChanges = false;
svcMeta.credentials.forEach((cred) => {
// Always apply forced credentials, regardless of existing value
if (cred.forced) {
if (updatedCredentials[cred.id] !== cred.forced) {
updatedCredentials[cred.id] = cred.forced;
hasChanges = true;
}
}
// Only apply defaults for missing credentials
else if (!service.credentials?.[cred.id] && cred.default) {
updatedCredentials[cred.id] = cred.default;
hasChanges = true;
}
});
// Only create a new object if there were changes
return hasChanges
? {
...service,
credentials: updatedCredentials,
}
: service;
});
let filtered = currentServices.filter((s) => allServiceIds.includes(s.id));
// Add any missing services from SERVICE_DETAILS
const missing = allServiceIds.filter(
@@ -169,18 +140,6 @@ function Content() {
const credentials: Record<string, any> = {};
let enabled = false;
// Apply forced/default credentials for new services
svcMeta.credentials.forEach((cred) => {
if (cred.forced) {
credentials[cred.id] = cred.forced;
// enable the service if it has forced credentials
enabled = true;
} else if (cred.default) {
credentials[cred.id] = cred.default;
enabled = true;
}
});
return {
id,
enabled,
@@ -437,7 +396,6 @@ function SortableServiceItem({
</li>
);
}
function ServiceModal({
open,
onOpenChange,
+99 -1
View File
@@ -1,8 +1,18 @@
import React from 'react';
import { UserData } from '@aiostreams/core';
import { QUALITIES, RESOLUTIONS } from '../../../core/src/utils/constants';
import {
QUALITIES,
RESOLUTIONS,
SERVICE_DETAILS,
} from '../../../core/src/utils/constants';
import { useStatus } from './status';
const DefaultUserData: UserData = {
services: Object.values(SERVICE_DETAILS).map((service) => ({
id: service.id,
enabled: false,
credentials: {},
})),
presets: [],
formatter: {
id: 'gdrive',
@@ -56,6 +66,7 @@ const UserDataContext = React.createContext<UserDataContextType | undefined>(
);
export function UserDataProvider({ children }: { children: React.ReactNode }) {
const { status } = useStatus();
const [userData, setUserData] = React.useState<UserData>(DefaultUserData);
const [uuid, setUuid] = React.useState<string | null>(null);
const [password, setPassword] = React.useState<string | null>(null);
@@ -63,6 +74,93 @@ export function UserDataProvider({ children }: { children: React.ReactNode }) {
string | null
>(null);
// Effect to apply forced and default values from status
React.useEffect(() => {
if (!status) return;
const forced = status.settings.forced;
const defaults = status.settings.defaults;
const services = status.settings.services;
setUserData((prev) => {
const newData = { ...prev };
// // Apply forced values first
// if (forced.proxy) {
// newData.proxy = {
// ...newData.proxy,
// enabled: forced.proxy.enabled ?? newData.proxy?.enabled ?? false,
// id: (forced.proxy.id ?? newData.proxy?.id) as
// | 'mediaflow'
// | 'stremthru'
// | undefined,
// url: forced.proxy.url ?? newData.proxy?.url,
// credentials: forced.proxy.credentials ?? newData.proxy?.credentials,
// proxiedServices:
// forced.proxy.proxiedServices ??
// newData.proxy?.proxiedServices ??
// [],
// };
// }
// // Apply default values if not already set
// if (defaults.proxy) {
// newData.proxy = {
// ...newData.proxy,
// enabled: defaults.proxy.enabled ?? false,
// id: (newData.proxy?.id ?? defaults.proxy.id ?? undefined) as
// | 'mediaflow'
// | 'stremthru'
// | undefined,
// url: newData.proxy?.url ?? defaults.proxy.url ?? undefined,
// credentials:
// newData.proxy?.credentials ??
// defaults.proxy.credentials ??
// undefined,
// proxiedServices:
// newData.proxy?.proxiedServices ??
// defaults.proxy.proxiedServices ??
// [],
// };
// }
newData.proxy = {
...newData.proxy,
enabled: forced.proxy.enabled ?? defaults.proxy?.enabled ?? undefined,
id: (forced.proxy.id ?? defaults.proxy?.id) as
| 'mediaflow'
| 'stremthru'
| undefined,
url: forced.proxy.url ?? defaults.proxy?.url ?? undefined,
credentials:
forced.proxy.credentials ?? defaults.proxy?.credentials ?? undefined,
proxiedServices:
forced.proxy.proxiedServices ?? defaults.proxy?.proxiedServices ?? [],
};
newData.services = (newData.services ?? []).map((service) => {
const serviceMeta = services[service.id];
if (!serviceMeta) return service;
serviceMeta.credentials.forEach((credential) => {
if (credential.forced) {
service.credentials[credential.id] = credential.forced;
} else if (credential.default) {
service.credentials[credential.id] = credential.default;
}
});
// enable if every credential is set
service.enabled = serviceMeta.credentials.every(
(credential) =>
credential.forced ||
credential.default ||
service.credentials[credential.id] !== undefined
);
return service;
});
return newData;
});
}, [status]);
const safeSetUserData = (
data: ((prev: UserData) => UserData | null) | null
) => {