refactor: support org

This commit is contained in:
fantasticit 2022-06-30 14:20:38 +08:00
parent 3e0eb5dc92
commit 808f7877eb
89 changed files with 1249 additions and 1723 deletions

View File

@ -23,6 +23,7 @@ server:
enableRateLimit: true # 是否限流
rateLimitWindowMs: 60000 # 限流时间
rateLimitMax: 1000 # 单位限流时间内单个 ip 最大访问数量
enableEmailVerify: false
email: # 邮箱服务,参考 http://help.163.com/09/1223/14/5R7P6CJ600753VB8.html?servCode=6010376 获取 SMTP 配置
host: ''
port: 465

View File

@ -30,6 +30,7 @@ export const System = () => {
<Banner type="warning" description="系统锁定后,除系统管理员外均不可登录,谨慎修改!" closeIcon={null} />
<Form labelPosition="left" initValues={data} onChange={onFormChange} onSubmit={onFinish}>
<Form.Switch field="isSystemLocked" label="系统锁定" />
<Form.Switch field="enableEmailVerify" label="邮箱检验" />
<Button
htmlType="submit"

View File

@ -20,7 +20,18 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
const { user: currentUser } = useUser();
const [visible, toggleVisible] = useToggle(false);
const [collaborationUsers, setCollaborationUsers] = useState([]);
const content = useMemo(() => <Members id={documentId} hook={useDoumentMembers} />, [documentId]);
const content = useMemo(
() => (
<div style={{ padding: '24px 0' }}>
<Members
id={documentId}
hook={useDoumentMembers}
descriptions={['权限继承:默认继承知识库成员权限', '超级管理员:知识库超级管理员和文档创建者']}
/>
</div>
),
[documentId]
);
const btn = useMemo(
() => (
<Button theme="borderless" type="tertiary" disabled={disabled} icon={<IconUserAdd />} onClick={toggleVisible} />
@ -79,7 +90,7 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
visible={visible}
footer={null}
onCancel={toggleVisible}
style={{ maxWidth: '96vw' }}
style={{ maxWidth: '96vw', maxHeight: '60vh', overflow: 'auto' }}
>
{content}
</Modal>
@ -97,6 +108,8 @@ export const DocumentCollaboration: React.FC<IProps> = ({ wikiId, documentId, di
width: 412,
maxWidth: '96vw',
padding: '0 24px',
maxHeight: '60vh',
overflow: 'auto',
}}
>
{content}

View File

@ -1,5 +1,5 @@
import { IconDelete } from '@douyinfe/semi-icons';
import { Modal, Popconfirm, Space, Typography } from '@douyinfe/semi-ui';
import { Popconfirm, Space, Typography } from '@douyinfe/semi-ui';
import { useDeleteDocument } from 'data/document';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
@ -15,8 +15,11 @@ interface IProps {
const { Text } = Typography;
export const DocumentDeletor: React.FC<IProps> = ({ wikiId, documentId, render, onDelete }) => {
const { wikiId: currentWikiId, documentId: currentDocumentId } =
useRouterQuery<{ wikiId?: string; documentId?: string }>();
const {
organizationId,
wikiId: currentWikiId,
documentId: currentDocumentId,
} = useRouterQuery<{ organizationId: string; wikiId?: string; documentId?: string }>();
const { deleteDocument: api, loading } = useDeleteDocument(documentId);
const deleteAction = useCallback(() => {
@ -26,14 +29,15 @@ export const DocumentDeletor: React.FC<IProps> = ({ wikiId, documentId, render,
return;
}
Router.push({
pathname: `/wiki/${wikiId}`,
pathname: `/app/org/[organizationId]/wiki/[wikiId]`,
query: { organizationId, wikiId },
});
};
navigate();
onDelete && onDelete();
});
}, [wikiId, documentId, api, onDelete, currentWikiId, currentDocumentId]);
}, [organizationId, wikiId, documentId, api, onDelete, currentWikiId, currentDocumentId]);
const content = useMemo(
() => (

View File

@ -1,16 +1,11 @@
import { IAuthority, ILoginUser } from '@think/domains';
import cls from 'classnames';
import { useDoumentMembers } from 'data/document';
import { event, triggerChangeDocumentTitle, triggerJoinUser, USE_DOCUMENT_VERSION } from 'event';
import { useMount } from 'hooks/use-mount';
import { useToggle } from 'hooks/use-toggle';
import Router from 'next/router';
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef } from 'react';
import { CollaborationEditor, ICollaborationRefProps } from 'tiptap/editor';
import { findMentions } from 'tiptap/prose-utils';
import styles from './index.module.scss';
import { DocumentUserSetting } from './users';
interface IProps {
user: ILoginUser;
@ -19,12 +14,8 @@ interface IProps {
}
export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, authority }) => {
const $hasShowUserSettingModal = useRef(false);
const $editor = useRef<ICollaborationRefProps>();
const mounted = useMount();
const { users, addUser, updateUser } = useDoumentMembers(documentId);
const [mentionUsersSettingVisible, toggleMentionUsersSettingVisible] = useToggle(false);
const [mentionUsers, setMentionUsers] = useState([]);
useEffect(() => {
const handler = (data) => {
@ -39,56 +30,6 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
};
}, []);
useEffect(() => {
const handler = () => {
const editor = $editor.current && $editor.current.getEditor();
if (!editor) return;
// 已经拦截过一次,不再拦截
if ($hasShowUserSettingModal.current) return;
const mentionUsers = findMentions(editor);
if (!mentionUsers || !mentionUsers.length) return;
const currentUserAuth = users.find((user) => {
return user.user.name === currentUser.name;
});
const isCurrentUserCreateUser = currentUserAuth.auth.createUserId === currentUser.id;
if (!isCurrentUserCreateUser) {
return;
}
const data = Array.from(new Set(mentionUsers))
.filter((userName) => {
const exist = users.find((user) => {
return user.user.name === userName;
});
if (!exist || !exist.auth.readable) return true;
return false;
})
.filter(Boolean);
if (!data.length) return;
setMentionUsers(data);
toggleMentionUsersSettingVisible(true);
$hasShowUserSettingModal.current = true;
// ignore-me
const newErr = new Error('请完成权限操作后关闭页面');
throw newErr;
};
Router.events.on('routeChangeStart', handler);
window.addEventListener('unload', handler);
return () => {
$hasShowUserSettingModal.current = false;
Router.events.off('routeChangeStart', handler);
window.removeEventListener('unload', handler);
};
}, [users, currentUser, toggleMentionUsersSettingVisible]);
return (
<div className={cls(styles.editorWrap)}>
{mounted && (
@ -103,14 +44,6 @@ export const Editor: React.FC<IProps> = ({ user: currentUser, documentId, author
onAwarenessUpdate={triggerJoinUser}
/>
)}
<DocumentUserSetting
visible={mentionUsersSettingVisible}
toggleVisible={toggleMentionUsersSettingVisible}
mentionUsers={mentionUsers}
users={users}
addUser={addUser}
updateUser={updateUser}
/>
</div>
);
};

View File

@ -1,94 +0,0 @@
import { Checkbox, Modal, Table, Typography } from '@douyinfe/semi-ui';
import { IAuthority, IUser } from '@think/domains';
import { DocAuth } from 'data/document';
import React, { useMemo } from 'react';
interface IProps {
visible: boolean;
toggleVisible: (arg: boolean) => void;
mentionUsers: string[];
users: Array<{ user: IUser; auth: IAuthority }>;
addUser: (auth: DocAuth) => Promise<unknown>;
updateUser: (auth: DocAuth) => Promise<unknown>;
}
const { Text } = Typography;
const { Column } = Table;
// eslint-disable-next-line react/display-name
const renderChecked = (onChange, authKey: 'readable' | 'editable') => (checked, data) => {
const handle = (evt) => {
const ret = {
...data,
};
ret[authKey] = evt.target.checked;
onChange(ret);
};
return <Checkbox style={{ display: 'inline-block' }} checked={checked} onChange={handle} />;
};
export const DocumentUserSetting: React.FC<IProps> = ({
visible,
toggleVisible,
mentionUsers,
users,
addUser,
updateUser,
}) => {
const renderUsers = useMemo(() => {
return mentionUsers
.map((mentionUser) => {
const exist = users.find((user) => {
return user.user.name === mentionUser;
});
if (!exist) return { userName: mentionUser, readable: false, editable: false, shouldAddToDocument: true };
return {
userName: mentionUser,
readable: exist.auth.readable,
editable: exist.auth.editable,
shouldAddToDocument: false,
};
})
.filter(Boolean);
}, [users, mentionUsers]);
const handler = async (data) => {
if (data.shouldAddToDocument) {
await addUser(data.userName);
}
await updateUser(data);
};
return (
<Modal
title={'权限操作'}
visible={visible}
onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }}
footer={null}
>
<Text> @ </Text>
<Table style={{ margin: '24px 0' }} dataSource={renderUsers} size="small" pagination>
<Column title="用户名" dataIndex="userName" key="name" />
<Column
title="是否可读"
dataIndex="readable"
key="readable"
render={renderChecked(handler, 'readable')}
align="center"
/>
<Column
title="是否可编辑"
dataIndex="editable"
key="editable"
render={renderChecked(handler, 'editable')}
align="center"
/>
</Table>
</Modal>
);
};

View File

@ -1,14 +1,12 @@
import { Banner, Button, Input, Modal, Select, Space } from '@douyinfe/semi-ui';
import { Banner, Input, Popconfirm, Select, Space } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray } from '@think/domains';
import React, { useCallback, useState } from 'react';
interface IProps {
visible: boolean;
toggleVisible: (arg) => void;
onOk: (arg) => any;
}
export const AddUser: React.FC<IProps> = ({ visible, toggleVisible, onOk }) => {
export const AddUser: React.FC<IProps> = ({ onOk, children }) => {
const [userAuth, setUserAuth] = useState(AuthEnum.noAccess);
const [userName, setUserName] = useState('');
@ -16,27 +14,26 @@ export const AddUser: React.FC<IProps> = ({ visible, toggleVisible, onOk }) => {
onOk({ userName, userAuth }).then(() => {
setUserAuth(AuthEnum.noAccess);
setUserName('');
toggleVisible(false);
});
}, [onOk, userName, userAuth, toggleVisible]);
}, [onOk, userName, userAuth]);
return (
<Modal
<Popconfirm
zIndex={1070}
title={'添加成员'}
okText={'邀请对方'}
visible={visible}
onOk={handleOk}
onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }}
footer={null}
>
<div style={{ marginTop: 16 }}>
style={{ maxWidth: '96vw', width: 380 }}
onConfirm={handleOk}
okButtonProps={{
disabled: !userName,
}}
content={
<div style={{ margin: '16px -68px 0 0' }}>
{[AuthEnum.creator, AuthEnum.admin].includes(userAuth) ? (
<Banner style={{ marginBottom: 16 }} type="warning" description="请谨慎操作管理员权限!" />
) : null}
<Space>
<Select value={userAuth} onChange={setUserAuth} style={{ width: 120 }}>
<Select value={userAuth} onChange={setUserAuth} style={{ width: 120 }} zIndex={1080}>
{AuthEnumArray.map((wikiStatus) => {
return (
<Select.Option key={wikiStatus.value} value={wikiStatus.value}>
@ -50,13 +47,13 @@ export const AddUser: React.FC<IProps> = ({ visible, toggleVisible, onOk }) => {
placeholder="输入对方用户名"
value={userName}
onChange={setUserName}
style={{ width: 270 }}
style={{ width: 160 }}
></Input>
</Space>
<Button theme="solid" block style={{ margin: '24px 0' }} onClick={handleOk} disabled={!userName}>
</Button>
</div>
</Modal>
}
>
{children}
</Popconfirm>
);
};

View File

@ -1,41 +1,26 @@
import { Banner, Button, Modal, Select } from '@douyinfe/semi-ui';
import { Banner, Popconfirm, Select, Toast } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
interface IProps {
visible: boolean;
toggleVisible: (arg) => void;
currentUser: { user: IUser; auth: IAuth };
onOk: (arg) => any;
userWithAuth: { user: IUser; auth: IAuth };
updateUser: (arg) => any;
}
export const EditUser: React.FC<IProps> = ({ visible, toggleVisible, currentUser, onOk }) => {
export const EditUser: React.FC<IProps> = ({ userWithAuth, updateUser, children }) => {
const [userAuth, setUserAuth] = useState(AuthEnum.noAccess);
const handleOk = useCallback(() => {
onOk(userAuth).then(() => {
setUserAuth(AuthEnum.noAccess);
toggleVisible(false);
return updateUser({ userName: userWithAuth.user.name, userAuth }).then(() => {
Toast.success('操作成功');
});
}, [onOk, userAuth, toggleVisible]);
useEffect(() => {
if (!visible) {
setUserAuth(AuthEnum.noAccess);
}
}, [visible]);
}, [updateUser, userAuth, userWithAuth]);
return (
<Modal
title={`修改用户${currentUser && currentUser.user.name}权限`}
visible={visible}
onOk={handleOk}
onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }}
footer={null}
>
<div style={{ marginTop: 16 }}>
<Popconfirm
title={`修改用户${userWithAuth && userWithAuth.user.name}权限`}
content={
<div style={{ margin: '16px -68px 0 0' }}>
{[AuthEnum.creator, AuthEnum.admin].includes(userAuth) ? (
<Banner style={{ marginBottom: 16 }} type="warning" description="请谨慎操作管理员权限!" />
) : null}
@ -49,10 +34,11 @@ export const EditUser: React.FC<IProps> = ({ visible, toggleVisible, currentUser
);
})}
</Select>
<Button theme="solid" block style={{ margin: '24px 0' }} onClick={handleOk}>
</Button>
</div>
</Modal>
}
onConfirm={handleOk}
>
{children}
</Popconfirm>
);
};

View File

@ -1,11 +1,9 @@
import { IconDelete, IconEdit } from '@douyinfe/semi-icons';
import { Banner, Button, Popconfirm, Table, Typography } from '@douyinfe/semi-ui';
import { AuthEnumTextMap, IOrganization } from '@think/domains';
import { AuthEnumTextMap } from '@think/domains';
import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
import { useOrganizationMembers } from 'data/organization';
import { useToggle } from 'hooks/use-toggle';
import React, { useState } from 'react';
import React from 'react';
import { AddUser } from './add';
import { EditUser } from './edit';
@ -14,33 +12,17 @@ import styles from './index.module.scss';
interface IProps {
id: string;
hook: any;
descriptions?: Array<string>;
}
const { Title, Paragraph } = Typography;
const { Column } = Table;
export const Members: React.FC<IProps> = ({ id, hook }) => {
const { data, loading, error, addUser, updateUser, deleteUser } = hook(id);
const [visible, toggleVisible] = useToggle(false);
const [editVisible, toggleEditVisible] = useToggle(false);
const [currentUser, setCurrentUser] = useState(null);
const editUser = (user) => {
setCurrentUser(user);
toggleEditVisible(true);
};
const handleEdit = (userAuth) => {
return updateUser({ userName: currentUser.user.name, userAuth }).then(() => {
setCurrentUser(null);
});
};
console.log(data);
export const Members: React.FC<IProps> = ({ id, hook, descriptions }) => {
const { data, loading, error, page, pageSize, setPage, addUser, updateUser, deleteUser } = hook(id);
return (
<div className={styles.wrap}>
<header>{/* <MemberAdder /> */}</header>
<DataRender
loading={loading}
error={error}
@ -55,19 +37,36 @@ export const Members: React.FC<IProps> = ({ id, hook }) => {
title={<Title heading={6}></Title>}
description={
<div>
{descriptions && descriptions.length ? (
descriptions.map((desc) => {
return <Paragraph key={desc}>{desc}</Paragraph>;
})
) : (
<>
<Paragraph></Paragraph>
<Paragraph></Paragraph>
<Paragraph>访</Paragraph>
</>
)}
</div>
}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button theme="solid" onClick={toggleVisible}>
</Button>
<AddUser onOk={addUser}>
<Button theme="solid"></Button>
</AddUser>
</div>
<Table style={{ margin: '16px 0' }} dataSource={data.data} size="small" pagination={false}>
<Table
style={{ margin: '16px 0' }}
dataSource={data.data}
size="small"
pagination={{
currentPage: page,
pageSize,
total: data.total,
onPageChange: setPage,
}}
>
<Column title="用户名" dataIndex="user.name" key="user.name" />
<Column
title="成员权限"
@ -90,7 +89,9 @@ export const Members: React.FC<IProps> = ({ id, hook }) => {
align="center"
render={(_, data) => (
<>
<Button type="tertiary" theme="borderless" icon={<IconEdit />} onClick={() => editUser(data)} />
<EditUser userWithAuth={data} updateUser={updateUser}>
<Button type="tertiary" theme="borderless" icon={<IconEdit />} />
</EditUser>
<Popconfirm
showArrow
title="确认删除该成员?"
@ -105,9 +106,6 @@ export const Members: React.FC<IProps> = ({ id, hook }) => {
</div>
)}
/>
<AddUser visible={visible} toggleVisible={toggleVisible} onOk={addUser} />
<EditUser visible={editVisible} toggleVisible={toggleEditVisible} currentUser={currentUser} onOk={handleEdit} />
</div>
);
};

View File

@ -0,0 +1,49 @@
import { IconDelete } from '@douyinfe/semi-icons';
import { Modal, Space, Typography } from '@douyinfe/semi-ui';
import { IOrganization } from '@think/domains';
import { useOrganizationDetail } from 'data/organization';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import React, { useCallback } from 'react';
interface IProps {
organizationId: IOrganization['id'];
onDelete?: () => void;
}
const { Text } = Typography;
export const OrganizationDeletor: React.FC<IProps> = ({ organizationId, onDelete, children }) => {
const { deleteOrganization } = useOrganizationDetail(organizationId);
const deleteAction = useCallback(() => {
Modal.error({
title: '确定删除吗?',
content: <Text></Text>,
onOk: () => {
deleteOrganization().then(() => {
onDelete
? onDelete()
: Router.push({
pathname: `/`,
});
});
},
okButtonProps: {
type: 'danger',
},
style: { maxWidth: '96vw' },
});
}, [deleteOrganization, onDelete]);
return (
<Text type="danger" onClick={deleteAction}>
{children || (
<Space>
<IconDelete />
</Space>
)}
</Text>
);
};

View File

@ -1,33 +1,23 @@
import { IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { IconAppCenter, IconApps, IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { Avatar, Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { LogoImage, LogoText } from 'components/logo';
import { useUserOrganizations } from 'data/organization';
import { useUser } from 'data/user';
import Link from 'next/link';
import Router from 'next/router';
import { useCallback } from 'react';
import styles from './index.module.scss';
const { Text, Paragraph } = Typography;
export const OrganizationPublicSwitcher = () => {
const Inner = () => {
const {
data: userOrganizations,
loading: userOrganizationsLoading,
error: userOrganizationsError,
} = useUserOrganizations();
const gotoCreate = useCallback(() => {
Router.push(`/app/org/create`);
}, []);
return (
<span className={styles.nameWrap}>
<Space>
<LogoImage />
<LogoText />
</Space>
<Dropdown
trigger="click"
render={
@ -37,7 +27,9 @@ export const OrganizationPublicSwitcher = () => {
normalContent={() => {
return (
<Dropdown.Menu>
{(userOrganizations || []).map((org) => {
{userOrganizations.length ? (
<>
{userOrganizations.map((org) => {
return (
<Dropdown.Item key={org.id}>
<Link
@ -66,10 +58,44 @@ export const OrganizationPublicSwitcher = () => {
);
})}
<Dropdown.Divider />
<Dropdown.Item onClick={gotoCreate}>
</>
) : null}
<Dropdown.Item>
<Link
href={{
pathname: '/',
}}
>
<a style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Text>
<Space></Space>
<Space>
<Avatar size="extra-small">
<IconApps />
</Avatar>
广
</Space>
</Text>
</a>
</Link>
</Dropdown.Item>
<Dropdown.Item>
<Link
href={{
pathname: '/app/org/create',
}}
>
<a style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Text>
<Space>
<Avatar size="extra-small">
<IconAppCenter />
</Avatar>
</Space>
</Text>
</a>
</Link>
</Dropdown.Item>
</Dropdown.Menu>
);
@ -79,6 +105,19 @@ export const OrganizationPublicSwitcher = () => {
>
<Button size="small" icon={<IconSmallTriangleDown />} style={{ marginLeft: 12 }} />
</Dropdown>
);
};
export const OrganizationPublicSwitcher = () => {
const { user } = useUser();
return (
<span className={styles.nameWrap}>
<Space>
<LogoImage />
<LogoText />
</Space>
{user && <Inner />}
</span>
);
};

View File

@ -36,29 +36,9 @@ export const OrganizationSetting: React.FC<IProps> = ({ organizationId, tab, onN
<OrganizationMembers organizationId={organizationId} />
</TabPane>
{/* <TabPane tab={TitleMap['base']} itemKey="base">
<Base wiki={data} update={update as any} />
</TabPane>
<TabPane tab={TitleMap['users']} itemKey="users">
<Users wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['tocs']} itemKey="tocs">
<WikiTocsManager wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['privacy']} itemKey="privacy">
<Privacy wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['import']} itemKey="import">
<Import wikiId={wikiId} />
</TabPane>
<TabPane tab={TitleMap['more']} itemKey="more">
<More wikiId={wikiId} />
</TabPane> */}
<More organizationId={organizationId} />
</TabPane>
</Tabs>
</>
);

View File

@ -1,62 +0,0 @@
import { Banner, Button, Input, Modal, Select, Space } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray } from '@think/domains';
import React, { useCallback, useState } from 'react';
interface IProps {
visible: boolean;
toggleVisible: (arg) => void;
onOk: (arg) => any;
}
export const AddUser: React.FC<IProps> = ({ visible, toggleVisible, onOk }) => {
const [userAuth, setUserAuth] = useState(AuthEnum.noAccess);
const [userName, setUserName] = useState('');
const handleOk = useCallback(() => {
onOk({ userName, userAuth }).then(() => {
setUserAuth(AuthEnum.noAccess);
setUserName('');
toggleVisible(false);
});
}, [onOk, userName, userAuth, toggleVisible]);
return (
<Modal
title={'添加成员'}
okText={'邀请对方'}
visible={visible}
onOk={handleOk}
onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }}
footer={null}
>
<div style={{ marginTop: 16 }}>
{[AuthEnum.creator, AuthEnum.admin].includes(userAuth) ? (
<Banner style={{ marginBottom: 16 }} type="warning" description="请谨慎操作管理员权限!" />
) : null}
<Space>
<Select value={userAuth} onChange={setUserAuth} style={{ width: 120 }}>
{AuthEnumArray.map((wikiStatus) => {
return (
<Select.Option key={wikiStatus.value} value={wikiStatus.value}>
{wikiStatus.label}
</Select.Option>
);
})}
</Select>
<Input
autofocus
placeholder="输入对方用户名"
value={userName}
onChange={setUserName}
style={{ width: 270 }}
></Input>
</Space>
<Button theme="solid" block style={{ margin: '24px 0' }} onClick={handleOk} disabled={!userName}>
</Button>
</div>
</Modal>
);
};

View File

@ -1,58 +0,0 @@
import { Banner, Button, Modal, Select } from '@douyinfe/semi-ui';
import { AuthEnum, AuthEnumArray, IAuth, IUser } from '@think/domains';
import React, { useCallback, useEffect, useState } from 'react';
interface IProps {
visible: boolean;
toggleVisible: (arg) => void;
currentUser: { user: IUser; auth: IAuth };
onOk: (arg) => any;
}
export const EditUser: React.FC<IProps> = ({ visible, toggleVisible, currentUser, onOk }) => {
const [userAuth, setUserAuth] = useState(AuthEnum.noAccess);
const handleOk = useCallback(() => {
onOk(userAuth).then(() => {
setUserAuth(AuthEnum.noAccess);
toggleVisible(false);
});
}, [onOk, userAuth, toggleVisible]);
useEffect(() => {
if (!visible) {
setUserAuth(AuthEnum.noAccess);
}
}, [visible]);
return (
<Modal
title={`修改用户${currentUser && currentUser.user.name}权限`}
visible={visible}
onOk={handleOk}
onCancel={() => toggleVisible(false)}
maskClosable={false}
style={{ maxWidth: '96vw' }}
footer={null}
>
<div style={{ marginTop: 16 }}>
{[AuthEnum.creator, AuthEnum.admin].includes(userAuth) ? (
<Banner style={{ marginBottom: 16 }} type="warning" description="请谨慎操作管理员权限!" />
) : null}
{}
<Select value={userAuth} onChange={setUserAuth} style={{ width: '100%' }}>
{AuthEnumArray.map((wikiStatus) => {
return (
<Select.Option key={wikiStatus.value} value={wikiStatus.value}>
{wikiStatus.label}
</Select.Option>
);
})}
</Select>
<Button theme="solid" block style={{ margin: '24px 0' }} onClick={handleOk}>
</Button>
</div>
</Modal>
);
};

View File

@ -1,6 +0,0 @@
.wrap {
> header {
display: flex;
justify-content: flex-end;
}
}

View File

@ -1,110 +1,12 @@
import { IconDelete, IconEdit } from '@douyinfe/semi-icons';
import { Banner, Button, Popconfirm, Table, Typography } from '@douyinfe/semi-ui';
import { AuthEnumTextMap, IOrganization } from '@think/domains';
import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
import { IOrganization } from '@think/domains';
import { Members } from 'components/members';
import { useOrganizationMembers } from 'data/organization';
import { useToggle } from 'hooks/use-toggle';
import React, { useState } from 'react';
import { AddUser } from './add';
import { EditUser } from './edit';
import styles from './index.module.scss';
import React from 'react';
interface IProps {
organizationId: IOrganization['id'];
}
const { Title, Paragraph } = Typography;
const { Column } = Table;
export const OrganizationMembers: React.FC<IProps> = ({ organizationId }) => {
const { data, loading, error, refresh, addUser, updateUser, deleteUser } = useOrganizationMembers(organizationId);
const [visible, toggleVisible] = useToggle(false);
const [editVisible, toggleEditVisible] = useToggle(false);
const [currentUser, setCurrentUser] = useState(null);
const editUser = (user) => {
setCurrentUser(user);
toggleEditVisible(true);
};
const handleEdit = (userAuth) => {
return updateUser({ userName: currentUser.user.name, userAuth }).then(() => {
setCurrentUser(null);
});
};
return (
<div className={styles.wrap}>
<header>{/* <MemberAdder /> */}</header>
<DataRender
loading={loading}
error={error}
normalContent={() => (
<div>
<Banner
fullMode={false}
type="info"
bordered
icon={null}
style={{ margin: '16px 0' }}
title={<Title heading={6}></Title>}
description={
<div>
<Paragraph></Paragraph>
<Paragraph></Paragraph>
<Paragraph>访</Paragraph>
</div>
}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button theme="solid" onClick={toggleVisible}>
</Button>
</div>
<Table style={{ margin: '16px 0' }} dataSource={data.data} size="small" pagination={false}>
<Column title="用户名" dataIndex="user.name" key="user.name" />
<Column
title="成员权限"
dataIndex="auth.auth"
key="auth.auth"
align="center"
render={(auth) => AuthEnumTextMap[auth]}
/>
<Column
title="加入时间"
dataIndex="auth.createdAt"
key="auth.createdAt"
align="center"
render={(d) => <LocaleTime date={d} />}
/>
<Column
title="操作"
dataIndex="operate"
key="operate"
align="center"
render={(_, data) => (
<>
<Button type="tertiary" theme="borderless" icon={<IconEdit />} onClick={() => editUser(data)} />
<Popconfirm
showArrow
title="确认删除该成员?"
onConfirm={() => deleteUser({ userName: data.user.name, userAuth: data.auth.auth })}
>
<Button type="tertiary" theme="borderless" icon={<IconDelete />} />
</Popconfirm>
</>
)}
/>
</Table>
</div>
)}
/>
<AddUser visible={visible} toggleVisible={toggleVisible} onOk={addUser} />
<EditUser visible={editVisible} toggleVisible={toggleEditVisible} currentUser={currentUser} onOk={handleEdit} />
</div>
);
return <Members id={organizationId} hook={useOrganizationMembers} />;
};

View File

@ -1,27 +1,23 @@
import { Banner, Button, Typography } from '@douyinfe/semi-ui';
import { WorkspaceDeletor } from 'components/wiki/delete';
interface IProps {
wikiId: string;
}
import { OrganizationDeletor } from 'components/organization/delete';
const { Paragraph } = Typography;
export const More: React.FC<IProps> = ({ wikiId }) => {
export const More = ({ organizationId }) => {
return (
<div style={{ marginTop: 16 }}>
<Banner
fullMode={false}
type="danger"
closeIcon={null}
description={<Paragraph></Paragraph>}
description={<Paragraph></Paragraph>}
style={{ marginBottom: 16 }}
/>
<WorkspaceDeletor wikiId={wikiId}>
<OrganizationDeletor organizationId={organizationId}>
<Button type="danger" theme="solid">
</Button>
</WorkspaceDeletor>
</OrganizationDeletor>
</div>
);
};

View File

@ -1,12 +1,10 @@
import { IconAppCenter, IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { IconAppCenter, IconApps, IconSmallTriangleDown } from '@douyinfe/semi-icons';
import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui';
import { Avatar } from '@douyinfe/semi-ui';
import { DataRender } from 'components/data-render';
import { useOrganizationDetail, useUserOrganizations } from 'data/organization';
import { useRouterQuery } from 'hooks/use-router-query';
import Link from 'next/link';
import Router from 'next/router';
import { useCallback } from 'react';
import styles from './index.module.scss';
@ -22,10 +20,6 @@ export const OrganizationSwitcher = () => {
error: userOrganizationsError,
} = useUserOrganizations();
const gotoCreate = useCallback(() => {
Router.push(`/app/org/create`);
}, []);
return (
<DataRender
loading={loading}
@ -64,7 +58,9 @@ export const OrganizationSwitcher = () => {
normalContent={() => {
return (
<Dropdown.Menu>
{(userOrganizations || []).map((org) => {
{userOrganizations.length ? (
<>
{userOrganizations.map((org) => {
return (
<Dropdown.Item key={org.id}>
<Link
@ -93,7 +89,34 @@ export const OrganizationSwitcher = () => {
);
})}
<Dropdown.Divider />
<Dropdown.Item onClick={gotoCreate}>
</>
) : null}
<Dropdown.Item>
<Link
href={{
pathname: '/',
}}
>
<a style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Text>
<Space>
<Avatar size="extra-small">
<IconApps />
</Avatar>
广
</Space>
</Text>
</a>
</Link>
</Dropdown.Item>
<Dropdown.Item>
<Link
href={{
pathname: '/app/org/create',
}}
>
<a style={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Text>
<Space>
<Avatar size="extra-small">
@ -102,6 +125,8 @@ export const OrganizationSwitcher = () => {
</Space>
</Text>
</a>
</Link>
</Dropdown.Item>
</Dropdown.Menu>
);

View File

@ -27,6 +27,7 @@ export const WikiCreator: React.FC<IProps> = ({ visible, toggleVisible }) => {
});
});
};
const handleCancel = () => {
toggleVisible(false);
};

View File

@ -1,6 +1,7 @@
import { IconDelete } from '@douyinfe/semi-icons';
import { Modal, Space, Typography } from '@douyinfe/semi-ui';
import { useOwnWikis } from 'data/wiki';
import { useRouterQuery } from 'hooks/use-router-query';
import Router from 'next/router';
import React, { useCallback } from 'react';
@ -11,8 +12,9 @@ interface IProps {
const { Text } = Typography;
export const WorkspaceDeletor: React.FC<IProps> = ({ wikiId, onDelete, children }) => {
const { deletWiki } = useOwnWikis();
export const WikiDeletor: React.FC<IProps> = ({ wikiId, onDelete, children }) => {
const { organizationId } = useRouterQuery<{ organizationId: string }>();
const { deletWiki } = useOwnWikis(organizationId);
const deleteAction = useCallback(() => {
Modal.error({
@ -23,7 +25,8 @@ export const WorkspaceDeletor: React.FC<IProps> = ({ wikiId, onDelete, children
onDelete
? onDelete()
: Router.push({
pathname: `/wiki`,
pathname: `/app/org/[organizationId]`,
query: { organizationId },
});
});
},
@ -32,7 +35,7 @@ export const WorkspaceDeletor: React.FC<IProps> = ({ wikiId, onDelete, children
},
style: { maxWidth: '96vw' },
});
}, [wikiId, deletWiki, onDelete]);
}, [organizationId, wikiId, deletWiki, onDelete]);
return (
<Text type="danger" onClick={deleteAction}>

View File

@ -1,5 +1,5 @@
import { Banner, Button, Typography } from '@douyinfe/semi-ui';
import { WorkspaceDeletor } from 'components/wiki/delete';
import { WikiDeletor } from 'components/wiki/delete';
interface IProps {
wikiId: string;
@ -17,11 +17,11 @@ export const More: React.FC<IProps> = ({ wikiId }) => {
description={<Paragraph></Paragraph>}
style={{ marginBottom: 16 }}
/>
<WorkspaceDeletor wikiId={wikiId}>
<WikiDeletor wikiId={wikiId}>
<Button type="danger" theme="solid">
</Button>
</WorkspaceDeletor>
</WikiDeletor>
</div>
);
};

View File

@ -7,5 +7,11 @@ interface IProps {
}
export const Users: React.FC<IProps> = ({ wikiId }) => {
return <Members id={wikiId} hook={useWikiMembers} />;
return (
<Members
id={wikiId}
hook={useWikiMembers}
descriptions={['权限继承:默认继承组织成员权限', '超级管理员:组织超级管理员和知识库创建者']}
/>
);
};

View File

@ -65,11 +65,20 @@ export type DocAuth = {
* @param cookie
* @returns
*/
export const getDocumentMembers = (documentId, cookie = null): Promise<Array<{ user: IUser; auth: IAuthority }>> => {
export const getDocumentMembers = (
documentId,
page,
pageSize,
cookie = null
): Promise<Array<{ user: IUser; auth: IAuthority }>> => {
return HttpClient.request({
method: DocumentApiDefinition.getMemberById.method,
url: DocumentApiDefinition.getMemberById.client(documentId),
cookie,
params: {
page,
pageSize,
},
});
};
@ -79,9 +88,11 @@ export const getDocumentMembers = (documentId, cookie = null): Promise<Array<{ u
* @returns
*/
export const useDoumentMembers = (documentId, options?: UseQueryOptions<Array<{ user: IUser; auth: IAuthority }>>) => {
const [pageSize] = useState(12);
const [page, setPage] = useState(1);
const { data, error, isLoading, refetch } = useQuery(
DocumentApiDefinition.getMemberById.client(documentId),
() => getDocumentMembers(documentId),
[DocumentApiDefinition.getMemberById.client(documentId), page],
() => getDocumentMembers(documentId, page, pageSize),
options
);
@ -124,7 +135,7 @@ export const useDoumentMembers = (documentId, options?: UseQueryOptions<Array<{
[refetch, documentId]
);
return { data, loading: isLoading, error, addUser, updateUser, deleteUser };
return { data, loading: isLoading, error, page, pageSize, setPage, addUser, updateUser, deleteUser };
};
/**

View File

@ -108,17 +108,32 @@ export const useOrganizationDetail = (id) => {
[refetch, id]
);
return { data, error, loading: isLoading, refresh: refetch, update };
const deleteOrganization = useCallback(async () => {
const res = await HttpClient.request({
method: OrganizationApiDefinition.deleteOrganization.method,
url: OrganizationApiDefinition.deleteOrganization.client(id),
});
refetch();
return res;
}, [refetch, id]);
return { data, error, loading: isLoading, refresh: refetch, update, deleteOrganization };
};
export const getOrganizationMembers = (
id,
page = 1,
pageSize,
cookie = null
): Promise<{ data: Array<{ auth: IAuth; user: IUser }>; total: number }> => {
return HttpClient.request({
method: OrganizationApiDefinition.getMembers.method,
url: OrganizationApiDefinition.getMembers.client(id),
cookie,
params: {
page,
pageSize,
},
});
};
@ -127,8 +142,10 @@ export const getOrganizationMembers = (
* @returns
*/
export const useOrganizationMembers = (id) => {
const { data, error, isLoading, refetch } = useQuery(OrganizationApiDefinition.getMembers.client(id), () =>
getOrganizationMembers(id)
const [pageSize] = useState(12);
const [page, setPage] = useState(1);
const { data, error, isLoading, refetch } = useQuery([OrganizationApiDefinition.getMembers.client(id), page], () =>
getOrganizationMembers(id, page, pageSize)
);
const addUser = useCallback(
@ -170,5 +187,16 @@ export const useOrganizationMembers = (id) => {
[refetch, id]
);
return { data, error, loading: isLoading, refresh: refetch, addUser, updateUser, deleteUser };
return {
data,
error,
loading: isLoading,
page,
pageSize,
setPage,
refresh: refetch,
addUser,
updateUser,
deleteUser,
};
};

View File

@ -1,5 +1,5 @@
import { Toast } from '@douyinfe/semi-ui';
import { ILoginUser, ISystemConfig, IUser, UserApiDefinition } from '@think/domains';
import { ILoginUser, ISystemConfig, IUser, SystemApiDefinition, UserApiDefinition } from '@think/domains';
import { getStorage, setStorage } from 'helpers/storage';
import { useAsyncLoading } from 'hooks/use-async-loading';
import Router, { useRouter } from 'next/router';
@ -138,10 +138,17 @@ export const useUser = () => {
};
};
/**
*
* @returns
*/
export const useSystemPublicConfig = () => {
const { data, error, isLoading, refetch } = useQuery(SystemApiDefinition.getPublicConfig.client(), () =>
HttpClient.request<ISystemConfig>({
method: SystemApiDefinition.getPublicConfig.method,
url: SystemApiDefinition.getPublicConfig.client(),
})
);
return { data, error, loading: isLoading, refresh: refetch };
};
export const useSystemConfig = () => {
const { data, error, isLoading, refetch } = useQuery(UserApiDefinition.getSystemConfig.client(), () =>
HttpClient.request<ISystemConfig>({

View File

@ -1,4 +1,4 @@
import { IAuth, IDocument, IUser, IWiki, IWikiUser, WikiApiDefinition } from '@think/domains';
import { IAuth, IDocument, IUser, IWiki, WikiApiDefinition } from '@think/domains';
import { event, REFRESH_TOCS } from 'event';
import { useCallback, useEffect, useState } from 'react';
import { useQuery } from 'react-query';
@ -97,7 +97,7 @@ export const useOwnWikis = (organizationId) => {
);
/**
*
*
* @param id
* @returns
*/
@ -318,12 +318,18 @@ export const useWikiDocuments = (wikiId) => {
*/
export const getWikiMembers = (
wikiId,
page,
pageSize,
cookie = null
): Promise<{ data: Array<{ auth: IAuth; user: IUser }>; total: number }> => {
return HttpClient.request({
method: WikiApiDefinition.getMemberById.method,
url: WikiApiDefinition.getMemberById.client(wikiId),
cookie,
params: {
page,
pageSize,
},
});
};
@ -333,8 +339,10 @@ export const getWikiMembers = (
* @returns
*/
export const useWikiMembers = (wikiId) => {
const { data, error, isLoading, refetch } = useQuery(WikiApiDefinition.getMemberById.client(wikiId), () =>
getWikiMembers(wikiId)
const [pageSize] = useState(12);
const [page, setPage] = useState(1);
const { data, error, isLoading, refetch } = useQuery([WikiApiDefinition.getMemberById.client(wikiId), page], () =>
getWikiMembers(wikiId, page, pageSize)
);
const addUser = useCallback(
@ -376,7 +384,7 @@ export const useWikiMembers = (wikiId) => {
[refetch, wikiId]
);
return { data, loading: isLoading, error, addUser, updateUser, deleteUser };
return { data, loading: isLoading, error, page, pageSize, setPage, addUser, updateUser, deleteUser };
};
/**

View File

@ -1,11 +1,12 @@
import { Layout as SemiLayout, Nav, Space } from '@douyinfe/semi-ui';
import { Button, Layout as SemiLayout, Nav, Space } from '@douyinfe/semi-ui';
import { Message } from 'components/message';
import { OrganizationPublicSwitcher } from 'components/organization/public-switcher';
import { Theme } from 'components/theme';
import { User } from 'components/user';
import { useUser } from 'data/user';
import { IsOnMobile } from 'hooks/use-on-mobile';
import Router, { useRouter } from 'next/router';
import React from 'react';
import React, { useCallback } from 'react';
const { Header: SemiHeader } = SemiLayout;
@ -28,12 +29,26 @@ const menus = [
});
},
},
{
itemKey: '/template',
text: '模板',
onClick: () => {
Router.push({
pathname: `/template`,
});
},
},
];
export const RouterHeader: React.FC = () => {
const { user } = useUser();
const { pathname } = useRouter();
const { isMobile } = IsOnMobile.useHook();
const gotoApp = useCallback(() => {
Router.push(`/app`);
}, []);
return (
<SemiHeader>
{isMobile ? (
@ -47,6 +62,11 @@ export const RouterHeader: React.FC = () => {
}
footer={
<Space>
{user && (
<Button theme="solid" onClick={gotoApp}>
</Button>
)}
<Theme />
<User />
</Space>
@ -65,7 +85,12 @@ export const RouterHeader: React.FC = () => {
items={menus}
footer={
<Space>
<Message />
{user && (
<Button theme="solid" onClick={gotoApp}>
</Button>
)}
{user && <Message />}
<Theme />
<User />
</Space>

View File

@ -1,4 +1,4 @@
import { Typography } from '@douyinfe/semi-ui';
import { Banner, Typography } from '@douyinfe/semi-ui';
import { SystemConfig } from 'components/admin/system-config';
import { Seo } from 'components/seo';
import { useUser } from 'data/user';
@ -37,6 +37,7 @@ const Page: NextPage = () => {
</Title>
</div>
<Banner type="info" description="该部分是全局的系统管理后台,用于系统配置管理等操作!" />
<SystemConfig tab={tab} onNavigate={navigate} />
</>
) : (

View File

@ -1,32 +1,103 @@
import { Spin } from '@douyinfe/semi-ui';
import { usePeronalOrganization } from 'data/organization';
import { Avatar, Button, Table, Typography } from '@douyinfe/semi-ui';
import { IOrganization } from '@think/domains';
import { DataRender } from 'components/data-render';
import { LocaleTime } from 'components/locale-time';
import { usePeronalOrganization, useUserOrganizations } from 'data/organization';
import { SingleColumnLayout } from 'layouts/single-column';
import Link from 'next/link';
import Router from 'next/router';
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
const { Title, Paragraph } = Typography;
const { Column } = Table;
const Page = () => {
const { data: organization } = usePeronalOrganization();
const {
data: userOrganizations,
loading: userOrganizationsLoading,
error: userOrganizationsError,
} = useUserOrganizations();
const gotoCreate = useCallback(() => {
Router.push({
pathname: '/app/org/create',
});
}, []);
useEffect(() => {
if (userOrganizations && userOrganizations.length) return;
if (!organization) return;
Router.replace({
pathname: `/app/org/[organizationId]`,
query: { organizationId: organization.id },
});
}, [organization]);
}, [organization, userOrganizations]);
return (
<SingleColumnLayout>
<div className="container">
<div
style={{
padding: '10vh',
textAlign: 'center',
<div style={{ marginBottom: 24, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Title heading={3} style={{ margin: '8px 0' }}>
</Title>
<Button theme="solid" onClick={gotoCreate}>
</Button>
</div>
<DataRender
loading={userOrganizationsLoading}
error={userOrganizationsError}
normalContent={() => (
<>
<Table style={{ margin: '16px 0' }} dataSource={userOrganizations} size="small" pagination={false}>
<Column
title="名称"
dataIndex="name"
key="name"
width={200}
render={(_, org: IOrganization) => {
return (
<Link
href={{
pathname: `/app/org/[organizationId]`,
query: {
organizationId: org.id,
},
}}
>
<Spin></Spin>
</div>
<a style={{ color: 'inherit', textDecoration: 'none' }}>
<span style={{ display: 'flex', alignItems: 'center' }}>
<Avatar size="small" src={org.logo} style={{ marginRight: 8 }} />
<Paragraph
style={{
maxWidth: 100,
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
}}
strong
>
{org.name}
</Paragraph>
</span>
</a>
</Link>
);
}}
/>
<Column
width={120}
title="创建时间"
dataIndex="createdAt"
key="createdAt"
render={(date) => <LocaleTime date={date} />}
/>
</Table>
</>
)}
/>
</div>
</SingleColumnLayout>
);

View File

@ -1,12 +1,3 @@
.wikiItemWrap {
padding: 12px 16px !important;
margin: 8px 2px;
cursor: pointer;
background-color: var(--semi-color-bg-2);
border: 1px solid var(--semi-color-border) !important;
}
.titleWrap {
.wrap {
display: flex;
justify-content: space-between;
}

View File

@ -1,35 +1,51 @@
import { Button, Typography } from '@douyinfe/semi-ui';
import { Seo } from 'components/seo';
import { toLogin, useUser } from 'data/user';
import { SingleColumnLayout } from 'layouts/single-column';
import type { NextPage } from 'next';
import Router from 'next/router';
import React, { useCallback } from 'react';
const { Title } = Typography;
import styles from './index.module.scss';
const { Title, Paragraph } = Typography;
const Page: NextPage = () => {
const gotoApp = useCallback(() => {
const { user } = useUser();
const start = useCallback(() => {
if (user) {
Router.push(`/app`);
} else {
toLogin();
}
}, [user]);
const toGithub = useCallback(() => {
window.open('https://github.com/fantasticit/think');
}, []);
return (
<SingleColumnLayout>
<Seo title="主页" />
<div className="container">
<div style={{ marginBottom: 24 }}>
<Title heading={3} style={{ margin: '8px 0' }}>
</Title>
<div className={styles.wrap}>
<div>
<div style={{ textAlign: 'center' }}>
<Title style={{ margin: 24 }}></Title>
<Paragraph type="tertiary">
线
</Paragraph>
</div>
<div
style={{
padding: '10vh',
textAlign: 'center',
}}
>
<Button theme="solid" onClick={gotoApp}>
<div style={{ margin: '48px 0', textAlign: 'center' }}>
<Button theme="solid" onClick={start}>
使
</Button>
<Button style={{ marginLeft: 12 }} onClick={toGithub}>
Github
</Button>
</div>
</div>
</div>
</div>
</SingleColumnLayout>

View File

@ -2,7 +2,7 @@ import { Button, Col, Form, Layout, Modal, Row, Space, Toast, Typography } from
import { Author } from 'components/author';
import { LogoImage, LogoText } from 'components/logo';
import { Seo } from 'components/seo';
import { useRegister, useVerifyCode } from 'data/user';
import { useRegister, useSystemPublicConfig, useVerifyCode } from 'data/user';
import { isEmail } from 'helpers/validator';
import { useInterval } from 'hooks/use-interval';
import { useRouterQuery } from 'hooks/use-router-query';
@ -24,6 +24,7 @@ const Page = () => {
const [hasSendVerifyCode, toggleHasSendVerifyCode] = useToggle(false);
const [countDown, setCountDown] = useState(0);
const { register, loading } = useRegister();
const { data: systemConfig } = useSystemPublicConfig();
const { sendVerifyCode, loading: sendVerifyCodeLoading } = useVerifyCode();
const onFormChange = useCallback((formState) => {
@ -133,6 +134,7 @@ const Page = () => {
]}
/>
{systemConfig && systemConfig.enableEmailVerify ? (
<Row gutter={8} style={{ paddingTop: 12 }}>
<Col span={16}>
<Form.Input
@ -144,11 +146,17 @@ const Page = () => {
/>
</Col>
<Col span={8}>
<Button disabled={!email || countDown > 0} loading={sendVerifyCodeLoading} onClick={getVerifyCode} block>
<Button
disabled={!email || countDown > 0}
loading={sendVerifyCodeLoading}
onClick={getVerifyCode}
block
>
{hasSendVerifyCode ? countDown : '获取验证码'}
</Button>
</Col>
</Row>
) : null}
<Button htmlType="submit" type="primary" theme="solid" block loading={loading} style={{ margin: '16px 0' }}>

View File

@ -10,7 +10,7 @@ interface AxiosInstance extends Axios {
export const HttpClient = axios.create({
baseURL: process.env.SERVER_API_URL,
timeout: 1 * 3 * 1000,
timeout: process.env.NODE_ENV === 'production' ? 10 * 60 * 1000 : 3000,
withCredentials: true,
}) as AxiosInstance;

View File

@ -7,3 +7,4 @@ export * from './template';
export * from './comment';
export * from './star';
export * from './organization';
export * from './system';

View File

@ -19,3 +19,4 @@ __exportStar(require("./template"), exports);
__exportStar(require("./comment"), exports);
__exportStar(require("./star"), exports);
__exportStar(require("./organization"), exports);
__exportStar(require("./system"), exports);

View File

@ -40,6 +40,14 @@ export declare const OrganizationApiDefinition: {
server: "/update/:id";
client: (id: IOrganization['id']) => string;
};
/**
*
*/
deleteOrganization: {
method: "delete";
server: "/delete/:id";
client: (id: IOrganization['id']) => string;
};
/**
*
*/

View File

@ -42,6 +42,14 @@ exports.OrganizationApiDefinition = {
server: '/update/:id',
client: function (id) { return "/organization/update/".concat(id); }
},
/**
*
*/
deleteOrganization: {
method: 'delete',
server: '/delete/:id',
client: function (id) { return "/organization/delete/".concat(id); }
},
/**
*
*/

7
packages/domains/lib/api/system.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
export declare const SystemApiDefinition: {
getPublicConfig: {
method: "get";
server: "/";
client: () => string;
};
};

View File

@ -0,0 +1,10 @@
"use strict";
exports.__esModule = true;
exports.SystemApiDefinition = void 0;
exports.SystemApiDefinition = {
getPublicConfig: {
method: 'get',
server: '/',
client: function () { return '/system'; }
}
};

View File

@ -1,33 +1,2 @@
"use strict";
exports.__esModule = true;
// /**
// * 创建组织数据定义
// */
// export interface CreateOrganizationDto {
// name: string;
// description: string;
// logo: string;
// }
// export enum OrganizationAuthEnum {
// superAdmin = 'superAdmin',
// admin = 'admin',
// member = 'member',
// noAccess = 'noAccess',
// }
// export const AuthEnumTextMap = {
// [OrganizationAuthEnum.superAdmin]: '超级管理员',
// [OrganizationAuthEnum.admin]: '管理员',
// [OrganizationAuthEnum.member]: '成员',
// [OrganizationAuthEnum.noAccess]: '无权限',
// };
// export const OrganizationAuthEnumArray = Object.keys(AuthEnumTextMap).map((value) => ({
// label: AuthEnumTextMap[value],
// value,
// }));
// export interface IOrganizationAuth {
// id: string;
// auth: OrganizationAuthEnum;
// organizationId: IOrganization['id'];
// createdAt: string;
// updatedAt: string;
// }

View File

@ -1,5 +1,6 @@
export interface ISystemConfig {
isSystemLocked: boolean;
enableEmailVerify: boolean;
emailServiceHost: string;
emailServicePassword: string;
emailServicePort: string;

View File

@ -1,11 +1,3 @@
/**
*
*/
export declare enum UserRole {
normal = "normal",
admin = "admin",
superadmin = "superadmin"
}
/**
*
*/
@ -22,7 +14,6 @@ export interface IUser {
password?: string;
avatar?: string;
email?: string;
role: UserRole;
status: UserStatus;
isSystemAdmin?: boolean;
}

View File

@ -1,15 +1,6 @@
"use strict";
exports.__esModule = true;
exports.UserStatus = exports.UserRole = void 0;
/**
*
*/
var UserRole;
(function (UserRole) {
UserRole["normal"] = "normal";
UserRole["admin"] = "admin";
UserRole["superadmin"] = "superadmin";
})(UserRole = exports.UserRole || (exports.UserRole = {}));
exports.UserStatus = void 0;
/**
*
*/

View File

@ -8,21 +8,6 @@ export declare enum WikiStatus {
private = "private",
public = "public"
}
/**
*
*/
export declare enum WikiUserStatus {
applying = "applying",
inviting = "inviting",
normal = "normal"
}
/**
*
*/
export declare enum WikiUserRole {
normal = "normal",
admin = "admin"
}
/**
*
*/
@ -39,11 +24,3 @@ export interface IWiki {
createdAt: Date;
updatedAt: Date;
}
/**
*
*/
export interface IWikiUser extends IUser {
userRole: WikiUserRole;
userStatus: WikiUserStatus;
isCreator: boolean;
}

View File

@ -1,6 +1,6 @@
"use strict";
exports.__esModule = true;
exports.WikiUserRole = exports.WikiUserStatus = exports.WikiStatus = void 0;
exports.WikiStatus = void 0;
/**
*
*/
@ -9,20 +9,3 @@ var WikiStatus;
WikiStatus["private"] = "private";
WikiStatus["public"] = "public";
})(WikiStatus = exports.WikiStatus || (exports.WikiStatus = {}));
/**
*
*/
var WikiUserStatus;
(function (WikiUserStatus) {
WikiUserStatus["applying"] = "applying";
WikiUserStatus["inviting"] = "inviting";
WikiUserStatus["normal"] = "normal";
})(WikiUserStatus = exports.WikiUserStatus || (exports.WikiUserStatus = {}));
/**
*
*/
var WikiUserRole;
(function (WikiUserRole) {
WikiUserRole["normal"] = "normal";
WikiUserRole["admin"] = "admin";
})(WikiUserRole = exports.WikiUserRole || (exports.WikiUserRole = {}));

View File

@ -1,4 +1,4 @@
import { WikiStatus, WikiUserRole, DocumentStatus, IWiki, IDocument } from './models';
import { WikiStatus, DocumentStatus, IWiki, IDocument } from './models';
/**
*
*/
@ -32,12 +32,6 @@ export declare const getWikiStatusText: (wiki: IWiki) => string;
* @returns
*/
export declare const isPublicWiki: (currentStatus: IWiki['status']) => boolean;
/**
*
* @param role role
* @returns
*/
export declare const getWikiUserRoleText: (role: WikiUserRole) => string;
/**
*
* @param currentStatus document status

View File

@ -1,6 +1,6 @@
"use strict";
exports.__esModule = true;
exports.isPublicDocument = exports.getWikiUserRoleText = exports.isPublicWiki = exports.getWikiStatusText = exports.DOCUMENT_STATUS = exports.WIKI_USER_ROLES = exports.WIKI_STATUS_LIST = void 0;
exports.isPublicDocument = exports.isPublicWiki = exports.getWikiStatusText = exports.DOCUMENT_STATUS = exports.WIKI_USER_ROLES = exports.WIKI_STATUS_LIST = void 0;
var models_1 = require("./models");
/**
*
@ -57,15 +57,6 @@ exports.getWikiStatusText = getWikiStatusText;
*/
var isPublicWiki = function (currentStatus) { return currentStatus === models_1.WikiStatus.public; };
exports.isPublicWiki = isPublicWiki;
/**
*
* @param role role
* @returns
*/
var getWikiUserRoleText = function (role) {
return exports.WIKI_USER_ROLES.find(function (d) { return d.value === role; }).label;
};
exports.getWikiUserRoleText = getWikiUserRoleText;
/**
*
* @param currentStatus document status

View File

@ -7,3 +7,4 @@ export * from './template';
export * from './comment';
export * from './star';
export * from './organization';
export * from './system';

View File

@ -46,6 +46,15 @@ export const OrganizationApiDefinition = {
client: (id: IOrganization['id']) => `/organization/update/${id}`,
},
/**
*
*/
deleteOrganization: {
method: 'delete' as const,
server: '/delete/:id' as const,
client: (id: IOrganization['id']) => `/organization/delete/${id}`,
},
/**
*
*/

View File

@ -0,0 +1,7 @@
export const SystemApiDefinition = {
getPublicConfig: {
method: 'get' as const,
server: '/' as const,
client: () => '/system',
},
};

View File

@ -1,5 +1,3 @@
import { IUser } from '../models';
export const UserApiDefinition = {
/**
*

View File

@ -11,39 +11,3 @@ export interface IOrganization {
createUserId: IUser['id'];
isPersonal: boolean;
}
// /**
// * 创建组织数据定义
// */
// export interface CreateOrganizationDto {
// name: string;
// description: string;
// logo: string;
// }
// export enum OrganizationAuthEnum {
// superAdmin = 'superAdmin',
// admin = 'admin',
// member = 'member',
// noAccess = 'noAccess',
// }
// export const AuthEnumTextMap = {
// [OrganizationAuthEnum.superAdmin]: '超级管理员',
// [OrganizationAuthEnum.admin]: '管理员',
// [OrganizationAuthEnum.member]: '成员',
// [OrganizationAuthEnum.noAccess]: '无权限',
// };
// export const OrganizationAuthEnumArray = Object.keys(AuthEnumTextMap).map((value) => ({
// label: AuthEnumTextMap[value],
// value,
// }));
// export interface IOrganizationAuth {
// id: string;
// auth: OrganizationAuthEnum;
// organizationId: IOrganization['id'];
// createdAt: string;
// updatedAt: string;
// }

View File

@ -1,5 +1,6 @@
export interface ISystemConfig {
isSystemLocked: boolean;
enableEmailVerify: boolean;
emailServiceHost: string;
emailServicePassword: string;
emailServicePort: string;

View File

@ -1,12 +1,3 @@
/**
*
*/
export enum UserRole {
normal = 'normal',
admin = 'admin',
superadmin = 'superadmin',
}
/**
*
*/
@ -24,7 +15,6 @@ export interface IUser {
password?: string;
avatar?: string;
email?: string;
role: UserRole;
status: UserStatus;
isSystemAdmin?: boolean;
}

View File

@ -10,23 +10,6 @@ export enum WikiStatus {
public = 'public',
}
/**
*
*/
export enum WikiUserStatus {
applying = 'applying',
inviting = 'inviting',
normal = 'normal',
}
/**
*
*/
export enum WikiUserRole {
normal = 'normal',
admin = 'admin',
}
/**
*
*/
@ -43,12 +26,3 @@ export interface IWiki {
createdAt: Date;
updatedAt: Date;
}
/**
*
*/
export interface IWikiUser extends IUser {
userRole: WikiUserRole;
userStatus: WikiUserStatus;
isCreator: boolean;
}

View File

@ -1,4 +1,4 @@
import { WikiStatus, WikiUserRole, DocumentStatus, IWiki, IDocument } from './models';
import { WikiStatus, DocumentStatus, IWiki, IDocument } from './models';
/**
*
@ -48,7 +48,7 @@ export const DOCUMENT_STATUS = [
* @returns
*/
export const getWikiStatusText = (wiki: IWiki): string => {
return WIKI_STATUS_LIST.find((t) => t.value === wiki.status).label;
return WIKI_STATUS_LIST.find((t) => t.value === wiki.status)!.label;
};
/**
@ -58,15 +58,6 @@ export const getWikiStatusText = (wiki: IWiki): string => {
*/
export const isPublicWiki = (currentStatus: IWiki['status']) => currentStatus === WikiStatus.public;
/**
*
* @param role role
* @returns
*/
export const getWikiUserRoleText = (role: WikiUserRole) => {
return WIKI_USER_ROLES.find((d) => d.value === role).label;
};
/**
*
* @param currentStatus document status

View File

@ -1,18 +1,13 @@
import { AuthEntity } from '@entities/auth.entity';
import { CommentEntity } from '@entities/comment.entity';
import { DocumentEntity } from '@entities/document.entity';
// import { DocumentUserEntity } from '@entities/document-user.entity';
import { MessageEntity } from '@entities/message.entity';
import { OrganizationEntity } from '@entities/organization.entity';
// import { OrganizationUserEntity } from '@entities/organization-user.entity';
import { StarEntity } from '@entities/star.entity';
import { SystemEntity } from '@entities/system.entity';
import { TemplateEntity } from '@entities/template.entity';
import { UserEntity } from '@entities/user.entity';
import { VerifyEntity } from '@entities/verify.entity';
import { ViewEntity } from '@entities/view.entity';
import { WikiEntity } from '@entities/wiki.entity';
// import { WikiUserEntity } from '@entities/wiki-user.entity';
import { IS_PRODUCTION } from '@helpers/env.helper';
import { getLogFileName, ONE_DAY } from '@helpers/log.helper';
import { AuthModule } from '@modules/auth.module';
@ -42,17 +37,12 @@ const ENTITIES = [
UserEntity,
AuthEntity,
OrganizationEntity,
// OrganizationUserEntity,
WikiEntity,
// WikiUserEntity,
DocumentEntity,
// DocumentUserEntity,
StarEntity,
CommentEntity,
MessageEntity,
TemplateEntity,
ViewEntity,
VerifyEntity,
SystemEntity,
];

View File

@ -1,4 +1,5 @@
export enum RedisDBEnum {
documentVersion = 0,
view = 1,
verify = 2,
}

View File

@ -1,9 +1,7 @@
import { OperateUserAuthDto } from '@dtos/auth.dto';
import { CreateDocumentDto } from '@dtos/create-document.dto';
import { DocAuthDto } from '@dtos/doc-auth.dto';
import { ShareDocumentDto } from '@dtos/share-document.dto';
import { UpdateDocumentDto } from '@dtos/update-document.dto';
// import { CheckDocumentAuthority, DocumentAuthorityGuard } from '@guard/document-auth.guard';
import { CheckDocumentStatus, DocumentStatusGuard } from '@guard/document-status.guard';
import { JwtGuard } from '@guard/jwt.guard';
import {
@ -23,10 +21,9 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { DocumentService } from '@services/document.service';
import { DocumentApiDefinition, DocumentStatus } from '@think/domains';
import { DocumentApiDefinition, DocumentStatus, IPagination } from '@think/domains';
@Controller('document')
// @UseGuards(DocumentAuthorityGuard)
@UseGuards(DocumentStatusGuard)
export class DocumentController {
constructor(private readonly documentService: DocumentService) {}
@ -78,7 +75,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(DocumentApiDefinition.getDetailById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('readable')
@UseGuards(JwtGuard)
async getDocumentDetail(@Request() req, @Param('id') documentId) {
return await this.documentService.getDocumentDetail(req.user, documentId);
@ -94,7 +90,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Patch(DocumentApiDefinition.updateById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('editable')
@UseGuards(JwtGuard)
async updateDocument(@Request() req, @Param('id') documentId, @Body() dto: UpdateDocumentDto) {
return await this.documentService.updateDocument(req.user, documentId, dto);
@ -109,7 +104,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(DocumentApiDefinition.getVersionById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('readable')
@UseGuards(JwtGuard)
async getDocumentVersion(@Request() req, @Param('id') documentId) {
return await this.documentService.getDocumentVersion(req.user, documentId);
@ -124,10 +118,9 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(DocumentApiDefinition.getMemberById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('readable')
@UseGuards(JwtGuard)
async getDocUsers(@Request() req, @Param('id') documentId) {
return await this.documentService.getDocUsers(req.user, documentId);
async getDocUsers(@Request() req, @Param('id') documentId, @Query() pagination: IPagination) {
return await this.documentService.getDocUsers(req.user, documentId, pagination);
}
/**
@ -140,7 +133,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Post(DocumentApiDefinition.addMemberById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('createUser')
@UseGuards(JwtGuard)
async addDocUser(@Request() req, @Param('id') documentId, @Body() dto: OperateUserAuthDto) {
return await this.documentService.addDocUser(req.user, documentId, dto);
@ -156,7 +148,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Patch(DocumentApiDefinition.updateMemberById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('createUser')
@UseGuards(JwtGuard)
async updateDocUser(@Request() req, @Param('id') documentId, @Body() dto: OperateUserAuthDto) {
return await this.documentService.updateDocUser(req.user, documentId, dto);
@ -172,7 +163,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Post(DocumentApiDefinition.deleteMemberById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('createUser')
@UseGuards(JwtGuard)
async deleteDocUser(@Request() req, @Param('id') documentId, @Body() dto: OperateUserAuthDto) {
return await this.documentService.deleteDocUser(req.user, documentId, dto);
@ -187,7 +177,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Post(DocumentApiDefinition.getChildren.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('readable')
@UseGuards(JwtGuard)
async getChildrenDocuments(@Request() req, @Body() data) {
return await this.documentService.getChildrenDocuments(req.user, data);
@ -202,7 +191,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Delete(DocumentApiDefinition.deleteById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('createUser')
@UseGuards(JwtGuard)
async deleteDocument(@Request() req, @Param('id') documentId) {
return await this.documentService.deleteDocument(req.user, documentId);
@ -218,7 +206,6 @@ export class DocumentController {
@UseInterceptors(ClassSerializerInterceptor)
@Post(DocumentApiDefinition.shareById.server)
@HttpCode(HttpStatus.OK)
// @CheckDocumentAuthority('editable')
@UseGuards(JwtGuard)
async shareDocument(@Request() req, @Param('id') documentId, @Body() dto: ShareDocumentDto) {
return await this.documentService.shareDocument(req.user, documentId, dto);

View File

@ -1,6 +1,5 @@
import { OperateUserAuthDto } from '@dtos/auth.dto';
import { CreateOrganizationDto } from '@dtos/organization.dto';
// import { OrganizationUserDto } from '@dtos/organization-user.dto';
import { JwtGuard } from '@guard/jwt.guard';
import {
Body,
@ -13,12 +12,13 @@ import {
Param,
Patch,
Post,
Query,
Request,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { OrganizationService } from '@services/organization.service';
import { OrganizationApiDefinition } from '@think/domains';
import { IPagination, OrganizationApiDefinition } from '@think/domains';
@Controller('organization')
export class OrganizationController {
@ -91,6 +91,20 @@ export class OrganizationController {
return await this.organizationService.getOrganizationDetail(req.user, id);
}
/**
*
* @param req
* @param id
* @returns
*/
@UseInterceptors(ClassSerializerInterceptor)
@Delete(OrganizationApiDefinition.deleteOrganization.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async deleteWiki(@Request() req, @Param('id') id) {
return await this.organizationService.deleteOrganization(req.user, id);
}
/**
*
* @param req
@ -100,8 +114,8 @@ export class OrganizationController {
@Get(OrganizationApiDefinition.getMembers.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async getMembers(@Request() req, @Param('id') id) {
return await this.organizationService.getMembers(req.user, id);
async getMembers(@Request() req, @Param('id') id, @Query() pagination: IPagination) {
return await this.organizationService.getMembers(req.user, id, pagination);
}
/**

View File

@ -1,7 +1,15 @@
import { Controller } from '@nestjs/common';
import { ClassSerializerInterceptor, Controller, Get, HttpCode, HttpStatus, UseInterceptors } from '@nestjs/common';
import { SystemService } from '@services/system.service';
import { SystemApiDefinition } from '@think/domains';
@Controller('system')
export class SystemController {
constructor(private readonly systemService: SystemService) {}
@UseInterceptors(ClassSerializerInterceptor)
@Get(SystemApiDefinition.getPublicConfig.server)
@HttpCode(HttpStatus.CREATED)
async getPublicConfig() {
return await this.systemService.getPublicConfig();
}
}

View File

@ -4,7 +4,6 @@ import { ShareWikiDto } from '@dtos/share-wiki.dto';
import { UpdateWikiDto } from '@dtos/update-wiki.dto';
import { JwtGuard } from '@guard/jwt.guard';
import { CheckWikiStatus, WikiStatusGuard } from '@guard/wiki-status.guard';
// import { CheckWikiUserRole, WikiUserRoleGuard } from '@guard/wiki-user.guard';
import {
Body,
ClassSerializerInterceptor,
@ -22,7 +21,7 @@ import {
UseInterceptors,
} from '@nestjs/common';
import { WikiService } from '@services/wiki.service';
import { IPagination, WikiApiDefinition, WikiStatus, WikiUserRole } from '@think/domains';
import { IPagination, WikiApiDefinition, WikiStatus } from '@think/domains';
@Controller('wiki')
export class WikiController {
@ -93,8 +92,6 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(WikiApiDefinition.getHomeDocumentById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole()
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async getWikiHomeDocument(@Request() req, @Param('id') wikiId) {
return await this.wikiService.getWikiHomeDocument(req.user, wikiId);
@ -109,8 +106,6 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(WikiApiDefinition.getTocsById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole()
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async getWikiTocs(@Request() req, @Param('id') wikiId) {
return await this.wikiService.getWikiTocs(req.user, wikiId);
@ -126,29 +121,11 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Patch(WikiApiDefinition.updateTocsById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole()
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async orderWikiTocs(@Body() relations) {
return await this.wikiService.orderWikiTocs(relations);
}
// /**
// * 获取知识库所有文档
// * @param req
// * @param wikiId
// * @returns
// */
// @UseInterceptors(ClassSerializerInterceptor)
// @Get(WikiApiDefinition.getDocumentsById.server)
// @HttpCode(HttpStatus.OK)
// @CheckWikiUserRole()
// @UseGuards(WikiUserRoleGuard)
// @UseGuards(JwtGuard)
// async getWikiDocs(@Request() req, @Param('id') wikiId) {
// return await this.wikiService.getWikiDocs(req.user, wikiId);
// }
/**
*
* @param req
@ -158,8 +135,6 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(WikiApiDefinition.getDetailById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole()
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async getWikiDetail(@Request() req, @Param('id') wikiId) {
return await this.wikiService.getWikiDetail(req.user, wikiId);
@ -167,7 +142,6 @@ export class WikiController {
/**
*
*
* @param req
* @param wikiId
* @param dto
@ -176,8 +150,6 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Patch(WikiApiDefinition.updateById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole(WikiUserRole.admin)
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async updateWiki(@Request() req, @Param('id') wikiId, @Body() dto: UpdateWikiDto) {
return await this.wikiService.updateWiki(req.user, wikiId, dto);
@ -185,7 +157,6 @@ export class WikiController {
/**
*
*
* @param req
* @param wikiId
* @returns
@ -193,8 +164,6 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Delete(WikiApiDefinition.deleteById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole(WikiUserRole.admin)
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async deleteWiki(@Request() req, @Param('id') wikiId) {
return await this.wikiService.deleteWiki(req.user, wikiId);
@ -202,7 +171,6 @@ export class WikiController {
/**
*
*
* @param req
* @param wikiId
* @returns
@ -210,16 +178,13 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Get(WikiApiDefinition.getMemberById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole(WikiUserRole.admin)
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async getWikiUsers(@Request() req, @Param('id') wikiId) {
return await this.wikiService.getWikiUsers(req.user, wikiId);
async getWikiUsers(@Request() req, @Param('id') wikiId, @Query() pagination: IPagination) {
return await this.wikiService.getWikiUsers(req.user, wikiId, pagination);
}
/**
*
*
* @param req
* @param wikiId
* @param dto
@ -228,8 +193,6 @@ export class WikiController {
@UseInterceptors(ClassSerializerInterceptor)
@Post(WikiApiDefinition.addMemberById.server)
@HttpCode(HttpStatus.OK)
// @CheckWikiUserRole(WikiUserRole.admin)
// @UseGuards(WikiUserRoleGuard)
@UseGuards(JwtGuard)
async addWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: OperateUserAuthDto) {
return await this.wikiService.addWikiUser(req.user, wikiId, dto);
@ -237,7 +200,6 @@ export class WikiController {
/**
*
*
* @param req
* @param wikiId
* @param dto
@ -253,7 +215,6 @@ export class WikiController {
/**
*
*
* @param req
* @param wikiId
* @param dto
@ -269,7 +230,6 @@ export class WikiController {
/**
*
*
* @param req
* @param wikiId
* @param dto

View File

@ -1,4 +1,4 @@
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { IsEmail, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator';
/**
*
@ -22,8 +22,8 @@ export class RegisterUserDto {
@MinLength(5, { message: '邮箱验证码至少5个字符' })
@IsString({ message: '邮箱验证码错误正确类型为String' })
@IsNotEmpty({ message: '邮箱验证码不能为空' })
verifyCode: string;
@IsOptional({ message: '邮箱验证码不能为空' })
verifyCode?: string;
}
/**

View File

@ -1,15 +0,0 @@
import { IsBoolean, IsString } from 'class-validator';
export class DocAuthDto {
@IsString()
readonly documentId: string;
@IsString()
readonly userName: string;
@IsBoolean()
readonly readable: boolean;
@IsBoolean()
readonly editable: boolean;
}

View File

@ -1,4 +1,4 @@
import { IsEmail, IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
export class LoginUserDto {
@IsString({ message: '用户名称类型错误正确类型为String' })

View File

@ -1,12 +0,0 @@
// import { OrganizationAuthEnum } from '@think/domains';
// import { IsNotEmpty, IsString } from 'class-validator';
// export class OrganizationAuthDto {
// @IsString({ message: '权限类型类型错误正确类型为String' })
// @IsNotEmpty({ message: '权限类型不能为空' })
// auth: OrganizationAuthEnum;
// @IsString({ message: '组织 Id 类型错误正确类型为String' })
// @IsNotEmpty({ message: '组织 Id 不能为空' })
// organizationId: string;
// }

View File

@ -1,10 +0,0 @@
// import { OrganizationAuthEnum } from '@think/domains';
// import { IsString } from 'class-validator';
// export class OrganizationUserDto {
// @IsString()
// readonly userName: string;
// @IsString()
// readonly userAuth: OrganizationAuthEnum;
// }

View File

@ -1,10 +0,0 @@
import { WikiUserRole } from '@think/domains';
import { IsString } from 'class-validator';
export class WikiUserDto {
@IsString()
readonly userName: string;
@IsString()
readonly userRole: WikiUserRole;
}

View File

@ -8,10 +8,15 @@ export class SystemEntity {
/**
*
*/
@Column({ type: 'boolean', default: false, comment: '是否锁定系统' })
isSystemLocked: boolean;
/**
*
*/
@Column({ type: 'boolean', default: false, comment: '是否启用邮箱校验' })
enableEmailVerify: boolean;
/**
*
*/

View File

@ -1,4 +1,4 @@
import { UserRole, UserStatus } from '@think/domains';
import { UserStatus } from '@think/domains';
import * as bcrypt from 'bcryptjs';
import { Exclude } from 'class-transformer';
import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@ -37,14 +37,6 @@ export class UserEntity {
@Column({ type: 'boolean', default: false, comment: '是否为系统管理员' })
isSystemAdmin: boolean;
@Column({
type: 'enum',
enum: UserRole,
default: UserRole.normal,
comment: '用户角色',
})
public role: UserRole;
@Column({
type: 'enum',
enum: UserStatus,

View File

@ -1,27 +0,0 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@Entity('verify')
export class VerifyEntity {
@PrimaryGeneratedColumn('uuid')
public id: string;
@Column({ type: 'varchar', comment: '邮箱地址' })
public email: string;
@Column({ type: 'varchar', comment: '验证码' })
public verifyCode: string;
@CreateDateColumn({
type: 'timestamp',
name: 'createdAt',
comment: '创建时间',
})
createdAt: Date;
@UpdateDateColumn({
type: 'timestamp',
name: 'updatedAt',
comment: '更新时间',
})
updatedAt: Date;
}

View File

@ -1,34 +0,0 @@
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@Entity('view')
export class ViewEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'varchar', comment: '文档 Id' })
documentId: string;
// public 表示从公开渠道访问
@Column({ type: 'varchar', comment: '访问用户 Id', default: 'public' })
userId: string;
@Column({ type: 'mediumtext', default: null, charset: 'utf8mb4' })
originUserAgent: string;
@Column({ type: 'mediumtext', default: null, charset: 'utf8mb4' })
parsedUserAgent: string;
@CreateDateColumn({
type: 'timestamp',
name: 'createdAt',
comment: '创建时间',
})
createdAt: Date;
@UpdateDateColumn({
type: 'timestamp',
name: 'updatedAt',
comment: '更新时间',
})
updatedAt: Date;
}

View File

@ -1,5 +1,6 @@
import { CommentController } from '@controllers/comment.controller';
import { CommentEntity } from '@entities/comment.entity';
import { AuthModule } from '@modules/auth.module';
import { DocumentModule } from '@modules/document.module';
import { MessageModule } from '@modules/message.module';
import { UserModule } from '@modules/user.module';
@ -11,6 +12,7 @@ import { CommentService } from '@services/comment.service';
@Module({
imports: [
TypeOrmModule.forFeature([CommentEntity]),
forwardRef(() => AuthModule),
forwardRef(() => UserModule),
forwardRef(() => WikiModule),
forwardRef(() => DocumentModule),

View File

@ -3,6 +3,7 @@ import { OrganizationEntity } from '@entities/organization.entity';
import { AuthModule } from '@modules/auth.module';
import { MessageModule } from '@modules/message.module';
import { UserModule } from '@modules/user.module';
import { WikiModule } from '@modules/wiki.module';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { OrganizationService } from '@services/organization.service';
@ -13,6 +14,7 @@ import { OrganizationService } from '@services/organization.service';
forwardRef(() => UserModule),
forwardRef(() => MessageModule),
forwardRef(() => AuthModule),
forwardRef(() => WikiModule),
],
providers: [OrganizationService],
exports: [OrganizationService],

View File

@ -1,12 +1,10 @@
import { VerifyController } from '@controllers/verify.controller';
import { VerifyEntity } from '@entities/verify.entity';
import { SystemModule } from '@modules/system.module';
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { VerifyService } from '@services/verify.service';
@Module({
imports: [TypeOrmModule.forFeature([VerifyEntity]), forwardRef(() => SystemModule)],
imports: [forwardRef(() => SystemModule)],
providers: [VerifyService],
exports: [VerifyService],
controllers: [VerifyController],

View File

@ -1,11 +1,8 @@
import { ViewController } from '@controllers/view.controller';
import { ViewEntity } from '@entities/view.entity';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ViewService } from '@services/view.service';
@Module({
imports: [TypeOrmModule.forFeature([ViewEntity])],
providers: [ViewService],
exports: [ViewService],
controllers: [ViewController],

View File

@ -3,7 +3,7 @@ import { AuthEntity } from '@entities/auth.entity';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { UserService } from '@services/user.service';
import { AuthEnum, IDocument, IOrganization, IUser, IWiki } from '@think/domains';
import { AuthEnum, IDocument, IOrganization, IPagination, IUser, IWiki } from '@think/domains';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@ -41,8 +41,6 @@ export class AuthService {
const wrappedAuth = { userId, ...auth };
const oldAuth = await this.authRepo.findOne(wrappedAuth);
// TODO: 这里可以判断权限继承
let newAuth: AuthEntity;
if (oldAuth) {
@ -80,6 +78,48 @@ export class AuthService {
}
}
/**
*
* canDelete
* @param organizationId
*/
async deleteOrganization(organizationId: IOrganization['id']) {
const res = await this.authRepo.find({
organizationId,
});
await this.authRepo.remove(res);
}
/**
*
* canDelete
* @param organizationId
* @param wikiId
*/
async deleteWiki(organizationId: IOrganization['id'], wikiId: IWiki['id']) {
const res = await this.authRepo.find({
organizationId,
wikiId,
});
await this.authRepo.remove(res);
}
/**
*
* canDelete
* @param organizationId
* @param wikiId
* @param documentId
*/
async deleteDocument(organizationId: IOrganization['id'], wikiId: IWiki['id'], documentId: IDocument['id']) {
const res = await this.authRepo.find({
organizationId,
wikiId,
documentId,
});
await this.authRepo.remove(res);
}
/**
*
* @param userId
@ -97,7 +137,7 @@ export class AuthService {
const userAuth = await this.authRepo.findOne(conditions);
if (!userAuth || userAuth.auth === AuthEnum.noAccess) {
throw new HttpException('您没有权限查看', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限', HttpStatus.FORBIDDEN);
}
return userAuth;
@ -120,7 +160,7 @@ export class AuthService {
const userAuth = await this.authRepo.findOne(conditions);
if (!userAuth || ![AuthEnum.creator, AuthEnum.admin].includes(userAuth.auth)) {
throw new HttpException('您没有权限编辑', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限', HttpStatus.FORBIDDEN);
}
return userAuth;
@ -143,7 +183,7 @@ export class AuthService {
const userAuth = await this.authRepo.findOne(conditions);
if (!userAuth || ![AuthEnum.creator].includes(userAuth.auth)) {
throw new HttpException('您没有权限删除', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限', HttpStatus.FORBIDDEN);
}
return userAuth;
@ -156,6 +196,12 @@ export class AuthService {
* @param dto
*/
private async operateOtherUserAuth(currentUserId: IUser['id'], targetUserId: IUser['id'], dto: AuthDto) {
const targetUser = await this.userService.findOne({ id: targetUserId });
if (!targetUser) {
throw new HttpException('用户不存在', HttpStatus.NOT_FOUND);
}
const conditions: Partial<AuthEntity> = {
organizationId: dto.organizationId,
wikiId: dto.wikiId || null,
@ -168,17 +214,17 @@ export class AuthService {
});
if (!currentUserAuth) {
throw new HttpException('您没有权限操作1', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限操作', HttpStatus.FORBIDDEN);
}
// 仅创建者、管理员可操作他人权限
if (![AuthEnum.creator, AuthEnum.admin].includes(currentUserAuth.auth)) {
throw new HttpException('您没有权限操作2', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限操作', HttpStatus.FORBIDDEN);
}
// 仅创建者可赋予他人创建者、管理员权限
if ([AuthEnum.creator, AuthEnum.admin].includes(dto.auth) && currentUserAuth.auth !== AuthEnum.creator) {
throw new HttpException('您没有权限操作3', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限操作', HttpStatus.FORBIDDEN);
}
const maybeTargetUserAuth = await this.authRepo.findOne({
@ -189,12 +235,12 @@ export class AuthService {
if (maybeTargetUserAuth) {
// 对方是创建者,无权操作
if (maybeTargetUserAuth.auth === AuthEnum.creator) {
throw new HttpException('您没有权限操作4', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限操作', HttpStatus.FORBIDDEN);
}
// 对方是管理员,仅创建者可操作
if (maybeTargetUserAuth.auth === AuthEnum.admin && currentUserAuth.auth !== AuthEnum.creator) {
throw new HttpException('您没有权限操作5', HttpStatus.FORBIDDEN);
throw new HttpException('您没有权限操作', HttpStatus.FORBIDDEN);
}
}
}
@ -300,10 +346,12 @@ export class AuthService {
/**
*
* @param userId
* @param organizationId
* @param pagination
* @returns
*/
async getUsersAuthInOrganization(organizationId: IOrganization['id']) {
const [data, total] = await this.authRepo
async getUsersAuthInOrganization(organizationId: IOrganization['id'], pagination: IPagination | null) {
const query = await this.authRepo
.createQueryBuilder('auth')
.where('auth.auth IN (:...types)', {
types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess],
@ -311,18 +359,28 @@ export class AuthService {
.andWhere('auth.organizationId=:organizationId')
.andWhere('auth.wikiId is NULL')
.andWhere('auth.documentId is NULL')
.setParameter('organizationId', organizationId)
.getManyAndCount();
.setParameter('organizationId', organizationId);
if (pagination) {
const { page = 1, pageSize = 12 } = pagination;
query.skip((+page - 1) * +pageSize);
query.take(+pageSize);
}
const [data, total] = await query.getManyAndCount();
return { data: data || [], total };
}
/**
*
* @param userId
* @param organizationId
* @param wikiId
* @param pagination
* @returns
*/
async getUsersAuthInWiki(organizationId: IOrganization['id'], wikiId: IWiki['id']) {
const [data, total] = await this.authRepo
async getUsersAuthInWiki(organizationId: IOrganization['id'], wikiId: IWiki['id'], pagination: IPagination | null) {
const query = await this.authRepo
.createQueryBuilder('auth')
.where('auth.auth IN (:...types)', {
types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess],
@ -331,8 +389,14 @@ export class AuthService {
.andWhere('auth.wikiId=:wikiId')
.andWhere('auth.documentId is NULL')
.setParameter('organizationId', organizationId)
.setParameter('wikiId', wikiId)
.getManyAndCount();
.setParameter('wikiId', wikiId);
if (pagination) {
const { page = 1, pageSize = 12 } = pagination;
query.skip((+page - 1) * +pageSize);
query.take(+pageSize);
}
const [data, total] = await query.getManyAndCount();
return { data: data || [], total };
}
@ -413,11 +477,20 @@ export class AuthService {
}
/**
*
* @param userId
*
* @param organizationId
* @param wikiId
* @param documentId
* @param pagination
* @returns
*/
async getUsersAuthInDocument(organizationId: IOrganization['id'], wikiId: IWiki['id'], documentId: IDocument['id']) {
const [data, total] = await this.authRepo
async getUsersAuthInDocument(
organizationId: IOrganization['id'],
wikiId: IWiki['id'],
documentId: IDocument['id'],
pagination: IPagination | null
) {
const query = await this.authRepo
.createQueryBuilder('auth')
.where('auth.auth IN (:...types)', {
types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess],
@ -427,8 +500,14 @@ export class AuthService {
.andWhere('auth.documentId=:documentId')
.setParameter('organizationId', organizationId)
.setParameter('wikiId', wikiId)
.setParameter('documentId', documentId)
.getManyAndCount();
.setParameter('documentId', documentId);
if (pagination) {
const { page = 1, pageSize = 12 } = pagination;
query.skip((+page - 1) * +pageSize);
query.take(+pageSize);
}
const [data, total] = await query.getManyAndCount();
return { data: data || [], total };
}

View File

@ -5,8 +5,8 @@ import { ConfigService } from '@nestjs/config';
import { DocumentService } from '@services/document.service';
import { DocumentVersionService } from '@services/document-version.service';
import { TemplateService } from '@services/template.service';
import { OutUser, UserService } from '@services/user.service';
import { DocumentStatus } from '@think/domains';
import { UserService } from '@services/user.service';
import { DocumentStatus, IUser } from '@think/domains';
import * as lodash from 'lodash';
import * as Y from 'yjs';
@ -101,14 +101,16 @@ export class CollaborationService {
}
return { user: { name: '匿名用户' } };
} else {
// TODO权限校验
// const authority = await this.documentService.getDocumentAuthority(targetId, user.id);
// if (!authority.readable) {
// throw new HttpException('您无权查看此文档', HttpStatus.FORBIDDEN);
// }
// if (!authority.editable) {
// connection.readOnly = true;
// }
const authority = await this.documentService.getDocumentUserAuth(user.id, targetId);
if (!authority.readable) {
throw new HttpException('您无权查看此文档', HttpStatus.FORBIDDEN);
}
if (!authority.editable) {
connection.readOnly = true;
}
return {
user,
};
@ -121,9 +123,11 @@ export class CollaborationService {
}
const template = await this.templateService.findById(targetId);
if (template.createUserId !== user.id) {
throw new HttpException('您无权查看此模板', HttpStatus.FORBIDDEN);
}
return {
user,
};
@ -149,6 +153,9 @@ export class CollaborationService {
switch (docType) {
case 'document': {
const res = await this.documentService.findById(targetId);
if (!res) {
throw new Error('文档不存在');
}
state = res.state;
break;
}
@ -179,7 +186,7 @@ export class CollaborationService {
const docType = requestParameters.get('docType');
const userId = requestParameters.get('userId');
const updateDocument = async (user: OutUser, documentId: string, data) => {
const updateDocument = async (user: IUser, documentId: string, data) => {
await this.documentService.updateDocument(user, documentId, data);
this.debounce(
`onStoreDocumentVersion-${documentId}`,
@ -218,7 +225,7 @@ export class CollaborationService {
const node = TiptapTransformer.fromYdoc(data.document);
const title = lodash.get(node, `default.content[0].content[0].text`, '').replace(/\s/g, '').slice(0, 255);
const state = Buffer.from(Y.encodeStateAsUpdate(data.document));
await updateHandler({ id: userId } as OutUser, targetId, {
await updateHandler({ id: userId } as IUser, targetId, {
title,
content: JSON.stringify(node),
state,
@ -234,7 +241,7 @@ export class CollaborationService {
if (docType === 'document') {
const data = await this.documentService.findById(targetId);
if (data && !data.title) {
await this.documentService.updateDocument({ id: userId } as OutUser, targetId, {
await this.documentService.updateDocument({ id: userId } as IUser, targetId, {
title: '未命名文档',
});
}
@ -244,7 +251,7 @@ export class CollaborationService {
if (docType === 'template') {
const data = await this.templateService.findById(targetId);
if (data && !data.title) {
await this.templateService.updateTemplate({ id: userId } as OutUser, targetId, {
await this.templateService.updateTemplate({ id: userId } as IUser, targetId, {
title: '未命名模板',
});
}

View File

@ -3,10 +3,11 @@ import { CommentEntity } from '@entities/comment.entity';
import { parseUserAgent } from '@helpers/ua.helper';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthService } from '@services/auth.service';
import { DocumentService } from '@services/document.service';
import { MessageService } from '@services/message.service';
import { OutUser, UserService } from '@services/user.service';
import { buildMessageURL, DocumentStatus } from '@think/domains';
import { UserService } from '@services/user.service';
import { AuthEnum, buildMessageURL, DocumentStatus, IUser } from '@think/domains';
import { Repository } from 'typeorm';
@Injectable()
@ -14,9 +15,17 @@ export class CommentService {
constructor(
@InjectRepository(CommentEntity)
private readonly commentRepo: Repository<CommentEntity>,
@Inject(forwardRef(() => AuthService))
private readonly authService: AuthService,
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
@Inject(forwardRef(() => MessageService))
private readonly messageService: MessageService,
private readonly userService: UserService,
@Inject(forwardRef(() => DocumentService))
private readonly documentService: DocumentService
) {}
@ -44,20 +53,20 @@ export class CommentService {
* @param dto
* @returns
*/
async create(user: OutUser, userAgent: string, dto: CommentDto) {
async create(user: IUser, userAgent: string, dto: CommentDto) {
const { documentId, html, replyUserId } = dto;
const doc = await this.documentService.findById(documentId);
if (doc.status !== DocumentStatus.public) {
// TODO权限校验
// const docAuth = await this.documentService.getDocumentAuthority(documentId, user.id);
// if (!docAuth) {
// throw new HttpException('文档不存在', HttpStatus.NOT_FOUND);
// }
// if (!docAuth.readable) {
// throw new HttpException('权限不足,无法评论', HttpStatus.FORBIDDEN);
// }
const authority = await this.documentService.getDocumentUserAuth(user.id, documentId);
if (!authority) {
throw new HttpException('文档不存在', HttpStatus.NOT_FOUND);
}
if (!authority.readable) {
throw new HttpException('权限不足,无法评论', HttpStatus.FORBIDDEN);
}
}
const { text: uaText } = parseUserAgent(userAgent);
@ -74,21 +83,23 @@ export class CommentService {
const res = await this.commentRepo.create(comment);
const ret = await this.commentRepo.save(res);
// const wikiUsersAuth = await this.documentService.getDocUsersWithoutAuthCheck(user, documentId);
const { data: users } = await this.authService.getUsersAuthInDocument(doc.organizationId, doc.wikiId, doc.id, null);
// await Promise.all(
// wikiUsersAuth.map(async (userAuth) => {
// await this.messageService.notify(userAuth.user, {
// title: `文档「${doc.title}」收到新评论`,
// message: `文档「${doc.title}」收到新评论,快去看看!`,
// url: buildMessageURL('toDocument')({
// organizationId: doc.organizationId,
// wikiId: doc.wikiId,
// documentId: doc.id,
// }),
// });
// })
// );
await Promise.all(
users
.filter((user) => user.auth !== AuthEnum.noAccess)
.map((user) => {
this.messageService.notify(user.userId, {
title: `文档「${doc.title}」收到新评论`,
message: `文档「${doc.title}」收到新评论,快去看看!`,
url: buildMessageURL('toDocument')({
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
}),
});
})
);
return ret;
}
@ -180,21 +191,24 @@ export class CommentService {
const newData = await this.commentRepo.merge(old, { html: dto.html });
const doc = await this.documentService.findById(old.documentId);
// const wikiUsersAuth = await this.documentService.getDocUsersWithoutAuthCheck(user, old.documentId);
// await Promise.all(
// wikiUsersAuth.map(async (userAuth) => {
// await this.messageService.notify(userAuth.user, {
// title: `文档「${doc.title}」评论更新`,
// message: `文档「${doc.title}」的评论已更新,快去看看!`,
// url: buildMessageURL('toDocument')({
// organizationId: doc.organizationId,
// wikiId: doc.wikiId,
// documentId: doc.id,
// }),
// });
// })
// );
const { data: users } = await this.authService.getUsersAuthInDocument(doc.organizationId, doc.wikiId, doc.id, null);
await Promise.all(
users
.filter((user) => user.auth !== AuthEnum.noAccess)
.map((user) => {
this.messageService.notify(user.userId, {
title: `文档「${doc.title}」收到新评论`,
message: `文档「${doc.title}」收到新评论,快去看看!`,
url: buildMessageURL('toDocument')({
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
}),
});
})
);
return this.commentRepo.save(newData);
}
@ -205,20 +219,25 @@ export class CommentService {
throw new HttpException('您不是评论创建者,无法删除', HttpStatus.FORBIDDEN);
}
const doc = await this.documentService.findById(data.documentId);
// const wikiUsersAuth = await this.documentService.getDocUsersWithoutAuthCheck(user, data.documentId);
// await Promise.all(
// wikiUsersAuth.map(async (userAuth) => {
// await this.messageService.notify(userAuth.user, {
// title: `文档「${doc.title}」的评论已被删除`,
// message: `文档「${doc.title}」的评论已被删除,快去看看`,
// url: buildMessageURL('toDocument')({
// organizationId: doc.organizationId,
// wikiId: doc.wikiId,
// documentId: doc.id,
// }),
// });
// })
// );
const { data: users } = await this.authService.getUsersAuthInDocument(doc.organizationId, doc.wikiId, doc.id, null);
await Promise.all(
users
.filter((user) => user.auth !== AuthEnum.noAccess)
.map((user) => {
this.messageService.notify(user.userId, {
title: `文档「${doc.title}」收到新评论`,
message: `文档「${doc.title}」收到新评论,快去看看!`,
url: buildMessageURL('toDocument')({
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
}),
});
})
);
return this.commentRepo.remove(data);
}
}

View File

@ -1,6 +1,5 @@
import { OperateUserAuthDto } from '@dtos/auth.dto';
import { CreateDocumentDto } from '@dtos/create-document.dto';
// import { DocAuthDto } from '@dtos/doc-auth.dto';
import { ShareDocumentDto } from '@dtos/share-document.dto';
import { UpdateDocumentDto } from '@dtos/update-document.dto';
import { DocumentEntity } from '@entities/document.entity';
@ -12,11 +11,11 @@ import { CollaborationService } from '@services/collaboration.service';
import { DocumentVersionService } from '@services/document-version.service';
import { MessageService } from '@services/message.service';
import { TemplateService } from '@services/template.service';
import { OutUser, UserService } from '@services/user.service';
import { UserService } from '@services/user.service';
import { ViewService } from '@services/view.service';
import { WikiService } from '@services/wiki.service';
import { EMPTY_DOCUMNENT } from '@think/constants';
import { AuthEnum, buildMessageURL, DocumentStatus, WikiUserRole } from '@think/domains';
import { AuthEnum, buildMessageURL, DocumentStatus, IUser } from '@think/domains';
import { instanceToPlain } from 'class-transformer';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@ -27,9 +26,6 @@ export class DocumentService {
private documentVersionService: DocumentVersionService;
constructor(
// @InjectRepository(DocumentUserEntity)
// public readonly documentUserRepo: Repository<DocumentUserEntity>,
@InjectRepository(DocumentEntity)
public readonly documentRepo: Repository<DocumentEntity>,
@ -70,7 +66,7 @@ export class DocumentService {
* @param dto
* @returns
*/
public async findById(id: string) {
public async findById(id: string): Promise<Partial<DocumentEntity>> {
const document = await this.documentRepo.findOne(id);
return instanceToPlain(document);
}
@ -81,7 +77,7 @@ export class DocumentService {
* @param dto
* @returns
*/
public async findByIds(ids: string[]) {
public async findByIds(ids: string[]): Promise<Array<Partial<DocumentEntity>>> {
const documents = await this.documentRepo.findByIds(ids);
return documents.map((doc) => instanceToPlain(doc));
}
@ -95,90 +91,6 @@ export class DocumentService {
return await this.documentRepo.findOne({ wikiId, isWikiHome: true });
}
// /**
// * 获取用户在指定文档的权限
// * @param documentId
// * @param userId
// * @returns
// */
// public async getDocumentAuthority(documentId: string, userId: string) {
// const authority = await this.documentUserRepo.findOne({
// documentId,
// userId,
// });
// return authority;
// }
/**
*
* @param param0
* @returns
// */
// async operateDocumentAuth({ currentUserId, documentId, targetUserId, readable = false, editable = false }) {
// const doc = await this.documentRepo.findOne({ id: documentId });
// if (!doc) {
// throw new HttpException('文档不存在', HttpStatus.BAD_REQUEST);
// }
// const isCurrentUserCreator = currentUserId === doc.createUserId;
// const isTargetUserCreator = targetUserId === doc.createUserId;
// if (!isCurrentUserCreator) {
// throw new HttpException('您不是文档创建者,无权操作', HttpStatus.FORBIDDEN);
// }
// const targetUser = await this.userService.findOne(targetUserId);
// const targetDocAuth = await this.documentUserRepo.findOne({
// documentId,
// userId: targetUserId,
// });
// if (!targetDocAuth) {
// const documentUser = {
// documentId,
// createUserId: doc.createUserId,
// wikiId: doc.wikiId,
// userId: targetUserId,
// readable: isTargetUserCreator ? true : editable ? true : readable,
// editable: isTargetUserCreator ? true : editable,
// };
// const res = await this.documentUserRepo.create(documentUser);
// const ret = await this.documentUserRepo.save(res);
// await this.messageService.notify(targetUser, {
// title: `您已被添加到文档「${doc.title}」`,
// message: `您已被添加到文档「${doc.title}」,快去看看!`,
// url: buildMessageURL('toDocument')({
// organizationId: doc.organizationId,
// wikiId: doc.wikiId,
// documentId: doc.id,
// }),
// });
// return ret;
// } else {
// const newData = {
// ...targetDocAuth,
// readable: isTargetUserCreator ? true : editable ? true : readable,
// editable: isTargetUserCreator ? true : editable,
// };
// const res = await this.documentUserRepo.merge(targetDocAuth, newData);
// const ret = await this.documentUserRepo.save(res);
// await this.messageService.notify(targetUser, {
// title: `您在文档「${doc.title}」的权限已变更`,
// message: `您在文档「${doc.title}」的权限已变更,快去看看!`,
// url: buildMessageURL('toDocument')({
// organizationId: doc.organizationId,
// wikiId: doc.wikiId,
// documentId: doc.id,
// }),
// });
// return ret;
// }
// }
/**
*
* @param user
@ -186,7 +98,7 @@ export class DocumentService {
* @param dto
* @returns
*/
async addDocUser(user: OutUser, documentId, dto: OperateUserAuthDto) {
async addDocUser(user: IUser, documentId, dto: OperateUserAuthDto) {
const targetUser = await this.userService.findOne({ name: dto.userName });
if (!targetUser) {
@ -199,12 +111,32 @@ export class DocumentService {
throw new HttpException('目标文档不存在', HttpStatus.NOT_FOUND);
}
if (
!(await this.authService.getAuth(targetUser.id, {
organizationId: doc.organizationId,
wikiId: null,
documentId: null,
}))
) {
throw new HttpException('该用户非组织成员', HttpStatus.FORBIDDEN);
}
await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
});
await this.messageService.notify(targetUser.id, {
title: `您被添加到文档「${doc.title}`,
message: `您被添加到文档「${doc.title}」,快去看看吧!`,
url: buildMessageURL('toWiki')({
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
}),
});
}
/**
@ -214,7 +146,7 @@ export class DocumentService {
* @param dto
* @returns
*/
async updateDocUser(user: OutUser, documentId, dto: OperateUserAuthDto) {
async updateDocUser(user: IUser, documentId, dto: OperateUserAuthDto) {
const targetUser = await this.userService.findOne({ name: dto.userName });
if (!targetUser) {
@ -233,6 +165,16 @@ export class DocumentService {
wikiId: doc.wikiId,
documentId: doc.id,
});
await this.messageService.notify(targetUser.id, {
title: `文档「${doc.title}」权限更新`,
message: `您在文档「${doc.title}」的权限已变更,快去看看吧!`,
url: buildMessageURL('toWiki')({
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
}),
});
}
/**
@ -242,7 +184,7 @@ export class DocumentService {
* @param dto
* @returns
*/
async deleteDocUser(user: OutUser, documentId, dto: OperateUserAuthDto) {
async deleteDocUser(user: IUser, documentId, dto: OperateUserAuthDto) {
const targetUser = await this.userService.findOne({ name: dto.userName });
if (!targetUser) {
@ -261,6 +203,16 @@ export class DocumentService {
wikiId: doc.wikiId,
documentId: doc.id,
});
await this.messageService.notify(targetUser.id, {
title: `文档「${doc.title}」权限已收回`,
message: `您在文档「${doc.title}」的权限已收回!`,
url: buildMessageURL('toWiki')({
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
}),
});
}
/**
@ -268,7 +220,7 @@ export class DocumentService {
* @param userId
* @param wikiId
*/
async getDocUsers(user: OutUser, documentId) {
async getDocUsers(user: IUser, documentId, pagination) {
const doc = await this.documentRepo.findOne({ id: documentId });
if (!doc) {
@ -284,7 +236,8 @@ export class DocumentService {
const { data: auths, total } = await this.authService.getUsersAuthInDocument(
doc.organizationId,
doc.wikiId,
doc.id
doc.id,
pagination
);
const res = await Promise.all(
@ -297,29 +250,6 @@ export class DocumentService {
return { data: res, total };
}
// /**
// * 获取文档成员
// * 忽略权限检查
// * @param userId
// * @param wikiId
// */
// async getDocUsersWithoutAuthCheck(user: OutUser, documentId) {
// const doc = await this.documentRepo.findOne({ id: documentId });
// if (!doc) {
// throw new HttpException('文档不存在', HttpStatus.BAD_REQUEST);
// }
// const data = await this.documentUserRepo.find({ documentId });
// return await Promise.all(
// data.map(async (auth) => {
// const user = await this.userService.findById(auth.userId);
// return { auth, user };
// })
// );
// }
/**
*
* @param user
@ -327,7 +257,7 @@ export class DocumentService {
* @param isWikiHome
* @returns
*/
public async createDocument(user: OutUser, dto: CreateDocumentDto, isWikiHome = false) {
public async createDocument(user: IUser, dto: CreateDocumentDto, isWikiHome = false) {
await this.authService.canView(user.id, {
organizationId: dto.organizationId,
wikiId: dto.wikiId,
@ -375,7 +305,11 @@ export class DocumentService {
}
const document = await this.documentRepo.save(await this.documentRepo.create(data));
const { data: userAuths } = await this.authService.getUsersAuthInWiki(document.organizationId, document.wikiId);
const { data: userAuths } = await this.authService.getUsersAuthInWiki(
document.organizationId,
document.wikiId,
null
);
await Promise.all([
...userAuths
@ -388,7 +322,7 @@ export class DocumentService {
documentId: document.id,
});
}),
await this.authService.createOrUpdateAuth(user.id, {
this.authService.createOrUpdateAuth(user.id, {
auth: AuthEnum.creator,
organizationId: document.organizationId,
wikiId: document.wikiId,
@ -399,26 +333,27 @@ export class DocumentService {
return instanceToPlain(document);
}
// /**
// * 删除知识库下所有文档
// * @param user
// * @param wikiId
// */
// async deleteWikiDocuments(user, wikiId) {
// const docs = await this.documentRepo.find({ wikiId });
// await Promise.all(
// docs.map((doc) => {
// return this.deleteDocument(user, doc.id);
// })
// );
// }
/**
*
* @param user
* @param wikiId
*/
async deleteWikiDocuments(user, wikiId) {
const docs = await this.documentRepo.find({ wikiId });
await Promise.all(
docs.map((doc) => {
return this.deleteDocument(user, doc.id);
})
);
}
/**
*
* @param idd
*/
async deleteDocument(user: OutUser, documentId) {
async deleteDocument(user: IUser, documentId) {
const document = await this.documentRepo.findOne(documentId);
if (document.isWikiHome) {
const isWikiExist = await this.wikiService.findById(document.wikiId);
if (isWikiExist) {
@ -435,6 +370,7 @@ export class DocumentService {
const children = await this.documentRepo.find({
parentDocumentId: document.id,
});
if (children && children.length) {
const parentDocumentId = document.parentDocumentId;
await Promise.all(
@ -448,11 +384,10 @@ export class DocumentService {
);
}
// TODO权限删除
// const auths = await this.documentUserRepo.find({ documentId });
// await this.documentUserRepo.remove(auths);
return this.documentRepo.remove(document);
await Promise.all([
this.authService.deleteDocument(document.organizationId, document.wikiId, document.id),
this.documentRepo.remove(document),
]);
}
/**
@ -462,7 +397,7 @@ export class DocumentService {
* @param dto
* @returns
*/
public async updateDocument(user: OutUser, documentId: string, dto: UpdateDocumentDto) {
public async updateDocument(user: IUser, documentId: string, dto: UpdateDocumentDto) {
const document = await this.documentRepo.findOne(documentId);
await this.authService.canEdit(user.id, {
@ -511,13 +446,40 @@ export class DocumentService {
};
}
/**
*
* @param userId
* @param documentId
* @returns
*/
public async getDocumentUserAuth(userId, documentId) {
const document = await this.documentRepo.findOne(documentId);
const authority = await this.authService.getAuth(userId, {
organizationId: document.organizationId,
wikiId: document.wikiId,
documentId: document.id,
});
return {
...authority,
readable: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member].includes(authority.auth),
editable: [AuthEnum.creator, AuthEnum.admin].includes(authority.auth),
};
}
/**
*
* @param user
* @param documentId
* @returns
*/
public async getDocumentVersion(user: OutUser, documentId: string) {
public async getDocumentVersion(user: IUser, documentId: string) {
const document = await this.documentRepo.findOne(documentId);
await this.authService.canView(user.id, {
organizationId: document.organizationId,
wikiId: document.wikiId,
documentId: document.id,
});
const data = await this.documentVersionService.getDocumentVersions(documentId);
return data;
}
@ -526,7 +488,7 @@ export class DocumentService {
*
* @param id
*/
async shareDocument(user: OutUser, documentId, dto: ShareDocumentDto, nextStatus = null) {
async shareDocument(user: IUser, documentId, dto: ShareDocumentDto, nextStatus = null) {
const document = await this.documentRepo.findOne(documentId);
await this.authService.canEdit(user.id, {
organizationId: document.organizationId,
@ -570,7 +532,6 @@ export class DocumentService {
]);
// 异步创建
this.viewService.create(null, document);
return { ...doc, views, wiki, createUser };
}
@ -581,7 +542,7 @@ export class DocumentService {
* @returns
*/
public async getChildrenDocuments(
user: OutUser,
user: IUser,
data: {
wikiId: string;
documentId?: string;
@ -694,7 +655,7 @@ export class DocumentService {
* @param dto
* @returns
*/
public async getRecentDocuments(user: OutUser, organizationId) {
public async getRecentDocuments(user: IUser, organizationId) {
await this.authService.canView(user.id, {
organizationId: organizationId,
wikiId: null,
@ -744,24 +705,25 @@ export class DocumentService {
.createQueryBuilder('document')
.andWhere('document.organizationId = :organizationId')
.andWhere('document.title LIKE :keyword')
// FIXME: 编辑器内容的 json 字段可能也被匹配
.orWhere('document.content LIKE :keyword')
.setParameter('organizationId', organizationId)
.setParameter('keyword', `%${keyword}%`)
.getMany();
// const ret = await Promise.all(
// res.map(async (doc) => {
// const auth = await this.documentUserRepo.findOne({
// documentId: doc.id,
// userId: user.id,
// });
// return auth && auth.readable ? doc : null;
// })
// );
const ret = await Promise.all(
res.map(async (doc) => {
const auth = await this.authService.getAuth(user.Id, {
organizationId: doc.organizationId,
wikiId: doc.wikiId,
documentId: doc.id,
});
// const data = ret.filter(Boolean);
return auth && [AuthEnum.creator, AuthEnum.admin, AuthEnum.member].includes(auth.auth) ? doc : null;
})
);
return res;
const data = ret.filter(Boolean);
return data;
}
}

View File

@ -1,7 +1,7 @@
import { MessageEntity } from '@entities/message.entity';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { OutUser } from '@services/user.service';
import { IUser } from '@think/domains';
import { Repository } from 'typeorm';
@Injectable()
@ -17,8 +17,8 @@ export class MessageService {
* @param msg
* @returns
*/
async notify(user: OutUser, msg) {
const data = { userId: user.id, ...msg };
async notify(userId: IUser['id'], msg) {
const data = { userId, ...msg };
const res = await this.messageRepo.create(data);
const ret = await this.messageRepo.save(res);
return ret;

View File

@ -1,6 +1,5 @@
import { OperateUserAuthDto } from '@dtos/auth.dto';
import { CreateOrganizationDto } from '@dtos/organization.dto';
// import { OperateUserAuthDto } from '@dtos/organization-user.dto';
import { OrganizationEntity } from '@entities/organization.entity';
import { UserEntity } from '@entities/user.entity';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
@ -8,6 +7,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { AuthService } from '@services/auth.service';
import { MessageService } from '@services/message.service';
import { UserService } from '@services/user.service';
import { WikiService } from '@services/wiki.service';
import { AuthEnum, buildMessageURL, IOrganization, IUser } from '@think/domains';
import { Repository } from 'typeorm';
@ -24,7 +24,10 @@ export class OrganizationService {
private readonly userService: UserService,
@Inject(forwardRef(() => MessageService))
private readonly messageService: MessageService
private readonly messageService: MessageService,
@Inject(forwardRef(() => WikiService))
private readonly wikiService: WikiService
) {}
public async findById(id: string) {
@ -90,6 +93,27 @@ export class OrganizationService {
return await this.organizationRepo.save(await this.organizationRepo.merge(oldData, dto));
}
/**
*
* @param user
* @param organizationId
* @returns
*/
async deleteOrganization(user: IUser, organizationId) {
const organization = await this.organizationRepo.findOne(organizationId);
await this.authService.canDelete(user.id, {
organizationId: organization.id,
wikiId: null,
documentId: null,
});
await Promise.all([
this.authService.deleteOrganization(organization.id),
this.organizationRepo.remove(organization),
this.wikiService.deleteOrganizationWiki(user, organizationId),
]);
return organization;
}
/**
*
* @param user
@ -101,7 +125,7 @@ export class OrganizationService {
}
/**
* 访
* 访
* @param user
*/
public async getUserOrganizations(user: IUser) {
@ -130,7 +154,7 @@ export class OrganizationService {
* @param shortId
* @returns
*/
public async getMembers(user: IUser, id: IOrganization['id']) {
public async getMembers(user: IUser, id: IOrganization['id'], pagination) {
const organization = await this.organizationRepo.findOne({ id });
if (!organization) {
@ -143,7 +167,7 @@ export class OrganizationService {
documentId: null,
});
const { data: usersAuth, total } = await this.authService.getUsersAuthInOrganization(organization.id);
const { data: usersAuth, total } = await this.authService.getUsersAuthInOrganization(organization.id, pagination);
const userIds = usersAuth.map((auth) => auth.userId);
const users = await this.userService.findByIds(userIds);
@ -185,7 +209,7 @@ export class OrganizationService {
documentId: null,
});
await this.messageService.notify(targetUser, {
await this.messageService.notify(targetUser.id, {
title: `您被添加到组织「${organization.name}`,
message: `您被添加到知识库「${organization.name}」,快去看看吧!`,
url: buildMessageURL('toOrganization')({
@ -223,7 +247,7 @@ export class OrganizationService {
documentId: null,
});
await this.messageService.notify(targetUser, {
await this.messageService.notify(targetUser.id, {
title: `组织「${organization.name}」权限变更`,
message: `您在组织「${organization.name}」权限已变更,快去看看吧!`,
url: buildMessageURL('toOrganization')({
@ -261,9 +285,9 @@ export class OrganizationService {
documentId: null,
});
await this.messageService.notify(targetUser, {
await this.messageService.notify(targetUser.id, {
title: `组织「${organization.name}」权限收回`,
message: `您在组织「${organization.name}」权限已收回,快去看看吧`,
message: `您在组织「${organization.name}」权限已收回`,
url: buildMessageURL('toOrganization')({
organizationId: organization.id,
}),

View File

@ -5,9 +5,9 @@ import { InjectRepository } from '@nestjs/typeorm';
import { AuthService } from '@services/auth.service';
import { DocumentService } from '@services/document.service';
import { OrganizationService } from '@services/organization.service';
import { OutUser, UserService } from '@services/user.service';
import { UserService } from '@services/user.service';
import { WikiService } from '@services/wiki.service';
import { IDocument } from '@think/domains';
import { IDocument, IUser } from '@think/domains';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@ -39,7 +39,7 @@ export class StarService {
* @param dto
* @returns
*/
async toggleStar(user: OutUser, dto: StarDto) {
async toggleStar(user: IUser, dto: StarDto) {
const data = {
...dto,
userId: user.id,
@ -61,7 +61,7 @@ export class StarService {
* @param dto
* @returns
*/
async isStared(user: OutUser, dto: StarDto) {
async isStared(user: IUser, dto: StarDto) {
const res = await this.starRepo.findOne({ userId: user.id, ...dto });
return Boolean(res);
}
@ -71,7 +71,7 @@ export class StarService {
* @param user
* @returns
*/
async getStarWikisInOrganization(user: OutUser, organizationId) {
async getStarWikisInOrganization(user: IUser, organizationId) {
await this.authService.canView(user.id, {
organizationId: organizationId,
wikiId: null,
@ -100,7 +100,7 @@ export class StarService {
* @param user
* @returns
*/
async getStarDocumentsInWiki(user: OutUser, dto: StarDto) {
async getStarDocumentsInWiki(user: IUser, dto: StarDto) {
const records = await this.starRepo.find({
userId: user.id,
wikiId: dto.wikiId,
@ -114,7 +114,7 @@ export class StarService {
const createUser = await this.userService.findById(doc.createUserId);
return { createUser, ...doc };
})
)) as Array<IDocument & { createUser: OutUser }>;
)) as Array<IDocument & { createUser: IUser }>;
return withCreateUserRes
.map((document) => {
@ -134,7 +134,7 @@ export class StarService {
* @param user
* @returns
*/
async getStarDocumentsInOrganization(user: OutUser, organizationId) {
async getStarDocumentsInOrganization(user: IUser, organizationId) {
await this.authService.canView(user.id, {
organizationId: organizationId,
wikiId: null,

View File

@ -40,21 +40,18 @@ export class SystemService {
*/
private async loadFromConfigFile() {
const currentConfig = await this.getConfigFromDatabase();
const emailConfigKeys = ['emailServiceHost', 'emailServicePort', 'emailServiceUser', 'emailServicePassword'];
if (currentConfig && emailConfigKeys.every((configKey) => Boolean(currentConfig[configKey]))) {
return;
}
// 同步邮件服务配置
const emailConfigFromConfigFile = await this.confifgService.get('server.email');
let emailConfig = {};
if (emailConfigFromConfigFile && typeof emailConfigFromConfigFile === 'object') {
emailConfig = {
emailServiceHost: emailConfigFromConfigFile.host,
emailServicePort: emailConfigFromConfigFile.port,
emailServiceUser: emailConfigFromConfigFile.user,
emailServicePassword: emailConfigFromConfigFile.password,
emailServiceHost: currentConfig ? currentConfig.emailServiceHost : emailConfigFromConfigFile.host,
emailServicePort: currentConfig ? currentConfig.emailServicePort : emailConfigFromConfigFile.port,
emailServiceUser: currentConfig ? currentConfig.emailServiceUser : emailConfigFromConfigFile.user,
emailServicePassword: currentConfig ? currentConfig.emailServicePassword : emailConfigFromConfigFile.password,
};
}
@ -118,4 +115,9 @@ export class SystemService {
);
});
}
async getPublicConfig() {
const config = await this.getConfigFromDatabase();
return { isSystemLocked: config.isSystemLocked, enableEmailVerify: config.enableEmailVerify };
}
}

View File

@ -2,7 +2,8 @@ import { TemplateDto } from '@dtos/template.dto';
import { TemplateEntity } from '@entities/template.entity';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { OutUser, UserService } from '@services/user.service';
import { UserService } from '@services/user.service';
import { IUser } from '@think/domains';
import { instanceToPlain } from 'class-transformer';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@ -45,7 +46,7 @@ export class TemplateService {
* @param dto
* @returns
*/
async create(user: OutUser, dto: TemplateDto) {
async create(user: IUser, dto: TemplateDto) {
const data = {
createUserId: user.id,
...dto,
@ -103,7 +104,7 @@ export class TemplateService {
* @param templateId
* @returns
*/
async useTemplate(user: OutUser, templateId) {
async useTemplate(user: IUser, templateId) {
const data = await this.templateRepo.findOne(templateId);
if (user.id !== data.createUserId && !data.isPublic) {
throw new HttpException('您不是模板创建者,无法编辑', HttpStatus.FORBIDDEN);
@ -146,7 +147,7 @@ export class TemplateService {
* @param queryParams
* @returns
*/
async getOwnTemplates(user: OutUser, queryParams) {
async getOwnTemplates(user: IUser, queryParams) {
const query = this.templateRepo
.createQueryBuilder('template')
.where('template.createUserId=:createUserId')

View File

@ -18,8 +18,6 @@ import { IUser, UserStatus } from '@think/domains';
import { instanceToPlain } from 'class-transformer';
import { Repository } from 'typeorm';
export type OutUser = Omit<UserEntity, 'comparePassword' | 'encryptPassword' | 'encrypt' | 'password'>;
@Injectable()
export class UserService {
constructor(
@ -95,9 +93,9 @@ export class UserService {
* @param id
* @returns
*/
async findById(id): Promise<OutUser> {
async findById(id): Promise<IUser> {
const user = await this.userRepo.findOne(id);
return instanceToPlain(user) as OutUser;
return instanceToPlain(user) as IUser;
}
/**
@ -105,7 +103,7 @@ export class UserService {
* @param opts
* @returns
*/
async findOne(opts: Partial<OutUser>): Promise<UserEntity> {
async findOne(opts: Partial<UserEntity>): Promise<UserEntity> {
const user = await this.userRepo.findOne(opts);
return user;
}
@ -115,9 +113,9 @@ export class UserService {
* @param id
* @returns
*/
async findByIds(ids): Promise<OutUser[]> {
async findByIds(ids): Promise<IUser[]> {
const users = await this.userRepo.findByIds(ids);
return users.map((user) => instanceToPlain(user)) as OutUser[];
return users.map((user) => instanceToPlain(user)) as IUser[];
}
/**
@ -125,7 +123,7 @@ export class UserService {
* @param user CreateUserDto
* @returns
*/
async createUser(user: RegisterUserDto): Promise<OutUser> {
async createUser(user: RegisterUserDto): Promise<IUser> {
const currentSystemConfig = await this.systemService.getConfigFromDatabase();
if (currentSystemConfig.isSystemLocked) {
@ -144,7 +142,10 @@ export class UserService {
throw new HttpException('该邮箱已被注册', HttpStatus.BAD_REQUEST);
}
if (!(await this.verifyService.checkVerifyCode(user.email, user.verifyCode))) {
if (
currentSystemConfig.enableEmailVerify &&
!(await this.verifyService.checkVerifyCode(user.email, user.verifyCode))
) {
throw new HttpException('验证码不正确,请检查', HttpStatus.BAD_REQUEST);
}
@ -158,20 +159,7 @@ export class UserService {
isPersonal: true,
});
// const wiki = await this.wikiService.createWiki(createdUser, {
// name: createdUser.name,
// description: `${createdUser.name}的个人空间`,
// });
// await this.starService.toggleStar(createdUser, {
// wikiId: wiki.id,
// });
// await this.messageService.notify(createdUser, {
// title: `欢迎「${createdUser.name}」`,
// message: `系统已自动为您创建知识库,快去看看吧!`,
// url: `/wiki/${wiki.id}`,
// });
return instanceToPlain(createdUser) as OutUser;
return instanceToPlain(createdUser) as IUser;
}
/**
@ -213,7 +201,7 @@ export class UserService {
* @param user
* @returns
*/
async login(user: LoginUserDto): Promise<{ user: OutUser; token: string; domain: string; expiresIn: number }> {
async login(user: LoginUserDto): Promise<{ user: IUser; token: string; domain: string; expiresIn: number }> {
const currentSystemConfig = await this.systemService.getConfigFromDatabase();
const { name, password } = user;
@ -237,7 +225,7 @@ export class UserService {
throw new HttpException('用户已锁定,无法登录', HttpStatus.BAD_REQUEST);
}
const res = instanceToPlain(existUser) as OutUser;
const res = instanceToPlain(existUser) as IUser;
const token = this.jwtService.sign(res);
const domain = this.confifgService.get('client.siteDomain');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -261,11 +249,11 @@ export class UserService {
* @param dto
* @returns
*/
async updateUser(user: UserEntity, dto: UpdateUserDto): Promise<OutUser> {
async updateUser(user: UserEntity, dto: UpdateUserDto): Promise<IUser> {
const oldData = await this.userRepo.findOne(user.id);
const res = await this.userRepo.merge(oldData, dto);
const ret = await this.userRepo.save(res);
return instanceToPlain(ret) as OutUser;
return instanceToPlain(ret) as IUser;
}
async decodeToken(token) {

View File

@ -1,54 +1,49 @@
import { VerifyEntity } from '@entities/verify.entity';
import { RedisDBEnum } from '@constants/*';
import { buildRedis } from '@helpers/redis.helper';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { SystemService } from '@services/system.service';
import Redis from 'ioredis';
import { randomInt } from 'node:crypto';
import { Repository } from 'typeorm';
import { isEmail } from 'validator';
@Injectable()
export class VerifyService {
constructor(
@InjectRepository(VerifyEntity)
private readonly verifyRepo: Repository<VerifyEntity>,
private redis: Redis;
constructor(
@Inject(forwardRef(() => SystemService))
private readonly systemService: SystemService
) {}
) {
this.buildRedis();
}
/**
*
* @param record
*/
private async deleteVerifyCode(id) {
const record = await this.verifyRepo.findOne(id);
await this.verifyRepo.remove(record);
private async buildRedis() {
try {
this.redis = await buildRedis(RedisDBEnum.verify);
console.log('[think] 验证码服务启动成功');
} catch (e) {
console.error(`[think] 验证码服务启动错误: "${e.message}"`);
}
}
/**
*
* @param email
*/
public async sendVerifyCode(email: string) {
public sendVerifyCode = async (email: string) => {
if (!email || !isEmail(email)) {
throw new HttpException('请检查邮箱地址', HttpStatus.BAD_REQUEST);
}
const verifyCode = randomInt(1000000).toString().padStart(6, '0');
const record = await this.verifyRepo.save(await this.verifyRepo.create({ email, verifyCode }));
console.log(verifyCode);
await this.redis.set(`verify-${email}`, verifyCode, 'EX', 10);
await this.systemService.sendEmail({
to: email,
subject: '验证码',
html: `<p>您的验证码为 ${verifyCode}</p>`,
});
const timer = setTimeout(() => {
this.deleteVerifyCode(record.id);
clearTimeout(timer);
}, 5 * 60 * 1000);
}
};
/**
*
@ -56,17 +51,21 @@ export class VerifyService {
* @param verifyCode
* @returns
*/
public async checkVerifyCode(email: string, verifyCode: string) {
public checkVerifyCode = async (email: string, verifyCode: string) => {
if (!email || !isEmail(email)) {
throw new HttpException('请检查邮箱地址', HttpStatus.BAD_REQUEST);
}
const ret = await this.verifyRepo.findOne({ email, verifyCode });
const ret = await this.redis.get(`verify-${email}`);
if (!ret) {
throw new HttpException('验证码已过期,请重新获取', HttpStatus.BAD_REQUEST);
}
if (ret !== verifyCode) {
throw new HttpException('验证码错误', HttpStatus.BAD_REQUEST);
}
return Boolean(ret);
}
};
}

View File

@ -1,23 +1,17 @@
import { RedisDBEnum } from '@constants/*';
import { DocumentEntity } from '@entities/document.entity';
import { UserEntity } from '@entities/user.entity';
import { ViewEntity } from '@entities/view.entity';
import { buildRedis } from '@helpers/redis.helper';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IDocument, IOrganization, IUser } from '@think/domains';
import Redis from 'ioredis';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@Injectable()
export class ViewService {
private redis: Redis;
constructor(
@InjectRepository(ViewEntity)
private readonly viewRepo: Repository<ViewEntity>
) {
constructor() {
this.buildRedis();
}

View File

@ -2,21 +2,17 @@ import { OperateUserAuthDto } from '@dtos/auth.dto';
import { CreateWikiDto } from '@dtos/create-wiki.dto';
import { ShareWikiDto } from '@dtos/share-wiki.dto';
import { UpdateWikiDto } from '@dtos/update-wiki.dto';
// import { WikiUserDto } from '@dtos/wiki-user.dto';
import { WikiEntity } from '@entities/wiki.entity';
// import { WikiUserEntity } from '@entities/wiki-user.entity';
import { array2tree } from '@helpers/tree.helper';
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AuthService } from '@services/auth.service';
import { DocumentService } from '@services/document.service';
import { MessageService } from '@services/message.service';
import { OrganizationService } from '@services/organization.service';
import { StarService } from '@services/star.service';
import { UserService } from '@services/user.service';
import { OutUser } from '@services/user.service';
import { ViewService } from '@services/view.service';
import { AuthEnum, buildMessageURL, DocumentStatus, IPagination, WikiStatus, WikiUserRole } from '@think/domains';
import { AuthEnum, buildMessageURL, DocumentStatus, IPagination, IUser, WikiStatus } from '@think/domains';
import { instanceToPlain } from 'class-transformer';
import * as lodash from 'lodash';
import { Repository } from 'typeorm';
@ -30,9 +26,6 @@ export class WikiService {
@Inject(forwardRef(() => AuthService))
private readonly authService: AuthService,
// @InjectRepository(WikiUserEntity)
// private readonly wikiUserRepo: Repository<WikiUserEntity>,
@Inject(forwardRef(() => MessageService))
private readonly messageService: MessageService,
@ -46,10 +39,7 @@ export class WikiService {
private readonly userService: UserService,
@Inject(forwardRef(() => ViewService))
private readonly viewService: ViewService,
@Inject(forwardRef(() => OrganizationService))
private readonly organizationService: OrganizationService
private readonly viewService: ViewService
) {}
/**
@ -73,115 +63,6 @@ export class WikiService {
return ret;
}
// /**
// * 目标用户是否为知识库成员
// * @param wikiId
// * @param userId
// * @returns
// */
// public async isMember(wikiId: string, userId: string) {
// const auth = await this.wikiUserRepo.findOne({ wikiId, userId });
// return !!auth && [WikiUserRole.admin, WikiUserRole.normal].includes(auth.userRole);
// }
// /**
// * 获取知识库成员信息
// * @param wikiId
// * @param userId
// * @returns
// */
// public async findWikiUser(wikiId: string, userId: string) {
// return await this.wikiUserRepo.findOne({
// userId,
// wikiId,
// });
// }
// /**
// * 操作知识库成员(添加、修改角色)
// * @param param0
// * @returns
// */
// async operateWikiUser({ wikiId, currentUserId, targetUserId, targetUserRole }) {
// const wiki = await this.wikiRepo.findOne(wikiId);
// // await this.organizationService.canUserVisitOrganization(currentUserId, wiki.organizationId);
// // 1. 检查知识库
// if (!wiki) {
// throw new HttpException('目标知识库不存在', HttpStatus.BAD_REQUEST);
// }
// const isCurrentUserCreator = currentUserId === wiki.createUserId;
// const isTargetUserCreator = targetUserId === wiki.createUserId;
// const currentWikiUserRole = isCurrentUserCreator
// ? WikiUserRole.admin
// : (
// await this.wikiUserRepo.findOne({
// wikiId: wiki.id,
// userId: currentUserId,
// })
// ).userRole;
// // 2. 检查成员是否存在
// const targetUser = await this.userService.findOne(targetUserId);
// const targetWikiUser = await this.wikiUserRepo.findOne({
// wikiId: wiki.id,
// userId: targetUserId,
// });
// if (targetWikiUser) {
// if (targetWikiUser.userRole === targetUserRole) return;
// // 2.1 修改知识库用户角色
// if (targetUserRole === WikiUserRole.admin) {
// if (currentWikiUserRole !== WikiUserRole.admin) {
// throw new HttpException('您无权限进行该操作', HttpStatus.FORBIDDEN);
// }
// }
// const userRole = isTargetUserCreator ? WikiUserRole.admin : targetUserRole;
// const newData = {
// ...targetWikiUser,
// userRole,
// };
// const res = await this.wikiUserRepo.merge(targetWikiUser, newData);
// const ret = await this.wikiUserRepo.save(res);
// await this.messageService.notify(targetUser, {
// title: `您在「${wiki.name}」的权限已变更`,
// message: `您在「${wiki.name}」的权限已变更,快去看看吧!`,
// url: buildMessageURL('toWiki')({
// organizationId: wiki.organizationId,
// wikiId: wiki.id,
// }),
// });
// return ret;
// } else {
// // 2.2. 添加知识库新用户
// if (currentWikiUserRole !== WikiUserRole.admin) {
// throw new HttpException('您无权限进行该操作', HttpStatus.FORBIDDEN);
// }
// const data: Partial<WikiUserEntity> = {
// wikiId,
// organizationId: wiki.organizationId,
// createUserId: wiki.createUserId,
// userId: targetUserId,
// userRole: isTargetUserCreator ? WikiUserRole.admin : targetUserRole,
// };
// const res = await this.wikiUserRepo.create(data);
// const ret = await this.wikiUserRepo.save(res);
// await this.messageService.notify(targetUser, {
// title: `您被添加到知识库「${wiki.name}」`,
// message: `您被添加到知识库「${wiki.name}」,快去看看吧!`,
// url: buildMessageURL('toWiki')({
// organizationId: wiki.organizationId,
// wikiId: wiki.id,
// }),
// });
// return ret;
// }
// }
/**
*
* @param user
@ -189,7 +70,7 @@ export class WikiService {
* @param dto
* @returns
*/
async addWikiUser(user: OutUser, wikiId, dto: OperateUserAuthDto) {
async addWikiUser(user: IUser, wikiId, dto: OperateUserAuthDto) {
const targetUser = await this.userService.findOne({ name: dto.userName });
if (!targetUser) {
@ -197,27 +78,40 @@ export class WikiService {
}
const wiki = await this.wikiRepo.findOne(wikiId);
const homeDoc = await this.getWikiHomeDocument(user, wikiId);
if (!wiki) {
throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND);
}
await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
if (
!(await this.authService.getAuth(targetUser.id, {
organizationId: wiki.organizationId,
wikiId: null,
documentId: null,
}))
) {
throw new HttpException('该用户非组织成员', HttpStatus.FORBIDDEN);
}
const homeDoc = await this.getWikiHomeDocument(user, wikiId);
await Promise.all([
this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: null,
});
}),
await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: homeDoc.id,
});
}),
]);
await this.messageService.notify(targetUser, {
await this.messageService.notify(targetUser.id, {
title: `您被添加到知识库「${wiki.name}`,
message: `您被添加到知识库「${wiki.name}」,快去看看吧!`,
url: buildMessageURL('toWiki')({
@ -234,7 +128,7 @@ export class WikiService {
* @param dto
* @returns
*/
async updateWikiUser(user: OutUser, wikiId, dto: OperateUserAuthDto) {
async updateWikiUser(user: IUser, wikiId, dto: OperateUserAuthDto) {
const targetUser = await this.userService.findOne({ name: dto.userName });
if (!targetUser) {
@ -248,21 +142,23 @@ export class WikiService {
throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND);
}
await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
await Promise.all([
this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: null,
});
}),
await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: homeDoc.id,
});
}),
]);
await this.messageService.notify(targetUser, {
await this.messageService.notify(targetUser.id, {
title: `您在知识库「${wiki.name}」的权限有更新`,
message: `您在知识库「${wiki.name}」的权限有更新,快去看看吧!`,
url: buildMessageURL('toWiki')({
@ -279,7 +175,7 @@ export class WikiService {
* @param dto
* @returns
*/
async deleteWikiUser(user: OutUser, wikiId, dto: OperateUserAuthDto) {
async deleteWikiUser(user: IUser, wikiId, dto: OperateUserAuthDto) {
const targetUser = await this.userService.findOne({ name: dto.userName });
if (!targetUser) {
@ -293,23 +189,25 @@ export class WikiService {
throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND);
}
await this.authService.deleteOtherUserAuth(user.id, targetUser.id, {
await Promise.all([
this.authService.deleteOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: null,
});
}),
await this.authService.deleteOtherUserAuth(user.id, targetUser.id, {
this.authService.deleteOtherUserAuth(user.id, targetUser.id, {
auth: dto.userAuth,
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: homeDoc.id,
});
}),
]);
await this.messageService.notify(targetUser, {
title: `您在「${wiki.name}」的权限已变更`,
message: `您在${wiki.name}」的权限已变更,快去看看吧`,
await this.messageService.notify(targetUser.id, {
title: `知识库「${wiki.name}」的权限收回`,
message: `您在知识库「${wiki.name}」的权限已收回`,
url: buildMessageURL('toWiki')({
organizationId: wiki.organizationId,
wikiId: wiki.id,
@ -322,7 +220,7 @@ export class WikiService {
* @param userId
* @param wikiId
*/
async getWikiUsers(user, wikiId) {
async getWikiUsers(user, wikiId, pagination) {
const wiki = await this.wikiRepo.findOne(wikiId);
if (!wiki) {
@ -335,7 +233,11 @@ export class WikiService {
documentId: null,
});
const { data: usersAuth, total } = await this.authService.getUsersAuthInWiki(wiki.organizationId, wiki.id);
const { data: usersAuth, total } = await this.authService.getUsersAuthInWiki(
wiki.organizationId,
wiki.id,
pagination
);
const userIds = usersAuth.map((auth) => auth.userId);
const users = await this.userService.findByIds(userIds);
@ -350,28 +252,13 @@ export class WikiService {
return { data: withUserData, total };
}
// /**
// * 查询知识库指定用户详情
// * @param workspaceId
// * @param userId
// * @returns
// */
// async getWikiUserDetail({ wikiId, userId }): Promise<WikiUserEntity> {
// const data = { wikiId, userId };
// const wikiUser = await this.wikiUserRepo.findOne(data);
// if (!wikiUser) {
// throw new HttpException('您不在该知识库中', HttpStatus.FORBIDDEN);
// }
// return wikiUser;
// }
/**
*
* @param user
* @param dto CreateWikiDto
* @returns
*/
async createWiki(user: OutUser, dto: CreateWikiDto) {
async createWiki(user: IUser, dto: CreateWikiDto) {
await this.authService.canView(user.id, {
organizationId: dto.organizationId,
wikiId: null,
@ -386,7 +273,7 @@ export class WikiService {
const toSaveWiki = await this.wikiRepo.create(data);
const wiki = await this.wikiRepo.save(toSaveWiki);
const { data: userAuths } = await this.authService.getUsersAuthInOrganization(wiki.organizationId);
const { data: userAuths } = await this.authService.getUsersAuthInOrganization(wiki.organizationId, null);
await Promise.all([
...userAuths
@ -399,6 +286,7 @@ export class WikiService {
documentId: null,
});
}),
await this.authService.createOrUpdateAuth(user.id, {
auth: AuthEnum.creator,
organizationId: wiki.organizationId,
@ -426,7 +314,6 @@ export class WikiService {
const homeDocumentId = homeDoc.id;
const withHomeDocumentIdWiki = await this.wikiRepo.merge(wiki, { homeDocumentId });
await this.wikiRepo.save(withHomeDocumentIdWiki);
return withHomeDocumentIdWiki;
}
@ -436,7 +323,7 @@ export class WikiService {
* @param pagination
* @returns
*/
async getAllWikis(user: OutUser, organizationId, pagination: IPagination) {
async getAllWikis(user: IUser, organizationId, pagination: IPagination) {
await this.authService.canView(user.id, {
organizationId,
wikiId: null,
@ -467,7 +354,7 @@ export class WikiService {
* @param pagination
* @returns
*/
async getOwnWikis(user: OutUser, organizationId, pagination: IPagination) {
async getOwnWikis(user: IUser, organizationId, pagination: IPagination) {
await this.authService.canView(user.id, {
organizationId,
wikiId: null,
@ -497,7 +384,7 @@ export class WikiService {
* @param pagination
* @returns
*/
async getJoinWikis(user: OutUser, organizationId, pagination: IPagination) {
async getJoinWikis(user: IUser, organizationId, pagination: IPagination) {
await this.authService.canView(user.id, {
organizationId,
wikiId: null,
@ -528,7 +415,7 @@ export class WikiService {
* @param wikiId
* @returns
*/
async getWikiDetail(user: OutUser, wikiId: string) {
async getWikiDetail(user: IUser, wikiId: string) {
const wiki = await this.wikiRepo.findOne(wikiId);
await this.authService.canView(user.id, {
organizationId: wiki.organizationId,
@ -545,7 +432,7 @@ export class WikiService {
* @param wikiId
* @returns
*/
async getWikiHomeDocument(user: OutUser, wikiId) {
async getWikiHomeDocument(user: IUser, wikiId) {
const res = await this.documentService.documentRepo.findOne({ wikiId, isWikiHome: true });
await this.authService.canView(user.id, {
organizationId: res.organizationId,
@ -562,7 +449,7 @@ export class WikiService {
* @param dto
* @returns
*/
async updateWiki(user: OutUser, wikiId, dto: UpdateWikiDto) {
async updateWiki(user: IUser, wikiId, dto: UpdateWikiDto) {
const oldData = await this.wikiRepo.findOne(wikiId);
if (!oldData) {
throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND);
@ -586,26 +473,35 @@ export class WikiService {
* @param wikiId
* @returns
*/
async deleteWiki(user: OutUser, wikiId) {
async deleteWiki(user: IUser, wikiId) {
const wiki = await this.wikiRepo.findOne(wikiId);
await this.authService.canDelete(user.id, {
organizationId: wiki.organizationId,
wikiId: wiki.id,
documentId: null,
});
await this.wikiRepo.remove(wiki);
// TODO: 删除相应文档以及对应权限
// if (user.id !== wiki.createUserId) {
// throw new HttpException('您不是创建者,无法删除该知识库', HttpStatus.FORBIDDEN);
// }
// await this.documentService.deleteWikiDocuments(user, wikiId);
// const users = await this.wikiUserRepo.find({ wikiId });
// await Promise.all([this.wikiUserRepo.remove(users)]);
await Promise.all([
this.authService.deleteWiki(wiki.organizationId, wiki.id),
this.wikiRepo.remove(wiki),
this.documentService.deleteWikiDocuments(user, wikiId),
]);
return wiki;
}
/**
*
* @param user
* @param wikiId
*/
async deleteOrganizationWiki(user, organizationId) {
const wikis = await this.wikiRepo.find({ organizationId });
await Promise.all(
wikis.map((wiki) => {
return this.deleteWiki(user, wiki.id);
})
);
}
/**
*
* @param user
@ -613,7 +509,7 @@ export class WikiService {
* @param dto
* @returns
*/
async shareWiki(user: OutUser, wikiId, dto: ShareWikiDto) {
async shareWiki(user: IUser, wikiId, dto: ShareWikiDto) {
const wiki = await this.wikiRepo.findOne(wikiId);
if (!wiki) {
@ -668,7 +564,7 @@ export class WikiService {
* @param wikiId
* @returns
*/
async getWikiTocs(user: OutUser, wikiId) {
async getWikiTocs(user: IUser, wikiId) {
const wiki = await this.wikiRepo.findOne(wikiId);
const records = await this.authService.getUserCanViewDocumentsInWiki(wiki.organizationId, wiki.id);
@ -719,46 +615,6 @@ export class WikiService {
);
}
// /**
// * 获取知识库所有文档(无结构嵌套)
// * @param user
// * @param wikiId
// * @returns
// */
// async getWikiDocs(user: OutUser, wikiId) {
// // 通过文档成员表获取当前用户可查阅的所有文档
// const records = await this.documentService.documentUserRepo.find({
// userId: user.id,
// wikiId,
// });
// const ids = records.map((record) => record.documentId);
// const documents = await this.documentService.documentRepo.findByIds(ids);
// const docs = documents
// .filter((doc) => !doc.isWikiHome)
// .map((doc) => {
// const res = instanceToPlain(doc);
// res.key = res.id;
// return res;
// })
// .map((item) => {
// return lodash.omit(item, ['content', 'state']);
// });
// docs.sort((a, b) => a.index - b.index);
// const docsWithCreateUser = await Promise.all(
// docs.map(async (doc) => {
// const createUser = await this.userService.findById(doc.createUserId);
// return { ...doc, createUser };
// })
// );
// return docsWithCreateUser;
// }
/**
*
* @param wikiId