mirror of
https://github.com/Viren070/AIOStreams.git
synced 2025-12-01 23:14:04 +01:00
feat: service cred env vars, better validation, handling of encrypted values
This commit is contained in:
@@ -34,7 +34,7 @@ const StreamProxyConfig = z.object({
|
||||
id: z.enum(constants.PROXY_SERVICES),
|
||||
url: z.string().url(),
|
||||
credentials: z.string().min(1),
|
||||
publicIp: z.string().min(1).optional(),
|
||||
publicIp: z.string().ip().optional(),
|
||||
proxiedAddons: z.array(z.string().min(1)),
|
||||
proxiedServices: z.array(z.string().min(1)),
|
||||
});
|
||||
@@ -80,7 +80,7 @@ const SizeFilterOptions = z.object({
|
||||
const ServiceSchema = z.object({
|
||||
id: ServiceIds,
|
||||
enabled: z.boolean().optional(),
|
||||
credentials: z.record(z.string().min(1), z.string().min(1)).optional(),
|
||||
credentials: z.record(z.string().min(1), z.string().min(1)),
|
||||
});
|
||||
|
||||
export type Service = z.infer<typeof ServiceSchema>;
|
||||
@@ -152,6 +152,7 @@ const OptionDefinition = z.object({
|
||||
emptyIsUndefined: z.boolean().optional(),
|
||||
type: z.enum([
|
||||
'string',
|
||||
'password',
|
||||
'number',
|
||||
'boolean',
|
||||
'select',
|
||||
@@ -160,7 +161,8 @@ const OptionDefinition = z.object({
|
||||
]),
|
||||
required: z.boolean().optional(),
|
||||
default: z.any().optional(),
|
||||
sensitive: z.boolean().optional(),
|
||||
// sensitive: z.boolean().optional(),
|
||||
forced: z.any().optional(),
|
||||
options: z
|
||||
.array(
|
||||
z.object({
|
||||
@@ -586,6 +588,17 @@ const StatusResponseSchema = z.object({
|
||||
excludedRegex: z.array(z.string()),
|
||||
}),
|
||||
presets: z.array(PresetMetadataSchema),
|
||||
services: z.record(
|
||||
z.enum(constants.SERVICES),
|
||||
z.object({
|
||||
id: z.enum(constants.SERVICES),
|
||||
name: z.string(),
|
||||
shortName: z.string(),
|
||||
knownNames: z.array(z.string()),
|
||||
signUpText: z.string(),
|
||||
credentials: z.array(OptionDefinition),
|
||||
})
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ export class UserRepository {
|
||||
);
|
||||
}
|
||||
|
||||
let validatedConfig: UserData;
|
||||
try {
|
||||
await validateConfig(config);
|
||||
validatedConfig = await validateConfig(config);
|
||||
} catch (error: any) {
|
||||
return Promise.reject(
|
||||
new APIError(
|
||||
@@ -61,7 +62,7 @@ export class UserRepository {
|
||||
config.uuid = uuid;
|
||||
|
||||
const { encryptedConfig, salt: configSalt } = await this.encryptConfig(
|
||||
config,
|
||||
validatedConfig,
|
||||
password
|
||||
);
|
||||
const hashedPassword = await getTextHash(password);
|
||||
@@ -139,11 +140,24 @@ export class UserRepository {
|
||||
result[0].config_salt
|
||||
);
|
||||
|
||||
decryptedConfig.admin =
|
||||
let validatedConfig: UserData;
|
||||
try {
|
||||
validatedConfig = await validateConfig(decryptedConfig);
|
||||
} catch (error: any) {
|
||||
return Promise.reject(
|
||||
new APIError(
|
||||
constants.ErrorCode.USER_INVALID_CONFIG,
|
||||
undefined,
|
||||
error.message
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
validatedConfig.admin =
|
||||
Env.ADMIN_UUIDS?.split(',').some((u) => new RegExp(u).test(uuid)) ??
|
||||
false;
|
||||
logger.info(`Retrieved configuration for user ${uuid}`);
|
||||
return decryptedConfig;
|
||||
return validatedConfig;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Error retrieving user ${uuid}: ${error instanceof Error ? error.message : String(error)}`
|
||||
@@ -172,8 +186,9 @@ export class UserRepository {
|
||||
);
|
||||
}
|
||||
|
||||
let validatedConfig: UserData;
|
||||
try {
|
||||
await validateConfig(config);
|
||||
validatedConfig = await validateConfig(config);
|
||||
} catch (error: any) {
|
||||
await tx.rollback();
|
||||
return Promise.reject(
|
||||
@@ -194,7 +209,7 @@ export class UserRepository {
|
||||
}
|
||||
|
||||
const { encryptedConfig } = await this.encryptConfig(
|
||||
config,
|
||||
validatedConfig,
|
||||
password,
|
||||
currentUser.rows[0].config_salt
|
||||
);
|
||||
|
||||
@@ -4,11 +4,17 @@ import {
|
||||
PresetObject,
|
||||
Service,
|
||||
Option,
|
||||
StreamProxyConfig,
|
||||
} from '../db/schemas';
|
||||
import { AIOStreams } from '../main';
|
||||
import { Preset, PresetManager } from '../presets';
|
||||
import { SERVICE_DETAILS } from './constants';
|
||||
import { createProxy } from '../proxy';
|
||||
import { constants } from '.';
|
||||
import { isEncrypted, decryptString, encryptString } from './crypto';
|
||||
import { Env } from './env';
|
||||
import { createLogger } from './logger';
|
||||
|
||||
const logger = createLogger('core');
|
||||
|
||||
const formatZodError = (error: any) => {
|
||||
let message = '';
|
||||
@@ -18,11 +24,219 @@ const formatZodError = (error: any) => {
|
||||
return message;
|
||||
};
|
||||
|
||||
function getServiceCredentialDefault(
|
||||
serviceId: constants.ServiceId,
|
||||
credentialId: string
|
||||
) {
|
||||
// env mapping
|
||||
logger.info(`Getting default credential for ${serviceId} ${credentialId}`);
|
||||
switch (serviceId) {
|
||||
case constants.REALDEBRID_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
logger.info(
|
||||
`Default credential for ${serviceId} ${credentialId} is ${Env.DEFAULT_REALDEBRID_API_KEY}`
|
||||
);
|
||||
return Env.DEFAULT_REALDEBRID_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.ALLEDEBRID_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.DEFAULT_ALLDEBRID_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.PREMIUMIZE_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.DEFAULT_PREMIUMIZE_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.DEBRIDLINK_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.DEFAULT_DEBRIDLINK_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.TORBOX_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.DEFAULT_TORBOX_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.EASYDEBRID_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.DEFAULT_EASYDEBRID_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.PUTIO_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'clientId':
|
||||
return Env.DEFAULT_PUTIO_CLIENT_ID;
|
||||
case 'clientSecret':
|
||||
return Env.DEFAULT_PUTIO_CLIENT_SECRET;
|
||||
}
|
||||
break;
|
||||
case constants.PIKPAK_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'email':
|
||||
return Env.DEFAULT_PIKPAK_EMAIL;
|
||||
case 'password':
|
||||
return Env.DEFAULT_PIKPAK_PASSWORD;
|
||||
}
|
||||
break;
|
||||
case constants.OFFCLOUD_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.DEFAULT_OFFCLOUD_API_KEY;
|
||||
case 'email':
|
||||
return Env.DEFAULT_OFFCLOUD_EMAIL;
|
||||
case 'password':
|
||||
return Env.DEFAULT_OFFCLOUD_PASSWORD;
|
||||
}
|
||||
break;
|
||||
case constants.SEEDR_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'encodedToken':
|
||||
return Env.DEFAULT_SEEDR_ENCODED_TOKEN;
|
||||
}
|
||||
break;
|
||||
case constants.EASYNEWS_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'username':
|
||||
return Env.DEFAULT_EASYNEWS_USERNAME;
|
||||
case 'password':
|
||||
return Env.DEFAULT_EASYNEWS_PASSWORD;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getServiceCredentialForced(
|
||||
serviceId: constants.ServiceId,
|
||||
credentialId: string
|
||||
) {
|
||||
// env mapping
|
||||
switch (serviceId) {
|
||||
case constants.REALDEBRID_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_REALDEBRID_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.ALLEDEBRID_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_ALLDEBRID_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.PREMIUMIZE_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_PREMIUMIZE_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.DEBRIDLINK_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_DEBRIDLINK_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.TORBOX_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_TORBOX_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.EASYDEBRID_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_EASYDEBRID_API_KEY;
|
||||
}
|
||||
break;
|
||||
case constants.PUTIO_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'clientId':
|
||||
return Env.FORCED_PUTIO_CLIENT_ID;
|
||||
case 'clientSecret':
|
||||
return Env.FORCED_PUTIO_CLIENT_SECRET;
|
||||
}
|
||||
break;
|
||||
case constants.PIKPAK_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'email':
|
||||
return Env.FORCED_PIKPAK_EMAIL;
|
||||
case 'password':
|
||||
return Env.FORCED_PIKPAK_PASSWORD;
|
||||
}
|
||||
break;
|
||||
case constants.OFFCLOUD_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'apiKey':
|
||||
return Env.FORCED_OFFCLOUD_API_KEY;
|
||||
case 'email':
|
||||
return Env.FORCED_OFFCLOUD_EMAIL;
|
||||
case 'password':
|
||||
return Env.FORCED_OFFCLOUD_PASSWORD;
|
||||
}
|
||||
break;
|
||||
case constants.SEEDR_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'encodedToken':
|
||||
return Env.FORCED_SEEDR_ENCODED_TOKEN;
|
||||
}
|
||||
break;
|
||||
case constants.EASYNEWS_SERVICE:
|
||||
switch (credentialId) {
|
||||
case 'username':
|
||||
return Env.FORCED_EASYNEWS_USERNAME;
|
||||
case 'password':
|
||||
return Env.FORCED_EASYNEWS_PASSWORD;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEnvironmentServiceDetails(): typeof constants.SERVICE_DETAILS {
|
||||
return Object.fromEntries(
|
||||
Object.entries(constants.SERVICE_DETAILS).map(([id, service]) => [
|
||||
id as constants.ServiceId,
|
||||
{
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
shortName: service.shortName,
|
||||
knownNames: service.knownNames,
|
||||
signUpText: service.signUpText,
|
||||
credentials: service.credentials.map((cred) => ({
|
||||
id: cred.id,
|
||||
name: cred.name,
|
||||
description: cred.description,
|
||||
type: cred.type,
|
||||
required: cred.required,
|
||||
default: getServiceCredentialDefault(service.id, cred.id)
|
||||
? encryptString(getServiceCredentialDefault(service.id, cred.id)!)
|
||||
.data
|
||||
: null,
|
||||
forced: getServiceCredentialForced(service.id, cred.id)
|
||||
? encryptString(getServiceCredentialForced(service.id, cred.id)!)
|
||||
.data
|
||||
: null,
|
||||
})),
|
||||
},
|
||||
])
|
||||
) as typeof constants.SERVICE_DETAILS;
|
||||
}
|
||||
|
||||
export async function validateConfig(
|
||||
config: any,
|
||||
data: any,
|
||||
skipFailedAddons: boolean = false
|
||||
): Promise<UserData> {
|
||||
const { success, data, error } = UserDataSchema.safeParse(config);
|
||||
const { success, data: config, error } = UserDataSchema.safeParse(data);
|
||||
if (!success) {
|
||||
throw new Error(formatZodError(error));
|
||||
}
|
||||
@@ -42,36 +256,50 @@ export async function validateConfig(
|
||||
}
|
||||
|
||||
if (config.services) {
|
||||
for (const service of config.services.filter((s: Service) => s.enabled)) {
|
||||
validateService(service);
|
||||
}
|
||||
config.services = config.services.map((service: Service) =>
|
||||
validateService(service)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.proxy) {
|
||||
config.proxy = await validateProxy(config.proxy);
|
||||
}
|
||||
|
||||
try {
|
||||
await new AIOStreams(data, skipFailedAddons).initialise();
|
||||
await new AIOStreams(config, skipFailedAddons).initialise();
|
||||
} catch (error: any) {
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return data;
|
||||
return config;
|
||||
}
|
||||
|
||||
function validateService(service: Service) {
|
||||
const serviceMeta = SERVICE_DETAILS?.[service.id];
|
||||
function validateService(service: Service): Service {
|
||||
const serviceMeta = getEnvironmentServiceDetails()[service.id];
|
||||
|
||||
if (!serviceMeta) {
|
||||
throw new Error(`Service ${service.id} not found`);
|
||||
}
|
||||
|
||||
for (const credential of serviceMeta.credentials) {
|
||||
try {
|
||||
validateOption(credential, service.credentials?.[credential.id]);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`The value for credential '${credential.name}' in service '${serviceMeta.name}' is invalid: ${error}`
|
||||
);
|
||||
if (serviceMeta.credentials.every((cred) => cred.forced)) {
|
||||
service.enabled = true;
|
||||
}
|
||||
|
||||
if (service.enabled) {
|
||||
for (const credential of serviceMeta.credentials) {
|
||||
try {
|
||||
service.credentials[credential.id] = validateOption(
|
||||
credential,
|
||||
service.credentials?.[credential.id]
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`The value for credential '${credential.name}' in service '${serviceMeta.name}' is invalid: ${error}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
function validatePreset(preset: PresetObject) {
|
||||
@@ -85,7 +313,7 @@ function validatePreset(preset: PresetObject) {
|
||||
throw new Error(`Option ${optionId} not found in preset ${preset.id}`);
|
||||
}
|
||||
try {
|
||||
validateOption(optionMeta, optionValue);
|
||||
preset.options[optionId] = validateOption(optionMeta, optionValue);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`The value for option '${optionMeta.name}' in preset '${presetMeta.NAME}' is invalid: ${error}`
|
||||
@@ -94,44 +322,125 @@ function validatePreset(preset: PresetObject) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateOption(option: Option, value: any) {
|
||||
function validateOption(option: Option, value: any): any {
|
||||
if (option.type === 'multi-select') {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`Option ${option.id} must be an array`);
|
||||
throw new Error(
|
||||
`Option ${option.id} must be an array, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'select') {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Option ${option.id} must be a string`);
|
||||
throw new Error(
|
||||
`Option ${option.id} must be a string, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'boolean') {
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new Error(`Option ${option.id} must be a boolean`);
|
||||
throw new Error(
|
||||
`Option ${option.id} must be a boolean, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'number') {
|
||||
if (typeof value !== 'number') {
|
||||
throw new Error(`Option ${option.id} must be a number`);
|
||||
throw new Error(
|
||||
`Option ${option.id} must be a number, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'string') {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Option ${option.id} must be a string`);
|
||||
throw new Error(
|
||||
`Option ${option.id} must be a string, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'password') {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(
|
||||
`Option ${option.id} must be a string, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
if (option.forced) {
|
||||
value = option.forced;
|
||||
}
|
||||
if (isEncrypted(value)) {
|
||||
const { success, data, error } = decryptString(value);
|
||||
if (!success) {
|
||||
throw new Error(
|
||||
`Option ${option.id} is encrypted but failed to decrypt: ${error}`
|
||||
);
|
||||
}
|
||||
value = data;
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'url') {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Option ${option.id} must be a string`);
|
||||
throw new Error(
|
||||
`Option ${option.id} must be a string, got ${typeof value}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (option.required && value === undefined) {
|
||||
throw new Error(`Option ${option.id} is required `);
|
||||
throw new Error(`Option ${option.id} is required, got ${value}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
async function validateProxy(
|
||||
proxy: StreamProxyConfig
|
||||
): Promise<StreamProxyConfig> {
|
||||
// apply forced values if they exist
|
||||
proxy.enabled = Env.FORCE_PROXY_ENABLED ?? proxy.enabled;
|
||||
proxy.id = Env.FORCE_PROXY_ID ?? proxy.id;
|
||||
proxy.url = Env.FORCE_PROXY_URL ?? proxy.url;
|
||||
proxy.credentials = Env.FORCE_PROXY_CREDENTIALS ?? proxy.credentials;
|
||||
proxy.publicIp = Env.FORCE_PROXY_PUBLIC_IP ?? proxy.publicIp;
|
||||
proxy.proxiedAddons = Env.FORCE_PROXY_PROXIED_ADDONS ?? proxy.proxiedAddons;
|
||||
proxy.proxiedServices =
|
||||
Env.FORCE_PROXY_PROXIED_SERVICES ?? proxy.proxiedServices;
|
||||
if (proxy.enabled) {
|
||||
if (!proxy.id) {
|
||||
throw new Error('Proxy ID is required');
|
||||
}
|
||||
if (!proxy.url) {
|
||||
throw new Error('Proxy URL is required');
|
||||
}
|
||||
if (!proxy.credentials) {
|
||||
throw new Error('Proxy credentials are required');
|
||||
}
|
||||
|
||||
const ProxyService = createProxy(proxy);
|
||||
|
||||
try {
|
||||
proxy.publicIp || (await ProxyService.getPublicIp());
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to get the public IP of the proxy service ${proxy.id}: ${error}`
|
||||
);
|
||||
}
|
||||
|
||||
if (isEncrypted(proxy.credentials)) {
|
||||
const { success, data, error } = decryptString(proxy.credentials);
|
||||
if (!success) {
|
||||
throw new Error(
|
||||
`Proxy credentials for ${proxy.id} are encrypted but failed to decrypt: ${error}`
|
||||
);
|
||||
}
|
||||
proxy.credentials = data;
|
||||
}
|
||||
}
|
||||
return proxy;
|
||||
}
|
||||
|
||||
@@ -248,9 +248,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'The API key for the Real-Debrid service. Obtain it from [here](https://real-debrid.com/apitoken)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -267,9 +266,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'The API key for the All-Debrid service. Create one [here](https://alldebrid.com/apikeys)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -286,9 +284,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'Your Premiumize API key. Obtain it from [here](https://www.premiumize.me/account)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -305,9 +302,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'Your Debrid-Link API key. Obtain it from [here](https://debrid-link.com/webapp/apikey)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -324,9 +320,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'Your Torbox API key. Obtain it from [here](https://torbox.app/settings)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -343,16 +338,15 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'Your Offcloud API key. Obtain it from [here](https://offcloud.com/#/account) on the `API Key` tab. ',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
{
|
||||
id: 'email',
|
||||
name: 'Email',
|
||||
description:
|
||||
'Your Offcloud email. (These credentials are necessary for some addons)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
@@ -360,7 +354,7 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'Password',
|
||||
description:
|
||||
'Your Offcloud password. (These credentials are necessary for some addons)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
@@ -377,18 +371,16 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'Client ID',
|
||||
description:
|
||||
'Your put.io Client ID. Obtain it from [here](https://app.put.io/oauth)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
{
|
||||
id: 'token',
|
||||
name: 'Token',
|
||||
description:
|
||||
'Your put.io Token. Obtain it from [here](https://app.put.io/oauth)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -404,16 +396,15 @@ const SERVICE_DETAILS: Record<
|
||||
id: 'username',
|
||||
name: 'Username',
|
||||
description: 'Your Easynews username',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
name: 'Password',
|
||||
description: 'Your Easynews password',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -430,9 +421,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'API Key',
|
||||
description:
|
||||
'Your EasyDebrid API key. Obtain it from [here](https://paradise-cloud.com/dashboard/)',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -448,16 +438,15 @@ const SERVICE_DETAILS: Record<
|
||||
id: 'email',
|
||||
name: 'Email',
|
||||
description: 'Your PikPak email address',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'password',
|
||||
name: 'Password',
|
||||
description: 'Your PikPak password',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -474,9 +463,8 @@ const SERVICE_DETAILS: Record<
|
||||
name: 'Encoded Token',
|
||||
description:
|
||||
'Please authorise at MediaFusion and copy the token into here.',
|
||||
type: 'string',
|
||||
type: 'password',
|
||||
required: true,
|
||||
sensitive: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -820,6 +808,7 @@ export {
|
||||
REALDEBRID_SERVICE,
|
||||
PREMIUMIZE_SERVICE,
|
||||
ALLEDEBRID_SERVICE,
|
||||
DEBRIDLINK_SERVICE,
|
||||
TORBOX_SERVICE,
|
||||
EASYDEBRID_SERVICE,
|
||||
PUTIO_SERVICE,
|
||||
|
||||
@@ -71,6 +71,9 @@ type ErrorResponse = {
|
||||
|
||||
export type Response = SuccessResponse | ErrorResponse;
|
||||
|
||||
export function isEncrypted(data: string): boolean {
|
||||
return data.startsWith('aioEncrypt:');
|
||||
}
|
||||
/**
|
||||
* Encrypts a string using AES-256-CBC encryption, returns a string in the format "iv:encrypted" where
|
||||
* iv and encrypted are url encoded.
|
||||
@@ -87,7 +90,7 @@ export function encryptString(data: string, secretKey?: Buffer): Response {
|
||||
const { iv, data: encrypted } = encryptData(secretKey, compressed);
|
||||
return {
|
||||
success: true,
|
||||
data: `${encodeURIComponent(iv)}:${encodeURIComponent(encrypted)}`,
|
||||
data: `aioEncrypt:${encodeURIComponent(iv)}:${encodeURIComponent(encrypted)}`,
|
||||
error: null,
|
||||
};
|
||||
} catch (error: any) {
|
||||
@@ -111,7 +114,7 @@ export function decryptString(data: string, secretKey?: Buffer): Response {
|
||||
secretKey = Buffer.from(Env.SECRET_KEY, 'hex');
|
||||
}
|
||||
try {
|
||||
const [ivHex, encryptedHex] = data.split(':').map(decodeURIComponent);
|
||||
const [_, ivHex, encryptedHex] = data.split(':').map(decodeURIComponent);
|
||||
const iv = Buffer.from(ivHex, 'base64');
|
||||
const encrypted = Buffer.from(encryptedHex, 'base64');
|
||||
const decrypted = decryptData(secretKey, encrypted, iv);
|
||||
|
||||
+133
-71
@@ -216,14 +216,6 @@ export const Env = cleanEnv(process.env, {
|
||||
default: '',
|
||||
desc: 'API key for the addon, can be set to anything',
|
||||
}),
|
||||
SHOW_DIE: bool({
|
||||
default: false,
|
||||
desc: 'Show a game die emoji in streams for non-custom formats',
|
||||
}),
|
||||
DETERMINISTIC_ADDON_ID: bool({
|
||||
default: true,
|
||||
desc: 'Deterministic addon ID',
|
||||
}),
|
||||
DATABASE_URI: str({
|
||||
default: 'sqlite://./data/db.sqlite',
|
||||
desc: 'Database URI for the addon',
|
||||
@@ -248,10 +240,6 @@ export const Env = cleanEnv(process.env, {
|
||||
default: undefined,
|
||||
desc: 'TMDB Read Access Token. Used for fetching metadata for the strict title matching option.',
|
||||
}),
|
||||
DISABLE_CUSTOM_CONFIG_GENERATOR_ROUTE: bool({
|
||||
default: false,
|
||||
desc: 'Disable custom config generator route',
|
||||
}),
|
||||
|
||||
// logging settings
|
||||
LOG_SENSITIVE_INFO: bool({
|
||||
@@ -409,79 +397,153 @@ export const Env = cleanEnv(process.env, {
|
||||
desc: 'Default proxy proxied services',
|
||||
}),
|
||||
|
||||
// // MediaFlow settings
|
||||
// FORCE_MEDIAFLOW_URL: url({
|
||||
// default: undefined,
|
||||
// desc: 'Force MediaFlow URL',
|
||||
// }),
|
||||
// FORCE_MEDIAFLOW_API_PASSWORD: str({
|
||||
// default: undefined,
|
||||
// desc: 'Force MediaFlow API password',
|
||||
// }),
|
||||
// FORCE_MEDIAFLOW_PUBLIC_IP: str({
|
||||
// default: undefined,
|
||||
// desc: 'Force MediaFlow public IP',
|
||||
// }),
|
||||
// DEFAULT_MEDIAFLOW_URL: url({
|
||||
// default: '',
|
||||
// desc: 'Default MediaFlow URL',
|
||||
// }),
|
||||
// DEFAULT_MEDIAFLOW_API_PASSWORD: str({
|
||||
// default: '',
|
||||
// desc: 'Default MediaFlow API password',
|
||||
// }),
|
||||
// DEFAULT_MEDIAFLOW_PUBLIC_IP: str({
|
||||
// default: '',
|
||||
// desc: 'Default MediaFlow public IP',
|
||||
// }),
|
||||
// MEDIAFLOW_IP_TIMEOUT: num({
|
||||
// default: 30000,
|
||||
// desc: 'MediaFlow IP timeout',
|
||||
// }),
|
||||
ENCRYPT_MEDIAFLOW_URLS: bool({
|
||||
default: true,
|
||||
desc: 'Encrypt MediaFlow URLs',
|
||||
}),
|
||||
|
||||
// // StremThru settings
|
||||
// FORCE_STREMTHRU_URL: url({
|
||||
// default: undefined,
|
||||
// desc: 'Force StremThru URL',
|
||||
// }),
|
||||
// FORCE_STREMTHRU_CREDENTIAL: str({
|
||||
// default: undefined,
|
||||
// desc: 'Force StremThru credential',
|
||||
// }),
|
||||
// FORCE_STREMTHRU_PUBLIC_IP: str({
|
||||
// default: undefined,
|
||||
// desc: 'Force StremThru public IP',
|
||||
// }),
|
||||
// DEFAULT_STREMTHRU_URL: url({
|
||||
// default: '',
|
||||
// desc: 'Default StremThru URL',
|
||||
// }),
|
||||
// DEFAULT_STREMTHRU_CREDENTIAL: str({
|
||||
// default: '',
|
||||
// desc: 'Default StremThru credential',
|
||||
// }),
|
||||
// DEFAULT_STREMTHRU_PUBLIC_IP: str({
|
||||
// default: '',
|
||||
// desc: 'Default StremThru public IP',
|
||||
// }),
|
||||
// STREMTHRU_TIMEOUT: num({
|
||||
// default: 30000,
|
||||
// desc: 'StremThru timeout',
|
||||
// }),
|
||||
ENCRYPT_STREMTHRU_URLS: bool({
|
||||
default: true,
|
||||
desc: 'Encrypt StremThru URLs',
|
||||
}),
|
||||
|
||||
// service settings
|
||||
DEFAULT_REALDEBRID_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default RealDebrid API key',
|
||||
}),
|
||||
DEFAULT_ALLDEBRID_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default AllDebrid API key',
|
||||
}),
|
||||
DEFAULT_PREMIUMIZE_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default Premiumize API key',
|
||||
}),
|
||||
DEFAULT_DEBRIDLINK_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default DebridLink API key',
|
||||
}),
|
||||
DEFAULT_TORBOX_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default Torbox API key',
|
||||
}),
|
||||
DEFAULT_OFFCLOUD_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default OffCloud API key',
|
||||
}),
|
||||
DEFAULT_OFFCLOUD_EMAIL: str({
|
||||
default: undefined,
|
||||
desc: 'Default OffCloud email',
|
||||
}),
|
||||
DEFAULT_OFFCLOUD_PASSWORD: str({
|
||||
default: undefined,
|
||||
desc: 'Default OffCloud password',
|
||||
}),
|
||||
DEFAULT_PUTIO_CLIENT_ID: str({
|
||||
default: undefined,
|
||||
desc: 'Default Putio client id',
|
||||
}),
|
||||
DEFAULT_PUTIO_CLIENT_SECRET: str({
|
||||
default: undefined,
|
||||
desc: 'Default Putio client secret',
|
||||
}),
|
||||
DEFAULT_EASYNEWS_USERNAME: str({
|
||||
default: undefined,
|
||||
desc: 'Default EasyNews username',
|
||||
}),
|
||||
DEFAULT_EASYNEWS_PASSWORD: str({
|
||||
default: undefined,
|
||||
desc: 'Default EasyNews password',
|
||||
}),
|
||||
DEFAULT_EASYDEBRID_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Default EasyDebrid API key',
|
||||
}),
|
||||
DEFAULT_PIKPAK_EMAIL: str({
|
||||
default: undefined,
|
||||
desc: 'Default PikPak email',
|
||||
}),
|
||||
DEFAULT_PIKPAK_PASSWORD: str({
|
||||
default: undefined,
|
||||
desc: 'Default PikPak password',
|
||||
}),
|
||||
DEFAULT_SEEDR_ENCODED_TOKEN: str({
|
||||
default: undefined,
|
||||
desc: 'Default Seedr encoded token',
|
||||
}),
|
||||
|
||||
// forced services
|
||||
FORCED_REALDEBRID_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced RealDebrid API key',
|
||||
}),
|
||||
FORCED_ALLDEBRID_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced AllDebrid API key',
|
||||
}),
|
||||
FORCED_PREMIUMIZE_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced Premiumize API key',
|
||||
}),
|
||||
FORCED_DEBRIDLINK_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced DebridLink API key',
|
||||
}),
|
||||
FORCED_TORBOX_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced Torbox API key',
|
||||
}),
|
||||
FORCED_OFFCLOUD_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced OffCloud API key',
|
||||
}),
|
||||
FORCED_OFFCLOUD_EMAIL: str({
|
||||
default: undefined,
|
||||
desc: 'Forced OffCloud email',
|
||||
}),
|
||||
FORCED_OFFCLOUD_PASSWORD: str({
|
||||
default: undefined,
|
||||
desc: 'Forced OffCloud password',
|
||||
}),
|
||||
FORCED_PUTIO_CLIENT_ID: str({
|
||||
default: undefined,
|
||||
desc: 'Forced Putio client id',
|
||||
}),
|
||||
FORCED_PUTIO_CLIENT_SECRET: str({
|
||||
default: undefined,
|
||||
desc: 'Forced Putio client secret',
|
||||
}),
|
||||
FORCED_EASYNEWS_USERNAME: str({
|
||||
default: undefined,
|
||||
desc: 'Forced EasyNews username',
|
||||
}),
|
||||
FORCED_EASYNEWS_PASSWORD: str({
|
||||
default: undefined,
|
||||
desc: 'Forced EasyNews password',
|
||||
}),
|
||||
FORCED_EASYDEBRID_API_KEY: str({
|
||||
default: undefined,
|
||||
desc: 'Forced EasyDebrid API key',
|
||||
}),
|
||||
FORCED_PIKPAK_EMAIL: str({
|
||||
default: undefined,
|
||||
desc: 'Forced PikPak email',
|
||||
}),
|
||||
FORCED_PIKPAK_PASSWORD: str({
|
||||
default: undefined,
|
||||
desc: 'Forced PikPak password',
|
||||
}),
|
||||
FORCED_SEEDR_ENCODED_TOKEN: str({
|
||||
default: undefined,
|
||||
desc: 'Forced Seedr encoded token',
|
||||
}),
|
||||
|
||||
COMET_URL: url({
|
||||
default: 'https://comet.elfhosted.com/',
|
||||
desc: 'Comet URL',
|
||||
}),
|
||||
COMET_INDEXERS: json({
|
||||
DEFAULT_COMET_INDEXERS: json({
|
||||
default: ['dmm_public_hash_shares_only'],
|
||||
desc: 'Comet indexers',
|
||||
}),
|
||||
|
||||
@@ -9,9 +9,8 @@ export function makeUrlLogSafe(url: string) {
|
||||
// for each component of the path, if it is longer than 10 characters, mask it
|
||||
return url
|
||||
.split('/')
|
||||
.filter((component) => !component.includes('.'))
|
||||
.map((component) => {
|
||||
if (component.length > 10) {
|
||||
if (component.length > 10 && !component.includes('.')) {
|
||||
return maskSensitiveInfo(component);
|
||||
}
|
||||
return component;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useStatus } from '@/context/status';
|
||||
import { PageWrapper } from '../shared/page-wrapper';
|
||||
import {
|
||||
SERVICE_DETAILS,
|
||||
// SERVICE_DETAILS,
|
||||
ServiceId,
|
||||
} from '../../../../core/src/utils/constants';
|
||||
import { useUserData } from '@/context/userData';
|
||||
@@ -49,13 +49,13 @@ export function ServicesMenu() {
|
||||
//
|
||||
|
||||
function Content() {
|
||||
const status = useStatus();
|
||||
const { status } = useStatus();
|
||||
if (!status) return null;
|
||||
const { setUserData, userData } = useUserData();
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const [modalService, setModalService] = useState<ServiceId | null>(null);
|
||||
const [modalValues, setModalValues] = useState<Record<string, any>>({});
|
||||
const [invalidServices, setInvalidServices] = useState<string[]>([]);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// DND logic
|
||||
@@ -97,11 +97,6 @@ function Content() {
|
||||
const newUserData = { ...prev };
|
||||
newUserData.services = (newUserData.services ?? []).map((service) => {
|
||||
if (service.id === modalService) {
|
||||
if (invalidServices.includes(SERVICE_DETAILS[service.id].name)) {
|
||||
setInvalidServices((prev) =>
|
||||
prev.filter((a) => a !== SERVICE_DETAILS[service.id].name)
|
||||
);
|
||||
}
|
||||
return {
|
||||
...service,
|
||||
enabled: true,
|
||||
@@ -116,28 +111,79 @@ function Content() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const allServiceIds = Object.keys(SERVICE_DETAILS);
|
||||
const allServiceIds: ServiceId[] = Object.keys(
|
||||
status.settings.services
|
||||
) as ServiceId[];
|
||||
const currentServices = userData.services ?? [];
|
||||
// Remove any services not in SERVICE_DETAILS
|
||||
let filtered = currentServices.filter((s: { id: string }) =>
|
||||
allServiceIds.includes(s.id)
|
||||
);
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
||||
// Add any missing services from SERVICE_DETAILS
|
||||
const missing = allServiceIds.filter(
|
||||
(id) => !filtered.some((s: { id: string }) => s.id === id)
|
||||
(id) => !filtered.some((s) => s.id === id)
|
||||
);
|
||||
|
||||
if (missing.length > 0 || filtered.length !== currentServices.length) {
|
||||
const toAdd = missing.map((id) => ({
|
||||
id,
|
||||
enabled: false,
|
||||
credentials: {},
|
||||
}));
|
||||
const toAdd = missing.map((id) => {
|
||||
const svcMeta = status.settings.services[id]!;
|
||||
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,
|
||||
credentials,
|
||||
};
|
||||
});
|
||||
|
||||
setUserData((prev: any) => ({
|
||||
...prev,
|
||||
services: [...filtered, ...toAdd],
|
||||
}));
|
||||
}
|
||||
}, [userData.services, setUserData]);
|
||||
}, [status.settings.services]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
@@ -167,6 +213,19 @@ function Content() {
|
||||
};
|
||||
}, [isDragging]);
|
||||
|
||||
const invalidServices =
|
||||
userData.services
|
||||
?.filter((service) => {
|
||||
const svcMeta = status.settings.services[service.id];
|
||||
if (!svcMeta) return false;
|
||||
// Check if any required credential is missing
|
||||
return (
|
||||
service.enabled &&
|
||||
svcMeta.credentials.some((cred) => !service.credentials?.[cred.id])
|
||||
);
|
||||
})
|
||||
.map((service) => status.settings.services[service.id]?.name) ?? [];
|
||||
|
||||
// Render
|
||||
return (
|
||||
<>
|
||||
@@ -179,7 +238,7 @@ function Content() {
|
||||
</div>
|
||||
<div className="flex flex-1"></div>
|
||||
</div>
|
||||
{invalidServices.length > 0 && (
|
||||
{invalidServices && invalidServices.length > 0 && (
|
||||
<div className="mb-6">
|
||||
<Alert
|
||||
intent="alert"
|
||||
@@ -225,7 +284,7 @@ function Content() {
|
||||
</li>
|
||||
) : (
|
||||
userData.services?.map((service, idx) => {
|
||||
const svcMeta = SERVICE_DETAILS[service.id];
|
||||
const svcMeta = status.settings.services[service.id]!;
|
||||
return (
|
||||
<SortableServiceItem
|
||||
key={service.id}
|
||||
@@ -234,36 +293,6 @@ function Content() {
|
||||
onEdit={() => handleServiceClick(service.id)}
|
||||
onToggleEnabled={(v: boolean) => {
|
||||
setUserData((prev) => {
|
||||
const existingService = prev.services?.find(
|
||||
(s) => s.id === service.id
|
||||
);
|
||||
if (!existingService) return prev;
|
||||
const missingCredentials =
|
||||
svcMeta.credentials.filter(
|
||||
(cred) =>
|
||||
!existingService.credentials?.[cred.id]
|
||||
);
|
||||
const invalidService = svcMeta.name;
|
||||
if (missingCredentials.length > 0) {
|
||||
if (v) {
|
||||
if (!invalidServices.includes(invalidService)) {
|
||||
setInvalidServices((prev) => [
|
||||
...prev,
|
||||
invalidService,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
setInvalidServices((prev) =>
|
||||
prev.filter((a) => a !== invalidService)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (invalidServices.includes(invalidService)) {
|
||||
setInvalidServices((prev) =>
|
||||
prev.filter((a) => a !== invalidService)
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
services: (prev.services ?? []).map((s) =>
|
||||
@@ -318,6 +347,9 @@ function SortableServiceItem({
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
const disableEdit = meta.credentials.every((cred: any) => {
|
||||
return cred.forced;
|
||||
});
|
||||
return (
|
||||
<li ref={setNodeRef} style={style}>
|
||||
<div className="px-2.5 py-2 bg-[var(--background)] rounded-[--radius-md] border flex gap-3 relative">
|
||||
@@ -335,11 +367,16 @@ function SortableServiceItem({
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Switch value={!!service.enabled} onValueChange={onToggleEnabled} />
|
||||
<Switch
|
||||
value={!!service.enabled}
|
||||
onValueChange={onToggleEnabled}
|
||||
disabled={disableEdit}
|
||||
/>
|
||||
<IconButton
|
||||
icon={<FiSettings />}
|
||||
intent="primary-outline"
|
||||
onClick={onEdit}
|
||||
disabled={disableEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,8 +401,10 @@ function ServiceModal({
|
||||
onSubmit: (v: Record<string, any>) => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { status } = useStatus();
|
||||
if (!status) return null;
|
||||
if (!serviceId) return null;
|
||||
const meta = SERVICE_DETAILS[serviceId];
|
||||
const meta = status.settings.services[serviceId]!;
|
||||
const credentials = meta.credentials || [];
|
||||
return (
|
||||
<Modal
|
||||
@@ -384,7 +423,7 @@ function ServiceModal({
|
||||
<TemplateOption
|
||||
key={opt.id}
|
||||
option={opt}
|
||||
value={values[opt.id]}
|
||||
value={opt.forced || opt.default || values[opt.id]}
|
||||
onChange={(v) => onChange({ ...values, [opt.id]: v })}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -36,12 +36,34 @@ const TemplateOption: React.FC<TemplateOptionProps> = ({
|
||||
required,
|
||||
options,
|
||||
constraints,
|
||||
forced,
|
||||
default: defaultValue,
|
||||
emptyIsUndefined = false,
|
||||
} = option;
|
||||
|
||||
const isDisabled = disabled;
|
||||
const isDisabled = disabled || !!forced;
|
||||
|
||||
switch (type) {
|
||||
case 'password':
|
||||
return (
|
||||
<div>
|
||||
<TextInput
|
||||
type="password"
|
||||
label={name}
|
||||
value={forced || defaultValue || value}
|
||||
onValueChange={(value: string) =>
|
||||
onChange(emptyIsUndefined ? value || undefined : value)
|
||||
}
|
||||
required={required}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
{description && (
|
||||
<div className="text-xs text-[--muted] mt-1">
|
||||
<MarkdownLite>{description}</MarkdownLite>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
case 'string':
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -24,18 +24,34 @@ export const errorMiddleware = (
|
||||
error = err;
|
||||
}
|
||||
|
||||
res.status(error.statusCode).json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
},
|
||||
req.path,
|
||||
true
|
||||
)
|
||||
let stremioResponse = false;
|
||||
const match = req.originalUrl.match(
|
||||
/\/stremio(?:\/[^\/]+){0,2}\/(stream|catalog|subtitles|meta)/
|
||||
);
|
||||
if (match) {
|
||||
stremioResponse = true;
|
||||
}
|
||||
|
||||
res
|
||||
.status(
|
||||
stremioResponse
|
||||
? req.userData?.hideErrors
|
||||
? error.statusCode
|
||||
: 200
|
||||
: error.statusCode
|
||||
)
|
||||
.json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: error.code,
|
||||
message: error.message,
|
||||
},
|
||||
},
|
||||
req.path,
|
||||
true
|
||||
)
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
decryptString,
|
||||
validateConfig,
|
||||
} from '@aiostreams/core';
|
||||
import { UserDataSchema, UserRepository } from '@aiostreams/core';
|
||||
import { UserDataSchema, UserRepository, UserData } from '@aiostreams/core';
|
||||
|
||||
const logger = createLogger('server');
|
||||
|
||||
@@ -74,19 +74,17 @@ export const userDataMiddleware = async (
|
||||
next(new APIError(constants.ErrorCode.USER_INVALID_PASSWORD));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
validateConfig(decryptedConfig, true);
|
||||
} catch (error) {
|
||||
next(new APIError(constants.ErrorCode.USER_INVALID_CONFIG));
|
||||
return;
|
||||
}
|
||||
|
||||
// Attach validated data to request
|
||||
req.userData = decryptedConfig;
|
||||
req.userData.ip = req.userIp;
|
||||
req.uuid = uuid;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new APIError(constants.ErrorCode.USER_ERROR));
|
||||
} catch (error: any) {
|
||||
logger.error(error.message);
|
||||
if (error instanceof APIError) {
|
||||
next(error);
|
||||
} else {
|
||||
next(new APIError(constants.ErrorCode.USER_ERROR));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { Env, PresetManager, UserRepository } from '@aiostreams/core';
|
||||
// import { PresetMetadata } from '@aiostreams/core/src/presets';
|
||||
import { PresetMetadata, StatusResponse } from '@aiostreams/core';
|
||||
import { APIError } from '@aiostreams/core/';
|
||||
import { constants, encryptString } from '@aiostreams/core';
|
||||
import {
|
||||
Env,
|
||||
getEnvironmentServiceDetails,
|
||||
PresetManager,
|
||||
UserRepository,
|
||||
} from '@aiostreams/core';
|
||||
import { StatusResponse } from '@aiostreams/core';
|
||||
import { encryptString } from '@aiostreams/core';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -56,6 +59,7 @@ router.get('/', async (req: Request, res: Response) => {
|
||||
excludedRegex: Env.DEFAULT_EXCLUDED_REGEX_PATTERNS ?? null,
|
||||
},
|
||||
presets: PresetManager.getPresetList(),
|
||||
services: getEnvironmentServiceDetails(),
|
||||
},
|
||||
};
|
||||
res.status(200).json({
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createResponse } from '../../utils/responses';
|
||||
const logger = createLogger('stremio/addonCatalog');
|
||||
const router = Router();
|
||||
|
||||
router.get('/:type/:id.json', async (req, res) => {
|
||||
router.get('/:type/:id.json', async (req, res, next) => {
|
||||
if (!req.userData) {
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
@@ -65,20 +65,7 @@ router.get('/:type/:id.json', async (req, res) => {
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error processing addon catalog request', { error });
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: constants.ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
},
|
||||
req.originalUrl,
|
||||
true
|
||||
)
|
||||
);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const router = Router();
|
||||
|
||||
router.use(stremioCatalogRateLimiter);
|
||||
|
||||
router.get('/:type/:id/:extras?.json', async (req, res) => {
|
||||
router.get('/:type/:id/:extras?.json', async (req, res, next) => {
|
||||
if (!req.userData) {
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
@@ -61,19 +61,7 @@ router.get('/:type/:id/:extras?.json', async (req, res) => {
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: constants.ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
},
|
||||
req.originalUrl,
|
||||
true
|
||||
)
|
||||
);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -17,14 +17,13 @@ export default router;
|
||||
|
||||
const manifest = async (config?: UserData): Promise<Manifest> => {
|
||||
let addonId = Env.ADDON_ID;
|
||||
if (config && Env.DETERMINISTIC_ADDON_ID) {
|
||||
if (config) {
|
||||
addonId = addonId += `.${config.uuid?.substring(0, 12)}`;
|
||||
}
|
||||
let catalogs: Manifest['catalogs'] = [];
|
||||
let resources: Manifest['resources'] = [];
|
||||
if (config) {
|
||||
const aiostreams = new AIOStreams(config);
|
||||
// wait till initialized
|
||||
|
||||
await aiostreams.initialise();
|
||||
|
||||
@@ -65,7 +64,6 @@ router.get('/', async (req, res, next) => {
|
||||
res.status(200).json(await manifest(req.userData));
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate manifest: ${error}`);
|
||||
logger.verbose(JSON.stringify(req.userData, null, 2));
|
||||
next(new APIError(constants.ErrorCode.INTERNAL_SERVER_ERROR));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createResponse } from '../../utils/responses';
|
||||
const logger = createLogger('stremio/meta');
|
||||
const router = Router();
|
||||
|
||||
router.get('/:type/:id.json', async (req, res) => {
|
||||
router.get('/:type/:id.json', async (req, res, next) => {
|
||||
if (!req.userData) {
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
@@ -61,20 +61,7 @@ router.get('/:type/:id.json', async (req, res) => {
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error processing meta request', { error });
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: constants.ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
},
|
||||
req.originalUrl,
|
||||
true
|
||||
)
|
||||
);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -55,20 +55,7 @@ router.get('/:type/:id.json', async (req, res, next) => {
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: constants.ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
message: `An error occurred while fetching streams: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
},
|
||||
req.originalUrl,
|
||||
true
|
||||
)
|
||||
);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createResponse } from '../../utils/responses';
|
||||
const logger = createLogger('stremio/subtitle');
|
||||
const router = Router();
|
||||
|
||||
router.get('/:type/:id.json', async (req, res) => {
|
||||
router.get('/:type/:id.json', async (req, res, next) => {
|
||||
if (!req.userData) {
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
@@ -59,20 +59,7 @@ router.get('/:type/:id.json', async (req, res) => {
|
||||
)
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('Error processing subtitle request', { error });
|
||||
res.status(200).json(
|
||||
createResponse(
|
||||
{
|
||||
success: false,
|
||||
error: {
|
||||
code: constants.ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
},
|
||||
req.originalUrl,
|
||||
true
|
||||
)
|
||||
);
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user