diff --git a/packages/client/src/components/document/actions/index.tsx b/packages/client/src/components/document/actions/index.tsx index 20c7ab4..f07679a 100644 --- a/packages/client/src/components/document/actions/index.tsx +++ b/packages/client/src/components/document/actions/index.tsx @@ -1,7 +1,7 @@ import { IconArticle, IconBranch, IconExport, IconHistory, IconMore, IconPlus, IconStar } from '@douyinfe/semi-icons'; import { Button, Dropdown, Space, Typography } from '@douyinfe/semi-ui'; import { ButtonProps } from '@douyinfe/semi-ui/button/Button'; -import { IDocument } from '@think/domains'; +import { IDocument, IOrganization, IWiki } from '@think/domains'; import cls from 'classnames'; import { DocumentCreator } from 'components/document/create'; import { DocumentDeletor } from 'components/document/delete'; @@ -17,8 +17,9 @@ import React, { useCallback } from 'react'; import styles from './index.module.scss'; interface IProps { - wikiId: string; - documentId: string; + organizationId: IOrganization['id']; + wikiId: IWiki['id']; + documentId: IDocument['id']; document?: IDocument; hoverVisible?: boolean; onStar?: () => void; @@ -34,6 +35,7 @@ interface IProps { const { Text } = Typography; export const DocumentActions: React.FC = ({ + organizationId, wikiId, documentId, hoverVisible, @@ -107,6 +109,7 @@ export const DocumentActions: React.FC = ({ /> ( diff --git a/packages/client/src/components/document/card/index.tsx b/packages/client/src/components/document/card/index.tsx index 1dc5494..56dd365 100644 --- a/packages/client/src/components/document/card/index.tsx +++ b/packages/client/src/components/document/card/index.tsx @@ -22,8 +22,8 @@ export const DocumentCard: React.FC<{ document: IDocument }> = ({ document }) =>
@@ -40,7 +40,11 @@ export const DocumentCard: React.FC<{ document: IDocument }> = ({ document }) =>
diff --git a/packages/client/src/components/document/collaboration/index.tsx b/packages/client/src/components/document/collaboration/index.tsx index 0fc6b5a..623a03f 100644 --- a/packages/client/src/components/document/collaboration/index.tsx +++ b/packages/client/src/components/document/collaboration/index.tsx @@ -1,29 +1,12 @@ -import { IconDelete, IconUserAdd } from '@douyinfe/semi-icons'; -import { - Avatar, - AvatarGroup, - Button, - Checkbox, - Dropdown, - Input, - Modal, - Popconfirm, - Spin, - Table, - TabPane, - Tabs, - Toast, - Tooltip, - Typography, -} from '@douyinfe/semi-ui'; -import { DataRender } from 'components/data-render'; -import { DocumentLinkCopyer } from 'components/document/link'; +import { IconUserAdd } from '@douyinfe/semi-icons'; +import { Avatar, AvatarGroup, Button, Dropdown, Modal, Toast, Tooltip } from '@douyinfe/semi-ui'; +import { Members } from 'components/members'; import { useDoumentMembers } from 'data/document'; import { useUser } from 'data/user'; import { event, JOIN_USER } from 'event'; import { IsOnMobile } from 'hooks/use-on-mobile'; import { useToggle } from 'hooks/use-toggle'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; interface IProps { wikiId: string; @@ -31,111 +14,13 @@ interface IProps { disabled?: boolean; } -const { Paragraph } = Typography; -const { Column } = Table; - -// eslint-disable-next-line react/display-name -const renderChecked = (onChange, authKey: 'readable' | 'editable') => (checked, docAuth) => { - const handle = (evt) => { - const data = { - ...docAuth.auth, - userName: docAuth.user.name, - }; - data[authKey] = evt.target.checked; - onChange(data); - }; - - return ; -}; - export const DocumentCollaboration: React.FC = ({ wikiId, documentId, disabled = false }) => { const { isMobile } = IsOnMobile.useHook(); - const ref = useRef(); const toastedUsersRef = useRef([]); const { user: currentUser } = useUser(); const [visible, toggleVisible] = useToggle(false); - const { users, loading, error, addUser, updateUser, deleteUser } = useDoumentMembers(documentId, { - enabled: visible, - }); - const [inviteUser, setInviteUser] = useState(''); const [collaborationUsers, setCollaborationUsers] = useState([]); - - const handleOk = useCallback(() => { - addUser(inviteUser).then(() => { - Toast.success('添加成功'); - setInviteUser(''); - }); - }, [addUser, inviteUser]); - - const handleDelete = useCallback( - (docAuth) => { - const data = { - ...docAuth.auth, - userName: docAuth.user.name, - }; - deleteUser(data); - }, - [deleteUser] - ); - - const content = useMemo( - () => ( - - -
- - - 将对方加入文档进行协作,您也可将该链接发送给对方。 - - - - - -
-
- - } - normalContent={() => ( - - - - - ( - handleDelete(document)}> -
- )} - /> -
-
- ), - [documentId, error, handleDelete, handleOk, inviteUser, loading, updateUser, users, wikiId] - ); - + const content = useMemo(() => , [documentId]); const btn = useMemo( () => ( - - ); - return ( <> diff --git a/packages/client/src/components/document/create/index.tsx b/packages/client/src/components/document/create/index.tsx index e252a0b..2846499 100644 --- a/packages/client/src/components/document/create/index.tsx +++ b/packages/client/src/components/document/create/index.tsx @@ -1,28 +1,32 @@ import { Checkbox, Modal, TabPane, Tabs } from '@douyinfe/semi-ui'; +import { IDocument, IWiki } from '@think/domains'; import { TemplateCardEmpty } from 'components/template/card'; import { TemplateList } from 'components/template/list'; import { useCreateDocument } from 'data/document'; import { useOwnTemplates, usePublicTemplates } from 'data/template'; +import { useRouterQuery } from 'hooks/use-router-query'; import Router from 'next/router'; import { Dispatch, SetStateAction, useCallback, useEffect, useState } from 'react'; import styles from './index.module.scss'; interface IProps { - wikiId: string; - parentDocumentId?: string; + wikiId: IWiki['id']; + parentDocumentId?: IDocument['id']; visible: boolean; toggleVisible: Dispatch>; onCreate?: () => void; } -export const DocumentCreator: React.FC = ({ wikiId, parentDocumentId, visible, toggleVisible, onCreate }) => { +export const DocumentCreator: React.FC = ({ parentDocumentId, wikiId, visible, toggleVisible, onCreate }) => { + const { organizationId } = useRouterQuery<{ organizationId?: string }>(); const { loading, create } = useCreateDocument(); const [createChildDoc, setCreateChildDoc] = useState(false); const [templateId, setTemplateId] = useState(''); const handleOk = () => { const data = { + organizationId, wikiId, parentDocumentId: createChildDoc ? parentDocumentId : null, templateId, @@ -32,7 +36,8 @@ export const DocumentCreator: React.FC = ({ wikiId, parentDocumentId, vi onCreate && onCreate(); setTemplateId(''); Router.push({ - pathname: `/wiki/${wikiId}/document/${res.id}/edit`, + pathname: `/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/edit`, + query: { organizationId, wikiId, documentId: res.id }, }); }); }; diff --git a/packages/client/src/components/document/editor/index.tsx b/packages/client/src/components/document/editor/index.tsx index af29ce6..f2af666 100644 --- a/packages/client/src/components/document/editor/index.tsx +++ b/packages/client/src/components/document/editor/index.tsx @@ -38,11 +38,12 @@ export const DocumentEditor: React.FC = ({ documentId }) => { const goback = useCallback(() => { Router.push({ - pathname: `/wiki/${document.wikiId}/document/${documentId}`, + pathname: `/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]`, + query: { organizationId: document.organizationId, wikiId: document.wikiId, documentId: document.id }, }).then(() => { triggerRefreshTocs(); }); - }, [document, documentId]); + }, [document]); const actions = useMemo( () => ( @@ -50,8 +51,17 @@ export const DocumentEditor: React.FC = ({ documentId }) => { {document && authority.readable && ( )} - {document && } - {document && } + {document && ( + + )} + {document && ( + + )} ), diff --git a/packages/client/src/components/document/reader/index.tsx b/packages/client/src/components/document/reader/index.tsx index 7224d23..6343c93 100644 --- a/packages/client/src/components/document/reader/index.tsx +++ b/packages/client/src/components/document/reader/index.tsx @@ -56,7 +56,10 @@ export const DocumentReader: React.FC = ({ documentId }) => { ); const gotoEdit = useCallback(() => { - Router.push(`/wiki/${document.wikiId}/document/${document.id}/edit`); + Router.push({ + pathname: `/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/edit`, + query: { organizationId: document.organizationId, wikiId: document.wikiId, documentId: document.id }, + }); }, [document]); const actions = useMemo(() => { @@ -70,11 +73,26 @@ export const DocumentReader: React.FC = ({ documentId }) => { documentId={documentId} /> )} - {document && } + {document && ( + + )} + + + + AuthEnumTextMap[auth]} + /> + } + /> + ( + <> +
+ + )} + /> + + + + + ); +}; diff --git a/packages/client/src/components/organization/index.module.scss b/packages/client/src/components/organization/index.module.scss new file mode 100644 index 0000000..e720445 --- /dev/null +++ b/packages/client/src/components/organization/index.module.scss @@ -0,0 +1,16 @@ +.wrap { + display: inline-flex !important; + flex-wrap: nowrap; + align-items: center; + + > svg { + width: 32px; + height: 32px; + } + + > span { + margin-left: 4px; + font-size: 1.2rem; + font-weight: 500; + } +} diff --git a/packages/client/src/components/organization/index.tsx b/packages/client/src/components/organization/index.tsx new file mode 100644 index 0000000..9e65800 --- /dev/null +++ b/packages/client/src/components/organization/index.tsx @@ -0,0 +1,66 @@ +import { Typography } from '@douyinfe/semi-ui'; +import { Avatar } from '@douyinfe/semi-ui'; +import { DataRender } from 'components/data-render'; +import { useOrganizationDetail } from 'data/organization'; +import { useRouterQuery } from 'hooks/use-router-query'; +import Link from 'next/link'; + +import styles from './index.module.scss'; + +const { Text } = Typography; + +export const OrganizationImage = () => { + const { organizationId } = useRouterQuery<{ organizationId: string }>(); + const { data, loading, error } = useOrganizationDetail(organizationId); + + return ( + { + return ( + +
+ + + + ); + }} + /> + ); +}; + +export const OrganizationText = () => { + const { organizationId } = useRouterQuery<{ organizationId: string }>(); + const { data, loading, error } = useOrganizationDetail(organizationId); + + return ( + { + return ( + + + {data.name} + + + ); + }} + /> + ); +}; diff --git a/packages/client/src/components/organization/public-switcher/index.module.scss b/packages/client/src/components/organization/public-switcher/index.module.scss new file mode 100644 index 0000000..5458e72 --- /dev/null +++ b/packages/client/src/components/organization/public-switcher/index.module.scss @@ -0,0 +1,4 @@ +.nameWrap { + display: flex; + align-items: center; +} diff --git a/packages/client/src/components/organization/public-switcher/index.tsx b/packages/client/src/components/organization/public-switcher/index.tsx new file mode 100644 index 0000000..915e899 --- /dev/null +++ b/packages/client/src/components/organization/public-switcher/index.tsx @@ -0,0 +1,84 @@ +import { 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 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 { + data: userOrganizations, + loading: userOrganizationsLoading, + error: userOrganizationsError, + } = useUserOrganizations(); + + const gotoCreate = useCallback(() => { + Router.push(`/app/org/create`); + }, []); + + return ( + + + + + + { + return ( + + {(userOrganizations || []).map((org) => { + return ( + + + + + + {org.name} + + + + + ); + })} + + + + 新建组织 + + + + ); + }} + /> + } + > + + + + + + + + + +
+ +
+ + )} + /> + ); +}; diff --git a/packages/client/src/components/organization/setting/index.tsx b/packages/client/src/components/organization/setting/index.tsx new file mode 100644 index 0000000..93fca90 --- /dev/null +++ b/packages/client/src/components/organization/setting/index.tsx @@ -0,0 +1,65 @@ +import { TabPane, Tabs } from '@douyinfe/semi-ui'; +import { IOrganization, IWiki } from '@think/domains'; +import { Seo } from 'components/seo'; +import { WikiTocsManager } from 'components/wiki/tocs/manager'; +import { useWikiDetail } from 'data/wiki'; +import React from 'react'; + +import { Base } from './base'; +import { OrganizationMembers } from './members'; +import { More } from './more'; + +interface IProps { + organizationId: IOrganization['id']; + tab?: string; + onNavigate: (arg: string) => void; +} + +const TitleMap = { + base: '基础信息', + members: '成员管理', + more: '更多', +}; + +export const OrganizationSetting: React.FC = ({ organizationId, tab, onNavigate }) => { + // const { data, update } = useWikiDetail(wikiId); + + return ( + <> + + + + + + + + + + + {/* + + + + + + + + + + + + + + + + + + + + + + */} + + + ); +}; diff --git a/packages/client/src/components/organization/setting/members/add.tsx b/packages/client/src/components/organization/setting/members/add.tsx new file mode 100644 index 0000000..b877db0 --- /dev/null +++ b/packages/client/src/components/organization/setting/members/add.tsx @@ -0,0 +1,62 @@ +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 = ({ 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 ( + toggleVisible(false)} + maskClosable={false} + style={{ maxWidth: '96vw' }} + footer={null} + > +
+ {[AuthEnum.creator, AuthEnum.admin].includes(userAuth) ? ( + + ) : null} + + + + + +
+
+ ); +}; diff --git a/packages/client/src/components/organization/setting/members/edit.tsx b/packages/client/src/components/organization/setting/members/edit.tsx new file mode 100644 index 0000000..cd72139 --- /dev/null +++ b/packages/client/src/components/organization/setting/members/edit.tsx @@ -0,0 +1,58 @@ +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 = ({ 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 ( + toggleVisible(false)} + maskClosable={false} + style={{ maxWidth: '96vw' }} + footer={null} + > +
+ {[AuthEnum.creator, AuthEnum.admin].includes(userAuth) ? ( + + ) : null} + {} + + +
+
+ ); +}; diff --git a/packages/client/src/components/organization/setting/members/index.module.scss b/packages/client/src/components/organization/setting/members/index.module.scss new file mode 100644 index 0000000..7eb36ff --- /dev/null +++ b/packages/client/src/components/organization/setting/members/index.module.scss @@ -0,0 +1,6 @@ +.wrap { + > header { + display: flex; + justify-content: flex-end; + } +} diff --git a/packages/client/src/components/organization/setting/members/index.tsx b/packages/client/src/components/organization/setting/members/index.tsx new file mode 100644 index 0000000..22faab7 --- /dev/null +++ b/packages/client/src/components/organization/setting/members/index.tsx @@ -0,0 +1,110 @@ +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 { 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'; + +interface IProps { + organizationId: IOrganization['id']; +} + +const { Title, Paragraph } = Typography; +const { Column } = Table; + +export const OrganizationMembers: React.FC = ({ 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 ( +
+
{/* */}
+ ( +
+ 权限说明} + description={ +
+ 创建者:管理组织内所有知识库、文档,可删除组织 + 管理员:管理组织内所有知识库、文档,不可删除组织 + 成员:可访问组织内所有知识库、文档,不可删除组织 +
+ } + /> + +
+ +
+ + + AuthEnumTextMap[auth]} + /> + } + /> + ( + <> +
+
+ )} + /> + + + +
+ ); +}; diff --git a/packages/client/src/components/organization/setting/more/index.tsx b/packages/client/src/components/organization/setting/more/index.tsx new file mode 100644 index 0000000..e699410 --- /dev/null +++ b/packages/client/src/components/organization/setting/more/index.tsx @@ -0,0 +1,27 @@ +import { Banner, Button, Typography } from '@douyinfe/semi-ui'; +import { WorkspaceDeletor } from 'components/wiki/delete'; + +interface IProps { + wikiId: string; +} + +const { Paragraph } = Typography; + +export const More: React.FC = ({ wikiId }) => { + return ( +
+ 删除知识库及内部所有文档,不可恢复!} + style={{ marginBottom: 16 }} + /> + + + +
+ ); +}; diff --git a/packages/client/src/components/organization/switcher/index.module.scss b/packages/client/src/components/organization/switcher/index.module.scss new file mode 100644 index 0000000..5458e72 --- /dev/null +++ b/packages/client/src/components/organization/switcher/index.module.scss @@ -0,0 +1,4 @@ +.nameWrap { + display: flex; + align-items: center; +} diff --git a/packages/client/src/components/organization/switcher/index.tsx b/packages/client/src/components/organization/switcher/index.tsx new file mode 100644 index 0000000..1ec59e3 --- /dev/null +++ b/packages/client/src/components/organization/switcher/index.tsx @@ -0,0 +1,128 @@ +import { IconAppCenter, 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'; + +const { Text, Paragraph } = Typography; + +export const OrganizationSwitcher = () => { + const { organizationId } = useRouterQuery<{ organizationId: string }>(); + const { data, loading, error } = useOrganizationDetail(organizationId); + + const { + data: userOrganizations, + loading: userOrganizationsLoading, + error: userOrganizationsError, + } = useUserOrganizations(); + + const gotoCreate = useCallback(() => { + Router.push(`/app/org/create`); + }, []); + + return ( + { + return ( + + + + + + {data.name} + + + { + return ( + + {(userOrganizations || []).map((org) => { + return ( + + + + + + {org.name} + + + + + ); + })} + + + + + + + + 新建组织 + + + + + ); + }} + /> + } + > + - - - - - } - /> - ( - <> -
- - )} - /> - - - - ); + return ; }; diff --git a/packages/client/src/components/wiki/setting/users/placeholder.tsx b/packages/client/src/components/wiki/setting/users/placeholder.tsx deleted file mode 100644 index 2c0d27b..0000000 --- a/packages/client/src/components/wiki/setting/users/placeholder.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Skeleton, Table } from '@douyinfe/semi-ui'; - -const columns = [ - { - title: '用户名', - dataIndex: 'userName', - key: 'userName', - }, - { - title: '成员角色', - dataIndex: 'userRole', - key: 'userRole', - }, - { - title: '加入时间', - dataIndex: 'createdAt', - key: 'createdAt', - }, - { - title: '操作', - dataIndex: 'actions', - key: 'actions', - }, -]; - -const PLACEHOLDER_DATA = Array.from({ length: 3 }).fill({ - name: , - userRole: , - createdAt: , -}); - -export const Placeholder = () => { - return ( - - } - loading={true} - > - ); -}; diff --git a/packages/client/src/components/wiki/star/index.tsx b/packages/client/src/components/wiki/star/index.tsx index ddc5931..794c9e5 100644 --- a/packages/client/src/components/wiki/star/index.tsx +++ b/packages/client/src/components/wiki/star/index.tsx @@ -1,16 +1,18 @@ import { IconStar } from '@douyinfe/semi-icons'; import { Button, Tooltip } from '@douyinfe/semi-ui'; +import { IOrganization, IWiki } from '@think/domains'; import { useWikiStarToggle } from 'data/star'; import React from 'react'; interface IProps { - wikiId: string; + organizationId: IOrganization['id']; + wikiId: IWiki['id']; render?: (arg: { star: boolean; text: string; toggleStar: () => Promise }) => React.ReactNode; onChange?: () => void; } -export const WikiStar: React.FC = ({ wikiId, render, onChange }) => { - const { data, toggle } = useWikiStarToggle(wikiId); +export const WikiStar: React.FC = ({ organizationId, wikiId, render, onChange }) => { + const { data, toggle } = useWikiStarToggle(organizationId, wikiId); const text = data ? '取消收藏' : '收藏知识库'; return ( diff --git a/packages/client/src/components/wiki/tocs/index.tsx b/packages/client/src/components/wiki/tocs/index.tsx index 3773484..55512a0 100644 --- a/packages/client/src/components/wiki/tocs/index.tsx +++ b/packages/client/src/components/wiki/tocs/index.tsx @@ -1,10 +1,11 @@ import { IconPlus, IconSmallTriangleDown } from '@douyinfe/semi-icons'; import { Avatar, Button, Dropdown, Skeleton, Typography } from '@douyinfe/semi-ui'; +import { IDocument } from '@think/domains'; import cls from 'classnames'; import { DataRender } from 'components/data-render'; import { IconOverview, IconSetting } from 'components/icons'; import { findParents } from 'components/wiki/tocs/utils'; -import { useStarWikis, useWikiStarDocuments } from 'data/star'; +import { useStarDocumentsInWiki, useStarWikisInOrganization } from 'data/star'; import { useWikiDetail, useWikiTocs } from 'data/wiki'; import { triggerCreateDocument } from 'event'; import Link from 'next/link'; @@ -18,7 +19,7 @@ interface IProps { wikiId: string; documentId?: string; docAsLink?: string; - getDocLink?: (arg: string) => string; + getDocLink?: (arg: IDocument) => string; } const { Text } = Typography; @@ -26,18 +27,18 @@ const { Text } = Typography; export const WikiTocs: React.FC = ({ wikiId, documentId = null, - docAsLink = '/wiki/[wikiId]/document/[documentId]', - getDocLink = (documentId) => `/wiki/${wikiId}/document/${documentId}`, + docAsLink = '/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]', + getDocLink = (document) => `/app/org/${document.organizationId}/wiki/${document.wikiId}/doc/${document.id}`, }) => { const { pathname, query } = useRouter(); const { data: wiki, loading: wikiLoading, error: wikiError } = useWikiDetail(wikiId); const { data: tocs, loading: tocsLoading, error: tocsError } = useWikiTocs(wikiId); - const { data: starWikis } = useStarWikis(); + const { data: starWikis } = useStarWikisInOrganization(query.organizationId); const { data: starDocuments, loading: starDocumentsLoading, error: starDocumentsError, - } = useWikiStarDocuments(wikiId); + } = useStarDocumentsInWiki(query.organizationId, wikiId); const [parentIds, setParentIds] = useState>([]); const otherStarWikis = useMemo(() => (starWikis || []).filter((wiki) => wiki.id !== wikiId), [starWikis, wikiId]); @@ -86,8 +87,8 @@ export const WikiTocs: React.FC = ({
= ({
@@ -235,11 +237,16 @@ export const WikiTocs: React.FC = ({ } error={wikiError} normalContent={() => ( -
+
diff --git a/packages/client/src/components/wiki/tocs/public.tsx b/packages/client/src/components/wiki/tocs/public.tsx index 986cc40..5ea059d 100644 --- a/packages/client/src/components/wiki/tocs/public.tsx +++ b/packages/client/src/components/wiki/tocs/public.tsx @@ -1,5 +1,6 @@ import { IconPlus } from '@douyinfe/semi-icons'; import { Avatar, Skeleton, Space, Typography } from '@douyinfe/semi-ui'; +import { IDocument } from '@think/domains'; import { DataRender } from 'components/data-render'; import { IconOverview } from 'components/icons'; import { LogoImage, LogoText } from 'components/logo'; @@ -17,7 +18,7 @@ interface IProps { wikiId: string; documentId?: string; docAsLink?: string; - getDocLink?: (arg: string) => string; + getDocLink?: (arg: IDocument) => string; pageTitle: string; } @@ -28,7 +29,7 @@ export const WikiPublicTocs: React.FC = ({ wikiId, documentId = null, docAsLink = '/share/wiki/[wikiId]/document/[documentId]', - getDocLink = (documentId) => `/share/wiki/${wikiId}/document/${documentId}`, + getDocLink = (document) => `/share/wiki/${document.wikiId}/document/${document.id}`, }) => { const { pathname } = useRouter(); const { data: wiki, loading: wikiLoading, error: wikiError } = usePublicWikiDetail(wikiId); diff --git a/packages/client/src/components/wiki/tocs/tree.tsx b/packages/client/src/components/wiki/tocs/tree.tsx index a93492f..dbc2b72 100644 --- a/packages/client/src/components/wiki/tocs/tree.tsx +++ b/packages/client/src/components/wiki/tocs/tree.tsx @@ -16,6 +16,7 @@ const Actions = ({ node }) => { (
- + >; +type ICreateDocument = Partial>; type IDocumentWithAuth = { document: IDocument; authority: IAuthority }; type IUpdateDocument = Partial>; +/** + * 搜索组织内文档 + * @returns + */ +export const useSearchDocuments = (organizationId) => { + const [apiWithLoading, loading] = useAsyncLoading((keyword) => + HttpClient.request({ + method: DocumentApiDefinition.search.method, + url: DocumentApiDefinition.search.client(organizationId), + params: { keyword }, + }) + ); + + return { + search: apiWithLoading, + loading, + }; +}; + /** * 获取用户最近访问的文档 * @returns */ -export const getRecentVisitedDocuments = (cookie = null): Promise => { +export const getRecentVisitedDocuments = (organizationId, cookie = null): Promise => { return HttpClient.request({ method: DocumentApiDefinition.recent.method, - url: DocumentApiDefinition.recent.client(), + url: DocumentApiDefinition.recent.client(organizationId), cookie, }); }; @@ -26,10 +45,10 @@ export const getRecentVisitedDocuments = (cookie = null): Promise { +export const useRecentDocuments = (organizationId) => { const { data, error, isLoading, refetch } = useQuery( - DocumentApiDefinition.recent.client(), - getRecentVisitedDocuments, + DocumentApiDefinition.recent.client(organizationId), + () => getRecentVisitedDocuments(organizationId), { refetchOnWindowFocus: false, enabled: false } ); return { data, error, loading: isLoading, refresh: refetch }; @@ -67,16 +86,11 @@ export const useDoumentMembers = (documentId, options?: UseQueryOptions { + async (data) => { const ret = await HttpClient.request({ method: DocumentApiDefinition.addMemberById.method, url: DocumentApiDefinition.addMemberById.client(documentId), - data: { - documentId, - userName, - readable: true, - editable: false, - }, + data, }); refetch(); return ret; @@ -85,14 +99,11 @@ export const useDoumentMembers = (documentId, options?: UseQueryOptions { + async (data) => { const ret = await HttpClient.request({ method: DocumentApiDefinition.updateMemberById.method, url: DocumentApiDefinition.updateMemberById.client(documentId), - data: { - documentId, - ...docAuth, - }, + data, }); refetch(); return ret; @@ -101,14 +112,11 @@ export const useDoumentMembers = (documentId, options?: UseQueryOptions { + async (data) => { const ret = await HttpClient.request({ method: DocumentApiDefinition.deleteMemberById.method, url: DocumentApiDefinition.deleteMemberById.client(documentId), - data: { - documentId, - ...docAuth, - }, + data, }); refetch(); return ret; @@ -116,7 +124,7 @@ export const useDoumentMembers = (documentId, options?: UseQueryOptions { + const [apiWithLoading, loading] = useAsyncLoading((data) => + HttpClient.request({ + method: OrganizationApiDefinition.createOrganization.method, + url: OrganizationApiDefinition.createOrganization.client(), + data, + }) + ); + + return { + create: apiWithLoading, + loading, + }; +}; + +export const getPersonalOrganization = (cookie = null): Promise => { + return HttpClient.request({ + method: OrganizationApiDefinition.getPersonalOrganization.method, + url: OrganizationApiDefinition.getPersonalOrganization.client(), + cookie, + }); +}; + +/** + * 获取个人组织 + * @returns + */ +export const usePeronalOrganization = () => { + const { data, error, isLoading, refetch } = useQuery( + OrganizationApiDefinition.getPersonalOrganization.client(), + getPersonalOrganization + ); + return { data, error, loading: isLoading, refresh: refetch }; +}; + +export const getUserOrganizations = (cookie = null): Promise> => { + return HttpClient.request({ + method: OrganizationApiDefinition.getUserOrganizations.method, + url: OrganizationApiDefinition.getUserOrganizations.client(), + cookie, + }); +}; + +/** + * 获取用户除个人组织外可访问的组织 + * @returns + */ +export const useUserOrganizations = () => { + const { data, error, isLoading, refetch } = useQuery( + OrganizationApiDefinition.getUserOrganizations.client(), + getUserOrganizations + ); + + useEffect(() => { + event.on(REFRESH_ORGANIZATIONS, refetch); + return () => { + event.off(REFRESH_ORGANIZATIONS, refetch); + }; + }, [refetch]); + + return { data, error, loading: isLoading, refresh: refetch }; +}; + +export const getOrganizationDetail = (id, cookie = null): Promise => { + return HttpClient.request({ + method: OrganizationApiDefinition.getOrganizationDetail.method, + url: OrganizationApiDefinition.getOrganizationDetail.client(id), + cookie, + }); +}; + +/** + * 获取组织详情 + * @returns + */ +export const useOrganizationDetail = (id) => { + const { data, error, isLoading, refetch } = useQuery(OrganizationApiDefinition.getOrganizationDetail.client(id), () => + getOrganizationDetail(id) + ); + + /** + * 更新组织信息 + * @param data + * @returns + */ + const update = useCallback( + async (data) => { + const res = await HttpClient.request({ + method: OrganizationApiDefinition.updateOrganization.method, + url: OrganizationApiDefinition.updateOrganization.client(id), + data, + }); + refetch(); + triggerRefreshOrganizations(); + return res; + }, + [refetch, id] + ); + + return { data, error, loading: isLoading, refresh: refetch, update }; +}; + +export const getOrganizationMembers = ( + id, + cookie = null +): Promise<{ data: Array<{ auth: IAuth; user: IUser }>; total: number }> => { + return HttpClient.request({ + method: OrganizationApiDefinition.getMembers.method, + url: OrganizationApiDefinition.getMembers.client(id), + cookie, + }); +}; + +/** + * 获取组织成员 + * @returns + */ +export const useOrganizationMembers = (id) => { + const { data, error, isLoading, refetch } = useQuery(OrganizationApiDefinition.getMembers.client(id), () => + getOrganizationMembers(id) + ); + + const addUser = useCallback( + async (data) => { + const ret = await HttpClient.request({ + method: OrganizationApiDefinition.addMemberById.method, + url: OrganizationApiDefinition.addMemberById.client(id), + data, + }); + refetch(); + return ret; + }, + [refetch, id] + ); + + const updateUser = useCallback( + async (data) => { + const ret = await HttpClient.request({ + method: OrganizationApiDefinition.updateMemberById.method, + url: OrganizationApiDefinition.updateMemberById.client(id), + data, + }); + refetch(); + return ret; + }, + [refetch, id] + ); + + const deleteUser = useCallback( + async (data) => { + const ret = await HttpClient.request({ + method: OrganizationApiDefinition.deleteMemberById.method, + url: OrganizationApiDefinition.deleteMemberById.client(id), + data, + }); + refetch(); + return ret; + }, + [refetch, id] + ); + + return { data, error, loading: isLoading, refresh: refetch, addUser, updateUser, deleteUser }; +}; diff --git a/packages/client/src/data/star.ts b/packages/client/src/data/star.ts index e56ade8..05e5cf2 100644 --- a/packages/client/src/data/star.ts +++ b/packages/client/src/data/star.ts @@ -7,25 +7,29 @@ import { HttpClient } from 'services/http-client'; export type IWikiWithIsMember = IWiki & { isMember?: boolean }; /** - * 获取用户收藏的知识库 + * 获取组织内加星的知识库 * @returns */ -export const getStarWikis = (cookie = null): Promise => { +export const getStarWikisInOrganization = (organizationId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.wikis.method, - url: StarApiDefinition.wikis.client(), + method: StarApiDefinition.getStarWikisInOrganization.method, + url: StarApiDefinition.getStarWikisInOrganization.client(organizationId), cookie, }); }; /** - * 获取用户收藏的知识库 + * 获取组织内加星的知识库 * @returns */ -export const useStarWikis = () => { - const { data, error, isLoading, refetch } = useQuery(StarApiDefinition.wikis.client(), getStarWikis, { - staleTime: 500, - }); +export const useStarWikisInOrganization = (organizationId) => { + const { data, error, isLoading, refetch } = useQuery( + StarApiDefinition.getStarWikisInOrganization.client(organizationId), + () => getStarWikisInOrganization(organizationId), + { + staleTime: 500, + } + ); useEffect(() => { event.on(TOGGLE_STAR_WIKI, refetch); @@ -43,12 +47,13 @@ export const useStarWikis = () => { * @param wikiId * @returns */ -export const getWikiIsStar = (wikiId, cookie = null): Promise => { +export const getWikiIsStar = (organizationId, wikiId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.check.method, - url: StarApiDefinition.check.client(), + method: StarApiDefinition.isStared.method, + url: StarApiDefinition.isStared.client(), cookie, data: { + organizationId, wikiId, }, }); @@ -59,30 +64,33 @@ export const getWikiIsStar = (wikiId, cookie = null): Promise => { * @param wikiId * @returns */ -export const toggleStarWiki = (wikiId, cookie = null): Promise => { +export const toggleStarWiki = (organizationId, wikiId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.toggle.method, - url: StarApiDefinition.toggle.client(), + method: StarApiDefinition.toggleStar.method, + url: StarApiDefinition.toggleStar.client(), cookie, data: { + organizationId, wikiId, }, }); }; /** - * 收藏知识库 + * 加星或取消 * @param wikiId * @returns */ -export const useWikiStarToggle = (wikiId) => { - const { data, error, refetch } = useQuery([StarApiDefinition.check.client(), wikiId], () => getWikiIsStar(wikiId)); +export const useWikiStarToggle = (organizationId, wikiId) => { + const { data, error, refetch } = useQuery([StarApiDefinition.toggleStar.client(), organizationId, wikiId], () => + getWikiIsStar(organizationId, wikiId) + ); const toggle = useCallback(async () => { - await toggleStarWiki(wikiId); + await toggleStarWiki(organizationId, wikiId); refetch(); triggerToggleStarWiki(); - }, [refetch, wikiId]); + }, [refetch, organizationId, wikiId]); return { data, error, toggle }; }; @@ -91,10 +99,10 @@ export const useWikiStarToggle = (wikiId) => { * 获取用户收藏的文档 * @returns */ -export const getStarDocuments = (cookie = null): Promise => { +export const getStarDocumentsInOrganization = (organizationId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.documents.method, - url: StarApiDefinition.documents.client(), + method: StarApiDefinition.getStarDocumentsInOrganization.method, + url: StarApiDefinition.getStarDocumentsInOrganization.client(organizationId), cookie, }); }; @@ -103,10 +111,14 @@ export const getStarDocuments = (cookie = null): Promise => { * 获取用户收藏的文档 * @returns */ -export const useStarDocuments = () => { - const { data, error, isLoading, refetch } = useQuery(StarApiDefinition.documents.client(), getStarDocuments, { - staleTime: 500, - }); +export const useStarDocumentsInOrganization = (organizationId) => { + const { data, error, isLoading, refetch } = useQuery( + StarApiDefinition.getStarDocumentsInOrganization.client(organizationId), + () => getStarDocumentsInOrganization(organizationId), + { + staleTime: 500, + } + ); useEffect(() => { event.on(TOGGLE_STAR_DOUCMENT, refetch); @@ -122,12 +134,13 @@ export const useStarDocuments = () => { * @param documentId * @returns */ -export const getDocumentIsStar = (wikiId, documentId, cookie = null): Promise => { +export const getDocumentIsStar = (organizationId, wikiId, documentId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.check.method, - url: StarApiDefinition.check.client(), + method: StarApiDefinition.isStared.method, + url: StarApiDefinition.isStared.client(), cookie, data: { + organizationId, wikiId, documentId, }, @@ -139,12 +152,13 @@ export const getDocumentIsStar = (wikiId, documentId, cookie = null): Promise => { +export const toggleDocumentStar = (organizationId, wikiId, documentId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.toggle.method, - url: StarApiDefinition.toggle.client(), + method: StarApiDefinition.toggleStar.method, + url: StarApiDefinition.toggleStar.client(), cookie, data: { + organizationId, wikiId, documentId, }, @@ -156,18 +170,18 @@ export const toggleDocumentStar = (wikiId, documentId, cookie = null): Promise) => { +export const useDocumentStarToggle = (organizationId, wikiId, documentId, options?: UseQueryOptions) => { const { data, error, refetch } = useQuery( - [StarApiDefinition.check.client(), wikiId, documentId], - () => getDocumentIsStar(wikiId, documentId), + [StarApiDefinition.isStared.client(), organizationId, wikiId, documentId], + () => getDocumentIsStar(organizationId, wikiId, documentId), options ); const toggle = useCallback(async () => { - await toggleDocumentStar(wikiId, documentId); + await toggleDocumentStar(organizationId, wikiId, documentId); refetch(); triggerToggleStarDocument(); - }, [refetch, wikiId, documentId]); + }, [refetch, organizationId, wikiId, documentId]); return { data, error, toggle }; }; @@ -176,12 +190,13 @@ export const useDocumentStarToggle = (wikiId, documentId, options?: UseQueryOpti * 获取知识库加星的文档 * @returns */ -export const getWikiStarDocuments = (wikiId, cookie = null): Promise => { +export const getStarDocumentsInWiki = (organizationId, wikiId, cookie = null): Promise => { return HttpClient.request({ - method: StarApiDefinition.wikiDocuments.method, - url: StarApiDefinition.wikiDocuments.client(), + method: StarApiDefinition.getStarDocumentsInWiki.method, + url: StarApiDefinition.getStarDocumentsInWiki.client(), cookie, params: { + organizationId, wikiId, }, }); @@ -191,10 +206,10 @@ export const getWikiStarDocuments = (wikiId, cookie = null): Promise { +export const useStarDocumentsInWiki = (organizationId, wikiId) => { const { data, error, isLoading, refetch } = useQuery( - [StarApiDefinition.wikiDocuments.client(), wikiId], - () => getWikiStarDocuments(wikiId), + [StarApiDefinition.getStarDocumentsInWiki.client(), organizationId, wikiId], + () => getStarDocumentsInWiki(organizationId, wikiId), { staleTime: 500, } diff --git a/packages/client/src/data/user.ts b/packages/client/src/data/user.ts index 59fa011..d52eb0d 100644 --- a/packages/client/src/data/user.ts +++ b/packages/client/src/data/user.ts @@ -109,7 +109,7 @@ export const useUser = () => { refetch(); setStorage('user', JSON.stringify(user)); user.token && setStorage('token,', user.token); - const next = router.query?.redirect || '/'; + const next = router.query?.redirect || '/app'; Router.replace(next as string); }); }, diff --git a/packages/client/src/data/wiki.ts b/packages/client/src/data/wiki.ts index 4954d8e..1c6665b 100644 --- a/packages/client/src/data/wiki.ts +++ b/packages/client/src/data/wiki.ts @@ -1,4 +1,4 @@ -import { IDocument, IUser, IWiki, IWikiUser, WikiApiDefinition } from '@think/domains'; +import { IAuth, IDocument, IUser, IWiki, IWikiUser, WikiApiDefinition } from '@think/domains'; import { event, REFRESH_TOCS } from 'event'; import { useCallback, useEffect, useState } from 'react'; import { useQuery } from 'react-query'; @@ -6,20 +6,16 @@ import { HttpClient } from 'services/http-client'; export type ICreateWiki = Pick; export type IUpdateWiki = Partial; -export type IWikiUserOpeateData = { - userName: Pick; - userRole: Pick; -}; export type IWikiWithIsMember = IWiki & { isMember: boolean }; /** * 获取用户所有知识库 * @returns */ -export const getAllWikis = (cookie = null): Promise<{ data: IWiki[]; total: number }> => { +export const getAllWikis = (organizationId, cookie = null): Promise<{ data: IWiki[]; total: number }> => { return HttpClient.request({ method: WikiApiDefinition.getAllWikis.method, - url: WikiApiDefinition.getAllWikis.client(), + url: WikiApiDefinition.getAllWikis.client(organizationId), cookie, }); }; @@ -28,8 +24,10 @@ export const getAllWikis = (cookie = null): Promise<{ data: IWiki[]; total: numb * 获取用户所有知识库 * @returns */ -export const useAllWikis = () => { - const { data, error, isLoading } = useQuery(WikiApiDefinition.getAllWikis.client(), getAllWikis); +export const useAllWikis = (organizationId) => { + const { data, error, isLoading } = useQuery(WikiApiDefinition.getAllWikis.client(organizationId), () => + getAllWikis(organizationId) + ); const list = (data && data.data) || []; const total = (data && data.total) || 0; return { data: list, total, error, loading: isLoading }; @@ -39,10 +37,10 @@ export const useAllWikis = () => { * 获取用户参与的知识库 * @returns */ -export const getJoinWikis = (cookie = null): Promise<{ data: IWiki[]; total: number }> => { +export const getJoinWikis = (organizationId, cookie = null): Promise<{ data: IWiki[]; total: number }> => { return HttpClient.request({ method: WikiApiDefinition.getJoinWikis.method, - url: WikiApiDefinition.getJoinWikis.client(), + url: WikiApiDefinition.getJoinWikis.client(organizationId), cookie, }); }; @@ -51,8 +49,10 @@ export const getJoinWikis = (cookie = null): Promise<{ data: IWiki[]; total: num * 获取用户参与的知识库 * @returns */ -export const useJoinWikis = () => { - const { data, error, isLoading } = useQuery(WikiApiDefinition.getJoinWikis.client(), getJoinWikis); +export const useJoinWikis = (organizationId) => { + const { data, error, isLoading } = useQuery(WikiApiDefinition.getJoinWikis.client(organizationId), () => + getJoinWikis(organizationId) + ); const list = (data && data.data) || []; const total = (data && data.total) || 0; @@ -63,10 +63,10 @@ export const useJoinWikis = () => { * 获取用户创建的知识库 * @returns */ -export const getOwnWikis = (cookie = null): Promise<{ data: IWiki[]; total: number }> => { +export const getOwnWikis = (organizationId, cookie = null): Promise<{ data: IWiki[]; total: number }> => { return HttpClient.request({ method: WikiApiDefinition.getOwnWikis.method, - url: WikiApiDefinition.getOwnWikis.client(), + url: WikiApiDefinition.getOwnWikis.client(organizationId), cookie, }); }; @@ -75,20 +75,25 @@ export const getOwnWikis = (cookie = null): Promise<{ data: IWiki[]; total: numb * 获取用户创建的知识库 * @returns */ -export const useOwnWikis = () => { - const { data, error, refetch } = useQuery(WikiApiDefinition.getOwnWikis.client(), getOwnWikis); +export const useOwnWikis = (organizationId) => { + const { data, error, refetch } = useQuery(WikiApiDefinition.getOwnWikis.client(organizationId), () => + getOwnWikis(organizationId) + ); const createWiki = useCallback( async (data: ICreateWiki) => { - const res = await HttpClient.request({ + const res = await HttpClient.request({ method: WikiApiDefinition.add.method, url: WikiApiDefinition.add.client(), - data, + data: { + organizationId, + ...data, + }, }); refetch(); return res; }, - [refetch] + [organizationId, refetch] ); /** @@ -311,7 +316,10 @@ export const useWikiDocuments = (wikiId) => { * @param cookie * @returns */ -export const getWikiMembers = (wikiId, cookie = null): Promise => { +export const getWikiMembers = ( + wikiId, + cookie = null +): Promise<{ data: Array<{ auth: IAuth; user: IUser }>; total: number }> => { return HttpClient.request({ method: WikiApiDefinition.getMemberById.method, url: WikiApiDefinition.getMemberById.client(wikiId), @@ -330,7 +338,7 @@ export const useWikiMembers = (wikiId) => { ); const addUser = useCallback( - async (data: IWikiUserOpeateData) => { + async (data) => { const ret = await HttpClient.request({ method: WikiApiDefinition.addMemberById.method, url: WikiApiDefinition.addMemberById.client(wikiId), @@ -343,7 +351,7 @@ export const useWikiMembers = (wikiId) => { ); const updateUser = useCallback( - async (data: IWikiUserOpeateData) => { + async (data) => { const ret = await HttpClient.request({ method: WikiApiDefinition.updateMemberById.method, url: WikiApiDefinition.updateMemberById.client(wikiId), @@ -356,7 +364,7 @@ export const useWikiMembers = (wikiId) => { ); const deleteUser = useCallback( - async (data: IWikiUserOpeateData) => { + async (data) => { const ret = await HttpClient.request({ method: WikiApiDefinition.deleteMemberById.method, url: WikiApiDefinition.deleteMemberById.client(wikiId), @@ -368,7 +376,7 @@ export const useWikiMembers = (wikiId) => { [refetch, wikiId] ); - return { users: data, loading: isLoading, error, addUser, updateUser, deleteUser }; + return { data, loading: isLoading, error, addUser, updateUser, deleteUser }; }; /** diff --git a/packages/client/src/event/index.ts b/packages/client/src/event/index.ts index 3a3627f..ff385f7 100644 --- a/packages/client/src/event/index.ts +++ b/packages/client/src/event/index.ts @@ -3,6 +3,7 @@ import { EventEmitter } from 'helpers/event-emitter'; export const event = new EventEmitter(); +export const REFRESH_ORGANIZATIONS = 'REFRESH_ORGANIZATIONS'; // 刷新组织列表 export const REFRESH_TOCS = `REFRESH_TOCS`; // 刷新知识库目录 export const CREATE_DOCUMENT = `CREATE_DOCUMENT`; export const TOGGLE_STAR_WIKI = `TOGGLE_STAR_WIKI`; // 收藏或取消收藏知识库 @@ -60,3 +61,7 @@ export const triggerToggleStarWiki = () => { export const triggerToggleStarDocument = () => { event.emit(TOGGLE_STAR_DOUCMENT); }; + +export const triggerRefreshOrganizations = () => { + event.emit(REFRESH_ORGANIZATIONS); +}; diff --git a/packages/client/src/hooks/use-router-query.tsx b/packages/client/src/hooks/use-router-query.tsx index 2374a20..6a6c849 100644 --- a/packages/client/src/hooks/use-router-query.tsx +++ b/packages/client/src/hooks/use-router-query.tsx @@ -2,5 +2,6 @@ import { Router, useRouter } from 'next/router'; export function useRouterQuery() { const router = useRouter(); + return router.query as T; } diff --git a/packages/client/src/layouts/app-double-column/index.module.scss b/packages/client/src/layouts/app-double-column/index.module.scss new file mode 100644 index 0000000..d766473 --- /dev/null +++ b/packages/client/src/layouts/app-double-column/index.module.scss @@ -0,0 +1,46 @@ +.wrap { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--semi-color-nav-bg); + + .contentWrap { + display: flex; + flex: 1; + flex-wrap: nowrap; + overflow: hidden; + + > div { + height: calc(100% - 60px) !important; + + :global { + .Pane2 { + height: 100%; + overflow: auto; + } + } + } + + .leftWrap { + position: relative; + border-right: 1px solid var(--semi-color-border); + + .leftContentWrap { + height: 100%; + } + + .collapseBtn { + position: absolute; + top: 1rem; + right: 0; + z-index: 100; + transform: translate(50%); + } + } + + .rightWrap { + height: 100%; + overflow: auto; + } + } +} diff --git a/packages/client/src/layouts/app-double-column/index.tsx b/packages/client/src/layouts/app-double-column/index.tsx new file mode 100644 index 0000000..39e2030 --- /dev/null +++ b/packages/client/src/layouts/app-double-column/index.tsx @@ -0,0 +1,49 @@ +import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons'; +import { Button, Layout as SemiLayout } from '@douyinfe/semi-ui'; +import cls from 'classnames'; +import { throttle } from 'helpers/throttle'; +import { useDragableWidth } from 'hooks/use-dragable-width'; +import React, { useMemo } from 'react'; +import SplitPane from 'react-split-pane'; + +import { AppRouterHeader } from '../app-router-header'; +import styles from './index.module.scss'; + +const { Sider, Content } = SemiLayout; + +interface IProps { + leftNode: React.ReactNode; + rightNode: React.ReactNode; +} + +export const AppDoubleColumnLayout: React.FC = ({ leftNode, rightNode }) => { + const { minWidth, maxWidth, width, isCollapsed, updateWidth, toggleCollapsed } = useDragableWidth(); + const debounceUpdate = useMemo(() => throttle(updateWidth, 200), [updateWidth]); + + return ( + + + + + +
- +
diff --git a/packages/client/src/layouts/router-header/wiki.tsx b/packages/client/src/layouts/app-router-header/wiki.tsx similarity index 82% rename from packages/client/src/layouts/router-header/wiki.tsx rename to packages/client/src/layouts/app-router-header/wiki.tsx index 431538a..d0002c1 100644 --- a/packages/client/src/layouts/router-header/wiki.tsx +++ b/packages/client/src/layouts/app-router-header/wiki.tsx @@ -3,7 +3,7 @@ import { Avatar, Dropdown, Modal, Space, Typography } from '@douyinfe/semi-ui'; import { DataRender } from 'components/data-render'; import { Empty } from 'components/empty'; import { WikiStar } from 'components/wiki/star'; -import { useStarWikis } from 'data/star'; +import { useStarWikisInOrganization } from 'data/star'; import { useWikiDetail } from 'data/wiki'; import Link from 'next/link'; import { useRouter } from 'next/router'; @@ -16,7 +16,12 @@ const { Text } = Typography; const WikiContent = () => { const { query } = useRouter(); - const { data: starWikis, loading, error, refresh: refreshStarWikis } = useStarWikis(); + const { + data: starWikis, + loading, + error, + refresh: refreshStarWikis, + } = useStarWikisInOrganization(query.organizationId); const { data: currentWiki } = useWikiDetail(query.wikiId); return ( @@ -31,8 +36,9 @@ const WikiContent = () => {
{
- +
@@ -84,8 +94,9 @@ const WikiContent = () => {
{
- +
@@ -130,7 +145,10 @@ const WikiContent = () => {
diff --git a/packages/client/src/layouts/app-single-column/index.module.scss b/packages/client/src/layouts/app-single-column/index.module.scss new file mode 100644 index 0000000..b6baeac --- /dev/null +++ b/packages/client/src/layouts/app-single-column/index.module.scss @@ -0,0 +1,11 @@ +.wrap { + display: flex; + flex-direction: column; + height: 100%; + background-color: var(--semi-color-nav-bg); + + .contentWrap { + flex: 1; + overflow: auto; + } +} diff --git a/packages/client/src/layouts/app-single-column/index.tsx b/packages/client/src/layouts/app-single-column/index.tsx new file mode 100644 index 0000000..a36d37d --- /dev/null +++ b/packages/client/src/layouts/app-single-column/index.tsx @@ -0,0 +1,25 @@ +import { Layout as SemiLayout } from '@douyinfe/semi-ui'; +import React from 'react'; + +import { AppRouterHeader } from '../app-router-header'; +import styles from './index.module.scss'; + +const { Content } = SemiLayout; + +export const AppSingleColumnLayout: React.FC = ({ children }) => { + return ( + + + + +
{children}
+
+
+
+ ); +}; diff --git a/packages/client/src/layouts/router-header/index.tsx b/packages/client/src/layouts/router-header/index.tsx index 2a27aef..d479f0d 100644 --- a/packages/client/src/layouts/router-header/index.tsx +++ b/packages/client/src/layouts/router-header/index.tsx @@ -1,22 +1,13 @@ -import { IconMenu } from '@douyinfe/semi-icons'; -import { Button, Dropdown, Layout as SemiLayout, Nav, Space, Typography } from '@douyinfe/semi-ui'; -import { LogoImage, LogoText } from 'components/logo'; +import { Layout as SemiLayout, Nav, Space } from '@douyinfe/semi-ui'; import { Message } from 'components/message'; -import { Search } from 'components/search'; +import { OrganizationPublicSwitcher } from 'components/organization/public-switcher'; import { Theme } from 'components/theme'; import { User } from 'components/user'; -import { WikiOrDocumentCreator } from 'components/wiki-or-document-creator'; import { IsOnMobile } from 'hooks/use-on-mobile'; -import { useToggle } from 'hooks/use-toggle'; -import { useWindowSize } from 'hooks/use-window-size'; import Router, { useRouter } from 'next/router'; import React from 'react'; -import { Recent, RecentModal } from './recent'; -import { Wiki, WikiModal } from './wiki'; - const { Header: SemiHeader } = SemiLayout; -const { Text } = Typography; const menus = [ { @@ -28,32 +19,6 @@ const menus = [ }); }, }, - { - itemKey: '/recent', - text: , - }, - { - itemKey: '/wiki', - text: , - }, - { - itemKey: '/star', - text: '星标', - onClick: () => { - Router.push({ - pathname: `/star`, - }); - }, - }, - { - itemKey: '/template', - text: '模板', - onClick: () => { - Router.push({ - pathname: `/template`, - }); - }, - }, { itemKey: '/find', text: '发现', @@ -68,10 +33,6 @@ const menus = [ export const RouterHeader: React.FC = () => { const { pathname } = useRouter(); const { isMobile } = IsOnMobile.useHook(); - const { width } = useWindowSize(); - const [dropdownVisible, toggleDropdownVisible] = useToggle(false); - const [recentModalVisible, toggleRecentModalVisible] = useToggle(false); - const [wikiModalVisible, toggleWikiModalVisible] = useToggle(false); return ( @@ -81,45 +42,11 @@ export const RouterHeader: React.FC = () => { style={{ overflow: 'auto' }} header={ - - - - - {menus.slice(0, 1).map((menu) => { - return ( - - {menu.text} - - ); - })} - 最近 - 知识库 - {menus.slice(3).map((menu) => { - return ( - - {menu.text} - - ); - })} - - } - > -
+ + +
+ ( + ( + + + + )} + /> + )} + error={error} + normalContent={() => ( + ( + + + + )} + emptyContent={} + /> + )} + /> + +
+ + ); +}; + +Page.getInitialProps = async (ctx) => { + const { organizationId } = ctx.query; + + const props = await serverPrefetcher(ctx, [ + { + url: StarApiDefinition.getStarWikisInOrganization.client(organizationId), + action: (cookie) => getStarWikisInOrganization(organizationId, cookie), + }, + { + url: DocumentApiDefinition.recent.client(organizationId), + action: (cookie) => getRecentVisitedDocuments(organizationId, cookie), + }, + ]); + return { ...props, organizationId } as any; +}; + +export default Page; diff --git a/packages/client/src/pages/app/org/[organizationId]/setting/index.tsx b/packages/client/src/pages/app/org/[organizationId]/setting/index.tsx new file mode 100644 index 0000000..f5fc3a1 --- /dev/null +++ b/packages/client/src/pages/app/org/[organizationId]/setting/index.tsx @@ -0,0 +1,43 @@ +import { IOrganization } from '@think/domains'; +import { OrganizationSetting } from 'components/organization/setting'; +import { AppSingleColumnLayout } from 'layouts/app-single-column'; +import { NextPage } from 'next'; +import Router, { useRouter } from 'next/router'; +import React, { useCallback } from 'react'; + +interface IProps { + organizationId: IOrganization['id']; +} + +const Page: NextPage = ({ organizationId }) => { + const { query = {} } = useRouter(); + const { tab = 'base' } = query as { + tab?: string; + }; + + const navigate = useCallback( + (tab = 'base') => { + console.log(tab); + Router.push({ + pathname: `/app/org/[organizationId]/setting`, + query: { organizationId, tab }, + }); + }, + [organizationId] + ); + + return ( + +
+ +
+
+ ); +}; + +Page.getInitialProps = async (ctx) => { + const { organizationId } = ctx.query; + return { organizationId } as IProps; +}; + +export default Page; diff --git a/packages/client/src/pages/wiki/index.module.scss b/packages/client/src/pages/app/org/[organizationId]/star/index.module.scss similarity index 100% rename from packages/client/src/pages/wiki/index.module.scss rename to packages/client/src/pages/app/org/[organizationId]/star/index.module.scss diff --git a/packages/client/src/pages/star/index.tsx b/packages/client/src/pages/app/org/[organizationId]/star/index.tsx similarity index 72% rename from packages/client/src/pages/star/index.tsx rename to packages/client/src/pages/app/org/[organizationId]/star/index.tsx index 0cfef3e..a6586cc 100644 --- a/packages/client/src/pages/star/index.tsx +++ b/packages/client/src/pages/app/org/[organizationId]/star/index.tsx @@ -5,8 +5,14 @@ import { DocumentCard, DocumentCardPlaceholder } from 'components/document/card' import { Empty } from 'components/empty'; import { Seo } from 'components/seo'; import { WikiCard, WikiCardPlaceholder } from 'components/wiki/card'; -import { getStarDocuments, getStarWikis, useStarDocuments, useStarWikis } from 'data/star'; -import { SingleColumnLayout } from 'layouts/single-column'; +import { + getStarDocumentsInOrganization, + getStarWikisInOrganization, + useStarDocumentsInOrganization, + useStarWikisInOrganization, +} from 'data/star'; +import { useRouterQuery } from 'hooks/use-router-query'; +import { AppSingleColumnLayout } from 'layouts/app-single-column'; import type { NextPage } from 'next'; import React from 'react'; import { serverPrefetcher } from 'services/server-prefetcher'; @@ -25,7 +31,8 @@ const grid = { }; const StarDocs = () => { - const { data: docs, loading, error } = useStarDocuments(); + const { organizationId } = useRouterQuery<{ organizationId: string }>(); + const { data: docs, loading, error } = useStarDocumentsInOrganization(organizationId); return ( { }; const StarWikis = () => { - const { data, loading, error } = useStarWikis(); + const { organizationId } = useRouterQuery<{ organizationId: string }>(); + const { data, loading, error } = useStarWikisInOrganization(organizationId); return ( { const Page: NextPage = () => { return ( - +
@@ -111,14 +119,22 @@ const Page: NextPage = () => {
-
+ ); }; Page.getInitialProps = async (ctx) => { + const { organizationId } = ctx.query; + const props = await serverPrefetcher(ctx, [ - { url: StarApiDefinition.wikis.client(), action: (cookie) => getStarWikis(cookie) }, - { url: StarApiDefinition.documents.client(), action: (cookie) => getStarDocuments(cookie) }, + { + url: StarApiDefinition.getStarWikisInOrganization.client(organizationId), + action: (cookie) => getStarWikisInOrganization(organizationId, cookie), + }, + { + url: StarApiDefinition.getStarDocumentsInOrganization.client(organizationId), + action: (cookie) => getStarDocumentsInOrganization(organizationId, cookie), + }, ]); return props; }; diff --git a/packages/client/src/pages/wiki/[wikiId]/document/[documentId]/edit/index.tsx b/packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/edit/index.tsx similarity index 100% rename from packages/client/src/pages/wiki/[wikiId]/document/[documentId]/edit/index.tsx rename to packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/edit/index.tsx diff --git a/packages/client/src/pages/wiki/[wikiId]/document/[documentId]/index.tsx b/packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/index.tsx similarity index 92% rename from packages/client/src/pages/wiki/[wikiId]/document/[documentId]/index.tsx rename to packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/index.tsx index 80453c5..478e473 100644 --- a/packages/client/src/pages/wiki/[wikiId]/document/[documentId]/index.tsx +++ b/packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/doc/[documentId]/index.tsx @@ -3,7 +3,7 @@ import { DocumentReader } from 'components/document/reader'; import { WikiTocs } from 'components/wiki/tocs'; import { getDocumentDetail } from 'data/document'; import { getWikiTocs } from 'data/wiki'; -import { DoubleColumnLayout } from 'layouts/double-column'; +import { AppDoubleColumnLayout } from 'layouts/app-double-column'; import { NextPage } from 'next'; import React from 'react'; import { serverPrefetcher } from 'services/server-prefetcher'; @@ -15,7 +15,7 @@ interface IProps { const Page: NextPage = ({ wikiId, documentId }) => { return ( - } rightNode={} /> diff --git a/packages/client/src/pages/wiki/[wikiId]/index.module.scss b/packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/index.module.scss similarity index 100% rename from packages/client/src/pages/wiki/[wikiId]/index.module.scss rename to packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/index.module.scss diff --git a/packages/client/src/pages/wiki/[wikiId]/index.tsx b/packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/index.tsx similarity index 94% rename from packages/client/src/pages/wiki/[wikiId]/index.tsx rename to packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/index.tsx index 61de53e..623260e 100644 --- a/packages/client/src/pages/wiki/[wikiId]/index.tsx +++ b/packages/client/src/pages/app/org/[organizationId]/wiki/[wikiId]/index.tsx @@ -4,7 +4,7 @@ import { DataRender } from 'components/data-render'; import { DocumentReader } from 'components/document/reader'; import { WikiTocs } from 'components/wiki/tocs'; import { getWikiDetail, getWikiTocs, useWikiDetail } from 'data/wiki'; -import { DoubleColumnLayout } from 'layouts/double-column'; +import { AppDoubleColumnLayout } from 'layouts/app-double-column'; import { NextPage } from 'next'; import React from 'react'; import { serverPrefetcher } from 'services/server-prefetcher'; @@ -17,7 +17,7 @@ const Page: NextPage = ({ wikiId }) => { const { data: wiki, loading, error } = useWikiDetail(wikiId); return ( - } rightNode={ = ({ wikiId }) => { (tab = 'base') => { return () => { Router.push({ - pathname: `/wiki/${wikiId}/setting`, - query: { tab }, + pathname: `/app/org/[organizationId]/wiki/[wikiId]/setting`, + query: { organizationId: query.organizationId, wikiId, tab }, }); }; }, - [wikiId] + [query, wikiId] ); return ( - } rightNode={
@@ -56,4 +56,5 @@ Page.getInitialProps = async (ctx) => { ]); return { ...res, wikiId } as IProps; }; + export default Page; diff --git a/packages/client/src/pages/app/org/[organizationId]/wiki/index.module.scss b/packages/client/src/pages/app/org/[organizationId]/wiki/index.module.scss new file mode 100644 index 0000000..4708e1b --- /dev/null +++ b/packages/client/src/pages/app/org/[organizationId]/wiki/index.module.scss @@ -0,0 +1,12 @@ +.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 { + display: flex; + justify-content: space-between; +} diff --git a/packages/client/src/pages/wiki/index.tsx b/packages/client/src/pages/app/org/[organizationId]/wiki/index.tsx similarity index 76% rename from packages/client/src/pages/wiki/index.tsx rename to packages/client/src/pages/app/org/[organizationId]/wiki/index.tsx index 30bca78..a831ab6 100644 --- a/packages/client/src/pages/wiki/index.tsx +++ b/packages/client/src/pages/app/org/[organizationId]/wiki/index.tsx @@ -6,8 +6,9 @@ import { Seo } from 'components/seo'; import { WikiCard, WikiCardPlaceholder } from 'components/wiki/card'; import { WikiCreator } from 'components/wiki-creator'; import { getAllWikis, getJoinWikis, getOwnWikis, useAllWikis, useJoinWikis, useOwnWikis } from 'data/wiki'; +import { useRouterQuery } from 'hooks/use-router-query'; import { CreateWikiIllustration } from 'illustrations/create-wiki'; -import { SingleColumnLayout } from 'layouts/single-column'; +import { AppSingleColumnLayout } from 'layouts/app-single-column'; import type { NextPage } from 'next'; import React from 'react'; import { serverPrefetcher } from 'services/server-prefetcher'; @@ -24,7 +25,8 @@ const grid = { const { Title } = Typography; const Wikis = ({ hook }) => { - const { data, loading, error } = hook(); + const { organizationId } = useRouterQuery<{ organizationId: string }>(); + const { data, loading, error } = hook(organizationId); return ( { const Page: NextPage = () => { return ( - +
@@ -77,15 +79,17 @@ const Page: NextPage = () => { </TabPane> </Tabs> </div> - </SingleColumnLayout> + </AppSingleColumnLayout> ); }; Page.getInitialProps = async (ctx) => { + const { orgId: organizationId } = ctx.query; + const props = await serverPrefetcher(ctx, [ - { url: WikiApiDefinition.getAllWikis.client(), action: (cookie) => getAllWikis(cookie) }, - { url: WikiApiDefinition.getJoinWikis.client(), action: (cookie) => getJoinWikis(cookie) }, - { url: WikiApiDefinition.getOwnWikis.client(), action: (cookie) => getOwnWikis(cookie) }, + { url: WikiApiDefinition.getAllWikis.client(organizationId), action: (cookie) => getAllWikis(cookie) }, + { url: WikiApiDefinition.getJoinWikis.client(organizationId), action: (cookie) => getJoinWikis(cookie) }, + { url: WikiApiDefinition.getOwnWikis.client(organizationId), action: (cookie) => getOwnWikis(cookie) }, ]); return props; }; diff --git a/packages/client/src/pages/app/org/create/index.module.scss b/packages/client/src/pages/app/org/create/index.module.scss new file mode 100644 index 0000000..c58f36b --- /dev/null +++ b/packages/client/src/pages/app/org/create/index.module.scss @@ -0,0 +1,9 @@ +.cover { + margin-right: 12px; +} + +@media (max-width: 768px) { + .cover { + width: 100%; + } +} diff --git a/packages/client/src/pages/app/org/create/index.tsx b/packages/client/src/pages/app/org/create/index.tsx new file mode 100644 index 0000000..e81a790 --- /dev/null +++ b/packages/client/src/pages/app/org/create/index.tsx @@ -0,0 +1,119 @@ +import { Avatar, Button, Form, Typography } from '@douyinfe/semi-ui'; +import { FormApi } from '@douyinfe/semi-ui/lib/es/form'; +import { ORGANIZATION_LOGOS } from '@think/constants'; +import { ImageUploader } from 'components/image-uploader'; +import { useCreateOrganization } from 'data/organization'; +import { useToggle } from 'hooks/use-toggle'; +import { SingleColumnLayout } from 'layouts/single-column'; +import Router from 'next/router'; +import { useCallback, useRef, useState } from 'react'; + +import styles from './index.module.scss'; + +const images = [ + { + key: 'placeholers', + title: '图库', + images: ORGANIZATION_LOGOS, + }, +]; + +const { Title } = Typography; + +const Page: React.FC = () => { + const $form = useRef<FormApi>(); + const [changed, toggleChanged] = useToggle(false); + const [currentCover, setCurrentCover] = useState(''); + const { create, loading } = useCreateOrganization(); + + const setCover = useCallback((url) => { + $form.current.setValue('logo', url); + setCurrentCover(url); + }, []); + + const onFormChange = useCallback(() => { + toggleChanged(true); + }, [toggleChanged]); + + const onSubmit = useCallback(() => { + $form.current.validate().then((values) => { + create(values).then((res) => { + Router.push({ + pathname: `/app/org/[organizationId]`, + query: { organizationId: res.id }, + }); + }); + }); + }, [create]); + + return ( + <SingleColumnLayout> + <div className="container"> + <div style={{ marginBottom: 24 }}> + <Title heading={3} style={{ margin: '8px 0' }}> + 新建组织 + +
+
($form.current = formApi)} + onChange={onFormChange} + onSubmit={onSubmit} + > + +
+
+ +
+ + + +
+
+ + + + + +
+ +
+ +
+ + ); +}; + +export default Page; diff --git a/packages/client/src/pages/find/index.tsx b/packages/client/src/pages/find/index.tsx index 73ec3ef..14d21cc 100644 --- a/packages/client/src/pages/find/index.tsx +++ b/packages/client/src/pages/find/index.tsx @@ -75,7 +75,7 @@ const Page: NextPage = () => { Page.getInitialProps = async (ctx) => { const props = await serverPrefetcher(ctx, [ - { url: [WikiApiDefinition.getAllWikis.client(), 1], action: (cookie) => getAllPublicWikis(1, cookie) }, + { url: [WikiApiDefinition.getPublicWikis.client(), 1], action: (cookie) => getAllPublicWikis(1, cookie) }, ]); return props; }; diff --git a/packages/client/src/pages/index.tsx b/packages/client/src/pages/index.tsx index 5b809cf..f8671e9 100644 --- a/packages/client/src/pages/index.tsx +++ b/packages/client/src/pages/index.tsx @@ -1,184 +1,39 @@ -import { Avatar, Button, List, Table, Typography } from '@douyinfe/semi-ui'; -import { DocumentApiDefinition, IDocument, StarApiDefinition } from '@think/domains'; -import { DataRender } from 'components/data-render'; -import { DocumentActions } from 'components/document/actions'; -import { Empty } from 'components/empty'; -import { LocaleTime } from 'components/locale-time'; +import { Button, Typography } from '@douyinfe/semi-ui'; import { Seo } from 'components/seo'; -import { WikiCreator } from 'components/wiki/create'; -import { WikiPinCard, WikiPinCardPlaceholder } from 'components/wiki/pin-card'; -import { getRecentVisitedDocuments, useRecentDocuments } from 'data/document'; -import { getStarWikis, useStarWikis } from 'data/star'; -import { useToggle } from 'hooks/use-toggle'; import { SingleColumnLayout } from 'layouts/single-column'; import type { NextPage } from 'next'; -import Link from 'next/link'; -import React, { useEffect, useMemo } from 'react'; -import { serverPrefetcher } from 'services/server-prefetcher'; - -import styles from './index.module.scss'; +import Router from 'next/router'; +import React, { useCallback } from 'react'; const { Title } = Typography; -const { Column } = Table; - -const grid = { - gutter: 16, - xs: 24, - sm: 12, - md: 12, - lg: 8, - xl: 8, -}; - -const RecentDocs = () => { - const { data, error, loading, refresh } = useRecentDocuments(); - - const columns = useMemo( - () => [ - { - return ( - - {document.title} - - ); - }} - />, - , - { - return ( - - - {createUser.name.slice(0, 1)} - - {createUser.name} - - ); - }} - />, - } - />, - ( - - )} - />, - ], - [refresh] - ); - - useEffect(() => { - refresh(); - }, [refresh]); - - return ( - <> - - 最近访问 - - - {columns} - - } - error={error} - normalContent={() => - data && data.length ? ( - - {columns} -
- ) : ( - - ) - } - /> - - ); -}; const Page: NextPage = () => { - const [visible, toggleVisible] = useToggle(false); - const { data: staredWikis, loading, error, refresh } = useStarWikis(); + const gotoApp = useCallback(() => { + Router.push(`/app`); + }, []); return (
-
+
- 快捷访问 + 主页 - <> - - -
- ( - ( - - - - )} - /> - )} - error={error} - normalContent={() => ( - ( - - - - )} - emptyContent={} - /> - )} - /> - +
+ +
); }; -Page.getInitialProps = async (ctx) => { - const props = await serverPrefetcher(ctx, [ - { url: StarApiDefinition.wikis.client(), action: (cookie) => getStarWikis(cookie) }, - { url: DocumentApiDefinition.recent.client(), action: (cookie) => getRecentVisitedDocuments(cookie) }, - ]); - return props; -}; - export default Page; diff --git a/packages/client/src/services/http-client.ts b/packages/client/src/services/http-client.ts index e882fbb..55eaeff 100644 --- a/packages/client/src/services/http-client.ts +++ b/packages/client/src/services/http-client.ts @@ -10,7 +10,7 @@ interface AxiosInstance extends Axios { export const HttpClient = axios.create({ baseURL: process.env.SERVER_API_URL, - timeout: 10 * 60 * 1000, + timeout: 1 * 3 * 1000, withCredentials: true, }) as AxiosInstance; diff --git a/packages/constants/lib/index.d.ts b/packages/constants/lib/index.d.ts index b44762c..cdf71f4 100644 --- a/packages/constants/lib/index.d.ts +++ b/packages/constants/lib/index.d.ts @@ -1,6 +1,7 @@ /// export declare const DEFAULT_WIKI_AVATAR = "https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default0-96.png"; export declare const WIKI_AVATARS: string[]; +export declare const ORGANIZATION_LOGOS: string[]; export declare const EMPTY_DOCUMNENT: { content: string; state: Buffer; diff --git a/packages/constants/lib/index.js b/packages/constants/lib/index.js index 0b1ab14..a4484c4 100644 --- a/packages/constants/lib/index.js +++ b/packages/constants/lib/index.js @@ -1,6 +1,6 @@ "use strict"; exports.__esModule = true; -exports.DOCUMENT_COVERS = exports.EMPTY_DOCUMNENT = exports.WIKI_AVATARS = exports.DEFAULT_WIKI_AVATAR = void 0; +exports.DOCUMENT_COVERS = exports.EMPTY_DOCUMNENT = exports.ORGANIZATION_LOGOS = exports.WIKI_AVATARS = exports.DEFAULT_WIKI_AVATAR = void 0; exports.DEFAULT_WIKI_AVATAR = 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default0-96.png'; exports.WIKI_AVATARS = [ exports.DEFAULT_WIKI_AVATAR, @@ -16,6 +16,20 @@ exports.WIKI_AVATARS = [ 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default17-96.png', 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default18-96.png', ]; +exports.ORGANIZATION_LOGOS = [ + exports.DEFAULT_WIKI_AVATAR, + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default2-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default7-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default8-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default14-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default21-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default23-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default1-96%20(1).png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default4-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default12-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default17-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default18-96.png', +]; exports.EMPTY_DOCUMNENT = { content: JSON.stringify({ "default": { diff --git a/packages/constants/src/index.ts b/packages/constants/src/index.ts index 4f164ec..66d0a8c 100644 --- a/packages/constants/src/index.ts +++ b/packages/constants/src/index.ts @@ -15,6 +15,21 @@ export const WIKI_AVATARS = [ 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default18-96.png', ]; +export const ORGANIZATION_LOGOS = [ + DEFAULT_WIKI_AVATAR, + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default2-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default7-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default8-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default14-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default21-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default23-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default1-96%20(1).png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default4-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default12-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default17-96.png', + 'https://wipi.oss-cn-shanghai.aliyuncs.com/2022-02-01/default18-96.png', +]; + export const EMPTY_DOCUMNENT = { content: JSON.stringify({ default: { diff --git a/packages/domains/lib/api/document.d.ts b/packages/domains/lib/api/document.d.ts index 6bcfecd..2e5ce1a 100644 --- a/packages/domains/lib/api/document.d.ts +++ b/packages/domains/lib/api/document.d.ts @@ -5,16 +5,16 @@ export declare const DocumentApiDefinition: { */ search: { method: "get"; - server: "search"; - client: () => string; + server: "/:organizationId/search"; + client: (organizationId: any) => string; }; /** * 获取用户最近访问的文档 */ recent: { method: "get"; - server: "recent"; - client: () => string; + server: "/:organizationId/recent"; + client: (organizationId: any) => string; }; /** * 新建文档 diff --git a/packages/domains/lib/api/document.js b/packages/domains/lib/api/document.js index d1a535f..5a32ce7 100644 --- a/packages/domains/lib/api/document.js +++ b/packages/domains/lib/api/document.js @@ -7,16 +7,16 @@ exports.DocumentApiDefinition = { */ search: { method: 'get', - server: 'search', - client: function () { return '/document/search'; } + server: '/:organizationId/search', + client: function (organizationId) { return "/document/".concat(organizationId, "/search"); } }, /** * 获取用户最近访问的文档 */ recent: { method: 'get', - server: 'recent', - client: function () { return '/document/recent'; } + server: '/:organizationId/recent', + client: function (organizationId) { return "/document/".concat(organizationId, "/recent"); } }, /** * 新建文档 diff --git a/packages/domains/lib/api/index.d.ts b/packages/domains/lib/api/index.d.ts index d53ffd4..4e35e4a 100644 --- a/packages/domains/lib/api/index.d.ts +++ b/packages/domains/lib/api/index.d.ts @@ -6,3 +6,4 @@ export * from './message'; export * from './template'; export * from './comment'; export * from './star'; +export * from './organization'; diff --git a/packages/domains/lib/api/index.js b/packages/domains/lib/api/index.js index 57e2c6a..156503f 100644 --- a/packages/domains/lib/api/index.js +++ b/packages/domains/lib/api/index.js @@ -18,3 +18,4 @@ __exportStar(require("./message"), exports); __exportStar(require("./template"), exports); __exportStar(require("./comment"), exports); __exportStar(require("./star"), exports); +__exportStar(require("./organization"), exports); diff --git a/packages/domains/lib/api/organization.d.ts b/packages/domains/lib/api/organization.d.ts new file mode 100644 index 0000000..52089ac --- /dev/null +++ b/packages/domains/lib/api/organization.d.ts @@ -0,0 +1,75 @@ +import { IOrganization } from '../models'; +export declare const OrganizationApiDefinition: { + /** + * 创建 + */ + createOrganization: { + method: "post"; + server: "/create"; + client: () => string; + }; + /** + * 获取用户个人组织 + */ + getPersonalOrganization: { + method: "get"; + server: "/personal"; + client: () => string; + }; + /** + * 获取用户除个人组织外其他组织 + */ + getUserOrganizations: { + method: "get"; + server: "/list/personal"; + client: () => string; + }; + /** + * 获取组织详情 + */ + getOrganizationDetail: { + method: "get"; + server: "/detail/:id"; + client: (id: IOrganization['id']) => string; + }; + /** + * 更新组织基本信息 + */ + updateOrganization: { + method: "post"; + server: "/update/:id"; + client: (id: IOrganization['id']) => string; + }; + /** + * 获取组织成员 + */ + getMembers: { + method: "get"; + server: "/member/:id"; + client: (id: IOrganization['id']) => string; + }; + /** + * 添加组织成员 + */ + addMemberById: { + method: "post"; + server: "member/:id/add"; + client: (id: IOrganization['id']) => string; + }; + /** + * 更新组织成员 + */ + updateMemberById: { + method: "patch"; + server: "member/:id/update"; + client: (id: IOrganization['id']) => string; + }; + /** + * 删除组织成员 + */ + deleteMemberById: { + method: "delete"; + server: "member/:id/delete"; + client: (id: IOrganization['id']) => string; + }; +}; diff --git a/packages/domains/lib/api/organization.js b/packages/domains/lib/api/organization.js new file mode 100644 index 0000000..b56c632 --- /dev/null +++ b/packages/domains/lib/api/organization.js @@ -0,0 +1,77 @@ +"use strict"; +exports.__esModule = true; +exports.OrganizationApiDefinition = void 0; +exports.OrganizationApiDefinition = { + /** + * 创建 + */ + createOrganization: { + method: 'post', + server: '/create', + client: function () { return '/organization/create'; } + }, + /** + * 获取用户个人组织 + */ + getPersonalOrganization: { + method: 'get', + server: '/personal', + client: function () { return '/organization/personal'; } + }, + /** + * 获取用户除个人组织外其他组织 + */ + getUserOrganizations: { + method: 'get', + server: '/list/personal', + client: function () { return '/organization/list/personal'; } + }, + /** + * 获取组织详情 + */ + getOrganizationDetail: { + method: 'get', + server: '/detail/:id', + client: function (id) { return "/organization/detail/".concat(id); } + }, + /** + * 更新组织基本信息 + */ + updateOrganization: { + method: 'post', + server: '/update/:id', + client: function (id) { return "/organization/update/".concat(id); } + }, + /** + * 获取组织成员 + */ + getMembers: { + method: 'get', + server: '/member/:id', + client: function (id) { return "/organization/member/".concat(id); } + }, + /** + * 添加组织成员 + */ + addMemberById: { + method: 'post', + server: 'member/:id/add', + client: function (id) { return "/organization/member/".concat(id, "/add"); } + }, + /** + * 更新组织成员 + */ + updateMemberById: { + method: 'patch', + server: 'member/:id/update', + client: function (id) { return "/organization/member/".concat(id, "/update"); } + }, + /** + * 删除组织成员 + */ + deleteMemberById: { + method: 'delete', + server: 'member/:id/delete', + client: function (id) { return "/organization/member/".concat(id, "/delete"); } + } +}; diff --git a/packages/domains/lib/api/star.d.ts b/packages/domains/lib/api/star.d.ts index bc698aa..d9e2e85 100644 --- a/packages/domains/lib/api/star.d.ts +++ b/packages/domains/lib/api/star.d.ts @@ -1,8 +1,8 @@ export declare const StarApiDefinition: { /** - * 收藏(或取消收藏) + * 加星或取消 */ - toggle: { + toggleStar: { method: "post"; server: "toggle"; client: () => string; @@ -10,33 +10,33 @@ export declare const StarApiDefinition: { /** * 检测是否收藏 */ - check: { + isStared: { method: "post"; - server: "check"; + server: "isStared"; client: () => string; }; /** - * 获取收藏的知识库 + * 获取组织内加星的知识库 */ - wikis: { + getStarWikisInOrganization: { method: "get"; - server: "wikis"; - client: () => string; + server: "/:organizationId/wikis"; + client: (organizationId: any) => string; }; /** * 获取知识库内加星的文章 */ - wikiDocuments: { + getStarDocumentsInWiki: { method: "get"; - server: "wiki/documents"; + server: "/wiki/documents"; client: () => string; }; /** - * 获取收藏的文档 + * 获取组织内加星的文档 */ - documents: { + getStarDocumentsInOrganization: { method: "get"; - server: "documents"; - client: () => string; + server: "/:organizationId/documents"; + client: (organizationId: any) => string; }; }; diff --git a/packages/domains/lib/api/star.js b/packages/domains/lib/api/star.js index 21d7ea7..55342f5 100644 --- a/packages/domains/lib/api/star.js +++ b/packages/domains/lib/api/star.js @@ -3,9 +3,9 @@ exports.__esModule = true; exports.StarApiDefinition = void 0; exports.StarApiDefinition = { /** - * 收藏(或取消收藏) + * 加星或取消 */ - toggle: { + toggleStar: { method: 'post', server: 'toggle', client: function () { return '/star/toggle'; } @@ -13,33 +13,33 @@ exports.StarApiDefinition = { /** * 检测是否收藏 */ - check: { + isStared: { method: 'post', - server: 'check', - client: function () { return '/star/check'; } + server: 'isStared', + client: function () { return '/star/isStared'; } }, /** - * 获取收藏的知识库 + * 获取组织内加星的知识库 */ - wikis: { + getStarWikisInOrganization: { method: 'get', - server: 'wikis', - client: function () { return '/star/wikis'; } + server: '/:organizationId/wikis', + client: function (organizationId) { return "/star/".concat(organizationId, "/wikis"); } }, /** * 获取知识库内加星的文章 */ - wikiDocuments: { + getStarDocumentsInWiki: { method: 'get', - server: 'wiki/documents', - client: function () { return '/star/wiki/documents'; } + server: '/wiki/documents', + client: function () { return "/star/wiki/documents"; } }, /** - * 获取收藏的文档 + * 获取组织内加星的文档 */ - documents: { + getStarDocumentsInOrganization: { method: 'get', - server: 'documents', - client: function () { return '/star/documents'; } + server: '/:organizationId/documents', + client: function (organizationId) { return "/star/".concat(organizationId, "/documents"); } } }; diff --git a/packages/domains/lib/api/wiki.d.ts b/packages/domains/lib/api/wiki.d.ts index a3dc27e..c7349a8 100644 --- a/packages/domains/lib/api/wiki.d.ts +++ b/packages/domains/lib/api/wiki.d.ts @@ -5,24 +5,24 @@ export declare const WikiApiDefinition: { */ getAllWikis: { method: "get"; - server: "list/all"; - client: () => string; + server: "list/all/:organizationId"; + client: (organizationId: any) => string; }; /** * 获取用户创建的知识库 */ getOwnWikis: { method: "get"; - server: "list/own"; - client: () => string; + server: "list/own/:organizationId"; + client: (organizationId: any) => string; }; /** * 获取用户参与的知识库 */ getJoinWikis: { method: "get"; - server: "list/join"; - client: () => string; + server: "list/join/:organizationId"; + client: (organizationId: any) => string; }; /** * 新建知识库 diff --git a/packages/domains/lib/api/wiki.js b/packages/domains/lib/api/wiki.js index 5475a61..95e4faa 100644 --- a/packages/domains/lib/api/wiki.js +++ b/packages/domains/lib/api/wiki.js @@ -7,24 +7,24 @@ exports.WikiApiDefinition = { */ getAllWikis: { method: 'get', - server: 'list/all', - client: function () { return '/wiki/list/all'; } + server: 'list/all/:organizationId', + client: function (organizationId) { return "/wiki/list/all/".concat(organizationId); } }, /** * 获取用户创建的知识库 */ getOwnWikis: { method: 'get', - server: 'list/own', - client: function () { return '/wiki/list/own'; } + server: 'list/own/:organizationId', + client: function (organizationId) { return "/wiki/list/own/".concat(organizationId); } }, /** * 获取用户参与的知识库 */ getJoinWikis: { method: 'get', - server: 'list/join', - client: function () { return '/wiki/list/join'; } + server: 'list/join/:organizationId', + client: function (organizationId) { return "/wiki/list/join/".concat(organizationId); } }, /** * 新建知识库 diff --git a/packages/domains/lib/models/auth.d.ts b/packages/domains/lib/models/auth.d.ts new file mode 100644 index 0000000..aa3b370 --- /dev/null +++ b/packages/domains/lib/models/auth.d.ts @@ -0,0 +1,26 @@ +import { IOrganization } from './organization'; +export declare enum AuthEnum { + creator = "creator", + admin = "admin", + member = "member", + noAccess = "noAccess" +} +export declare const AuthEnumTextMap: { + creator: string; + admin: string; + member: string; + noAccess: string; +}; +export declare const AuthEnumArray: { + label: any; + value: string; +}[]; +export interface IAuth { + id: string; + type: AuthEnum; + organizationId: IOrganization['id']; + wikiId?: string; + documentId?: string; + createdAt: string; + updatedAt: string; +} diff --git a/packages/domains/lib/models/auth.js b/packages/domains/lib/models/auth.js new file mode 100644 index 0000000..67c1283 --- /dev/null +++ b/packages/domains/lib/models/auth.js @@ -0,0 +1,21 @@ +"use strict"; +var _a; +exports.__esModule = true; +exports.AuthEnumArray = exports.AuthEnumTextMap = exports.AuthEnum = void 0; +var AuthEnum; +(function (AuthEnum) { + AuthEnum["creator"] = "creator"; + AuthEnum["admin"] = "admin"; + AuthEnum["member"] = "member"; + AuthEnum["noAccess"] = "noAccess"; +})(AuthEnum = exports.AuthEnum || (exports.AuthEnum = {})); +exports.AuthEnumTextMap = (_a = {}, + _a[AuthEnum.creator] = '创建者', + _a[AuthEnum.admin] = '管理员', + _a[AuthEnum.member] = '成员', + _a[AuthEnum.noAccess] = '无权限', + _a); +exports.AuthEnumArray = Object.keys(exports.AuthEnumTextMap).map(function (value) { return ({ + label: exports.AuthEnumTextMap[value], + value: value +}); }); diff --git a/packages/domains/lib/models/document.d.ts b/packages/domains/lib/models/document.d.ts index b85e92c..4c10d01 100644 --- a/packages/domains/lib/models/document.d.ts +++ b/packages/domains/lib/models/document.d.ts @@ -1,3 +1,4 @@ +import { IOrganization } from './organization'; import { IUser } from './user'; import { IWiki } from './wiki'; /** @@ -12,6 +13,7 @@ export declare enum DocumentStatus { */ export interface IDocument { id: string; + organizationId: IOrganization['id']; wikiId: IWiki['id']; isWikiHome: boolean; createUserId: IUser['id']; diff --git a/packages/domains/lib/models/index.d.ts b/packages/domains/lib/models/index.d.ts index 88796f1..c2dd944 100644 --- a/packages/domains/lib/models/index.d.ts +++ b/packages/domains/lib/models/index.d.ts @@ -6,3 +6,5 @@ export * from './template'; export * from './comment'; export * from './pagination'; export * from './system'; +export * from './organization'; +export * from './auth'; diff --git a/packages/domains/lib/models/index.js b/packages/domains/lib/models/index.js index ef200cb..f96093b 100644 --- a/packages/domains/lib/models/index.js +++ b/packages/domains/lib/models/index.js @@ -18,3 +18,5 @@ __exportStar(require("./template"), exports); __exportStar(require("./comment"), exports); __exportStar(require("./pagination"), exports); __exportStar(require("./system"), exports); +__exportStar(require("./organization"), exports); +__exportStar(require("./auth"), exports); diff --git a/packages/domains/lib/models/message.d.ts b/packages/domains/lib/models/message.d.ts index 9823949..8007911 100644 --- a/packages/domains/lib/models/message.d.ts +++ b/packages/domains/lib/models/message.d.ts @@ -1,4 +1,7 @@ import { IUser } from './user'; +import { IOrganization } from './organization'; +import { IWiki } from './wiki'; +import { IDocument } from './document'; /** * 消息数据定义 */ @@ -12,3 +15,10 @@ export interface IMessage { createdAt: Date; updatedAt: Date; } +declare type MessageType = 'toOrganization' | 'toWiki' | 'toDocument'; +export declare const buildMessageURL: (type: MessageType) => (arg: { + organizationId: IOrganization['id']; + wikiId?: IWiki['id']; + documentId?: IDocument['id']; +}) => string; +export {}; diff --git a/packages/domains/lib/models/message.js b/packages/domains/lib/models/message.js index 0e34578..7059a7d 100644 --- a/packages/domains/lib/models/message.js +++ b/packages/domains/lib/models/message.js @@ -1,2 +1,25 @@ "use strict"; exports.__esModule = true; +exports.buildMessageURL = void 0; +var buildMessageURL = function (type) { + switch (type) { + case 'toDocument': + return function (_a) { + var organizationId = _a.organizationId, wikiId = _a.wikiId, documentId = _a.documentId; + return "/app/org/".concat(organizationId, "/wiki/").concat(wikiId, "/doc/").concat(documentId); + }; + case 'toWiki': + return function (_a) { + var organizationId = _a.organizationId, wikiId = _a.wikiId; + return "/app/org/".concat(organizationId, "/wiki/").concat(wikiId); + }; + case 'toOrganization': + return function (_a) { + var organizationId = _a.organizationId; + return "/app/org/".concat(organizationId); + }; + default: + throw new Error(); + } +}; +exports.buildMessageURL = buildMessageURL; diff --git a/packages/domains/lib/models/organization.d.ts b/packages/domains/lib/models/organization.d.ts new file mode 100644 index 0000000..e615eb9 --- /dev/null +++ b/packages/domains/lib/models/organization.d.ts @@ -0,0 +1,12 @@ +import { IUser } from './user'; +/** + * 组织数据定义 + */ +export interface IOrganization { + id: string; + name: string; + description: string; + logo: string; + createUserId: IUser['id']; + isPersonal: boolean; +} diff --git a/packages/domains/lib/models/organization.js b/packages/domains/lib/models/organization.js new file mode 100644 index 0000000..ac136d9 --- /dev/null +++ b/packages/domains/lib/models/organization.js @@ -0,0 +1,33 @@ +"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; +// } diff --git a/packages/domains/lib/models/wiki.d.ts b/packages/domains/lib/models/wiki.d.ts index 47ac6e6..e20a26c 100644 --- a/packages/domains/lib/models/wiki.d.ts +++ b/packages/domains/lib/models/wiki.d.ts @@ -1,5 +1,6 @@ import { IUser } from './user'; import { IDocument } from './document'; +import { IOrganization } from './organization'; /** * 知识库状态枚举 */ @@ -27,6 +28,7 @@ export declare enum WikiUserRole { */ export interface IWiki { id: string; + organizationId: IOrganization['id']; name: string; avatar: string; description: string; diff --git a/packages/domains/src/api/document.ts b/packages/domains/src/api/document.ts index 56805c0..8605b63 100644 --- a/packages/domains/src/api/document.ts +++ b/packages/domains/src/api/document.ts @@ -6,8 +6,8 @@ export const DocumentApiDefinition = { */ search: { method: 'get' as const, - server: 'search' as const, - client: () => '/document/search', + server: '/:organizationId/search' as const, + client: (organizationId) => `/document/${organizationId}/search`, }, /** @@ -15,8 +15,8 @@ export const DocumentApiDefinition = { */ recent: { method: 'get' as const, - server: 'recent' as const, - client: () => '/document/recent', + server: '/:organizationId/recent' as const, + client: (organizationId) => `/document/${organizationId}/recent`, }, /** diff --git a/packages/domains/src/api/index.ts b/packages/domains/src/api/index.ts index d53ffd4..4e35e4a 100644 --- a/packages/domains/src/api/index.ts +++ b/packages/domains/src/api/index.ts @@ -6,3 +6,4 @@ export * from './message'; export * from './template'; export * from './comment'; export * from './star'; +export * from './organization'; diff --git a/packages/domains/src/api/organization.ts b/packages/domains/src/api/organization.ts new file mode 100644 index 0000000..c0aed27 --- /dev/null +++ b/packages/domains/src/api/organization.ts @@ -0,0 +1,84 @@ +import { IOrganization } from '../models'; + +export const OrganizationApiDefinition = { + /** + * 创建 + */ + createOrganization: { + method: 'post' as const, + server: '/create' as const, + client: () => '/organization/create', + }, + + /** + * 获取用户个人组织 + */ + getPersonalOrganization: { + method: 'get' as const, + server: '/personal' as const, + client: () => '/organization/personal', + }, + + /** + * 获取用户除个人组织外其他组织 + */ + getUserOrganizations: { + method: 'get' as const, + server: '/list/personal' as const, + client: () => '/organization/list/personal', + }, + + /** + * 获取组织详情 + */ + getOrganizationDetail: { + method: 'get' as const, + server: '/detail/:id' as const, + client: (id: IOrganization['id']) => `/organization/detail/${id}`, + }, + + /** + * 更新组织基本信息 + */ + updateOrganization: { + method: 'post' as const, + server: '/update/:id' as const, + client: (id: IOrganization['id']) => `/organization/update/${id}`, + }, + + /** + * 获取组织成员 + */ + getMembers: { + method: 'get' as const, + server: '/member/:id' as const, + client: (id: IOrganization['id']) => `/organization/member/${id}`, + }, + + /** + * 添加组织成员 + */ + addMemberById: { + method: 'post' as const, + server: 'member/:id/add' as const, + client: (id: IOrganization['id']) => `/organization/member/${id}/add`, + }, + + /** + * 更新组织成员 + */ + updateMemberById: { + method: 'patch' as const, + server: 'member/:id/update' as const, + client: (id: IOrganization['id']) => `/organization/member/${id}/update`, + }, + + /** + * 删除组织成员 + */ + deleteMemberById: { + method: 'delete' as const, + server: 'member/:id/delete' as const, + client: (id: IOrganization['id']) => `/organization/member/${id}/delete`, + }, +}; diff --git a/packages/domains/src/api/star.ts b/packages/domains/src/api/star.ts index d85ae24..f79c543 100644 --- a/packages/domains/src/api/star.ts +++ b/packages/domains/src/api/star.ts @@ -1,8 +1,8 @@ export const StarApiDefinition = { /** - * 收藏(或取消收藏) + * 加星或取消 */ - toggle: { + toggleStar: { method: 'post' as const, server: 'toggle' as const, client: () => '/star/toggle', @@ -11,36 +11,36 @@ export const StarApiDefinition = { /** * 检测是否收藏 */ - check: { + isStared: { method: 'post' as const, - server: 'check' as const, - client: () => '/star/check', + server: 'isStared' as const, + client: () => '/star/isStared', }, /** - * 获取收藏的知识库 + * 获取组织内加星的知识库 */ - wikis: { + getStarWikisInOrganization: { method: 'get' as const, - server: 'wikis' as const, - client: () => '/star/wikis', + server: '/:organizationId/wikis' as const, + client: (organizationId) => `/star/${organizationId}/wikis`, }, /** * 获取知识库内加星的文章 */ - wikiDocuments: { + getStarDocumentsInWiki: { method: 'get' as const, - server: 'wiki/documents' as const, - client: () => '/star/wiki/documents', + server: '/wiki/documents' as const, + client: () => `/star/wiki/documents`, }, /** - * 获取收藏的文档 + * 获取组织内加星的文档 */ - documents: { + getStarDocumentsInOrganization: { method: 'get' as const, - server: 'documents' as const, - client: () => '/star/documents', + server: '/:organizationId/documents' as const, + client: (organizationId) => `/star/${organizationId}/documents`, }, }; diff --git a/packages/domains/src/api/wiki.ts b/packages/domains/src/api/wiki.ts index 810367f..9e91425 100644 --- a/packages/domains/src/api/wiki.ts +++ b/packages/domains/src/api/wiki.ts @@ -6,8 +6,8 @@ export const WikiApiDefinition = { */ getAllWikis: { method: 'get' as const, - server: 'list/all' as const, - client: () => '/wiki/list/all', + server: 'list/all/:organizationId' as const, + client: (organizationId) => `/wiki/list/all/${organizationId}`, }, /** @@ -15,8 +15,8 @@ export const WikiApiDefinition = { */ getOwnWikis: { method: 'get' as const, - server: 'list/own' as const, - client: () => '/wiki/list/own', + server: 'list/own/:organizationId' as const, + client: (organizationId) => `/wiki/list/own/${organizationId}`, }, /** @@ -24,8 +24,8 @@ export const WikiApiDefinition = { */ getJoinWikis: { method: 'get' as const, - server: 'list/join' as const, - client: () => '/wiki/list/join', + server: 'list/join/:organizationId' as const, + client: (organizationId) => `/wiki/list/join/${organizationId}`, }, /** diff --git a/packages/domains/src/models/auth.ts b/packages/domains/src/models/auth.ts new file mode 100644 index 0000000..7827a78 --- /dev/null +++ b/packages/domains/src/models/auth.ts @@ -0,0 +1,30 @@ +import { IOrganization } from './organization'; + +export enum AuthEnum { + creator = 'creator', + admin = 'admin', + member = 'member', + noAccess = 'noAccess', +} + +export const AuthEnumTextMap = { + [AuthEnum.creator]: '创建者', + [AuthEnum.admin]: '管理员', + [AuthEnum.member]: '成员', + [AuthEnum.noAccess]: '无权限', +}; + +export const AuthEnumArray = Object.keys(AuthEnumTextMap).map((value) => ({ + label: AuthEnumTextMap[value], + value, +})); + +export interface IAuth { + id: string; + type: AuthEnum; + organizationId: IOrganization['id']; + wikiId?: string; + documentId?: string; + createdAt: string; + updatedAt: string; +} diff --git a/packages/domains/src/models/document.ts b/packages/domains/src/models/document.ts index c5ee906..69ec1f4 100644 --- a/packages/domains/src/models/document.ts +++ b/packages/domains/src/models/document.ts @@ -1,3 +1,4 @@ +import { IOrganization } from './organization'; import { IUser } from './user'; import { IWiki } from './wiki'; @@ -14,6 +15,7 @@ export enum DocumentStatus { */ export interface IDocument { id: string; + organizationId: IOrganization['id']; wikiId: IWiki['id']; isWikiHome: boolean; createUserId: IUser['id']; diff --git a/packages/domains/src/models/index.ts b/packages/domains/src/models/index.ts index 88796f1..c2dd944 100644 --- a/packages/domains/src/models/index.ts +++ b/packages/domains/src/models/index.ts @@ -6,3 +6,5 @@ export * from './template'; export * from './comment'; export * from './pagination'; export * from './system'; +export * from './organization'; +export * from './auth'; diff --git a/packages/domains/src/models/message.ts b/packages/domains/src/models/message.ts index ebcc1d7..d97459c 100644 --- a/packages/domains/src/models/message.ts +++ b/packages/domains/src/models/message.ts @@ -1,4 +1,7 @@ import { IUser } from './user'; +import { IOrganization } from './organization'; +import { IWiki } from './wiki'; +import { IDocument } from './document'; /** * 消息数据定义 @@ -13,3 +16,29 @@ export interface IMessage { createdAt: Date; updatedAt: Date; } + +type MessageType = 'toOrganization' | 'toWiki' | 'toDocument'; + +export const buildMessageURL = ( + type: MessageType +): ((arg: { organizationId: IOrganization['id']; wikiId?: IWiki['id']; documentId?: IDocument['id'] }) => string) => { + switch (type) { + case 'toDocument': + return ({ organizationId, wikiId, documentId }) => { + return `/app/org/${organizationId}/wiki/${wikiId}/doc/${documentId}`; + }; + + case 'toWiki': + return ({ organizationId, wikiId }) => { + return `/app/org/${organizationId}/wiki/${wikiId}`; + }; + + case 'toOrganization': + return ({ organizationId }) => { + return `/app/org/${organizationId}`; + }; + + default: + throw new Error() as never; + } +}; diff --git a/packages/domains/src/models/organization.ts b/packages/domains/src/models/organization.ts new file mode 100644 index 0000000..855e177 --- /dev/null +++ b/packages/domains/src/models/organization.ts @@ -0,0 +1,49 @@ +import { IUser } from './user'; + +/** + * 组织数据定义 + */ +export interface IOrganization { + id: string; + name: string; + description: string; + logo: string; + 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; +// } diff --git a/packages/domains/src/models/wiki.ts b/packages/domains/src/models/wiki.ts index b198743..9aec30a 100644 --- a/packages/domains/src/models/wiki.ts +++ b/packages/domains/src/models/wiki.ts @@ -1,5 +1,6 @@ import { IUser } from './user'; import { IDocument } from './document'; +import { IOrganization } from './organization'; /** * 知识库状态枚举 @@ -31,6 +32,7 @@ export enum WikiUserRole { */ export interface IWiki { id: string; + organizationId: IOrganization['id']; name: string; avatar: string; description: string; diff --git a/packages/server/src/app.module.ts b/packages/server/src/app.module.ts index ab41797..4615a31 100644 --- a/packages/server/src/app.module.ts +++ b/packages/server/src/app.module.ts @@ -1,7 +1,10 @@ +import { AuthEntity } from '@entities/auth.entity'; import { CommentEntity } from '@entities/comment.entity'; import { DocumentEntity } from '@entities/document.entity'; -import { DocumentAuthorityEntity } from '@entities/document-authority.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'; @@ -9,13 +12,15 @@ 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 { 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'; import { CommentModule } from '@modules/comment.module'; import { DocumentModule } from '@modules/document.module'; import { FileModule } from '@modules/file.module'; import { MessageModule } from '@modules/message.module'; +import { OrganizationModule } from '@modules/organization.module'; import { StarModule } from '@modules/star.module'; import { SystemModule } from '@modules/system.module'; import { TemplateModule } from '@modules/template.module'; @@ -35,10 +40,13 @@ import pino from 'pino'; const ENTITIES = [ UserEntity, + AuthEntity, + OrganizationEntity, + // OrganizationUserEntity, WikiEntity, - WikiUserEntity, - DocumentAuthorityEntity, + // WikiUserEntity, DocumentEntity, + // DocumentUserEntity, StarEntity, CommentEntity, MessageEntity, @@ -50,6 +58,8 @@ const ENTITIES = [ const MODULES = [ UserModule, + AuthModule, + OrganizationModule, WikiModule, DocumentModule, StarModule, diff --git a/packages/server/src/constants/index.ts b/packages/server/src/constants/index.ts new file mode 100644 index 0000000..2fef5a8 --- /dev/null +++ b/packages/server/src/constants/index.ts @@ -0,0 +1,4 @@ +export enum RedisDBEnum { + documentVersion = 0, + view = 1, +} diff --git a/packages/server/src/controllers/auth.controller.ts b/packages/server/src/controllers/auth.controller.ts new file mode 100644 index 0000000..18b7495 --- /dev/null +++ b/packages/server/src/controllers/auth.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { AuthService } from '@services/auth.service'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} +} diff --git a/packages/server/src/controllers/document.controller.ts b/packages/server/src/controllers/document.controller.ts index 8e91595..b9e0d0f 100644 --- a/packages/server/src/controllers/document.controller.ts +++ b/packages/server/src/controllers/document.controller.ts @@ -1,8 +1,9 @@ +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 { CheckDocumentAuthority, DocumentAuthorityGuard } from '@guard/document-auth.guard'; import { CheckDocumentStatus, DocumentStatusGuard } from '@guard/document-status.guard'; import { JwtGuard } from '@guard/jwt.guard'; import { @@ -25,7 +26,7 @@ import { DocumentService } from '@services/document.service'; import { DocumentApiDefinition, DocumentStatus } from '@think/domains'; @Controller('document') -@UseGuards(DocumentAuthorityGuard) +// @UseGuards(DocumentAuthorityGuard) @UseGuards(DocumentStatusGuard) export class DocumentController { constructor(private readonly documentService: DocumentService) {} @@ -40,8 +41,8 @@ export class DocumentController { @Get(DocumentApiDefinition.search.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async search(@Request() req, @Query('keyword') keyword) { - return await this.documentService.search(req.user, keyword); + async search(@Request() req, @Param('organizationId') organizationId, @Query('keyword') keyword) { + return await this.documentService.search(req.user, organizationId, keyword); } /** @@ -53,8 +54,8 @@ export class DocumentController { @Get(DocumentApiDefinition.recent.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getWorkspaceDocuments(@Request() req) { - return await this.documentService.getRecentDocuments(req.user); + async getWorkspaceDocuments(@Request() req, @Param('organizationId') organizationId) { + return await this.documentService.getRecentDocuments(req.user, organizationId); } /** @@ -77,10 +78,10 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Get(DocumentApiDefinition.getDetailById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('readable') + // @CheckDocumentAuthority('readable') @UseGuards(JwtGuard) async getDocumentDetail(@Request() req, @Param('id') documentId) { - return await this.documentService.getDocumentDetail(req.user, documentId, req.headers['user-agent']); + return await this.documentService.getDocumentDetail(req.user, documentId); } /** @@ -93,7 +94,7 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Patch(DocumentApiDefinition.updateById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('editable') + // @CheckDocumentAuthority('editable') @UseGuards(JwtGuard) async updateDocument(@Request() req, @Param('id') documentId, @Body() dto: UpdateDocumentDto) { return await this.documentService.updateDocument(req.user, documentId, dto); @@ -108,7 +109,7 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Get(DocumentApiDefinition.getVersionById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('readable') + // @CheckDocumentAuthority('readable') @UseGuards(JwtGuard) async getDocumentVersion(@Request() req, @Param('id') documentId) { return await this.documentService.getDocumentVersion(req.user, documentId); @@ -123,7 +124,7 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Get(DocumentApiDefinition.getMemberById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('readable') + // @CheckDocumentAuthority('readable') @UseGuards(JwtGuard) async getDocUsers(@Request() req, @Param('id') documentId) { return await this.documentService.getDocUsers(req.user, documentId); @@ -139,10 +140,10 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Post(DocumentApiDefinition.addMemberById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('createUser') + // @CheckDocumentAuthority('createUser') @UseGuards(JwtGuard) - async addDocUser(@Request() req, @Body() dto: DocAuthDto) { - return await this.documentService.addDocUser(req.user, dto); + async addDocUser(@Request() req, @Param('id') documentId, @Body() dto: OperateUserAuthDto) { + return await this.documentService.addDocUser(req.user, documentId, dto); } /** @@ -155,10 +156,10 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Patch(DocumentApiDefinition.updateMemberById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('createUser') + // @CheckDocumentAuthority('createUser') @UseGuards(JwtGuard) - async updateDocUser(@Request() req, @Body() dto: DocAuthDto) { - return await this.documentService.updateDocUser(req.user, dto); + async updateDocUser(@Request() req, @Param('id') documentId, @Body() dto: OperateUserAuthDto) { + return await this.documentService.updateDocUser(req.user, documentId, dto); } /** @@ -171,10 +172,10 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Post(DocumentApiDefinition.deleteMemberById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('createUser') + // @CheckDocumentAuthority('createUser') @UseGuards(JwtGuard) - async deleteDocUser(@Request() req, @Body() dto: DocAuthDto) { - return await this.documentService.deleteDocUser(req.user, dto); + async deleteDocUser(@Request() req, @Param('id') documentId, @Body() dto: OperateUserAuthDto) { + return await this.documentService.deleteDocUser(req.user, documentId, dto); } /** @@ -186,7 +187,7 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Post(DocumentApiDefinition.getChildren.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('readable') + // @CheckDocumentAuthority('readable') @UseGuards(JwtGuard) async getChildrenDocuments(@Request() req, @Body() data) { return await this.documentService.getChildrenDocuments(req.user, data); @@ -201,7 +202,7 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Delete(DocumentApiDefinition.deleteById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('createUser') + // @CheckDocumentAuthority('createUser') @UseGuards(JwtGuard) async deleteDocument(@Request() req, @Param('id') documentId) { return await this.documentService.deleteDocument(req.user, documentId); @@ -217,7 +218,7 @@ export class DocumentController { @UseInterceptors(ClassSerializerInterceptor) @Post(DocumentApiDefinition.shareById.server) @HttpCode(HttpStatus.OK) - @CheckDocumentAuthority('editable') + // @CheckDocumentAuthority('editable') @UseGuards(JwtGuard) async shareDocument(@Request() req, @Param('id') documentId, @Body() dto: ShareDocumentDto) { return await this.documentService.shareDocument(req.user, documentId, dto); @@ -235,7 +236,7 @@ export class DocumentController { @CheckDocumentStatus(DocumentStatus.public) @HttpCode(HttpStatus.OK) async getShareDocumentDetail(@Request() req, @Param('id') documentId, @Body() dto: ShareDocumentDto) { - return await this.documentService.getPublicDocumentDetail(documentId, dto, req.headers['user-agent']); + return await this.documentService.getPublicDocumentDetail(documentId, dto); } /** diff --git a/packages/server/src/controllers/organization.controller.ts b/packages/server/src/controllers/organization.controller.ts new file mode 100644 index 0000000..fc516f2 --- /dev/null +++ b/packages/server/src/controllers/organization.controller.ts @@ -0,0 +1,154 @@ +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, + ClassSerializerInterceptor, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + Param, + Patch, + Post, + Request, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { OrganizationService } from '@services/organization.service'; +import { OrganizationApiDefinition } from '@think/domains'; + +@Controller('organization') +export class OrganizationController { + constructor(private readonly organizationService: OrganizationService) {} + + /** + * 创建组织 + * @param req + * @param dto + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(OrganizationApiDefinition.createOrganization.server) + @HttpCode(HttpStatus.CREATED) + @UseGuards(JwtGuard) + async register(@Request() req, @Body() dto: CreateOrganizationDto) { + return await this.organizationService.createOrganization(req.user, dto); + } + + /** + * 更新组织信息 + * @param req + * @param id + * @param dto + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(OrganizationApiDefinition.updateOrganization.server) + @HttpCode(HttpStatus.CREATED) + @UseGuards(JwtGuard) + async updateOrganization(@Request() req, @Param('id') id, @Body() dto: CreateOrganizationDto) { + return await this.organizationService.updateOrganization(req.user, id, dto); + } + + /** + * 获取用户个人组织 + * @param req + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Get(OrganizationApiDefinition.getPersonalOrganization.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async getPersonalOrganization(@Request() req) { + return await this.organizationService.getPersonalOrganization(req.user); + } + + /** + * 获取用户除个人组织外可访问的组织 + * @param user + */ + @UseInterceptors(ClassSerializerInterceptor) + @Get(OrganizationApiDefinition.getUserOrganizations.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async getUserOrganizations(@Request() req) { + return await this.organizationService.getUserOrganizations(req.user); + } + + /** + * 获取组织详情 + * @param req + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Get(OrganizationApiDefinition.getOrganizationDetail.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async getOrganizationDetail(@Request() req, @Param('id') id) { + return await this.organizationService.getOrganizationDetail(req.user, id); + } + + /** + * 获取组织成员 + * @param req + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Get(OrganizationApiDefinition.getMembers.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async getMembers(@Request() req, @Param('id') id) { + return await this.organizationService.getMembers(req.user, id); + } + + /** + * 添加组织成员 + * 只有管理员可操作 + * @param req + * @param id + * @param dto + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Post(OrganizationApiDefinition.addMemberById.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async addMember(@Request() req, @Param('id') id, @Body() dto: OperateUserAuthDto) { + return await this.organizationService.addMember(req.user, id, dto); + } + + /** + * 更新组织成员(一般为角色操作) + * 只有管理员可操作 + * @param req + * @param id + * @param dto + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Patch(OrganizationApiDefinition.updateMemberById.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async updateMember(@Request() req, @Param('id') id, @Body() dto: OperateUserAuthDto) { + return await this.organizationService.updateMember(req.user, id, dto); + } + + /** + * 删除组织成员 + * 只有管理员可操作 + * @param req + * @param id + * @param dto + * @returns + */ + @UseInterceptors(ClassSerializerInterceptor) + @Delete(OrganizationApiDefinition.deleteMemberById.server) + @HttpCode(HttpStatus.OK) + @UseGuards(JwtGuard) + async deleteMember(@Request() req, @Param('id') id, @Body() dto: OperateUserAuthDto) { + return await this.organizationService.deleteMember(req.user, id, dto); + } +} diff --git a/packages/server/src/controllers/star.controller.ts b/packages/server/src/controllers/star.controller.ts index 213d221..c19ee19 100644 --- a/packages/server/src/controllers/star.controller.ts +++ b/packages/server/src/controllers/star.controller.ts @@ -7,6 +7,7 @@ import { Get, HttpCode, HttpStatus, + Param, Post, Query, Request, @@ -24,7 +25,7 @@ export class StarController { * 收藏(或取消收藏) */ @UseInterceptors(ClassSerializerInterceptor) - @Post(StarApiDefinition.toggle.server) + @Post(StarApiDefinition.toggleStar.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) async toggleStar(@Request() req, @Body() dto: StarDto) { @@ -35,7 +36,7 @@ export class StarController { * 检测是否收藏 */ @UseInterceptors(ClassSerializerInterceptor) - @Post(StarApiDefinition.check.server) + @Post(StarApiDefinition.isStared.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) async checkStar(@Request() req, @Body() dto: StarDto) { @@ -43,35 +44,35 @@ export class StarController { } /** - * 获取收藏的知识库 + * 获取组织内加星的知识库 */ @UseInterceptors(ClassSerializerInterceptor) - @Get(StarApiDefinition.wikis.server) + @Get(StarApiDefinition.getStarWikisInOrganization.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getWikis(@Request() req) { - return await this.starService.getWikis(req.user); + async getStarWikisInOrganization(@Request() req, @Param('organizationId') organizationId) { + return await this.starService.getStarWikisInOrganization(req.user, organizationId); } /** * 获取知识库内收藏的文档 */ @UseInterceptors(ClassSerializerInterceptor) - @Get(StarApiDefinition.wikiDocuments.server) + @Get(StarApiDefinition.getStarDocumentsInWiki.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getWikiDocuments(@Request() req, @Query() dto: StarDto) { - return await this.starService.getWikiDocuments(req.user, dto); + async getStarDocumentsInWiki(@Request() req, @Query() dto: StarDto) { + return await this.starService.getStarDocumentsInWiki(req.user, dto); } /** - * 获取收藏的文档 + * 获取组织内加星的文档(平铺) */ @UseInterceptors(ClassSerializerInterceptor) - @Get(StarApiDefinition.documents.server) + @Get(StarApiDefinition.getStarDocumentsInOrganization.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getDocuments(@Request() req) { - return await this.starService.getDocuments(req.user); + async getStarDocumentsInOrganization(@Request() req, @Param('organizationId') organizationId) { + return await this.starService.getStarDocumentsInOrganization(req.user, organizationId); } } diff --git a/packages/server/src/controllers/wiki.controller.ts b/packages/server/src/controllers/wiki.controller.ts index 49deb8f..12ed530 100644 --- a/packages/server/src/controllers/wiki.controller.ts +++ b/packages/server/src/controllers/wiki.controller.ts @@ -1,10 +1,10 @@ +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 { JwtGuard } from '@guard/jwt.guard'; import { CheckWikiStatus, WikiStatusGuard } from '@guard/wiki-status.guard'; -import { CheckWikiUserRole, WikiUserRoleGuard } from '@guard/wiki-user.guard'; +// import { CheckWikiUserRole, WikiUserRoleGuard } from '@guard/wiki-user.guard'; import { Body, ClassSerializerInterceptor, @@ -38,8 +38,8 @@ export class WikiController { @Get(WikiApiDefinition.getAllWikis.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getAllWikis(@Request() req, @Query() pagination: IPagination) { - return await this.wikiService.getAllWikis(req.user, pagination); + async getAllWikis(@Request() req, @Param('organizationId') organizationId, @Query() pagination: IPagination) { + return await this.wikiService.getAllWikis(req.user, organizationId, pagination); } /** @@ -52,8 +52,8 @@ export class WikiController { @Get(WikiApiDefinition.getOwnWikis.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getOwnWikis(@Request() req, @Query() pagination: IPagination) { - return await this.wikiService.getOwnWikis(req.user, pagination); + async getOwnWikis(@Request() req, @Param('organizationId') organizationId, @Query() pagination: IPagination) { + return await this.wikiService.getOwnWikis(req.user, organizationId, pagination); } /** @@ -66,8 +66,8 @@ export class WikiController { @Get(WikiApiDefinition.getJoinWikis.server) @HttpCode(HttpStatus.OK) @UseGuards(JwtGuard) - async getJoinWikis(@Request() req, @Query() pagination: IPagination) { - return await this.wikiService.getJoinWikis(req.user, pagination); + async getJoinWikis(@Request() req, @Param('organizationId') organizationId, @Query() pagination: IPagination) { + return await this.wikiService.getJoinWikis(req.user, organizationId, pagination); } /** @@ -93,8 +93,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Get(WikiApiDefinition.getHomeDocumentById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole() - @UseGuards(WikiUserRoleGuard) + // @CheckWikiUserRole() + // @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) async getWikiHomeDocument(@Request() req, @Param('id') wikiId) { return await this.wikiService.getWikiHomeDocument(req.user, wikiId); @@ -109,8 +109,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Get(WikiApiDefinition.getTocsById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole() - @UseGuards(WikiUserRoleGuard) + // @CheckWikiUserRole() + // @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) async getWikiTocs(@Request() req, @Param('id') wikiId) { return await this.wikiService.getWikiTocs(req.user, wikiId); @@ -126,28 +126,28 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Patch(WikiApiDefinition.updateTocsById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole() - @UseGuards(WikiUserRoleGuard) + // @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 + // * @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); + // } /** * 获取知识库详情 @@ -158,8 +158,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Get(WikiApiDefinition.getDetailById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole() - @UseGuards(WikiUserRoleGuard) + // @CheckWikiUserRole() + // @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) async getWikiDetail(@Request() req, @Param('id') wikiId) { return await this.wikiService.getWikiDetail(req.user, wikiId); @@ -176,8 +176,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Patch(WikiApiDefinition.updateById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) + // @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); @@ -193,8 +193,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Delete(WikiApiDefinition.deleteById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) + // @CheckWikiUserRole(WikiUserRole.admin) + // @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) async deleteWiki(@Request() req, @Param('id') wikiId) { return await this.wikiService.deleteWiki(req.user, wikiId); @@ -210,11 +210,11 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Get(WikiApiDefinition.getMemberById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) + // @CheckWikiUserRole(WikiUserRole.admin) + // @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) - async getWikiUsers(@Param('id') wikiId) { - return await this.wikiService.getWikiUsers(wikiId); + async getWikiUsers(@Request() req, @Param('id') wikiId) { + return await this.wikiService.getWikiUsers(req.user, wikiId); } /** @@ -228,10 +228,10 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Post(WikiApiDefinition.addMemberById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) + // @CheckWikiUserRole(WikiUserRole.admin) + // @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) - async addWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: WikiUserDto) { + async addWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: OperateUserAuthDto) { return await this.wikiService.addWikiUser(req.user, wikiId, dto); } @@ -246,10 +246,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Patch(WikiApiDefinition.updateMemberById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) - async updateWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: WikiUserDto) { + async updateWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: OperateUserAuthDto) { return await this.wikiService.updateWikiUser(req.user, wikiId, dto); } @@ -264,10 +262,8 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Delete(WikiApiDefinition.deleteMemberById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) - async deleteWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: WikiUserDto) { + async deleteWikiUser(@Request() req, @Param('id') wikiId, @Body() dto: OperateUserAuthDto) { return await this.wikiService.deleteWikiUser(req.user, wikiId, dto); } @@ -282,8 +278,6 @@ export class WikiController { @UseInterceptors(ClassSerializerInterceptor) @Post(WikiApiDefinition.shareById.server) @HttpCode(HttpStatus.OK) - @CheckWikiUserRole(WikiUserRole.admin) - @UseGuards(WikiUserRoleGuard) @UseGuards(JwtGuard) async toggleWorkspaceStatus(@Request() req, @Param('id') wikiId, @Body() dto: ShareWikiDto) { return await this.wikiService.shareWiki(req.user, wikiId, dto); @@ -301,7 +295,7 @@ export class WikiController { @UseGuards(WikiStatusGuard) @HttpCode(HttpStatus.OK) async getWikiPublicHomeDocument(@Request() req, @Param('id') wikiId) { - return await this.wikiService.getPublicWikiHomeDocument(wikiId, req.headers['user-agent']); + return await this.wikiService.getPublicWikiHomeDocument(wikiId); } /** diff --git a/packages/server/src/dtos/auth.dto.ts b/packages/server/src/dtos/auth.dto.ts new file mode 100644 index 0000000..c85c7d7 --- /dev/null +++ b/packages/server/src/dtos/auth.dto.ts @@ -0,0 +1,28 @@ +import { AuthEnum } from '@think/domains'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class AuthDto { + @IsString({ message: '权限类型类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '权限类型不能为空' }) + auth: AuthEnum; + + @IsString({ message: '组织 Id 类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '组织 Id 不能为空' }) + organizationId: string; + + @IsString({ message: '知识库 Id 类型错误(正确类型为:String)' }) + @IsOptional() + wikiId: string; + + @IsString({ message: '文档 Id 类型错误(正确类型为:String)' }) + @IsOptional() + documentId: string; +} + +export class OperateUserAuthDto { + @IsString() + readonly userAuth: AuthEnum; + + @IsString() + readonly userName: string; +} diff --git a/packages/server/src/dtos/create-document.dto.ts b/packages/server/src/dtos/create-document.dto.ts index f116c39..3b0c704 100644 --- a/packages/server/src/dtos/create-document.dto.ts +++ b/packages/server/src/dtos/create-document.dto.ts @@ -1,6 +1,9 @@ import { IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator'; export class CreateDocumentDto { + @IsNotEmpty({ message: '组织Id不能为空' }) + readonly organizationId: string; + @IsNotEmpty({ message: '知识库Id不能为空' }) readonly wikiId: string; diff --git a/packages/server/src/dtos/create-wiki.dto.ts b/packages/server/src/dtos/create-wiki.dto.ts index 54cc373..ac9c06f 100644 --- a/packages/server/src/dtos/create-wiki.dto.ts +++ b/packages/server/src/dtos/create-wiki.dto.ts @@ -1,6 +1,9 @@ import { IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; export class CreateWikiDto { + @IsNotEmpty({ message: '组织Id不能为空' }) + readonly organizationId: string; + @IsString({ message: '知识库名称类型错误(正确类型为:String)' }) @IsNotEmpty({ message: '知识库名称不能为空' }) @MinLength(1, { message: '知识库名称至少1个字符' }) diff --git a/packages/server/src/dtos/organization-auth.dto.ts b/packages/server/src/dtos/organization-auth.dto.ts new file mode 100644 index 0000000..ac17c71 --- /dev/null +++ b/packages/server/src/dtos/organization-auth.dto.ts @@ -0,0 +1,12 @@ +// 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; +// } diff --git a/packages/server/src/dtos/organization-user.dto.ts b/packages/server/src/dtos/organization-user.dto.ts new file mode 100644 index 0000000..0d07c27 --- /dev/null +++ b/packages/server/src/dtos/organization-user.dto.ts @@ -0,0 +1,10 @@ +// import { OrganizationAuthEnum } from '@think/domains'; +// import { IsString } from 'class-validator'; + +// export class OrganizationUserDto { +// @IsString() +// readonly userName: string; + +// @IsString() +// readonly userAuth: OrganizationAuthEnum; +// } diff --git a/packages/server/src/dtos/organization.dto.ts b/packages/server/src/dtos/organization.dto.ts new file mode 100644 index 0000000..28b0266 --- /dev/null +++ b/packages/server/src/dtos/organization.dto.ts @@ -0,0 +1,21 @@ +import { IsBoolean, IsNotEmpty, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +export class CreateOrganizationDto { + @MinLength(1, { message: '组织名称至少1个字符' }) + @MaxLength(20, { message: '组织名称最多20个字符' }) + @IsString({ message: '组织名称类型错误(正确类型为:String)' }) + @IsNotEmpty({ message: '组织名称不能为空' }) + name: string; + + @IsString({ message: '组织描述类型错误(正确类型为:String)' }) + @IsOptional() + description: string; + + @IsString({ message: '组织Logo类型错误(正确类型为:String)' }) + @IsOptional() + logo: string; + + @IsBoolean() + @IsOptional() + isPersonal?: boolean; +} diff --git a/packages/server/src/dtos/star.dto.ts b/packages/server/src/dtos/star.dto.ts index eabd77d..47bef19 100644 --- a/packages/server/src/dtos/star.dto.ts +++ b/packages/server/src/dtos/star.dto.ts @@ -1,12 +1,13 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; export class StarDto { + @IsNotEmpty({ message: '组织Id不能为空' }) + readonly organizationId: string; + @IsString({ message: '加星 wikiId 类型错误(正确类型为:String)' }) @IsNotEmpty({ message: '加星 wikiId 不能为空' }) wikiId: string; - @IsString({ message: '加星 documentId 类型错误(正确类型为:String)' }) - @IsNotEmpty({ message: '加星 documentId 不能为空' }) @IsOptional() documentId?: string; } diff --git a/packages/server/src/entities/auth.entity.ts b/packages/server/src/entities/auth.entity.ts new file mode 100644 index 0000000..304fb9a --- /dev/null +++ b/packages/server/src/entities/auth.entity.ts @@ -0,0 +1,57 @@ +import { AuthEnum } from '@think/domains'; +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('auth') +export class AuthEntity { + @PrimaryGeneratedColumn('uuid') + public id: string; + + /** + * 关联用户 Id + */ + @Column({ type: 'varchar', comment: '关联用户 Id' }) + public userId: string; + + /** + * 权限类型 + */ + @Column({ + type: 'enum', + enum: AuthEnum, + default: AuthEnum.noAccess, + comment: '权限类型', + }) + public auth: AuthEnum; + + /** + * 所属组织 Id + */ + @Column({ type: 'varchar', comment: '所属组织 Id' }) + public organizationId: string; + + /** + * 所属知识库 Id + */ + @Column({ type: 'varchar', default: null, comment: '所属知识库 Id' }) + public wikiId: string; + + /** + * 所属文档 Id + */ + @Column({ type: 'varchar', default: null, comment: '所属文档 Id' }) + public documentId: string; + + @CreateDateColumn({ + type: 'timestamp', + name: 'createdAt', + comment: '创建时间', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updatedAt', + comment: '更新时间', + }) + updatedAt: Date; +} diff --git a/packages/server/src/entities/comment.entity.ts b/packages/server/src/entities/comment.entity.ts index 9e0d77f..af837a3 100644 --- a/packages/server/src/entities/comment.entity.ts +++ b/packages/server/src/entities/comment.entity.ts @@ -28,14 +28,14 @@ export class CommentEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/document-authority.entity.ts b/packages/server/src/entities/document-authority.entity.ts deleted file mode 100644 index b733c35..0000000 --- a/packages/server/src/entities/document-authority.entity.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; - -@Entity('document_authority') -export class DocumentAuthorityEntity { - @PrimaryGeneratedColumn('uuid') - public id: string; - - @Column({ type: 'varchar', comment: '文档所属知识库 Id' }) - public wikiId: string; - - @Column({ type: 'varchar', comment: '文档 Id' }) - public documentId: string; - - @Column({ type: 'varchar', comment: '文档创建用户 Id' }) - public createUserId: string; - - @Column({ type: 'varchar', comment: '用户 Id' }) - public userId: string; - - @Column({ type: 'bool', default: false }) - public readable: boolean; - - @Column({ type: 'bool', default: false }) - public editable: boolean; - - @CreateDateColumn({ - type: 'timestamp', - name: 'created_at', - comment: '创建时间', - }) - createdAt: Date; - - @UpdateDateColumn({ - type: 'timestamp', - name: 'updated_at', - comment: '更新时间', - }) - updatedAt: Date; -} diff --git a/packages/server/src/entities/document.entity.ts b/packages/server/src/entities/document.entity.ts index b1ee38a..a4c1718 100644 --- a/packages/server/src/entities/document.entity.ts +++ b/packages/server/src/entities/document.entity.ts @@ -13,10 +13,13 @@ export class DocumentEntity { @PrimaryColumn() public id: string; - @Column({ type: 'varchar', comment: '文档所属知识库 Id' }) + @Column({ type: 'varchar', comment: '所属组织 Id' }) + public organizationId: string; + + @Column({ type: 'varchar', comment: '所属知识库 Id' }) public wikiId: string; - @Column({ type: 'boolean', default: false, comment: '知识库首页文档' }) + @Column({ type: 'boolean', default: false, comment: '是否为知识库首页文档' }) public isWikiHome: boolean; @Column({ type: 'varchar', comment: '创建用户 Id' }) @@ -54,14 +57,14 @@ export class DocumentEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/message.entity.ts b/packages/server/src/entities/message.entity.ts index e8b653a..13c6620 100644 --- a/packages/server/src/entities/message.entity.ts +++ b/packages/server/src/entities/message.entity.ts @@ -22,14 +22,14 @@ export class MessageEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/organization.entity.ts b/packages/server/src/entities/organization.entity.ts new file mode 100644 index 0000000..6a8f930 --- /dev/null +++ b/packages/server/src/entities/organization.entity.ts @@ -0,0 +1,57 @@ +import { getShortId } from '@helpers/shortid.herlper'; +import { BeforeInsert, Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from 'typeorm'; + +@Entity('organization') +export class OrganizationEntity { + @BeforeInsert() + getShortId() { + this.id = getShortId(); + } + + @PrimaryColumn() + public id: string; + + /** + * 组织名称 + */ + @Column({ type: 'varchar', length: 50, comment: '组织名称' }) + public name: string; + + /** + * 组织描述信息 + */ + @Column({ type: 'varchar', default: '', comment: '描述信息' }) + public description: string; + + /** + * 组织Logo + */ + @Column({ type: 'varchar', default: '', comment: '组织Logo' }) + public logo: string; + + /** + * 创建用户 Id + */ + @Column({ type: 'varchar', comment: '创建用户 Id' }) + public createUserId: string; + + /** + * 是否为个人组织 + */ + @Column({ type: 'boolean', default: false, comment: '是否为个人组织' }) + isPersonal: boolean; + + @CreateDateColumn({ + type: 'timestamp', + name: 'createdAt', + comment: '创建时间', + }) + createdAt: Date; + + @UpdateDateColumn({ + type: 'timestamp', + name: 'updatedAt', + comment: '更新时间', + }) + updatedAt: Date; +} diff --git a/packages/server/src/entities/star.entity.ts b/packages/server/src/entities/star.entity.ts index be2766b..c9c7a4a 100644 --- a/packages/server/src/entities/star.entity.ts +++ b/packages/server/src/entities/star.entity.ts @@ -8,6 +8,9 @@ export class StarEntity { @Column({ type: 'varchar', comment: '用户 Id' }) public userId: string; + @Column({ type: 'varchar', comment: '所属组织 Id' }) + public organizationId: string; + @Column({ type: 'varchar', comment: '知识库 Id' }) public wikiId: string; @@ -16,14 +19,14 @@ export class StarEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/template.entity.ts b/packages/server/src/entities/template.entity.ts index 9ac379e..c298cba 100644 --- a/packages/server/src/entities/template.entity.ts +++ b/packages/server/src/entities/template.entity.ts @@ -31,14 +31,14 @@ export class TemplateEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/user.entity.ts b/packages/server/src/entities/user.entity.ts index aecb806..da02abc 100644 --- a/packages/server/src/entities/user.entity.ts +++ b/packages/server/src/entities/user.entity.ts @@ -55,14 +55,14 @@ export class UserEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/view.entity.ts b/packages/server/src/entities/view.entity.ts index 7a17437..ada864b 100644 --- a/packages/server/src/entities/view.entity.ts +++ b/packages/server/src/entities/view.entity.ts @@ -20,14 +20,14 @@ export class ViewEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/entities/wiki-user.entity.ts b/packages/server/src/entities/wiki-user.entity.ts deleted file mode 100644 index 9ef469e..0000000 --- a/packages/server/src/entities/wiki-user.entity.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { WikiUserRole, WikiUserStatus } from '@think/domains'; -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; - -@Entity('wiki_user') -export class WikiUserEntity { - @PrimaryGeneratedColumn('uuid') - public id: string; - - @Column({ type: 'varchar', comment: '知识库 Id' }) - public wikiId: string; - - @Column({ type: 'varchar', comment: '知识库创建用户 Id' }) - public createUserId: string; - - @Column({ type: 'varchar', comment: '知识库成员 Id' }) - public userId: string; - - @Column({ - type: 'enum', - enum: WikiUserStatus, - default: WikiUserStatus.normal, - comment: '知识库成员状态', - }) - public userStatus: WikiUserStatus; - - @Column({ - type: 'enum', - enum: WikiUserRole, - default: WikiUserRole.normal, - comment: '知识库成员角色', - }) - public userRole: WikiUserRole; - - @CreateDateColumn({ - type: 'timestamp', - name: 'created_at', - comment: '创建时间', - }) - createdAt: Date; - - @UpdateDateColumn({ - type: 'timestamp', - name: 'updated_at', - comment: '更新时间', - }) - updatedAt: Date; -} diff --git a/packages/server/src/entities/wiki.entity.ts b/packages/server/src/entities/wiki.entity.ts index 988b1b3..8679c37 100644 --- a/packages/server/src/entities/wiki.entity.ts +++ b/packages/server/src/entities/wiki.entity.ts @@ -29,6 +29,9 @@ export class WikiEntity { @Column({ type: 'varchar', comment: '创建用户 Id' }) public createUserId: string; + @Column({ type: 'varchar', comment: '所属组织 Id' }) + public organizationId: string; + @Column({ type: 'varchar', comment: '知识库首页文档Id', default: '' }) public homeDocumentId: string; @@ -42,14 +45,14 @@ export class WikiEntity { @CreateDateColumn({ type: 'timestamp', - name: 'created_at', + name: 'createdAt', comment: '创建时间', }) createdAt: Date; @UpdateDateColumn({ type: 'timestamp', - name: 'updated_at', + name: 'updatedAt', comment: '更新时间', }) updatedAt: Date; diff --git a/packages/server/src/guard/document-auth.guard.ts b/packages/server/src/guard/document-auth.guard.ts index 5a5e1ce..55a0cca 100644 --- a/packages/server/src/guard/document-auth.guard.ts +++ b/packages/server/src/guard/document-auth.guard.ts @@ -1,61 +1,61 @@ -import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, SetMetadata } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { JwtService } from '@nestjs/jwt'; -import { DocumentService } from '@services/document.service'; -import { IUser } from '@think/domains'; +// import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, SetMetadata } from '@nestjs/common'; +// import { Reflector } from '@nestjs/core'; +// import { JwtService } from '@nestjs/jwt'; +// import { DocumentService } from '@services/document.service'; +// import { IUser } from '@think/domains'; -const KEY = 'DocumentAuthority'; -export const CheckDocumentAuthority = (auth: 'readable' | 'editable' | 'createUser' | null) => SetMetadata(KEY, auth); +// const KEY = 'DocumentAuthority'; +// export const CheckDocumentAuthority = (auth: 'readable' | 'editable' | 'createUser' | null) => SetMetadata(KEY, auth); -@Injectable() -export class DocumentAuthorityGuard implements CanActivate { - constructor( - private readonly reflector: Reflector, - private readonly jwtService: JwtService, - private readonly documentService: DocumentService - ) {} +// @Injectable() +// export class DocumentAuthorityGuard implements CanActivate { +// constructor( +// private readonly reflector: Reflector, +// private readonly jwtService: JwtService, +// private readonly documentService: DocumentService +// ) {} - async canActivate(context: ExecutionContext): Promise { - const needAuth = this.reflector.get(KEY, context.getHandler()); +// async canActivate(context: ExecutionContext): Promise { +// const needAuth = this.reflector.get(KEY, context.getHandler()); - if (!needAuth) { - return true; - } +// if (!needAuth) { +// return true; +// } - const request = context.switchToHttp().getRequest(); - const token = request?.cookies['token']; - const user = this.jwtService.decode(token) as IUser; - const { params, query, body } = request; - const documentId = params?.id || params?.documentId || query?.id || query?.documentId || body?.documentId; +// const request = context.switchToHttp().getRequest(); +// const token = request?.cookies['token']; +// const user = this.jwtService.decode(token) as IUser; +// const { params, query, body } = request; +// const documentId = params?.id || params?.documentId || query?.id || query?.documentId || body?.documentId; - let document = null; +// let document = null; - if (documentId) { - document = await this.documentService.findById(documentId); - } else { - if (body.wikiId) { - document = await this.documentService.findWikiHomeDocument(body.wikiId); - } - } +// if (documentId) { +// document = await this.documentService.findById(documentId); +// } else { +// if (body.wikiId) { +// document = await this.documentService.findWikiHomeDocument(body.wikiId); +// } +// } - if (!document) { - throw new HttpException('文档不存在', HttpStatus.NOT_FOUND); - } +// if (!document) { +// throw new HttpException('文档不存在', HttpStatus.NOT_FOUND); +// } - if (needAuth === 'createUser') { - if (document.createUserId !== user.id) { - throw new HttpException('您不是该文档的创建者,无法操作', HttpStatus.FORBIDDEN); - } - } else if (needAuth) { - if (!user) { - throw new HttpException('请登录后使用', HttpStatus.UNAUTHORIZED); - } - const authority = await this.documentService.getDocumentAuthority(documentId, user.id); - if (!authority || !authority[needAuth]) { - throw new HttpException('您无权操作此文档', HttpStatus.FORBIDDEN); - } - } +// if (needAuth === 'createUser') { +// if (document.createUserId !== user.id) { +// throw new HttpException('您不是该文档的创建者,无法操作', HttpStatus.FORBIDDEN); +// } +// } else if (needAuth) { +// if (!user) { +// throw new HttpException('请登录后使用', HttpStatus.UNAUTHORIZED); +// } +// const authority = await this.documentService.getDocumentAuthority(documentId, user.id); +// if (!authority || !authority[needAuth]) { +// throw new HttpException('您无权操作此文档', HttpStatus.FORBIDDEN); +// } +// } - return true; - } -} +// return true; +// } +// } diff --git a/packages/server/src/guard/wiki-user.guard.ts b/packages/server/src/guard/wiki-user.guard.ts index 83f7ef6..bc6cdef 100644 --- a/packages/server/src/guard/wiki-user.guard.ts +++ b/packages/server/src/guard/wiki-user.guard.ts @@ -1,51 +1,51 @@ -import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, SetMetadata } from '@nestjs/common'; -import { Reflector } from '@nestjs/core'; -import { JwtService } from '@nestjs/jwt'; -import { WikiService } from '@services/wiki.service'; -import { IUser, WikiUserRole } from '@think/domains'; +// import { CanActivate, ExecutionContext, HttpException, HttpStatus, Injectable, SetMetadata } from '@nestjs/common'; +// import { Reflector } from '@nestjs/core'; +// import { JwtService } from '@nestjs/jwt'; +// import { WikiService } from '@services/wiki.service'; +// import { IUser, WikiUserRole } from '@think/domains'; -const KEY = 'WIKI_USER_ROLE'; +// const KEY = 'WIKI_USER_ROLE'; -/** - * 知识库成员角色检测 - * @param role 不传意味只要是成员即可 - * @returns - */ -export const CheckWikiUserRole = (role: WikiUserRole | null = null) => SetMetadata(KEY, role); +// /** +// * 知识库成员角色检测 +// * @param role 不传意味只要是成员即可 +// * @returns +// */ +// export const CheckWikiUserRole = (role: WikiUserRole | null = null) => SetMetadata(KEY, role); -@Injectable() -export class WikiUserRoleGuard implements CanActivate { - constructor( - private readonly reflector: Reflector, - private readonly jwtService: JwtService, - private readonly wikiService: WikiService - ) {} +// @Injectable() +// export class WikiUserRoleGuard implements CanActivate { +// constructor( +// private readonly reflector: Reflector, +// private readonly jwtService: JwtService, +// private readonly wikiService: WikiService +// ) {} - async canActivate(context: ExecutionContext): Promise { - const targetUserRole = this.reflector.get(KEY, context.getHandler()); - const request = context.switchToHttp().getRequest(); - const token = request?.cookies['token']; - const user = this.jwtService.decode(token) as IUser; +// async canActivate(context: ExecutionContext): Promise { +// const targetUserRole = this.reflector.get(KEY, context.getHandler()); +// const request = context.switchToHttp().getRequest(); +// const token = request?.cookies['token']; +// const user = this.jwtService.decode(token) as IUser; - if (!user) { - throw new HttpException('请登录', HttpStatus.UNAUTHORIZED); - } +// if (!user) { +// throw new HttpException('请登录', HttpStatus.UNAUTHORIZED); +// } - const { params, query, body } = request; - const wikiId = params?.id || params?.wikiId || query?.id || query?.wikiId || body?.wikiId; +// const { params, query, body } = request; +// const wikiId = params?.id || params?.wikiId || query?.id || query?.wikiId || body?.wikiId; - const wiki = await this.wikiService.findById(wikiId); +// const wiki = await this.wikiService.findById(wikiId); - if (!wiki) { - throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); - } +// if (!wiki) { +// throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); +// } - const wikiUser = await this.wikiService.findWikiUser(wikiId, user.id); +// const wikiUser = await this.wikiService.findWikiUser(wikiId, user.id); - if (!wikiUser && targetUserRole && wikiUser.userRole !== targetUserRole) { - throw new HttpException('您无权操作该知识库', HttpStatus.FORBIDDEN); - } +// if (!wikiUser && targetUserRole && wikiUser.userRole !== targetUserRole) { +// throw new HttpException('您无权操作该知识库', HttpStatus.FORBIDDEN); +// } - return true; - } -} +// return true; +// } +// } diff --git a/packages/server/src/helpers/redis.helper.ts b/packages/server/src/helpers/redis.helper.ts new file mode 100644 index 0000000..c057723 --- /dev/null +++ b/packages/server/src/helpers/redis.helper.ts @@ -0,0 +1,32 @@ +import { RedisDBEnum } from '@constants/*'; +import { getConfig } from '@think/config'; +import Redis from 'ioredis'; +import * as lodash from 'lodash'; + +export const buildRedis = (db: RedisDBEnum): Promise => { + const config = getConfig(); + const redisConfig = lodash.get(config, 'db.redis', null); + + if (!redisConfig) { + console.error('[think] Redis 未配置,无法启动 Redis 服务'); + return; + } + + return new Promise((resolve, reject) => { + const redis = new Redis({ + ...redisConfig, + showFriendlyErrorStack: true, + lazyConnect: true, + db, + }); + redis.on('ready', () => { + resolve(redis); + }); + redis.on('error', (err) => { + reject(err); + }); + redis.connect().catch((err) => { + reject(err); + }); + }); +}; diff --git a/packages/server/src/modules/auth.module.ts b/packages/server/src/modules/auth.module.ts new file mode 100644 index 0000000..e31b753 --- /dev/null +++ b/packages/server/src/modules/auth.module.ts @@ -0,0 +1,15 @@ +import { AuthController } from '@controllers/auth.controller'; +import { AuthEntity } from '@entities/auth.entity'; +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { AuthService } from '@services/auth.service'; + +import { UserModule } from './user.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([AuthEntity]), forwardRef(() => UserModule)], + providers: [AuthService], + exports: [AuthService], + controllers: [AuthController], +}) +export class AuthModule {} diff --git a/packages/server/src/modules/document.module.ts b/packages/server/src/modules/document.module.ts index e8b404f..3586fee 100644 --- a/packages/server/src/modules/document.module.ts +++ b/packages/server/src/modules/document.module.ts @@ -1,6 +1,6 @@ import { DocumentController } from '@controllers/document.controller'; import { DocumentEntity } from '@entities/document.entity'; -import { DocumentAuthorityEntity } from '@entities/document-authority.entity'; +import { AuthModule } from '@modules/auth.module'; import { MessageModule } from '@modules/message.module'; import { StarModule } from '@modules/star.module'; import { TemplateModule } from '@modules/template.module'; @@ -14,7 +14,8 @@ import { DocumentService } from '@services/document.service'; @Module({ imports: [ - TypeOrmModule.forFeature([DocumentAuthorityEntity, DocumentEntity]), + TypeOrmModule.forFeature([DocumentEntity]), + forwardRef(() => AuthModule), forwardRef(() => ConfigModule), forwardRef(() => UserModule), forwardRef(() => WikiModule), diff --git a/packages/server/src/modules/organization.module.ts b/packages/server/src/modules/organization.module.ts new file mode 100644 index 0000000..d8ee18b --- /dev/null +++ b/packages/server/src/modules/organization.module.ts @@ -0,0 +1,21 @@ +import { OrganizationController } from '@controllers/organization.controller'; +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 { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { OrganizationService } from '@services/organization.service'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([OrganizationEntity]), + forwardRef(() => UserModule), + forwardRef(() => MessageModule), + forwardRef(() => AuthModule), + ], + providers: [OrganizationService], + exports: [OrganizationService], + controllers: [OrganizationController], +}) +export class OrganizationModule {} diff --git a/packages/server/src/modules/star.module.ts b/packages/server/src/modules/star.module.ts index 01b2d75..e0cc9c8 100644 --- a/packages/server/src/modules/star.module.ts +++ b/packages/server/src/modules/star.module.ts @@ -1,6 +1,8 @@ import { StarController } from '@controllers/star.controller'; import { StarEntity } from '@entities/star.entity'; +import { AuthModule } from '@modules/auth.module'; import { DocumentModule } from '@modules/document.module'; +import { OrganizationModule } from '@modules/organization.module'; import { UserModule } from '@modules/user.module'; import { WikiModule } from '@modules/wiki.module'; import { forwardRef, Module } from '@nestjs/common'; @@ -10,7 +12,9 @@ import { StarService } from '@services/star.service'; @Module({ imports: [ TypeOrmModule.forFeature([StarEntity]), + forwardRef(() => AuthModule), forwardRef(() => UserModule), + forwardRef(() => OrganizationModule), forwardRef(() => WikiModule), forwardRef(() => DocumentModule), ], diff --git a/packages/server/src/modules/user.module.ts b/packages/server/src/modules/user.module.ts index c311a3f..62ea9b1 100644 --- a/packages/server/src/modules/user.module.ts +++ b/packages/server/src/modules/user.module.ts @@ -1,7 +1,10 @@ import { UserController } from '@controllers/user.controller'; import { UserEntity } from '@entities/user.entity'; import { MessageModule } from '@modules/message.module'; +import { OrganizationModule } from '@modules/organization.module'; import { StarModule } from '@modules/star.module'; +import { SystemModule } from '@modules/system.module'; +import { VerifyModule } from '@modules/verify.module'; import { WikiModule } from '@modules/wiki.module'; import { forwardRef, Inject, Injectable, Module, UnauthorizedException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; @@ -13,9 +16,6 @@ import { getConfig } from '@think/config'; import { Request as RequestType } from 'express'; import { ExtractJwt, Strategy } from 'passport-jwt'; -import { SystemModule } from './system.module'; -import { VerifyModule } from './verify.module'; - const config = getConfig(); const jwtConfig = config.jwt as { secretkey: string; @@ -66,6 +66,7 @@ const jwtModule = JwtModule.register({ forwardRef(() => StarModule), forwardRef(() => VerifyModule), forwardRef(() => SystemModule), + forwardRef(() => OrganizationModule), passModule, jwtModule, ], diff --git a/packages/server/src/modules/wiki.module.ts b/packages/server/src/modules/wiki.module.ts index 4efda75..7ba0f8d 100644 --- a/packages/server/src/modules/wiki.module.ts +++ b/packages/server/src/modules/wiki.module.ts @@ -1,8 +1,9 @@ import { WikiController } from '@controllers/wiki.controller'; import { WikiEntity } from '@entities/wiki.entity'; -import { WikiUserEntity } from '@entities/wiki-user.entity'; +import { AuthModule } from '@modules/auth.module'; import { DocumentModule } from '@modules/document.module'; import { MessageModule } from '@modules/message.module'; +import { OrganizationModule } from '@modules/organization.module'; import { StarModule } from '@modules/star.module'; import { UserModule } from '@modules/user.module'; import { ViewModule } from '@modules/view.module'; @@ -12,12 +13,14 @@ import { WikiService } from '@services/wiki.service'; @Module({ imports: [ - TypeOrmModule.forFeature([WikiEntity, WikiUserEntity]), + TypeOrmModule.forFeature([WikiEntity]), + forwardRef(() => AuthModule), forwardRef(() => UserModule), forwardRef(() => DocumentModule), forwardRef(() => MessageModule), forwardRef(() => ViewModule), forwardRef(() => StarModule), + forwardRef(() => OrganizationModule), ], providers: [WikiService], exports: [WikiService], diff --git a/packages/server/src/services/auth.service.ts b/packages/server/src/services/auth.service.ts new file mode 100644 index 0000000..ca2fede --- /dev/null +++ b/packages/server/src/services/auth.service.ts @@ -0,0 +1,435 @@ +import { AuthDto } from '@dtos/auth.dto'; +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 * as lodash from 'lodash'; +import { Repository } from 'typeorm'; + +@Injectable() +export class AuthService { + constructor( + @InjectRepository(AuthEntity) + private readonly authRepo: Repository, + + @Inject(forwardRef(() => UserService)) + private readonly userService: UserService + ) {} + + /** + * 获取用户权限 + * @param user + * @param auth + * @returns + */ + public async getAuth(userId: IUser['id'], dto: Omit) { + const conditions = { userId, ...dto }; + const userAuth = await this.authRepo.findOne(conditions); + return userAuth; + } + + /** + * 创建或更新用户权限 + * @param user + * @param auth + * @returns + */ + public async createOrUpdateAuth(userId: IUser['id'], auth: AuthDto) { + const targetAuth = auth.auth; + delete auth.auth; + const wrappedAuth = { userId, ...auth }; + const oldAuth = await this.authRepo.findOne(wrappedAuth); + + // TODO: 这里可以判断权限继承 + + let newAuth: AuthEntity; + + if (oldAuth) { + newAuth = await this.authRepo.save(await this.authRepo.merge(oldAuth, wrappedAuth, { auth: targetAuth })); + } else { + newAuth = await this.authRepo.save(await this.authRepo.create({ ...wrappedAuth, auth: targetAuth })); + } + + if (newAuth.organizationId && !newAuth.wikiId && !newAuth.documentId) { + // 用户被添加到组织,在组织内添加对应权限 + const wikisAuth = await this.getWikisInOrganization(newAuth.organizationId); + await Promise.all( + wikisAuth.map((wikiAuth) => { + return this.createOrUpdateAuth(newAuth.userId, { + auth: newAuth.auth, + organizationId: newAuth.organizationId, + wikiId: wikiAuth.wikiId, + documentId: null, + }); + }) + ); + } else if (newAuth.organizationId && newAuth.wikiId && !newAuth.documentId) { + // 用户被添加到知识库,在知识库内添加对应权限 + const docsAuth = await this.getDocumentsInWiki(newAuth.organizationId, newAuth.wikiId); + await Promise.all( + docsAuth.map((auth) => { + return this.createOrUpdateAuth(newAuth.userId, { + auth: newAuth.auth, + organizationId: newAuth.organizationId, + wikiId: newAuth.wikiId, + documentId: auth.documentId, + }); + }) + ); + } + } + + /** + * 用户是否可查看目标 + * @param userId + * @param dto + * @returns + */ + async canView(userId: IUser['id'], dto: Omit) { + const conditions: Partial = { + userId, + organizationId: dto.organizationId, + wikiId: dto.wikiId || null, + documentId: dto.documentId || null, + }; + + const userAuth = await this.authRepo.findOne(conditions); + + if (!userAuth || userAuth.auth === AuthEnum.noAccess) { + throw new HttpException('您没有权限查看', HttpStatus.FORBIDDEN); + } + + return userAuth; + } + + /** + * 用户是否可编辑目标 + * @param userId + * @param dto + * @returns + */ + async canEdit(userId: IUser['id'], dto: Omit) { + const conditions: Partial = { + userId, + organizationId: dto.organizationId, + wikiId: dto.wikiId || null, + documentId: dto.documentId || null, + }; + + const userAuth = await this.authRepo.findOne(conditions); + + if (!userAuth || ![AuthEnum.creator, AuthEnum.admin].includes(userAuth.auth)) { + throw new HttpException('您没有权限编辑', HttpStatus.FORBIDDEN); + } + + return userAuth; + } + + /** + * 用户是否可删除目标 + * @param userId + * @param dto + * @returns + */ + async canDelete(userId: IUser['id'], dto: Omit) { + const conditions: Partial = { + userId, + organizationId: dto.organizationId, + wikiId: dto.wikiId || null, + documentId: dto.documentId || null, + }; + + const userAuth = await this.authRepo.findOne(conditions); + + if (!userAuth || ![AuthEnum.creator].includes(userAuth.auth)) { + throw new HttpException('您没有权限删除', HttpStatus.FORBIDDEN); + } + + return userAuth; + } + + /** + * 操作他人权限 + * @param currentUserId + * @param targetUserId + * @param dto + */ + private async operateOtherUserAuth(currentUserId: IUser['id'], targetUserId: IUser['id'], dto: AuthDto) { + const conditions: Partial = { + organizationId: dto.organizationId, + wikiId: dto.wikiId || null, + documentId: dto.documentId || null, + }; + + const currentUserAuth = await this.authRepo.findOne({ + userId: currentUserId, + ...conditions, + }); + + if (!currentUserAuth) { + throw new HttpException('您没有权限操作1', HttpStatus.FORBIDDEN); + } + + // 仅创建者、管理员可操作他人权限 + if (![AuthEnum.creator, AuthEnum.admin].includes(currentUserAuth.auth)) { + throw new HttpException('您没有权限操作2', HttpStatus.FORBIDDEN); + } + + // 仅创建者可赋予他人创建者、管理员权限 + if ([AuthEnum.creator, AuthEnum.admin].includes(dto.auth) && currentUserAuth.auth !== AuthEnum.creator) { + throw new HttpException('您没有权限操作3', HttpStatus.FORBIDDEN); + } + + const maybeTargetUserAuth = await this.authRepo.findOne({ + userId: targetUserId, + ...conditions, + }); + + if (maybeTargetUserAuth) { + // 对方是创建者,无权操作 + if (maybeTargetUserAuth.auth === AuthEnum.creator) { + throw new HttpException('您没有权限操作4', HttpStatus.FORBIDDEN); + } + + // 对方是管理员,仅创建者可操作 + if (maybeTargetUserAuth.auth === AuthEnum.admin && currentUserAuth.auth !== AuthEnum.creator) { + throw new HttpException('您没有权限操作5', HttpStatus.FORBIDDEN); + } + } + } + + /** + * 为他人创建或更新权限 + * @param currentUserId + * @param targetUserId + * @param dto + */ + async createOrUpdateOtherUserAuth(currentUserId: IUser['id'], targetUserId: IUser['id'], dto: AuthDto) { + await this.operateOtherUserAuth(currentUserId, targetUserId, dto); + await this.createOrUpdateAuth(targetUserId, dto); + } + + /** + * 删除他人权限 + * @param currentUserId + * @param targetUserId + * @param dto + */ + async deleteOtherUserAuth(currentUserId: IUser['id'], targetUserId: IUser['id'], dto: AuthDto) { + await this.operateOtherUserAuth(currentUserId, targetUserId, dto); + + const conditions: Partial = { + userId: targetUserId, + auth: dto.auth, + organizationId: dto.organizationId, + wikiId: dto.wikiId || null, + documentId: dto.documentId || null, + }; + + const targetUserAuth = await this.authRepo.findOne(conditions); + await this.authRepo.remove(targetUserAuth); + + if (targetUserAuth.organizationId && !targetUserAuth.wikiId && !targetUserAuth.documentId) { + // 用户被从组织删除,需要删除在组织内的所有权限 + const res = await this.authRepo.find({ + userId: targetUserAuth.userId, + organizationId: targetUserAuth.organizationId, + }); + await this.authRepo.remove(res); + } else if (targetUserAuth.organizationId && targetUserAuth.wikiId && !targetUserAuth.documentId) { + // 用户被从知识库删除,需要删除在知识库的所有权限 + const res = await this.authRepo.find({ + userId: targetUserAuth.userId, + organizationId: targetUserAuth.organizationId, + wikiId: targetUserAuth.wikiId, + }); + await this.authRepo.remove(res); + } + } + + /** + * 获取用户可查看的组织 id 列表 + * @param userId + */ + async getUserCanViewOrganizationIds(userId: IUser['id']) { + const data = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member] }) + .andWhere('auth.userId=:userId') + .andWhere('auth.wikiId is NULL') + .andWhere('auth.documentId is NULL') + .setParameter('userId', userId) + .getMany(); + + return (data || []).map((d) => d.organizationId); + } + + /** + * 获取指定组织内所有知识库 + * @param userId + */ + async getWikisInOrganization(organizationId: IOrganization['id']) { + const data = await this.authRepo + .createQueryBuilder('auth') + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.wikiId is NOT NULL') + .andWhere('auth.documentId is NULL') + .setParameter('organizationId', organizationId) + .getMany(); + + return lodash.uniqBy(data || [], (w) => w.wikiId); + } + + /** + * 获取指定知识库内所有文档 + * @param userId + */ + async getDocumentsInWiki(organizationId: IOrganization['id'], wikiId: IWiki['id']) { + const data = await this.authRepo + .createQueryBuilder('auth') + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.wikiId=:wikiId') + .andWhere('auth.documentId IS NOT NULL') + .setParameter('organizationId', organizationId) + .setParameter('wikiId', wikiId) + .getMany(); + + return lodash.uniqBy(data || [], (w) => w.documentId); + } + + /** + * 获取指定组织的所有用户权限 + * @param userId + */ + async getUsersAuthInOrganization(organizationId: IOrganization['id']) { + const [data, total] = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { + types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess], + }) + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.wikiId is NULL') + .andWhere('auth.documentId is NULL') + .setParameter('organizationId', organizationId) + .getManyAndCount(); + + return { data: data || [], total }; + } + + /** + * 获取指定知识库的所有用户权限 + * @param userId + */ + async getUsersAuthInWiki(organizationId: IOrganization['id'], wikiId: IWiki['id']) { + const [data, total] = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { + types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess], + }) + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.wikiId=:wikiId') + .andWhere('auth.documentId is NULL') + .setParameter('organizationId', organizationId) + .setParameter('wikiId', wikiId) + .getManyAndCount(); + + return { data: data || [], total }; + } + + /** + * 获取用户在指定组织可查看的知识库列表 + * @param userId + */ + async getUserCanViewWikisInOrganization(userId: IUser['id'], organizationId: IOrganization['id']) { + const [data, total] = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { + types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess], + }) + .andWhere('auth.userId=:userId') + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.documentId is NULL') + .setParameter('userId', userId) + .setParameter('organizationId', organizationId) + .getManyAndCount(); + + return { data: data || [], total }; + } + + /** + * 获取用户在指定组织创建的知识库列表 + * @param userId + */ + async getUserCreateWikisInOrganization(userId: IUser['id'], organizationId: IOrganization['id']) { + const [data, total] = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth=:auth') + .andWhere('auth.userId=:userId') + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.documentId is NULL') + .setParameter('auth', AuthEnum.creator) + .setParameter('userId', userId) + .setParameter('organizationId', organizationId) + .getManyAndCount(); + + return { data: data || [], total }; + } + + /** + * 获取用户在指定组织参与的知识库列表 + * @param userId + */ + async getUserJoinWikisInOrganization(userId: IUser['id'], organizationId: IOrganization['id']) { + const [data, total] = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member] }) + .andWhere('auth.userId=:userId') + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.documentId is NULL') + .setParameter('userId', userId) + .setParameter('organizationId', organizationId) + .getManyAndCount(); + + return { data: data || [], total }; + } + + /** + * 获取用户在指定知识库可查看的所有文档 + * @param userId + */ + async getUserCanViewDocumentsInWiki(organizationId: IOrganization['id'], wikiId: IWiki['id']) { + const data = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member] }) + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.wikiId=:wikiId') + .andWhere('auth.documentId IS NOT NULL') + .setParameter('organizationId', organizationId) + .setParameter('wikiId', wikiId) + .getMany(); + + return data; + } + + /** + * 获取指定知识库的所有用户权限 + * @param userId + */ + async getUsersAuthInDocument(organizationId: IOrganization['id'], wikiId: IWiki['id'], documentId: IDocument['id']) { + const [data, total] = await this.authRepo + .createQueryBuilder('auth') + .where('auth.auth IN (:...types)', { + types: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member, AuthEnum.noAccess], + }) + .andWhere('auth.organizationId=:organizationId') + .andWhere('auth.wikiId=:wikiId') + .andWhere('auth.documentId=:documentId') + .setParameter('organizationId', organizationId) + .setParameter('wikiId', wikiId) + .setParameter('documentId', documentId) + .getManyAndCount(); + + return { data: data || [], total }; + } +} diff --git a/packages/server/src/services/collaboration.service.ts b/packages/server/src/services/collaboration.service.ts index 1b0460e..5d42019 100644 --- a/packages/server/src/services/collaboration.service.ts +++ b/packages/server/src/services/collaboration.service.ts @@ -101,13 +101,14 @@ export class CollaborationService { } return { user: { name: '匿名用户' } }; } else { - const authority = await this.documentService.getDocumentAuthority(targetId, user.id); - if (!authority.readable) { - throw new HttpException('您无权查看此文档', HttpStatus.FORBIDDEN); - } - if (!authority.editable) { - connection.readOnly = true; - } + // TODO:权限校验 + // const authority = await this.documentService.getDocumentAuthority(targetId, user.id); + // if (!authority.readable) { + // throw new HttpException('您无权查看此文档', HttpStatus.FORBIDDEN); + // } + // if (!authority.editable) { + // connection.readOnly = true; + // } return { user, }; diff --git a/packages/server/src/services/comment.service.ts b/packages/server/src/services/comment.service.ts index 1eea598..78fab57 100644 --- a/packages/server/src/services/comment.service.ts +++ b/packages/server/src/services/comment.service.ts @@ -6,7 +6,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { DocumentService } from '@services/document.service'; import { MessageService } from '@services/message.service'; import { OutUser, UserService } from '@services/user.service'; -import { DocumentStatus } from '@think/domains'; +import { buildMessageURL, DocumentStatus } from '@think/domains'; import { Repository } from 'typeorm'; @Injectable() @@ -50,15 +50,14 @@ export class CommentService { const doc = await this.documentService.findById(documentId); if (doc.status !== DocumentStatus.public) { - 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); - } + // 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 { text: uaText } = parseUserAgent(userAgent); @@ -75,17 +74,21 @@ 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 wikiUsersAuth = await this.documentService.getDocUsersWithoutAuthCheck(user, documentId); - await Promise.all( - wikiUsersAuth.map(async (userAuth) => { - await this.messageService.notify(userAuth.user, { - title: `文档「${doc.title}」收到新评论`, - message: `文档「${doc.title}」收到新评论,快去看看!`, - url: `/wiki/${doc.wikiId}/document/${doc.id}`, - }); - }) - ); + // 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, + // }), + // }); + // }) + // ); return ret; } @@ -177,17 +180,21 @@ 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); + // 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: `/wiki/${doc.wikiId}/document/${doc.id}`, - }); - }) - ); + // 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, + // }), + // }); + // }) + // ); return this.commentRepo.save(newData); } @@ -198,16 +205,20 @@ 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: `/wiki/${doc.wikiId}/document/${doc.id}`, - }); - }) - ); + // 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, + // }), + // }); + // }) + // ); return this.commentRepo.remove(data); } } diff --git a/packages/server/src/services/document-version.service.ts b/packages/server/src/services/document-version.service.ts index 2422280..37e8cc5 100644 --- a/packages/server/src/services/document-version.service.ts +++ b/packages/server/src/services/document-version.service.ts @@ -1,3 +1,5 @@ +import { RedisDBEnum } from '@constants/*'; +import { buildRedis } from '@helpers/redis.helper'; import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { getConfig } from '@think/config'; import { IDocument, IUser } from '@think/domains'; @@ -51,36 +53,14 @@ export class DocumentVersionService { private async init() { const config = getConfig(); - const redisConfig = lodash.get(config, 'db.redis', null); - - if (!redisConfig) { - console.error('[think] Redis 未配置,无法启动文档版本服务'); - return; - } - this.max = lodash.get(config, 'server.maxDocumentVersion', 0) as number; try { - const redis = new Redis({ - ...redisConfig, - db: 0, - showFriendlyErrorStack: true, - lazyConnect: true, - }); - redis.on('ready', () => { - console.log('[think] 文档版本服务启动成功'); - this.redis = redis; - this.error = null; - }); - redis.on('error', (e) => { - console.error(`[think] 文档版本服务启动错误: "${e}"`); - }); - redis.connect().catch(() => { - this.redis = null; - this.error = '[think] 文档版本服务启动失败!'; - }); + this.redis = await buildRedis(RedisDBEnum.documentVersion); + console.log('[think] 文档版本服务启动成功'); } catch (e) { - // + this.error = e.message; + console.error(`[think] 文档版本服务启动错误: "${e.message}"`); } } diff --git a/packages/server/src/services/document.service.ts b/packages/server/src/services/document.service.ts index c6e15ca..050b6a3 100644 --- a/packages/server/src/services/document.service.ts +++ b/packages/server/src/services/document.service.ts @@ -1,12 +1,13 @@ +import { OperateUserAuthDto } from '@dtos/auth.dto'; import { CreateDocumentDto } from '@dtos/create-document.dto'; -import { DocAuthDto } from '@dtos/doc-auth.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'; -import { DocumentAuthorityEntity } from '@entities/document-authority.entity'; import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; +import { AuthService } from '@services/auth.service'; import { CollaborationService } from '@services/collaboration.service'; import { DocumentVersionService } from '@services/document-version.service'; import { MessageService } from '@services/message.service'; @@ -15,7 +16,7 @@ import { OutUser, UserService } from '@services/user.service'; import { ViewService } from '@services/view.service'; import { WikiService } from '@services/wiki.service'; import { EMPTY_DOCUMNENT } from '@think/constants'; -import { DocumentStatus, WikiUserRole } from '@think/domains'; +import { AuthEnum, buildMessageURL, DocumentStatus, WikiUserRole } from '@think/domains'; import { instanceToPlain } from 'class-transformer'; import * as lodash from 'lodash'; import { Repository } from 'typeorm'; @@ -26,8 +27,8 @@ export class DocumentService { private documentVersionService: DocumentVersionService; constructor( - @InjectRepository(DocumentAuthorityEntity) - public readonly documentAuthorityRepo: Repository, + // @InjectRepository(DocumentUserEntity) + // public readonly documentUserRepo: Repository, @InjectRepository(DocumentEntity) public readonly documentRepo: Repository, @@ -35,6 +36,9 @@ export class DocumentService { @Inject(forwardRef(() => ConfigService)) private readonly configService: ConfigService, + @Inject(forwardRef(() => AuthService)) + private readonly authService: AuthService, + @Inject(forwardRef(() => MessageService)) private readonly messageService: MessageService, @@ -91,81 +95,89 @@ 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.documentAuthorityRepo.findOne({ - documentId, - userId, - }); - return authority; - } + // /** + // * 获取用户在指定文档的权限 + // * @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); - } + // */ + // 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; + // const isCurrentUserCreator = currentUserId === doc.createUserId; + // const isTargetUserCreator = targetUserId === doc.createUserId; - if (!isCurrentUserCreator) { - throw new HttpException('您不是文档创建者,无权操作', HttpStatus.FORBIDDEN); - } + // if (!isCurrentUserCreator) { + // throw new HttpException('您不是文档创建者,无权操作', HttpStatus.FORBIDDEN); + // } - const targetUser = await this.userService.findOne(targetUserId); - const targetDocAuth = await this.documentAuthorityRepo.findOne({ - documentId, - userId: targetUserId, - }); + // 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.documentAuthorityRepo.create(documentUser); - const ret = await this.documentAuthorityRepo.save(res); + // 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: `/wiki/${doc.wikiId}/document/${doc.id}`, - }); + // 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.documentAuthorityRepo.merge(targetDocAuth, newData); - const ret = await this.documentAuthorityRepo.save(res); + // 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: `/wiki/${doc.wikiId}/document/${doc.id}`, - }); + // 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; - } - } + // return ret; + // } + // } /** * 添加文档成员 @@ -174,26 +186,24 @@ export class DocumentService { * @param dto * @returns */ - async addDocUser(user: OutUser, dto: DocAuthDto) { + async addDocUser(user: OutUser, documentId, dto: OperateUserAuthDto) { const targetUser = await this.userService.findOne({ name: dto.userName }); if (!targetUser) { - throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); + throw new HttpException('用户不存在', HttpStatus.NOT_FOUND); } - const doc = await this.documentRepo.findOne(dto.documentId); + const doc = await this.documentRepo.findOne(documentId); - await this.wikiService.addWikiUser(user, doc.wikiId, { - userName: targetUser.name, - userRole: WikiUserRole.normal, - }); + if (!doc) { + throw new HttpException('目标文档不存在', HttpStatus.NOT_FOUND); + } - return await this.operateDocumentAuth({ - currentUserId: user.id, - documentId: dto.documentId, - targetUserId: targetUser.id, - readable: dto.readable, - editable: dto.editable, + await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: doc.organizationId, + wikiId: doc.wikiId, + documentId: doc.id, }); } @@ -204,14 +214,24 @@ export class DocumentService { * @param dto * @returns */ - async updateDocUser(user: OutUser, dto: DocAuthDto): Promise { + async updateDocUser(user: OutUser, documentId, dto: OperateUserAuthDto) { const targetUser = await this.userService.findOne({ name: dto.userName }); - return this.operateDocumentAuth({ - currentUserId: user.id, - documentId: dto.documentId, - targetUserId: targetUser.id, - readable: dto.readable, - editable: dto.editable, + + if (!targetUser) { + throw new HttpException('用户不存在', HttpStatus.NOT_FOUND); + } + + const doc = await this.documentRepo.findOne(documentId); + + if (!doc) { + throw new HttpException('目标文档不存在', HttpStatus.NOT_FOUND); + } + + await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: doc.organizationId, + wikiId: doc.wikiId, + documentId: doc.id, }); } @@ -222,26 +242,25 @@ export class DocumentService { * @param dto * @returns */ - async deleteDocUser(user: OutUser, dto: DocAuthDto): Promise { - const doc = await this.documentRepo.findOne({ id: dto.documentId }); + async deleteDocUser(user: OutUser, documentId, dto: OperateUserAuthDto) { const targetUser = await this.userService.findOne({ name: dto.userName }); - if (targetUser.id === doc.createUserId) { - throw new HttpException('无法删除文档创建者', HttpStatus.FORBIDDEN); + if (!targetUser) { + throw new HttpException('用户不存在', HttpStatus.NOT_FOUND); } - const targetDocAuth = await this.documentAuthorityRepo.findOne({ - documentId: dto.documentId, - userId: targetUser.id, - }); + const doc = await this.documentRepo.findOne(documentId); - await this.messageService.notify(targetUser, { - title: `您已被移出文档「${doc.title}」`, - message: `${user.name}已将您从文档「${doc.title}」移出!`, - url: `/wiki/${doc.wikiId}/document/${doc.id}`, - }); + if (!doc) { + throw new HttpException('目标文档不存在', HttpStatus.NOT_FOUND); + } - await this.documentAuthorityRepo.remove(targetDocAuth); + await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: doc.organizationId, + wikiId: doc.wikiId, + documentId: doc.id, + }); } /** @@ -256,44 +275,50 @@ export class DocumentService { throw new HttpException('文档不存在', HttpStatus.BAD_REQUEST); } - const auth = await this.getDocumentAuthority(documentId, user.id); + await this.authService.canView(user.id, { + organizationId: doc.organizationId, + wikiId: doc.wikiId, + documentId: doc.id, + }); - if (!auth.readable) { - throw new HttpException('您无权查看', HttpStatus.FORBIDDEN); - } + const { data: auths, total } = await this.authService.getUsersAuthInDocument( + doc.organizationId, + doc.wikiId, + doc.id + ); - const data = await this.documentAuthorityRepo.find({ documentId }); - - return await Promise.all( - data.map(async (auth) => { + const res = await Promise.all( + auths.map(async (auth) => { const user = await this.userService.findById(auth.userId); return { auth, user }; }) ); + + return { data: res, total }; } - /** - * 获取文档成员 - * 忽略权限检查 - * @param userId - * @param wikiId - */ - async getDocUsersWithoutAuthCheck(user: OutUser, documentId) { - const doc = await this.documentRepo.findOne({ id: documentId }); + // /** + // * 获取文档成员 + // * 忽略权限检查 + // * @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); - } + // if (!doc) { + // throw new HttpException('文档不存在', HttpStatus.BAD_REQUEST); + // } - const data = await this.documentAuthorityRepo.find({ documentId }); + // 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 }; - }) - ); - } + // return await Promise.all( + // data.map(async (auth) => { + // const user = await this.userService.findById(auth.userId); + // return { auth, user }; + // }) + // ); + // } /** * 创建文档 @@ -303,9 +328,10 @@ export class DocumentService { * @returns */ public async createDocument(user: OutUser, dto: CreateDocumentDto, isWikiHome = false) { - await this.wikiService.getWikiUserDetail({ + await this.authService.canView(user.id, { + organizationId: dto.organizationId, wikiId: dto.wikiId, - userId: user.id, + documentId: null, }); const [docs] = await this.documentRepo.findAndCount({ createUserId: user.id }); @@ -348,47 +374,44 @@ export class DocumentService { } } - const res = await this.documentRepo.create(data); - const document = await this.documentRepo.save(res); - - // 知识库成员权限继承 - const wikiUsers = await this.wikiService.getWikiUsers(dto.wikiId); + const document = await this.documentRepo.save(await this.documentRepo.create(data)); + const { data: userAuths } = await this.authService.getUsersAuthInWiki(document.organizationId, document.wikiId); await Promise.all([ - await this.operateDocumentAuth({ - currentUserId: user.id, + ...userAuths + .filter((userAuth) => userAuth.userId !== user.id) + .map((userAuth) => { + return this.authService.createOrUpdateAuth(userAuth.userId, { + auth: userAuth.auth, + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); + }), + await this.authService.createOrUpdateAuth(user.id, { + auth: AuthEnum.creator, + organizationId: document.organizationId, + wikiId: document.wikiId, documentId: document.id, - targetUserId: user.id, - readable: true, - editable: true, - }), - ...wikiUsers.map(async (wikiUser) => { - await this.operateDocumentAuth({ - currentUserId: user.id, - documentId: document.id, - targetUserId: wikiUser.userId, - readable: true, - editable: wikiUser.userRole === WikiUserRole.admin, - }); }), ]); - return instanceToPlain(res); + 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); + // }) + // ); + // } /** * 删除文档 @@ -402,6 +425,13 @@ export class DocumentService { throw new HttpException('该文档作为知识库首页使用,无法删除', HttpStatus.FORBIDDEN); } } + + await this.authService.canDelete(user.id, { + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); + const children = await this.documentRepo.find({ parentDocumentId: document.id, }); @@ -417,9 +447,11 @@ export class DocumentService { }) ); } - const auths = await this.documentAuthorityRepo.find({ documentId }); - await this.documentAuthorityRepo.remove(auths); - await this.viewService.deleteViews(documentId); + + // TODO:权限删除 + // const auths = await this.documentUserRepo.find({ documentId }); + // await this.documentUserRepo.remove(auths); + return this.documentRepo.remove(document); } @@ -432,6 +464,13 @@ export class DocumentService { */ public async updateDocument(user: OutUser, documentId: string, dto: UpdateDocumentDto) { const document = await this.documentRepo.findOne(documentId); + + await this.authService.canEdit(user.id, { + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); + const res = await this.documentRepo.create({ ...document, ...dto }); const ret = await this.documentRepo.save(res); return instanceToPlain(ret); @@ -443,20 +482,33 @@ export class DocumentService { * @param documentId * @returns */ - public async getDocumentDetail(user: OutUser, documentId: string, userAgent) { - // 异步记录访问 - this.viewService.create({ userId: user.id, documentId, userAgent }); - const [document, authority, views] = await Promise.all([ + public async getDocumentDetail(user, documentId: string) { + const [document, views] = await Promise.all([ this.documentRepo.findOne(documentId), - this.documentAuthorityRepo.findOne({ - documentId, - userId: user.id, - }), this.viewService.getDocumentTotalViews(documentId), ]); + await this.authService.canView(user.id, { + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); + const authority = await this.authService.getAuth(user.id, { + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); + // 异步记录访问 + this.viewService.create(user, document); const doc = lodash.omit(instanceToPlain(document), ['state']); const createUser = await this.userService.findById(doc.createUserId); - return { document: { ...doc, views, createUser }, authority }; + return { + document: { ...doc, views, createUser }, + authority: { + ...authority, + readable: [AuthEnum.creator, AuthEnum.admin, AuthEnum.member].includes(authority.auth), + editable: [AuthEnum.creator, AuthEnum.admin].includes(authority.auth), + }, + }; } /** @@ -476,6 +528,11 @@ export class DocumentService { */ async shareDocument(user: OutUser, documentId, dto: ShareDocumentDto, nextStatus = null) { const document = await this.documentRepo.findOne(documentId); + await this.authService.canEdit(user.id, { + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); nextStatus = !nextStatus ? document.status === DocumentStatus.private ? DocumentStatus.public @@ -494,7 +551,7 @@ export class DocumentService { * 获取公开文档详情 * @param documentId */ - async getPublicDocumentDetail(documentId, dto: ShareDocumentDto, userAgent) { + async getPublicDocumentDetail(documentId, dto: ShareDocumentDto) { const document = await this.documentRepo.findOne(documentId); if (document.sharePassword && !dto.sharePassword) { @@ -512,7 +569,7 @@ export class DocumentService { this.wikiService.getPublicWikiDetail(document.wikiId), ]); // 异步创建 - this.viewService.create({ userId: 'public', documentId, userAgent }); + this.viewService.create(null, document); return { ...doc, views, wiki, createUser }; } @@ -536,6 +593,12 @@ export class DocumentService { ? await this.documentRepo.findOne(documentId) : await this.documentRepo.findOne({ wikiId, isWikiHome: true }); + await this.authService.canView(user.id, { + organizationId: document.organizationId, + wikiId: document.wikiId, + documentId: document.id, + }); + let unSortDocuments = []; if (document.isWikiHome) { @@ -573,13 +636,6 @@ export class DocumentService { 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 docs; } @@ -629,13 +685,6 @@ export class DocumentService { 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 docs; } @@ -645,8 +694,14 @@ export class DocumentService { * @param dto * @returns */ - public async getRecentDocuments(user: OutUser) { - const records = await this.viewService.getUserRecentVisitedDocuments(user.id); + public async getRecentDocuments(user: OutUser, organizationId) { + await this.authService.canView(user.id, { + organizationId: organizationId, + wikiId: null, + documentId: null, + }); + + const records = await this.viewService.getUserRecentVisitedDocuments(user.id, organizationId); const documentIds = records.map((r) => r.documentId); const visitedAtMap = records.reduce((a, c) => { a[c.documentId] = c.visitedAt; @@ -684,32 +739,29 @@ export class DocumentService { * 关键词搜索文档 * @param keyword */ - async search(user, keyword) { + async search(user, organizationId, keyword) { const res = await this.documentRepo .createQueryBuilder('document') - .where('document.title LIKE :keyword') + .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.documentAuthorityRepo.findOne({ - documentId: doc.id, - userId: user.id, - }); - return auth && auth.readable ? doc : null; - }) - ); - - const data = ret.filter(Boolean); - - // const withCreateUserRes = await Promise.all( - // data.map(async (doc) => { - // const createUser = await this.userService.findById(doc.createUserId); - // return { createUser, ...doc }; + // 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; // }) // ); - return data; + // const data = ret.filter(Boolean); + + return res; } } diff --git a/packages/server/src/services/organization.service.ts b/packages/server/src/services/organization.service.ts new file mode 100644 index 0000000..664e912 --- /dev/null +++ b/packages/server/src/services/organization.service.ts @@ -0,0 +1,274 @@ +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'; +import { InjectRepository } from '@nestjs/typeorm'; +import { AuthService } from '@services/auth.service'; +import { MessageService } from '@services/message.service'; +import { UserService } from '@services/user.service'; +import { AuthEnum, buildMessageURL, IOrganization, IUser } from '@think/domains'; +import { Repository } from 'typeorm'; + +@Injectable() +export class OrganizationService { + constructor( + @InjectRepository(OrganizationEntity) + private readonly organizationRepo: Repository, + + @Inject(forwardRef(() => AuthService)) + private readonly authService: AuthService, + + @Inject(forwardRef(() => UserService)) + private readonly userService: UserService, + + @Inject(forwardRef(() => MessageService)) + private readonly messageService: MessageService + ) {} + + public async findById(id: string) { + return await this.organizationRepo.findOne(id); + } + + public async findByIds(ids) { + return await this.organizationRepo.findByIds(ids); + } + + /** + * 创建组织 + * @param user + * @param dto + * @returns + */ + public async createOrganization(user: IUser, dto: CreateOrganizationDto) { + const [, count] = await this.organizationRepo.findAndCount({ createUserId: user.id }); + + if (count >= 5) { + throw new HttpException('个人可创建组织上限为 5 个', HttpStatus.FORBIDDEN); + } + + const data = { + ...dto, + createUserId: user.id, + }; + + if (await this.organizationRepo.findOne({ name: data.name })) { + throw new HttpException('该组织已存在', HttpStatus.BAD_REQUEST); + } + + const organization = await this.organizationRepo.save(await this.organizationRepo.create(data)); + + await this.authService.createOrUpdateAuth(user.id, { + auth: AuthEnum.creator, + organizationId: organization.id, + wikiId: null, + documentId: null, + }); + + return organization; + } + + /** + * 更新组织信息 + * @param user + * @param dto + */ + public async updateOrganization(user: IUser, organizationId: IOrganization['id'], dto: CreateOrganizationDto) { + await this.authService.canEdit(user.id, { + organizationId, + wikiId: null, + documentId: null, + }); + + const oldData = await this.organizationRepo.findOne(organizationId); + + if (!oldData) { + throw new HttpException('目标组织不存在', HttpStatus.NOT_FOUND); + } + + return await this.organizationRepo.save(await this.organizationRepo.merge(oldData, dto)); + } + + /** + * 获取用户个人组织 + * @param user + * @returns + */ + public async getPersonalOrganization(user: IUser) { + const organization = await this.organizationRepo.findOne({ createUserId: user.id, isPersonal: true }); + return organization; + } + + /** + * 获取用户除个人组织外可访问的组织 + * @param user + */ + public async getUserOrganizations(user: IUser) { + const ids = await this.authService.getUserCanViewOrganizationIds(user.id); + return await this.organizationRepo.findByIds(ids); + } + + /** + * 获取组织详情 + * @param user + * @returns + */ + public async getOrganizationDetail(user: IUser, id: IOrganization['id']) { + await this.authService.canView(user.id, { + organizationId: id, + wikiId: null, + documentId: null, + }); + const organization = await this.organizationRepo.findOne({ id }); + return organization; + } + + /** + * 获取组织成员 + * @param user + * @param shortId + * @returns + */ + public async getMembers(user: IUser, id: IOrganization['id']) { + const organization = await this.organizationRepo.findOne({ id }); + + if (!organization) { + throw new HttpException('组织不存在', HttpStatus.NOT_FOUND); + } + + await this.authService.canView(user.id, { + organizationId: id, + wikiId: null, + documentId: null, + }); + + const { data: usersAuth, total } = await this.authService.getUsersAuthInOrganization(organization.id); + + const userIds = usersAuth.map((auth) => auth.userId); + const users = await this.userService.findByIds(userIds); + + const withUserData = usersAuth.map((auth) => { + return { + auth, + user: users.find((user) => user.id === auth.userId), + }; + }); + + return { data: withUserData, total }; + } + + /** + * 添加组织成员 + * @param user + * @param wikiId + * @param dto + * @returns + */ + async addMember(user: UserEntity, organizationId, dto: OperateUserAuthDto) { + const organization = await this.organizationRepo.findOne(organizationId); + + if (!organization) { + throw new HttpException('组织不存在', HttpStatus.NOT_FOUND); + } + + const targetUser = await this.userService.findOne({ name: dto.userName }); + + if (!targetUser) { + throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); + } + + const ret = await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: organization.id, + wikiId: null, + documentId: null, + }); + + await this.messageService.notify(targetUser, { + title: `您被添加到组织「${organization.name}」`, + message: `您被添加到知识库「${organization.name}」,快去看看吧!`, + url: buildMessageURL('toOrganization')({ + organizationId: organization.id, + }), + }); + + return ret; + } + + /** + * 修改组织成员 + * @param user + * @param wikiId + * @param dto + * @returns + */ + async updateMember(user: UserEntity, organizationId, dto: OperateUserAuthDto) { + const organization = await this.organizationRepo.findOne(organizationId); + + if (!organization) { + throw new HttpException('组织不存在', HttpStatus.NOT_FOUND); + } + + const targetUser = await this.userService.findOne({ name: dto.userName }); + + if (!targetUser) { + throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); + } + + const ret = await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: organization.id, + wikiId: null, + documentId: null, + }); + + await this.messageService.notify(targetUser, { + title: `组织「${organization.name}」权限变更`, + message: `您在组织「${organization.name}」权限已变更,快去看看吧!`, + url: buildMessageURL('toOrganization')({ + organizationId: organization.id, + }), + }); + + return ret; + } + + /** + * 删除组织成员 + * @param user + * @param wikiId + * @param dto + * @returns + */ + async deleteMember(user: UserEntity, organizationId, dto: OperateUserAuthDto) { + const organization = await this.organizationRepo.findOne(organizationId); + + if (!organization) { + throw new HttpException('组织不存在', HttpStatus.NOT_FOUND); + } + + const targetUser = await this.userService.findOne({ name: dto.userName }); + + if (!targetUser) { + throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); + } + + const ret = await this.authService.deleteOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: organization.id, + wikiId: null, + documentId: null, + }); + + await this.messageService.notify(targetUser, { + title: `组织「${organization.name}」权限收回`, + message: `您在组织「${organization.name}」权限已收回,快去看看吧!`, + url: buildMessageURL('toOrganization')({ + organizationId: organization.id, + }), + }); + + return ret; + } +} diff --git a/packages/server/src/services/star.service.ts b/packages/server/src/services/star.service.ts index 304d15e..b90a93d 100644 --- a/packages/server/src/services/star.service.ts +++ b/packages/server/src/services/star.service.ts @@ -2,7 +2,9 @@ import { StarDto } from '@dtos/star.dto'; import { StarEntity } from '@entities/star.entity'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; 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 { WikiService } from '@services/wiki.service'; import { IDocument } from '@think/domains'; @@ -14,10 +16,19 @@ export class StarService { constructor( @InjectRepository(StarEntity) private readonly starRepo: Repository, + + @Inject(forwardRef(() => AuthService)) + private readonly authService: AuthService, + @Inject(forwardRef(() => UserService)) private readonly userService: UserService, + + @Inject(forwardRef(() => OrganizationService)) + private readonly organizationService: OrganizationService, + @Inject(forwardRef(() => WikiService)) private readonly wikiService: WikiService, + @Inject(forwardRef(() => DocumentService)) private readonly documentService: DocumentService ) {} @@ -56,12 +67,19 @@ export class StarService { } /** - * 获取加星的知识库 + * 获取组织内加星的知识库 * @param user * @returns */ - async getWikis(user: OutUser) { + async getStarWikisInOrganization(user: OutUser, organizationId) { + await this.authService.canView(user.id, { + organizationId: organizationId, + wikiId: null, + documentId: null, + }); + const records = await this.starRepo.find({ + organizationId, userId: user.id, documentId: null, }); @@ -69,8 +87,8 @@ export class StarService { const withCreateUserRes = await Promise.all( res.map(async (wiki) => { const createUser = await this.userService.findById(wiki.createUserId); - const isMember = await this.wikiService.isMember(wiki.id, user.id); - return { createUser, isMember, ...wiki }; + // const isMember = await this.wikiService.isMember(wiki.id, user.id); + return { createUser, isMember: true, ...wiki }; }) ); @@ -82,7 +100,7 @@ export class StarService { * @param user * @returns */ - async getWikiDocuments(user: OutUser, dto: StarDto) { + async getStarDocumentsInWiki(user: OutUser, dto: StarDto) { const records = await this.starRepo.find({ userId: user.id, wikiId: dto.wikiId, @@ -112,12 +130,19 @@ export class StarService { } /** - * 获取加星的文档(平铺) + * 获取组织内加星的文档(平铺) * @param user * @returns */ - async getDocuments(user: OutUser) { + async getStarDocumentsInOrganization(user: OutUser, organizationId) { + await this.authService.canView(user.id, { + organizationId: organizationId, + wikiId: null, + documentId: null, + }); + const records = await this.starRepo.find({ + organizationId, userId: user.id, }); const res = await this.documentService.findByIds(records.map((record) => record.documentId)); diff --git a/packages/server/src/services/user.service.ts b/packages/server/src/services/user.service.ts index d7099bd..528019e 100644 --- a/packages/server/src/services/user.service.ts +++ b/packages/server/src/services/user.service.ts @@ -8,15 +8,16 @@ import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { InjectRepository } from '@nestjs/typeorm'; import { MessageService } from '@services/message.service'; +import { OrganizationService } from '@services/organization.service'; import { StarService } from '@services/star.service'; +import { SystemService } from '@services/system.service'; import { VerifyService } from '@services/verify.service'; import { WikiService } from '@services/wiki.service'; -import { UserStatus } from '@think/domains'; +import { ORGANIZATION_LOGOS } from '@think/constants'; +import { IUser, UserStatus } from '@think/domains'; import { instanceToPlain } from 'class-transformer'; import { Repository } from 'typeorm'; -import { SystemService } from './system.service'; - export type OutUser = Omit; @Injectable() @@ -36,6 +37,9 @@ export class UserService { @Inject(forwardRef(() => StarService)) private readonly starService: StarService, + @Inject(forwardRef(() => OrganizationService)) + private readonly organizationService: OrganizationService, + @Inject(forwardRef(() => WikiService)) private readonly wikiService: WikiService, @@ -67,12 +71,19 @@ export class UserService { } try { - await this.userRepo.save( - await this.userRepo.create({ - ...config, - isSystemAdmin: true, - }) - ); + const res = await this.userRepo.create({ + ...config, + isSystemAdmin: true, + }); + const createdUser = (await this.userRepo.save(res)) as unknown as IUser; + + await this.organizationService.createOrganization(createdUser, { + name: createdUser.name, + description: `${createdUser.name}的个人组织`, + logo: ORGANIZATION_LOGOS[0], + isPersonal: true, + }); + console.log('[think] 已创建默认系统管理员,请尽快登录系统修改密码'); } catch (e) { console.error(`[think] 创建默认系统管理员失败:`, e.message); @@ -139,18 +150,27 @@ export class UserService { const res = await this.userRepo.create(user); const createdUser = await this.userRepo.save(res); - const wiki = await this.wikiService.createWiki(createdUser, { + + await this.organizationService.createOrganization(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}`, + description: `${createdUser.name}的个人组织`, + logo: ORGANIZATION_LOGOS[0], + 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; } diff --git a/packages/server/src/services/verify.service.ts b/packages/server/src/services/verify.service.ts index bf1371c..4dbbe27 100644 --- a/packages/server/src/services/verify.service.ts +++ b/packages/server/src/services/verify.service.ts @@ -36,6 +36,8 @@ export class VerifyService { 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.systemService.sendEmail({ to: email, subject: '验证码', diff --git a/packages/server/src/services/view.service.ts b/packages/server/src/services/view.service.ts index 1702d12..b25a000 100644 --- a/packages/server/src/services/view.service.ts +++ b/packages/server/src/services/view.service.ts @@ -1,92 +1,157 @@ +import { RedisDBEnum } from '@constants/*'; +import { DocumentEntity } from '@entities/document.entity'; +import { UserEntity } from '@entities/user.entity'; import { ViewEntity } from '@entities/view.entity'; -import { parseUserAgent } from '@helpers/ua.helper'; +import { buildRedis } from '@helpers/redis.helper'; import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { IDocument, IPagination, IUser } from '@think/domains'; +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 - ) {} + ) { + this.buildRedis(); + } + + private async buildRedis() { + try { + this.redis = await buildRedis(RedisDBEnum.view); + console.log('[think] 访问记录服务启动成功'); + } catch (e) { + console.error(`[think] 访问记录服务启动错误: "${e.message}"`); + } + } + + /** + * 文档访问量统计 + * @param documentId + * @returns + */ + private buildDocumentViewKey(documentId: IDocument['id']) { + return `views-${documentId}`; + } + + /** + * 获取文档访问量 + * @param documentId + * @returns + */ + private getDocumentViews(documentId): Promise { + return new Promise((resolve, reject) => { + this.redis.get(this.buildDocumentViewKey(documentId), (err, views) => { + if (err) { + reject(err); + } else { + resolve(+views); + } + }); + }); + } + + /** + * 文档访问量 +1 + * @param documentId + */ + private async recordDocumentViews(documentId) { + const views = await this.getDocumentViews(documentId); + this.redis.set(this.buildDocumentViewKey(documentId), String(views + 1)); + } + + /** + * 组织内用户访问记录 + * @param organizationId + * @returns + */ + private buildVisitedDocumentInOrganizationKey(organizationId) { + return `user-activity-in-${organizationId}`; + } + + /** + * 记录组织内用户访问行为 + * @param user + * @param document + */ + private async recordUserVisitedDocumentInOrganization(user: UserEntity, document: DocumentEntity) { + const { id: userId } = user; + const { organizationId, id: documentId } = document; + const key = this.buildVisitedDocumentInOrganizationKey(organizationId); + + const exists = await this.redis.hexists(key, userId); + + if (!exists) { + await this.redis.hset( + key, + userId, + JSON.stringify([ + { + documentId, + visitedAt: Date.now(), + }, + ]) + ); + } else { + const oldData = await this.redis.hget(key, userId); + const visitedDocuments = (JSON.parse(oldData || '[]') || []).filter((record) => record.documentId !== documentId); + await this.redis.hset( + key, + userId, + JSON.stringify( + lodash.uniqBy( + [ + { + documentId, + visitedAt: Date.now(), + }, + ...visitedDocuments, + ], + (record) => record.documentId + ) + ) + ); + } + } /** * 创建访问记录(内部调用,无公开接口) * @returns */ - async create({ userId = 'public', documentId, userAgent }) { - const data = await this.viewRepo.create({ - userId, - documentId, - originUserAgent: userAgent, - parsedUserAgent: parseUserAgent(userAgent).text, - }); - const ret = await this.viewRepo.save(data); - return ret; + async create(user: UserEntity | null, document: DocumentEntity) { + await Promise.all([ + this.recordDocumentViews(document.id), + user && this.recordUserVisitedDocumentInOrganization(user, document), + ]); } - async deleteViews(documentId) { - const records = await this.viewRepo.find({ documentId }); - await this.viewRepo.remove(records); + public async getDocumentTotalViews(documentId) { + return await this.getDocumentViews(documentId); } - async getDocumentTotalViews(documentId) { - try { - const count = await this.viewRepo.query( - `SELECT COUNT(1) - FROM view - WHERE view.documentId = '${documentId}' - ` - ); - return count[0]['COUNT(1)']; - } catch (e) { - return 0; - } - } - - async getDocumentViews(documentId, pagination: IPagination) { - let { page = 1, pageSize = 12 } = pagination; - if (page <= 0) { - page = 1; - } - if (pageSize <= 0) { - pageSize = 12; - } - const take = pageSize; - const skip = page === 1 ? 0 : (page - 1) * pageSize; - - const [data, total] = await this.viewRepo.findAndCount({ - where: { documentId }, - take, - skip, - }); - - return { data, total }; - } - - async getUserRecentVisitedDocuments(userId: IUser['id']): Promise< + public async getUserRecentVisitedDocuments( + userId: IUser['id'], + organizationId: IOrganization['id'] + ): Promise< Array<{ documentId: IDocument['id']; visitedAt: Date; }> > { - const count = 20; - const ret = await this.viewRepo.query( - ` - SELECT v.documentId, v.visitedAt FROM ( - SELECT ANY_VALUE(documentId) as documentId, ANY_VALUE(created_at) as visitedAt - FROM view - WHERE view.userId = '${userId}' - GROUP BY visitedAt - ORDER BY visitedAt DESC - ) v - GROUP BY v.documentId - LIMIT ${count} - ` - ); - ret.sort((a, b) => -new Date(a.visitedAt).getTime() + new Date(b.visitedAt).getTime()); - return ret; + const key = this.buildVisitedDocumentInOrganizationKey(organizationId); + const exists = await this.redis.hexists(key, userId); + let res = []; + + if (exists) { + const oldData = await this.redis.hget(key, userId); + res = JSON.parse(oldData); + } + + return res; } } diff --git a/packages/server/src/services/wiki.service.ts b/packages/server/src/services/wiki.service.ts index e5e943d..0ba71df 100644 --- a/packages/server/src/services/wiki.service.ts +++ b/packages/server/src/services/wiki.service.ts @@ -1,19 +1,22 @@ +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 { WikiUserDto } from '@dtos/wiki-user.dto'; import { WikiEntity } from '@entities/wiki.entity'; -import { WikiUserEntity } from '@entities/wiki-user.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 { DocumentStatus, IPagination, WikiStatus, WikiUserRole } from '@think/domains'; +import { AuthEnum, buildMessageURL, DocumentStatus, IPagination, WikiStatus, WikiUserRole } from '@think/domains'; import { instanceToPlain } from 'class-transformer'; import * as lodash from 'lodash'; import { Repository } from 'typeorm'; @@ -24,8 +27,11 @@ export class WikiService { @InjectRepository(WikiEntity) private readonly wikiRepo: Repository, - @InjectRepository(WikiUserEntity) - private readonly wikiUserRepo: Repository, + @Inject(forwardRef(() => AuthService)) + private readonly authService: AuthService, + + // @InjectRepository(WikiUserEntity) + // private readonly wikiUserRepo: Repository, @Inject(forwardRef(() => MessageService)) private readonly messageService: MessageService, @@ -40,7 +46,10 @@ export class WikiService { private readonly userService: UserService, @Inject(forwardRef(() => ViewService)) - private readonly viewService: ViewService + private readonly viewService: ViewService, + + @Inject(forwardRef(() => OrganizationService)) + private readonly organizationService: OrganizationService ) {} /** @@ -64,105 +73,114 @@ 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 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 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); + // /** + // * 操作知识库成员(添加、修改角色) + // * @param param0 + // * @returns + // */ + // async operateWikiUser({ wikiId, currentUserId, targetUserId, targetUserRole }) { + // const wiki = await this.wikiRepo.findOne(wikiId); - // 1. 检查知识库 - if (!wiki) { - throw new HttpException('目标知识库不存在', HttpStatus.BAD_REQUEST); - } + // // await this.organizationService.canUserVisitOrganization(currentUserId, wiki.organizationId); - const isCurrentUserCreator = currentUserId === wiki.createUserId; - const isTargetUserCreator = targetUserId === wiki.createUserId; + // // 1. 检查知识库 + // if (!wiki) { + // throw new HttpException('目标知识库不存在', HttpStatus.BAD_REQUEST); + // } - const currentWikiUserRole = isCurrentUserCreator - ? WikiUserRole.admin - : ( - await this.wikiUserRepo.findOne({ - wikiId: wiki.id, - userId: currentUserId, - }) - ).userRole; + // const isCurrentUserCreator = currentUserId === wiki.createUserId; + // const isTargetUserCreator = targetUserId === wiki.createUserId; - // 2. 检查成员是否存在 - const targetUser = await this.userService.findOne(targetUserId); - const targetWikiUser = await this.wikiUserRepo.findOne({ - wikiId: wiki.id, - userId: targetUserId, - }); + // const currentWikiUserRole = isCurrentUserCreator + // ? WikiUserRole.admin + // : ( + // await this.wikiUserRepo.findOne({ + // wikiId: wiki.id, + // userId: currentUserId, + // }) + // ).userRole; - if (targetWikiUser) { - if (targetWikiUser.userRole === targetUserRole) return; + // // 2. 检查成员是否存在 + // const targetUser = await this.userService.findOne(targetUserId); + // const targetWikiUser = await this.wikiUserRepo.findOne({ + // wikiId: wiki.id, + // userId: targetUserId, + // }); - // 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: `/wiki/${wiki.id}`, - }); - return ret; - } else { - // 2.2. 添加知识库新用户 - if (currentWikiUserRole !== WikiUserRole.admin) { - throw new HttpException('您无权限进行该操作', HttpStatus.FORBIDDEN); - } - const data = { - wikiId, - 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: `/wiki/${wiki.id}`, - }); - return ret; - } - } + // 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 = { + // 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; + // } + // } /** * 添加知识库成员 @@ -171,28 +189,41 @@ export class WikiService { * @param dto * @returns */ - async addWikiUser(user: OutUser, wikiId, dto: WikiUserDto): Promise { + async addWikiUser(user: OutUser, wikiId, dto: OperateUserAuthDto) { const targetUser = await this.userService.findOne({ name: dto.userName }); if (!targetUser) { - throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); + throw new HttpException('用户不存在', HttpStatus.NOT_FOUND); } + const wiki = await this.wikiRepo.findOne(wikiId); const homeDoc = await this.getWikiHomeDocument(user, wikiId); - await this.documentService.operateDocumentAuth({ - currentUserId: user.id, - documentId: homeDoc.id, - targetUserId: targetUser.id, - readable: true, - editable: dto.userRole === WikiUserRole.admin, + if (!wiki) { + throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); + } + + await this.authService.createOrUpdateOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, }); - return this.operateWikiUser({ - wikiId, - currentUserId: user.id, - targetUserId: targetUser.id, - targetUserRole: dto.userRole, + await this.authService.createOrUpdateOtherUserAuth(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}」,快去看看吧!`, + url: buildMessageURL('toWiki')({ + organizationId: wiki.organizationId, + wikiId: wiki.id, + }), }); } @@ -203,17 +234,41 @@ export class WikiService { * @param dto * @returns */ - async updateWikiUser(user: OutUser, wikiId, dto: WikiUserDto): Promise { + async updateWikiUser(user: OutUser, wikiId, dto: OperateUserAuthDto) { const targetUser = await this.userService.findOne({ name: dto.userName }); if (!targetUser) { - throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); + throw new HttpException('用户不存在', HttpStatus.NOT_FOUND); } - return this.operateWikiUser({ - wikiId, - currentUserId: user.id, - targetUserId: targetUser.id, - targetUserRole: dto.userRole, + + 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, { + auth: dto.userAuth, + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }); + + await this.authService.createOrUpdateOtherUserAuth(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}」的权限有更新,快去看看吧!`, + url: buildMessageURL('toWiki')({ + organizationId: wiki.organizationId, + wikiId: wiki.id, + }), }); } @@ -224,31 +279,42 @@ export class WikiService { * @param dto * @returns */ - async deleteWikiUser(user: OutUser, wikiId, dto: WikiUserDto): Promise { + async deleteWikiUser(user: OutUser, wikiId, dto: OperateUserAuthDto) { const targetUser = await this.userService.findOne({ name: dto.userName }); if (!targetUser) { - throw new HttpException('用户不存在', HttpStatus.BAD_REQUEST); - } - - const targetWikiUser = await this.wikiUserRepo.findOne({ - wikiId, - userId: targetUser.id, - }); - - if (targetWikiUser.createUserId === targetWikiUser.userId) { - throw new HttpException('无法删除知识库创建者', HttpStatus.FORBIDDEN); + throw new HttpException('用户不存在', HttpStatus.NOT_FOUND); } const wiki = await this.wikiRepo.findOne(wikiId); + const homeDoc = await this.getWikiHomeDocument(user, wikiId); - await this.messageService.notify(targetUser, { - title: `您已被移出知识库「${wiki.name}」`, - message: `${user.name}已将您从知识库「${wiki.name}」移出!`, - url: `/wiki/${wiki.id}`, + if (!wiki) { + throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); + } + + await this.authService.deleteOtherUserAuth(user.id, targetUser.id, { + auth: dto.userAuth, + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, }); - await this.wikiUserRepo.remove(targetWikiUser); + await 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}」的权限已变更,快去看看吧!`, + url: buildMessageURL('toWiki')({ + organizationId: wiki.organizationId, + wikiId: wiki.id, + }), + }); } /** @@ -256,38 +322,48 @@ export class WikiService { * @param userId * @param wikiId */ - async getWikiUsers(wikiId) { - const records = await this.wikiUserRepo.find({ wikiId }); - const ids = records.map((record) => record.userId); - const users = await this.userService.findByIds(ids); - const res = users.map((user) => { - const record = records.find((record) => record.userId === user.id); + async getWikiUsers(user, wikiId) { + const wiki = await this.wikiRepo.findOne(wikiId); + + if (!wiki) { + throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); + } + + await this.authService.canView(user.id, { + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }); + + const { data: usersAuth, total } = await this.authService.getUsersAuthInWiki(wiki.organizationId, wiki.id); + + const userIds = usersAuth.map((auth) => auth.userId); + const users = await this.userService.findByIds(userIds); + + const withUserData = usersAuth.map((auth) => { return { - userId: user.id, - userName: user.name, - userRole: record.userRole, - userStatus: record.userStatus, - isCreator: record.createUserId === user.id, - createdAt: record.createdAt, + auth, + user: users.find((user) => user.id === auth.userId), }; }); - return res; + + return { data: withUserData, total }; } - /** - * 查询知识库指定用户详情 - * @param workspaceId - * @param userId - * @returns - */ - async getWikiUserDetail({ wikiId, userId }): Promise { - const data = { wikiId, userId }; - const wikiUser = await this.wikiUserRepo.findOne(data); - if (!wikiUser) { - throw new HttpException('您不在该知识库中', HttpStatus.FORBIDDEN); - } - return wikiUser; - } + // /** + // * 查询知识库指定用户详情 + // * @param workspaceId + // * @param userId + // * @returns + // */ + // async getWikiUserDetail({ wikiId, userId }): Promise { + // const data = { wikiId, userId }; + // const wikiUser = await this.wikiUserRepo.findOne(data); + // if (!wikiUser) { + // throw new HttpException('您不在该知识库中', HttpStatus.FORBIDDEN); + // } + // return wikiUser; + // } /** * 创建知识库 @@ -296,6 +372,12 @@ export class WikiService { * @returns */ async createWiki(user: OutUser, dto: CreateWikiDto) { + await this.authService.canView(user.id, { + organizationId: dto.organizationId, + wikiId: null, + documentId: null, + }); + const createUserId = user.id; const data = { ...dto, @@ -303,33 +385,48 @@ export class WikiService { }; const toSaveWiki = await this.wikiRepo.create(data); const wiki = await this.wikiRepo.save(toSaveWiki); - await this.operateWikiUser({ - wikiId: wiki.id, - currentUserId: user.id, - targetUserId: createUserId, - targetUserRole: WikiUserRole.admin, - }); - // 知识库首页文档 - const [doc] = await Promise.all([ - await this.documentService.createDocument( - user, - { - wikiId: wiki.id, - parentDocumentId: null, - title: wiki.name, - }, - true - ), - await this.starService.toggleStar(user, { wikiId: wiki.id }), + + const { data: userAuths } = await this.authService.getUsersAuthInOrganization(wiki.organizationId); + + await Promise.all([ + ...userAuths + .filter((userAuth) => userAuth.userId !== user.id) + .map((userAuth) => { + return this.authService.createOrUpdateAuth(userAuth.userId, { + auth: userAuth.auth, + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }); + }), + await this.authService.createOrUpdateAuth(user.id, { + auth: AuthEnum.creator, + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }), + + await this.starService.toggleStar(user, { + organizationId: wiki.organizationId, + wikiId: wiki.id, + }), ]); - const homeDocumentId = doc.id; + + const homeDoc = await this.documentService.createDocument( + user, + { + organizationId: wiki.organizationId, + wikiId: wiki.id, + parentDocumentId: null, + title: wiki.name, + }, + true + ); + + const homeDocumentId = homeDoc.id; const withHomeDocumentIdWiki = await this.wikiRepo.merge(wiki, { homeDocumentId }); await this.wikiRepo.save(withHomeDocumentIdWiki); - await this.starService.toggleStar(user, { - wikiId: wiki.id, - }); - return withHomeDocumentIdWiki; } @@ -339,18 +436,21 @@ export class WikiService { * @param pagination * @returns */ - async getAllWikis(user: OutUser, pagination: IPagination) { - const { page = 1, pageSize = 12 } = pagination; - const query = await this.wikiUserRepo - .createQueryBuilder('WikiUser') - .where('WikiUser.userId=:userId') - .setParameter('userId', user.id); - query.skip((+page - 1) * +pageSize); - query.take(+pageSize); - const [wikis, total] = await query.getManyAndCount(); - const workspaceIds = wikis.map((wiki) => wiki.wikiId); - const data = await this.wikiRepo.findByIds(workspaceIds); + async getAllWikis(user: OutUser, organizationId, pagination: IPagination) { + await this.authService.canView(user.id, { + organizationId, + wikiId: null, + documentId: null, + }); + const { page = 1, pageSize = 12 } = pagination; + const { data: userWikiAuths, total } = await this.authService.getUserCanViewWikisInOrganization( + user.id, + organizationId + ); + const wikiIds = userWikiAuths.map((userAuth) => userAuth.wikiId); + + const data = await this.wikiRepo.findByIds(wikiIds); const ret = await Promise.all( data.map(async (wiki) => { const createUser = await this.userService.findById(wiki.createUserId); @@ -367,23 +467,27 @@ export class WikiService { * @param pagination * @returns */ - async getOwnWikis(user: OutUser, pagination: IPagination) { - const { page = 1, pageSize = 12 } = pagination; - const query = await this.wikiRepo - .createQueryBuilder('wiki') - .where('wiki.createUserId=:createUserId') - .setParameter('createUserId', user.id); - query.skip((+page - 1) * +pageSize); - query.take(+pageSize); - const [data, total] = await query.getManyAndCount(); + async getOwnWikis(user: OutUser, organizationId, pagination: IPagination) { + await this.authService.canView(user.id, { + organizationId, + wikiId: null, + documentId: null, + }); + const { page = 1, pageSize = 12 } = pagination; + const { data: userWikiAuths, total } = await this.authService.getUserCreateWikisInOrganization( + user.id, + organizationId + ); + const wikiIds = userWikiAuths.map((userAuth) => userAuth.wikiId); + + const data = await this.wikiRepo.findByIds(wikiIds); const ret = await Promise.all( data.map(async (wiki) => { const createUser = await this.userService.findById(wiki.createUserId); return { ...wiki, createUser, isMember: true }; }) ); - return { data: ret, total }; } @@ -393,19 +497,21 @@ export class WikiService { * @param pagination * @returns */ - async getJoinWikis(user: OutUser, pagination: IPagination) { - const { page = 1, pageSize = 12 } = pagination; - const query = await this.wikiUserRepo - .createQueryBuilder('WikiUser') - .where('WikiUser.userId=:userId') - .andWhere('WikiUser.createUserId!=:userId') - .setParameter('userId', user.id); - query.skip((+page - 1) * +pageSize); - query.take(+pageSize); - const [wikis, total] = await query.getManyAndCount(); - const workspaceIds = wikis.map((wiki) => wiki.wikiId); - const data = await this.wikiRepo.findByIds(workspaceIds); + async getJoinWikis(user: OutUser, organizationId, pagination: IPagination) { + await this.authService.canView(user.id, { + organizationId, + wikiId: null, + documentId: null, + }); + const { page = 1, pageSize = 12 } = pagination; + const { data: userWikiAuths, total } = await this.authService.getUserJoinWikisInOrganization( + user.id, + organizationId + ); + const wikiIds = userWikiAuths.map((userAuth) => userAuth.wikiId); + + const data = await this.wikiRepo.findByIds(wikiIds); const ret = await Promise.all( data.map(async (wiki) => { const createUser = await this.userService.findById(wiki.createUserId); @@ -424,6 +530,11 @@ export class WikiService { */ async getWikiDetail(user: OutUser, wikiId: string) { const wiki = await this.wikiRepo.findOne(wikiId); + await this.authService.canView(user.id, { + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }); const createUser = await this.userService.findById(wiki.createUserId); return { ...wiki, createUser }; } @@ -436,6 +547,11 @@ export class WikiService { */ async getWikiHomeDocument(user: OutUser, wikiId) { const res = await this.documentService.documentRepo.findOne({ wikiId, isWikiHome: true }); + await this.authService.canView(user.id, { + organizationId: res.organizationId, + wikiId: res.wikiId, + documentId: null, + }); return lodash.omit(instanceToPlain(res), ['state']); } @@ -451,6 +567,11 @@ export class WikiService { if (!oldData) { throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); } + await this.authService.canEdit(user.id, { + organizationId: oldData.organizationId, + wikiId: oldData.id, + documentId: null, + }); const newData = { ...oldData, ...dto, @@ -467,13 +588,21 @@ export class WikiService { */ async deleteWiki(user: OutUser, wikiId) { const wiki = await this.wikiRepo.findOne(wikiId); - if (user.id !== wiki.createUserId) { - throw new HttpException('您不是创建者,无法删除该知识库', HttpStatus.FORBIDDEN); - } + await this.authService.canDelete(user.id, { + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }); await this.wikiRepo.remove(wiki); - await this.documentService.deleteWikiDocuments(user, wikiId); - const users = await this.wikiUserRepo.find({ wikiId }); - await Promise.all([this.wikiUserRepo.remove(users)]); + // 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)]); return wiki; } @@ -485,18 +614,18 @@ export class WikiService { * @returns */ async shareWiki(user: OutUser, wikiId, dto: ShareWikiDto) { - const wikiUser = await this.getWikiUserDetail({ wikiId, userId: user.id }); - - if (wikiUser.userRole !== WikiUserRole.admin) { - throw new HttpException('您没有权限操作该知识库', HttpStatus.FORBIDDEN); - } - const wiki = await this.wikiRepo.findOne(wikiId); if (!wiki) { throw new HttpException('目标知识库不存在', HttpStatus.NOT_FOUND); } + await this.authService.canEdit(user.id, { + organizationId: wiki.organizationId, + wikiId: wiki.id, + documentId: null, + }); + const newData = await this.wikiRepo.merge(wiki, { status: dto.nextStatus, }); @@ -540,10 +669,9 @@ export class WikiService { * @returns */ async getWikiTocs(user: OutUser, wikiId) { - const records = await this.documentService.documentAuthorityRepo.find({ - userId: user.id, - wikiId, - }); + const wiki = await this.wikiRepo.findOne(wikiId); + + const records = await this.authService.getUserCanViewDocumentsInWiki(wiki.organizationId, wiki.id); const ids = records.map((record) => record.documentId); const documents = await this.documentService.documentRepo.findByIds(ids, { @@ -563,13 +691,6 @@ export class WikiService { return lodash.omit(item, ['content', 'state']); }); - // const docsWithCreateUser = await Promise.all( - // docs.map(async (doc) => { - // const createUser = await this.userService.findById(doc.createUserId); - // return { ...doc, createUser }; - // }) - // ); - return array2tree(docs); } @@ -598,45 +719,45 @@ export class WikiService { ); } - /** - * 获取知识库所有文档(无结构嵌套) - * @param user - * @param wikiId - * @returns - */ - async getWikiDocs(user: OutUser, wikiId) { - // 通过文档成员表获取当前用户可查阅的所有文档 - const records = await this.documentService.documentAuthorityRepo.find({ - userId: user.id, - wikiId, - }); + // /** + // * 获取知识库所有文档(无结构嵌套) + // * @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 ids = records.map((record) => record.documentId); - const documents = await this.documentService.documentRepo.findByIds(ids); + // 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']); - }); + // 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); + // 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 }; - }) - ); + // const docsWithCreateUser = await Promise.all( + // docs.map(async (doc) => { + // const createUser = await this.userService.findById(doc.createUserId); + // return { ...doc, createUser }; + // }) + // ); - return docsWithCreateUser; - } + // return docsWithCreateUser; + // } /** * 获取公开知识库目录 @@ -679,14 +800,9 @@ export class WikiService { * @param wikiId * @returns */ - async getPublicWikiHomeDocument(wikiId, userAgent) { + async getPublicWikiHomeDocument(wikiId) { const res = await this.documentService.documentRepo.findOne({ wikiId, isWikiHome: true }); - - await this.viewService.create({ - userId: 'public', - documentId: res.id, - userAgent, - }); + this.viewService.create(null, res); const views = await this.viewService.getDocumentTotalViews(res.id); return { ...lodash.omit(instanceToPlain(res), ['state']), views }; } diff --git a/packages/server/tsconfig.json b/packages/server/tsconfig.json index c3550dd..646b38f 100644 --- a/packages/server/tsconfig.json +++ b/packages/server/tsconfig.json @@ -21,7 +21,8 @@ "@entities/*": ["src/entities/*"], "@services/*": ["src/services/*"], "@controllers/*": ["src/controllers/*"], - "@modules/*": ["src/modules/*"] + "@modules/*": ["src/modules/*"], + "@constants/*": ["src/constants"] } }, "watchOptions": {