refactor(workflows): replace hardcoded webhook URL with secret for improved security in Docker publish workflows
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m50s

This commit is contained in:
2026-02-16 15:31:47 +07:00
parent 5cc19b8c3e
commit 336a5b42a6
3 changed files with 191 additions and 6 deletions
+1 -1
View File
@@ -56,4 +56,4 @@ jobs:
- name: Trigger container update webhook
run: |
curl -X POST -sS --fail https://docker.shts.su/api/webhooks/d3fd3b6a-c670-4035-9357-2fde8aa76b6e
curl -X POST -sS --fail "${{ secrets.portainer_webhook }}"
+1 -1
View File
@@ -48,4 +48,4 @@ jobs:
- name: Trigger container update webhook
run: |
curl -X POST -sS --fail https://docker.shts.su/api/webhooks/d3fd3b6a-c670-4035-9357-2fde8aa76b6e
curl -X POST -sS --fail "${{ secrets.portainer_webhook }}"
+189 -4
View File
@@ -20,7 +20,9 @@ import {
IconDownload,
IconCreditCard,
IconMenu2,
IconArrowsExchange
IconArrowsExchange,
IconLayoutSidebarLeftExpand,
IconLayoutNavbar
} from '@tabler/icons-react';
import ServerManager from './ServerManager';
import FilterManager from './FilterManager';
@@ -56,14 +58,16 @@ function LanguageProvider({ children }) {
dashboard: 'Панель', domains: 'Домены', ipRanges: 'IP-диапазоны', asns: 'AS',
communities: 'Community', servers: 'Серверы', filters: 'Фильтры', billing: 'Биллинг', autoUrls: 'Авто URL',
easySwitch: 'Easy Switch', networkConfig: 'Сетевые настройки', mikrotikBackups: 'MikroTik Бэкапы',
light: 'Светлая', dark: 'Тёмная'
light: 'Светлая', dark: 'Тёмная',
layoutSidebar: 'Сайдбар', layoutHorizontal: 'Верхнее меню'
},
en: {
home: 'Home', data: 'Data', management: 'Management', tools: 'Tools',
dashboard: 'Dashboard', domains: 'Domains', ipRanges: 'IP Ranges', asns: 'ASNs',
communities: 'Communities', servers: 'Servers', filters: 'Filters', billing: 'Billing', autoUrls: 'Auto URLs',
easySwitch: 'Easy Switch', networkConfig: 'Network Config', mikrotikBackups: 'MikroTik Backups',
light: 'Light', dark: 'Dark'
light: 'Light', dark: 'Dark',
layoutSidebar: 'Sidebar', layoutHorizontal: 'Top menu'
}
};
const t = (key) => (dict[lang] && key in dict[lang] ? dict[lang][key] : key);
@@ -122,12 +126,19 @@ function App() {
);
}
const LAYOUT_FLUID = 'fluid';
const LAYOUT_HORIZONTAL = 'horizontal';
function MainLayout() {
const location = useLocation();
const { lang, setLang, t } = useContext(LanguageContext);
const { theme, setTheme } = useContext(ThemeContext);
// Навбар стал лаконичным: без неиспользуемых уведомлений/иконок
const [settingsOpen, setSettingsOpen] = useState(false);
const [layout, setLayout] = useState(() => localStorage.getItem('layout') || LAYOUT_FLUID);
useEffect(() => {
localStorage.setItem('layout', layout);
}, [layout]);
// Состояние для управления выпадающими меню
const [dropdownStates, setDropdownStates] = useState({
@@ -233,6 +244,152 @@ function MainLayout() {
const activeTab = getActiveTab();
// Старый вид: один верхний навбар с горизонтальным меню
if (layout === LAYOUT_HORIZONTAL) {
return (
<div className="page d-flex flex-column min-vh-100">
<a href="#main-content" className="skip-link">Перейти к основному содержанию</a>
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{ boxShadow: '0 1px 0 0 #e9ecef' }}>
<div className="container-xl">
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
<Link to="/" className="navbar-brand d-flex align-items-center">
<IconBrandTabler className="navbar-brand-icon me-2" />
<span className="fw-bold">S3 Lists Manager</span>
</Link>
</div>
<button
className="navbar-toggler d-md-none"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbar-menu-horizontal"
aria-controls="navbar-menu-horizontal"
aria-expanded="false"
aria-label="Toggle navigation"
>
<IconMenu2 />
</button>
<div className="collapse navbar-collapse" id="navbar-menu-horizontal">
<div className="d-flex flex-column flex-md-row flex-fill align-items-stretch align-items-md-center">
<ul className="navbar-nav">
{navCategories.map(category => (
<li key={category.id} className="nav-item dropdown">
{category.single ? (
<Link
to={category.path}
className={`nav-link${activeTab === category.id ? ' active' : ''}`}
>
<category.icon className="icon icon-tabler" />
<span className="nav-link-title">{category.title}</span>
</Link>
) : (
<>
<a
className={`nav-link dropdown-toggle${activeTab.startsWith(category.id) ? ' active' : ''}`}
href="#"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
toggleDropdown(category.id);
}}
>
<category.icon className="icon icon-tabler" />
<span className="nav-link-title">{category.title}</span>
</a>
{dropdownStates[category.id] && (
<div className="dropdown-menu show" onClick={(e) => e.stopPropagation()}>
<div className="dropdown-menu-columns">
<div className="dropdown-menu-column">
{category.items.map(item => (
<Link
key={item.id}
to={item.path}
className={`dropdown-item${activeTab === item.id ? ' active' : ''}`}
>
<item.icon className="icon icon-tabler" />
{item.title}
</Link>
))}
</div>
</div>
</div>
)}
</>
)}
</li>
))}
</ul>
</div>
</div>
<div className="navbar-nav flex-row order-md-last">
<div className="nav-item dropdown me-1">
<a href="#" className="nav-link px-2 dropdown-toggle" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()}>
{layout === LAYOUT_FLUID ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutNavbar size={20} />}
</a>
<div className="dropdown-menu dropdown-menu-end">
<a href="#" className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_FLUID ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_FLUID); }}>
<IconLayoutSidebarLeftExpand className="me-2" size={18} /> {t('layoutSidebar')}
</a>
<a href="#" className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_HORIZONTAL ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_HORIZONTAL); }}>
<IconLayoutNavbar className="me-2" size={18} /> {t('layoutHorizontal')}
</a>
</div>
</div>
<div className="nav-item dropdown me-1">
<a href="#" className="nav-link px-2 dropdown-toggle" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()}>
<span className="badge">{lang.toUpperCase()}</span>
</a>
<div className="dropdown-menu dropdown-menu-end">
<a href="#" className={`dropdown-item ${lang === 'ru' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLang('ru'); }}>Русский</a>
<a href="#" className={`dropdown-item ${lang === 'en' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setLang('en'); }}>English</a>
</div>
</div>
<div className="nav-item dropdown me-1">
<a href="#" className="nav-link px-2 dropdown-toggle" data-bs-toggle="dropdown" onClick={(e) => e.preventDefault()}>
{theme === 'dark' ? '🌙' : '☀️'}
</a>
<div className="dropdown-menu dropdown-menu-end">
<a href="#" className={`dropdown-item ${theme === 'light' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setTheme('light'); }}> {t('light')}</a>
<a href="#" className={`dropdown-item ${theme === 'dark' ? 'active' : ''}`} onClick={(e) => { e.preventDefault(); setTheme('dark'); }}>🌙 {t('dark')}</a>
</div>
</div>
<div className="nav-item me-1">
<KeyboardShortcutsButton className="nav-link px-2" />
</div>
<div className="nav-item">
<a href="#" className="nav-link px-2" onClick={(e) => { e.preventDefault(); setSettingsOpen(true); }} title="Настройки">
<IconSettings size={20} />
</a>
</div>
</div>
</div>
</header>
<main id="main-content" className="container-xl mt-4 flex-grow-1" tabIndex="-1">
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/domains" element={<DomainsNewManager />} />
<Route path="/ip-ranges" element={<IPRangesManager />} />
<Route path="/asns" element={<ASNsNewManager />} />
<Route path="/communities" element={<CommunitiesManager />} />
<Route path="/auto-urls" element={<AutoUrlManager />} />
<Route path="/servers" element={<ServerManager />} />
<Route path="/billing" element={<BillingManager />} />
<Route path="/filters" element={<FilterManager />} />
<Route path="/network-config" element={<NetworkConfigManager />} />
<Route path="/easy-switch" element={<EasySwitchManager />} />
<Route path="/mikrotik-backups" element={<MikrotikBackupsManager />} />
<Route path="/mikrotik-tools" element={<MikrotikTools />} />
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</main>
<footer className="footer mt-auto py-2">
<div className="container-xl text-muted small">Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}</div>
</footer>
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
<CommandPalette />
</div>
);
}
// Fluid Vertical: вертикальный сайдбар + верхний навбар + page-wrapper
return (
<div className="page">
@@ -354,6 +511,34 @@ function MainLayout() {
</a>
</div>
</div>
<div className="nav-item dropdown">
<a
href="#"
className="nav-link px-2 dropdown-toggle"
data-bs-toggle="dropdown"
onClick={(e) => e.preventDefault()}
>
{layout === LAYOUT_FLUID ? <IconLayoutSidebarLeftExpand size={20} /> : <IconLayoutNavbar size={20} />}
</a>
<div className="dropdown-menu dropdown-menu-end">
<a
href="#"
className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_FLUID ? 'active' : ''}`}
onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_FLUID); }}
>
<IconLayoutSidebarLeftExpand className="me-2" size={18} />
{t('layoutSidebar')}
</a>
<a
href="#"
className={`dropdown-item d-flex align-items-center ${layout === LAYOUT_HORIZONTAL ? 'active' : ''}`}
onClick={(e) => { e.preventDefault(); setLayout(LAYOUT_HORIZONTAL); }}
>
<IconLayoutNavbar className="me-2" size={18} />
{t('layoutHorizontal')}
</a>
</div>
</div>
<div className="nav-item dropdown">
<a
href="#"