From 2de4d4846e57b853a07f4861419d25b2de327b65 Mon Sep 17 00:00:00 2001 From: lian Date: Thu, 19 Mar 2020 20:15:26 +0800 Subject: [PATCH] dingtalk (#4426) Co-authored-by: lian --- .../dialog/confirm-disconnect-dingtalk.js | 45 + .../import-dingtalk-department-dialog.js | 54 + .../user-settings/social-login-dingtalk.js | 60 + frontend/src/css/dingtalk-departments.css | 87 + .../pages/sys-admin/dingtalk-departments.js | 355 ++ .../dingtalk-department-members-list.js | 84 + .../dingtalk-departments-tree-node.js | 147 + .../dingtalk-departments-tree-panel.js | 51 + .../src/pages/sys-admin/dingtalk/index.js | 5 + frontend/src/pages/sys-admin/index.js | 6 + frontend/src/pages/sys-admin/side-panel.js | 16 +- frontend/src/seafile-api.js | 3074 +++++++++++++++++ frontend/src/settings.js | 10 +- frontend/src/utils/constants.js | 1 + frontend/src/utils/seafile-api.js | 2 +- seahub/api2/endpoints/admin/dingtalk.py | 459 +++ seahub/api2/endpoints/admin/work_weixin.py | 1 + seahub/auth/views.py | 1 + seahub/dingtalk/__init__.py | 0 seahub/dingtalk/admin.py | 3 + seahub/dingtalk/migrations/__init__.py | 0 seahub/dingtalk/models.py | 3 + seahub/dingtalk/settings.py | 23 + seahub/dingtalk/tests.py | 3 + seahub/dingtalk/views.py | 212 ++ seahub/oauth/backends.py | 7 + .../templates/profile/set_profile_react.html | 6 + seahub/profile/views.py | 14 +- seahub/settings.py | 5 +- .../sysadmin/sysadmin_react_app.html | 1 + seahub/urls.py | 23 + seahub/views/sso.py | 3 + seahub/views/sysadmin.py | 2 + 33 files changed, 4756 insertions(+), 7 deletions(-) create mode 100644 frontend/src/components/dialog/confirm-disconnect-dingtalk.js create mode 100644 frontend/src/components/dialog/import-dingtalk-department-dialog.js create mode 100644 frontend/src/components/user-settings/social-login-dingtalk.js create mode 100644 frontend/src/css/dingtalk-departments.css create mode 100644 frontend/src/pages/sys-admin/dingtalk-departments.js create mode 100644 frontend/src/pages/sys-admin/dingtalk/dingtalk-department-members-list.js create mode 100644 frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-node.js create mode 100644 frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-panel.js create mode 100644 frontend/src/pages/sys-admin/dingtalk/index.js create mode 100644 frontend/src/seafile-api.js create mode 100644 seahub/api2/endpoints/admin/dingtalk.py create mode 100644 seahub/dingtalk/__init__.py create mode 100644 seahub/dingtalk/admin.py create mode 100644 seahub/dingtalk/migrations/__init__.py create mode 100644 seahub/dingtalk/models.py create mode 100644 seahub/dingtalk/settings.py create mode 100644 seahub/dingtalk/tests.py create mode 100644 seahub/dingtalk/views.py diff --git a/frontend/src/components/dialog/confirm-disconnect-dingtalk.js b/frontend/src/components/dialog/confirm-disconnect-dingtalk.js new file mode 100644 index 0000000000..04312f07e8 --- /dev/null +++ b/frontend/src/components/dialog/confirm-disconnect-dingtalk.js @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + formActionURL: PropTypes.string.isRequired, + csrfToken: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired +}; + +class ConfirmDisconnectDingtalk extends Component { + + constructor(props) { + super(props); + this.form = React.createRef(); + } + + disconnect = () => { + this.form.current.submit(); + } + + render() { + const {formActionURL, csrfToken, toggle} = this.props; + return ( + + {gettext('Disconnect')} + +

{gettext('Are you sure you want to disconnect?')}

+
+ +
+
+ + + + +
+ ); + } +} + +ConfirmDisconnectDingtalk.propTypes = propTypes; + +export default ConfirmDisconnectDingtalk; diff --git a/frontend/src/components/dialog/import-dingtalk-department-dialog.js b/frontend/src/components/dialog/import-dingtalk-department-dialog.js new file mode 100644 index 0000000000..c3b9f95508 --- /dev/null +++ b/frontend/src/components/dialog/import-dingtalk-department-dialog.js @@ -0,0 +1,54 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import Loading from '../loading'; + +const propTypes = { + importDepartmentDialogToggle: PropTypes.func.isRequired, + onImportDepartmentSubmit: PropTypes.func.isRequired, + departmentsCount: PropTypes.number.isRequired, + membersCount: PropTypes.number.isRequired, + departmentName: PropTypes.string.isRequired, +}; + +class ImportDingtalkDepartmentDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + isLoading : false, + }; + } + + toggle = () => { + this.props.importDepartmentDialogToggle(null); + }; + + handleSubmit = () => { + this.props.onImportDepartmentSubmit(); + this.setState({ isLoading : true }); + }; + + render() { + const { departmentsCount, membersCount, departmentName } = this.props; + return ( + + + {'导入部门 '}{departmentName} + + +

{'将要导入 '}{departmentsCount}{' 个部门,其中包括 '}{membersCount}{' 个成员'}

+ {this.state.isLoading && } +
+ + + + +
+ ); + } +} + +ImportDingtalkDepartmentDialog.propTypes = propTypes; + +export default ImportDingtalkDepartmentDialog; diff --git a/frontend/src/components/user-settings/social-login-dingtalk.js b/frontend/src/components/user-settings/social-login-dingtalk.js new file mode 100644 index 0000000000..a8608b02db --- /dev/null +++ b/frontend/src/components/user-settings/social-login-dingtalk.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { gettext, siteRoot } from '../../utils/constants'; +import ModalPortal from '../modal-portal'; +import ConfirmDisconnectDingtalk from '../dialog/confirm-disconnect-dingtalk'; + +const { + csrfToken, + langCode, + socialConnectedDingtalk, + socialNextPage +} = window.app.pageOptions; + +class SocialLoginDintalk extends React.Component { + + constructor(props) { + super(props); + this.state = { + isConfirmDialogOpen: false + }; + } + + confirmDisconnect = (e) => { + e.preventDefault(); + this.setState({ + isConfirmDialogOpen: true + }); + } + + toggleDialog = () => { + this.setState({ + isConfirmDialogOpen: !this.state.isConfirmDialogOpen + }); + } + + render() { + return ( + +
+

{gettext('Social Login')}

+

{langCode == 'zh-cn' ? '钉钉': 'Dingtalk'}

+ {socialConnectedDingtalk ? + {gettext('Disconnect')} : + {gettext('Connect')} + } +
+ {this.state.isConfirmDialogOpen && ( + + + + )} +
+ ); + } +} + +export default SocialLoginDintalk; diff --git a/frontend/src/css/dingtalk-departments.css b/frontend/src/css/dingtalk-departments.css new file mode 100644 index 0000000000..72fef9fdc7 --- /dev/null +++ b/frontend/src/css/dingtalk-departments.css @@ -0,0 +1,87 @@ +.cur-view-content { + position: relative; +} +.dir-content-main { + position: absolute; + right: 0; + height: 100%; + width: 75%; + overflow-y: hidden; + padding-right: 1rem; +} +.dir-content-main:hover { + overflow-y: auto; +} +.dir-content-main table td { + line-height: 2rem; +} +.dir-content-main .empty-tip { + box-shadow: none; +} +.dir-content-main .empty-tip img { + width: 140px; + height: 140px; +} +.dir-content-nav { + position: absolute; + overflow: hidden; + width: 24%; +} +.dir-content-nav:hover { + overflow: auto; +} +.dir-content-resize { + position: absolute; + left: 25%; + height: 100%; + width: 1%; + border-left: 1px solid #eee; +} +.department-children { + padding-left: 1rem; + position: relative; +} +.tree-node-inner { + position: relative; + display: flex; + padding-right: 1rem; +} +.tree-node-inner i { + position: absolute; + top: 20%; + left: 0.3rem; + color: silver; +} +.tree-node-inner-hover { + background-color: #FFEFB2; + border-radius: 0.25rem; + cursor: pointer; +} +.tree-node-hight-light { + color: #fff; + border-radius: 4px; + background-color: #feac74 !important; +} +.tree-node-hight-light i { + color: #fff; +} +.tree-node-hight-light .attr-action-icon, +.tree-node-hight-light .attr-action-icon:focus, +.tree-node-hight-light .attr-action-icon:hover { + color: #fff !important; +} +.tree-node-icon { + padding-right: 1.5rem; +} +.tree-node-text { + width: calc(100% - 2.5rem); + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + line-height: 24px; +} +.tree-view { + padding: 12px 12px 12px 0; + flex: 1 1; +} \ No newline at end of file diff --git a/frontend/src/pages/sys-admin/dingtalk-departments.js b/frontend/src/pages/sys-admin/dingtalk-departments.js new file mode 100644 index 0000000000..a46857c3b1 --- /dev/null +++ b/frontend/src/pages/sys-admin/dingtalk-departments.js @@ -0,0 +1,355 @@ +import React, { Component, Fragment } from 'react'; +import { Button } from 'reactstrap'; +import _ from 'lodash'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, siteRoot, isPro } from '../../utils/constants'; +import { Utils } from '../../utils/utils'; +import toaster from '../../components/toast'; +import Account from '../../components/common/account'; +import { DingtalkDepartmentMembersList, DingtalkDepartmentsTreePanel } from './dingtalk'; +import ImportDingtalkDepartmentDialog from '../../components/dialog/import-dingtalk-department-dialog'; + +import '../../css/work-weixin-departments.css'; +import '../../css/dingtalk-departments.css'; + +class DingtalkDepartments extends Component { + + constructor(props) { + super(props); + this.state = { + isTreeLoading: true, + isMembersListLoading: true, + departmentsTree: [], + checkedDepartmentId: 0, + membersTempObj: {}, + membersList: [], + newUsersTempObj: {}, + isCheckedAll: false, + canCheckUserIds: [], + isImportDepartmentDialogShow: false, + importDepartment: null, + importDepartmentChildrenCount: 0, + importDepartmentMembersCount: 0, + }; + } + + getDepartmentsTree = (list) => { + let childIds = []; + let parentIds = []; + for (let i = 0; i < list.length; i++) { + if (childIds.indexOf(list[i].id) === -1) { + childIds.push(list[i].id); + } + if (parentIds.indexOf(list[i].parentid) === -1) { + parentIds.push(list[i].parentid); + } + } + let intersection = parentIds.filter((v) => { + return childIds.indexOf(v) !== -1; + }); + let rootIds = parentIds.concat(intersection).filter((v) => { + return parentIds.indexOf(v) === -1 || intersection.indexOf(v) === -1; + }); + let cloneData = _.cloneDeep(list); + return cloneData.filter(father => { + let branchArr = cloneData.filter(child => father.id === child.parentid); + branchArr.length > 0 ? father.children = branchArr : ''; + return rootIds.indexOf(father.parentid) !== -1; + }); + }; + + getDingtalkDepartmentsList = (departmentID) => { + seafileAPI.adminListDingtalkDepartments(departmentID).then((res) => { + if (!departmentID) { + let departmentsTree = this.getDepartmentsTree(res.data.department); + this.setState({ + isTreeLoading: false, + departmentsTree: departmentsTree, + }); + } else { + this.setState({ + importDepartmentChildrenCount: res.data.department.length, + importDepartmentMembersCount: this.state.membersTempObj[departmentID].length, + }); + } + }).catch((error) => { + this.handleError(error); + this.setState({ + isTreeLoading: false, + isMembersListLoading: false, + }); + if (error.response && error.response.status === 403) { + window.location = siteRoot + 'sys/useradmin/'; + } + }); + }; + + getDingtalkDepartmentMembersList = (department_id) => { + this.setState({ + isMembersListLoading: true, + }); + seafileAPI.adminListDingtalkDepartmentMembers(department_id).then((res) => { + let membersTempObj = this.state.membersTempObj; + membersTempObj[department_id] = res.data.userlist; + let canCheckUserIds = this.getCanCheckUserIds(res.data.userlist); + this.setState({ + membersTempObj: membersTempObj, + membersList: res.data.userlist, + isMembersListLoading: false, + canCheckUserIds: canCheckUserIds, + }); + }).catch((error) => { + this.setState({isMembersListLoading: false}); + this.handleError(error); + }); + } + getCanCheckUserIds = (membersList) => { + let userIds = []; + membersList.forEach((member) => { + if (!member.email) userIds.push(member.userid); + }); + return userIds; + }; + + onChangeDepartment = (department_id) => { + this.setState({ + newUsersTempObj: {}, + isCheckedAll: false, + checkedDepartmentId: department_id, + }); + if (!(department_id in this.state.membersTempObj)) { + this.getDingtalkDepartmentMembersList(department_id); + } else { + let canCheckUserIds = this.getCanCheckUserIds(this.state.membersTempObj[department_id]); + this.setState({ + membersList: this.state.membersTempObj[department_id], + canCheckUserIds: canCheckUserIds, + }); + } + }; + + onUserChecked = (user) => { + if (this.state.canCheckUserIds.indexOf(user.userid) !== -1) { + let newUsersTempObj = this.state.newUsersTempObj; + if (user.userid in newUsersTempObj) { + delete newUsersTempObj[user.userid]; + if (this.state.isCheckedAll) { + this.setState({ isCheckedAll: false }); + } + } else { + newUsersTempObj[user.userid] = user; + if (Object.keys(newUsersTempObj).length === this.state.canCheckUserIds.length) { + this.setState({ isCheckedAll: true }); + } + } + this.setState({ newUsersTempObj: newUsersTempObj }); + } + }; + + onAllUsersChecked = () => { + this.setState({ + isCheckedAll: !this.state.isCheckedAll, + }, () => { + if (this.state.isCheckedAll) { + let newUsersTempObj = {}; + let newUsersTempList = this.state.membersList.filter(user => { + return this.state.canCheckUserIds.indexOf(user.userid) !== -1; + }); + for (let i = 0; i < newUsersTempList.length; i++) { + newUsersTempObj[newUsersTempList[i].userid] = newUsersTempList[i]; + } + this.setState({ newUsersTempObj: newUsersTempObj }); + } else { + this.setState({ newUsersTempObj: {} }); + } + }); + }; + + onSubmit = () => { + const { newUsersTempObj } = this.state; + if (JSON.stringify(newUsersTempObj) === '{}') return; + let userList = []; + for (let i in newUsersTempObj) { + userList.push(newUsersTempObj[i]); + } + if (userList.length === 0) { + toaster.danger('未选择成员', {duration: 3}); + return; + } + seafileAPI.adminAddDingtalkUsersBatch(userList).then((res) => { + this.setState({ + newUsersTempObj: {}, + isCheckedAll: false, + }); + if (res.data.success) { + this.handleSubmitSuccess(res.data.success); + } + if (res.data.failed) { + const fails= res.data.failed; + for (let i = 0; i < fails.length; i++) { + toaster.danger(fails[i].name + ' ' + fails[i].error_msg, {duration: 3}); + } + } + }).catch((error) => { + this.handleError(error); + }); + + }; + + handleSubmitSuccess = (success) => { + let { membersTempObj, membersList, canCheckUserIds } = this.state; + for (let i = 0; i < success.length; i++) { + let { userid, name, email } = success[i]; + toaster.success(name + ' 成功导入', {duration: 1}); + // refresh all temp + if (canCheckUserIds.indexOf(userid) !== -1) { + canCheckUserIds.splice(canCheckUserIds.indexOf(userid), 1); + } + for (let j = 0; j < membersList.length; j++) { + if (membersList[j].userid === userid) { + membersList[j].email = email; + break; + } + } + for (let departmentId in membersTempObj) { + for (let k = 0; k < membersTempObj[departmentId].length; k++) { + if (membersTempObj[departmentId][k].userid === userid) { + membersTempObj[departmentId][k].email = email; + break; + } + } + } + } + this.setState({ + membersTempObj: membersTempObj, + membersList: membersList, + canCheckUserIds: canCheckUserIds, + }); + } + + importDepartmentDialogToggle = (importDepartment) => { + this.setState({ + isImportDepartmentDialogShow: !this.state.isImportDepartmentDialogShow, + importDepartment: importDepartment, + }, () => { + if (importDepartment) { + this.getDingtalkDepartmentsList(importDepartment.id); + } + }); + }; + + onImportDepartmentSubmit = () => { + let importDepartment = this.state.importDepartment; + if (!importDepartment) return; + seafileAPI.adminImportDingtalkDepartment(importDepartment.id).then((res) => { + this.setState({ + isMembersListLoading: true, + checkedDepartmentId: importDepartment.id, + membersTempObj: {}, + membersList: [], + newUsersTempObj: {}, + isCheckedAll: false, + canCheckUserIds: [], + }); + this.getDingtalkDepartmentMembersList(importDepartment.id); + this.importDepartmentDialogToggle(null); + if (res.data.success) { + this.handleImportDepartmentSubmitSuccess(res.data.success); + } + if (res.data.failed) { + this.handleImportDepartmentSubmitFailed(res.data.failed); + } + }).catch((error) => { + this.handleError(error); + }); + }; + + handleImportDepartmentSubmitSuccess = (successes) => { + for (let i = 0, len = successes.length; i < len; i++) { + let success = successes[i]; + let successMsg = success.type === 'department' ? '部门 ' + success.department_name + ' 导入成功' : success.api_user_name + ' 导入成功' ; + toaster.success(successMsg, { duration: 3 }); + } + }; + + handleImportDepartmentSubmitFailed = (fails) => { + for (let i = 0, len = fails.length; i < len; i++) { + let fail = fails[i]; + let failName = fail.type === 'department' ? fail.department_name : fail.api_user_name; + toaster.danger(failName + ' ' + fail.msg, { duration: 3} ); + } + }; + + handleError = (error) => { + const errorMsg = Utils.getErrorMsg(error); + toaster.danger(errorMsg); + } + + componentDidMount() { + this.getDingtalkDepartmentsList(null); + } + + renderNav() { + const btnClass = 'btn btn-secondary operation-item '; + return ( +
+
+ + + +
+
+ +
+
+ ); + } + + render() { + const { isImportDepartmentDialogShow, isTreeLoading, importDepartment, importDepartmentChildrenCount, importDepartmentMembersCount } = this.state; + let canImportDepartment = !!(isPro && isImportDepartmentDialogShow && !isTreeLoading && importDepartment); + return ( + + {this.renderNav()} +
+
+
+

{'钉钉集成'}

+
+
+ +
+ +
+
+
+ {canImportDepartment && + + } +
+ ); + } +} + +export default DingtalkDepartments; diff --git a/frontend/src/pages/sys-admin/dingtalk/dingtalk-department-members-list.js b/frontend/src/pages/sys-admin/dingtalk/dingtalk-department-members-list.js new file mode 100644 index 0000000000..b7f4b56624 --- /dev/null +++ b/frontend/src/pages/sys-admin/dingtalk/dingtalk-department-members-list.js @@ -0,0 +1,84 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Table } from 'reactstrap'; +import { siteRoot } from '../../../utils/constants'; +import Loading from '../../../components/loading'; + +const DingtalkDepartmentMembersListPropTypes = { + isMembersListLoading: PropTypes.bool.isRequired, + membersList: PropTypes.array.isRequired, + newUsersTempObj: PropTypes.object.isRequired, + checkedDepartmentId: PropTypes.number.isRequired, + onUserChecked: PropTypes.func.isRequired, + onAllUsersChecked: PropTypes.func.isRequired, + isCheckedAll: PropTypes.bool.isRequired, + canCheckUserIds: PropTypes.array.isRequired, +}; + +class DingtalkDepartmentMembersList extends Component { + + constructor(props) { + super(props); + } + + render() { + const { newUsersTempObj, checkedDepartmentId, isMembersListLoading, canCheckUserIds } = this.props; + const membersList = this.props.membersList.map((member, index) => { + let avatar = member.avatar; + if (member.avatar.length > 0) { + avatar = member.avatar; + } else { + avatar = siteRoot + 'media/avatars/default.png'; + } + return ( + + + {!member.email && + this.props.onUserChecked(member)} + checked={(member.userid in newUsersTempObj) ? 'checked' : ''}>} + + + {member.name} + {member.mobile} + {member.contact_email} + {member.email && } + + ); + }); + + return ( +
+ {isMembersListLoading && } + {!isMembersListLoading && this.props.membersList.length > 0 && + + + + + + + + + + + + {membersList} +
+ {canCheckUserIds.length > 0 && + this.props.onAllUsersChecked()}>} + {'名称'}{'手机号'}{'邮箱'}{'已添加'}
+ } + {!isMembersListLoading && this.props.membersList.length === 0 && +
+ +

{'成员列表为空'}

+
+ } +
+ ); + } +} + +DingtalkDepartmentMembersList.propTypes = DingtalkDepartmentMembersListPropTypes; + +export default DingtalkDepartmentMembersList; diff --git a/frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-node.js b/frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-node.js new file mode 100644 index 0000000000..323294a73a --- /dev/null +++ b/frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-node.js @@ -0,0 +1,147 @@ +import React, { Component, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { gettext, isPro } from '../../../utils/constants'; + +const DingtalkDepartmentsTreeNodePropTypes = { + index: PropTypes.number, + department: PropTypes.object.isRequired, + isChildrenShow: PropTypes.bool.isRequired, + onChangeDepartment: PropTypes.func.isRequired, + checkedDepartmentId: PropTypes.number.isRequired, + importDepartmentDialogToggle: PropTypes.func.isRequired, +}; + +class DingtalkDepartmentsTreeNode extends Component { + + constructor(props) { + super(props); + this.state = { + isChildrenShow: false, + dropdownOpen: false, + active: false, + }; + } + + toggleChildren = (e) => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ + isChildrenShow: !this.state.isChildrenShow, + }); + }; + + dropdownToggle = (e) => { + e.stopPropagation(); + this.setState({ dropdownOpen: !this.state.dropdownOpen }); + }; + + onMouseEnter = () => { + this.setState({ active: true }); + }; + + onMouseLeave = () => { + if (this.state.dropdownOpen) return; + this.setState({ active: false }); + }; + + importDepartmentDialogToggle = (depart) => { + this.setState({ active: false }); + this.props.importDepartmentDialogToggle(depart); + } + + componentDidMount() { + if (this.props.index === 0) { + this.setState({ isChildrenShow: true }); + this.props.onChangeDepartment(this.props.department.id); + } + } + + renderTreeNodes = (departmentsTree) => { + if (departmentsTree.length > 0) { + return departmentsTree.map((department) => { + return ( + + ); + }); + } + }; + + changeDept = (departmentID) => { + const { department, checkedDepartmentId } = this.props; + this.props.onChangeDepartment(departmentID); + if (checkedDepartmentId === department.id && !this.state.isChildrenShow) { + this.setState({ isChildrenShow: true }); + } + } + + render() { + const { isChildrenShow, department, checkedDepartmentId } = this.props; + let toggleClass = classNames({ + 'folder-toggle-icon fa fa-caret-down': department.children && this.state.isChildrenShow, + 'folder-toggle-icon fa fa-caret-right': department.children && !this.state.isChildrenShow, + }); + let nodeInnerClass = classNames({ + 'tree-node-inner': true, + 'tree-node-inner-hover': this.state.active, + 'tree-node-hight-light': checkedDepartmentId === department.id + }); + return ( + + {isChildrenShow && +
this.changeDept(department.id)} + onMouseEnter={this.onMouseEnter} + onMouseLeave={this.onMouseLeave} + > + this.toggleChildren(e)}> + + + {department.name} + {isPro && + this.dropdownToggle(e)} + direction="down" + style={this.state.active ? {} : { opacity: 0 }} + > + + + + {'导入部门'} + + + } +
+ } + {this.state.isChildrenShow && +
+ {department.children && this.renderTreeNodes(department.children)} +
+ } +
+ ); + } +} + +DingtalkDepartmentsTreeNode.propTypes = DingtalkDepartmentsTreeNodePropTypes; + +export default DingtalkDepartmentsTreeNode; diff --git a/frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-panel.js b/frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-panel.js new file mode 100644 index 0000000000..0b77144f46 --- /dev/null +++ b/frontend/src/pages/sys-admin/dingtalk/dingtalk-departments-tree-panel.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import Loading from '../../../components/loading'; +import DingtalkDepartmentsTreeNode from './dingtalk-departments-tree-node'; + +const DingtalkDepartmentsTreePanelPropTypes = { + isTreeLoading: PropTypes.bool.isRequired, + departmentsTree: PropTypes.array.isRequired, + onChangeDepartment: PropTypes.func.isRequired, + checkedDepartmentId: PropTypes.number.isRequired, + importDepartmentDialogToggle: PropTypes.func.isRequired, +}; + +class DingtalkDepartmentsTreePanel extends Component { + + constructor(props) { + super(props); + } + + render() { + const { departmentsTree } = this.props; + return ( +
+
+ {this.props.isTreeLoading ? + : +
+ {departmentsTree.length > 0 && departmentsTree.map((department, index) => { + return ( + + ); + })} +
+ } +
+
+ ); + } +} + +DingtalkDepartmentsTreePanel.propTypes = DingtalkDepartmentsTreePanelPropTypes; + +export default DingtalkDepartmentsTreePanel; diff --git a/frontend/src/pages/sys-admin/dingtalk/index.js b/frontend/src/pages/sys-admin/dingtalk/index.js new file mode 100644 index 0000000000..6dde4ea74a --- /dev/null +++ b/frontend/src/pages/sys-admin/dingtalk/index.js @@ -0,0 +1,5 @@ +import DingtalkDepartmentMembersList from './dingtalk-department-members-list'; +import DingtalkDepartmentsTreePanel from './dingtalk-departments-tree-panel'; +import DingtalkDepartmentsTreeNode from './dingtalk-departments-tree-node'; + +export { DingtalkDepartmentMembersList, DingtalkDepartmentsTreePanel, DingtalkDepartmentsTreeNode }; diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index 4c66ef053a..36990b4dc4 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -68,6 +68,7 @@ import Notifications from './notifications/notifications'; import FileScanRecords from './file-scan-records'; import VirusScanRecords from './virus-scan-records'; import WorkWeixinDepartments from './work-weixin-departments'; +import DingtalkDepartments from './dingtalk-departments'; import Invitations from './invitations/invitations'; import StatisticFile from './statistic/statistic-file'; @@ -258,6 +259,11 @@ class SysAdmin extends React.Component { currentTab={currentTab} tabItemClick={this.tabItemClick} /> + diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js index 50eb0a4d2e..c42b355d01 100644 --- a/frontend/src/pages/sys-admin/side-panel.js +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -5,7 +5,7 @@ import Logo from '../../components/logo'; import { gettext, siteRoot, isPro, otherPermission, canViewSystemInfo, canViewStatistic, canConfigSystem, canManageLibrary, canManageUser, canManageGroup, canViewUserLog, canViewAdminLog, constanceEnabled, multiTenancy, multiInstitution, sysadminExtraEnabled, - enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixin, + enableGuestInvitation, enableTermsAndConditions, enableFileScan, enableWorkWeixin, enableDingtalk, enableShareLinkReportAbuse } from '../../utils/constants'; const propTypes = { @@ -259,6 +259,20 @@ class SidePanel extends React.Component { } + + {otherPermission && enableDingtalk && +
  • + this.props.tabItemClick('dingtalk')} + > + + {'钉钉集成'} + +
  • + } + {otherPermission && enableShareLinkReportAbuse &&
  • { + this.token = response.data; + return this.token; + }) + } + + /** + * Login to server and create axios instance for future usage + */ + login() { + const url = this.server + '/api2/auth-token/'; + return axios.post(url, { + username: this.username, + password: this.password + }).then((response) => { + this.token = response.data.token; + this.req = axios.create({ + baseURL: this.server, + headers: { 'Authorization': 'Token ' + this.token } + }); + }) + } + + authPing() { + const url = this.server + '/api2/auth/ping/'; + return this.req.get(url); + } + + //---- Account API + + getAccountInfo() { + const url = this.server + '/api2/account/info/'; + return this.req.get(url); + } + + //---- Group operation + + listDepartments() { + const url = this.server + '/api/v2.1/departments/'; + return this.req.get(url); + } + + listGroups(withRepos = false) { + let options = {with_repos: withRepos ? 1 : 0}; + const url = this.server + '/api/v2.1/groups/'; + return this.req.get(url, {params: options}); + } + + listGroupRepos(groupID) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/libraries/'; + return this.req.get(url); + } + + + getGroup(groupID) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/'; + return this.req.get(url); + } + + createGroup(name) { + const url = this.server + '/api/v2.1/groups/'; + let form = new FormData(); + form.append('name', name); + return this._sendPostRequest(url, form); + } + + renameGroup(groupID, name) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/'; + const params = { + name: name + } + return this.req.put(url, params); + } + + deleteGroup(groupID) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/'; + return this.req.delete(url); + } + + transferGroup(groupID, ownerName) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/'; + const params = { + owner: ownerName + } + return this.req.put(url, params); + } + + quitGroup(groupID, userName) { + const name = encodeURIComponent(userName); + const url = this.server + '/api/v2.1/groups/' + groupID + '/members/' + name + '/'; + return this.req.delete(url); + } + + listGroupMembers(groupID, isAdmin=false, avatarSize=64) { + let url = this.server + '/api/v2.1/groups/' + groupID + '/members/?avatar_size=' + avatarSize + '&is_admin=' + isAdmin; + return this.req.get(url); + } + + addGroupMember(groupID, userName) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/members/'; + const params = { + email: userName + } + return this.req.post(url, params); + } + + addGroupMembers(groupID, userNames) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/members/bulk/'; + let form = new FormData(); + form.append('emails', userNames.join(',')); + return this._sendPostRequest(url, form); + } + + deleteGroupMember(groupID, userName) { + const name = encodeURIComponent(userName); + const url = this.server + '/api/v2.1/groups/' + groupID + '/members/' + name + '/'; + return this.req.delete(url); + } + + setGroupAdmin(groupID, userName, isAdmin) { + let name = encodeURIComponent(userName); + let url = this.server + '/api/v2.1/groups/' + groupID + '/members/' + name + '/'; + const params = { + is_admin: isAdmin + } + return this.req.put(url, params); + } + + createGroupOwnedLibrary(groupID, repo) { + let repoName = repo.repo_name; + let permission = repo.permission ? repo.permission : 'rw'; + const url = this.server + '/api/v2.1/groups/'+ groupID + '/group-owned-libraries/'; + let form = new FormData(); + form.append('name', repoName); // need to modify endpoint api; + if (repo.passwd) { + form.append("passwd", repo.passwd); + } + form.append('permission', permission); + return this._sendPostRequest(url, form); + } + + deleteGroupOwnedLibrary(groupID, repoID) { + const url = this.server + '/api/v2.1/groups/'+ groupID + '/group-owned-libraries/' + repoID+ '/'; + return this.req.delete(url); + } + + renameGroupOwnedLibrary(groupID, repoID, newName) { + const url = this.server + '/api/v2.1/groups/'+ groupID + '/group-owned-libraries/' + repoID + '/'; + let form = new FormData(); + form.append('name', newName); + return this.req.put(url, form); + } + + shareGroupOwnedRepoToUser(repoID, permission, username, path) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-share/' + let form = new FormData(); + form.append('permission', permission); + form.append('path', path); + if (Array.isArray(username)) { + username.forEach(item => { + form.append('username', item); + }); + } else { + form.append('username', username); + } + return this._sendPostRequest(url, form); + } + + modifyGroupOwnedRepoUserSharedPermission(repoID, permission, username, path) { //need check + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-share/' + let form = new FormData(); + form.append('permission', permission); + form.append('username', username); + form.append('path', path); + return this.req.put(url, form); + } + + deleteGroupOwnedRepoSharedUserItem(repoID, username, path) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-share/' + let params = {username: username, path: path}; + return this.req.delete(url, {data: params}); + } + + shareGroupOwnedRepoToGroup(repoID, permission, groupID, path) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-share/' + let form = new FormData(); + form.append('permission', permission); + form.append('path', path); + if (Array.isArray(groupID)) { + groupID.forEach(item => { + form.append('group_id', item); + }); + } else { + form.append('group_id', groupID); + } + return this._sendPostRequest(url, form); + } + + modifyGroupOwnedRepoGroupSharedPermission(repoID, permission, groupID, path) { //need check + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-share/' + let form = new FormData(); + form.append('permission', permission); + form.append('group_id', groupID); + form.append('path', path); + return this.req.put(url, form); + } + + deleteGroupOwnedRepoSharedGroupItem(repoID, groupID, path) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-share/' + let params = {group_id: groupID, path: path}; + return this.req.delete(url, {data: params}); + } + + //---- share operation + + // share-link + listUserShareLinks() { + const url = this.server + '/api/v2.1/share-links/'; + return this.req.get(url); + } + + getShareLink(repoID, filePath) { //list folder(file) links + const path = encodeURIComponent(filePath); + const url = this.server + '/api/v2.1/share-links/?repo_id=' + repoID + '&path=' + path; + return this.req.get(url); + } + + createShareLink(repoID, path, password, expireDays, permissions) { + const url = this.server + '/api/v2.1/share-links/'; + let form = new FormData(); + form.append('path', path); + form.append('repo_id', repoID); + if (permissions) { + form.append('permissions', permissions); + } + if (password) { + form.append('password', password); + } + if (expireDays) { + form.append('expire_days', expireDays); + } + return this._sendPostRequest(url, form); + } + + updateShareLink(token, permissions) { + var url = this.server + '/api/v2.1/share-links/' + token + '/'; + let form = new FormData(); + form.append('permissions', permissions); + return this.req.put(url, form); + } + + deleteShareLink(token) { + const url = this.server + '/api/v2.1/share-links/' + token + '/'; + return this.req.delete(url); + } + + sendShareLink(token, email, extraMsg) { + const url = this.server + '/api2/send-share-link/'; + let form = new FormData(); + form.append('token', token); + form.append('email', email); + if (extraMsg) { + form.append('extra_msg', extraMsg); + } + return this._sendPostRequest(url, form); + } + + listRepoShareLinks(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/share-links/'; + return this.req.get(url); + } + + deleteRepoShareLink(repoID, token) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/share-links/' + token + '/'; + return this.req.delete(url); + } + + listSharedRepos() { + const url = this.server + '/api/v2.1/shared-repos/'; + return this.req.get(url); + } + + // upload-link + listUserUploadLinks() { + const url = this.server + '/api/v2.1/upload-links/'; + return this.req.get(url); + } + + getUploadLink(repoID, path) { + const url = this.server + '/api/v2.1/upload-links/?repo_id=' + repoID + '&path=' + encodeURIComponent(path); + return this.req.get(url); + } + + createUploadLink(repoID, path, password, expireDays) { + const url = this.server + '/api/v2.1/upload-links/'; + let form = new FormData(); + form.append('path', path); + form.append('repo_id', repoID); + if (password) { + form.append('password', password); + } + if (expireDays) { + form.append('expire_days', expireDays); + } + return this._sendPostRequest(url, form); + } + + deleteUploadLink(token) { + const url = this.server + '/api/v2.1/upload-links/' + token + '/'; + return this.req.delete(url); + } + + sendUploadLink(token, email, extraMsg) { + const url = this.server + '/api2/send-upload-link/'; + let form = new FormData(); + form.append('token', token); + form.append('email', email); + if (extraMsg) { + form.append('extra_msg', extraMsg); + } + return this._sendPostRequest(url, form); + } + + listRepoUploadLinks(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/upload-links/'; + return this.req.get(url); + } + + deleteRepoUploadLink(repoID, token) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/upload-links/' + token + '/'; + return this.req.delete(url); + } + + // todo send upload link email + + // shared-libraries + listSharedItems(repoID, path, shareType) { // shareType: user, group + path = encodeURIComponent(path); + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/?p=' + path + '&share_type=' + shareType; + return this.req.get(url); + } + + getBeSharedRepos() { //listBeSharedRepos + const url = this.server + '/api2/beshared-repos/'; + return this.req.get(url); + } + + leaveShareRepo(repoID, options) { // deleteBeSharedRepo + const url = this.server + '/api2/beshared-repos/' + repoID + '/'; + return this.req.delete(url, {params: options}); + } + + // share repo to user is same to share Folder + + // unshare repo to user is same to unshare Folder + + deleteShareToUserItem(repoID, path, shareType, username) { + path = encodeURIComponent(path); + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/?p=' + path + '&share_type=' + shareType + '&username=' + encodeURIComponent(username); + return this.req.delete(url); + } + + updateShareToUserItemPermission(repoID, path, shareType, username, permission) { + path = encodeURIComponent(path); + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/?p=' + path + '&share_type=' + shareType + '&username=' + encodeURIComponent(username); + let form = new FormData(); + form.append('permission', permission); + return this._sendPostRequest(url, form); + } + + // share repo to group is same to share folder + + // unshare repo to group is same to unshare folder + + deleteShareToGroupItem(repoID, path, shareType, groupID) { + path = encodeURIComponent(path); + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/?p=' + path + '&share_type=' + shareType + '&group_id=' + groupID; + return this.req.delete(url); + } + + updateShareToGroupItemPermission(repoID, path, shareType, groupID, permission) { + path = encodeURIComponent(path); + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/?p=' + path + '&share_type=' + shareType + '&group_id=' + groupID; + let form = new FormData(); + form.append('permission', permission); + return this._sendPostRequest(url, form); + } + + leaveShareGroupOwnedRepo(repoID) { + const url = this.server + '/api/v2.1/group-owned-libraries/user-share-in-libraries/' + repoID + '/'; + return this.req.delete(url); + } + + shareableGroups() { + const url = this.server + '/api/v2.1/shareable-groups/'; + return this.req.get(url); + } + + getSharedRepos() { + const url = this.server + '/api2/shared-repos/'; + return this.req.get(url); + } + + updateRepoSharePerm(repoID, options) { + const url = this.server + '/api/v2.1/shared-repos/' + repoID + '/'; + return this.req.put(url, options); + } + + unshareRepo(repoID, options) { + const url = this.server + '/api/v2.1/shared-repos/' + repoID + '/'; + return this.req.delete(url, {params: options}); + } + + unshareRepoToGroup(repoID, groupID) { + const url = this.server + '/api/v2.1/groups/' + groupID + '/libraries/' + repoID +'/'; + return this.req.delete(url); + } + + // shared folders + shareFolder(repoID, path, shareType, permission, paramArray) { // shareType: user group + path = encodeURIComponent(path); + var form = new FormData(); + form.append('share_type', shareType); + form.append('permission', permission); + if (shareType == 'user') { + for (let i = 0; i < paramArray.length; i++) { + form.append('username', paramArray[i]); + } + } else { + for (let i = 0; i < paramArray.length; i++) { + form.append('group_id', paramArray[i]); + } + } + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/?p=' + path; + return this.req.put(url, form); + } + + listSharedFolders() { + const url = this.server + '/api/v2.1/shared-folders/'; + return this.req.get(url); + } + + updateFolderSharePerm(repoID, data, options) { + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/'; + return this.req.post(url, data, {params: options}); // due to the old api, use 'post' here + } + + unshareFolder(repoID, options) { + const url = this.server + '/api2/repos/' + repoID + '/dir/shared_items/'; + return this.req.delete(url, {params: options}); + } + + //---- repo(library) operation + createMineRepo(repo) { + const url = this.server + '/api2/repos/?from=web'; + return this.req.post(url, repo); + } + + createGroupRepo(groupID, repo) { + const url = this.server + '/api/v2.1/groups/'+ groupID + '/libraries/'; + let form = new FormData(); + form.append("repo_name", repo.repo_name); + if (repo.password) { + form.append("password", repo.password); + } + form.append("permission", repo.permission); + return this._sendPostRequest(url, form); + } + + listRepos(options) { + /* + * options: `{type: 'shared'}`, `{type: ['mine', 'shared', ...]}` + */ + let url = this.server + '/api/v2.1/repos/'; + + if (!options) { + // fetch all types of repos + return this.req.get(url); + } + + return this.req.get(url, { + params: options, + paramsSerializer: function(params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + }); + } + + getRepoInfo(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/'; + return this.req.get(url); + } + + getRepoHistoryLimit(repoID) { + const url = this.server + '/api2/repos/' + repoID + '/history-limit/'; + return this.req.get(url); + } + + setRepoHistoryLimit(repoID, historyDays) { + const url = this.server + '/api2/repos/' + repoID + '/history-limit/'; + let form = new FormData(); + form.append('keep_days', historyDays); + return this.req.put(url, form); + } + + resetAndSendEncryptedRepoPassword(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/send-new-password/'; + let form = new FormData(); + return this._sendPostRequest(url, form); + } + + deleteRepo(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/'; + return this.req.delete(url); + } + + renameRepo(repoID, newName) { + const url = this.server + '/api2/repos/' + repoID + '/?op=rename'; + let form = new FormData(); + form.append('repo_name', newName); + return this._sendPostRequest(url, form); + } + + transferRepo(repoID, owner) { + const url = this.server + '/api2/repos/' + repoID + '/owner/'; + let form = new FormData(); + form.append('owner', owner); + return this.req.put(url, form); + } + + setRepoDecryptPassword(repoID, password) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/set-password/'; + let form = new FormData(); + form.append('password', password); + return this._sendPostRequest(url, form); + } + + changeEncryptedRepoPassword(repoID, oldPassword, newPassword) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/set-password/'; + const data = { + 'old_password': oldPassword, + 'new_password': newPassword + }; + return this.req.put(url, data); + } + + createPublicRepo(repo) { + const url = this.server + '/api2/repos/public/'; + return this.req.post(url, repo); + } + + selectOwnedRepoToPublic(repoID, options) { // todo change a exist repo to public + const url = this.server + '/api/v2.1/shared-repos/' + repoID + '/'; + return this.req.put(url, options); + } + + // remove public repo is same to unshareRepo; + + getSource() { // for search + let CancelToken = axios.CancelToken; + let source = CancelToken.source(); + return source; + } + + searchFilesInPublishedRepo(repoID, q, page, perPage) { + const url = this.server + '/api/v2.1/published-repo-search/'; + let params = { + repo_id: repoID, + q: q, + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + searchFiles(searchParams, cancelToken) { + let url = this.server + '/api2/search/'; + url = url + '?q=' + searchParams.q; + if (searchParams.search_repo) {url = url + '&search_repo=' + searchParams.search_repo;} + if (searchParams.search_ftypes) {url = url + '&search_ftypes=' + searchParams.search_ftypes;} + if (searchParams.page) {url = url + '&page=' + searchParams.page;} + if (searchParams.per_page) {url = url + '&per_page=' + searchParams.per_page;} + if (searchParams.search_path) {url = url + '&search_path=' + searchParams.search_path;} + if (searchParams.obj_type) {url = url + '&obj_type=' + searchParams.obj_type;} + if (searchParams.input_fexts) {url = url + '&input_fexts=' + searchParams.input_fexts;} + if (searchParams.with_permission){url = url + '&with_permission=' + searchParams.with_permission;} + if (searchParams.time_from) {url = url + '&time_from=' + searchParams.time_from;} + if (searchParams.time_to) {url = url + '&time_to=' + searchParams.time_to;} + if (searchParams.size_from) {url = url + '&size_from=' + searchParams.size_from;} + if (searchParams.size_to) {url = url + '&size_to=' + searchParams.size_to;} + if (searchParams.shared_from) {url = url + '&shared_from=' + searchParams.shared_from;} + if (searchParams.not_shared_from) {url = url + '¬_shared_from=' + searchParams.not_shared_from;} + if (searchParams.ftype) { + for (let i= 0; i< searchParams.ftype.length; i++){ + url = url + '&ftype=' + searchParams.ftype[i];} + } + return this.req.get(url, {cancelToken : cancelToken}); + } + + listRepoAPITokens(repo_id) { + console.log('server in function is: ', this.server); + var url = this.server + '/api/v2.1/repos/' + repo_id + '/repo-api-tokens/'; + return this.req.get(url); + } + + addRepoAPIToken(repo_id, app_name, permission) { + var url = this.server + '/api/v2.1/repos/' + repo_id + '/repo-api-tokens/'; + var form = new FormData(); + form.append('app_name', app_name); + form.append('permission', permission); + return this._sendPostRequest(url, form); + } + + deleteRepoAPIToken(repo_id, app_name) { + var url = this.server + '/api/v2.1/repos/' + repo_id + '/repo-api-tokens/' + app_name + '/'; + return this.req.delete(url); + } + + updateRepoAPIToken(repo_id, app_name, permission) { + var url = this.server + '/api/v2.1/repos/' + repo_id + '/repo-api-tokens/' + app_name + '/'; + var form = new FormData(); + form.append('permission', permission); + return this.req.put(url, form); + } + + //admin + listDeletedRepo() { + const url = this.server + '/api/v2.1/deleted-repos/'; + return this.req.get(url); + } + + restoreDeletedRepo(repoID) { + const url = this.server + '/api/v2.1/deleted-repos/'; + let form = new FormData(); + form.append('repo_id', repoID); + return this._sendPostRequest(url, form); + } + + //---- directory operation + listDir(repoID, dirPath, { recursive = false, type = '', with_thumbnail = false, with_parents = false } = {}) { + /* + * opts: `{recursive: true}`, `{'with_thumbnail': true}` + */ + const url = this.server + '/api/v2.1/repos/' + repoID + '/dir/'; + let params = {}; + params.p = dirPath; + if (recursive) { + params.recursive = recursive ? 1 : 0; + } + if (type) { + params.t = type; + } + if (with_thumbnail) { + params.with_thumbnail = with_thumbnail; + } + if (with_parents) { + params.with_parents = with_parents; + } + return this.req.get(url, {params: params}); + } + + listWikiDir(slug, dirPath, withParents) { + const path = encodeURIComponent(dirPath); + let url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/dir/?p=' + path; + if (withParents) { + url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/dir/?p=' + path + '&with_parents=' + withParents; + } + return this.req.get(url); + } + + getDirInfo(repoID, dirPath) { + const path = encodeURIComponent(dirPath); + const url = this.server + '/api/v2.1/repos/' + repoID + '/dir/detail/?path=' + path; + return this.req.get(url); + } + + createDir(repoID, dirPath) { + const path = encodeURIComponent(dirPath); + const url = this.server + '/api2/repos/' + repoID + '/dir/?p=' + path; + let form = new FormData(); + form.append('operation', 'mkdir'); + return this._sendPostRequest(url, form); + } + + renameDir(repoID, dirPath, newdirName) { + const path = encodeURIComponent(dirPath); + const url = this.server + '/api2/repos/' + repoID + '/dir/?p=' + path; + let form = new FormData(); + form.append("operation", 'rename'); + form.append("newname", newdirName); + return this._sendPostRequest(url, form); + } + + deleteDir(repoID, dirPath) { + const path = encodeURIComponent(dirPath); + const url = this.server + '/api/v2.1/repos/' + repoID + '/dir/?p=' + path; + return this.req.delete(url); + } + + //---- multiple(File&Folder) operation + copyDir(repoID, dstrepoID, dstfilePath, dirPath, direntNames) { + let paths = []; + let url = this.server; + url += repoID === dstrepoID ? '/api/v2.1/repos/sync-batch-copy-item/' : '/api/v2.1/repos/async-batch-copy-item/'; + if (Array.isArray(direntNames)) { + paths = direntNames; + } else { + paths.push(direntNames) + } + let operation = { + 'src_repo_id': repoID, + 'src_parent_dir': dirPath, + 'dst_repo_id': dstrepoID, + 'dst_parent_dir': dstfilePath, + 'src_dirents': paths + } + + return this._sendPostRequest(url, operation, {headers: {'Content-Type': 'application/json'}}); + } + + moveDir(repoID, dstrepoID, dstfilePath, dirPath, direntNames) { + let paths = []; + let url = this.server; + + url += repoID === dstrepoID ? '/api/v2.1/repos/sync-batch-move-item/' : '/api/v2.1/repos/async-batch-move-item/'; + if (Array.isArray(direntNames)) { + paths = direntNames; + } else { + paths.push(direntNames); + } + let operation = { + 'src_repo_id': repoID, + 'src_parent_dir': dirPath, + 'dst_repo_id': dstrepoID, + 'dst_parent_dir': dstfilePath, + 'src_dirents': paths + } + + return this._sendPostRequest(url, operation, {headers: {'Content-Type': 'application/json'}}); + } + + queryAsyncOperationProgress(task_id) { + const url = this.server + '/api/v2.1/query-copy-move-progress/?task_id=' + task_id; + return this.req.get(url); + } + + deleteMutipleDirents(repoID, parentDir, direntNames) { + const url = this.server + '/api/v2.1/repos/batch-delete-item/'; + let operation = { + 'repo_id': repoID, + 'parent_dir': parentDir, + 'dirents': direntNames + }; + return this.req.delete(url, {data: operation}, {headers: {'Content-Type': 'application/json'}}); + } + + zipDownload(repoID, parent_dir, dirents) { // can download one dir + const url = this.server + '/api/v2.1/repos/' + repoID + '/zip-task/'; + const form = new FormData(); + form.append('parent_dir', parent_dir); + if (Array.isArray(dirents)) { + dirents.forEach(item => { + form.append('dirents', item); + }); + } else { + form.append('dirents', dirents); + } + + return this._sendPostRequest(url, form); + } + + queryZipProgress(zip_token) { + const url = this.server + '/api/v2.1/query-zip-progress/?token=' + zip_token; + return this.req.get(url); + } + + cancelZipTask(zip_token) { + const url = this.server + '/api/v2.1/cancel-zip-task/'; + const form = new FormData(); + form.append("token", zip_token); + return this.req.post(url, form); + } + + //---- File Operation + getFileInfo(repoID, filePath) { + const path = encodeURIComponent(filePath); + const url = this.server + '/api2/repos/' + repoID + '/file/detail/?p=' + path; + return this.req.get(url); + } + + getFileHistory(repoID, folderPath) { + const url = this.server + "/api2/repos/" + repoID + "/file/history/?p=" + encodeURIComponent(folderPath); + return this.req.get(url); + } + + getFileDownloadLink(repoID, filePath) { + // reuse default to 1 to eliminate cross domain request problem + // In browser, the browser will send an option request to server first, the access Token + // will become invalid if reuse=0 + const path = encodeURIComponent(filePath); + const url = this.server + '/api2/repos/' + repoID + '/file/?p=' + path + '&reuse=1'; + return this.req.get(url); + } + + getFileContent(downloadLink) { + return axios.create().get(downloadLink); + } + + createFile(repoID, filePath, isDraft) { + const path = encodeURIComponent(filePath); + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/?p=' + path; + let form = new FormData(); + form.append('operation', 'create'); + form.append('is_draft', isDraft); + return this._sendPostRequest(url, form); + } + + renameFile(repoID, filePath, newfileName) { + const path = encodeURIComponent(filePath); + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/?p=' + path; + let form = new FormData(); + form.append('operation', 'rename'); + form.append('newname', newfileName); + return this._sendPostRequest(url, form); + } + + lockfile(repoID, filePath) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/?p=' + encodeURIComponent(filePath); + let form = new FormData(); + form.append('operation', 'lock'); + return this.req.put(url, form); + } + + unlockfile(repoID, filePath) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/?p=' + encodeURIComponent(filePath); + let form = new FormData(); + form.append('operation', 'unlock'); + return this.req.put(url, form); + } + + // move need to add + + // copy need to add + + revertFile(repoID, path, commitID) { + const url = this.server + '/api/v2.1/repos/'+ repoID + '/file/?p=' + encodeURIComponent(path); + let form = new FormData(); + form.append("operation", 'revert'); + form.append("commit_id", commitID); + return this._sendPostRequest(url, form); + } + + revertFolder(repoID, path, commitID) { + const url = this.server + '/api/v2.1/repos/'+ repoID + '/dir/?p=' + encodeURIComponent(path); + let form = new FormData(); + form.append("operation", 'revert'); + form.append("commit_id", commitID); + return this._sendPostRequest(url, form); + } + + revertRepo(repoID, commitID) { + const url = this.server + '/api/v2.1/repos/'+ repoID + '/commits/' + commitID + '/revert/'; + return this.req.post(url); + } + + deleteFile(repoID, filePath) { + const path = encodeURIComponent(filePath); + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/?p=' + path; + return this.req.delete(url); + } + + getFileServerUploadLink(repoID, folderPath) { + const path = encodeURIComponent(folderPath); + const url = this.server + '/api2/repos/' + repoID + '/upload-link/?p=' + path + '&from=web'; + return this.req.get(url); + } + + getFileUploadedBytes(repoID, filePath, fileName) { + let url = this.server + '/api/v2.1/repos/' + repoID + '/file-uploaded-bytes/'; + let params = { + parent_dir: filePath, + file_name: fileName, + }; + return this.req.get(url, {params: params}); + } + + uploadImage (uploadLink, formData, onUploadProgress = null) { + return ( + axios.create()({ + method: "post", + data: formData, + url: uploadLink, + onUploadProgress: onUploadProgress + }) + ); + } + + getUpdateLink(repoID, folderPath) { + const url = this.server + '/api2/repos/' + repoID + '/update-link/?p=' + encodeURIComponent(folderPath); + return this.req.get(url) + } + + updateFile(uploadLink, filePath, fileName, data) { + let formData = new FormData(); + formData.append("target_file", filePath); + formData.append("filename", fileName); + let blob = new Blob([data], { type: "text/plain" }); + formData.append("file", blob); + return ( + axios.create()({ + method: 'post', + url: uploadLink, + data: formData, + }) + ); + } + + listFileHistoryRecords(repoID, path, page, per_page) { + const url = this.server + '/api/v2.1/repos/'+ repoID + '/file/new_history/'; + const params = { + path: path, + page: page, + per_page: per_page, + } + return this.req.get(url, {params: params}); + } + + listOldFileHistoryRecords(repoID, path, commitID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/file/history/'; + const params = { + path: path, + commit_id: commitID, + }; + return this.req.get(url, {params: params}); + } + + getFileRevision(repoID, commitID, filePath) { + let url = this.server + '/api2/' + 'repos/' + repoID + '/file' + '/revision/?p=' + encodeURIComponent(filePath) + '&commit_id=' + commitID + return this.req.get(url); + } + + // file commit api + deleteComment(repoID, commentID) { + const url = this.server + '/api2/repos/' + repoID + '/file/comments/' + commentID + '/'; + return this.req.delete(url); + } + + listComments(repoID, filePath, resolved) { + const path = encodeURIComponent(filePath); + let url = this.server + '/api2/repos/' + repoID + '/file/comments/?p=' + path; + if (resolved) { + url = url + '&resolved=' + resolved; + } + return this.req.get(url); + } + + postComment(repoID, filePath, comment, detail) { + const path = encodeURIComponent(filePath); + const url = this.server + '/api2/repos/' + repoID + '/file/comments/?p=' + path; + let form = new FormData(); + form.append("comment", comment); + if (detail) { + form.append("detail", detail); + } + return this._sendPostRequest(url, form); + } + + getCommentsNumber(repoID, path) { + const p = encodeURIComponent(path); + const url = this.server + '/api2/repos/' + repoID + '/file/comments/counts/?p=' + p; + return this.req.get(url); + } + + updateComment(repoID, commentID, resolved, detail, comment) { + const url = this.server + '/api2/repos/' + repoID + '/file/comments/' + commentID + '/'; + let params = {}; + if (resolved) params.resolved = resolved; + if (detail) params.detail = detail; + if (comment) params.comment = comment; + return this.req.put(url, params); + } + + getRepoDraftCounts(repoID) { + const url = this.server + '/api/v2.1/repo/' + repoID + '/draft-counts/' + return this.req.get(url); + } + + listRepoDrafts(repoID) { + const url = this.server + '/api/v2.1/repo/' + repoID + '/drafts/'; + return this.req.get(url); + } + + // draft operation api + getDraft(id) { + const url = this.server + '/api/v2.1/drafts/' + id + '/'; + return this.req.get(url) + } + + listDrafts() { + const url = this.server + '/api/v2.1/drafts'; + return this.req.get(url); + } + + createDraft(repoID, filePath) { + const url = this.server + '/api/v2.1/drafts/'; + const form = new FormData(); + form.append("repo_id", repoID); + form.append("file_path", filePath); + return this.req.post(url, form); + } + + deleteDraft(id) { + const url = this.server + '/api/v2.1/drafts/' + id + '/'; + return this.req.delete(url); + } + + publishDraft(id) { + const url = this.server + '/api/v2.1/drafts/' + id + '/'; + const params = { + operation: 'publish' + } + return this.req.put(url, params); + } + + // review api + listDraftReviewers(draftID) { + const url = this.server + '/api/v2.1/drafts/' + draftID + '/reviewer/'; + return this.req.get(url); + } + + addDraftReviewers(draftID, reviewers) { + const url = this.server + '/api/v2.1/drafts/' + draftID + '/reviewer/'; + let form = new FormData(); + for(let i = 0 ; i < reviewers.length ; i ++) { + form.append('reviewer', reviewers[i]); + } + return this._sendPostRequest(url, form); + } + + deleteDraftReviewer(draftID, reviewer) { + const url = this.server + '/api/v2.1/drafts/' + draftID + '/reviewer/?username=' + encodeURIComponent(reviewer); + return this.req.delete(url); + } + + // starred + listStarredItems() { + const url = this.server + '/api/v2.1/starred-items/'; + return this.req.get(url); + } + + starItem(repoID, path) { + const url = this.server + '/api/v2.1/starred-items/'; + let form = new FormData(); + form.append('repo_id', repoID); + form.append('path', path); + return this._sendPostRequest(url, form); + } + + unstarItem(repoID, path) { + const url = this.server + '/api/v2.1/starred-items/?repo_id=' + repoID + '&path=' + encodeURIComponent(path); + return this.req.delete(url); + } + + //---- tags module api + // repo tags + listRepoTags(repoID) { + var url = this.server + '/api/v2.1/repos/' + repoID + '/repo-tags/'; + return this.req.get(url); + } + + createRepoTag(repoID, name, color) { + var url = this.server + '/api/v2.1/repos/' + repoID + '/repo-tags/'; + var form = new FormData(); + form.append('name', name); + form.append('color', color); + return this._sendPostRequest(url, form); + } + + deleteRepoTag(repoID, repo_tag_id) { + var url = this.server + '/api/v2.1/repos/' + repoID + '/repo-tags/' + repo_tag_id + '/'; + return this.req.delete(url); + } + + updateRepoTag(repoID, repo_tag_id, name, color) { + var url = this.server + '/api/v2.1/repos/' + repoID + '/repo-tags/' + repo_tag_id + '/'; + var params = { + name: name, + color: color, + }; + return this.req.put(url, params); + } + + listTaggedFiles(repoID, repoTagId) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/tagged-files/' + repoTagId + '/'; + return this.req.get(url); + } + + // file tag api + listFileTags(repoID, filePath) { + var p = encodeURIComponent(filePath) + var url = this.server + '/api/v2.1/repos/' + repoID + '/file-tags/?file_path=' + p; + return this.req.get(url); + } + + addFileTag(repoID, filePath, repoTagId) { + var form = new FormData(); + form.append('file_path', filePath); + form.append('repo_tag_id', repoTagId); + var url = this.server + '/api/v2.1/repos/' + repoID + '/file-tags/'; + return this._sendPostRequest(url, form); + } + + deleteFileTag(repoID, fileTagId) { + var url = this.server + '/api/v2.1/repos/' + repoID + '/file-tags/' + fileTagId + '/'; + return this.req.delete(url); + } + + //---- RelatedFile API + listRelatedFiles(repoID, filePath) { + const p = encodeURIComponent(filePath); + const url = this.server + '/api/v2.1/related-files/?repo_id=' + repoID + '&file_path=' + p; + return this.req.get(url); + } + + addRelatedFile(oRepoID, rRepoID, oFilePath, rFilePath) { + const form = new FormData(); + form.append('o_repo_id', oRepoID); + form.append('r_repo_id', rRepoID); + form.append('o_path', oFilePath); + form.append('r_path', rFilePath); + const url = this.server + '/api/v2.1/related-files/'; + return this._sendPostRequest(url, form); + } + + deleteRelatedFile(repoID, filePath, relatedID) { + const url = this.server + '/api/v2.1/related-files/' + relatedID + '/'; + const params = { + repo_id: repoID, + file_path: filePath + }; + return this.req.delete(url, { data: params }); + } + + saveSharedFile(repoID, filePath, sharedToken) { + const url = this.server + '/share/link/save/?t=' + sharedToken; + let form = new FormData(); + form.append('dst_repo', repoID); + form.append('dst_path', filePath); + form.append('s_token', sharedToken); + return this._sendPostRequest(url, form); + } + + addAbuseReport(sharedToken, abuseType, description, reporter, filePath) { + const url = this.server + '/api/v2.1/abuse-reports/'; + let form = new FormData(); + form.append('share_link_token', sharedToken); + form.append('abuse_type', abuseType); + form.append('description', description); + form.append('reporter', reporter); + form.append('file_path', filePath); + return this._sendPostRequest(url, form); + } + + sysAdminListAbuseReports() { + let url = this.server + '/api/v2.1/admin/abuse-reports/'; + return this.req.get(url); + } + + sysAdminUpdateAbuseReport(handled, abuseReportId) { + const url = this.server + '/api/v2.1/admin/abuse-reports/' + abuseReportId + '/'; + let form = new FormData(); + form.append('handled', handled); + return this.req.put(url, form); + } + + getInternalLink(repoID, filePath, direntType) { + let isDir = direntType === 'dir' ? true : false; + const path = encodeURIComponent(filePath); + const url = this.server + '/api/v2.1/smart-link/?repo_id=' + repoID + '&path=' + path + '&is_dir=' + isDir; + return this.req.get(url); + } + + getWikiFileContent(slug, filePath) { + const path = encodeURIComponent(filePath); + const time = new Date().getTime(); + const url = this.server + '/api/v2.1/wikis/' + encodeURIComponent(slug) + '/content/' + '?p=' + path + '&_=' + time; + return this.req.get(url) + } + + //---- Avatar API + getUserAvatar(user, size) { + const url = this.server + '/api2/avatars/user/' + encodeURIComponent(user) + '/resized/' + size +'/'; + return this.req.get(url); + } + + //---- Notification API + listPopupNotices(page, perPage) { + const url = this.server + '/api/v2.1/notifications/'; + let form = new FormData(); + form.append('page', page); + form.append('per_page', perPage); + return this.req.get(url, form); + } + + updateNotifications() { + const url = this.server + '/api/v2.1/notifications/'; + return this.req.put(url); + } + + getUnseenNotificationCount() { + const url = this.server + '/api/v2.1/notifications/'; + return this.req.get(url); + } + + markNoticeAsRead(noticeId) { + const url = this.server + '/api/v2.1/notification/'; + let from = new FormData(); + from.append('notice_id', noticeId); + return this.req.put(url, from); + } + + //---- Linked Devices API + listLinkedDevices() { + const url = this.server + '/api2/devices/'; + return this.req.get(url); + } + + unlinkDevice(platform, device_id) { + const url = this.server + "/api2/devices/"; + let param = { + platform: platform, + device_id: device_id + }; + return this.req.delete(url, {data: param}); + } + + //---- Activities API + listActivities(pageNum, avatarSize=36) { + const url = this.server + '/api/v2.1/activities/?page=' + pageNum + '&avatar_size=' + avatarSize; + return this.req.get(url); + } + + //---- Thumbnail API + createThumbnail(repo_id, path, thumbnail_size) { + const url = this.server + '/thumbnail/' + repo_id + '/create/?path=' + + encodeURIComponent(path) + '&size=' + thumbnail_size; + return this.req.get(url, {headers: {'X-Requested-With': 'XMLHttpRequest'}}); + } + + //---- Users API + searchUsers(searchParam) { + const url = this.server + '/api2/search-user/?q=' + encodeURIComponent(searchParam); + return this.req.get(url); + } + + //---- wiki module API + listWikis(options) { + /* + * options: `{type: 'shared'}`, `{type: ['mine', 'shared', ...]}` + */ + let url = this.server + '/api/v2.1/wikis/'; + if (!options) { + // fetch all types of wikis + return this.req.get(url); + } + return this.req.get(url, { + params: options, + paramsSerializer: function paramsSerializer(params) { + let list = []; + for (let key in params) { + if (Array.isArray(params[key])) { + for (let i = 0, len = params[key].length; i < len; i++) { + list.push(key + '=' + encodeURIComponent(params[key][i])); + } + } else { + list.push(key + '=' + encodeURIComponent(params[key])); + } + } + return list.join('&'); + } + }); + } + + addWiki(repoID) { + const url = this.server + '/api/v2.1/wikis/'; + let form = new FormData(); + form.append('repo_id', repoID); + return this._sendPostRequest(url, form); + } + + renameWiki(slug, name) { + const url = this.server + '/api/v2.1/wikis/' + slug + '/'; + let form = new FormData(); + form.append('wiki_name', name); + return this._sendPostRequest(url, form); + } + + updateWikiPermission(wikiSlug, permission) { + const url = this.server + '/api/v2.1/wikis/' + wikiSlug + '/'; + let params = { + permission: permission + }; + return this.req.put(url, params); + } + + deleteWiki(slug) { + const url = this.server + '/api/v2.1/wikis/' + slug + '/'; + return this.req.delete(url); + } + + //----MetaData API + fileMetaData(repoID, filePath) { + const url = this.server + '/api2/repos/' + repoID + '/file/metadata/?p=' + encodeURIComponent(filePath); + return this.req.get(url); + } + + dirMetaData(repoID, dirPath) { + const url = this.server + '/api2/repos/' + repoID + '/dir/metadata/?p=' + encodeURIComponent(dirPath); + return this.req.get(url); + } + + // single org admin api + orgAdminGetOrgInfo() { + const url = this.server + '/api/v2.1/org/admin/info/'; + return this.req.get(url); + } + + orgAdminListOrgUsers(orgID, isStaff, page) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/?is_staff=' + isStaff + '&page=' + page; + return this.req.get(url); + } + + orgAdminGetOrgUserBesharedRepos(orgID, email) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/beshared-repos/'; + return this.req.get(url); + } + + orgAdminGetOrgUserOwnedRepos(orgID, email) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/repos/'; + return this.req.get(url); + } + + orgAdminGetOrgUserInfo(orgID, email) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/'; + return this.req.get(url); + } + + orgAdminSetOrgUserName(orgID, email, name) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/'; + const data = { + name: name + }; + return this.req.put(url, data); + } + + orgAdminSetOrgUserContactEmail(orgID, email, contactEmail) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/'; + const data = { + contact_email: contactEmail + }; + return this.req.put(url, data); + } + + orgAdminSetOrgUserQuota(orgID, email, quota) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/'; + const data = { + quota_total: quota + }; + return this.req.put(url, data); + } + + orgAdminDeleteOrgUser(orgID, email) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/'; + return this.req.delete(url); + } + + orgAdminResetOrgUserPassword(orgID, email) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/'+ encodeURIComponent(email) + '/set-password/'; + return this.req.put(url); + } + + orgAdminChangeOrgUserStatus(userID, statusCode) { + const url = this.server + '/org/useradmin/toggle_status/' + userID + '/'; + let form = new FormData(); + form.append('s', statusCode); + return this.req.post(url, form, { headers: {'X-Requested-With': 'XMLHttpRequest'}}); + } + + orgAdminAddOrgUser(orgID, email, name, password) { + const url = this.server + '/api/v2.1/org/' + orgID +'/admin/users/'; + let form = new FormData(); + form.append('email', email); + form.append('name', name); + form.append('password', password); + return this._sendPostRequest(url, form); + } + + orgAdminSetOrgAdmin(orgID, email, isStaff) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/users/' + encodeURIComponent(email) + '/'; + let form = new FormData(); + form.append('is_staff', isStaff) + return this.req.put(url, form); + } + + orgAdminListOrgGroups(orgID, page) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/?page=' + page; + return this.req.get(url); + } + + orgAdminGetGroup(orgID, groupID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/'; + return this.req.get(url); + } + + orgAdminDeleteOrgGroup(orgID, groupID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/'; + return this.req.delete(url); + } + + orgAdminListOrgRepos(orgID, page) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/repos/?page=' + page; + return this.req.get(url); + } + + orgAdminDeleteOrgRepo(orgID, repoID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/repos/' + repoID + '/'; + return this.req.delete(url); + } + + orgAdminTransferOrgRepo(orgID, repoID, email) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/repos/' + repoID + '/'; + let form = new FormData(); + form.append('email', email); + return this.req.put(url, form); + } + + // org links + orgAdminListOrgLinks(page) { + const url = this.server + '/api/v2.1/org/admin/links/?page=' + page; + return this.req.get(url); + } + + orgAdminDeleteOrgLink(token) { + const url = this.server + '/api/v2.1/org/admin/links/' + token + '/'; + return this.req.delete(url); + } + + // org depart group + orgAdminListDepartGroups(orgID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/'; + return this.req.get(url); + } + + orgAdminListGroupInfo(orgID, groupID, showAncestors) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/' + groupID + '/?return_ancestors=' + showAncestors; + return this.req.get(url); + } + + orgAdminAddDepartGroup(orgID, parentGroup, groupName, groupOwner, groupStaff) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/'; + let form = new FormData(); + form.append('parent_group', parentGroup); + form.append('group_name', groupName); + if (groupOwner) { + form.append('group_owner', groupOwner); + } + if (groupStaff) { + form.append('group_staff', groupStaff.join(',')); + } + return this._sendPostRequest(url, form); + } + + orgAdminDeleteDepartGroup(orgID, groupID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/address-book/groups/' + groupID + '/'; + return this.req.delete(url); + } + + orgAdminSetGroupQuota(orgID, groupID, quota) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/'; + let form = new FormData(); + form.append('quota', quota); + return this.req.put(url, form); + } + + orgAdminListGroupRepos(orgID, groupID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/libraries/'; + return this.req.get(url); + } + + orgAdminAddDepartmentRepo(orgID, groupID, repoName) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/group-owned-libraries/'; + let form = new FormData(); + form.append('repo_name', repoName); + return this._sendPostRequest(url, form); + } + + orgAdminDeleteDepartmentRepo(orgID, groupID, repoID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/group-owned-libraries/' + repoID; + return this.req.delete(url); + } + + orgAdminListGroupMembers(orgID, groupID) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/members/'; + return this.req.get(url); + } + + orgAdminDeleteGroupMember(orgID, groupID, userEmail) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/members/' + encodeURIComponent(userEmail) + '/'; + return this.req.delete(url); + } + + orgAdminAddGroupMember(orgID, groupID, userEmail) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/members/'; + let form = new FormData(); + form.append('email', userEmail); + return this._sendPostRequest(url, form); + } + + orgAdminSetGroupMemberRole(orgID, groupID, userEmail, isAdmin) { + const url = this.server + '/api/v2.1/org/' + orgID + '/admin/groups/' + groupID + '/members/' + encodeURIComponent(userEmail) + '/'; + let form = new FormData(); + form.append('is_admin', isAdmin); + return this.req.put(url, form); + } + + // org admin logs + orgAdminListFileAudit(email, repoID, page) { + let url = this.server + '/api/v2.1/org/admin/logs/file-access/?page=' + page; + if (email) { + url = url + '&email=' + encodeURIComponent(email); + } + if (repoID) { + url = url + '&repo_id=' + repoID; + } + return this.req.get(url); + } + + orgAdminListFileUpdate(email, repoID, page) { + let url = this.server + '/api/v2.1/org/admin/logs/file-update/?page=' + page; + if (email) { + url = url + '&email=' + encodeURIComponent(email); + } + if (repoID) { + url = url + '&repo_id=' + repoID; + } + return this.req.get(url); + } + + orgAdminListPermAudit(email, repoID, page) { + let url = this.server + '/api/v2.1/org/admin/logs/repo-permission/?page=' + page; + if (email) { + url = url + '&email=' + encodeURIComponent(email); + } + if (repoID) { + url = url + '&repo_id=' + repoID; + } + return this.req.get(url); + } + + orgAdminGetFileUpdateDetail(repoID, commitID) { + let url = this.server + '/ajax/repo/' + repoID + '/history/changes/?commit_id=' + commitID; + return this.req.get(url, { headers: {'X-Requested-With': 'XMLHttpRequest'}}); + } + + markdownLint(slateValue) { + const url = this.server + '/api/v2.1/markdown-lint/'; + let form = new FormData(); + form.append('slate', slateValue); + return this._sendPostRequest(url, form); + } + + listFileScanRecords() { + const url = this.server + '/api/v2.1/admin/file-scan-records/'; + return this.req.get(url); + } + + queryOfficeFileConvertStatus(repoID, commitID, path, fileType, shareToken) { + const url = this.server + '/office-convert/status/'; + const params = { + repo_id: repoID, + commit_id: commitID, + path: path, + doctype: fileType // 'document' or 'spreadsheet' + }; + // for view of share link + if (shareToken) { + params['token'] = shareToken; + } + return this.req.get(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, + params: params + }); + } + + listSharedDir(token, path, thumbnailSize) { + const url = this.server + '/api/v2.1/share-links/' + token + '/dirents/'; + const params = { + thumbnail_size: thumbnailSize, + path: path + }; + return this.req.get(url, { + params: params + }); + } + + getShareLinkZipTask(token, path) { + const url = this.server + '/api/v2.1/share-link-zip-task/'; + const params = { + share_link_token: token, + path: path + }; + return this.req.get(url, { + params: params + }); + } + + getShareLinkThumbnail(token, filePath, thumbnailSize) { + const url = this.server + '/thumbnail/' + token + '/create/'; + const params = { + path: filePath, + size: thumbnailSize + }; + return this.req.get(url, { + params: params + }); + } + + // get all existing repo snapshot labels of the user + getAllRepoSnapshotLabels() { + const url = this.server + '/api/v2.1/revision-tags/tag-names/'; + return this.req.get(url); + } + + addNewRepoLabels(repoID, labels) { + const url = this.server + '/api/v2.1/revision-tags/tagged-items/'; + const data = { + 'repo_id': repoID, + 'tag_names': labels + }; + return this.req.post(url, data); + } + + updateRepoCommitLabels(repoID, commitID, labels) { + const url = this.server + '/api/v2.1/revision-tags/tagged-items/'; + const data = { + 'repo_id': repoID, + 'commit_id': commitID, + 'tag_names': labels + }; + return this.req.put(url, data); + } + + invitePeople(emails) { + const url = this.server + '/api/v2.1/invitations/batch/'; + let form = new FormData(); + form.append('type', 'guest'); + for (let i = 0; i < emails.length; i++) { + form.append('accepter', emails[i]); + } + return this._sendPostRequest(url, form); + } + + listInvitations() { + const url = this.server + '/api/v2.1/invitations/'; + return this.req.get(url); + } + + deleteInvitation(token) { + const url = this.server + '/api/v2.1/invitations/' + token + '/'; + return this.req.delete(url); + } + + revokeInvitation(token) { + const url = this.server + '/api/v2.1/invitations/' + token + '/revoke/'; + return this.req.post(url); + } + + addRepoShareInvitations(repoID, path, emails, permission) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/shared/invitations/batch/'; + const data = { + type: 'guest', + accepters: emails, + path: path, + permission: permission, + }; + return this.req.post(url, data); + } + + listRepoShareInvitations(repoID, path) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/shared/invitations/?path=' + path; + return this.req.get(url); + } + + updateRepoShareInvitation(repoID, path, token, permission) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/shared/invitation/'; + let data = {token: token, path: path, permission: permission}; + return this.req.put(url, data); + } + + deleteRepoShareInvitation(repoID, path, token) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/shared/invitation/'; + let params = {token: token, path: path}; + return this.req.delete(url, {data: params}); + } + + updateUserAvatar(avatarFile, avatarSize) { + const url = this.server + '/api/v2.1/user-avatar/'; + let form = new FormData(); + form.append('avatar', avatarFile); + form.append('avatar_size', avatarSize); + return this._sendPostRequest(url, form); + } + + getUserInfo() { + const url = this.server + '/api/v2.1/user/'; + return this.req.get(url); + } + + updateUserInfo({name, telephone, contact_email, list_in_address_book}) { + const url = this.server + '/api/v2.1/user/'; + let data = {}; + if (name != undefined) { + data.name = name; + } + if (telephone != undefined) { + data.telephone = telephone; + } + if (contact_email != undefined) { + data.contact_email = contact_email; + } + if (list_in_address_book != undefined) { + data.list_in_address_book = list_in_address_book; + } + return this.req.put(url, data); + } + + updateEmailNotificationInterval(interval) { + const url = this.server + '/api2/account/info/'; + const data = { + 'email_notification_interval': interval + }; + return this.req.put(url, data); + } + + updateWebdavSecret(password) { + const url = this.server + '/api/v2.1/webdav-secret/'; + const data = { + 'secret': password + }; + return this.req.put(url, data); + } + + listUserFolderPerm(repoID, folderPath) { + let url = this.server + '/api2/repos/' + repoID + '/user-folder-perm/'; + if (folderPath) { + url = url + '?folder_path=' + encodeURIComponent(folderPath); + } + return this.req.get(url); + } + + addUserFolderPerm(repoID, permission, folderPath, userEmail) { + const url = this.server + '/api2/repos/' + repoID + '/user-folder-perm/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + if (Array.isArray(userEmail)) { + userEmail.forEach(item => { + form.append('user_email', item); + }); + } else { + form.append('user_email', userEmail); + } + return this._sendPostRequest(url, form); + } + + updateUserFolderPerm(repoID, permission, folderPath, userEmail) { + const url = this.server + '/api2/repos/' + repoID + '/user-folder-perm/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + form.append('user_email', userEmail); + return this.req.put(url, form); + } + + deleteUserFolderPerm(repoID, permission, folderPath, userEmail) { + const url = this.server + '/api2/repos/' + repoID + '/user-folder-perm/'; + let param = { + permission: permission, + folder_path: folderPath, + user_email: userEmail, + }; + return this.req.delete(url, {data: param}); + } + + listGroupFolderPerm(repoID, folderPath) { + let url = this.server + '/api2/repos/' + repoID + '/group-folder-perm/'; + if (folderPath) { + url = url + '?folder_path=' + encodeURIComponent(folderPath); + } + return this.req.get(url); + } + + addGroupFolderPerm(repoID, permission, folderPath, groupID) { + const url = this.server + '/api2/repos/' + repoID + '/group-folder-perm/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + form.append('group_id', groupID); + return this._sendPostRequest(url, form); + } + + updateGroupFolderPerm(repoID, permission, folderPath, groupID) { + const url = this.server + '/api2/repos/' + repoID + '/group-folder-perm/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + form.append('group_id', groupID); + return this.req.put(url, form); + } + + deleteGroupFolderPerm(repoID, permission, folderPath, groupID) { + const url = this.server + '/api2/repos/' + repoID + '/group-folder-perm/'; + let param = { + permission: permission, + folder_path: folderPath, + group_id: groupID, + }; + return this.req.delete(url, {data: param}); + } + + listDepartmentRepoUserFolderPerm(repoID, folderPath) { + let url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-folder-permission/'; + if (folderPath) { + url = url + '?folder_path=' + encodeURIComponent(folderPath); + } + return this.req.get(url); + } + + addDepartmentRepoUserFolderPerm(repoID, permission, folderPath, userEmail) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-folder-permission/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + if (Array.isArray(userEmail)) { + userEmail.forEach(item => { + form.append('user_email', item); + }); + } else { + form.append('user_email', userEmail); + } + return this._sendPostRequest(url, form); + } + + updateDepartmentRepoUserFolderPerm(repoID, permission, folderPath, userEmail) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-folder-permission/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + form.append('user_email', userEmail); + return this.req.put(url, form); + } + + deleteDepartmentRepoUserFolderPerm(repoID, permission, folderPath, userEmail) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/user-folder-permission/'; + let param = { + permission: permission, + folder_path: folderPath, + user_email: userEmail, + }; + return this.req.delete(url, {data: param}); + } + + listDepartmentRepoGroupFolderPerm(repoID, folderPath) { + let url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-folder-permission/'; + if (folderPath) { + url = url + '?folder_path=' + encodeURIComponent(folderPath); + } + return this.req.get(url); + } + + addDepartmentRepoGroupFolderPerm(repoID, permission, folderPath, groupID) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-folder-permission/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + form.append('group_id', groupID); + return this._sendPostRequest(url, form); + } + + updateDepartmentRepoGroupFolderPerm(repoID, permission, folderPath, groupID) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-folder-permission/'; + let form = new FormData(); + form.append('permission', permission); + form.append('folder_path', folderPath); + form.append('group_id', groupID); + return this.req.put(url, form); + } + + deleteDepartmentRepoGroupFolderPerm(repoID, permission, folderPath, groupID) { + const url = this.server + '/api/v2.1/group-owned-libraries/' + repoID + '/group-folder-permission/'; + let param = { + permission: permission, + folder_path: folderPath, + group_id: groupID, + }; + return this.req.delete(url, {data: param}); + } + + adminListWorkWeixinDepartments(departmentID) { + const url = this.server + '/api/v2.1/admin/work-weixin/departments/'; + const params = {}; + if (departmentID) { + params.department_id = departmentID; + } + return this.req.get(url, {params: params}); + } + + adminListWorkWeixinDepartmentMembers(departmentID, params) { + const url = this.server + '/api/v2.1/admin/work-weixin/departments/' + departmentID + '/members/'; + return this.req.get(url, {params: params}); + } + + adminAddWorkWeixinUsersBatch(userList) { + const url = this.server + '/api/v2.1/admin/work-weixin/users/batch/'; + return this.req.post(url, {userlist: userList}); + } + + adminImportWorkWeixinDepartment(departmentID) { + const url = this.server + '/api/v2.1/admin/work-weixin/departments/import/'; + return this.req.post(url, {work_weixin_department_id: departmentID}); + } + + adminListDingtalkDepartments(departmentID) { + const url = this.server + '/api/v2.1/admin/dingtalk/departments/'; + const params = {}; + if (departmentID) { + params.department_id = departmentID; + } + return this.req.get(url, {params: params}); + } + + adminListDingtalkDepartmentMembers(departmentID) { + const url = this.server + '/api/v2.1/admin/dingtalk/departments/' + departmentID + '/members/'; + return this.req.get(url); + } + + adminAddDingtalkUsersBatch(userList) { + const url = this.server + '/api/v2.1/admin/dingtalk/users/batch/'; + return this.req.post(url, {userlist: userList}); + } + + adminImportDingtalkDepartment(departmentID) { + const url = this.server + '/api/v2.1/admin/dingtalk/departments/import/'; + return this.req.post(url, {department_id: departmentID}); + } + + getRepoHistory(repoID, page, perPage) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/history/'; + const params = { + page: page || 1, + per_page: perPage || 100 + }; + return this.req.get(url, {params: params}); + } + + getCommitDetails(repoID, commitID) { + const url = this.server + '/ajax/repo/' + repoID + '/history/changes/'; + const params = { + commit_id: commitID + }; + return this.req.get(url, { + headers: {'X-Requested-With': 'XMLHttpRequest'}, + params: params + }); + } + + getRepoFolderTrash(repoID, path, scanStat) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/trash/'; + let params = { + path: path + }; + if (scanStat) { + params.scan_stat = scanStat; + } + return this.req.get(url, {params: params}); + } + + deleteRepoTrash(repoID, days) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/trash/'; + const params = { + keep_days: days + }; + return this.req.delete(url, {data: params}); + } + + restoreFolder(repoID, commitID, path) { + const url = this.server + '/api2/repos/' + repoID + '/dir/revert/'; + const data = { + 'commit_id': commitID, + 'p': path + }; + return this.req.put(url, data); + } + + restoreFile(repoID, commitID, path) { + const url = this.server + '/api2/repos/' + repoID + '/file/revert/'; + const data = { + 'commit_id': commitID, + 'p': path + }; + return this.req.put(url, data); + } + + listCommitDir(repoID, commitID, path) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/commits/' + commitID + '/dir/'; + const params = { + path: path + }; + return this.req.get(url, {params: params}); + } + + listRepoRelatedUsers(repoID) { + let url = this.server + '/api/v2.1/repos/' + repoID + '/related-users/'; + return this.req.get(url); + } + + listFileParticipants(repoID, filePath) { + const path = encodeURIComponent(filePath); + let url = this.server + '/api/v2.1/repos/' + repoID + '/file/participants/?path=' + path; + return this.req.get(url); + } + + addFileParticipants(repoID, filePath, emails) { + let url = this.server + '/api/v2.1/repos/' + repoID + '/file/participants/'; + let params = { + path: filePath, + emails: emails + }; + return this.req.post(url, params); + } + + deleteFileParticipant(repoID, filePath, email) { + let url = this.server + '/api/v2.1/repos/' + repoID + '/file/participant/'; + let params = { + path: filePath, + email: email + }; + return this.req.delete(url, { data: params }); + } + + sysAdminUploadLicense(file) { + const url = this.server + '/api/v2.1/admin/license/'; + let formData = new FormData(); + formData.append('license', file); + return this._sendPostRequest(url, formData); + } + + sysAdminGetSysInfo() { + const url = this.server + '/api/v2.1/admin/sysinfo/'; + return this.req.get(url); + } + + sysAdminListDevices(platform, page, per_page) { + const url = this.server + '/api/v2.1/admin/devices/'; + let params = { + platform: platform, + page: page, + per_page: per_page + }; + return this.req.get(url, {params: params}); + } + + sysAdminUnlinkDevice(platform, deviceID, user, wipeDevice) { + const url = this.server + '/api/v2.1/admin/devices/'; + let params = { + platform: platform, + device_id: deviceID, + user: user + }; + if (wipeDevice) { + params.wipe_device = wipeDevice + } + return this.req.delete(url, {data: params}); + } + + sysAdminListDeviceErrors() { + const url = this.server + '/api/v2.1/admin/device-errors/'; + return this.req.get(url); + } + + sysAdminClearDeviceErrors() { + const url = this.server + '/api/v2.1/admin/device-errors/'; + return this.req.delete(url); + } + + sysAdminGetSysSettingInfo() { + const url = this.server + '/api/v2.1/admin/web-settings/'; + return this.req.get(url); + } + + sysAdminSetSysSettingInfo(key, value) { + const url = this.server + '/api/v2.1/admin/web-settings/'; + let formData = new FormData(); + formData.append(key, value); + return this.req.put(url, formData); + } + + sysAdminUpdateLogo(file) { + const url = this.server + '/api/v2.1/admin/logo/'; + let formData = new FormData(); + formData.append('logo', file); + return this._sendPostRequest(url, formData); + } + + sysAdminUpdateFavicon(file) { + const url = this.server + '/api/v2.1/admin/favicon/'; + let formData = new FormData(); + formData.append('favicon', file); + return this._sendPostRequest(url, formData); + } + + sysAdminUpdateLoginBG(file) { + const url = this.server + '/api/v2.1/admin/login-background-image/'; + let formData = new FormData(); + formData.append('login_bg_image', file); + return this._sendPostRequest(url, formData); + } + + sysAdminListAllRepos(page, perPage) { + const url = this.server + '/api/v2.1/admin/libraries/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminSearchRepos(name) { + const url = this.server + '/api/v2.1/admin/search-library/'; + let params = { + query: name || '' + }; + return this.req.get(url, {params: params}); + } + + sysAdminGetSystemRepoInfo() { + const url = this.server + '/api/v2.1/admin/system-library/'; + return this.req.get(url); + } + + sysAdminDeleteRepo(repoID) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/'; + return this.req.delete(url); + } + + sysAdminTransferRepo(repoID, userEmail) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/'; + const params = { + owner: userEmail + }; + return this.req.put(url, params); + } + + sysAdminGetRepoHistorySetting(repoID) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/history-limit/'; + return this.req.get(url); + } + + sysAdminUpdateRepoHistorySetting(repoID, keepDays) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/history-limit/'; + let form = new FormData(); + form.append('keep_days', keepDays); + return this.req.put(url, form); + } + + sysAdminListRepoSharedItems(repoID, shareType) { + const url = this.server + '/api/v2.1/admin/shares/'; + const params = { + repo_id: repoID, + share_type: shareType + }; + return this.req.get(url, {params: params}); + } + + sysAdminUpdateRepoSharedItemPermission(repoID, shareType, shareTo, permission) { + const url = this.server + '/api/v2.1/admin/shares/'; + const params = { + repo_id: repoID, + share_type: shareType, + permission: permission, + share_to: shareTo + }; + return this.req.put(url, params); + } + + sysAdminAddRepoSharedItem(repoID, shareType, shareToList, permission) { + const url = this.server + '/api/v2.1/admin/shares/'; + let form = new FormData(); + form.append('repo_id', repoID); + form.append('share_type', shareType); + form.append('permission', permission); + shareToList.map((shareTo) => { + form.append('share_to', shareTo); + }); + return this._sendPostRequest(url, form); + } + + sysAdminDeleteRepoSharedItem(repoID, shareType, shareTo) { + const url = this.server + '/api/v2.1/admin/shares/'; + const params = { + repo_id: repoID, + share_type: shareType, + share_to: shareTo + }; + return this.req.delete(url, {data: params}); + } + + sysAdminCreateRepo(repoName, owner) { + const url = this.server + '/api/v2.1/admin/libraries/'; + let form = new FormData(); + form.append('name', repoName); + form.append('owner', owner); + return this._sendPostRequest(url, form); + } + + sysAdminListTrashRepos(page, perPage) { + const url = this.server + '/api/v2.1/admin/trash-libraries/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminSearchTrashRepos(owner) { + const url = this.server + '/api/v2.1/admin/trash-libraries/'; + let params = { + owner: owner || '' + }; + return this.req.get(url, {params: params}); + } + + sysAdminDeleteTrashRepo(repoID) { + const url = this.server + '/api/v2.1/admin/trash-libraries/' + repoID + '/'; + return this.req.delete(url); + } + + sysAdminRestoreTrashRepo(repoID) { + const url = this.server + '/api/v2.1/admin/trash-libraries/' + repoID + '/'; + return this.req.put(url); + } + + sysAdminCleanTrashRepos() { + const url = this.server + '/api/v2.1/admin/trash-libraries/'; + return this.req.delete(url); + } + + sysAdminListRepoDirents(repoID, dir) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/dirents/'; + let params = { + parent_dir: dir + }; + return this.req.get(url, {params: params}); + } + + sysAdminDeleteRepoDirent(repoID, path) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/dirent/'; + let params = { + path: path + }; + return this.req.delete(url, {params: params}); + } + + sysAdminGetRepoFileDownloadURL(repoID, path) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/dirent/'; + let params = { + path: path, + dl: 1 + }; + return this.req.get(url, {params: params}); + } + + sydAdminGetSysRepoItemUploadURL(path) { + const url = this.server + '/api/v2.1/admin/system-library/upload-link/?from=web'; + let params = { + path: path + }; + return this.req.get(url, {params: params}); + } + + sysAdminGetSysRepoItemInfo(repoID, path) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/dirent/'; + let params = { + path: path + }; + return this.req.get(url, {params: params}); + } + + sysAdminCreateSysRepoFolder(repoID, path, name) { + const url = this.server + '/api/v2.1/admin/libraries/' + repoID + '/dirents/?parent_dir=' + encodeURIComponent(path); + let form = new FormData(); + form.append('obj_name', name); + return this._sendPostRequest(url, form); + } + + sysAdminListAllGroups(page, perPage) { + const url = this.server + '/api/v2.1/admin/groups/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminSearchGroups(name) { + const url = this.server + '/api/v2.1/admin/search-group/'; + let params = { + query: name + }; + return this.req.get(url, {params: params}); + } + + sysAdminDismissGroupByID(groupID) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/'; + return this.req.delete(url); + } + + sysAdminTransferGroup(receiverEmail, groupID) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/'; + let formData = new FormData(); + formData.append('new_owner', receiverEmail); + return this.req.put(url, formData); + } + + sysAdminCreateNewGroup(groupName, ownerEmail) { + const url = this.server + '/api/v2.1/admin/groups/'; + let formData = new FormData(); + formData.append('group_name', groupName); + formData.append('group_owner', ownerEmail); + return this._sendPostRequest(url, formData); + } + + sysAdminListGroupRepos(groupID) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/libraries/'; + return this.req.get(url); + } + + sysAdminListGroupMembers(groupID) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/members/'; + return this.req.get(url); + } + + sysAdminUnshareRepoFromGroup(groupID, repoID) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/libraries/' + repoID + '/'; + return this.req.delete(url); + } + + sysAdminAddGroupMember(groupID, emails) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/members/'; + let form = new FormData(); + for (let i = 0; i < emails.length; i++) { + form.append('email', emails[i]); + } + return this._sendPostRequest(url, form); + } + + sysAdminDeleteGroupMember(groupID, email) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/members/' + encodeURIComponent(email) + '/'; + return this.req.delete(url); + } + + sysAdminUpdateGroupMemberRole(groupID, email, isAdmin) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/members/' + encodeURIComponent(email) + '/'; + let formData = new FormData(); + formData.append('is_admin', isAdmin); + return this.req.put(url, formData); + } + + sysAdminListAllSysNotifications() { + const url = this.server + '/api/v2.1/admin/sys-notifications/'; + return this.req.get(url); + } + + sysAdminAddSysNotification(msg) { + const url = this.server + '/api/v2.1/admin/sys-notifications/'; + let formData = new FormData(); + formData.append('msg', msg); + return this._sendPostRequest(url, formData); + } + + sysAdminSetSysNotificationToCurrent(nid) { + const url = this.server + '/api/v2.1/admin/sys-notifications/' + nid + '/'; + return this.req.put(url); + } + + sysAdminDeleteSysNotification(nid) { + const url = this.server + '/api/v2.1/admin/sys-notifications/' + nid + '/'; + return this.req.delete(url); + } + + sysAdminListAllDepartments() { + const url = this.server + '/api/v2.1/admin/address-book/groups/'; + return this.req.get(url); + } + + sysAdminAddNewDepartment(parentGroupID, groupName) { + const url = this.server + '/api/v2.1/admin/address-book/groups/'; + let formData = new FormData(); + formData.append('parent_group', parentGroupID); + formData.append('group_name', groupName); + formData.append('group_owner', 'system admin'); + return this._sendPostRequest(url, formData); + } + + sysAdminGetDepartmentInfo(groupID, showAncestors) { + let url = this.server + '/api/v2.1/admin/address-book/groups/' + groupID + '/'; + url += showAncestors ? '?return_ancestors=true' : ''; + return this.req.get(url); + } + + sysAdminUpdateDepartmentQuota(groupID, quota) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/'; + let formData = new FormData(); + formData.append('quota', quota); + return this.req.put(url, formData); + } + + sysAdminDeleteDepartment(groupID) { + const url = this.server + '/api/v2.1/admin/address-book/groups/' + groupID + '/'; + return this.req.delete(url); + } + + sysAdminAddRepoInDepartment(groupID, repoName) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/group-owned-libraries/'; + let formData = new FormData(); + formData.append('repo_name', repoName); + return this._sendPostRequest(url, formData); + } + + sysAdminDeleteRepoInDepartment(groupID, repoID) { + const url = this.server + '/api/v2.1/admin/groups/' + groupID + '/group-owned-libraries/' + repoID + '/'; + return this.req.delete(url); + } + + sysAdminListAllShareLinks(page, perPage) { + const url = this.server + '/api/v2.1/admin/share-links/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminDeleteShareLink(token) { + const url = this.server + '/api/v2.1/admin/share-links/' + token + '/'; + return this.req.delete(url); + } + + sysAdminListAllUploadLinks(page, perPage) { + const url = this.server + '/api/v2.1/admin/upload-links/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminDeleteUploadLink(token) { + const url = this.server + '/api/v2.1/admin/upload-links/' + token + '/'; + return this.req.delete(url); + } + + sysAdminListOrgs(page, perPage) { + const url = this.server + '/api/v2.1/admin/organizations/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminSearchOrgs(name) { + let url = this.server + '/api/v2.1/admin/search-organization/'; + let params = { + query: name + }; + return this.req.get(url, {params: params}); + } + + sysAdminGetOrg(orgID) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/'; + return this.req.get(url); + } + + sysAdminUpdateOrg(orgID, orgInfo) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/'; + let formData = new FormData(); + if (orgInfo.orgName) { + formData.append('org_name', orgInfo.orgName); + } + if (orgInfo.maxUserNumber) { + formData.append('max_user_number', orgInfo.maxUserNumber); + } + if (orgInfo.quota) { + formData.append('quota', orgInfo.quota); + } + if (orgInfo.role) { + formData.append('role', orgInfo.role); + } + return this.req.put(url, formData); + } + + sysAdminAddOrg(orgName, ownerEmail, owner_password) { + const url = this.server + '/api/v2.1/admin/organizations/'; + let formData = new FormData(); + formData.append('org_name', orgName); + formData.append('owner_email', ownerEmail); + formData.append('owner_password', owner_password); + return this._sendPostRequest(url, formData); + } + + sysAdminDeleteOrg(orgID) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/'; + return this.req.delete(url); + } + + sysAdminListOrgUsers(orgID) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/users/'; + return this.req.get(url); + } + + sysAdminAddOrgUser(orgID, email, name, password) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/users/'; + let formData = new FormData(); + formData.append('email', email); + formData.append('name', name); + formData.append('password', password); + return this._sendPostRequest(url, formData); + } + + sysAdminUpdateOrgUser(orgID, email, attribute, value) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/users/' + encodeURIComponent(email) + '/'; + let formData = new FormData(); + switch (attribute) { + case 'active': + formData.append('active', value); + break; + case 'name': + formData.append('name', value); + break; + case 'contact_email': + formData.append('contact_email', value); + break; + case 'quota_total': + formData.append('quota_total', value); + break; + } + return this.req.put(url, formData); + } + + sysAdminDeleteOrgUser(orgID, email) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/users/' + encodeURIComponent(email) + '/'; + return this.req.delete(url); + } + + sysAdminListOrgGroups(orgID) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/groups/'; + return this.req.get(url); + } + + sysAdminListOrgRepos(orgID) { + const url = this.server + '/api/v2.1/admin/organizations/' + orgID + '/repos/'; + return this.req.get(url); + } + + sysAdminListLoginLogs(page, perPage) { + const url = this.server + '/api/v2.1/admin/logs/login-logs/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListFileAccessLogs(page, perPage) { + const url = this.server + '/api/v2.1/admin/logs/file-access-logs/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListFileUpdateLogs(page, perPage) { + const url = this.server + '/api/v2.1/admin/logs/file-update-logs/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListSharePermissionLogs(page, perPage) { + const url = this.server + '/api/v2.1/admin/logs/share-permission-logs/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListAdminLogs(page, perPage) { + const url = this.server + '/api/v2.1/admin/admin-logs/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListAdminLoginLogs(page, perPage) { + const url = this.server + '/api/v2.1/admin/admin-login-logs/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListShareInRepos(receiverEmail) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(receiverEmail) + '/beshared-repos/'; + return this.req.get(url); + } + + sysAdminListShareLinksByUser(email) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/share-links/'; + return this.req.get(url); + } + + sysAdminListUploadLinksByUser(email) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/upload-links/'; + return this.req.get(url); + } + + sysAdminListGroupsJoinedByUser(email) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/groups/'; + return this.req.get(url); + } + + sysAdminSetForceTwoFactorAuth(email, isForce2FA) { + let isForce = isForce2FA ? 1 : 0; + const url = this.server + '/api2/two-factor-auth/' + encodeURIComponent(email) + '/'; + let formData = new FormData(); + formData.append('force_2fa', isForce); + return this.req.put(url, formData); + } + + sysAdminDeleteTwoFactorAuth(email) { + const url = this.server + '/api2/two-factor-auth/' + encodeURIComponent(email) + '/'; + return this.req.delete(url); + } + + sysAdminListUsers(page, perPage, isLDAPImport) { + let url = this.server + '/api/v2.1/admin/users/'; + let params = { + page: page, + per_page: perPage + }; + if (isLDAPImport) { + url += '?source=ldapimport'; + } + return this.req.get(url, {params: params}); + } + + sysAdminSearchUsers(query) { + let url = this.server + '/api/v2.1/admin/search-user/'; + let params = { + query: query + }; + return this.req.get(url, {params: params}); + } + + sysAdminListLDAPUsers(page, perPage) { + const url = this.server + '/api/v2.1/admin/ldap-users/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminAddUser(email, name, role, password) { + const url = this.server + '/api/v2.1/admin/users/'; + let formData = new FormData(); + formData.append('email', email); + formData.append('name', name); + formData.append('password', password); + if (role) { + formData.append('role', role); + } + return this._sendPostRequest(url, formData); + } + + sysAdminUpdateUser(email, attribute, value) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/'; + let formData = new FormData(); + switch (attribute) { + case 'password': + formData.append('password', value); + break; + case 'is_active': + formData.append('is_active', value); + break; + case 'is_staff': + formData.append('is_staff', value); + break; + case 'role': + formData.append('role', value); + break; + case 'name': + formData.append('name', value); + break; + case 'login_id': + formData.append('login_id', value); + break; + case 'contact_email': + formData.append('contact_email', value); + break; + case 'reference_id': + formData.append('reference_id', value); + break; + case 'department': + formData.append('department', value); + break; + case 'quota_total': + formData.append('quota_total', value); + break; + case 'institution': + formData.append('institution', value); + break; + } + return this.req.put(url, formData); + } + + sysAdminDeleteUser(email) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/'; + return this.req.delete(url); + } + + sysAdminGetUser(email, avatarSize) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/'; + let params = {}; + if (avatarSize) { + params.avatar_size = avatarSize; + } + return this.req.get(url, {params: params}); + } + + sysAdminResetUserPassword(email) { + const url = this.server + '/api/v2.1/admin/users/' + encodeURIComponent(email) + '/reset-password/'; + return this.req.put(url); + } + + sysAdminSetUserQuotaInBatch(emails, quotaTotal) { + const url = this.server + '/api/v2.1/admin/users/batch/'; + let formData = new FormData(); + emails.map(email => { + formData.append('email', email); + }); + formData.append('operation', 'set-quota'); + formData.append('quota_total', quotaTotal); + return this._sendPostRequest(url, formData); + } + + sysAdminDeleteUserInBatch(emails) { + const url = this.server + '/api/v2.1/admin/users/batch/'; + let formData = new FormData(); + emails.map(email => { + formData.append('email', email); + }); + formData.append('operation', 'delete-user'); + return this._sendPostRequest(url, formData); + } + + sysAdminImportUserViaFile(file) { + const url = this.server + '/api/v2.1/admin/import-users/'; + let formData = new FormData(); + formData.append('file', file); + return this._sendPostRequest(url, formData); + } + + sysAdminListAdmins() { + const url = this.server + '/api/v2.1/admin/admin-users/'; + return this.req.get(url); + } + + sysAdminUpdateAdminRole(email, role) { + const url = this.server + '/api/v2.1/admin/admin-role/'; + let formData = new FormData(); + formData.append('email', email); + formData.append('role', role); + return this.req.put(url, formData); + } + + sysAdminAddAdminInBatch(emails) { + const url = this.server + '/api/v2.1/admin/admin-users/batch/'; + let formData = new FormData(); + emails.map(email => { + formData.append('email', email); + }); + return this._sendPostRequest(url, formData); + } + + sysAdminListReposByOwner(email) { + const url = this.server + '/api/v2.1/admin/libraries/'; + let params = { + owner: email + }; + return this.req.get(url, {params: params}); + } + + sysAdminListInvitations(page, perPage) { + const url = this.server + '/api/v2.1/admin/invitations/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminDeleteInvitation(token) { + const url = this.server + '/api/v2.1/admin/invitations/' + token + '/'; + return this.req.delete(url); + } + + sysAdminDeleteExpiredInvitations() { + const url = this.server + '/api/v2.1/admin/invitations/?type=expired'; + return this.req.delete(url); + } + + sysAdminListInstitutions(page, perPage) { + const url = this.server + '/api/v2.1/admin/institutions/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminAddInstitution(name) { + const url = this.server + '/api/v2.1/admin/institutions/'; + let formData = new FormData(); + formData.append('name', name); + return this._sendPostRequest(url, formData); + } + + sysAdminDeleteInstitution(institutionID) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/'; + return this.req.delete(url); + } + + sysAdminGetInstitution(institutionID) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/'; + return this.req.get(url); + } + + sysAdminUpdateInstitution(institutionID, quota) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/'; + let formData = new FormData(); + formData.append('quota', quota); + return this.req.put(url, formData); + } + + sysAdminListInstitutionUsers(institutionID, page, perPage) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/users/'; + let params = { + page: page, + per_page: perPage + }; + return this.req.get(url, {params: params}); + } + + sysAdminListInstitutionAdmins(institutionID) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/users/'; + let params = { + is_institution_admin: true, + }; + return this.req.get(url, {params: params}); + } + + sysAdminAddInstitutionUserBatch(institutionID, emailArray) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/users/'; + let formData = new FormData(); + emailArray.map(email => { + formData.append('email', email); + }); + return this.req.post(url, formData); + } + + sysAdminUpdateInstitutionUser(institutionID, email, isInstitutionAdmin) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/users/' + encodeURIComponent(email) + '/'; + let formData = new FormData(); + formData.append('is_institution_admin', isInstitutionAdmin); + return this.req.put(url, formData); + } + + sysAdminDeleteInstitutionUser(institutionID, email) { + const url = this.server + '/api/v2.1/admin/institutions/' + institutionID + '/users/' + encodeURIComponent(email) + '/'; + return this.req.delete(url); + } + + sysAdminListTermsAndConditions() { + const url = this.server + '/api/v2.1/admin/terms-and-conditions/'; + return this.req.get(url); + } + + sysAdminAddTermAndCondition(name, versionNumber, text, isActive) { + const url = this.server + '/api/v2.1/admin/terms-and-conditions/'; + let formData = new FormData(); + formData.append('name', name); + formData.append('version_number', versionNumber); + formData.append('text', text); + formData.append('is_active', isActive); + return this._sendPostRequest(url, formData); + } + + sysAdminUpdateTermAndCondition(termID, name, versionNumber, text, isActive) { + const url = this.server + '/api/v2.1/admin/terms-and-conditions/' + termID + '/'; + let formData = new FormData(); + formData.append('name', name); + formData.append('version_number', versionNumber); + formData.append('text', text); + formData.append('is_active', isActive); + return this.req.put(url, formData); + } + + sysAdminDeleteTermAndCondition(termID) { + const url = this.server + '/api/v2.1/admin/terms-and-conditions/' + termID + '/'; + return this.req.delete(url); + } + + listVirusScanRecords(pageNum) { + const url = this.server + '/api/v2.1/admin/virus-scan-records/?page=' + pageNum; + return this.req.get(url); + } + + deleteVirusScanRecord(virusID) { + const url = this.server + '/api/v2.1/admin/virus-scan-records/' + virusID + '/'; + return this.req.delete(url); + } + + sysAdminStatisticFiles(startTime, endTime, groupBy) { + const url = this.server + '/api/v2.1/admin/statistics/file-operations/'; + let params = { + start: startTime, + end: endTime, + group_by: groupBy + } + return this.req.get(url, {params: params}); + } + + sysAdminStatisticStorages(startTime, endTime, groupBy) { + const url = this.server + '/api/v2.1/admin/statistics/total-storage/'; + let params = { + start: startTime, + end: endTime, + group_by: groupBy + } + return this.req.get(url, {params: params}); + } + + sysAdminStatisticActiveUsers(startTime, endTime, groupBy) { + const url = this.server + '/api/v2.1/admin/statistics/active-users/'; + let params = { + start: startTime, + end: endTime, + group_by: groupBy + } + return this.req.get(url, {params: params}); + } + + sysAdminStatisticTraffic(startTime, endTime, groupBy) { + const url = this.server + '/api/v2.1/admin/statistics/system-traffic/'; + let params = { + start: startTime, + end: endTime, + group_by: groupBy + } + return this.req.get(url, {params: params}); + } + + sysAdminListUserTraffic(month, page, per_page) { + const url = this.server + '/api/v2.1/admin/statistics/system-user-traffic/'; + let params = { + month: month, + page: page, + per_page: per_page + } + return this.req.get(url, {params: params}); + } + + sysAdminListOrgTraffic(month, page, per_page) { + const url = this.server + '/api/v2.1/admin/statistics/system-org-traffic/'; + let params = { + month: month, + page: page, + per_page: per_page + } + return this.req.get(url, {params: params}); + } + +} + +export { SeafileAPI }; diff --git a/frontend/src/settings.js b/frontend/src/settings.js index d22ab337a3..0215241ee9 100644 --- a/frontend/src/settings.js +++ b/frontend/src/settings.js @@ -15,6 +15,7 @@ import ListInAddressBook from './components/user-settings/list-in-address-book'; import EmailNotice from './components/user-settings/email-notice'; import TwoFactorAuthentication from './components/user-settings/two-factor-auth'; import SocialLogin from './components/user-settings/social-login'; +import SocialLoginDingtalk from './components/user-settings/social-login-dingtalk'; import DeleteAccount from './components/user-settings/delete-account'; import './css/toolbar.css'; @@ -22,12 +23,13 @@ import './css/search.css'; import './css/user-settings.css'; -const { +const { canUpdatePassword, passwordOperationText, enableAddressBook, enableWebdavSecret, twoFactorAuthEnabled, enableWechatWork, + enableDingtalk, enableDeleteAccount } = window.app.pageOptions; @@ -44,6 +46,7 @@ class Settings extends React.Component { {show: isPro, href: '#email-notice', text: gettext('Email Notification')}, {show: twoFactorAuthEnabled, href: '#two-factor-auth', text: gettext('Two-Factor Authentication')}, {show: enableWechatWork, href: '#social-auth', text: gettext('Social Login')}, + {show: enableDingtalk, href: '#social-auth', text: gettext('Social Login')}, {show: enableDeleteAccount, href: '#del-account', text: gettext('Delete Account')}, ]; @@ -62,7 +65,7 @@ class Settings extends React.Component { toaster.danger(errMessage); }); } - + updateUserInfo = (data) => { seafileAPI.updateUserInfo(data).then((res) => { this.setState({ @@ -127,12 +130,13 @@ class Settings extends React.Component { } {enableWebdavSecret && } - {enableAddressBook && this.state.userInfo && + {enableAddressBook && this.state.userInfo && } {isPro && } {twoFactorAuthEnabled && } {enableWechatWork && } + {enableDingtalk && } {enableDeleteAccount && } diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 9aa8dabca2..67d0a5a947 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -128,6 +128,7 @@ export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admi export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : ''; export const otherPermission = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.other_permission : ''; export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : ''; +export const enableDingtalk = window.sysadmin ? window.sysadmin.pageOptions.enable_dingtalk : ''; export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : ''; export const haveLDAP = window.sysadmin ? window.sysadmin.pageOptions.haveLDAP : ''; export const enableShareLinkReportAbuse = window.sysadmin ? window.sysadmin.pageOptions.enable_share_link_report_abuse : ''; diff --git a/frontend/src/utils/seafile-api.js b/frontend/src/utils/seafile-api.js index b23da9611f..947a10b219 100644 --- a/frontend/src/utils/seafile-api.js +++ b/frontend/src/utils/seafile-api.js @@ -1,5 +1,5 @@ import cookie from 'react-cookies'; -import { SeafileAPI } from 'seafile-js'; +import { SeafileAPI } from '../seafile-api'; import { siteRoot } from './constants'; let seafileAPI = new SeafileAPI(); diff --git a/seahub/api2/endpoints/admin/dingtalk.py b/seahub/api2/endpoints/admin/dingtalk.py new file mode 100644 index 0000000000..426b95f76f --- /dev/null +++ b/seahub/api2/endpoints/admin/dingtalk.py @@ -0,0 +1,459 @@ +# Copyright (c) 2012-2019 Seafile Ltd. + +# encoding: utf-8 + +import logging +import requests + +from django.core.cache import cache +from django.core.files.base import ContentFile + +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import IsAdminUser +from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework import status + +from seaserv import seafile_api, ccnet_api + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.api2.permissions import IsProVersion + +from seahub.base.accounts import User +from seahub.utils.auth import gen_user_virtual_id +from seahub.auth.models import SocialAuthUser +from seahub.profile.models import Profile +from seahub.avatar.models import Avatar +from seahub.group.utils import validate_group_name + +from seahub.utils import normalize_cache_key +from seahub.dingtalk.settings import ENABLE_DINGTALK, DINGTALK_DEPARTMENT_APP_KEY, \ + DINGTALK_DEPARTMENT_APP_SECRET, DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL, \ + DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL, \ + DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL, \ + DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL, \ + DINGTALK_DEPARTMENT_USER_SIZE + +DEPARTMENT_OWNER = 'system admin' + +logger = logging.getLogger(__name__) + + +def get_dingtalk_access_token(): + + cache_key = normalize_cache_key('DINGTALK_ACCESS_TOKEN') + access_token = cache.get(cache_key, None) + + if not access_token: + + data = { + 'appkey': DINGTALK_DEPARTMENT_APP_KEY, + 'appsecret': DINGTALK_DEPARTMENT_APP_SECRET, + } + resp_json = requests.get(DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL, + params=data).json() + + access_token = resp_json.get('access_token', '') + if not access_token: + logger.error('failed to get dingtalk access_token') + logger.error(data) + logger.error(DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL) + logger.error(resp_json) + return '' + + expires_in = resp_json.get('expires_in', 7200) + cache.set(cache_key, access_token, expires_in) + + return access_token + +def update_dingtalk_user_info(email, name, contact_email, avatar_url): + + # make sure the contact_email is unique + if contact_email and Profile.objects.get_profile_by_contact_email(contact_email): + logger.warning('contact email %s already exists' % contact_email) + contact_email = '' + + profile_kwargs = {} + if name: + profile_kwargs['nickname'] = name + if contact_email: + profile_kwargs['contact_email'] = contact_email + + if profile_kwargs: + try: + Profile.objects.add_or_update(email, **profile_kwargs) + except Exception as e: + logger.error(e) + + try: + image_name = 'dingtalk_avatar' + image_file = requests.get(avatar_url).content + avatar = Avatar.objects.filter(emailuser=email, primary=True).first() + avatar = avatar or Avatar(emailuser=email, primary=True) + avatar_file = ContentFile(image_file) + avatar_file.name = image_name + avatar.avatar = avatar_file + avatar.save() + except Exception as e: + logger.error(e) + +class AdminDingtalkDepartments(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def get(self, request): + + if not ENABLE_DINGTALK: + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + if not request.user.admin_permissions.can_manage_user(): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + access_token = get_dingtalk_access_token() + if not access_token: + error_msg = '获取钉钉组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + data = { + 'access_token': access_token, + } + department_id = request.GET.get('department_id', '') + if department_id: + data['id'] = department_id + current_department_resp_json = requests.get(DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL, params=data).json() + current_department_list = [current_department_resp_json] + sub_department_resp_json = requests.get(DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL, params=data).json() + sub_department_list = sub_department_resp_json.get('department', []) + department_list = current_department_list + sub_department_list + return Response({'department': department_list}) + else: + resp_json = requests.get(DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL, params=data).json() + return Response(resp_json) + + +class AdminDingtalkDepartmentMembers(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser,) + + def get(self, request, department_id): + + if not ENABLE_DINGTALK: + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + if not request.user.admin_permissions.can_manage_user(): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + access_token = get_dingtalk_access_token() + if not access_token: + error_msg = '获取钉钉组织架构成员失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + data = { + 'access_token': access_token, + 'department_id': department_id, + 'offset': 0, + 'size': DINGTALK_DEPARTMENT_USER_SIZE, + } + resp_json = requests.get(DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL, params=data).json() + + user_id_name_dict = {} + auth_users = SocialAuthUser.objects.filter(provider='dingtalk') + for user in auth_users: + user_id_name_dict[user.uid] = user.username + + for user in resp_json['userlist']: + uid = user.get('unionid', '') + user['contact_email'] = user.get('email', '') + user['userid'] = uid + + # # determine the user exists + if uid in user_id_name_dict.keys(): + user['email'] = user_id_name_dict[uid] + else: + user['email'] = '' + + return Response(resp_json) + + +class AdminDingtalkUsersBatch(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAdminUser,) + throttle_classes = (UserRateThrottle,) + + def post(self, request): + + # parameter check + user_list = request.data.get('userlist', []) + if not user_list: + error_msg = 'userlist invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + # permission check + if not ENABLE_DINGTALK: + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + if not request.user.admin_permissions.can_manage_user(): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + user_ids_in_db = [] + auth_users = SocialAuthUser.objects.filter(provider='dingtalk') + for user in auth_users: + user_ids_in_db.append(user.uid) + + success = [] + failed = [] + + for user in user_list: + + user_id = user.get('userid') + + if user_id in user_ids_in_db: + failed.append({ + 'userid': user_id, + 'name': user.get('name'), + 'error_msg': '用户已存在', + }) + continue + + email = gen_user_virtual_id() + try: + User.objects.create_user(email) + SocialAuthUser.objects.add(email, 'dingtalk', user_id) + success.append({ + 'userid': user_id, + 'name': user.get('name'), + 'email': email, + }) + except Exception as e: + logger.error(e) + failed.append({ + 'userid': user_id, + 'name': user.get('name'), + 'error_msg': '导入失败' + }) + + try: + update_dingtalk_user_info(email, user.get('name'), + user.get('contact_email'), user.get('avatar')) + except Exception as e: + logger.error(e) + + return Response({'success': success, 'failed': failed}) + + +class AdminDingtalkDepartmentsImport(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + throttle_classes = (UserRateThrottle,) + permission_classes = (IsAdminUser, IsProVersion) + + def _admin_check_group_name_conflict(self, new_group_name): + checked_groups = ccnet_api.search_groups(new_group_name, -1, -1) + + for g in checked_groups: + if g.group_name == new_group_name: + return True, g + + return False, None + + def _api_department_success_msg(self, department_obj_id, department_obj_name, group_id): + return { + 'type': 'department', + 'department_id': department_obj_id, + 'department_name': department_obj_name, + 'group_id': group_id, + } + + def _api_department_failed_msg(self, department_obj_id, department_obj_name, msg): + return { + 'type': 'department', + 'department_id': department_obj_id, + 'department_name': department_obj_name, + 'msg': msg, + } + + def _api_user_success_msg(self, email, api_user_name, department_obj_id, group_id): + return { + 'type': 'user', + 'email': email, + 'api_user_name': api_user_name, + 'department_id': department_obj_id, + 'group_id': group_id, + } + + def _api_user_failed_msg(self, email, api_user_name, department_obj_id, msg): + return { + 'type': 'user', + 'email': email, + 'api_user_name': api_user_name, + 'department_id': department_obj_id, + 'msg': msg, + } + + def post(self, request): + """import department from dingtalk + """ + + if not ENABLE_DINGTALK: + error_msg = 'Feature is not enabled.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + if not request.user.admin_permissions.can_manage_user(): + return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') + + # argument check + department_id = request.data.get('department_id') + try: + department_id = int(department_id) + except Exception as e: + logger.error(e) + error_msg = 'department_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + access_token = get_dingtalk_access_token() + if not access_token: + error_msg = '获取钉钉组织架构失败' + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # get department list + data = {'access_token': access_token, 'id': department_id} + current_department_resp_json = requests.get(DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL, params=data).json() + current_department_list = [current_department_resp_json] + sub_department_resp_json = requests.get(DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL, params=data).json() + sub_department_list = sub_department_resp_json.get('department', []) + department_list = current_department_list + sub_department_list + + # get department user list + data = { + 'access_token': access_token, + 'department_id': department_id, + 'offset': 0, + 'size': DINGTALK_DEPARTMENT_USER_SIZE, + } + user_resp_json = requests.get(DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL, params=data).json() + api_user_list = user_resp_json.get('userlist', []) + + # main + success = list() + failed = list() + department_map_to_group_dict = dict() + + for index, department_obj in enumerate(department_list): + # check department argument + new_group_name = department_obj.get('name') + department_obj_id = department_obj.get('id') + if department_obj_id is None or not new_group_name or not validate_group_name(new_group_name): + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '部门参数错误') + failed.append(failed_msg) + continue + + # check parent group + if index == 0: + parent_group_id = -1 + else: + parent_department_id = department_obj.get('parentid') + parent_group_id = department_map_to_group_dict.get(parent_department_id) + + if parent_group_id is None: + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '父级部门不存在') + failed.append(failed_msg) + continue + + # check department exist by group name + exist, exist_group = self._admin_check_group_name_conflict(new_group_name) + if exist: + department_map_to_group_dict[department_obj_id] = exist_group.id + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '部门已存在') + failed.append(failed_msg) + continue + + # import department + try: + group_id = ccnet_api.create_group( + new_group_name, DEPARTMENT_OWNER, parent_group_id=parent_group_id) + + seafile_api.set_group_quota(group_id, -2) + + department_map_to_group_dict[department_obj_id] = group_id + success_msg = self._api_department_success_msg( + department_obj_id, new_group_name, group_id) + success.append(success_msg) + except Exception as e: + logger.error(e) + failed_msg = self._api_department_failed_msg( + department_obj_id, new_group_name, '部门导入失败') + failed.append(failed_msg) + + # todo filter ccnet User database + social_auth_queryset = SocialAuthUser.objects.filter(provider='dingtalk') + + # import api_user + for api_user in api_user_list: + uid = api_user.get('unionid', '') + api_user['contact_email'] = api_user['email'] + api_user_name = api_user.get('name') + + # determine the user exists + if social_auth_queryset.filter(uid=uid).exists(): + email = social_auth_queryset.get(uid=uid).username + else: + # create user + email = gen_user_virtual_id() + try: + User.objects.create_user(email) + SocialAuthUser.objects.add(email, 'dingtalk', uid) + except Exception as e: + logger.error(e) + failed_msg = self._api_user_failed_msg( + '', api_user_name, department_id, '导入用户失败') + failed.append(failed_msg) + continue + + # bind user to department + api_user_department_list = api_user.get('department') + for department_obj_id in api_user_department_list: + group_id = department_map_to_group_dict.get(department_obj_id) + if group_id is None: + # the api_user also exist in the brother department which not import + continue + + if ccnet_api.is_group_user(group_id, email, in_structure=False): + failed_msg = self._api_user_failed_msg( + email, api_user_name, department_obj_id, '部门成员已存在') + failed.append(failed_msg) + continue + + try: + ccnet_api.group_add_member(group_id, DEPARTMENT_OWNER, email) + success_msg = self._api_user_success_msg( + email, api_user_name, department_obj_id, group_id) + success.append(success_msg) + except Exception as e: + logger.error(e) + failed_msg = self._api_user_failed_msg( + email, api_user_name, department_id, '导入部门成员失败') + failed.append(failed_msg) + + try: + update_dingtalk_user_info(email, api_user.get('name'), + api_user.get('contact_email'), api_user.get('avatar')) + except Exception as e: + logger.error(e) + + return Response({ + 'success': success, + 'failed': failed, + }) diff --git a/seahub/api2/endpoints/admin/work_weixin.py b/seahub/api2/endpoints/admin/work_weixin.py index 5e4aac15e1..bdb0b9c49c 100644 --- a/seahub/api2/endpoints/admin/work_weixin.py +++ b/seahub/api2/endpoints/admin/work_weixin.py @@ -1,4 +1,5 @@ # Copyright (c) 2012-2019 Seafile Ltd. + # encoding: utf-8 import logging diff --git a/seahub/auth/views.py b/seahub/auth/views.py index dc09a4a832..874b488a4a 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -187,6 +187,7 @@ def login(request, template_name='registration/login.html', getattr(settings, 'ENABLE_KRB5_LOGIN', False) or \ getattr(settings, 'ENABLE_ADFS_LOGIN', False) or \ getattr(settings, 'ENABLE_OAUTH', False) or \ + getattr(settings, 'ENABLE_DINGTALK', False) or \ getattr(settings, 'ENABLE_CAS', False) or \ getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) or \ getattr(settings, 'ENABLE_WORK_WEIXIN', False) diff --git a/seahub/dingtalk/__init__.py b/seahub/dingtalk/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/dingtalk/admin.py b/seahub/dingtalk/admin.py new file mode 100644 index 0000000000..8c38f3f3da --- /dev/null +++ b/seahub/dingtalk/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/seahub/dingtalk/migrations/__init__.py b/seahub/dingtalk/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/seahub/dingtalk/models.py b/seahub/dingtalk/models.py new file mode 100644 index 0000000000..71a8362390 --- /dev/null +++ b/seahub/dingtalk/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/seahub/dingtalk/settings.py b/seahub/dingtalk/settings.py new file mode 100644 index 0000000000..ca3756702b --- /dev/null +++ b/seahub/dingtalk/settings.py @@ -0,0 +1,23 @@ +import seahub.settings as settings + +ENABLE_DINGTALK = getattr(settings, 'ENABLE_DINGTALK', False) + +# for dingtalk qr connect +DINGTALK_QR_CONNECT_LOGIN_REMEMBER_ME = True +DINGTALK_QR_CONNECT_APP_ID = getattr(settings, 'DINGTALK_QR_CONNECT_APP_ID', '') +DINGTALK_QR_CONNECT_APP_SECRET = getattr(settings, 'DINGTALK_QR_CONNECT_APP_SECRET', '') +DINGTALK_QR_CONNECT_AUTHORIZATION_URL = getattr(settings, 'DINGTALK_QR_CONNECT_AUTHORIZATION_URL', 'https://oapi.dingtalk.com/connect/qrconnect') +DINGTALK_QR_CONNECT_USER_INFO_URL = getattr(settings, 'DINGTALK_QR_CONNECT_USER_INFO_URL', 'https://oapi.dingtalk.com/sns/getuserinfo_bycode') +DINGTALK_QR_CONNECT_RESPONSE_TYPE = getattr(settings, 'DINGTALK_QR_CONNECT_RESPONSE_TYPE', 'code') +DINGTALK_QR_CONNECT_SCOPE = getattr(settings, 'DINGTALK_QR_CONNECT_SCOPE', 'snsapi_login') +DINGTALK_QR_CONNECT_CREATE_UNKNOWN_USER = getattr(settings, 'DINGTALK_QR_CONNECT_CREATE_UNKNOWN_USER', True) +DINGTALK_QR_CONNECT_ACTIVATE_USER_AFTER_CREATION = getattr(settings, 'DINGTALK_QR_CONNECT_ACTIVATE_USER_AFTER_CREATION', True) + +# for dingtalk department +DINGTALK_DEPARTMENT_APP_KEY = getattr(settings, 'DINGTALK_DEPARTMENT_APP_KEY', '') +DINGTALK_DEPARTMENT_APP_SECRET = getattr(settings, 'DINGTALK_DEPARTMENT_APP_SECRET', '') +DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL = getattr(settings, 'DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL', 'https://oapi.dingtalk.com/gettoken') +DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL = getattr(settings, 'DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL', 'https://oapi.dingtalk.com/department/list') +DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL = getattr(settings, 'DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL', 'https://oapi.dingtalk.com/department/get') +DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL = getattr(settings, 'DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL', 'https://oapi.dingtalk.com/user/listbypage') +DINGTALK_DEPARTMENT_USER_SIZE = 100 diff --git a/seahub/dingtalk/tests.py b/seahub/dingtalk/tests.py new file mode 100644 index 0000000000..7ce503c2dd --- /dev/null +++ b/seahub/dingtalk/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/seahub/dingtalk/views.py b/seahub/dingtalk/views.py new file mode 100644 index 0000000000..3d20126e66 --- /dev/null +++ b/seahub/dingtalk/views.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- + +import uuid +import json +import time +import hmac +import base64 +import urllib +import logging +import requests +from hashlib import sha256 + +from django.http import HttpResponseRedirect +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ + +from seahub.api2.utils import get_api_token +from seahub import auth +from seahub.profile.models import Profile +from seahub.utils import render_error, get_site_scheme_and_netloc +from seahub.utils.auth import gen_user_virtual_id +from seahub.base.accounts import User +from seahub.auth.models import SocialAuthUser +from seahub.auth.decorators import login_required + +from seahub.dingtalk.settings import ENABLE_DINGTALK, \ + DINGTALK_QR_CONNECT_APP_ID, DINGTALK_QR_CONNECT_APP_SECRET, \ + DINGTALK_QR_CONNECT_AUTHORIZATION_URL, \ + DINGTALK_QR_CONNECT_USER_INFO_URL, DINGTALK_QR_CONNECT_RESPONSE_TYPE, \ + DINGTALK_QR_CONNECT_SCOPE, DINGTALK_QR_CONNECT_LOGIN_REMEMBER_ME + +logger = logging.getLogger(__name__) + +def dingtalk_login(request): + + if not ENABLE_DINGTALK: + return render_error(request, _('Error, please contact administrator.')) + + state = str(uuid.uuid4()) + request.session['dingtalk_login_state'] = state + request.session['dingtalk_login_redirect'] = request.GET.get(auth.REDIRECT_FIELD_NAME, '/') + + data = { + 'appid': DINGTALK_QR_CONNECT_APP_ID, + 'response_type': DINGTALK_QR_CONNECT_RESPONSE_TYPE, + 'scope': DINGTALK_QR_CONNECT_SCOPE, + 'redirect_uri': get_site_scheme_and_netloc() + reverse('dingtalk_callback'), + 'state': state, + } + url = DINGTALK_QR_CONNECT_AUTHORIZATION_URL + '?' + urllib.parse.urlencode(data) + return HttpResponseRedirect(url) + +def dingtalk_callback(request): + + if not ENABLE_DINGTALK: + return render_error(request, _('Error, please contact administrator.')) + + state = request.GET.get('state', '') + if not state or state != request.session.get('dingtalk_login_state', ''): + logger.error('invalid state') + return render_error(request, _('Error, please contact administrator.')) + + timestamp = str(int(time.time()*1000)).encode('utf-8') + appsecret = DINGTALK_QR_CONNECT_APP_SECRET.encode('utf-8') + signature = base64.b64encode(hmac.new(appsecret, timestamp, digestmod=sha256).digest()) + parameters = { + 'accessKey': DINGTALK_QR_CONNECT_APP_ID, + 'timestamp': timestamp, + 'signature': signature, + } + + code = request.GET.get('code') + data = {"tmp_auth_code": code} + + full_user_info_url = DINGTALK_QR_CONNECT_USER_INFO_URL + '?' + urllib.parse.urlencode(parameters) + user_info_resp = requests.post(full_user_info_url, data=json.dumps(data)) + user_info = user_info_resp.json()['user_info'] + + # seahub authenticate user + if 'unionid' not in user_info: + logger.error('Required user info not found.') + logger.error(user_info) + return render_error(request, _('Error, please contact administrator.')) + + auth_user = SocialAuthUser.objects.get_by_provider_and_uid('dingtalk', user_info['unionid']) + if auth_user: + email = auth_user.username + else: + email = gen_user_virtual_id() + SocialAuthUser.objects.add(email, 'dingtalk', user_info['unionid']) + + try: + user = auth.authenticate(remote_user=email) + except User.DoesNotExist: + user = None + + if not user or not user.is_active: + return render_error(request, _('User %s not found or inactive.') % email) + + # User is valid. Set request.user and persist user in the session + # by logging the user in. + request.user = user + request.session['remember_me'] = DINGTALK_QR_CONNECT_LOGIN_REMEMBER_ME + auth.login(request, user) + + # update user's profile + name = user_info['nick'] if 'nick' in user_info else '' + if name: + + profile = Profile.objects.get_profile_by_user(email) + if not profile: + profile = Profile(user=email) + + profile.nickname = name.strip() + profile.save() + + # generate auth token for Seafile client + api_token = get_api_token(request) + + # redirect user to home page + response = HttpResponseRedirect(request.session['dingtalk_login_redirect']) + response.set_cookie('seahub_auth', email + '@' + api_token.key) + return response + +@login_required +def dingtalk_connect(request): + + if not ENABLE_DINGTALK: + return render_error(request, _('Error, please contact administrator.')) + + state = str(uuid.uuid4()) + request.session['dingtalk_connect_state'] = state + request.session['dingtalk_connect_redirect'] = request.GET.get(auth.REDIRECT_FIELD_NAME, '/') + + data = { + 'appid': DINGTALK_QR_CONNECT_APP_ID, + 'response_type': DINGTALK_QR_CONNECT_RESPONSE_TYPE, + 'scope': DINGTALK_QR_CONNECT_SCOPE, + 'redirect_uri': get_site_scheme_and_netloc() + reverse('dingtalk_connect_callback'), + 'state': state, + } + url = DINGTALK_QR_CONNECT_AUTHORIZATION_URL + '?' + urllib.parse.urlencode(data) + return HttpResponseRedirect(url) + +@login_required +def dingtalk_connect_callback(request): + + if not ENABLE_DINGTALK: + return render_error(request, _('Error, please contact administrator.')) + + state = request.GET.get('state', '') + if not state or state != request.session.get('dingtalk_connect_state', ''): + logger.error('invalid state') + return render_error(request, _('Error, please contact administrator.')) + + timestamp = str(int(time.time()*1000)).encode('utf-8') + appsecret = DINGTALK_QR_CONNECT_APP_SECRET.encode('utf-8') + signature = base64.b64encode(hmac.new(appsecret, timestamp, digestmod=sha256).digest()) + parameters = { + 'accessKey': DINGTALK_QR_CONNECT_APP_ID, + 'timestamp': timestamp, + 'signature': signature, + } + + code = request.GET.get('code') + data = {"tmp_auth_code": code} + + full_user_info_url = DINGTALK_QR_CONNECT_USER_INFO_URL + '?' + urllib.parse.urlencode(parameters) + user_info_resp = requests.post(full_user_info_url, data=json.dumps(data)) + user_info = user_info_resp.json()['user_info'] + + # seahub authenticate user + if 'unionid' not in user_info: + logger.error('Required user info not found.') + logger.error(user_info) + return render_error(request, _('Error, please contact administrator.')) + + username = request.user.username + dingtalk_user_id = user_info['unionid'] + + auth_user = SocialAuthUser.objects.get_by_provider_and_uid('dingtalk', + dingtalk_user_id) + if auth_user: + logger.error('dingtalk account already exists %s' % dingtalk_user_id) + return render_error(request, '出错了,此钉钉账号已被绑定') + + SocialAuthUser.objects.add(username, 'dingtalk', dingtalk_user_id) + + # update user's profile + name = user_info['nick'] if 'nick' in user_info else '' + if name: + + profile = Profile.objects.get_profile_by_user(username) + if not profile: + profile = Profile(user=username) + + profile.nickname = name.strip() + profile.save() + + response = HttpResponseRedirect(request.session['dingtalk_connect_redirect']) + return response + +@login_required +def dingtalk_disconnect(request): + + if not ENABLE_DINGTALK: + return render_error(request, _('Error, please contact administrator.')) + + username = request.user.username + SocialAuthUser.objects.delete_by_username_and_provider(username, 'dingtalk') + response = HttpResponseRedirect(request.GET.get(auth.REDIRECT_FIELD_NAME, '/')) + return response diff --git a/seahub/oauth/backends.py b/seahub/oauth/backends.py index 4f826dc0d0..6da8dca8a7 100644 --- a/seahub/oauth/backends.py +++ b/seahub/oauth/backends.py @@ -5,6 +5,7 @@ from seahub.base.accounts import User from registration.models import (notify_admins_on_activate_request, notify_admins_on_register_complete) from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN +from seahub.dingtalk.settings import ENABLE_DINGTALK class OauthRemoteUserBackend(RemoteUserBackend): """ @@ -27,6 +28,12 @@ class OauthRemoteUserBackend(RemoteUserBackend): create_unknown_user = getattr(settings, 'WORK_WEIXIN_OAUTH_CREATE_UNKNOWN_USER', True) activate_after_creation = getattr(settings, 'WORK_WEIXIN_OAUTH_ACTIVATE_USER_AFTER_CREATION', True) + if ENABLE_DINGTALK: + from seahub.dingtalk.settings import DINGTALK_QR_CONNECT_CREATE_UNKNOWN_USER, \ + DINGTALK_QR_CONNECT_ACTIVATE_USER_AFTER_CREATION + create_unknown_user = DINGTALK_QR_CONNECT_CREATE_UNKNOWN_USER + activate_after_creation = DINGTALK_QR_CONNECT_ACTIVATE_USER_AFTER_CREATION + def get_user(self, username): try: user = User.objects.get(email=username) diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html index 0a4df14fed..52c2a504ed 100644 --- a/seahub/profile/templates/profile/set_profile_react.html +++ b/seahub/profile/templates/profile/set_profile_react.html @@ -63,6 +63,12 @@ window.app.pageOptions = { socialNextPage: "{{ social_next_page|escapejs }}", {% endif %} + enableDingtalk: {% if enable_dingtalk %} true {% else %} false {% endif %}, + {% if enable_dingtalk %} + socialConnectedDingtalk: {% if social_connected_dingtalk %} true {% else %} false {% endif %}, + socialNextPage: "{{ social_next_page|escapejs }}", + {% endif %} + enableDeleteAccount: {% if ENABLE_DELETE_ACCOUNT %} true {% else %} false {% endif %} }; diff --git a/seahub/profile/views.py b/seahub/profile/views.py index 0830c12c41..56c03f7811 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -24,6 +24,7 @@ from seahub.utils.two_factor_auth import has_two_factor_auth from seahub.views import get_owned_repo_list from seahub.work_weixin.utils import work_weixin_oauth_check from seahub.settings import ENABLE_DELETE_ACCOUNT, ENABLE_UPDATE_USER_INFO +from seahub.dingtalk.settings import ENABLE_DINGTALK @login_required def edit_profile(request): @@ -97,6 +98,15 @@ def edit_profile(request): enable_wechat_work = False social_connected = False + if ENABLE_DINGTALK: + enable_dingtalk = True + from seahub.auth.models import SocialAuthUser + social_connected_dingtalk = SocialAuthUser.objects.filter( + username=request.user.username, provider='dingtalk').count() > 0 + else: + enable_dingtalk = False + social_connected_dingtalk = False + resp_dict = { 'form': form, 'server_crypto': server_crypto, @@ -113,9 +123,11 @@ def edit_profile(request): 'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO, 'webdav_passwd': webdav_passwd, 'email_notification_interval': email_inverval, - 'social_connected': social_connected, 'social_next_page': reverse('edit_profile'), 'enable_wechat_work': enable_wechat_work, + 'social_connected': social_connected, + 'enable_dingtalk': enable_dingtalk, + 'social_connected_dingtalk': social_connected_dingtalk, 'ENABLE_USER_SET_CONTACT_EMAIL': settings.ENABLE_USER_SET_CONTACT_EMAIL, 'user_unusable_password': request.user.enc_password == UNUSABLE_PASSWORD, } diff --git a/seahub/settings.py b/seahub/settings.py index b785ce21d3..5a9ab5958f 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -279,6 +279,9 @@ ENABLE_WATERMARK = False # enable work weixin ENABLE_WORK_WEIXIN = False +# enable dingtalk +ENABLE_DINGTALK = False + # allow user to clean library trash ENABLE_USER_CLEAN_TRASH = True @@ -852,7 +855,7 @@ if ENABLE_REMOTE_USER_AUTHENTICATION: MIDDLEWARE_CLASSES += ('seahub.auth.middleware.SeafileRemoteUserMiddleware',) AUTHENTICATION_BACKENDS += ('seahub.auth.backends.SeafileRemoteUserBackend',) -if ENABLE_OAUTH or ENABLE_WORK_WEIXIN: +if ENABLE_OAUTH or ENABLE_WORK_WEIXIN or ENABLE_DINGTALK: AUTHENTICATION_BACKENDS += ('seahub.oauth.backends.OauthRemoteUserBackend',) ##################### diff --git a/seahub/templates/sysadmin/sysadmin_react_app.html b/seahub/templates/sysadmin/sysadmin_react_app.html index 1be62e2a29..bd11b3beb0 100644 --- a/seahub/templates/sysadmin/sysadmin_react_app.html +++ b/seahub/templates/sysadmin/sysadmin_react_app.html @@ -23,6 +23,7 @@ is_default_admin: {% if is_default_admin %} true {% else %} false {% endif %}, enable_file_scan: {% if enable_file_scan %} true {% else %} false {% endif %}, enable_work_weixin: {% if enable_work_weixin %} true {% else %} false {% endif %}, + enable_dingtalk: {% if enable_dingtalk %} true {% else %} false {% endif %}, enableSysAdminViewRepo: {% if enable_sys_admin_view_repo %} true {% else %} false {% endif %}, trashReposExpireDays: {{ trash_repos_expire_days }}, send_email_on_adding_system_member: {% if send_email_on_adding_system_member %} true {% else %} false {% endif %}, diff --git a/seahub/urls.py b/seahub/urls.py index 7e0ec0eb56..d255c45405 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -16,6 +16,10 @@ from seahub.views.file import view_history_file, view_trash_file,\ view_media_file_via_public_wiki from seahub.views.repo import repo_history_view, repo_snapshot, view_shared_dir, \ view_shared_upload_link, view_lib_as_wiki + +from seahub.dingtalk.views import dingtalk_login, dingtalk_callback, \ + dingtalk_connect, dingtalk_connect_callback, dingtalk_disconnect + from seahub.api2.endpoints.smart_link import SmartLink, SmartLinkToken from seahub.api2.endpoints.groups import Groups, Group from seahub.api2.endpoints.all_groups import AllGroups @@ -163,6 +167,9 @@ from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAc from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \ AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport +from seahub.api2.endpoints.admin.dingtalk import AdminDingtalkDepartments, \ + AdminDingtalkDepartmentMembers, AdminDingtalkUsersBatch, \ + AdminDingtalkDepartmentsImport from seahub.api2.endpoints.admin.virus_scan_records import AdminVirusScanRecords, AdminVirusScanRecord from seahub.api2.endpoints.file_participants import FileParticipantsView, FileParticipantView from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView @@ -211,6 +218,14 @@ urlpatterns = [ url(r'^u/d/(?P[a-f0-9]+)/$', view_shared_upload_link, name='view_shared_upload_link'), url(r'^view-image-via-share-link/$', view_media_file_via_share_link, name='view_media_file_via_share_link'), + + # dingtalk + url(r'dingtalk/login/$', dingtalk_login, name='dingtalk_login'), + url(r'dingtalk/callback/$', dingtalk_callback, name='dingtalk_callback'), + url(r'dingtalk/connect/$', dingtalk_connect, name='dingtalk_connect'), + url(r'dingtalk/connect-callback/$', dingtalk_connect_callback, name='dingtalk_connect_callback'), + url(r'dingtalk/disconnect/$', dingtalk_disconnect, name='dingtalk_disconnect'), + ### Misc ### url(r'^image-view/(?P.*)$', image_view, name='image_view'), url(r'^custom-css/$', custom_css_view, name='custom_css'), @@ -615,6 +630,12 @@ urlpatterns = [ url(r'^api/v2.1/admin/work-weixin/users/batch/$', AdminWorkWeixinUsersBatch.as_view(), name='api-v2.1-admin-work-weixin-users'), url(r'^api/v2.1/admin/work-weixin/departments/import/$', AdminWorkWeixinDepartmentsImport.as_view(), name='api-v2.1-admin-work-weixin-department-import'), + ## admin:dingtalk departments + url(r'^api/v2.1/admin/dingtalk/departments/$', AdminDingtalkDepartments.as_view(), name='api-v2.1-admin-dingtalk-departments'), + url(r'^api/v2.1/admin/dingtalk/departments/(?P\d+)/members/$', AdminDingtalkDepartmentMembers.as_view(), name='api-v2.1-admin-dingtalk-department-members'), + url(r'^api/v2.1/admin/dingtalk/users/batch/$', AdminDingtalkUsersBatch.as_view(), name='api-v2.1-admin-dingtalk-users-batch'), + url(r'^api/v2.1/admin/dingtalk/departments/import/$', AdminDingtalkDepartmentsImport.as_view(), name='api-v2.1-admin-dingtalk-department-import'), + ### system admin ### url(r'^sys/seafadmin/delete/(?P[-0-9a-f]{36})/$', sys_repo_delete, name='sys_repo_delete'), url(r'^sys/useradmin/export-excel/$', sys_useradmin_export_excel, name='sys_useradmin_export_excel'), @@ -687,6 +708,8 @@ urlpatterns = [ url(r'^sys/upload-links/$', sysadmin_react_fake_view, name="sys_upload_links"), url(r'^sys/work-weixin/$', sysadmin_react_fake_view, name="sys_work_weixin"), url(r'^sys/work-weixin/departments/$', sysadmin_react_fake_view, name="sys_work_weixin_departments"), + url(r'^sys/dingtalk/$', sysadmin_react_fake_view, name="sys_dingtalk"), + url(r'^sys/dingtalk/departments/$', sysadmin_react_fake_view, name="sys_dingtalk_departments"), url(r'^sys/invitations/$', sysadmin_react_fake_view, name="sys_invitations"), url(r'^sys/abuse-reports/$', sysadmin_react_fake_view, name="sys_abuse_reports"), diff --git a/seahub/views/sso.py b/seahub/views/sso.py index 9b9c874214..d4a671f5d2 100644 --- a/seahub/views/sso.py +++ b/seahub/views/sso.py @@ -32,6 +32,9 @@ def sso(request): if getattr(settings, 'ENABLE_OAUTH', False): return HttpResponseRedirect(reverse('oauth_login') + next_param) + if getattr(settings, 'ENABLE_DINGTALK', False): + return HttpResponseRedirect(reverse('dingtalk_login') + next_param) + if getattr(settings, 'ENABLE_CAS', False): return HttpResponseRedirect(reverse('cas_ng_login') + next_param) diff --git a/seahub/views/sysadmin.py b/seahub/views/sysadmin.py index 6f6a1e5f2c..a9a02fd61f 100644 --- a/seahub/views/sysadmin.py +++ b/seahub/views/sysadmin.py @@ -100,6 +100,7 @@ try: except ImportError: ENABLE_FILE_SCAN = False from seahub.work_weixin.settings import ENABLE_WORK_WEIXIN +from seahub.dingtalk.settings import ENABLE_DINGTALK logger = logging.getLogger(__name__) @@ -130,6 +131,7 @@ def sysadmin_react_fake_view(request, **kwargs): 'enable_terms_and_conditions': config.ENABLE_TERMS_AND_CONDITIONS, 'enable_file_scan': ENABLE_FILE_SCAN, 'enable_work_weixin': ENABLE_WORK_WEIXIN, + 'enable_dingtalk': ENABLE_DINGTALK, 'enable_sys_admin_view_repo': ENABLE_SYS_ADMIN_VIEW_REPO, 'trash_repos_expire_days': expire_days if expire_days > 0 else 30, 'available_roles': get_available_roles(),