feat(company): add subscription expiry management and expired company guard
Add validity period fields to company CRUD, display expiry info in table and profile, and block access for expired companies via a full-screen guard component. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,8 @@ export default {
|
|||||||
'/api': {
|
'/api': {
|
||||||
// The address to proxy
|
// The address to proxy
|
||||||
// target: 'http://2333z061l7.51mypc.cn',
|
// target: 'http://2333z061l7.51mypc.cn',
|
||||||
target: 'http://172.20.177.121:8080',
|
// target: 'http://172.20.177.121:8080',
|
||||||
|
target: 'http://172.20.177.121:8082',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
'^/api': '',
|
'^/api': '',
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import ServerErrorPage from './pages/500';
|
|||||||
import { errorConfig } from './requestErrorConfig';
|
import { errorConfig } from './requestErrorConfig';
|
||||||
import { getDeviceId } from './utils/fp';
|
import { getDeviceId } from './utils/fp';
|
||||||
import './utils/sentry';
|
import './utils/sentry';
|
||||||
|
import CompanyExpireGuard from './components/CompanyExpireGuard';
|
||||||
import GlobalErrorBoundary from './components/GlobalErrorBoundary';
|
import GlobalErrorBoundary from './components/GlobalErrorBoundary';
|
||||||
import PermissionGuard from './components/PermissionGuard';
|
import PermissionGuard from './components/PermissionGuard';
|
||||||
import SSEController from './components/SSEController';
|
import SSEController from './components/SSEController';
|
||||||
@@ -197,6 +198,12 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
|||||||
// Add a loading state
|
// Add a loading state
|
||||||
childrenRender: (children) => {
|
childrenRender: (children) => {
|
||||||
if (initialState?.loading) return <PageLoading />;
|
if (initialState?.loading) return <PageLoading />;
|
||||||
|
|
||||||
|
const { company } = currentUser ?? {};
|
||||||
|
|
||||||
|
if (company?.special !== 0 && company?.validityStatus === 2) {
|
||||||
|
return <CompanyExpireGuard />;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentUser && (
|
{currentUser && (
|
||||||
|
|||||||
50
src/components/CompanyExpireGuard/index.tsx
Normal file
50
src/components/CompanyExpireGuard/index.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Result, Space } from 'antd';
|
||||||
|
import { logoutAndToLoginPage } from '@/access';
|
||||||
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
|
import { $t } from '@/utils/i18n';
|
||||||
|
|
||||||
|
const CompanyExpireGuard: React.FC = () => {
|
||||||
|
const { userInfo } = useUserInfo();
|
||||||
|
const isExpired = userInfo?.company?.validityStatus === 2;
|
||||||
|
|
||||||
|
if (!isExpired) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 9999,
|
||||||
|
background: '#fff',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title={$t('pages.company.expireTitle')}
|
||||||
|
subTitle={$t('pages.company.expireSubTitle')}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={() => window.location.reload()}>
|
||||||
|
{$t('pages.refresh')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
logoutAndToLoginPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('pages.logout')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyExpireGuard;
|
||||||
@@ -46,6 +46,8 @@ export default {
|
|||||||
'table.contactEmail': 'Contact Email',
|
'table.contactEmail': 'Contact Email',
|
||||||
'table.dataSourceCount': 'Data Sources',
|
'table.dataSourceCount': 'Data Sources',
|
||||||
'table.userCount': 'Users',
|
'table.userCount': 'Users',
|
||||||
|
'table.validityEndTime': 'Expire At (UTC+0)',
|
||||||
|
'table.validityEndTime.tips': 'Company subscription expiry date ',
|
||||||
'table.createTime': 'Create Time',
|
'table.createTime': 'Create Time',
|
||||||
'table.name': 'Name',
|
'table.name': 'Name',
|
||||||
'table.ipAddress': 'IP Address',
|
'table.ipAddress': 'IP Address',
|
||||||
@@ -87,6 +89,13 @@ export default {
|
|||||||
'pages.totalAlertCount': 'Total Alerts',
|
'pages.totalAlertCount': 'Total Alerts',
|
||||||
'pages.totalCompanyCount': 'Total Companies',
|
'pages.totalCompanyCount': 'Total Companies',
|
||||||
'pages.activeCompanyCount': 'Active Companies',
|
'pages.activeCompanyCount': 'Active Companies',
|
||||||
|
'pages.company.expired': 'Expired',
|
||||||
|
'pages.company.permanent': 'Permanent',
|
||||||
|
'pages.company.expireTitle': 'Company Subscription Expired',
|
||||||
|
'pages.company.expireSubTitle':
|
||||||
|
"Your company's license has expired. All features are temporarily locked. Please contact your sales representative to renew.",
|
||||||
|
'pages.company.expireInDays': 'Subscription expires in {days} days',
|
||||||
|
'pages.company.expireToday': 'Expires Today',
|
||||||
'pages.totalDataSourceCount': 'Total Data Sources',
|
'pages.totalDataSourceCount': 'Total Data Sources',
|
||||||
'pages.activeDataSourceCount': 'Active Data Sources',
|
'pages.activeDataSourceCount': 'Active Data Sources',
|
||||||
'pages.mt4DataSourceCount': 'MT4 Data Sources',
|
'pages.mt4DataSourceCount': 'MT4 Data Sources',
|
||||||
@@ -329,6 +338,9 @@ export default {
|
|||||||
'form.role.placeholder2': 'System roles cannot modify permission levels',
|
'form.role.placeholder2': 'System roles cannot modify permission levels',
|
||||||
'form.role.badgeColor': 'Badge Color',
|
'form.role.badgeColor': 'Badge Color',
|
||||||
'form.role.menuIds': 'Bound Permissions (Menus)',
|
'form.role.menuIds': 'Bound Permissions (Menus)',
|
||||||
|
'form.company.validityEndTime': 'Expiry At (UTC+0)',
|
||||||
|
'form.company.validityEndTime.placeholder': 'Select expiry date',
|
||||||
|
'form.company.validityEndTime.tips': 'Blank for Permanent',
|
||||||
'form.role.permissionGuide': 'Permission Guide',
|
'form.role.permissionGuide': 'Permission Guide',
|
||||||
'form.role.otherPermissions': 'Other Menu Permissions',
|
'form.role.otherPermissions': 'Other Menu Permissions',
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export default {
|
|||||||
'table.contactEmail': '联系邮箱',
|
'table.contactEmail': '联系邮箱',
|
||||||
'table.dataSourceCount': '数据源',
|
'table.dataSourceCount': '数据源',
|
||||||
'table.userCount': '用户数',
|
'table.userCount': '用户数',
|
||||||
|
'table.validityEndTime': '订阅到期时间(UTC+0)',
|
||||||
|
'table.validityEndTime.tips': '公司订阅服务到期时间',
|
||||||
'table.createTime': '创建时间',
|
'table.createTime': '创建时间',
|
||||||
'table.name': '名称',
|
'table.name': '名称',
|
||||||
'table.ipAddress': 'IP地址',
|
'table.ipAddress': 'IP地址',
|
||||||
@@ -87,6 +89,13 @@ export default {
|
|||||||
'pages.totalAlertCount': '总告警数',
|
'pages.totalAlertCount': '总告警数',
|
||||||
'pages.totalCompanyCount': '总公司数',
|
'pages.totalCompanyCount': '总公司数',
|
||||||
'pages.activeCompanyCount': '活跃公司数',
|
'pages.activeCompanyCount': '活跃公司数',
|
||||||
|
'pages.company.expired': '已过期',
|
||||||
|
'pages.company.permanent': '永久有效',
|
||||||
|
'pages.company.expireTitle': '公司订阅已过期',
|
||||||
|
'pages.company.expireSubTitle':
|
||||||
|
'您的公司在该系统的使用授权已到期,各项功能已暂时锁定。请联系商务代表进行续费操作以恢复访问。',
|
||||||
|
'pages.company.expireInDays': '订阅即将到期,剩余 {days}天',
|
||||||
|
'pages.company.expireToday': '今天到期',
|
||||||
'pages.totalDataSourceCount': '数据源总数',
|
'pages.totalDataSourceCount': '数据源总数',
|
||||||
'pages.activeDataSourceCount': '活跃数据源',
|
'pages.activeDataSourceCount': '活跃数据源',
|
||||||
'pages.mt4DataSourceCount': 'MT4 数据源',
|
'pages.mt4DataSourceCount': 'MT4 数据源',
|
||||||
@@ -321,6 +330,9 @@ export default {
|
|||||||
'form.role.placeholder2': '系统角色权限级别不可修改',
|
'form.role.placeholder2': '系统角色权限级别不可修改',
|
||||||
'form.role.badgeColor': '徽章颜色',
|
'form.role.badgeColor': '徽章颜色',
|
||||||
'form.role.menuIds': '绑定权限(菜单)',
|
'form.role.menuIds': '绑定权限(菜单)',
|
||||||
|
'form.company.validityEndTime': '到期时间(UTC+0)',
|
||||||
|
'form.company.validityEndTime.placeholder': '请选择到期时间',
|
||||||
|
'form.company.validityEndTime.tips': '留空为永久有效',
|
||||||
'form.role.permissionGuide': '权限怎么选',
|
'form.role.permissionGuide': '权限怎么选',
|
||||||
'form.role.otherPermissions': '其他菜单权限',
|
'form.role.otherPermissions': '其他菜单权限',
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ModalForm,
|
ModalForm,
|
||||||
|
ProFormDatePicker,
|
||||||
ProFormDependency,
|
ProFormDependency,
|
||||||
type ProFormInstance,
|
type ProFormInstance,
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { useAccess } from '@umijs/max';
|
import { useAccess } from '@umijs/max';
|
||||||
import { Divider, Space, Typography } from 'antd';
|
import { Divider, Space, Typography } from 'antd';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +18,7 @@ import {
|
|||||||
getEmailConfigList,
|
getEmailConfigList,
|
||||||
} from '@/services/api';
|
} from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
import { utcInstance as dayjs } from '@/utils/timeFormat';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
type CompanyListItem = Partial<API.CompanyListItem>;
|
type CompanyListItem = Partial<API.CompanyListItem>;
|
||||||
@@ -25,7 +28,7 @@ export default (props: {
|
|||||||
onSubmit: (values: CompanyListItem) => void;
|
onSubmit: (values: CompanyListItem) => void;
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { isGlobalCompany } = useAccess();
|
const { isGlobalCompany, isSuperAdmin } = useAccess();
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo } = useUserInfo();
|
||||||
const { modalVisit, formValues, onSubmit, onDone } = props;
|
const { modalVisit, formValues, onSubmit, onDone } = props;
|
||||||
|
|
||||||
@@ -43,6 +46,13 @@ export default (props: {
|
|||||||
if (!isGlobalCompany) {
|
if (!isGlobalCompany) {
|
||||||
values.companyId = userInfo?.companyId;
|
values.companyId = userInfo?.companyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
values.validityEndTime = values.validityEndTime
|
||||||
|
? dayjs(values.validityEndTime).endOf('day').format('YYYY-MM-DD')
|
||||||
|
: '';
|
||||||
|
values.validityStartTime = values.validityEndTime ? '2026-01-01' : '';
|
||||||
|
}
|
||||||
return onSubmit(values);
|
return onSubmit(values);
|
||||||
}}
|
}}
|
||||||
modalProps={{
|
modalProps={{
|
||||||
@@ -70,6 +80,7 @@ export default (props: {
|
|||||||
return {
|
return {
|
||||||
...values,
|
...values,
|
||||||
...companyMailHostConfig,
|
...companyMailHostConfig,
|
||||||
|
validityEndTime: values.validityEndTime || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return values || {};
|
return values || {};
|
||||||
@@ -106,6 +117,23 @@ export default (props: {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<ProFormDatePicker
|
||||||
|
hidden={formValues?.id === void 0}
|
||||||
|
name="validityEndTime"
|
||||||
|
label={`${$t('form.company.validityEndTime')} (${$t('form.company.validityEndTime.tips')})`}
|
||||||
|
placeholder={$t('form.company.validityEndTime.placeholder')}
|
||||||
|
convertValue={(value: string | Dayjs | null | undefined) =>
|
||||||
|
value ? value : undefined
|
||||||
|
}
|
||||||
|
fieldProps={{
|
||||||
|
style: { width: '100%' },
|
||||||
|
disabledDate: (current: Dayjs) =>
|
||||||
|
current?.isBefore(dayjs().startOf('day')),
|
||||||
|
}}
|
||||||
|
// extra={<Text type="danger">{$t('pages.company.expired')}</Text>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{formValues?.id !== void 0 && (
|
{formValues?.id !== void 0 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
type ProColumns,
|
type ProColumns,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Badge, Button, Col, Row } from 'antd';
|
import { useAccess } from '@umijs/max';
|
||||||
|
import { Badge, Button, Col, Row, Space, Typography } from 'antd';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import HeadStatisticCard, {
|
import HeadStatisticCard, {
|
||||||
type CardInfo,
|
type CardInfo,
|
||||||
@@ -13,9 +14,13 @@ import HeadStatisticCard, {
|
|||||||
import MyTag from '@/components/MyTag';
|
import MyTag from '@/components/MyTag';
|
||||||
import { companyHandle, getCompanyPages } from '@/services/api';
|
import { companyHandle, getCompanyPages } from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
import { utcInstance as dayjs } from '@/utils/timeFormat';
|
||||||
import PopUp from './Popup';
|
import PopUp from './Popup';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
const Company: React.FC = () => {
|
const Company: React.FC = () => {
|
||||||
|
const { isSuperAdmin } = useAccess();
|
||||||
const [modalVisit, setModalVisit] = useState(false);
|
const [modalVisit, setModalVisit] = useState(false);
|
||||||
|
|
||||||
const [cardInfo, setCardInfo] = useState({
|
const [cardInfo, setCardInfo] = useState({
|
||||||
@@ -92,6 +97,28 @@ const Company: React.FC = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: $t('table.validityEndTime'),
|
||||||
|
dataIndex: 'validityEndTime',
|
||||||
|
tooltip: $t('table.validityEndTime.tips'),
|
||||||
|
hideInTable: !isSuperAdmin,
|
||||||
|
render: (_, { validityEndTime, validityStatus }) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
{!validityEndTime ? (
|
||||||
|
<Text type="secondary">{$t('pages.company.permanent')}</Text>
|
||||||
|
) : (
|
||||||
|
<span>{dayjs(validityEndTime).format('YYYY-MM-DD')}</span>
|
||||||
|
)}
|
||||||
|
{validityStatus === 2 && (
|
||||||
|
<Text type="danger" style={{ fontSize: 11 }}>
|
||||||
|
{$t('pages.company.expired')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: $t('table.createTime'),
|
title: $t('table.createTime'),
|
||||||
dataIndex: 'createTime',
|
dataIndex: 'createTime',
|
||||||
@@ -122,7 +149,10 @@ const Company: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFormValues(item);
|
setFormValues({
|
||||||
|
...item,
|
||||||
|
validityEndTime: item.validityEndTime || undefined,
|
||||||
|
});
|
||||||
setModalVisit(true);
|
setModalVisit(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
ProForm,
|
ProForm,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { history } from '@umijs/max';
|
import { history, useAccess } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Tag,
|
Tag,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { clearAccessToken } from '@/access';
|
import { clearAccessToken } from '@/access';
|
||||||
import PasswordLevel from '@/components/PasswordLevel';
|
import PasswordLevel from '@/components/PasswordLevel';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
@@ -24,6 +24,7 @@ import { $t } from '@/utils/i18n';
|
|||||||
|
|
||||||
const Profile: React.FC = () => {
|
const Profile: React.FC = () => {
|
||||||
const { userInfo, setUserInfo } = useUserInfo();
|
const { userInfo, setUserInfo } = useUserInfo();
|
||||||
|
const { isGlobalCompany } = useAccess();
|
||||||
const userCompany = userInfo?.company;
|
const userCompany = userInfo?.company;
|
||||||
const { message: antdMessage, modal } = App.useApp();
|
const { message: antdMessage, modal } = App.useApp();
|
||||||
|
|
||||||
@@ -71,6 +72,19 @@ const Profile: React.FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGlobalCompany) {
|
||||||
|
items.push({
|
||||||
|
key: '6',
|
||||||
|
span: 'filled',
|
||||||
|
label: $t('pages.company.permanent'),
|
||||||
|
children: userCompany?.validityEndTime
|
||||||
|
? userCompany.validityEndTime
|
||||||
|
: $t('pages.company.permanent'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userInfo]);
|
||||||
|
|
||||||
const changePasswordSuccessHadle = async () => {
|
const changePasswordSuccessHadle = async () => {
|
||||||
let countdown = 5;
|
let countdown = 5;
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ export async function companyHandle(
|
|||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
validityStartTime?: string | null;
|
||||||
|
validityEndTime?: string | null;
|
||||||
},
|
},
|
||||||
options?: ObjType,
|
options?: ObjType,
|
||||||
) {
|
) {
|
||||||
|
|||||||
3
src/services/typings.d.ts
vendored
3
src/services/typings.d.ts
vendored
@@ -113,6 +113,9 @@ declare namespace API {
|
|||||||
fromName: string | null;
|
fromName: string | null;
|
||||||
mailHostConfigId: number | string | null;
|
mailHostConfigId: number | string | null;
|
||||||
mailStatus: number | null;
|
mailStatus: number | null;
|
||||||
|
validityStartTime?: string | null;
|
||||||
|
validityEndTime?: string | null;
|
||||||
|
validityStatus?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// "DataSource" Type
|
// "DataSource" Type
|
||||||
|
|||||||
Reference in New Issue
Block a user