diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 46c0b0c46a..ae8ae36c04 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -3762,11 +3762,11 @@ } }, "@seafile/seafile-editor": { - "version": "0.3.63", - "resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.3.63.tgz", - "integrity": "sha512-mzqdQlyfs6MpN4n263GtKI6nlYwbVAVO+fj+Q28CiZH8uMwMsa6e9KcQn0yyJWAvxEy6i/L1kWpgztcZFV42Ow==", + "version": "0.3.72", + "resolved": "https://registry.npmjs.org/@seafile/seafile-editor/-/seafile-editor-0.3.72.tgz", + "integrity": "sha512-fV7Arc6aIAGKCxbQ+QNhLiieafJefQsCnaXrGlyjWzLF46T8ivRMPZxfik6YZqQLt165JtJ1SET6dEgK7QBlYA==", "requires": { - "@seafile/slate-react": "^0.54.12", + "@seafile/slate-react": "^0.54.13", "codemirror": "^5.37.0", "crypto-js": "^3.1.9-1", "deep-equal": "^1.0.1", @@ -3856,9 +3856,9 @@ } }, "@seafile/slate-react": { - "version": "0.54.12", - "resolved": "https://registry.npmjs.org/@seafile/slate-react/-/slate-react-0.54.12.tgz", - "integrity": "sha512-AYV7u8zq/ztnJuMY5Vic9uaPeAJUVGXt9zkTU05Jh/j9eEr4rzuGUbJIoRVHbTFckAOn+spvUNUpCYME5OF+gg==", + "version": "0.54.13", + "resolved": "https://registry.npmjs.org/@seafile/slate-react/-/slate-react-0.54.13.tgz", + "integrity": "sha512-NjaY2EXwgMMJFJp2523w2+hK2S+dAWbHCMXadhuuM0S7ZzrerO86XA2N4ZBsaTwQqzJiEJBjg6+I/szD0HaNEA==", "requires": { "@types/debounce": "^1.2.0", "@types/debug": "^4.1.5", @@ -4270,9 +4270,9 @@ "integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==" }, "@types/debug": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", - "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.6.tgz", + "integrity": "sha512-7fDOJFA/x8B+sO1901BmHlf5dE1cxBU8mRXj8QOEDnn16hhGJv/IHxJtZhvsabZsIMn0eLIyeOKAeqSNJJYTpA==" }, "@types/eslint": { "version": "7.2.8", @@ -4321,9 +4321,9 @@ "dev": true }, "@types/is-hotkey": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.2.tgz", - "integrity": "sha512-SUw9LpI3AIwbRNXS7FYy9AlXrTPIdBZGI7y4XxfIEYqgSW1UfFCUM9cMwHE/yCfTl0qeI0UQ/q8TdIxsIFgKPg==" + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.3.tgz", + "integrity": "sha512-Hz+eHHpMWLBX1CpDXSuQre9nYXN2e2VGVHvkkldxDzo9eFtRpHm5iOlJlZvnNGvele5584cUSkRnFRQb+Wcu0w==" }, "@types/istanbul-lib-coverage": { "version": "2.0.3", @@ -4372,9 +4372,9 @@ "dev": true }, "@types/lodash": { - "version": "4.14.168", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.168.tgz", - "integrity": "sha512-oVfRvqHV/V6D1yifJbVRU3TMp8OT6o6BG+U9MkwuJ3U8/CsDHvalRpsxBqivn71ztOFZBTfJMvETbqHiaNSj7Q==" + "version": "4.14.171", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.171.tgz", + "integrity": "sha512-7eQ2xYLLI/LsicL2nejW9Wyko3lcpN6O/z0ZLHrEQsg280zIdCv1t/0m6UtBjUHokCGBQ3gYTbHzDkZ1xOBwwg==" }, "@types/minimatch": { "version": "3.0.4", @@ -6956,9 +6956,9 @@ "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==" }, "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.0.0.tgz", + "integrity": "sha512-Xvf85aAtu6v22+E5hfVoLHqyul/jyxh91zvqk/ioJTQuJR7Z78n7H558vMPKanPSRgIEeZemT92I2g9Y8LPbSQ==" }, "common-tags": { "version": "1.8.0", @@ -7433,7 +7433,7 @@ }, "css-in-js-utils": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", + "resolved": "http://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz", "integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==", "requires": { "hyphenate-style-name": "^1.0.2", @@ -13957,13 +13957,14 @@ } }, "mathjax-full": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.1.2.tgz", - "integrity": "sha512-jFCwRFdFwIOa8J7r6VZT0AIv9ZwbLQ9aPc9YZp695NTvv7XKU2NunJodA+zDWzElIFJ7mTsImyfe5R3QyRNZjw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.0.tgz", + "integrity": "sha512-D2EBNvUG+mJyhn+M1C858k0f2Fc4KxXvbEX2WCMXroV10212JwfYqaBJ336ECBSz5X9L5LRoamxb7AJtg3KaJA==", "requires": { "esm": "^3.2.25", + "mhchemparser": "^4.1.0", "mj-context-menu": "^0.6.1", - "speech-rule-engine": "^3.1.1" + "speech-rule-engine": "^3.3.3" } }, "md5.js": { @@ -14077,6 +14078,11 @@ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", "dev": true }, + "mhchemparser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz", + "integrity": "sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA==" + }, "microevent.ts": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/microevent.ts/-/microevent.ts-0.1.1.tgz", @@ -19187,11 +19193,11 @@ } }, "speech-rule-engine": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.1.1.tgz", - "integrity": "sha512-FGX8B44yI3yGhmcw8nZ/by2ffUlZG6m5b/O3RULXsSiwhL/evL+jwQ6BXQxV3gGtOYptOFalTVCAFknAJgBKAg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-3.3.3.tgz", + "integrity": "sha512-0exWw+0XauLjat+f/aFeo5T8SiDsO1JtwpY3qgJE4cWt+yL/Stl0WP4VNDWdh7lzGkubUD9lWP4J1ASnORXfyQ==", "requires": { - "commander": "^6.0.0", + "commander": ">=7.0.0", "wicked-good-xpath": "^1.3.0", "xmldom-sre": "^0.1.31" } diff --git a/frontend/package.json b/frontend/package.json index 8bbbc4f0b9..fe5ace2e84 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,7 @@ "@seafile/react-image-lightbox": "0.0.1", "@seafile/resumablejs": "1.1.16", "@seafile/seafile-calendar": "0.0.12", - "@seafile/seafile-editor": "^0.3.63", + "@seafile/seafile-editor": "^0.3.72", "MD5": "^1.3.0", "chart.js": "2.9.4", "classnames": "^2.2.6", @@ -35,7 +35,7 @@ "react-responsive": "^6.1.2", "react-select": "^2.4.1", "reactstrap": "^6.4.0", - "seafile-js": "0.2.173", + "seafile-js": "0.2.175", "socket.io-client": "^2.2.0", "unified": "^7.0.0", "url-parse": "^1.4.3", diff --git a/frontend/src/components/dialog/zip-download-dialog.js b/frontend/src/components/dialog/zip-download-dialog.js index a4791d7377..153646e82b 100644 --- a/frontend/src/components/dialog/zip-download-dialog.js +++ b/frontend/src/components/dialog/zip-download-dialog.js @@ -61,13 +61,21 @@ class ZipDownloadDialog extends React.Component { const zipToken = this.state.zipToken; seafileAPI.queryZipProgress(zipToken).then((res) => { const data = res.data; - this.setState({ - zipProgress: data.total == 0 ? '100%' : (data.zipped/data.total*100).toFixed(2) + '%' - }); - if (data['total'] == data['zipped']) { + if (data.failed == 1) { clearInterval(interval); - this.props.toggleDialog(); - location.href = `${fileServerRoot}zip/${zipToken}`; + this.setState({ + isLoading: false, + errorMsg: data.failed_reason + }); + } else { + this.setState({ + zipProgress: data.total == 0 ? '100%' : (data.zipped/data.total*100).toFixed(2) + '%' + }); + if (data['total'] == data['zipped']) { + clearInterval(interval); + this.props.toggleDialog(); + location.href = `${fileServerRoot}zip/${zipToken}`; + } } }).catch((error) => { clearInterval(interval); diff --git a/frontend/src/components/file-view/file-toolbar.js b/frontend/src/components/file-view/file-toolbar.js index fd17d708f1..215369f516 100644 --- a/frontend/src/components/file-view/file-toolbar.js +++ b/frontend/src/components/file-view/file-toolbar.js @@ -107,7 +107,7 @@ class FileToolbar extends React.Component { {(canEditFile && !err) && ( this.props.isSaving ? - : ( this.props.needSave ? @@ -149,7 +149,7 @@ class FileToolbar extends React.Component { /> )} - + @@ -172,8 +172,8 @@ class FileToolbar extends React.Component { {(canEditFile && !err) && - ( this.props.isSaving ? - : ( this.props.needSave ? @@ -192,7 +192,7 @@ class FileToolbar extends React.Component { )} - + diff --git a/frontend/src/components/icon-button.js b/frontend/src/components/icon-button.js index 0c0b491c63..c046aacf34 100644 --- a/frontend/src/components/icon-button.js +++ b/frontend/src/components/icon-button.js @@ -47,6 +47,7 @@ class IconButton extends React.Component { className={className} tag="a" href={this.props.href} + aria-label={this.props.text} > {btnContent} @@ -57,6 +58,7 @@ class IconButton extends React.Component { id={this.props.id} className={className} onClick={this.props.onClick} + aria-label={this.props.text} > {btnContent} diff --git a/frontend/src/components/select-editor/select-editor.js b/frontend/src/components/select-editor/select-editor.js index 84674d34d2..d1dc41ea56 100644 --- a/frontend/src/components/select-editor/select-editor.js +++ b/frontend/src/components/select-editor/select-editor.js @@ -95,6 +95,7 @@ class SelectEditor extends React.Component { className="permission-editor-select" classNamePrefix="permission-editor" placeholder={this.props.translateOption(currentOption)} + value={currentOption} onChange={this.onOptionChanged} captureMenuScroll={false} /> diff --git a/frontend/src/components/select-editor/sysadmin-user-membership-editor.js b/frontend/src/components/select-editor/sysadmin-user-membership-editor.js new file mode 100644 index 0000000000..7a858129dc --- /dev/null +++ b/frontend/src/components/select-editor/sysadmin-user-membership-editor.js @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { gettext } from '../../utils/constants'; +import SelectEditor from './select-editor'; + +const propTypes = { + isTextMode: PropTypes.bool.isRequired, + isEditIconShow: PropTypes.bool.isRequired, + statusOptions: PropTypes.array.isRequired, + currentStatus: PropTypes.string.isRequired, + onStatusChanged: PropTypes.func.isRequired +}; + +class SysAdminUserMembershipEditor extends React.Component { + + translateStatus = (status) => { + switch (status) { + case 'is_org_staff': + return gettext('Admin'); + case 'not_is_org_staff': + return gettext('Member'); + } + } + + render() { + return ( + + ); + } +} + +SysAdminUserMembershipEditor.propTypes = propTypes; + +export default SysAdminUserMembershipEditor; diff --git a/frontend/src/components/toolbar/markdown-viewer-toolbar.js b/frontend/src/components/toolbar/markdown-viewer-toolbar.js index c97d4eacf4..ac4a17f6a8 100644 --- a/frontend/src/components/toolbar/markdown-viewer-toolbar.js +++ b/frontend/src/components/toolbar/markdown-viewer-toolbar.js @@ -72,7 +72,7 @@ class MoreMenu extends React.PureComponent { const isSmall = this.props.isSmallScreen; return ( - + {gettext('More')} @@ -179,11 +179,11 @@ class MarkdownViewerToolbar extends React.Component { } - { saving ? - : - } {canDownloadFile && ( @@ -224,7 +224,7 @@ class MarkdownViewerToolbar extends React.Component {
{saving ? - : + + diff --git a/frontend/src/pages/org-admin/main-panel-topbar.js b/frontend/src/pages/org-admin/main-panel-topbar.js index 2c0acc636f..792d9e1d05 100644 --- a/frontend/src/pages/org-admin/main-panel-topbar.js +++ b/frontend/src/pages/org-admin/main-panel-topbar.js @@ -18,6 +18,7 @@ class MainPanelTopbar extends Component {
+ {this.props.search && this.props.search}
diff --git a/frontend/src/pages/org-admin/org-groups-search-groups.js b/frontend/src/pages/org-admin/org-groups-search-groups.js new file mode 100644 index 0000000000..a7e6cd9b7c --- /dev/null +++ b/frontend/src/pages/org-admin/org-groups-search-groups.js @@ -0,0 +1,279 @@ +import React, { Component, Fragment } from 'react'; +import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { Button, Form, FormGroup, Input, Col } from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, orgID, siteRoot } from '../../utils/constants'; +import toaster from '../../components/toast'; +import OrgGroupInfo from '../../models/org-group'; + +class GroupItem extends React.Component { + + constructor(props) { + super(props); + this.state = { + highlight: false, + showMenu: false, + isItemMenuShow: false + }; + } + + onMouseEnter = () => { + if (!this.props.isItemFreezed) { + this.setState({ + showMenu: true, + highlight: true, + }); + } + } + + onMouseLeave = () => { + if (!this.props.isItemFreezed) { + this.setState({ + showMenu: false, + highlight: false + }); + } + } + + onDropdownToggleClick = (e) => { + e.preventDefault(); + this.toggleOperationMenu(e); + } + + toggleOperationMenu = (e) => { + e.stopPropagation(); + this.setState( + {isItemMenuShow: !this.state.isItemMenuShow }, () => { + if (this.state.isItemMenuShow) { + this.props.onFreezedItem(); + } else { + this.setState({ + highlight: false, + showMenu: false, + }); + this.props.onUnfreezedItem(); + } + } + ); + } + + toggleDelete = () => { + this.props.deleteGroupItem(this.props.group); + } + + renderGroupHref = (group) => { + let groupInfoHref; + if (group.creatorName == 'system admin') { + groupInfoHref = siteRoot + 'org/departmentadmin/groups/' + group.id + '/'; + } else { + groupInfoHref = siteRoot + 'org/groupadmin/' + group.id + '/'; + } + + return groupInfoHref; + } + + renderGroupCreator = (group) => { + let userInfoHref = siteRoot + 'org/useradmin/info/' + group.creatorEmail + '/'; + if (group.creatorName == 'system admin') { + return ( + -- + ); + } else { + return( + + {group.creatorName} + + ); + } + } + + render() { + let { group } = this.props; + let isOperationMenuShow = (group.creatorName != 'system admin') && this.state.showMenu; + return ( + + + {group.groupName} + + {this.renderGroupCreator(group)} + {group.ctime} + + {isOperationMenuShow && + + + + {gettext('Delete')} + + + } + + + ); + } +} + + +class OrgGroupsSearchGroupsResult extends React.Component { + + constructor(props) { + super(props); + this.state = { + isItemFreezed: false + }; + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + render() { + let { orgGroups } = this.props; + return ( +
+ + + + + + + + + + + {orgGroups.map(item => { + return ( + + ); + })} + +
{gettext('Name')}{gettext('Creator')}{gettext('Created At')}{gettext('Operations')}
+
+ ); + } +} + +class OrgGroupsSearchGroups extends Component { + + constructor(props) { + super(props); + this.state = { + query: '', + orgGroups: [], + isSubmitBtnActive: false, + loading: true, + errorMsg: '', + }; + } + + componentDidMount () { + let params = (new URL(document.location)).searchParams; + this.setState({ + query: params.get('query') || '', + }, () => {this.getItems();}); + } + + getItems = () => { + seafileAPI.orgAdminSearchGroup(orgID, this.state.query.trim()).then(res => { + let groupList = res.data.group_list.map(item => { + return new OrgGroupInfo(item); + }); + this.setState({ + orgGroups: groupList, + loading: false, + }); + }).catch((error) => { + this.setState({ + loading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); + }); + } + + deleteGroupItem = (group) => { + seafileAPI.orgAdminDeleteOrgGroup(orgID, group.id).then(res => { + this.setState({ + orgGroups: this.state.orgGroups.filter(item => item.id != group.id) + }); + let msg = gettext('Successfully deleted {name}'); + msg = msg.replace('{name}', group.groupName); + toaster.success(msg); + }).catch(error => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + handleInputChange = (e) => { + this.setState({ + query: e.target.value + }, this.checkSubmitBtnActive); + } + + checkSubmitBtnActive = () => { + const { query } = this.state; + this.setState({ + isSubmitBtnActive: query.trim() + }); + } + + render() { + const { query, isSubmitBtnActive } = this.state; + + return ( + +
+
+
+

{gettext('Groups')}

+
+
+
+

{gettext('Search Groups')}

+
+ + + + + + + + + + +
+
+
+

{gettext('Result')}

+ +
+
+
+
+
+ ); + } +} + +export default OrgGroupsSearchGroups; diff --git a/frontend/src/pages/org-admin/org-groups.js b/frontend/src/pages/org-admin/org-groups.js index 82f635e053..a07d871c59 100644 --- a/frontend/src/pages/org-admin/org-groups.js +++ b/frontend/src/pages/org-admin/org-groups.js @@ -1,4 +1,5 @@ import React, { Component, Fragment } from 'react'; +import { navigate } from '@reach/router'; import PropTypes from 'prop-types'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; import { siteRoot, gettext, orgID } from '../../utils/constants'; @@ -8,6 +9,55 @@ import toaster from '../../components/toast'; import OrgGroupInfo from '../../models/org-group'; import MainPanelTopbar from './main-panel-topbar'; +class Search extends React.Component { + + constructor(props) { + super(props); + this.state = { + value: '' + }; + } + + handleInputChange = (e) => { + this.setState({ + value: e.target.value + }); + } + + handleKeyPress = (e) => { + if (e.key == 'Enter') { + e.preventDefault(); + this.handleSubmit(); + } + } + + handleSubmit = () => { + const value = this.state.value.trim(); + if (!value) { + return false; + } + this.props.submit(value); + } + + render() { + return ( +
+ + +
+ ); + } +} + class OrgGroups extends Component { constructor(props) { @@ -77,11 +127,22 @@ class OrgGroups extends Component { }); } + searchItems = (keyword) => { + navigate(`${siteRoot}org/groupadmin/search-groups/?query=${encodeURIComponent(keyword)}`); + } + + getSearch = () => { + return ; + } + render() { let groups = this.state.orgGroups; return ( - +
diff --git a/frontend/src/pages/org-admin/org-users-search-users.js b/frontend/src/pages/org-admin/org-users-search-users.js new file mode 100644 index 0000000000..85a7370038 --- /dev/null +++ b/frontend/src/pages/org-admin/org-users-search-users.js @@ -0,0 +1,186 @@ +import React, { Component, Fragment } from 'react'; +import { Button, Form, FormGroup, Input, Col } from 'reactstrap'; +import { Utils } from '../../utils/utils'; +import { seafileAPI } from '../../utils/seafile-api'; +import { gettext, orgID } from '../../utils/constants'; +import toaster from '../../components/toast'; +import UserItem from './org-user-item'; +import OrgUserInfo from '../../models/org-user'; + +class OrgUsersSearchUsersResult extends React.Component { + + constructor(props) { + super(props); + this.state = { + isItemFreezed: false + }; + } + + onFreezedItem = () => { + this.setState({isItemFreezed: true}); + } + + onUnfreezedItem = () => { + this.setState({isItemFreezed: false}); + } + + render() { + let { orgUsers } = this.props; + return ( +
+ + + + + + + + + + + + {orgUsers.map((item, index) => { + return ( + + );})} + +
{gettext('Name')}{gettext('Status')} + {gettext('Space Used')} / {gettext('Quota')} + {gettext('Created At')} / {gettext('Last Login')}{/*Operations*/}
+
+ ); + } +} + +class OrgUsersSearchUsers extends Component { + + constructor(props) { + super(props); + this.state = { + query: '', + orgUsers: [], + org_id: '', + isSubmitBtnActive: false, + loading: true, + errorMsg: '', + }; + } + + componentDidMount () { + let params = (new URL(document.location)).searchParams; + this.setState({ + query: params.get('query') || '', + }, () => {this.getItems();}); + } + + getItems = () => { + seafileAPI.orgAdminSearchUser(orgID, this.state.query.trim()).then(res => { + let userList = res.data.user_list.map(item => { + return new OrgUserInfo(item); + }); + this.setState({ + orgUsers: userList, + loading: false, + }); + }).catch((error) => { + this.setState({ + loading: false, + errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403 + }); + }); + } + + deleteUser = (email) => { + seafileAPI.orgAdminDeleteOrgUser(orgID, email).then(res => { + let newUserList = this.state.orgUsers.filter(item => { + return item.email != email; + }); + this.setState({orgUsers: newUserList}); + toaster.success(gettext('Successfully deleted 1 item.')); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + updateUser = (email, key, value) => { + seafileAPI.sysAdminUpdateUser(email, key, value).then(res => { + let newUserList = this.state.orgUsers.map(item => { + if (item.email == email) { + item[key]= res.data[key]; + } + return item; + }); + this.setState({orgUsers: newUserList}); + const msg = (key == 'is_active' && value) ? + res.data.update_status_tip : gettext('Edit succeeded'); + toaster.success(msg); + }).catch((error) => { + let errMessage = Utils.getErrorMsg(error); + toaster.danger(errMessage); + }); + } + + handleInputChange = (e) => { + this.setState({ + query: e.target.value + }, this.checkSubmitBtnActive); + } + + checkSubmitBtnActive = () => { + const { query } = this.state; + this.setState({ + isSubmitBtnActive: query.trim() + }); + } + + render() { + const { query, isSubmitBtnActive } = this.state; + + return ( + +
+
+
+

{gettext('Users')}

+
+
+
+

{gettext('Search Users')}

+
+ + + + + + + + + + +
+
+
+

{gettext('Result')}

+ +
+
+
+
+
+ ); + } +} + +export default OrgUsersSearchUsers; diff --git a/frontend/src/pages/org-admin/org-users-users.js b/frontend/src/pages/org-admin/org-users-users.js index d25bf229dc..26a2a44619 100644 --- a/frontend/src/pages/org-admin/org-users-users.js +++ b/frontend/src/pages/org-admin/org-users-users.js @@ -9,9 +9,58 @@ import InviteUserDialog from '../../components/dialog/org-admin-invite-user-dial import toaster from '../../components/toast'; import { seafileAPI } from '../../utils/seafile-api'; import OrgUserInfo from '../../models/org-user'; -import { gettext, invitationLink, orgID } from '../../utils/constants'; +import { gettext, invitationLink, orgID, siteRoot } from '../../utils/constants'; import { Utils } from '../../utils/utils'; +class Search extends React.Component { + + constructor(props) { + super(props); + this.state = { + value: '' + }; + } + + handleInputChange = (e) => { + this.setState({ + value: e.target.value + }); + } + + handleKeyPress = (e) => { + if (e.key == 'Enter') { + e.preventDefault(); + this.handleSubmit(); + } + } + + handleSubmit = () => { + const value = this.state.value.trim(); + if (!value) { + return false; + } + this.props.submit(value); + } + + render() { + return ( +
+ + +
+ ); + } +} + class OrgUsers extends Component { constructor(props) { @@ -117,6 +166,17 @@ class OrgUsers extends Component { }); } + searchItems = (keyword) => { + navigate(`${siteRoot}org/useradmin/search-users/?query=${encodeURIComponent(keyword)}`); + } + + getSearch = () => { + return ; + } + render() { const topBtn = 'btn btn-secondary operation-item'; let topbarChildren; @@ -143,7 +203,7 @@ class OrgUsers extends Component { return ( - +
diff --git a/seahub/api2/endpoints/admin/invitations.py b/seahub/api2/endpoints/admin/invitations.py index da98f7a05e..d6d515bb57 100644 --- a/seahub/api2/endpoints/admin/invitations.py +++ b/seahub/api2/endpoints/admin/invitations.py @@ -89,7 +89,7 @@ class AdminInvitations(APIView): data['invite_type'] = invitation.invite_type data['invite_time'] = datetime_to_isoformat_timestr(invitation.invite_time) - data['accept_time'] = datetime_to_isoformat_timestr(invitation.accept_time) + data['accept_time'] = datetime_to_isoformat_timestr(invitation.accept_time) if invitation.accept_time else '' data['expire_time'] = datetime_to_isoformat_timestr(invitation.expire_time) data['is_expired'] = invitation.is_expired() diff --git a/seahub/api2/endpoints/admin/org_users.py b/seahub/api2/endpoints/admin/org_users.py index 6c8bde1dda..03c38c77e8 100644 --- a/seahub/api2/endpoints/admin/org_users.py +++ b/seahub/api2/endpoints/admin/org_users.py @@ -61,6 +61,8 @@ def get_org_user_info(org_id, user_obj): if last_login: user_info['last_login'] = datetime_to_isoformat_timestr(last_login) + user_info['is_org_staff'] = True if ccnet_api.is_org_staff(org_id, email) == 1 else False + return user_info def check_org_user(func): @@ -352,6 +354,28 @@ class AdminOrgUser(APIView): seafile_api.set_org_user_quota(org_id, email, user_quota) + # update is_org_staff + is_org_staff = request.data.get("is_org_staff", '') + if is_org_staff: + + is_org_staff = is_org_staff.lower() + if is_org_staff not in ('true', 'false'): + error_msg = 'is_org_staff invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if is_org_staff == 'true': + if ccnet_api.is_org_staff(org_id, email): + error_msg = '%s is already organization staff.' % email + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + ccnet_api.set_org_staff(org_id, email) + else: + if not ccnet_api.is_org_staff(org_id, email): + error_msg = '%s is not organization staff.' % email + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + ccnet_api.unset_org_staff(org_id, email) + user_info = get_org_user_info(org_id, user) user_info['active'] = user.is_active return Response(user_info) diff --git a/seahub/api2/endpoints/admin/users.py b/seahub/api2/endpoints/admin/users.py index 9dc1c24928..d21aa647ef 100644 --- a/seahub/api2/endpoints/admin/users.py +++ b/seahub/api2/endpoints/admin/users.py @@ -689,6 +689,9 @@ class AdminUsers(APIView): admin_operation.send(sender=None, admin_name=request.user.username, operation=USER_ADD, detail=admin_op_detail) + if config.FORCE_PASSWORD_CHANGE: + UserOptions.objects.set_force_passwd_change(email) + return Response(user_info) diff --git a/seahub/api2/endpoints/repo_share_invitations.py b/seahub/api2/endpoints/repo_share_invitations.py index e45d636e77..c1310f23f2 100644 --- a/seahub/api2/endpoints/repo_share_invitations.py +++ b/seahub/api2/endpoints/repo_share_invitations.py @@ -1,7 +1,9 @@ # Copyright (c) 2012-2019 Seafile Ltd. import logging +from datetime import timedelta +from django.utils import timezone from django.utils.translation import ugettext as _ from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -19,6 +21,7 @@ from seahub.base.accounts import User from seahub.utils import is_valid_email from seahub.invitations.models import Invitation, RepoShareInvitation from seahub.invitations.utils import block_accepter +from seahub.invitations.settings import INVITATIONS_TOKEN_AGE from seahub.constants import PERMISSION_READ, PERMISSION_READ_WRITE, GUEST_USER from seahub.share.utils import is_repo_admin from seahub.utils import is_org_context @@ -175,6 +178,8 @@ class RepoShareInvitationsBatchView(APIView): if invitation_queryset.filter(accepter=accepter).exists(): invitation = invitation_queryset.filter(accepter=accepter)[0] + invitation.expire_time = timezone.now() + timedelta(hours=int(INVITATIONS_TOKEN_AGE)) + invitation.save() else: invitation = Invitation.objects.add( inviter=request.user.username, accepter=accepter) diff --git a/seahub/api2/endpoints/repo_trash.py b/seahub/api2/endpoints/repo_trash.py index 7952d2b747..232a1ca8f3 100644 --- a/seahub/api2/endpoints/repo_trash.py +++ b/seahub/api2/endpoints/repo_trash.py @@ -25,6 +25,7 @@ from constance import config logger = logging.getLogger(__name__) + class RepoTrash(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) @@ -97,8 +98,81 @@ class RepoTrash(APIView): try: # a list will be returned, with at least 1 item in it # the last item is not a deleted entry, and it contains an attribute named 'scan_stat' - deleted_entries = seafile_api.get_deleted(repo_id, - show_days, path, scan_stat) + deleted_entries = seafile_api.get_deleted(repo_id, show_days, path, scan_stat) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + scan_stat = deleted_entries[-1].scan_stat + more = True if scan_stat is not None else False + + items = [] + if len(deleted_entries) > 1: + entries_without_scan_stat = deleted_entries[0:-1] + + # sort entry by delete time + entries_without_scan_stat.sort( + key=lambda x: x.delete_time, reverse=True) + + for item in entries_without_scan_stat: + item_info = self.get_item_info(item) + items.append(item_info) + + result = { + 'data': items, + 'more': more, + 'scan_stat': scan_stat, + } + + return Response(result) + + def post(self, request, repo_id, format=None): + """ Return deleted files/dirs of a repo/folder + + Permission checking: + 1. all authenticated user can perform this action. + """ + + # argument check + path = request.data.get('path', '/') + + # resource check + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + try: + dir_id = seafile_api.get_dir_id_by_path(repo_id, path) + except SearpcError as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + if not dir_id: + error_msg = 'Folder %s not found.' % path + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + if check_folder_permission(request, repo_id, path) is None: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + show_days = int(request.data.get('show_days', '0')) + except ValueError: + show_days = 0 + + if show_days < 0: + error_msg = 'show_days invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + scan_stat = request.data.get('scan_stat', None) + try: + # a list will be returned, with at least 1 item in it + # the last item is not a deleted entry, and it contains an attribute named 'scan_stat' + deleted_entries = seafile_api.get_deleted(repo_id, show_days, path, scan_stat) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' @@ -169,8 +243,8 @@ class RepoTrash(APIView): seafile_api.clean_up_repo_history(repo_id, keep_days) org_id = None if not request.user.org else request.user.org.org_id clean_up_repo_trash.send(sender=None, org_id=org_id, - operator=username, repo_id=repo_id, repo_name=repo.name, - repo_owner=repo_owner, days=keep_days) + operator=username, repo_id=repo_id, repo_name=repo.name, + repo_owner=repo_owner, days=keep_days) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' diff --git a/seahub/api2/endpoints/webdav_secret.py b/seahub/api2/endpoints/webdav_secret.py index 76e5258931..a83b26d6c1 100644 --- a/seahub/api2/endpoints/webdav_secret.py +++ b/seahub/api2/endpoints/webdav_secret.py @@ -7,6 +7,8 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from django.utils.translation import ugettext as _ + from seahub.api2.authentication import TokenAuthentication from seahub.api2.throttling import UserRateThrottle from seahub.api2.utils import api_error @@ -43,6 +45,9 @@ class WebdavSecretView(APIView): username = request.user.username secret = request.data.get("secret", None) + if len(secret) >= 30: + return api_error(status.HTTP_400_BAD_REQUEST, + _("Length of WebDav password should be less then 30.")) if secret: encoded = aes.encode(secret) diff --git a/seahub/onlyoffice/utils.py b/seahub/onlyoffice/utils.py index 8054c2e719..e071965bce 100644 --- a/seahub/onlyoffice/utils.py +++ b/seahub/onlyoffice/utils.py @@ -13,7 +13,7 @@ from seaserv import seafile_api from seahub.base.templatetags.seahub_tags import email2nickname from seahub.utils import get_file_type_and_ext, gen_file_get_url, \ - get_site_scheme_and_netloc, normalize_cache_key + get_site_scheme_and_netloc, encrypt_with_sha1 from seahub.utils.file_op import if_locked_by_online_office from seahub.settings import ENABLE_WATERMARK @@ -25,9 +25,8 @@ logger = logging.getLogger('onlyoffice') def generate_onlyoffice_cache_key(repo_id, file_path): - prefix = "ONLYOFFICE_" - value = "%s_%s" % (repo_id, file_path) - return normalize_cache_key(value, prefix) + + return "ONLYOFFICE_{}_{}".format(repo_id, encrypt_with_sha1(file_path)) def get_onlyoffice_dict(request, username, repo_id, file_path, file_id='', diff --git a/seahub/two_factor/models/static.py b/seahub/two_factor/models/static.py index 19f898e70c..0c3193f8f9 100644 --- a/seahub/two_factor/models/static.py +++ b/seahub/two_factor/models/static.py @@ -70,4 +70,4 @@ class StaticToken(models.Model): :rtype: str """ - return b32encode(urandom(5)).lower() + return b32encode(urandom(5)).lower().decode() diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index 46be5789f8..4bb58705fe 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -1387,3 +1387,8 @@ def is_valid_org_id(org_id): return True else: return False + + +def encrypt_with_sha1(origin_str): + + return hashlib.sha1(origin_str.encode()).hexdigest() diff --git a/seahub/utils/hasher.py b/seahub/utils/hasher.py index f9f7e9cb04..ac5a490b8f 100644 --- a/seahub/utils/hasher.py +++ b/seahub/utils/hasher.py @@ -51,4 +51,5 @@ class AESPasswordHasher: raise AESPasswordDecodeError data = data.encode('utf-8') + data += b'='*4 return DecodeAES(self.cipher, data) diff --git a/seahub/utils/timeutils.py b/seahub/utils/timeutils.py index 47d9c66c5b..a0cc505090 100644 --- a/seahub/utils/timeutils.py +++ b/seahub/utils/timeutils.py @@ -12,6 +12,7 @@ logger = logging.getLogger(__name__) # https://docs.djangoproject.com/en/1.11/ref/utils/#django.utils.timezone.get_current_timezone current_timezone = get_current_timezone() + def dt(value): """Convert 32/64 bits timestamp to datetime object. """ @@ -21,6 +22,7 @@ def dt(value): # TODO: need a better way to handle 64 bits timestamp. return datetime.datetime.utcfromtimestamp(value/1000000) + def value_to_db_datetime(value): if value is None: return None @@ -35,6 +37,7 @@ def value_to_db_datetime(value): # MySQL doesn't support microseconds return six.text_type(value.replace(microsecond=0)) + def utc_to_local(dt): # change from UTC timezone to current seahub timezone tz = timezone.get_default_timezone() @@ -42,6 +45,7 @@ def utc_to_local(dt): local = timezone.make_naive(utc, tz) return local + def timestamp_to_isoformat_timestr(timestamp): try: min_ts = -(1 << 31) @@ -58,9 +62,13 @@ def timestamp_to_isoformat_timestr(timestamp): logger.error(e) return '' + # https://pypi.org/project/pytz/ def datetime_to_isoformat_timestr(datetime): + if not datetime: + return '' + from django.utils.timezone import make_naive, is_aware if is_aware(datetime): datetime = make_naive(datetime) @@ -76,6 +84,7 @@ def datetime_to_isoformat_timestr(datetime): logger.error(e) return '' + def utc_datetime_to_isoformat_timestr(utc_datetime): try: # The second way of building a localized time is by converting an existing @@ -88,6 +97,7 @@ def utc_datetime_to_isoformat_timestr(utc_datetime): logger.error(e) return '' + def datetime_to_timestamp(datetime_obj): epoch = datetime.datetime(1970, 1, 1) local = utc_to_local(datetime_obj)