Merge remote-tracking branch 'origin/master'

pull/47/head
shao1121353141 2023-09-10 23:18:11 +08:00
commit 8482454136
43 changed files with 1045 additions and 140 deletions

View File

@ -21,6 +21,8 @@ import java.io.Serializable;
import org.springdoc.core.annotations.ParameterObject; import org.springdoc.core.annotations.ParameterObject;
import cn.topiam.employee.common.enums.app.AppGroupType;
import lombok.Data; import lombok.Data;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -41,12 +43,18 @@ public class AppGroupQuery implements Serializable {
* *
*/ */
@Parameter(description = "分组名称") @Parameter(description = "分组名称")
private String name; private String name;
/** /**
* *
*/ */
@Parameter(description = "分组编码") @Parameter(description = "分组编码")
private String code; private String code;
/**
*
*/
@Parameter(description = "分组类型")
private AppGroupType type;
} }

View File

@ -19,6 +19,7 @@ package cn.topiam.employee.common.repository.app.impl;
import java.util.List; import java.util.List;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
@ -77,6 +78,11 @@ public class AppGroupRepositoryCustomizedImpl implements AppGroupRepositoryCusto
builder.append(" AND `group`.code_ like '%").append(query.getCode()).append("%'"); builder.append(" AND `group`.code_ like '%").append(query.getCode()).append("%'");
} }
//分组类型
if (ObjectUtils.isNotEmpty(query.getType())) {
builder.append(" AND `group`.type_ like '%").append(query.getType().getCode()).append("%'");
}
builder.append(" ORDER BY `group`.create_time DESC"); builder.append(" ORDER BY `group`.create_time DESC");
//@formatter:on //@formatter:on
String sql = builder.toString(); String sql = builder.toString();

View File

@ -134,26 +134,44 @@ export default [
}, },
], ],
}, },
//应用列表 //应用管理
{ {
name: 'app', name: 'app',
icon: 'AppstoreOutlined', icon: 'AppstoreOutlined',
path: '/app', path: '/app',
component: './app/AppList', routes: [
}, // 应用列表
//创建应用 {
{ path: '/app',
name: 'app.create', redirect: '/app/list',
path: '/app/create', },
hideInMenu: true, // 应用列表
component: './app/AppCreate', {
}, name: 'list',
//应用配置 path: '/app/list',
{ component: './app/AppList',
name: 'app.config', },
path: '/app/config', //创建应用
hideInMenu: true, {
component: './app/AppConfig', name: 'create',
path: '/app/list/create',
hideInMenu: true,
component: './app/AppCreate',
},
//应用配置
{
name: 'config',
path: '/app/list/config',
hideInMenu: true,
component: './app/AppConfig',
},
// 应用分组
{
name: 'group',
path: '/app/group',
component: './app/AppGroup',
},
],
}, },
//行为审计 //行为审计
{ {

View File

@ -49,7 +49,7 @@
"@ant-design/maps": "^1.0.7", "@ant-design/maps": "^1.0.7",
"@ant-design/pro-components": "^2.6.18", "@ant-design/pro-components": "^2.6.18",
"ahooks": "^3.7.8", "ahooks": "^3.7.8",
"antd": "^5.8.6", "antd": "^5.9.0",
"antd-img-crop": "^4.12.2", "antd-img-crop": "^4.12.2",
"antd-style": "^3.4.5", "antd-style": "^3.4.5",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -99,7 +99,7 @@
"@umijs/max": "^4.0.80", "@umijs/max": "^4.0.80",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cross-port-killer": "^1.4.0", "cross-port-killer": "^1.4.0",
"eslint": "^8.48.0", "eslint": "^8.49.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",

View File

@ -57,7 +57,7 @@ const GlobalHeaderRight: React.FC = () => {
return ( return (
<div className={styles.main}> <div className={styles.main}>
<Helmet> <Helmet>
<link rel="icon" href={initialState?.globalConfig?.appearance?.favicon} /> <link rel="icon" href={'/favicon.ico'} />
</Helmet> </Helmet>
<About /> <About />
<SelectLang className={styles.action} /> <SelectLang className={styles.action} />

View File

@ -18,7 +18,7 @@
import { getUserGroupList } from '@/services/account'; import { getUserGroupList } from '@/services/account';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
import { Select, Spin } from 'antd'; import { Select, Spin } from 'antd';
import { ReactText, useState } from 'react'; import { useState } from 'react';
import { useAsyncEffect } from 'ahooks'; import { useAsyncEffect } from 'ahooks';
import { SortOrder } from 'antd/es/table/interface'; import { SortOrder } from 'antd/es/table/interface';
import { RequestData } from '@ant-design/pro-components'; import { RequestData } from '@ant-design/pro-components';
@ -35,9 +35,9 @@ export type UserGroupSelectProps<ValueType = UserData> = Omit<
>; >;
async function getAllUserGroupList( async function getAllUserGroupList(
params?: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, ReactText[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AccountAPI.ListUserGroup>> { ): Promise<RequestData<AccountAPI.ListUserGroup>> {
let pageSize = 100, let pageSize = 100,
current = 1; current = 1;
@ -79,7 +79,7 @@ const UserGroupSelect = (props: UserGroupSelectProps) => {
useAsyncEffect(async () => { useAsyncEffect(async () => {
setFetching(true); setFetching(true);
const { success, data } = await getAllUserGroupList().finally(() => { const { success, data } = await getAllUserGroupList({}, {}, {}).finally(() => {
setFetching(false); setFetching(false);
}); });
if (success && data) { if (success && data) {

View File

@ -39,6 +39,8 @@ export default {
'menu.account.logout': '退出登录', 'menu.account.logout': '退出登录',
'menu.social-bind': '用户绑定', 'menu.social-bind': '用户绑定',
'menu.app': '应用管理', 'menu.app': '应用管理',
'menu.app.list': '应用列表',
'menu.app.group': '应用分组',
'menu.app.create': '创建应用', 'menu.app.create': '创建应用',
'menu.app.config': '应用配置', 'menu.app.config': '应用配置',
'menu.account': '账户管理', 'menu.account': '账户管理',

View File

@ -81,7 +81,7 @@ export default () => {
history.push(`/account/identity-source`); history.push(`/account/identity-source`);
return; return;
} }
if (!type) { if (!type || !IdentitySourceDetailTabs[type]) {
setTabActiveKey(IdentitySourceDetailTabs.config); setTabActiveKey(IdentitySourceDetailTabs.config);
history.push({ history.push({
pathname: location.pathname, pathname: location.pathname,

View File

@ -42,9 +42,9 @@ export default (props: CreateModelProps) => {
wrapperCol={{ span: 19 }} wrapperCol={{ span: 19 }}
onFinish={async (values: Record<string, string>) => { onFinish={async (values: Record<string, string>) => {
setLoading(true); setLoading(true);
const result = await onFinish(values); await onFinish(values).finally(() => {
setLoading(false); setLoading(false);
return !!result; });
}} }}
modalProps={{ modalProps={{
destroyOnClose: true, destroyOnClose: true,

View File

@ -78,7 +78,7 @@ export default (props: CreateOrganizationFormProps<AccountAPI.CreateOrganization
*/ */
const cancel = async () => { const cancel = async () => {
if (onCancel) { if (onCancel) {
await onCancel(); onCancel();
} }
form.resetFields(); form.resetFields();
}; };

View File

@ -78,9 +78,9 @@ const UpdateModel = (props: UpdateFormProps<AccountAPI.UpdateOrganization>) => {
open={visible} open={visible}
onFinish={async (values: AccountAPI.UpdateOrganization) => { onFinish={async (values: AccountAPI.UpdateOrganization) => {
setUpdateLoading(true); setUpdateLoading(true);
const result = await onFinish(values); await onFinish(values).finally(() => {
setUpdateLoading(false); setUpdateLoading(false);
return !!result; });
}} }}
> >
<Skeleton loading={loading} active={true}> <Skeleton loading={loading} active={true}>

View File

@ -61,7 +61,7 @@ export default (props: UpdateOrganizationFormProps<AccountAPI.UpdateOrganization
*/ */
const cancel = async () => { const cancel = async () => {
if (onCancel) { if (onCancel) {
await onCancel(); onCancel();
} }
form.resetFields(); form.resetFields();
}; };

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { history } from '@@/core/history'; import { history } from '@@/core/history';
import { DesktopOutlined, ProfileOutlined } from '@ant-design/icons'; import { DesktopOutlined, ProfileOutlined, SafetyOutlined } from '@ant-design/icons';
import { GridContent, PageContainer } from '@ant-design/pro-components'; import { GridContent, PageContainer } from '@ant-design/pro-components';
import { useAsyncEffect } from 'ahooks'; import { useAsyncEffect } from 'ahooks';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
@ -32,6 +32,8 @@ import { useIntl, useLocation } from '@umijs/max';
import useStyle from './style'; import useStyle from './style';
import classNames from 'classnames'; import classNames from 'classnames';
import { AppProtocolType } from '@/constant'; import { AppProtocolType } from '@/constant';
import PermissionResource from './components/PermissionResource';
import PermissionRole from './components/PermissionRole';
const prefixCls = 'app-config'; const prefixCls = 'app-config';
@ -118,6 +120,27 @@ export default () => {
}, },
], ],
}, },
{
key: ConfigTabs.app_permission,
label: intl.formatMessage({ id: 'pages.app.config.items.app_permission' }),
icon: React.createElement(() => {
return <SafetyOutlined />;
}),
children: [
{
key: ConfigTabs.permission_resource,
label: intl.formatMessage({
id: 'pages.app.config.items.app_permission.permission_resource',
}),
},
{
key: ConfigTabs.permission_role,
label: intl.formatMessage({
id: 'pages.app.config.items.app_permission.permission_role',
}),
},
],
},
]; ];
useAsyncEffect(async () => { useAsyncEffect(async () => {
@ -126,12 +149,12 @@ export default () => {
history.push('/app'); history.push('/app');
return; return;
} }
if (!type) { if (!type || !ConfigTabs[type]) {
setKeys([ConfigTabs.protocol_config]); setKeys([ConfigTabs.basic]);
history.replace({ history.replace({
pathname: location.pathname, pathname: location.pathname,
search: queryString.stringify({ search: queryString.stringify({
type: ConfigTabs.protocol_config, type: ConfigTabs.basic,
id, id,
protocol, protocol,
name, name,
@ -152,6 +175,8 @@ export default () => {
[ConfigTabs.protocol_config]: AppProtocol, [ConfigTabs.protocol_config]: AppProtocol,
[ConfigTabs.app_account]: AppAccount, [ConfigTabs.app_account]: AppAccount,
[ConfigTabs.access_policy]: AccessPolicy, [ConfigTabs.access_policy]: AccessPolicy,
[ConfigTabs.permission_resource]: PermissionResource,
[ConfigTabs.permission_role]: PermissionRole,
}; };
const Component = components[key]; const Component = components[key];
return <Component {...rest} />; return <Component {...rest} />;

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { getApp, updateApp } from '@/services/app'; import { updateApp } from '@/services/app';
import { ProCard, ProDescriptions } from '@ant-design/pro-components'; import { ProCard, ProDescriptions } from '@ant-design/pro-components';
import { useAsyncEffect } from 'ahooks'; import { useAsyncEffect } from 'ahooks';
@ -30,6 +30,7 @@ import classNames from 'classnames';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons'; import { LoadingOutlined, PlusOutlined } from '@ant-design/icons';
import { uploadFile } from '@/services/upload'; import { uploadFile } from '@/services/upload';
import { GetApp } from '@/pages/app/AppConfig/data'; import { GetApp } from '@/pages/app/AppConfig/data';
import { getApp } from '../../service';
const prefixCls = 'app-basic-info'; const prefixCls = 'app-basic-info';
const AppBasic = (props: { appId: string }) => { const AppBasic = (props: { appId: string }) => {

View File

@ -23,6 +23,9 @@ const useStyle = createStyles(({ prefixCls, token }, props) => {
return { return {
main: { main: {
height: 'calc(100vh - 178px)', height: 'calc(100vh - 178px)',
[`${antCls}-pro-card-body`]: {
overflow: 'auto !important',
},
[`.${prefix}-descriptions`]: { [`.${prefix}-descriptions`]: {
[`${antCls}-descriptions-item-container ${antCls}-space-item`]: { [`${antCls}-descriptions-item-container ${antCls}-space-item`]: {
span: { span: {

View File

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { AppProtocolType } from '@/constant'; import { AppProtocolType } from '@/constant';
import { getApp } from '@/services/app'; import { getApp } from '../../service';
import { ProCard } from '@ant-design/pro-components'; import { ProCard } from '@ant-design/pro-components';
import { useAsyncEffect } from 'ahooks'; import { useAsyncEffect } from 'ahooks';
@ -25,7 +25,7 @@ import { useState } from 'react';
import FromConfig from './FromProtocolConfig'; import FromConfig from './FromProtocolConfig';
import JwtConfig from './JwtProtocolConfig'; import JwtConfig from './JwtProtocolConfig';
import OidcConfig from './OidcProtocolConfig'; import OidcConfig from './OidcProtocolConfig';
import { GetApp } from '../../data'; import { GetApp } from '../../data.d';
import { useIntl } from '@@/exports'; import { useIntl } from '@@/exports';
export default (props: { appId: string }) => { export default (props: { appId: string }) => {

View File

@ -0,0 +1,152 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getPermissionResource, permissionResourceParamCheck } from '../../service';
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
import { Form, Spin } from 'antd';
import React, { useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import Paragraph from 'antd/es/typography/Paragraph';
type UpdateFormProps = {
/**
* ID
*/
id?: string;
/**
* SYSTEM
*/
appId: string;
/**
*
*/
open: boolean;
/**
*
*/
onCancel: (e?: React.MouseEvent | React.KeyboardEvent) => void;
/**
*
*/
onFinish?: (formData: Record<string, string>) => Promise<boolean | void>;
};
const UpdateResource: React.FC<UpdateFormProps> = (props) => {
const { open, onCancel, onFinish, id, appId } = props;
const [loading, setLoading] = useState<boolean>(false);
const [form] = Form.useForm();
useAsyncEffect(async () => {
if (open && id) {
setLoading(true);
const { success, result } = await getPermissionResource(id);
if (success && result) {
setLoading(false);
return;
}
}
}, [id, onCancel, open]);
return (
<ModalForm
title="修改资源"
width={'500px'}
open={open}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
layout={'horizontal'}
labelAlign={'right'}
modalProps={{
maskClosable: true,
destroyOnClose: true,
onCancel: onCancel,
}}
form={form}
onFinish={async (values) => {
setLoading(true);
const result = await onFinish?.(values);
setLoading(false);
return result;
}}
>
<Spin spinning={loading}>
<ProFormText name={'id'} hidden />
<ProFormText
name="name"
label="资源名称"
placeholder="请输入资源名称"
rules={[
{
required: true,
message: '请输入资源名称',
},
{
validator: async (rule, value) => {
if (!value) {
return Promise.resolve();
}
setLoading(true);
const { success, result } = await permissionResourceParamCheck(
appId,
'NAME',
value,
id,
);
setLoading(false);
if (!success) {
return Promise.reject<any>();
}
if (!result) {
return Promise.reject<any>(new Error('资源名称已存在'));
}
},
validateTrigger: ['onBlur'],
},
]}
/>
<ProFormText
name="code"
label="资源编码"
placeholder="请输入资源编码"
proFieldProps={{
render: (value: string) => {
return (
value && (
<Paragraph copyable={{ text: value }} style={{ marginBottom: '0' }}>
<span
dangerouslySetInnerHTML={{
__html: `<span>${value}</span>`,
}}
/>
</Paragraph>
)
);
},
}}
readonly
extra="资源编码在当前应用中的唯一标识,不能重复,仅支持英文、数字、下划线,创建后不可修改。"
/>
<ProFormTextArea
name="desc"
fieldProps={{ rows: 2, maxLength: 20, showCount: false }}
label="资源描述"
placeholder="请输入资源描述"
/>
</Spin>
</ModalForm>
);
};
export default UpdateResource;

View File

@ -0,0 +1,149 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { getPermissionRole, permissionRoleParamCheck } from '../../service';
import type { ProFormInstance } from '@ant-design/pro-components';
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
import { Spin } from 'antd';
import React, { useRef, useState } from 'react';
import { useAsyncEffect } from 'ahooks';
import Paragraph from 'antd/es/typography/Paragraph';
type UpdateFormProps = {
/**
* ID
*/
id: string | undefined;
/**
*
*/
open: boolean;
/**
*
*/
onCancel: () => void;
/**
*
*/
onFinish?: (formData: Record<string, string>) => Promise<boolean | void>;
};
const UpdateRole: React.FC<UpdateFormProps> = (props) => {
const { open, onCancel, onFinish, id } = props;
const [loading, setLoading] = useState<boolean>(false);
const formRef = useRef<ProFormInstance>();
useAsyncEffect(async () => {
if (open && id) {
setLoading(true);
const { success, result } = await getPermissionRole(id);
if (success && result) {
setLoading(false);
return;
}
}
}, [id, open]);
return (
<ModalForm
title="修改角色"
width={600}
modalProps={{
onCancel: onCancel,
destroyOnClose: true,
}}
layout={'horizontal'}
labelCol={{ span: 4 }}
wrapperCol={{ span: 19 }}
labelAlign={'right'}
open={open}
key={'update'}
onFinish={async (values) => {
setLoading(true);
const result = await onFinish?.(values);
setLoading(false);
return result;
}}
>
<Spin spinning={loading}>
<ProFormText name="id" hidden />
<ProFormText name="appId" hidden />
<ProFormText
name="name"
label="名称"
rules={[
{
required: true,
message: '请输入角色名称',
},
{
validator: async (rule, value) => {
if (!value) {
return Promise.resolve();
}
setLoading(true);
const { success, result } = await permissionRoleParamCheck(
formRef.current?.getFieldValue('appId'),
'NAME',
value,
id,
);
setLoading(false);
if (!success) {
return Promise.reject<any>();
}
if (!result) {
return Promise.reject<any>(new Error('手机号已存在'));
}
},
validateTrigger: ['onBlur'],
},
]}
placeholder="请输入角色名称"
/>
<ProFormText
name="code"
label="标识"
placeholder="请输入角色标识"
readonly
proFieldProps={{
render: (value: string) => {
return (
value && (
<Paragraph copyable={{ text: value }} style={{ marginBottom: '0' }}>
<span
dangerouslySetInnerHTML={{
__html: `<span>${value}</span>`,
}}
/>
</Paragraph>
)
);
},
}}
extra="角色编码在当前应用中的唯一标识,不能重复,仅支持英文、数字、下划线,创建后不可修改。"
/>
<ProFormTextArea
name="remark"
fieldProps={{ rows: 2, maxLength: 20, showCount: false }}
label="描述"
placeholder="请输入角色描述"
/>
</Spin>
</ModalForm>
);
};
export default UpdateRole;

View File

@ -24,6 +24,7 @@ export default {
'pages.app.config.basic.icon.desc.2': '建议使用 256 * 256 像素方形图标', 'pages.app.config.basic.icon.desc.2': '建议使用 256 * 256 像素方形图标',
'pages.app.config.basic.enabled': '应用状态', 'pages.app.config.basic.enabled': '应用状态',
'pages.app.config.basic.type': '应用类型', 'pages.app.config.basic.type': '应用类型',
'pages.app.config.basic.group': '应用分组',
'pages.app.config.basic.type.value_enum.custom_made': '定制应用', 'pages.app.config.basic.type.value_enum.custom_made': '定制应用',
'pages.app.config.basic.type.value_enum.standard': '标准应用', 'pages.app.config.basic.type.value_enum.standard': '标准应用',
'pages.app.config.basic.type.value_enum.self_developed': '自研应用', 'pages.app.config.basic.type.value_enum.self_developed': '自研应用',
@ -591,6 +592,10 @@ export default {
'授权组织', '授权组织',
'pages.app.config.items.login_access.access_policy.create_policy.modal_form.subject_type.auth_organization.rule.0.message': 'pages.app.config.items.login_access.access_policy.create_policy.modal_form.subject_type.auth_organization.rule.0.message':
'请选择组织节点', '请选择组织节点',
'pages.app.config.items.app_permission': '权限管理',
'pages.app.config.items.app_permission.permission_resource': '资源管理',
'pages.app.config.items.app_permission.permission_role': '角色管理',
'pages.app.config.items.app_permission.permission_audit': '权限审计',
'pages.app.config.items.account_sync': '账户同步', 'pages.app.config.items.account_sync': '账户同步',
'pages.app.config.error': '未指定应用', 'pages.app.config.error': '未指定应用',
}; };

View File

@ -21,6 +21,7 @@ import {
ModalForm, ModalForm,
PageContainer, PageContainer,
ProCard, ProCard,
ProFormSelect,
ProFormText, ProFormText,
ProFormTextArea, ProFormTextArea,
ProList, ProList,
@ -38,6 +39,7 @@ import classnames from 'classnames';
import { ListTemplate } from './data.d'; import { ListTemplate } from './data.d';
import { AppType } from '@/constant'; import { AppType } from '@/constant';
import { createApp, getAppTemplateList } from './service'; import { createApp, getAppTemplateList } from './service';
import { getAllAppGroupList } from '@/services/app';
const { Paragraph } = Typography; const { Paragraph } = Typography;
const prefixCls = 'topiam-create-app'; const prefixCls = 'topiam-create-app';
@ -106,7 +108,7 @@ const CreateApp = (props: {
onOk: () => { onOk: () => {
successModal.destroy(); successModal.destroy();
history.push( history.push(
`/app/config?id=${result.id}&name=${values.name}&protocol=${protocol}`, `/app/list/config?id=${result.id}&name=${values.name}&protocol=${protocol}`,
); );
}, },
}); });
@ -128,6 +130,23 @@ const CreateApp = (props: {
}, },
]} ]}
/> />
<ProFormSelect
name="groups"
mode="multiple"
label={'归属分组'}
request={async () => {
setLoading(true);
const { success, data } = await getAllAppGroupList({}, {}, {}).finally(() => {
setLoading(false);
});
if (success && data) {
return data.map((i) => {
return { label: i.name, value: i.id };
});
}
return [];
}}
/>
<ProFormTextArea <ProFormTextArea
name="remark" name="remark"
preserve={false} preserve={false}
@ -232,22 +251,20 @@ const AppCreate = () => {
); );
}} }}
/> />
{createAppTemplate && (
<CreateApp
code={createAppTemplate?.code}
name={createAppTemplate?.name}
protocol={createAppTemplate.protocol}
open={createAppOpen}
onCancel={() => {
setCreateAppOpen(false);
setCreateAppTemplate(undefined);
}}
/>
)}
</PageContainer> </PageContainer>
{createAppTemplate && (
<CreateApp
code={createAppTemplate?.code}
name={createAppTemplate?.name}
protocol={createAppTemplate.protocol}
open={createAppOpen}
onCancel={() => {
setCreateAppOpen(false);
setCreateAppTemplate(undefined);
}}
/>
)}
</div> </div>
); );
}; };
export default () => { export default AppCreate;
return <AppCreate />;
};

View File

@ -0,0 +1,221 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { PageContainer, ProTable } from '@ant-design/pro-components';
import { App, Button, Popconfirm, Tag } from 'antd';
import { useRef, useState } from 'react';
import CreateModal from './components/CreateModal';
import UpdateModal from './components/UpdateModal';
import { createAppGroup, removeAppGroup, updateAppGroup } from './service';
import { useIntl } from '@@/exports';
import { getAppGroupList } from '@/services/app';
export default () => {
const intl = useIntl();
const actionRef = useRef<ActionType>();
const [createModalOpen, setCreateModalOpen] = useState<boolean>(false);
const [updateModalOpen, setUpdateModalOpen] = useState<boolean>(false);
const [id, setId] = useState<string>();
const { message } = App.useApp();
const columns: ProColumns<AppAPI.AppGroupList>[] = [
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.name' }),
dataIndex: 'name',
fixed: 'left',
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.code' }),
dataIndex: 'code',
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.app_count' }),
dataIndex: 'appCount',
search: false,
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.type' }),
dataIndex: 'type',
valueEnum: {
default: {
text: intl.formatMessage({
id: 'pages.app_group.list.column.type.default',
}),
},
custom: {
text: intl.formatMessage({
id: 'pages.app_group.list.column.type.custom',
}),
},
},
render: (_, record) => (
<>
{record.type === 'custom' && (
<Tag color={'#108ee9'} key={'custom'}>
{intl.formatMessage({
id: 'pages.app_group.list.column.type.custom',
})}
</Tag>
)}
{record.type === 'default' && (
<Tag color={'#2db7f5'} key={'default'}>
{intl.formatMessage({
id: 'pages.app_group.list.column.type.default',
})}
</Tag>
)}
</>
),
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.create_time' }),
dataIndex: 'createTime',
search: false,
align: 'center',
ellipsis: true,
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.remark' }),
dataIndex: 'remark',
search: false,
ellipsis: true,
},
{
title: intl.formatMessage({ id: 'pages.app_group.list.column.option' }),
valueType: 'option',
key: 'option',
width: 100,
align: 'center',
render: (text, record) => [
<a
key="editable"
onClick={() => {
setId(record.id);
setUpdateModalOpen(true);
}}
>
{intl.formatMessage({ id: 'app.update' })}
</a>,
<Popconfirm
title={intl.formatMessage({
id: 'pages.app_group.list.actions.popconfirm.delete',
})}
placement="bottomRight"
icon={
<QuestionCircleOutlined
style={{
color: 'red',
}}
/>
}
onConfirm={async () => {
const { success } = await removeAppGroup(record.id);
if (success) {
message.success(intl.formatMessage({ id: 'app.operation_success' }));
actionRef.current?.reload();
return;
}
}}
okText={intl.formatMessage({ id: 'app.yes' })}
cancelText={intl.formatMessage({ id: 'app.no' })}
key="delete"
>
<a target="_blank" key="remove" style={{ color: 'red' }}>
{intl.formatMessage({ id: 'app.delete' })}
</a>
</Popconfirm>,
],
},
];
return (
<PageContainer>
<ProTable<AppAPI.AppGroupList>
columns={columns}
actionRef={actionRef}
request={getAppGroupList}
rowKey="id"
search={{
labelWidth: 'auto',
}}
scroll={{ x: 900 }}
form={{
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
};
}
return values;
},
}}
pagination={{
pageSize: 5,
}}
dateFormatter="string"
toolBarRender={() => [
<Button
key="button"
icon={<PlusOutlined />}
onClick={() => {
setCreateModalOpen(true);
}}
type="primary"
>
{intl.formatMessage({ id: 'pages.app_group.list.create' })}
</Button>,
]}
/>
<CreateModal
open={createModalOpen}
onCancel={() => {
setCreateModalOpen(false);
}}
onFinish={async (values) => {
const { result, success } = await createAppGroup(values);
if (success && result) {
message.success(intl.formatMessage({ id: 'app.create_success' }));
actionRef.current?.reload();
}
actionRef.current?.reload();
setCreateModalOpen(false);
return true;
}}
/>
{id && (
<UpdateModal
open={updateModalOpen}
id={id}
onCancel={() => {
setUpdateModalOpen(false);
}}
onFinish={async (values) => {
const { result, success } = await updateAppGroup(values);
if (success && result) {
message.success(intl.formatMessage({ id: 'app.create_success' }));
actionRef.current?.reload();
}
actionRef.current?.reload();
setUpdateModalOpen(false);
return true;
}}
/>
)}
</PageContainer>
);
};

View File

@ -0,0 +1,105 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { ModalForm, ProFormText, ProFormTextArea } from '@ant-design/pro-components';
import { Form, Spin } from 'antd';
import React, { useState } from 'react';
import { useIntl } from '@@/exports';
import { getAppGroup } from '@/pages/app/AppGroup/service';
export default (props: {
id: string;
open: boolean;
onFinish: (formData: Record<string, string>) => Promise<boolean | void>;
onCancel: (e: React.MouseEvent<HTMLButtonElement>) => void;
}) => {
const { id, open, onCancel, onFinish } = props;
const [form] = Form.useForm();
const intl = useIntl();
const [loading, setLoading] = useState<boolean>(false);
return (
<ModalForm
title={intl.formatMessage({ id: 'pages.app_group.update.modal_form.title' })}
form={form}
open={open}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
width={'500px'}
labelAlign={'right'}
preserve={false}
layout={'horizontal'}
autoFocusFirstInput
modalProps={{
maskClosable: true,
destroyOnClose: true,
onCancel: onCancel,
}}
onOpenChange={async (visible) => {
if (visible) {
setLoading(true);
const { result, success } = await getAppGroup(id).finally(() => {
setLoading(false);
});
if (success) {
form.setFieldsValue(result);
}
}
}}
onFinish={async (values) => {
setLoading(true);
await onFinish(values).finally(() => {
setLoading(false);
});
}}
>
<Spin spinning={loading}>
<ProFormText hidden name="id" />
<ProFormText
label={intl.formatMessage({ id: 'pages.app_group.modal_form.name' })}
name="name"
rules={[
{
required: true,
message: intl.formatMessage({
id: 'pages.app_group.modal_form.name.rule.0.message',
}),
},
]}
/>
<ProFormText
label={intl.formatMessage({ id: 'pages.app_group.modal_form.name' })}
name="code"
readonly
/>
<ProFormTextArea
label={intl.formatMessage({ id: 'pages.app_group.modal_form.remark' })}
name="remark"
fieldProps={{
placeholder: intl.formatMessage({
id: 'pages.app_group.modal_form.remark.placeholder',
}),
rows: 2,
maxLength: 20,
autoComplete: 'off',
showCount: true,
}}
/>
</Spin>
</ModalForm>
);
};

View File

@ -0,0 +1,17 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

View File

@ -0,0 +1,19 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import AppGroup from './AppGroup';
export default AppGroup;

View File

@ -0,0 +1,40 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export default {
'pages.app_group.list.create': '创建分组',
'pages.app_group.list.column.name': '分组名称',
'pages.app_group.list.column.code': '分组编码',
'pages.app_group.list.column.create_time': '创建时间',
'pages.app_group.list.column.remark': '备注',
'pages.app_group.list.column.app_count': '应用数量',
'pages.app_group.list.column.type': '分组类型',
'pages.app_group.list.column.type.default': '系统默认',
'pages.app_group.list.column.type.custom': '自定义',
'pages.app_group.list.column.option': '操作',
'pages.app_group.list.actions.popconfirm.delete': '您确定要删除此应用分组?',
'pages.app_group.create.modal_form.title': '添加分组',
'pages.app_group.modal_form.name': '分组名称',
'pages.app_group.modal_form.name.placeholder': '请输入分组名称',
'pages.app_group.modal_form.name.rule.0.message': '分组名称为必填项',
'pages.app_group.modal_form.code': '分组编码',
'pages.app_group.modal_form.code.placeholder': '请输入分组编码',
'pages.app_group.modal_form.code.rule.0.message': '分组编码为必填项',
'pages.app_group.modal_form.remark': '备注',
'pages.app_group.modal_form.remark.placeholder': '请输入备注',
'pages.app_group.update.modal_form.title': '修改分组',
};

View File

@ -0,0 +1,62 @@
/*
* eiam-console - Employee Identity and Access Management
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { request } from '@umijs/max';
/**
*
*/
export async function createAppGroup(
params: Record<string, string>,
): Promise<API.ApiResult<Record<string, string>>> {
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/group/create`, {
method: 'POST',
data: params,
requestType: 'json',
});
}
/**
*
*/
export async function getAppGroup(id: string): Promise<API.ApiResult<Record<string, string>>> {
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/group/get/${id}`, {
method: 'GET',
});
}
/**
*
*/
export async function updateAppGroup(
params: Record<string, string>,
): Promise<API.ApiResult<Record<string, string>>> {
return request<API.ApiResult<Record<string, string>>>(`/api/v1/app/group/update`, {
method: 'PUT',
data: params,
requestType: 'json',
});
}
/**
* Remove App Group
*/
export async function removeAppGroup(id: string): Promise<API.ApiResult<boolean>> {
return request<API.ApiResult<boolean>>(`/api/v1/app/group/delete/${id}`, {
method: 'DELETE',
});
}

View File

@ -15,7 +15,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { getAppList } from '@/services/app'; import { getAllAppGroupList, getAppList } from '@/services/app';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import type { ActionType } from '@ant-design/pro-components'; import type { ActionType } from '@ant-design/pro-components';
import { PageContainer, ProList } from '@ant-design/pro-components'; import { PageContainer, ProList } from '@ant-design/pro-components';
@ -86,7 +86,7 @@ export default () => {
key={'create'} key={'create'}
type="primary" type="primary"
onClick={() => { onClick={() => {
history.push('/app/create'); history.push('/app/list/create');
}} }}
> >
<PlusOutlined /> <PlusOutlined />
@ -102,7 +102,7 @@ export default () => {
<span <span
onClick={() => { onClick={() => {
history.push( history.push(
`/app/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`, `/app/list/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`,
); );
}} }}
> >
@ -177,7 +177,7 @@ export default () => {
key="config" key="config"
onClick={() => { onClick={() => {
history.push( history.push(
`/app/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`, `/app/list/config?id=${row.id}&protocol=${row.protocol}&name=${row.name}`,
); );
}} }}
> >
@ -213,6 +213,25 @@ export default () => {
</Popconfirm>, </Popconfirm>,
], ],
}, },
status: {
// 自己扩展的字段,主要用于筛选,不在列表中显示
title: intl.formatMessage({
id: 'pages.app.list.metas.group',
}),
valueType: 'select',
fieldProps: {
mode: 'multiple',
},
request: async () => {
const { success, data } = await getAllAppGroupList({}, {}, {});
if (success && data) {
return data.map((i) => {
return { label: i.name, value: i.id };
});
}
return [];
},
},
}} }}
/> />
</PageContainer> </PageContainer>

View File

@ -21,6 +21,7 @@ export default {
'pages.app.list.title': '应用列表', 'pages.app.list.title': '应用列表',
'pages.app.list.tool_bar_render.add_app': '添加应用', 'pages.app.list.tool_bar_render.add_app': '添加应用',
'pages.app.list.metas.title': '应用名称', 'pages.app.list.metas.title': '应用名称',
'pages.app.list.metas.group': '应用分组',
'pages.app.list.actions.popconfirm.disable_app': '确定禁用该应用吗?', 'pages.app.list.actions.popconfirm.disable_app': '确定禁用该应用吗?',
'pages.app.list.actions.popconfirm.enable_app': '确定启用该应用吗?', 'pages.app.list.actions.popconfirm.enable_app': '确定启用该应用吗?',
'pages.app.list.actions.popconfirm.delete_app': '您确定要删除此应用?', 'pages.app.list.actions.popconfirm.delete_app': '您确定要删除此应用?',

View File

@ -83,9 +83,9 @@ export default (props: CreateDrawerProps) => {
{...DRAWER_FORM_ITEM_LAYOUT} {...DRAWER_FORM_ITEM_LAYOUT}
onFinish={async (values: Record<string, string>) => { onFinish={async (values: Record<string, string>) => {
setLoading(true); setLoading(true);
const result = await onFinish(values); await onFinish(values).finally(() => {
setLoading(false); setLoading(false);
return !!result; });
}} }}
> >
<Spin spinning={loading}> <Spin spinning={loading}>

View File

@ -74,9 +74,9 @@ export default (props: CreateDrawerProps) => {
scrollToFirstError scrollToFirstError
onFinish={async (values: Record<string, string>) => { onFinish={async (values: Record<string, string>) => {
setUpdateLoading(true); setUpdateLoading(true);
const result = await onFinish(values); await onFinish(values).finally(() => {
setUpdateLoading(false); setUpdateLoading(false);
return !!result; });
}} }}
open={visible} open={visible}
> >

View File

@ -16,13 +16,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import { BasicSettingConfig, SecurityDefensePolicyConfig } from './data.d';
/** /**
* *
*/ */
export async function getBasicSettingConfig(): Promise< export async function getBasicSettingConfig(): Promise<API.ApiResult<BasicSettingConfig>> {
API.ApiResult<SettingAPI.AdvancedSettingConfig>
> {
return request('/api/v1/setting/security/basic/config'); return request('/api/v1/setting/security/basic/config');
} }
@ -30,7 +29,7 @@ export async function getBasicSettingConfig(): Promise<
* *
*/ */
export async function saveBasicSettingConfig( export async function saveBasicSettingConfig(
params: Record<string, any>, params: BasicSettingConfig,
): Promise<API.ApiResult<boolean>> { ): Promise<API.ApiResult<boolean>> {
return request('/api/v1/setting/security/basic/save', { return request('/api/v1/setting/security/basic/save', {
method: 'POST', method: 'POST',
@ -43,7 +42,7 @@ export async function saveBasicSettingConfig(
* *
*/ */
export async function getSecurityDefensePolicyConfig(): Promise< export async function getSecurityDefensePolicyConfig(): Promise<
API.ApiResult<SettingAPI.SecurityDefensePolicyConfig> API.ApiResult<SecurityDefensePolicyConfig>
> { > {
return request('/api/v1/setting/security/defense_policy/config'); return request('/api/v1/setting/security/defense_policy/config');
} }
@ -52,7 +51,7 @@ export async function getSecurityDefensePolicyConfig(): Promise<
* *
*/ */
export async function saveSecurityDefensePolicyConfig( export async function saveSecurityDefensePolicyConfig(
params: Record<string, any>, params: SecurityDefensePolicyConfig,
): Promise<API.ApiResult<boolean>> { ): Promise<API.ApiResult<boolean>> {
return request('/api/v1/setting/security/defense_policy/save', { return request('/api/v1/setting/security/defense_policy/save', {
method: 'POST', method: 'POST',

View File

@ -24,7 +24,7 @@ import { EmailTemplateList, GetEmailTemplate, SmsTemplateList } from './data.d';
* @param params * @param params
*/ */
export async function getMailTemplateList( export async function getMailTemplateList(
params?: Record<string, any>, params: Record<string, any>,
): Promise<API.ApiResult<EmailTemplateList>> { ): Promise<API.ApiResult<EmailTemplateList>> {
return request('/api/v1/setting/mail_template/list', { params }); return request('/api/v1/setting/mail_template/list', { params });
} }

View File

@ -26,16 +26,16 @@ export default () => {
<ProFormText <ProFormText
name={['config', 'domain']} name={['config', 'domain']}
label={intl.formatMessage({ label={intl.formatMessage({
id: 'pages.setting.storage_provider.provider.tencent_cos.domain', id: 'pages.setting.storage_provider.provider.aliyun_oss.domain',
})} })}
placeholder={intl.formatMessage({ placeholder={intl.formatMessage({
id: 'pages.setting.storage_provider.provider.tencent_cos.domain.placeholder', id: 'pages.setting.storage_provider.provider.aliyun_oss.domain.placeholder',
})} })}
rules={[ rules={[
{ {
required: true, required: true,
message: intl.formatMessage({ message: intl.formatMessage({
id: 'pages.setting.storage_provider.provider.tencent_cos.domain.rule.0.message', id: 'pages.setting.storage_provider.provider.aliyun_oss.domain.rule.0.message',
}), }),
}, },
]} ]}

View File

@ -128,8 +128,8 @@ export async function moveOrganization(
*/ */
export async function getUserList( export async function getUserList(
params: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, (string | number)[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AccountAPI.ListUser>> { ): Promise<RequestData<AccountAPI.ListUser>> {
return request<API.ApiResult<AccountAPI.ListUser>>('/api/v1/user/list', { return request<API.ApiResult<AccountAPI.ListUser>>('/api/v1/user/list', {
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) }, params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
@ -170,8 +170,8 @@ export async function batchGetUser(ids: string[]): Promise<API.ApiResult<Account
*/ */
export async function getLoginAuditList( export async function getLoginAuditList(
params: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, (string | number)[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AccountAPI.UserLoginAuditList>> { ): Promise<RequestData<AccountAPI.UserLoginAuditList>> {
return request<API.ApiResult<AccountAPI.UserLoginAuditList>>('/api/v1/user/login_audit/list', { return request<API.ApiResult<AccountAPI.UserLoginAuditList>>('/api/v1/user/login_audit/list', {
method: 'GET', method: 'GET',
@ -353,8 +353,8 @@ export async function getUserListNotInGroup(
*/ */
export async function getUserGroupList( export async function getUserGroupList(
params: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, (string | number)[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AccountAPI.ListUserGroup>> { ): Promise<RequestData<AccountAPI.ListUserGroup>> {
return request<API.ApiResult<AccountAPI.ListUserGroup>>('/api/v1/user_group/list', { return request<API.ApiResult<AccountAPI.ListUserGroup>>('/api/v1/user_group/list', {
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) }, params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
@ -433,8 +433,8 @@ export async function removeUserGroupMember(
*/ */
export async function getUserGroupMemberList( export async function getUserGroupMemberList(
params: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, (string | number)[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AccountAPI.ListUser>> { ): Promise<RequestData<AccountAPI.ListUser>> {
return request<API.ApiResult<AccountAPI.ListUser>>( return request<API.ApiResult<AccountAPI.ListUser>>(
`/api/v1/user_group/${params.id}/member_list`, `/api/v1/user_group/${params.id}/member_list`,

View File

@ -19,15 +19,14 @@ import { download, filterParamConverter, sortParamConverter } from '@/utils/util
import type { RequestData } from '@ant-design/pro-components'; import type { RequestData } from '@ant-design/pro-components';
import type { SortOrder } from 'antd/es/table/interface'; import type { SortOrder } from 'antd/es/table/interface';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import type { UploadFile } from 'antd/es/upload/interface';
/** /**
* *
*/ */
export async function getAppList( export async function getAppList(
params?: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, (string | number)[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AppAPI.AppList>> { ): Promise<RequestData<AppAPI.AppList>> {
return request<API.ApiResult<AppAPI.AppList>>('/api/v1/app/list', { return request<API.ApiResult<AppAPI.AppList>>('/api/v1/app/list', {
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) }, params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
@ -192,32 +191,61 @@ export async function removeAppAccessPolicy(id: string): Promise<API.ApiResult<b
} }
/** /**
* parse Saml2 MetadataUrl *
*/ */
export async function parseSaml2MetadataUrl( export async function getAppGroupList(
metadataUrl: string, params: Record<string, any>,
): Promise<API.ApiResult<Record<string, any>>> { sort: Record<string, SortOrder>,
return request(`/api/v1/app/saml2/parse/metadata_url`, { filter: Record<string, (string | number)[] | null>,
method: 'POST', ): Promise<RequestData<AppAPI.AppGroupList>> {
params: { metadataUrl }, return request<API.ApiResult<AppAPI.AppGroupList>>('/api/v1/app/group/list', {
}).catch(({ response: { data } }) => { params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
return data; }).then((result: API.ApiResult<AppAPI.AppGroupList>) => {
const data: RequestData<AppAPI.AppGroupList> = {
data: result?.result?.list ? result?.result?.list : [],
success: result?.success,
total: result?.result?.pagination ? result?.result?.pagination.total : 0,
totalPages: result?.result?.pagination?.totalPages,
};
return Promise.resolve(data);
}); });
} }
/** export async function getAllAppGroupList(
* parse Saml2 MetadataFile params: Record<string, any>,
*/ sort: Record<string, SortOrder>,
export async function parseSaml2MetadataFile( filter: Record<string, (string | number)[] | null>,
file: UploadFile, ): Promise<RequestData<AppAPI.AppGroupList>> {
): Promise<API.ApiResult<Record<string, any>>> { let pageSize = 100,
return request(`/api/v1/app/saml2/parse/metadata_file`, { current = 1;
method: 'POST', // 存储所有数据的数组
data: { file: file.originFileObj }, let result: RequestData<AppAPI.AppGroupList> = {
headers: { data: [],
'content-type': 'multipart/form-data', success: false,
}, total: undefined,
}).catch(({ response: { data } }) => { };
return data;
}); while (true) {
// 调用分页接口
const { success, data, total } = await getAppGroupList(
{ current, pageSize, ...params },
sort,
filter,
);
if (success && data) {
// 如果当前页没有数据,表示已经加载完全部数据,退出循环
if (data?.length === 0) {
break;
}
result = { data: result.data?.concat(data), success: success, total: total };
// 增加当前页码
if (total && total <= pageSize * current) {
break;
} else {
current = current + 1;
}
}
}
return result;
} }

View File

@ -422,4 +422,13 @@ declare namespace AppAPI {
template: string; template: string;
remark: string; remark: string;
}; };
export type AppGroupList = {
id: string;
name: string;
code: string;
type: string;
appCount: string;
remark: string;
};
} }

View File

@ -18,7 +18,6 @@
import type { SortOrder } from 'antd/es/table/interface'; import type { SortOrder } from 'antd/es/table/interface';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { parse } from 'querystring'; import { parse } from 'querystring';
import type { ReactText } from 'react';
import { history, matchPath } from '@umijs/max'; import { history, matchPath } from '@umijs/max';
import YAML from 'yaml'; import YAML from 'yaml';
import { PhoneNumber } from 'google-libphonenumber'; import { PhoneNumber } from 'google-libphonenumber';
@ -106,7 +105,7 @@ export const sortParamConverter = (value: Record<string, SortOrder> | undefined)
* *
* @param value * @param value
*/ */
export const filterParamConverter = (value: Record<string, ReactText[] | null> | undefined) => { export const filterParamConverter = (value: Record<string, (string | number)[] | null>) => {
const param: Record<string, any> = {}; const param: Record<string, any> = {};
if (value) if (value)
Object.entries(value).forEach(([key], index) => { Object.entries(value).forEach(([key], index) => {

View File

@ -33,9 +33,9 @@ import cn.topiam.employee.audit.event.type.EventType;
import cn.topiam.employee.common.entity.setting.AdministratorEntity; import cn.topiam.employee.common.entity.setting.AdministratorEntity;
import cn.topiam.employee.common.repository.setting.AdministratorRepository; import cn.topiam.employee.common.repository.setting.AdministratorRepository;
import cn.topiam.employee.support.context.ApplicationContextHelp; import cn.topiam.employee.support.context.ApplicationContextHelp;
import cn.topiam.employee.support.security.userdetails.UserDetails;
import cn.topiam.employee.support.security.userdetails.UserType; import cn.topiam.employee.support.security.userdetails.UserType;
import static cn.topiam.employee.core.security.util.SecurityUtils.getFailureMessage; import static cn.topiam.employee.core.security.util.SecurityUtils.getFailureMessage;
import static cn.topiam.employee.support.security.util.SecurityUtils.getPrincipal;
/** /**
* *
@ -59,13 +59,7 @@ public class ConsoleAuthenticationFailureEventListener implements
AuditEventPublish publish = ApplicationContextHelp.getBean(AuditEventPublish.class); AuditEventPublish publish = ApplicationContextHelp.getBean(AuditEventPublish.class);
String content = getFailureMessage(event); String content = getFailureMessage(event);
logger.error("认证失败 [{}]",content); logger.error("认证失败 [{}]",content);
String principal = (String) event.getAuthentication().getPrincipal(); String principal = getPrincipal(event);
if (event.getAuthentication().getPrincipal() instanceof String){
principal = (String) event.getAuthentication().getPrincipal();
}
if (event.getAuthentication().getPrincipal() instanceof UserDetails || event.getAuthentication().getPrincipal() instanceof org.springframework.security.core.userdetails.UserDetails){
principal = ((UserDetails) event.getAuthentication().getPrincipal()).getUsername();
}
if (StringUtils.isNotBlank(principal)){ if (StringUtils.isNotBlank(principal)){
Optional<AdministratorEntity> optional = getAdministratorRepository().findByUsername(principal); Optional<AdministratorEntity> optional = getAdministratorRepository().findByUsername(principal);
if (optional.isEmpty()) { if (optional.isEmpty()) {

View File

@ -55,6 +55,12 @@ public class AppGroupListResult implements Serializable {
@Parameter(description = "分组编码") @Parameter(description = "分组编码")
private String code; private String code;
/**
*
*/
@Parameter(description = "应用数量")
private Integer appCount;
/** /**
* *
*/ */

View File

@ -38,11 +38,11 @@ import cn.topiam.employee.common.enums.UserStatus;
import cn.topiam.employee.common.repository.account.UserRepository; import cn.topiam.employee.common.repository.account.UserRepository;
import cn.topiam.employee.core.help.SettingHelp; import cn.topiam.employee.core.help.SettingHelp;
import cn.topiam.employee.support.context.ApplicationContextHelp; import cn.topiam.employee.support.context.ApplicationContextHelp;
import cn.topiam.employee.support.security.userdetails.UserDetails;
import cn.topiam.employee.support.util.PhoneNumberUtils; import cn.topiam.employee.support.util.PhoneNumberUtils;
import static cn.topiam.employee.core.help.SettingHelp.getLoginFailureDuration; import static cn.topiam.employee.core.help.SettingHelp.getLoginFailureDuration;
import static cn.topiam.employee.core.security.util.SecurityUtils.getFailureMessage; import static cn.topiam.employee.core.security.util.SecurityUtils.getFailureMessage;
import static cn.topiam.employee.support.security.userdetails.UserType.USER; import static cn.topiam.employee.support.security.userdetails.UserType.USER;
import static cn.topiam.employee.support.security.util.SecurityUtils.getPrincipal;
/** /**
* *
@ -67,13 +67,7 @@ public class PortalAuthenticationFailureEventListener implements
AuditEventPublish publish = ApplicationContextHelp.getBean(AuditEventPublish.class); AuditEventPublish publish = ApplicationContextHelp.getBean(AuditEventPublish.class);
String content = getFailureMessage(event); String content = getFailureMessage(event);
logger.error("认证失败", event.getException()); logger.error("认证失败", event.getException());
String principal = null; String principal = getPrincipal(event);
if (event.getAuthentication().getPrincipal() instanceof String) {
principal = (String) event.getAuthentication().getPrincipal();
}
if (event.getAuthentication().getPrincipal() instanceof UserDetails || event.getAuthentication().getPrincipal() instanceof org.springframework.security.core.userdetails.UserDetails) {
principal = ((UserDetails) event.getAuthentication().getPrincipal()).getUsername();
}
if (StringUtils.isNotBlank(principal)) { if (StringUtils.isNotBlank(principal)) {
UserEntity user = getUserRepository().findByUsername(principal); UserEntity user = getUserRepository().findByUsername(principal);
if (ObjectUtils.isEmpty(user)) { if (ObjectUtils.isEmpty(user)) {

View File

@ -50,7 +50,7 @@
"@ant-design/maps": "^1.0.7", "@ant-design/maps": "^1.0.7",
"@ant-design/pro-components": "^2.6.18", "@ant-design/pro-components": "^2.6.18",
"ahooks": "^3.7.8", "ahooks": "^3.7.8",
"antd": "^5.8.6", "antd": "^5.9.0",
"antd-img-crop": "^4.12.2", "antd-img-crop": "^4.12.2",
"antd-style": "^3.4.5", "antd-style": "^3.4.5",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@ -94,7 +94,7 @@
"@umijs/max": "^4.0.80", "@umijs/max": "^4.0.80",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"cross-port-killer": "^1.4.0", "cross-port-killer": "^1.4.0",
"eslint": "^8.48.0", "eslint": "^8.49.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"prettier": "^3.0.3", "prettier": "^3.0.3",

View File

@ -19,15 +19,15 @@ import { filterParamConverter, sortParamConverter } from '@/utils/utils';
import type { RequestData } from '@ant-design/pro-components'; import type { RequestData } from '@ant-design/pro-components';
import type { SortOrder } from 'antd/es/table/interface'; import type { SortOrder } from 'antd/es/table/interface';
import { request } from '@umijs/max'; import { request } from '@umijs/max';
import type { AppList } from './data.d'; import { AppGroupList, AppList } from './data.d';
/** /**
* *
*/ */
export async function queryAppList( export async function queryAppList(
params?: Record<string, any>, params: Record<string, any>,
sort?: Record<string, SortOrder>, sort: Record<string, SortOrder>,
filter?: Record<string, (string | number)[] | null>, filter: Record<string, (string | number)[] | null>,
): Promise<RequestData<AppList>> { ): Promise<RequestData<AppList>> {
const { result, success } = await request<API.ApiResult<AppList>>('/api/v1/app/list', { const { result, success } = await request<API.ApiResult<AppList>>('/api/v1/app/list', {
params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) }, params: { ...params, ...sortParamConverter(sort), ...filterParamConverter(filter) },
@ -38,3 +38,10 @@ export async function queryAppList(
total: result?.pagination ? result?.pagination.total : 0, total: result?.pagination ? result?.pagination.total : 0,
}; };
} }
/**
*
*/
export async function getAppGroupList(): Promise<API.ApiResult<AppGroupList>> {
return request<API.ApiResult<any>>('/api/v1/app/group_list', {});
}

View File

@ -18,7 +18,6 @@
import { getEncryptSecret } from '@/services'; import { getEncryptSecret } from '@/services';
import type { SortOrder } from 'antd/es/table/interface'; import type { SortOrder } from 'antd/es/table/interface';
import { parse } from 'querystring'; import { parse } from 'querystring';
import type { ReactText } from 'react';
import { history, matchPath } from '@umijs/max'; import { history, matchPath } from '@umijs/max';
import { PhoneNumber } from 'google-libphonenumber'; import { PhoneNumber } from 'google-libphonenumber';
@ -94,7 +93,7 @@ export const sortParamConverter = (value: Record<string, SortOrder> | undefined)
* *
* @param value * @param value
*/ */
export const filterParamConverter = (value: Record<string, ReactText[] | null> | undefined) => { export const filterParamConverter = (value: Record<string, (string | number)[] | null>) => {
const param: Record<string, any> = {}; const param: Record<string, any> = {};
if (value) if (value)
Object.entries(value).forEach(([key], index) => { Object.entries(value).forEach(([key], index) => {