mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-05 13:39:24 +08:00
* feat(auth): init auth package & collection * feat(auth): register * feat(auth): use authenticator * feat(auth): mapRoles * feat(auth): refactor * feat(auth): base auth class * feat(auth): add plugin * chore(auth): test * chore(auth): add test cases * feat(auth): authenticators pane * chore(auth): custom hook useAuthTypes * feat(auth): authenticator pane * chore(auth): store options schema using context * feat(auth): signInPage provider * feat(auth): signUpPage provider * chore(auth): solve build errors * chore(auth): add dependency * chore(auth): remove dependency cycles * chore(auth): add plugin-auth to preset * chore(auth): fix test * feat(auth): authenticator enable status * fix(test): fix test using new authentication * feat(auth): migration, set up basic auth * chore(auth): can set options ui by component * fix(test): workflow manunal.test * fix(test): typo * feat(auth): support multi-language * chore(auth): imporve code * chore(auth): hide button if no configuration * chore(auth): readme * chore(auth): remove allowSignup prop * chore(auth): move configure pane to edit form * fix(auth): jwt options bug * feat(auth): init sms-auth * chore(auth): at least authenticator required * chore(auth): add test * feat(auth): support sms auth * fix(auth): fix test * chore(auth): move findOrCreateUser to AuthModel * chore(auth): history compatible processing * feat(auth): support SAML auth * chore(auth): saml auth list * chore(saml-auth): improve ui * Merge branch 'main' into feat/authentication * chore(auth): improve code * fix(saml-auth): fix bug * fix(saml-auth): fix saml options * chore(saml-auth): compatible processing && ut * fix(auth): signin page bug * chore(auth): saml compatible processing * feat(auth): oidc-auth * fix(oidc-auth): bug * fix(oidc-auth): bug * fix(auth): fix test * chore(auth): filter enabled authenticator * chore(oidc): add field map * chore(auth): update readme * docs(auth): create sms-auth readme * feat(auth): allow signup config * test(auth): fix test * feat(auth): allow saml and oidc use http * chore(oidc-auth): extends timeout * docs(auth): update readme * feat(auth): support sort * docs(saml): update readme * feat(auth): support sort all authenticator * Merge branch 'main' into feat/authentication * Merge branch 'main' into feat/authentication * feat: improve code * docs(auth): add doc * Merge branch 'main' into feat/authentication * chore: update yarn.lock * feat: improve code * chore(acl): write role to acl if it exists in database and not found … (#2001) * chore(acl): write role to acl if it exists in database and not found in acl * fix: test * fix: eager load with nested association (#2002) * chore: upgrade vitest * chore: edit * refactor: auth class * fix: set options * chore(acl): write role to acl if it exists in database and not found … (#2001) * chore(acl): write role to acl if it exists in database and not found in acl * fix: test * fix: eager load with nested association (#2002) * chore: upgrade vitest * chore: add migrations * test: fix api-client test * chore: add sms-auth * feat: avoid no permission after auth type disabled * fix: translation --------- Co-authored-by: chenos <chenlinxh@gmail.com>
398 lines
10 KiB
TypeScript
398 lines
10 KiB
TypeScript
import React, { useEffect, useMemo, useState, useCallback, MouseEventHandler } from 'react';
|
|
import { useAPIClient, useRequest } from '../api-client';
|
|
import {
|
|
Avatar,
|
|
Card,
|
|
message,
|
|
Modal,
|
|
Popconfirm,
|
|
Spin,
|
|
Switch,
|
|
Tabs,
|
|
TabsProps,
|
|
Tag,
|
|
Tooltip,
|
|
Typography,
|
|
} from 'antd';
|
|
import { css } from '@emotion/css';
|
|
import cls from 'classnames';
|
|
import { useHistory } from 'react-router-dom';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons';
|
|
import { useParseMarkdown } from '../schema-component/antd/markdown/util';
|
|
import type { IPluginData } from '.';
|
|
|
|
interface PluginDocumentProps {
|
|
path: string;
|
|
name: string;
|
|
}
|
|
|
|
interface ICommonCard {
|
|
onClick: () => void;
|
|
name: string;
|
|
description: string;
|
|
title: string;
|
|
displayName: string;
|
|
actions?: JSX.Element[];
|
|
}
|
|
|
|
interface IPluginDetail {
|
|
plugin: any;
|
|
onCancel: () => void;
|
|
items: TabsProps['items'];
|
|
}
|
|
|
|
/**
|
|
* get color by string
|
|
* TODO: real avatar
|
|
* @param str
|
|
*/
|
|
const stringToColor = function (str: string) {
|
|
let hash = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
}
|
|
let color = '#';
|
|
for (let i = 0; i < 3; i++) {
|
|
const value = (hash >> (i * 8)) & 0xff;
|
|
color += ('00' + value.toString(16)).substr(-2);
|
|
}
|
|
return color;
|
|
};
|
|
|
|
const PluginDocument: React.FC<PluginDocumentProps> = (props) => {
|
|
const [docLang, setDocLang] = useState('');
|
|
const { name, path } = props;
|
|
const { data, loading, error } = useRequest(
|
|
{
|
|
url: '/plugins:getTabInfo',
|
|
params: {
|
|
filterByTk: name,
|
|
path: path,
|
|
locale: docLang,
|
|
},
|
|
},
|
|
{
|
|
refreshDeps: [name, path, docLang],
|
|
},
|
|
);
|
|
const { html, loading: parseLoading } = useParseMarkdown(data?.data?.content);
|
|
|
|
const htmlWithOutRelativeDirect = useMemo(() => {
|
|
if (html) {
|
|
const pattern = /<a\s+href="\..*?\/([^/]+)"/g;
|
|
return html.replace(pattern, (match, $1) => match + `onclick="return false;"`); // prevent the default event of <a/>
|
|
}
|
|
}, [html]);
|
|
|
|
const handleSwitchDocLang = useCallback((e: MouseEvent) => {
|
|
const lang = (e.target as HTMLDivElement).innerHTML;
|
|
if (lang.trim() === '中文') {
|
|
setDocLang('zh-CN');
|
|
} else if (lang.trim() === 'English') {
|
|
setDocLang('en-US');
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const md = document.getElementById('pm-md-preview');
|
|
md.addEventListener('click', handleSwitchDocLang);
|
|
return () => {
|
|
removeEventListener('click', handleSwitchDocLang);
|
|
};
|
|
}, [handleSwitchDocLang]);
|
|
|
|
return (
|
|
<div
|
|
className={css`
|
|
background: #ffffff;
|
|
padding: var(--nb-spacing); // if the antd can upgrade to v5.0, theme token will be better
|
|
height: 60vh;
|
|
overflow-y: auto;
|
|
`}
|
|
id="pm-md-preview"
|
|
>
|
|
{loading || parseLoading ? (
|
|
<Spin />
|
|
) : (
|
|
<div className="nb-markdown" dangerouslySetInnerHTML={{ __html: error ? '' : htmlWithOutRelativeDirect }}></div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function PluginDetail(props: IPluginDetail) {
|
|
const { plugin, onCancel, items } = props;
|
|
return (
|
|
<Modal
|
|
footer={false}
|
|
className={css`
|
|
.ant-modal-header {
|
|
background: #f0f2f5;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.ant-modal-body {
|
|
padding-top: 0;
|
|
}
|
|
|
|
.ant-modal-body {
|
|
background: #f0f2f5;
|
|
.plugin-desc {
|
|
padding-bottom: 8px;
|
|
}
|
|
}
|
|
`}
|
|
width="70%"
|
|
title={
|
|
<Typography.Title level={2} style={{ margin: 0 }}>
|
|
{plugin?.displayName || plugin?.name}
|
|
<Tag
|
|
className={css`
|
|
vertical-align: middle;
|
|
margin-top: -3px;
|
|
margin-left: 8px;
|
|
`}
|
|
>
|
|
v{plugin?.version}
|
|
</Tag>
|
|
</Typography.Title>
|
|
}
|
|
open={!!plugin}
|
|
onCancel={onCancel}
|
|
destroyOnClose
|
|
>
|
|
{plugin?.description && <div className={'plugin-desc'}>{plugin?.description}</div>}
|
|
<Tabs items={items}></Tabs>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
function CommonCard(props: ICommonCard) {
|
|
const { onClick, name, displayName, actions, description, title } = props;
|
|
return (
|
|
<Card
|
|
bordered={false}
|
|
style={{ width: 'calc(20% - 24px)', marginRight: 24, marginBottom: 24, transition: 'all 0.35s ease-in-out' }}
|
|
onClick={onClick}
|
|
className={cls(css`
|
|
&:hover {
|
|
border: 1px solid var(--antd-wave-shadow-color);
|
|
cursor: pointer;
|
|
}
|
|
|
|
border: 1px solid transparent;
|
|
`)}
|
|
actions={actions}
|
|
// actions={[<a>Settings</a>, <a>Remove</a>, <Switch size={'small'} defaultChecked={true}></Switch>]}
|
|
>
|
|
<Card.Meta
|
|
className={css`
|
|
.ant-card-meta-avatar {
|
|
margin-top: 8px;
|
|
|
|
.ant-avatar {
|
|
border-radius: 2px;
|
|
}
|
|
}
|
|
`}
|
|
avatar={<Avatar style={{ background: `${stringToColor(name)}` }}>{name?.[0]}</Avatar>}
|
|
description={
|
|
<Tooltip title={description} placement="bottom">
|
|
<div
|
|
style={{
|
|
overflow: 'hidden',
|
|
whiteSpace: 'nowrap',
|
|
textOverflow: 'ellipsis',
|
|
}}
|
|
>
|
|
{description || '-'}
|
|
</div>
|
|
</Tooltip>
|
|
}
|
|
title={
|
|
<span>
|
|
{displayName || name}
|
|
<span
|
|
className={css`
|
|
display: block;
|
|
color: rgba(0, 0, 0, 0.45);
|
|
font-weight: normal;
|
|
font-size: 13px;
|
|
// margin-left: 8px;
|
|
`}
|
|
>
|
|
{title}
|
|
</span>
|
|
</span>
|
|
}
|
|
/>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
export const PluginCard = (props: { data: IPluginData }) => {
|
|
const history = useHistory<any>();
|
|
const { data } = props;
|
|
const api = useAPIClient();
|
|
const { t } = useTranslation();
|
|
const { enabled, name, displayName, id, description, version } = data;
|
|
const [plugin, setPlugin] = useState<any>(null);
|
|
const { data: tabsData, run } = useRequest(
|
|
{
|
|
url: '/plugins:getTabs',
|
|
},
|
|
{
|
|
manual: true,
|
|
},
|
|
);
|
|
const items = useMemo<TabsProps['items']>(() => {
|
|
return tabsData?.data?.tabs.map((item) => {
|
|
return {
|
|
label: item.title,
|
|
key: item.path,
|
|
children: React.createElement(PluginDocument, {
|
|
name: tabsData?.data.filterByTk,
|
|
path: item.path,
|
|
}),
|
|
};
|
|
});
|
|
}, [tabsData?.data]);
|
|
|
|
const actions = useMemo(
|
|
() =>
|
|
[
|
|
enabled ? (
|
|
<SettingOutlined
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
history.push(`/admin/settings/${name}`);
|
|
}}
|
|
/>
|
|
) : null,
|
|
<Popconfirm
|
|
key={id}
|
|
title={t('Are you sure to delete this plugin?')}
|
|
onConfirm={async (e) => {
|
|
e.stopPropagation();
|
|
await api.request({
|
|
url: `pm:remove/${name}`,
|
|
});
|
|
message.success(t('插件删除成功'));
|
|
window.location.reload();
|
|
}}
|
|
onCancel={(e) => e.stopPropagation()}
|
|
okText={t('Yes')}
|
|
cancelText={t('No')}
|
|
>
|
|
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
|
|
</Popconfirm>,
|
|
<Switch
|
|
key={id}
|
|
size={'small'}
|
|
onChange={async (checked, e) => {
|
|
e.stopPropagation();
|
|
Modal.warn({
|
|
title: checked ? t('Plugin starting') : t('Plugin stopping'),
|
|
content: t('The application is reloading, please do not close the page.'),
|
|
okButtonProps: {
|
|
style: {
|
|
display: 'none',
|
|
},
|
|
},
|
|
});
|
|
await api.request({
|
|
url: `pm:${checked ? 'enable' : 'disable'}/${name}`,
|
|
});
|
|
window.location.reload();
|
|
// message.success(checked ? t('插件激活成功') : t('插件禁用成功'));
|
|
}}
|
|
defaultChecked={enabled}
|
|
></Switch>,
|
|
].filter(Boolean),
|
|
[api, enabled, history, id, name, t],
|
|
);
|
|
return (
|
|
<>
|
|
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
|
|
<CommonCard
|
|
onClick={() => {
|
|
setPlugin(data);
|
|
run({
|
|
params: {
|
|
filterByTk: name,
|
|
},
|
|
});
|
|
}}
|
|
name={name}
|
|
description={description}
|
|
title={version}
|
|
actions={actions}
|
|
displayName={displayName}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const BuiltInPluginCard = (props: { data: IPluginData }) => {
|
|
const {
|
|
data: { description, name, version, displayName },
|
|
data,
|
|
} = props;
|
|
const history = useHistory();
|
|
const [plugin, setPlugin] = useState<any>(null);
|
|
const { data: tabsData, run } = useRequest(
|
|
{
|
|
url: '/plugins:getTabs',
|
|
},
|
|
{
|
|
manual: true,
|
|
},
|
|
);
|
|
const items = useMemo(() => {
|
|
return tabsData?.data?.tabs.map((item) => {
|
|
return {
|
|
label: item.title,
|
|
key: item.path,
|
|
children: React.createElement(PluginDocument, {
|
|
name: tabsData?.data.filterByTk,
|
|
path: item.path,
|
|
}),
|
|
};
|
|
});
|
|
}, [tabsData?.data]);
|
|
|
|
return (
|
|
<>
|
|
<PluginDetail plugin={plugin} onCancel={() => setPlugin(null)} items={items} />
|
|
<CommonCard
|
|
onClick={() => {
|
|
setPlugin(data);
|
|
run({
|
|
params: {
|
|
filterByTk: name,
|
|
},
|
|
});
|
|
}}
|
|
name={name}
|
|
displayName={displayName}
|
|
description={description}
|
|
title={version}
|
|
actions={[
|
|
<div key="placeholder-comp"></div>,
|
|
<SettingOutlined
|
|
key="settings"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
history.push(`/admin/settings/${name}`);
|
|
}}
|
|
/>,
|
|
]}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
function useCallabck(arg0: () => void, arg1: undefined[]) {
|
|
throw new Error('Function not implemented.');
|
|
}
|