mirror of
https://github.com/agregarr/agregarr.git
synced 2026-04-30 06:59:30 -05:00
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:
@@ -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 &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user