mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -161,7 +161,8 @@ export interface UserPanelInfo {
|
|||||||
|
|
||||||
export interface SubscriptionRequestRecord {
|
export interface SubscriptionRequestRecord {
|
||||||
id: number;
|
id: number;
|
||||||
userUuid: string;
|
// Remnawave 2.8.0 renamed this panel field userUuid (uuid) -> userId (number).
|
||||||
|
userId: number;
|
||||||
requestAt: string;
|
requestAt: string;
|
||||||
requestIp: string | null;
|
requestIp: string | null;
|
||||||
userAgent: string | null;
|
userAgent: string | null;
|
||||||
|
|||||||
@@ -241,6 +241,25 @@ export const brandingApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Legal footer enabled (public read for the login page, admin-only update)
|
||||||
|
getFooterEnabled: async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<{ enabled: boolean }>(
|
||||||
|
'/cabinet/branding/footer-enabled',
|
||||||
|
);
|
||||||
|
return response.data.enabled;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateFooterEnabled: async (enabled: boolean): Promise<boolean> => {
|
||||||
|
const response = await apiClient.patch<{ enabled: boolean }>(
|
||||||
|
'/cabinet/branding/footer-enabled',
|
||||||
|
{ enabled },
|
||||||
|
);
|
||||||
|
return response.data.enabled;
|
||||||
|
},
|
||||||
|
|
||||||
// Get email auth enabled (public, no auth required)
|
// Get email auth enabled (public, no auth required)
|
||||||
getEmailAuthEnabled: async (): Promise<EmailAuthEnabled> => {
|
getEmailAuthEnabled: async (): Promise<EmailAuthEnabled> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
46
src/components/LegalFooter.tsx
Normal file
46
src/components/LegalFooter.tsx
Normal file
@@ -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 (
|
||||||
|
<footer
|
||||||
|
className={`flex flex-wrap items-center justify-center gap-x-2.5 gap-y-1 text-center text-[11px] leading-relaxed text-dark-500 ${className}`}
|
||||||
|
>
|
||||||
|
{LINKS.map((link, index) => (
|
||||||
|
<Fragment key={link.href}>
|
||||||
|
{index > 0 && (
|
||||||
|
<span className="text-dark-700" aria-hidden="true">
|
||||||
|
·
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="transition-colors hover:text-accent-400"
|
||||||
|
>
|
||||||
|
{t(link.labelKey, link.fallback)}
|
||||||
|
</a>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,6 +40,11 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) {
|
|||||||
queryFn: brandingApi.getGiftEnabled,
|
queryFn: brandingApi.getGiftEnabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: footerEnabled } = useQuery({
|
||||||
|
queryKey: ['footer-enabled'],
|
||||||
|
queryFn: brandingApi.getFooterEnabled,
|
||||||
|
});
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
const updateBrandingMutation = useMutation({
|
const updateBrandingMutation = useMutation({
|
||||||
mutationFn: brandingApi.updateName,
|
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<HTMLInputElement>) => {
|
const handleLogoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
@@ -250,6 +262,25 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) {
|
|||||||
disabled={updateGiftMutation.isPending}
|
disabled={updateGiftMutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-xl bg-dark-700/30 p-4">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-dark-100">
|
||||||
|
{t('admin.settings.legalFooter', 'Юридический футер')}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-dark-400">
|
||||||
|
{t(
|
||||||
|
'admin.settings.legalFooterDesc',
|
||||||
|
'Ссылки на оферту/политику/рекурренты внизу страницы входа',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Toggle
|
||||||
|
checked={footerEnabled ?? true}
|
||||||
|
onChange={() => updateFooterMutation.mutate(!(footerEnabled ?? true))}
|
||||||
|
disabled={updateFooterMutation.isPending}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2228,7 +2228,9 @@
|
|||||||
"telegramOidcClientSecret": "Client Secret"
|
"telegramOidcClientSecret": "Client Secret"
|
||||||
},
|
},
|
||||||
"offlineConv": "Offline Conversions",
|
"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": {
|
"bulkActions": {
|
||||||
"title": "Bulk Actions",
|
"title": "Bulk Actions",
|
||||||
@@ -5265,5 +5267,10 @@
|
|||||||
"empty": "You have no subscriptions yet",
|
"empty": "You have no subscriptions yet",
|
||||||
"emptyDesc": "Choose a tariff to get started",
|
"emptyDesc": "Choose a tariff to get started",
|
||||||
"title": "My subscriptions"
|
"title": "My subscriptions"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"offer": "Public Offer",
|
||||||
|
"privacy": "Privacy Policy",
|
||||||
|
"recurrent": "Recurring Payments"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1908,7 +1908,9 @@
|
|||||||
"telegramOidcClientSecret": "Client Secret"
|
"telegramOidcClientSecret": "Client Secret"
|
||||||
},
|
},
|
||||||
"offlineConv": "تبدیلهای آفلاین",
|
"offlineConv": "تبدیلهای آفلاین",
|
||||||
"apiKey": "کلید API"
|
"apiKey": "کلید API",
|
||||||
|
"legalFooter": "پاورقی حقوقی",
|
||||||
|
"legalFooterDesc": "پیوند به پیشنهاد، سیاست حریم خصوصی و پرداختهای مکرر در پایین صفحه ورود"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"color": "رنگ دکمه",
|
"color": "رنگ دکمه",
|
||||||
@@ -4899,5 +4901,10 @@
|
|||||||
"empty": "هنوز اشتراکی ندارید",
|
"empty": "هنوز اشتراکی ندارید",
|
||||||
"emptyDesc": "برای شروع تعرفهای انتخاب کنید",
|
"emptyDesc": "برای شروع تعرفهای انتخاب کنید",
|
||||||
"title": "اشتراکهای من"
|
"title": "اشتراکهای من"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"offer": "پیشنهاد عمومی",
|
||||||
|
"privacy": "سیاست حفظ حریم خصوصی",
|
||||||
|
"recurrent": "پرداختهای مکرر"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2742,7 +2742,9 @@
|
|||||||
"MODERATION": "Модерация"
|
"MODERATION": "Модерация"
|
||||||
},
|
},
|
||||||
"offlineConv": "Офлайн-конверсии",
|
"offlineConv": "Офлайн-конверсии",
|
||||||
"apiKey": "API-ключ"
|
"apiKey": "API-ключ",
|
||||||
|
"legalFooter": "Юридический футер",
|
||||||
|
"legalFooterDesc": "Ссылки на оферту, политику и рекуррентные платежи внизу страницы входа"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"color": "Цвет кнопки",
|
"color": "Цвет кнопки",
|
||||||
@@ -5827,5 +5829,10 @@
|
|||||||
"empty": "У вас пока нет подписок",
|
"empty": "У вас пока нет подписок",
|
||||||
"emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом",
|
"emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом",
|
||||||
"title": "Мои подписки"
|
"title": "Мои подписки"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"offer": "Публичная оферта",
|
||||||
|
"privacy": "Политика конфиденциальности",
|
||||||
|
"recurrent": "Рекуррентные платежи"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1973,7 +1973,9 @@
|
|||||||
"telegramOidcClientSecret": "Client Secret"
|
"telegramOidcClientSecret": "Client Secret"
|
||||||
},
|
},
|
||||||
"offlineConv": "离线转化",
|
"offlineConv": "离线转化",
|
||||||
"apiKey": "API 密钥"
|
"apiKey": "API 密钥",
|
||||||
|
"legalFooter": "法律页脚",
|
||||||
|
"legalFooterDesc": "登录页面底部的要约、隐私政策和定期付款链接"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"color": "按钮颜色",
|
"color": "按钮颜色",
|
||||||
@@ -4898,5 +4900,10 @@
|
|||||||
"empty": "您还没有订阅",
|
"empty": "您还没有订阅",
|
||||||
"emptyDesc": "选择套餐以开始使用",
|
"emptyDesc": "选择套餐以开始使用",
|
||||||
"title": "我的订阅"
|
"title": "我的订阅"
|
||||||
|
},
|
||||||
|
"footer": {
|
||||||
|
"offer": "公开要约",
|
||||||
|
"privacy": "隐私政策",
|
||||||
|
"recurrent": "定期付款"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import OAuthProviderIcon from '../components/OAuthProviderIcon';
|
|||||||
import { saveOAuthState } from '../utils/oauth';
|
import { saveOAuthState } from '../utils/oauth';
|
||||||
import { getPendingReferralCode } from '../utils/referral';
|
import { getPendingReferralCode } from '../utils/referral';
|
||||||
import { UsersIcon, EmailIcon, RefreshIcon, ChevronDownIcon } from '@/components/icons';
|
import { UsersIcon, EmailIcon, RefreshIcon, ChevronDownIcon } from '@/components/icons';
|
||||||
|
import LegalFooter from '../components/LegalFooter';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -112,6 +113,12 @@ export default function Login() {
|
|||||||
});
|
});
|
||||||
const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true;
|
const isEmailAuthEnabled = emailAuthConfig?.enabled ?? true;
|
||||||
|
|
||||||
|
const { data: footerEnabled } = useQuery({
|
||||||
|
queryKey: ['footer-enabled'],
|
||||||
|
queryFn: brandingApi.getFooterEnabled,
|
||||||
|
staleTime: 60000,
|
||||||
|
});
|
||||||
|
|
||||||
// Fetch enabled OAuth providers
|
// Fetch enabled OAuth providers
|
||||||
const { data: oauthData } = useQuery({
|
const { data: oauthData } = useQuery({
|
||||||
queryKey: ['oauth-providers'],
|
queryKey: ['oauth-providers'],
|
||||||
@@ -737,6 +744,7 @@ export default function Login() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{footerEnabled && <LegalFooter className="pt-1" />}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user