diff --git a/src/api/adminUsers.ts b/src/api/adminUsers.ts index ce21696..a3f0e74 100644 --- a/src/api/adminUsers.ts +++ b/src/api/adminUsers.ts @@ -161,7 +161,8 @@ export interface UserPanelInfo { export interface SubscriptionRequestRecord { id: number; - userUuid: string; + // Remnawave 2.8.0 renamed this panel field userUuid (uuid) -> userId (number). + userId: number; requestAt: string; requestIp: string | null; userAgent: string | null; diff --git a/src/api/branding.ts b/src/api/branding.ts index c2aa0da..e57a130 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -241,6 +241,25 @@ export const brandingApi = { return response.data; }, + // Legal footer enabled (public read for the login page, admin-only update) + getFooterEnabled: async (): Promise => { + try { + const response = await apiClient.get<{ enabled: boolean }>( + '/cabinet/branding/footer-enabled', + ); + return response.data.enabled; + } catch { + return false; + } + }, + updateFooterEnabled: async (enabled: boolean): Promise => { + const response = await apiClient.patch<{ enabled: boolean }>( + '/cabinet/branding/footer-enabled', + { enabled }, + ); + return response.data.enabled; + }, + // Get email auth enabled (public, no auth required) getEmailAuthEnabled: async (): Promise => { try { diff --git a/src/components/LegalFooter.tsx b/src/components/LegalFooter.tsx new file mode 100644 index 0000000..c26c16c --- /dev/null +++ b/src/components/LegalFooter.tsx @@ -0,0 +1,46 @@ +import { Fragment } from 'react'; +import { useTranslation } from 'react-i18next'; + +interface LegalLink { + href: string; + labelKey: string; + fallback: string; +} + +const LINKS: LegalLink[] = [ + { href: '/offer', labelKey: 'footer.offer', fallback: 'Публичная оферта' }, + { href: '/privacy', labelKey: 'footer.privacy', fallback: 'Политика конфиденциальности' }, + { href: '/recurrent-payments', labelKey: 'footer.recurrent', fallback: 'Рекуррентные платежи' }, +]; + +interface LegalFooterProps { + className?: string; +} + +export default function LegalFooter({ className = '' }: LegalFooterProps) { + const { t } = useTranslation(); + + return ( + + ); +} diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx index 0e9727f..aec1362 100644 --- a/src/components/admin/BrandingTab.tsx +++ b/src/components/admin/BrandingTab.tsx @@ -40,6 +40,11 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { queryFn: brandingApi.getGiftEnabled, }); + const { data: footerEnabled } = useQuery({ + queryKey: ['footer-enabled'], + queryFn: brandingApi.getFooterEnabled, + }); + // Mutations const updateBrandingMutation = useMutation({ mutationFn: brandingApi.updateName, @@ -88,6 +93,13 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { }, }); + const updateFooterMutation = useMutation({ + mutationFn: (enabled: boolean) => brandingApi.updateFooterEnabled(enabled), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['footer-enabled'] }); + }, + }); + const handleLogoUpload = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { @@ -250,6 +262,25 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { disabled={updateGiftMutation.isPending} /> + +
+
+ + {t('admin.settings.legalFooter', 'Юридический футер')} + +

+ {t( + 'admin.settings.legalFooterDesc', + 'Ссылки на оферту/политику/рекурренты внизу страницы входа', + )} +

+
+ updateFooterMutation.mutate(!(footerEnabled ?? true))} + disabled={updateFooterMutation.isPending} + /> +
diff --git a/src/locales/en.json b/src/locales/en.json index 4b3af11..ccfaef1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -2228,7 +2228,9 @@ "telegramOidcClientSecret": "Client Secret" }, "offlineConv": "Offline Conversions", - "apiKey": "API Key" + "apiKey": "API Key", + "legalFooter": "Legal Footer", + "legalFooterDesc": "Links to the offer, privacy policy and recurring payments at the bottom of the login page" }, "bulkActions": { "title": "Bulk Actions", @@ -5265,5 +5267,10 @@ "empty": "You have no subscriptions yet", "emptyDesc": "Choose a tariff to get started", "title": "My subscriptions" + }, + "footer": { + "offer": "Public Offer", + "privacy": "Privacy Policy", + "recurrent": "Recurring Payments" } } diff --git a/src/locales/fa.json b/src/locales/fa.json index a7b6900..9558add 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -1908,7 +1908,9 @@ "telegramOidcClientSecret": "Client Secret" }, "offlineConv": "تبدیل‌های آفلاین", - "apiKey": "کلید API" + "apiKey": "کلید API", + "legalFooter": "پاورقی حقوقی", + "legalFooterDesc": "پیوند به پیشنهاد، سیاست حریم خصوصی و پرداخت‌های مکرر در پایین صفحه ورود" }, "buttons": { "color": "رنگ دکمه", @@ -4899,5 +4901,10 @@ "empty": "هنوز اشتراکی ندارید", "emptyDesc": "برای شروع تعرفه‌ای انتخاب کنید", "title": "اشتراک‌های من" + }, + "footer": { + "offer": "پیشنهاد عمومی", + "privacy": "سیاست حفظ حریم خصوصی", + "recurrent": "پرداخت‌های مکرر" } } diff --git a/src/locales/ru.json b/src/locales/ru.json index bd0bbd8..970050e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -2742,7 +2742,9 @@ "MODERATION": "Модерация" }, "offlineConv": "Офлайн-конверсии", - "apiKey": "API-ключ" + "apiKey": "API-ключ", + "legalFooter": "Юридический футер", + "legalFooterDesc": "Ссылки на оферту, политику и рекуррентные платежи внизу страницы входа" }, "buttons": { "color": "Цвет кнопки", @@ -5827,5 +5829,10 @@ "empty": "У вас пока нет подписок", "emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом", "title": "Мои подписки" + }, + "footer": { + "offer": "Публичная оферта", + "privacy": "Политика конфиденциальности", + "recurrent": "Рекуррентные платежи" } } diff --git a/src/locales/zh.json b/src/locales/zh.json index de95231..5fd3a47 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -1973,7 +1973,9 @@ "telegramOidcClientSecret": "Client Secret" }, "offlineConv": "离线转化", - "apiKey": "API 密钥" + "apiKey": "API 密钥", + "legalFooter": "法律页脚", + "legalFooterDesc": "登录页面底部的要约、隐私政策和定期付款链接" }, "buttons": { "color": "按钮颜色", @@ -4898,5 +4900,10 @@ "empty": "您还没有订阅", "emptyDesc": "选择套餐以开始使用", "title": "我的订阅" + }, + "footer": { + "offer": "公开要约", + "privacy": "隐私政策", + "recurrent": "定期付款" } } diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index 0882f41..7becbdc 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -24,6 +24,7 @@ import OAuthProviderIcon from '../components/OAuthProviderIcon'; import { saveOAuthState } from '../utils/oauth'; import { getPendingReferralCode } from '../utils/referral'; import { UsersIcon, EmailIcon, RefreshIcon, ChevronDownIcon } from '@/components/icons'; +import LegalFooter from '../components/LegalFooter'; export default function Login() { const { t } = useTranslation(); @@ -112,6 +113,12 @@ export default function Login() { }); const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true; + const { data: footerEnabled } = useQuery({ + queryKey: ['footer-enabled'], + queryFn: brandingApi.getFooterEnabled, + staleTime: 60000, + }); + // Fetch enabled OAuth providers const { data: oauthData } = useQuery({ queryKey: ['oauth-providers'], @@ -737,6 +744,7 @@ export default function Login() { )} )} + {footerEnabled && } );