fix(api keys): add API key warnings

adds a warning to the config source selection if the API key has not been set for the source
This commit is contained in:
Tom Wheeler
2025-09-20 02:35:12 +12:00
parent dd5055e35f
commit bc4a1700c6
4 changed files with 225 additions and 10 deletions
@@ -0,0 +1,41 @@
import Alert from '@app/components/Common/Alert';
import type { ApiKeyValidationResult } from '@app/utils/apiKeyValidation';
import type React from 'react';
import { defineMessages, useIntl } from 'react-intl';
const messages = defineMessages({
apiKeyWarning:
'{services} API key required. Configure in Settings > Sources.',
});
interface ApiKeyWarningProps {
validation: ApiKeyValidationResult;
className?: string;
}
const ApiKeyWarning: React.FC<ApiKeyWarningProps> = ({
validation,
className = '',
}) => {
const intl = useIntl();
// Don't show warning if all required keys are present
if (validation.hasRequiredKeys) {
return null;
}
const servicesText = validation.missingServices.join(', ');
return (
<div className={`mt-2 ${className}`}>
<Alert
title={intl.formatMessage(messages.apiKeyWarning, {
services: servicesText,
})}
type="warning"
/>
</div>
);
};
export default ApiKeyWarning;
@@ -1,7 +1,17 @@
import type { CollectionFormConfig } from '@app/types/collections';
import { validateApiKeysForCollectionType } from '@app/utils/apiKeyValidation';
import type {
MDBListSettings,
OverseerrSettings,
TautulliSettings,
TraktSettings,
} from '@server/lib/settings';
import { Field, type FormikErrors, type FormikTouched } from 'formik';
import type React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import ApiKeyWarning from './ApiKeyWarning';
interface TemplatePreset {
value: string;
@@ -42,8 +52,30 @@ const CollectionTypeSection = ({
}: CollectionTypeSectionProps) => {
const intl = useIntl();
// Fetch API settings for validation
const { data: traktSettings } = useSWR<TraktSettings>(
'/api/v1/settings/trakt'
);
const { data: mdblistSettings } = useSWR<MDBListSettings>(
'/api/v1/settings/mdblist'
);
const { data: tautulliSettings } = useSWR<TautulliSettings>(
'/api/v1/settings/tautulli'
);
const { data: overseerrSettings } = useSWR<OverseerrSettings>(
'/api/v1/settings/overseerr'
);
if (!isVisible) return null;
// Validate API keys for the current collection type
const apiKeyValidation = validateApiKeysForCollectionType(values.type || '', {
trakt: traktSettings,
mdblist: mdblistSettings,
tautulli: tautulliSettings,
overseerr: overseerrSettings,
});
const collectionTypes = [
{ value: 'overseerr', label: 'Overseerr Requests' },
{ value: 'tautulli', label: 'Tautulli Statistics' },
@@ -236,6 +268,9 @@ const CollectionTypeSection = ({
</option>
))}
</Field>
{/* API Key Warning - Show after type selection */}
{values.type && <ApiKeyWarning validation={apiKeyValidation} />}
</div>
{/* Collection Sub-Type */}
@@ -4,11 +4,20 @@ import type {
MultiSourceCombineMode,
MultiSourceType,
} from '@app/types/collections';
import { validateApiKeysForCollectionType } from '@app/utils/apiKeyValidation';
import type {
MDBListSettings,
OverseerrSettings,
TautulliSettings,
TraktSettings,
} from '@server/lib/settings';
import { Field } from 'formik';
import type React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import useSWR from 'swr';
import ApiKeyWarning from './ApiKeyWarning';
const messages = defineMessages({
sourceType: 'Source Type',
sourceSubtype: 'Collection Sub-Type',
@@ -122,6 +131,20 @@ const MultiSourceConfigSection = ({
(url) => fetch(url).then((res) => res.json())
);
// Fetch API settings for validation
const { data: traktSettings } = useSWR<TraktSettings>(
'/api/v1/settings/trakt'
);
const { data: mdblistSettings } = useSWR<MDBListSettings>(
'/api/v1/settings/mdblist'
);
const { data: tautulliSettings } = useSWR<TautulliSettings>(
'/api/v1/settings/tautulli'
);
const { data: overseerrSettings } = useSWR<OverseerrSettings>(
'/api/v1/settings/overseerr'
);
if (!isVisible) return null;
const sources = values.sources || [];
@@ -214,16 +237,6 @@ const MultiSourceConfigSection = ({
];
case 'mdblist':
return [
{
value: 'user_lists',
label: 'User Lists',
description: 'Your personal MDBList lists',
},
{
value: 'top_lists',
label: 'Top Lists',
description: 'Most popular public lists on MDBList',
},
{
value: 'custom',
label: 'Custom List',
@@ -374,6 +387,22 @@ const MultiSourceConfigSection = ({
<option value="mdblist">MDBList Lists</option>
<option value="networks">Networks</option>
</Field>
{/* API Key Warning for this source */}
{values.sources?.[index]?.type &&
(() => {
const sourceType = values.sources[index].type;
const apiKeyValidation = validateApiKeysForCollectionType(
sourceType,
{
trakt: traktSettings,
mdblist: mdblistSettings,
tautulli: tautulliSettings,
overseerr: overseerrSettings,
}
);
return <ApiKeyWarning validation={apiKeyValidation} />;
})()}
</div>
{values.sources?.[index]?.type &&
+110
View File
@@ -0,0 +1,110 @@
import type {
MDBListSettings,
OverseerrSettings,
TautulliSettings,
TraktSettings,
} from '@server/lib/settings';
export interface ApiKeyRequirement {
service: string;
required: boolean;
configured: boolean;
settingsPath: string;
}
export interface ApiKeyValidationResult {
hasRequiredKeys: boolean;
missingServices: string[];
requirements: ApiKeyRequirement[];
}
/**
* Check which collection types require API keys and whether they are configured
*/
export function validateApiKeysForCollectionType(
collectionType: string,
settings: {
trakt?: TraktSettings;
mdblist?: MDBListSettings;
tautulli?: TautulliSettings;
overseerr?: OverseerrSettings;
}
): ApiKeyValidationResult {
const requirements: ApiKeyRequirement[] = [];
switch (collectionType) {
case 'trakt':
requirements.push({
service: 'Trakt',
required: true,
configured: !!settings.trakt?.apiKey,
settingsPath: '/settings/sources',
});
break;
case 'mdblist':
requirements.push({
service: 'MDBList',
required: true,
configured: !!settings.mdblist?.apiKey,
settingsPath: '/settings/sources',
});
break;
case 'tautulli':
requirements.push({
service: 'Tautulli',
required: true,
configured: !!settings.tautulli?.apiKey,
settingsPath: '/settings/sources',
});
break;
case 'overseerr':
requirements.push({
service: 'Overseerr',
required: true,
configured: !!settings.overseerr?.apiKey,
settingsPath: '/settings/sources',
});
break;
// These don't require API keys
case 'imdb':
case 'tmdb':
case 'letterboxd':
case 'networks':
case 'multi-source':
default:
// No API key requirements
break;
}
const missingServices = requirements
.filter((req) => req.required && !req.configured)
.map((req) => req.service);
return {
hasRequiredKeys: missingServices.length === 0,
missingServices,
requirements,
};
}
/**
* Get user-friendly service names
*/
export function getServiceDisplayName(serviceType: string): string {
const serviceNames: Record<string, string> = {
trakt: 'Trakt',
mdblist: 'MDBList',
tautulli: 'Tautulli',
overseerr: 'Overseerr',
tmdb: 'TMDb',
imdb: 'IMDb',
letterboxd: 'Letterboxd',
networks: 'Networks',
};
return serviceNames[serviceType] || serviceType;
}