Merge pull request #1200 from sv-dispatcharr/feat/plugin-hub-disclaimer
Some checks are pending
CI Pipeline / prepare (push) Waiting to run
CI Pipeline / docker (amd64, ubuntu-24.04) (push) Blocked by required conditions
CI Pipeline / docker (arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
CI Pipeline / create-manifest (push) Blocked by required conditions
Build and Push Multi-Arch Docker Image / build-and-push (push) Waiting to run
Frontend Tests / test (push) Waiting to run

This commit is contained in:
SergeantPanda 2026-04-18 19:28:50 -05:00 committed by GitHub
commit 3586dba677
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 244 additions and 36 deletions

View file

@ -0,0 +1,120 @@
import React from 'react';
import { Box, Text } from '@mantine/core';
import { AlertTriangle, Info, OctagonAlert } from 'lucide-react';
import dispatcharrLogo from '../images/logo.png';
export const PluginSecurityWarning = ({ children }) => (
<Box
style={{
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.35)',
borderRadius: 'var(--mantine-radius-sm)',
padding: '10px 14px',
display: 'flex',
gap: 10,
alignItems: 'flex-start',
}}
>
<Box style={{ color: '#ef4444', flexShrink: 0, paddingTop: 1 }}>
<OctagonAlert size={16} />
</Box>
<Text size="xs" style={{ color: '#f87171' }}>
{children}
</Text>
</Box>
);
export const PluginSupportDisclaimer = () => (
<Box
style={{
background: 'rgba(20, 145, 126, 0.1)',
border: '1px solid rgba(20, 145, 126, 0.35)',
borderRadius: 'var(--mantine-radius-sm)',
padding: '10px 14px',
display: 'flex',
gap: 10,
alignItems: 'flex-start',
}}
>
<Box style={{ flexShrink: 0, paddingTop: 1 }}>
<img
src={dispatcharrLogo}
alt="Dispatcharr"
width={16}
height={16}
draggable={false}
style={{ display: 'block', objectFit: 'contain' }}
/>
</Box>
<Text size="xs" style={{ color: '#4db8a8' }}>
Dispatcharr community support cannot assist with third-party plugin
issues. For help, use the plugin&apos;s Discord thread or submit an issue
on the plugin&apos;s repository.
</Text>
</Box>
);
export const PluginDowngradeWarning = ({ children }) => (
<Box
style={{
background: 'rgba(249, 115, 22, 0.1)',
border: '1px solid rgba(249, 115, 22, 0.35)',
borderRadius: 'var(--mantine-radius-sm)',
padding: '10px 14px',
display: 'flex',
gap: 10,
alignItems: 'flex-start',
}}
>
<Box style={{ color: '#f97316', flexShrink: 0, paddingTop: 1 }}>
<AlertTriangle size={16} />
</Box>
<Text size="xs" style={{ color: '#fb923c' }}>
{children}
</Text>
</Box>
);
export const PluginInfoNote = ({ children }) => (
<Box
style={{
background: 'rgba(148, 163, 184, 0.08)',
border: '1px solid rgba(148, 163, 184, 0.25)',
borderRadius: 'var(--mantine-radius-sm)',
padding: '10px 14px',
display: 'flex',
gap: 10,
alignItems: 'flex-start',
}}
>
<Box style={{ color: '#94a3b8', flexShrink: 0, paddingTop: 1 }}>
<Info size={16} />
</Box>
<Text size="xs" style={{ color: '#cbd5e1' }}>
{children}
</Text>
</Box>
);
export const PluginRestartWarning = () => (
<Box
style={{
background: 'rgba(234, 179, 8, 0.1)',
border: '1px solid rgba(234, 179, 8, 0.35)',
borderRadius: 'var(--mantine-radius-sm)',
padding: '10px 14px',
display: 'flex',
gap: 10,
alignItems: 'flex-start',
}}
>
<Box style={{ color: '#eab308', flexShrink: 0, paddingTop: 1 }}>
<AlertTriangle size={16} />
</Box>
<Text size="xs" style={{ color: '#ca8a04' }}>
Importing a plugin may briefly restart the backend (you might see a
temporary disconnect). Please wait a few seconds and the app will
reconnect automatically.
</Text>
</Box>
);

View file

@ -27,6 +27,12 @@ import {
ShieldCheck,
Trash2,
} from 'lucide-react';
import {
PluginDowngradeWarning,
PluginInfoNote,
PluginSecurityWarning,
PluginSupportDisclaimer,
} from '../PluginWarnings.jsx';
import { useNavigate, useLocation } from 'react-router-dom';
import API from '../../api';
import { usePluginStore } from '../../store/plugins';
@ -730,6 +736,13 @@ const AvailablePluginCard = ({
stop working with future versions of Dispatcharr. It is recommended
to look for an alternative.
</Text>
<PluginSecurityWarning>
Plugins run server-side code with full access to your Dispatcharr
instance and its data. Only install plugins from developers you
trust. Malicious plugins could read or modify data, call internal
APIs, or perform unwanted actions.
</PluginSecurityWarning>
<PluginSupportDisclaimer />
<Text size="sm" fw={500}>
Do you still want to proceed?
</Text>
@ -830,38 +843,37 @@ const AvailablePluginCard = ({
)}
.
</Text>
<Text size="sm" c="dimmed">
<PluginSecurityWarning>
Plugins run server-side code with full access to your
Dispatcharr instance and its data. Only install plugins from
developers you trust. Malicious plugins could read or modify
data, call internal APIs, or perform unwanted actions.
</Text>
</PluginSecurityWarning>
<PluginSupportDisclaimer />
{isDowngrade && (
<Text size="sm" c="orange">
<b>Warning:</b> Downgrading may cause issues with saved
settings or data.
</Text>
<PluginDowngradeWarning>
Downgrading may cause issues with saved settings or data.
</PluginDowngradeWarning>
)}
{isBadSig && (
<Text size="sm" c="red">
<b>Warning:</b> This repository has an invalid or unverified
signature. Installing plugins from unverified sources may be
risky.
</Text>
<PluginSecurityWarning>
This repository has an invalid or unverified signature.
Installing plugins from unverified sources may be risky.
</PluginSecurityWarning>
)}
{plugin.install_status === 'unmanaged' && (
<Text size="sm" c="orange">
<b>Note:</b> This plugin was installed manually. Installing
from this repo will bring it under repo management and enable
future update checks.
</Text>
<PluginInfoNote>
This plugin was installed manually. Installing from this repo
will bring it under repo management and enable future update
checks.
</PluginInfoNote>
)}
{plugin.install_status === 'different_repo' && (
<Text size="sm" c="orange">
<b>Note:</b> This plugin is currently managed by{' '}
<PluginInfoNote>
This plugin is currently managed by{' '}
<b>{plugin.installed_source_repo_name || 'another repo'}</b>.
Installing will transfer management to this repo.
</Text>
</PluginInfoNote>
)}
<Text size="sm" fw={500}>
Are you sure you want to proceed?

View file

@ -17,7 +17,7 @@ import {
Text,
Tooltip,
} from '@mantine/core';
import { Ban, Check, FlaskConical, Info, RefreshCw, Settings, Trash2, Zap } from 'lucide-react';
import { Ban, Check, Download, FlaskConical, Info, RefreshCw, Settings, Trash2, Zap } from 'lucide-react';
import { getConfirmationDetails } from '../../utils/cards/PluginCardUtils.js';
import { SUBSCRIPTION_EVENTS } from '../../constants.js';
import useSettingsStore from '../../store/settings.jsx';
@ -25,6 +25,11 @@ import { usePluginStore } from '../../store/plugins.jsx';
import API from '../../api';
import PluginDetailPanel from '../PluginDetailPanel.jsx';
import { compareVersions } from '../pluginUtils.js';
import {
PluginDowngradeWarning,
PluginSecurityWarning,
PluginSupportDisclaimer,
} from '../PluginWarnings.jsx';
const PluginFieldList = ({ plugin, settings, updateField }) => {
return plugin.fields.map((f) => (
@ -128,6 +133,8 @@ const PluginCard = ({
const [selectedVersion, setSelectedVersion] = useState(null);
const [installing, setInstalling] = useState(false);
const [uninstalling] = useState(false);
const [installConfirmOpen, setInstallConfirmOpen] = useState(false);
const [pendingInstallParams, setPendingInstallParams] = useState(null);
const installPlugin = usePluginStore((s) => s.installPlugin);
@ -311,14 +318,18 @@ const PluginCard = ({
};
const handleDetailInstall = async (params) => {
setPendingInstallParams(params);
setInstallConfirmOpen(true);
};
const confirmAndInstall = async () => {
if (!pendingInstallParams) return;
const params = pendingInstallParams;
const selVer = params.version;
const isDown = plugin.version && compareVersions(selVer, plugin.version) < 0;
const action = isDown ? 'downgrade' : 'update';
const confirmed = await onRequestConfirm(
`${isDown ? 'Downgrade' : 'Update'} ${plugin.name}?`,
`${isDown ? 'Downgrade' : 'Update'} from v${plugin.version} to v${selVer}?`
);
if (!confirmed) return;
setInstallConfirmOpen(false);
setPendingInstallParams(null);
setInstalling(true);
try {
const result = await installPlugin(params);
@ -616,6 +627,71 @@ const PluginCard = ({
)}
</Tabs>
</Modal>
{/* Install confirmation modal */}
{(() => {
const selVer = pendingInstallParams?.version;
const isDown = plugin.version && selVer && compareVersions(selVer, plugin.version) < 0;
const actionLabel = isDown ? 'Downgrade' : 'Update';
return (
<Modal
opened={installConfirmOpen}
onClose={() => {
setInstallConfirmOpen(false);
setPendingInstallParams(null);
}}
zIndex={300}
title={
<Group gap="xs" align="center">
{isDown
? <Download size={18} color="var(--mantine-color-orange-6)" />
: <Download size={18} />}
<Text fw={600}>Confirm {actionLabel}</Text>
</Group>
}
size="sm"
>
<Stack gap="md">
<Text size="sm">
You are about to {actionLabel.toLowerCase()} <b>{plugin.name}</b>{' '}
from <b>v{plugin.version}</b> to <b>v{selVer}</b>.
</Text>
<PluginSecurityWarning>
Plugins run server-side code with full access to your Dispatcharr
instance and its data. Only install plugins from developers you
trust. Malicious plugins could read or modify data, call internal
APIs, or perform unwanted actions.
</PluginSecurityWarning>
<PluginSupportDisclaimer />
{isDown && (
<PluginDowngradeWarning>
Downgrading may cause issues with saved settings or data.
</PluginDowngradeWarning>
)}
<Text size="sm" fw={500}>Are you sure you want to proceed?</Text>
<Group justify="flex-end" gap="xs">
<Button
size="xs"
variant="default"
onClick={() => {
setInstallConfirmOpen(false);
setPendingInstallParams(null);
}}
>
Cancel
</Button>
<Button
size="xs"
color={isDown ? 'orange' : undefined}
onClick={confirmAndInstall}
>
{actionLabel}
</Button>
</Group>
</Stack>
</Modal>
);
})()}
</div>
);
};

View file

@ -41,6 +41,11 @@ import {
} from '../utils/pages/PluginsUtils.js';
import { RefreshCcw, Search } from 'lucide-react';
import ErrorBoundary from '../components/ErrorBoundary.jsx';
import {
PluginRestartWarning,
PluginSecurityWarning,
PluginSupportDisclaimer,
} from '../components/PluginWarnings.jsx';
const PluginCard = React.lazy(
() => import('../components/cards/PluginCard.jsx')
);
@ -426,11 +431,8 @@ export default function PluginsPage() {
<Text size="sm" c="dimmed">
Upload a ZIP containing your plugin folder or package.
</Text>
<Alert color="yellow" variant="light" title="Heads up">
Importing a plugin may briefly restart the backend (you might see a
temporary disconnect). Please wait a few seconds and the app will
reconnect automatically.
</Alert>
<PluginRestartWarning />
<PluginSupportDisclaimer />
<Dropzone
onDrop={(files) => files[0] && setImportFile(files[0])}
onReject={() => {}}
@ -536,16 +538,14 @@ export default function PluginsPage() {
zIndex={300}
>
<Stack>
<Text size="sm">
<PluginSecurityWarning>
Plugins run server-side code with full access to your Dispatcharr
instance and its data. Only enable plugins from developers you
trust.
</Text>
<Text size="sm" c="dimmed">
Why: Malicious plugins could read or modify data, call internal
trust. Malicious plugins could read or modify data, call internal
APIs, or perform unwanted actions. Review the source or trust the
author before enabling.
</Text>
</PluginSecurityWarning>
<PluginSupportDisclaimer />
<Group justify="flex-end">
<Button
variant="default"