1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-05-03 21:46:47 +00:00

Merge branch '7.1' into master

This commit is contained in:
lian 2020-09-30 11:05:44 +08:00
commit 0bff0c1104
27 changed files with 524 additions and 220 deletions

View File

@ -12022,9 +12022,9 @@
} }
}, },
"seafile-js": { "seafile-js": {
"version": "0.2.154", "version": "0.2.156",
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.154.tgz", "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.156.tgz",
"integrity": "sha512-8Vy+RIK4P7yZowr/smgd/6eLOOpT4pq4UbJXhjE0XDp6s6WERmLr1nZUMA1QjMERMlfJ3ymq2Jk30rLsN82Lrg==", "integrity": "sha512-D8ZUwNNay8WTFqIMF6AzysKFFkzFAdGgGapHsvpijOl2lETg9Pg+OlWawlFzDxaTKu4JxSTk2g9On/RGvqswpQ==",
"requires": { "requires": {
"axios": "^0.18.0", "axios": "^0.18.0",
"form-data": "^2.3.2", "form-data": "^2.3.2",

View File

@ -44,7 +44,7 @@
"react-responsive": "^6.1.2", "react-responsive": "^6.1.2",
"react-select": "^2.4.1", "react-select": "^2.4.1",
"reactstrap": "^6.4.0", "reactstrap": "^6.4.0",
"seafile-js": "^0.2.154", "seafile-js": "0.2.156",
"socket.io-client": "^2.2.0", "socket.io-client": "^2.2.0",
"sw-precache-webpack-plugin": "0.11.4", "sw-precache-webpack-plugin": "0.11.4",
"unified": "^7.0.0", "unified": "^7.0.0",

View File

@ -0,0 +1,59 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button, FormGroup, Label, Input } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
executeOperation: PropTypes.func.isRequired,
toggleDialog: PropTypes.func.isRequired
};
class ConfirmUnlinkDevice extends Component {
constructor(props) {
super(props);
this.state = {
isChecked: false
};
}
toggle = () => {
this.props.toggleDialog();
}
executeOperation = () => {
this.toggle();
this.props.executeOperation(this.state.isChecked);
}
onInputChange = (e) => {
this.setState({
isChecked: e.target.checked
});
}
render() {
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Unlink device')}</ModalHeader>
<ModalBody>
<p>{gettext('Are you sure you want to unlink this device?')}</p>
<FormGroup check>
<Label check>
<Input type="checkbox" checked={this.state.isChecked} onChange={this.onInputChange} />
<span>{gettext('Delete files from this device the next time it comes online.')}</span>
</Label>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.executeOperation}>{gettext('Unlink')}</Button>
</ModalFooter>
</Modal>
);
}
}
ConfirmUnlinkDevice.propTypes = propTypes;
export default ConfirmUnlinkDevice;

View File

@ -30,7 +30,7 @@ class AddOrgUserDialog extends React.Component {
if (isValid) { if (isValid) {
let { email, name, password } = this.state; let { email, name, password } = this.state;
this.setState({isAddingUser: true}); this.setState({isAddingUser: true});
this.props.handleSubmit(email, name, password); this.props.handleSubmit(email, name.trim(), password);
} }
} }
@ -71,7 +71,7 @@ class AddOrgUserDialog extends React.Component {
} }
inputName = (e) => { inputName = (e) => {
let name = e.target.value.trim(); let name = e.target.value;
this.setState({name: name}); this.setState({name: name});
} }
@ -107,7 +107,7 @@ class AddOrgUserDialog extends React.Component {
this.setState({errMessage: errMessage}); this.setState({errMessage: errMessage});
return false; return false;
} }
let name = this.state.name; let name = this.state.name.trim();
if (!name.length) { if (!name.length) {
errMessage = gettext('Name is required'); errMessage = gettext('Name is required');
this.setState({errMessage: errMessage}); this.setState({errMessage: errMessage});

View File

@ -4,11 +4,13 @@ import moment from 'moment';
import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap'; import { Dropdown, DropdownMenu, DropdownToggle, DropdownItem } from 'reactstrap';
import { Link } from '@reach/router'; import { Link } from '@reach/router';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff } from '../../utils/constants'; import { gettext, siteRoot, isPro, username, folderPermEnabled, isSystemStaff, enableResetEncryptedRepoPassword, isEmailConfigured } from '../../utils/constants';
import ModalPortal from '../../components/modal-portal'; import ModalPortal from '../../components/modal-portal';
import ShareDialog from '../../components/dialog/share-dialog'; import ShareDialog from '../../components/dialog/share-dialog';
import LibSubFolderPermissionDialog from '../../components/dialog/lib-sub-folder-permission-dialog'; import LibSubFolderPermissionDialog from '../../components/dialog/lib-sub-folder-permission-dialog';
import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog'; import DeleteRepoDialog from '../../components/dialog/delete-repo-dialog';
import ChangeRepoPasswordDialog from '../../components/dialog/change-repo-password-dialog';
import ResetEncryptedRepoPasswordDialog from '../../components/dialog/reset-encrypted-repo-password-dialog';
import Rename from '../rename'; import Rename from '../rename';
import { seafileAPI } from '../../utils/seafile-api'; import { seafileAPI } from '../../utils/seafile-api';
import LibHistorySettingDialog from '../dialog/lib-history-setting-dialog'; import LibHistorySettingDialog from '../dialog/lib-history-setting-dialog';
@ -46,6 +48,8 @@ class SharedRepoListItem extends React.Component {
isAPITokenDialogShow: false, isAPITokenDialogShow: false,
isRepoShareUploadLinksDialogOpen: false, isRepoShareUploadLinksDialogOpen: false,
isRepoDeleted: false, isRepoDeleted: false,
isChangePasswordDialogShow: false,
isResetPasswordDialogShow: false
}; };
this.isDeparementOnwerGroupMember = false; this.isDeparementOnwerGroupMember = false;
} }
@ -141,6 +145,12 @@ class SharedRepoListItem extends React.Component {
case 'Share Links Admin': case 'Share Links Admin':
this.toggleRepoShareUploadLinksDialog(); this.toggleRepoShareUploadLinksDialog();
break; break;
case 'Change Password':
this.onChangePasswordToggle();
break;
case 'Reset Password':
this.onResetPasswordToggle();
break;
default: default:
break; break;
} }
@ -231,6 +241,14 @@ class SharedRepoListItem extends React.Component {
this.setState({isAPITokenDialogShow: !this.state.isAPITokenDialogShow}); this.setState({isAPITokenDialogShow: !this.state.isAPITokenDialogShow});
} }
onChangePasswordToggle = () => {
this.setState({isChangePasswordDialogShow: !this.state.isChangePasswordDialogShow});
}
onResetPasswordToggle = () => {
this.setState({isResetPasswordDialogShow: !this.state.isResetPasswordDialogShow});
}
translateMenuItem = (menuItem) => { translateMenuItem = (menuItem) => {
let translateResult = ''; let translateResult = '';
switch(menuItem) { switch(menuItem) {
@ -255,6 +273,12 @@ class SharedRepoListItem extends React.Component {
case 'Share Links Admin': case 'Share Links Admin':
translateResult = gettext('Share Links Admin'); translateResult = gettext('Share Links Admin');
break; break;
case 'Change Password':
translateResult = gettext('Change Password');
break;
case 'Reset Password':
translateResult = gettext('Reset Password');
break;
case 'API Token': case 'API Token':
translateResult = 'API Token'; // translation is not needed here translateResult = 'API Token'; // translation is not needed here
break; break;
@ -280,7 +304,14 @@ class SharedRepoListItem extends React.Component {
if (folderPermEnabled) { if (folderPermEnabled) {
operations.push('Folder Permission'); operations.push('Folder Permission');
} }
operations.push('Share Links Admin', 'History Setting', 'API Token', 'Details'); operations.push('Share Links Admin');
if (repo.encrypted) {
operations.push('Change Password');
}
if (repo.encrypted && enableResetEncryptedRepoPassword && isEmailConfigured) {
operations.push('Reset Password');
}
operations.push('History Setting', 'API Token', 'Details');
} else { } else {
operations.push('Unshare'); operations.push('Unshare');
} }
@ -538,6 +569,23 @@ class SharedRepoListItem extends React.Component {
/> />
</ModalPortal> </ModalPortal>
)} )}
{this.state.isChangePasswordDialogShow && (
<ModalPortal>
<ChangeRepoPasswordDialog
repoID={repo.repo_id}
repoName={repo.repo_name}
toggleDialog={this.onChangePasswordToggle}
/>
</ModalPortal>
)}
{this.state.isResetPasswordDialogShow && (
<ModalPortal>
<ResetEncryptedRepoPasswordDialog
repoID={repo.repo_id}
toggleDialog={this.onResetPasswordToggle}
/>
</ModalPortal>
)}
</Fragment> </Fragment>
); );
} }

View File

@ -11,8 +11,8 @@ class OrgUserInfo {
this.email = object.email; this.email = object.email;
this.contact_email = object.owner_contact_email; this.contact_email = object.owner_contact_email;
this.is_active = object.is_active; this.is_active = object.is_active;
this.quota = object.quota > 0 ? Utils.bytesToSize(object.quota) : ''; this.quota_usage = object.quota_usage;
this.self_usage = Utils.bytesToSize(object.self_usage); this.quota_total = object.quota_total;
this.last_login = object.last_login ? moment(object.last_login).fromNow() : '--'; this.last_login = object.last_login ? moment(object.last_login).fromNow() : '--';
this.ctime = moment(object.ctime).format('YYYY-MM-DD HH:mm:ss'); this.ctime = moment(object.ctime).format('YYYY-MM-DD HH:mm:ss');
} }

View File

@ -5,6 +5,7 @@ import { seafileAPI } from '../../utils/seafile-api';
import { gettext } from '../../utils/constants'; import { gettext } from '../../utils/constants';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
import EmptyTip from '../../components/empty-tip'; import EmptyTip from '../../components/empty-tip';
import ConfirmUnlinkDeviceDialog from '../../components/dialog/confirm-unlink-device';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
class Content extends Component { class Content extends Component {
@ -65,8 +66,9 @@ class Item extends Component {
super(props); super(props);
this.state = { this.state = {
isOpMenuOpen: false, // for mobile isOpMenuOpen: false, // for mobile
showOpIcon: false, isOpIconShown: false,
unlinked: false unlinked: false,
isConfirmUnlinkDialogOpen: false
}; };
} }
@ -78,13 +80,19 @@ class Item extends Component {
handleMouseOver = () => { handleMouseOver = () => {
this.setState({ this.setState({
showOpIcon: true isOpIconShown: true
}); });
} }
handleMouseOut = () => { handleMouseOut = () => {
this.setState({ this.setState({
showOpIcon: false isOpIconShown: false
});
}
toggleDialog = () => {
this.setState({
isConfirmUnlinkDialogOpen: !this.state.isConfirmUnlinkDialogOpen
}); });
} }
@ -92,14 +100,23 @@ class Item extends Component {
e.preventDefault(); e.preventDefault();
const data = this.props.data; const data = this.props.data;
if (data.is_desktop_client) {
this.toggleDialog();
} else {
const wipeDevice = true;
this.unlinkDevice(wipeDevice);
}
}
seafileAPI.unlinkDevice(data.platform, data.device_id).then((res) => { unlinkDevice = (wipeDevice) => {
const data = this.props.data;
seafileAPI.unlinkDevice(data.platform, data.device_id, wipeDevice).then((res) => {
this.setState({ this.setState({
unlinked: true unlinked: true
}); });
let msg_s = gettext('Successfully unlinked %(name)s.'); let msg = gettext('Successfully unlinked %(name)s.');
msg_s = msg_s.replace('%(name)s', data.device_name); msg = msg.replace('%(name)s', data.device_name);
toaster.success(msg_s); toaster.success(msg);
}).catch((error) => { }).catch((error) => {
let errMessage = Utils.getErrorMsg(error); let errMessage = Utils.getErrorMsg(error);
toaster.danger(errMessage); toaster.danger(errMessage);
@ -114,7 +131,7 @@ class Item extends Component {
const data = this.props.data; const data = this.props.data;
let opClasses = 'sf2-icon-delete unlink-device action-icon'; let opClasses = 'sf2-icon-delete unlink-device action-icon';
opClasses += this.state.showOpIcon ? '' : ' invisible'; opClasses += this.state.isOpIconShown ? '' : ' invisible';
const desktopItem = ( const desktopItem = (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}> <tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
@ -156,7 +173,17 @@ class Item extends Component {
</tr> </tr>
); );
return this.props.isDesktop ? desktopItem : mobileItem; return (
<React.Fragment>
{this.props.isDesktop ? desktopItem : mobileItem}
{this.state.isConfirmUnlinkDialogOpen &&
<ConfirmUnlinkDeviceDialog
executeOperation={this.unlinkDevice}
toggleDialog={this.toggleDialog}
/>
}
</React.Fragment>
);
} }
} }

View File

@ -53,7 +53,7 @@ class OrgAdminList extends React.Component {
{orgAdminUsers.map(item => { {orgAdminUsers.map(item => {
return ( return (
<UserItem <UserItem
key={item.id} key={item.index}
user={item} user={item}
currentTab="admins" currentTab="admins"
isItemFreezed={this.state.isItemFreezed} isItemFreezed={this.state.isItemFreezed}

View File

@ -55,13 +55,13 @@ class UserItem extends React.Component {
} }
toggleResetPW = () => { toggleResetPW = () => {
const email = this.props.user.email; const { email, name } = this.props.user;
toaster.success(gettext('Resetting user\'s password, please wait for a moment.')); toaster.success(gettext('Resetting user\'s password, please wait for a moment.'));
seafileAPI.orgAdminResetOrgUserPassword(orgID, email).then(res => { seafileAPI.orgAdminResetOrgUserPassword(orgID, email).then(res => {
let msg; let msg;
msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.'); msg = gettext('Successfully reset password to %(passwd)s for user %(user)s.');
msg = msg.replace('%(passwd)s', res.data.new_password); msg = msg.replace('%(passwd)s', res.data.new_password);
msg = msg.replace('%(user)s', email); msg = msg.replace('%(user)s', name);
toaster.success(msg, { toaster.success(msg, {
duration: 15 duration: 15
}); });
@ -122,6 +122,17 @@ class UserItem extends React.Component {
); );
} }
getQuotaTotal = (data) => {
switch (data) {
case -1: // failed to fetch quota
return gettext('Failed');
case -2:
return '--';
default: // data > 0
return Utils.formatSize({bytes: data});
}
}
render() { render() {
let { user, currentTab } = this.props; let { user, currentTab } = this.props;
let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/'; let href = siteRoot + 'org/useradmin/info/' + encodeURIComponent(user.email) + '/';
@ -141,7 +152,7 @@ class UserItem extends React.Component {
onStatusChanged={this.changeStatus} onStatusChanged={this.changeStatus}
/> />
</td> </td>
<td>{`${user.self_usage} / ${user.quota || '--'}`}</td> <td>{`${Utils.formatSize({bytes: user.quota_usage})} / ${this.getQuotaTotal(user.quota_total)}`}</td>
<td> <td>
{user.ctime} / {user.ctime} /
<br /> <br />

View File

@ -71,10 +71,10 @@ class OrgUsersList extends React.Component {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{orgUsers.map(item => { {orgUsers.map((item, index) => {
return ( return (
<UserItem <UserItem
key={item.id} key={index}
user={item} user={item}
currentTab="users" currentTab="users"
isItemFreezed={this.state.isItemFreezed} isItemFreezed={this.state.isItemFreezed}

View File

@ -1,17 +1,18 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import { Link } from '@reach/router';
import moment from 'moment';
import { Button } from 'reactstrap';
import { seafileAPI } from '../../../utils/seafile-api'; import { seafileAPI } from '../../../utils/seafile-api';
import { gettext, siteRoot } from '../../../utils/constants'; import { gettext, siteRoot } from '../../../utils/constants';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import EmptyTip from '../../../components/empty-tip';
import moment from 'moment';
import { Button } from 'reactstrap';
import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator';
import LogsNav from './logs-nav';
import MainPanelTopbar from '../main-panel-topbar';
import UserLink from '../user-link';
import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog'; import LogsExportExcelDialog from '../../../components/dialog/sysadmin-dialog/sysadmin-logs-export-excel-dialog';
import ModalPortal from '../../../components/modal-portal'; import ModalPortal from '../../../components/modal-portal';
import EmptyTip from '../../../components/empty-tip';
import Loading from '../../../components/loading';
import Paginator from '../../../components/paginator';
import MainPanelTopbar from '../main-panel-topbar';
import UserLink from '../user-link';
import LogsNav from './logs-nav';
class Content extends Component { class Content extends Component {
@ -44,9 +45,9 @@ class Content extends Component {
<table className="table-hover"> <table className="table-hover">
<thead> <thead>
<tr> <tr>
<th width="10%">{gettext('Share From')}</th> <th width="15%">{gettext('Share From')}</th>
<th width="10%">{gettext('Share To')}</th> <th width="15%">{gettext('Share To')}</th>
<th width="20%">{gettext('Actions')}</th> <th width="10%">{gettext('Actions')}</th>
<th width="13%">{gettext('Permission')}</th> <th width="13%">{gettext('Permission')}</th>
<th width="20%">{gettext('Library')}</th> <th width="20%">{gettext('Library')}</th>
<th width="12%">{gettext('Folder')}</th> <th width="12%">{gettext('Folder')}</th>
@ -112,12 +113,27 @@ class Item extends Component {
} }
} }
getShareTo = (item) => {
switch(item.share_type) {
case 'user':
return <UserLink email={item.to_user_email} name={item.to_user_name} />;
case 'group':
return <Link to={`${siteRoot}sys/groups/${item.to_group_id}/libraries/`}>{item.to_group_name}</Link>;
case 'department':
return <Link to={`${siteRoot}sys/departments/${item.to_group_id}/`}>{item.to_group_name}</Link>;
case 'all':
return <Link to={`${siteRoot}org/`}>{gettext('All')}</Link>;
default:
return gettext('Deleted');
}
}
render() { render() {
let { item } = this.props; let { item } = this.props;
return ( return (
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}> <tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
<td><UserLink email={item.from_user_email} name={item.from_user_name} /></td> <td><UserLink email={item.from_user_email} name={item.from_user_name} /></td>
<td><UserLink email={item.to_user_email} name={item.to_user_name} /></td> <td>{this.getShareTo(item)}</td>
<td>{this.getActionTextByEType(item.etype)}</td> <td>{this.getActionTextByEType(item.etype)}</td>
<td>{Utils.sharePerms(item.permission)}</td> <td>{Utils.sharePerms(item.permission)}</td>
<td>{item.repo_name ? item.repo_name : gettext('Deleted')}</td> <td>{item.repo_name ? item.repo_name : gettext('Deleted')}</td>

View File

@ -10,19 +10,20 @@ import Loading from '../../../components/loading';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast'; import toaster from '../../../components/toast';
class TrafficOrganizationsTable extends React.Component { class OrgsTraffic extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
userTrafficList: [], orgTrafficList: [],
perPage: 25, perPage: 25,
currentPage: 1, currentPage: 1,
hasNextPage: false, hasNextPage: false,
month: moment().format('YYYYMM'), month: moment().format('YYYYMM'),
isLoading: false, isLoading: false,
errorMessage: '', errorMessage: '',
sortOrder: 'asc' sortBy: 'link_file_download',
sortOrder: 'desc'
}; };
this.initPage = 1; this.initPage = 1;
this.initMonth = moment().format('YYYYMM'); this.initMonth = moment().format('YYYYMM');
@ -35,16 +36,16 @@ class TrafficOrganizationsTable extends React.Component {
perPage: parseInt(urlParams.get('per_page') || perPage), perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage) currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => { }, () => {
this.onGenerateReports(this.initMonth, this.state.currentPage); this.getTrafficList(this.initMonth, this.state.currentPage);
}); });
} }
getPreviousPage = () => { getPreviousPage = () => {
this.onGenerateReports(this.state.month, this.state.currentPage - 1); this.getTrafficList(this.state.month, this.state.currentPage - 1);
} }
getNextPage = () => { getNextPage = () => {
this.onGenerateReports(this.state.month, this.state.currentPage + 1); this.getTrafficList(this.state.month, this.state.currentPage + 1);
} }
handleChange = (e) => { handleChange = (e) => {
@ -65,28 +66,22 @@ class TrafficOrganizationsTable extends React.Component {
}); });
return; return;
} }
this.onGenerateReports(month, this.initPage); this.getTrafficList(month, this.initPage);
e.target.blur(); e.target.blur();
e.preventDefault(); e.preventDefault();
} }
} }
sortBySize = (sortByType, sortOrder) => { getTrafficList = (month, page) => {
let { userTrafficList } = this.state; const { perPage, sortBy, sortOrder } = this.state;
let newUserTrafficList = Utils.sortTraffic(userTrafficList, sortByType, sortOrder); const orderBy = `${sortBy}_${sortOrder}`;
this.setState({
userTrafficList: newUserTrafficList,
sortOrder: sortOrder
});
}
onGenerateReports = (month, page) => {
let { perPage } = this.state;
this.setState({isLoading: true, errorMessage: ''}); this.setState({isLoading: true, errorMessage: ''});
seafileAPI.sysAdminListOrgTraffic(month, page, perPage).then(res => { seafileAPI.sysAdminListOrgTraffic(month, page, perPage, orderBy).then(res => {
let userTrafficList = res.data.org_monthly_traffic_list.slice(0); let orgTrafficList = res.data.org_monthly_traffic_list.slice(0);
this.setState({ this.setState({
userTrafficList: userTrafficList, month: month,
currentPage: page,
orgTrafficList: orgTrafficList,
hasNextPage: res.data.has_next_page, hasNextPage: res.data.has_next_page,
isLoading: false isLoading: false
}); });
@ -96,14 +91,28 @@ class TrafficOrganizationsTable extends React.Component {
}); });
} }
sortItems = (sortBy) => {
this.setState({
sortBy: sortBy,
sortOrder: this.state.sortOrder == 'asc' ? 'desc' : 'asc'
}, () => {
const { month, currentPage } = this.state;
this.getTrafficList(month, currentPage);
});
}
resetPerPage = (newPerPage) => { resetPerPage = (newPerPage) => {
this.setState({ this.setState({
perPage: newPerPage, perPage: newPerPage,
}, () => this.onGenerateReports(this.initPage, this.initMonth)); }, () => this.getTrafficList(this.initPage, this.initMonth));
} }
render() { render() {
let { userTrafficList, currentPage, hasNextPage, perPage, isLoading, errorMessage, sortOrder } = this.state; const {
isLoading, errorMessage, orgTrafficList,
currentPage, hasNextPage, perPage,
sortBy, sortOrder
} = this.state;
return ( return (
<Fragment> <Fragment>
<div className="d-flex align-items-center mt-4"> <div className="d-flex align-items-center mt-4">
@ -118,8 +127,8 @@ class TrafficOrganizationsTable extends React.Component {
</div> </div>
{isLoading && <Loading />} {isLoading && <Loading />}
{!isLoading && {!isLoading &&
<TrafficTable type={'org'} sortOrder={sortOrder} sortBySize={this.sortBySize} > <TrafficTable type={'org'} sortItems={this.sortItems} sortBy={sortBy} sortOrder={sortOrder}>
{userTrafficList.length > 0 && userTrafficList.map((item, index) => { {orgTrafficList.length > 0 && orgTrafficList.map((item, index) => {
return( return(
<TrafficTableBody <TrafficTableBody
key={index} key={index}
@ -143,4 +152,4 @@ class TrafficOrganizationsTable extends React.Component {
} }
} }
export default TrafficOrganizationsTable; export default OrgsTraffic;

View File

@ -10,7 +10,7 @@ import { gettext } from '../../../utils/constants';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast'; import toaster from '../../../components/toast';
class TrafficOrganizationsTable extends React.Component { class UsersTraffic extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -22,7 +22,8 @@ class TrafficOrganizationsTable extends React.Component {
month: moment().format('YYYYMM'), month: moment().format('YYYYMM'),
isLoading: false, isLoading: false,
errorMessage: '', errorMessage: '',
sortOrder: 'asc' sortBy: 'link_file_download',
sortOrder: 'desc'
}; };
this.initPage = 1; this.initPage = 1;
this.initMonth = moment().format('YYYYMM'); this.initMonth = moment().format('YYYYMM');
@ -35,16 +36,16 @@ class TrafficOrganizationsTable extends React.Component {
perPage: parseInt(urlParams.get('per_page') || perPage), perPage: parseInt(urlParams.get('per_page') || perPage),
currentPage: parseInt(urlParams.get('page') || currentPage) currentPage: parseInt(urlParams.get('page') || currentPage)
}, () => { }, () => {
this.onGenerateReports(this.initMonth, this.state.currentPage); this.getTrafficList(this.initMonth, this.state.currentPage);
}); });
} }
getPreviousPage = () => { getPreviousPage = () => {
this.onGenerateReports(this.state.month, this.state.currentPage - 1); this.getTrafficList(this.state.month, this.state.currentPage - 1);
} }
getNextPage = () => { getNextPage = () => {
this.onGenerateReports(this.state.month, this.state.currentPage + 1); this.getTrafficList(this.state.month, this.state.currentPage + 1);
} }
handleChange = (e) => { handleChange = (e) => {
@ -54,15 +55,6 @@ class TrafficOrganizationsTable extends React.Component {
}); });
} }
sortBySize = (sortByType, sortOrder) => {
let { userTrafficList } = this.state;
let newUserTrafficList = Utils.sortTraffic(userTrafficList, sortByType, sortOrder);
this.setState({
userTrafficList: newUserTrafficList,
sortOrder: sortOrder
});
}
handleKeyPress = (e) => { handleKeyPress = (e) => {
let { month } = this.state; let { month } = this.state;
if (e.key === 'Enter') { if (e.key === 'Enter') {
@ -74,21 +66,24 @@ class TrafficOrganizationsTable extends React.Component {
}); });
return; return;
} }
this.onGenerateReports(month, this.initPage); this.getTrafficList(month, this.initPage);
e.target.blur(); e.target.blur();
e.preventDefault(); e.preventDefault();
} }
} }
onGenerateReports = (month, page) => { getTrafficList = (month, page) => {
let { perPage } = this.state; const { perPage, sortBy, sortOrder } = this.state;
const orderBy = `${sortBy}_${sortOrder}`;
this.setState({ this.setState({
isLoading: true, isLoading: true,
errorMessage: '' errorMessage: ''
}); });
seafileAPI.sysAdminListUserTraffic(month, page, perPage).then(res => { seafileAPI.sysAdminListUserTraffic(month, page, perPage, orderBy).then(res => {
let userTrafficList = res.data.user_monthly_traffic_list.slice(0); let userTrafficList = res.data.user_monthly_traffic_list.slice(0);
this.setState({ this.setState({
month: month,
currentPage: page,
userTrafficList: userTrafficList, userTrafficList: userTrafficList,
hasNextPage: res.data.has_next_page, hasNextPage: res.data.has_next_page,
isLoading: false isLoading: false
@ -99,14 +94,28 @@ class TrafficOrganizationsTable extends React.Component {
}); });
} }
sortItems = (sortBy) => {
this.setState({
sortBy: sortBy,
sortOrder: this.state.sortOrder == 'asc' ? 'desc' : 'asc'
}, () => {
const { month, currentPage } = this.state;
this.getTrafficList(month, currentPage);
});
}
resetPerPage = (newPerPage) => { resetPerPage = (newPerPage) => {
this.setState({ this.setState({
perPage: newPerPage, perPage: newPerPage,
}, () => this.onGenerateReports(this.initMonth, this.initPage)); }, () => this.getTrafficList(this.initMonth, this.initPage));
} }
render() { render() {
let { userTrafficList, currentPage, hasNextPage, perPage, isLoading, errorMessage, sortOrder } = this.state; const {
isLoading, errorMessage, userTrafficList,
currentPage, hasNextPage, perPage,
sortBy, sortOrder
} = this.state;
return ( return (
<Fragment> <Fragment>
<div className="d-flex align-items-center mt-4"> <div className="d-flex align-items-center mt-4">
@ -121,7 +130,7 @@ class TrafficOrganizationsTable extends React.Component {
</div> </div>
{isLoading && <Loading />} {isLoading && <Loading />}
{!isLoading && {!isLoading &&
<TrafficTable type={'user'} sortBySize={this.sortBySize} sortOrder={sortOrder}> <TrafficTable type={'user'} sortItems={this.sortItems} sortBy={sortBy} sortOrder={sortOrder}>
{userTrafficList.length > 0 && userTrafficList.map((item, index) => { {userTrafficList.length > 0 && userTrafficList.map((item, index) => {
return( return(
<TrafficTableBody <TrafficTableBody
@ -146,4 +155,4 @@ class TrafficOrganizationsTable extends React.Component {
} }
} }
export default TrafficOrganizationsTable; export default UsersTraffic;

View File

@ -6,8 +6,8 @@ import MainPanelTopbar from '../main-panel-topbar';
import StatisticNav from './statistic-nav'; import StatisticNav from './statistic-nav';
import StatisticCommonTool from './statistic-common-tool'; import StatisticCommonTool from './statistic-common-tool';
import Loading from '../../../components/loading'; import Loading from '../../../components/loading';
import TrafficOrganizationsTable from './traffic-organizations-table'; import OrgsTraffic from './statistic-traffic-orgs';
import TrafficUserTable from './traffic-user-table'; import UsersTraffic from './statistic-traffic-users';
import StatisticChart from './statistic-chart'; import StatisticChart from './statistic-chart';
import { Utils } from '../../../utils/utils'; import { Utils } from '../../../utils/utils';
import toaster from '../../../components/toast'; import toaster from '../../../components/toast';
@ -204,10 +204,10 @@ class StatisticTraffic extends React.Component {
</div> </div>
} }
{!isLoading && tabActive === 'user' && {!isLoading && tabActive === 'user' &&
<TrafficUserTable /> <UsersTraffic />
} }
{!isLoading && tabActive === 'organizations' && {!isLoading && tabActive === 'organizations' &&
<TrafficOrganizationsTable /> <OrgsTraffic />
} }
</div> </div>
</div> </div>

View File

@ -4,6 +4,9 @@ import { gettext } from '../../../utils/constants';
const propTypes = { const propTypes = {
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
sortBy: PropTypes.string.isRequired,
sortOrder: PropTypes.string.isRequired,
sortItems: PropTypes.func.isRequired,
children: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), children: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]),
}; };
@ -11,30 +14,10 @@ class TrafficTable extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {
showIconName: 'link_file_download'
};
}
componentDidMount() {
let { showIconName } = this.state;
let { sortOrder } = this.props;
this.props.sortBySize(showIconName, sortOrder);
}
sortBySize = (sortByType) => {
let { sortOrder } = this.props;
let newSortOrder = sortOrder === 'asc' ? 'desc' : 'asc';
this.setState({
showIconName: sortByType
});
this.props.sortBySize(sortByType, newSortOrder);
} }
render() { render() {
const { type, sortOrder } = this.props; const { type, sortBy, sortOrder } = this.props;
const { showIconName } = this.state;
const sortIcon = sortOrder == 'asc' ? <span className="fas fa-caret-up"></span> : <span className="fas fa-caret-down"></span>; const sortIcon = sortOrder == 'asc' ? <span className="fas fa-caret-up"></span> : <span className="fas fa-caret-down"></span>;
return ( return (
@ -42,12 +25,12 @@ class TrafficTable extends React.Component {
<thead> <thead>
<tr> <tr>
<th width="16%">{type == 'user' ? gettext('User') : gettext('Organization')}</th> <th width="16%">{type == 'user' ? gettext('User') : gettext('Organization')}</th>
<th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'sync_file_upload')}>{gettext('Sync Upload')} {showIconName === 'sync_file_upload' && sortIcon}</div></th> <th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'sync_file_upload')}>{gettext('Sync Upload')} {sortBy === 'sync_file_upload' && sortIcon}</div></th>
<th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'sync_file_donwload')}>{gettext('Sync Download')} {showIconName === 'sync_file_donwload' && sortIcon}</div></th> <th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'sync_file_download')}>{gettext('Sync Download')} {sortBy === 'sync_file_download' && sortIcon}</div></th>
<th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'web_file_upload')}>{gettext('Web Upload')} {showIconName === 'web_file_upload' && sortIcon}</div></th> <th width="11%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'web_file_upload')}>{gettext('Web Upload')} {sortBy === 'web_file_upload' && sortIcon}</div></th>
<th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'web_file_download')}>{gettext('Web Download')} {showIconName === 'web_file_download' && sortIcon}</div></th> <th width="14%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'web_file_download')}>{gettext('Web Download')} {sortBy === 'web_file_download' && sortIcon}</div></th>
<th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'link_file_upload')}>{gettext('Share link upload')} {showIconName === 'link_file_upload' && sortIcon}</div></th> <th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'link_file_upload')}>{gettext('Share link upload')} {sortBy === 'link_file_upload' && sortIcon}</div></th>
<th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.sortBySize.bind(this, 'link_file_download')}>{gettext('Share link download')} {showIconName === 'link_file_download' && sortIcon}</div></th> <th width="17%"><div className="d-block table-sort-op cursor-pointer" onClick={this.props.sortItems.bind(this, 'link_file_download')}>{gettext('Share link download')} {sortBy === 'link_file_download' && sortIcon}</div></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -1042,24 +1042,6 @@ export const Utils = {
return items; return items;
}, },
sortTraffic(items, sortBy, sortOrder) {
let comparator;
switch(sortOrder) {
case 'asc':
comparator = function(a, b) {
return a[sortBy] < b[sortBy] ? -1 : 1;
};
break;
case 'desc':
comparator = function(a, b) {
return a[sortBy] < b[sortBy] ? 1 : -1;
};
break;
}
items.sort(comparator);
return items;
},
/* /*
* only used in the 'catch' part of a seafileAPI request * only used in the 'catch' part of a seafileAPI request
*/ */

View File

@ -11,7 +11,7 @@ openpyxl
qrcode qrcode
django-formtools django-formtools
django-simple-captcha django-simple-captcha
djangorestframework djangorestframework==3.11.1
python-dateutil python-dateutil
requests requests
pillow pillow

View File

@ -310,40 +310,55 @@ class AdminLogsSharePermissionLogs(APIView):
to_nickname_dict = {} to_nickname_dict = {}
to_contact_email_dict = {} to_contact_email_dict = {}
repo_dict = {} repo_dict = {}
to_group_name_dict = {}
from_user_email_set = set() from_user_email_set = set()
to_user_email_set = set() to_user_email_set = set()
repo_id_set = set() repo_id_set = set()
to_group_id_set = set()
department_set = set()
for event in events: for event in events:
from_user_email_set.add(event.from_user) from_user_email_set.add(event.from_user)
to_user_email_set.add(event.to)
repo_id_set.add(event.repo_id) repo_id_set.add(event.repo_id)
if is_valid_email(event.to):
to_user_email_set.add(event.to)
if event.to.isdigit():
to_group_id_set.add(event.to)
for e in from_user_email_set: for e in from_user_email_set:
if e not in from_nickname_dict: if e not in from_nickname_dict:
from_nickname_dict[e] = email2nickname(e) from_nickname_dict[e] = email2nickname(e)
if e not in from_contact_email_dict: if e not in from_contact_email_dict:
from_contact_email_dict[e] = email2contact_email(e) from_contact_email_dict[e] = email2contact_email(e)
for e in to_user_email_set: for e in to_user_email_set:
if e not in to_nickname_dict: if e not in to_nickname_dict:
to_nickname_dict[e] = email2nickname(e) to_nickname_dict[e] = email2nickname(e)
if e not in to_contact_email_dict: if e not in to_contact_email_dict:
to_contact_email_dict[e] = email2contact_email(e) to_contact_email_dict[e] = email2contact_email(e)
for e in repo_id_set: for e in repo_id_set:
if e not in repo_dict: if e not in repo_dict:
repo_dict[e] = seafile_api.get_repo(e) repo_dict[e] = seafile_api.get_repo(e)
for group_id in to_group_id_set:
if group_id not in to_group_name_dict:
group = ccnet_api.get_group(int(group_id))
to_group_name_dict[group_id] = group.group_name
if group.parent_group_id != 0:
department_set.add(group_id)
events_info = [] events_info = []
for ev in events: for ev in events:
data = {} data = {}
from_user_email = ev.from_user from_user_email = ev.from_user
to_user_email = ev.to
data['from_user_email'] = from_user_email data['from_user_email'] = from_user_email
data['from_user_name'] = from_nickname_dict.get(from_user_email, '') data['from_user_name'] = from_nickname_dict.get(from_user_email, '')
data['from_user_contact_email'] = from_contact_email_dict.get(from_user_email, '') data['from_user_contact_email'] = from_contact_email_dict.get(from_user_email, '')
data['to_user_email'] = to_user_email
data['to_user_name'] = to_nickname_dict.get(to_user_email, '')
data['to_user_contact_email'] = to_contact_email_dict.get(to_user_email, '')
data['etype'] = ev.etype data['etype'] = ev.etype
data['permission'] = ev.permission data['permission'] = ev.permission
@ -355,6 +370,33 @@ class AdminLogsSharePermissionLogs(APIView):
data['folder'] = '/' if ev.file_path == '/' else os.path.basename(ev.file_path.rstrip('/')) data['folder'] = '/' if ev.file_path == '/' else os.path.basename(ev.file_path.rstrip('/'))
data['date'] = utc_datetime_to_isoformat_timestr(ev.timestamp) data['date'] = utc_datetime_to_isoformat_timestr(ev.timestamp)
data['share_type'] = 'all'
data['to_user_email'] = ''
data['to_user_name'] = ''
data['to_user_contact_email'] = ''
data['to_group_id'] = ''
data['to_group_name'] = ''
if is_valid_email(ev.to):
to_user_email = ev.to
data['to_user_email'] = to_user_email
data['to_user_name'] = to_nickname_dict.get(to_user_email, '')
data['to_user_contact_email'] = to_contact_email_dict.get(to_user_email, '')
data['share_type'] = 'user'
if ev.to.isdigit():
to_group_id = ev.to
data['to_group_id'] = to_group_id
data['to_group_name'] = to_group_name_dict.get(to_group_id, '')
if to_group_id in department_set:
data['share_type'] = 'department'
else:
data['share_type'] = 'group'
events_info.append(data) events_info.append(data)
resp = { resp = {

View File

@ -241,9 +241,22 @@ class SystemUserTrafficView(APIView):
per_page = 25 per_page = 25
start = (page - 1) * per_page start = (page - 1) * per_page
order_by = request.GET.get('order_by', '')
filters = [
'sync_file_upload', 'sync_file_download',
'web_file_upload', 'web_file_download',
'link_file_upload', 'link_file_download',
]
if order_by not in filters and \
order_by not in map(lambda x: x + '_desc', filters):
order_by = 'link_file_download_desc'
# get one more item than per_page, to judge has_next_page # get one more item than per_page, to judge has_next_page
try: try:
traffics = seafevents_api.get_all_users_traffic_by_month(month_obj, start, start + per_page + 1) traffics = seafevents_api.get_all_users_traffic_by_month(month_obj,
start,
start + per_page + 1,
order_by)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'
@ -305,9 +318,22 @@ class SystemOrgTrafficView(APIView):
per_page = 25 per_page = 25
start = (page - 1) * per_page start = (page - 1) * per_page
order_by = request.GET.get('order_by', '')
filters = [
'sync_file_upload', 'sync_file_download',
'web_file_upload', 'web_file_download',
'link_file_upload', 'link_file_download',
]
if order_by not in filters and \
order_by not in map(lambda x: x + '_desc', filters):
order_by = 'link_file_download_desc'
# get one more item than per_page, to judge has_next_page # get one more item than per_page, to judge has_next_page
try: try:
traffics = seafevents_api.get_all_orgs_traffic_by_month(month_obj, start, start + per_page + 1) traffics = seafevents_api.get_all_orgs_traffic_by_month(month_obj,
start,
start + per_page + 1,
order_by)
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'

View File

@ -849,7 +849,7 @@ class AdminUser(APIView):
login_id = login_id.strip() login_id = login_id.strip()
username_by_login_id = Profile.objects.get_username_by_login_id(login_id) username_by_login_id = Profile.objects.get_username_by_login_id(login_id)
if username_by_login_id is not None: if username_by_login_id is not None:
return api_error(status.HTTP_400_BAD_REQUEST, return api_error(status.HTTP_400_BAD_REQUEST,
_("Login id %s already exists." % login_id)) _("Login id %s already exists." % login_id))
contact_email = request.data.get("contact_email", None) contact_email = request.data.get("contact_email", None)
@ -860,7 +860,7 @@ class AdminUser(APIView):
password = request.data.get("password") password = request.data.get("password")
reference_id = request.data.get("reference_id", "") reference_id = request.data.get("reference_id", None)
if reference_id: if reference_id:
if ' ' in reference_id: if ' ' in reference_id:
return api_error(status.HTTP_400_BAD_REQUEST, 'Reference ID can not contain spaces.') return api_error(status.HTTP_400_BAD_REQUEST, 'Reference ID can not contain spaces.')

View File

@ -0,0 +1,23 @@
# Copyright (c) 2012-2016 Seafile Ltd.
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import get_token_v1
class AuthTokenBySession(APIView):
""" Get user's auth token.
"""
authentication_classes = (SessionAuthentication,)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)
def get(self, request):
token = get_token_v1(request.user.username)
return Response({'token': token.key})

View File

@ -9,7 +9,7 @@ from rest_framework import status
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from seaserv import seafile_api from seaserv import seafile_api, ccnet_api
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
@ -17,14 +17,15 @@ from seahub.api2.utils import api_error
from seahub.api2.views import HTTP_520_OPERATION_FAILED from seahub.api2.views import HTTP_520_OPERATION_FAILED
from seahub.utils import IS_EMAIL_CONFIGURED, send_html_email from seahub.utils import IS_EMAIL_CONFIGURED, send_html_email
from seahub.utils.repo import is_repo_owner from seahub.utils.repo import get_repo_owner
from seahub.base.models import RepoSecretKey from seahub.base.models import RepoSecretKey
from seahub.base.templatetags.seahub_tags import email2contact_email from seahub.base.templatetags.seahub_tags import email2contact_email, email2nickname
from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RepoSendNewPassword(APIView): class RepoSendNewPassword(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
@ -55,12 +56,21 @@ class RepoSendNewPassword(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# permission check # permission check
username = request.user.username
if not is_repo_owner(request, repo_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
secret_key = RepoSecretKey.objects.get_secret_key(repo_id) username = request.user.username
repo_owner = get_repo_owner(request, repo_id)
if '@seafile_group' in repo_owner:
group_id = email2nickname(repo_owner)
if not ccnet_api.check_group_staff(int(group_id), username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != repo_owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
secret_key = RepoSecretKey.objects.get_secret_key(repo_id)
if not secret_key: if not secret_key:
error_msg = _("Can not reset this library's password.") error_msg = _("Can not reset this library's password.")
return api_error(HTTP_520_OPERATION_FAILED, error_msg) return api_error(HTTP_520_OPERATION_FAILED, error_msg)
@ -68,10 +78,10 @@ class RepoSendNewPassword(APIView):
new_password = get_random_string(10) new_password = get_random_string(10)
try: try:
seafile_api.reset_repo_passwd(repo_id, username, secret_key, new_password) seafile_api.reset_repo_passwd(repo_id, username, secret_key, new_password)
content = {'repo_name': repo.name, 'password': new_password,} content = {'repo_name': repo.name, 'password': new_password}
send_html_email(_('New password of library %s') % repo.name, send_html_email(_('New password of library %s') % repo.name,
'snippets/reset_repo_password.html', content, 'snippets/reset_repo_password.html', content,
None, [email2contact_email(username)]) None, [email2contact_email(username)])
except Exception as e: except Exception as e:
logger.error(e) logger.error(e)
error_msg = 'Internal Server Error' error_msg = 'Internal Server Error'

View File

@ -8,15 +8,17 @@ from rest_framework.views import APIView
from rest_framework import status from rest_framework import status
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from seaserv import seafile_api from seaserv import seafile_api, ccnet_api
from pysearpc import SearpcError from pysearpc import SearpcError
from seahub.api2.authentication import TokenAuthentication from seahub.api2.authentication import TokenAuthentication
from seahub.api2.throttling import UserRateThrottle from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error from seahub.api2.utils import api_error
from seahub.utils.repo import is_repo_owner, add_encrypted_repo_secret_key_to_database from seahub.utils.repo import is_repo_owner, get_repo_owner, \
add_encrypted_repo_secret_key_to_database
from seahub.base.models import RepoSecretKey from seahub.base.models import RepoSecretKey
from seahub.views import check_folder_permission from seahub.views import check_folder_permission
from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD from seahub.settings import ENABLE_RESET_ENCRYPTED_REPO_PASSWORD
@ -103,10 +105,19 @@ class RepoSetPassword(APIView):
return api_error(status.HTTP_400_BAD_REQUEST, error_msg) return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# permission check # permission check
username = request.user.username username = request.user.username
if not is_repo_owner(request, repo_id, username): repo_owner = get_repo_owner(request, repo_id)
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg) if '@seafile_group' in repo_owner:
group_id = email2nickname(repo_owner)
if not ccnet_api.check_group_staff(int(group_id), username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != repo_owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if operation == 'change-password': if operation == 'change-password':

View File

@ -10,38 +10,45 @@ from django.utils.encoding import force_bytes
from seaserv import seafile_api 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, \ 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, normalize_cache_key
from seahub.settings import ENABLE_WATERMARK from seahub.settings import ENABLE_WATERMARK
from seahub.onlyoffice.settings import ONLYOFFICE_APIJS_URL, \ from seahub.onlyoffice.settings import ONLYOFFICE_APIJS_URL, \
ONLYOFFICE_FORCE_SAVE ONLYOFFICE_FORCE_SAVE, ONLYOFFICE_JWT_SECRET
def generate_onlyoffice_cache_key(repo_id, file_path): def generate_onlyoffice_cache_key(repo_id, file_path):
prefix = "ONLYOFFICE_" prefix = "ONLYOFFICE_"
value = "%s_%s" % (repo_id, file_path) value = "%s_%s" % (repo_id, file_path)
return normalize_cache_key(value, prefix) return normalize_cache_key(value, prefix)
def get_onlyoffice_dict(username, repo_id, file_path,
file_id='', can_edit=False, can_download=True): def get_onlyoffice_dict(request, username, repo_id, file_path, file_id='',
can_edit=False, can_download=True):
repo = seafile_api.get_repo(repo_id) repo = seafile_api.get_repo(repo_id)
if repo.is_virtual: if repo.is_virtual:
origin_repo_id = repo.origin_repo_id origin_repo_id = repo.origin_repo_id
origin_file_path = posixpath.join(repo.origin_path, file_path.strip('/')) origin_file_path = posixpath.join(repo.origin_path,
file_path.strip('/'))
# for view history/trash/snapshot file # for view history/trash/snapshot file
if not file_id: if not file_id:
file_id = seafile_api.get_file_id_by_path(origin_repo_id, file_id = seafile_api.get_file_id_by_path(origin_repo_id,
origin_file_path) origin_file_path)
else: else:
origin_repo_id = repo_id origin_repo_id = repo_id
origin_file_path = file_path origin_file_path = file_path
if not file_id: if not file_id:
file_id = seafile_api.get_file_id_by_path(repo_id, file_id = seafile_api.get_file_id_by_path(repo_id,
file_path) file_path)
dl_token = seafile_api.get_fileserver_access_token(repo_id, dl_token = seafile_api.get_fileserver_access_token(repo_id,
file_id, 'download', username, use_onetime=True) file_id,
'download',
username,
use_onetime=True)
if not dl_token: if not dl_token:
return None return None
@ -62,9 +69,12 @@ def get_onlyoffice_dict(username, repo_id, file_path,
doc_key = cache.get(cache_key) doc_key = cache.get(cache_key)
if not doc_key: if not doc_key:
doc_key = hashlib.md5(force_bytes(origin_repo_id + origin_file_path + file_id)).hexdigest()[:20] info_bytes = force_bytes(origin_repo_id + origin_file_path + file_id)
doc_key = hashlib.md5(info_bytes).hexdigest()[:20]
doc_info = json.dumps({'repo_id': repo_id, 'file_path': file_path, 'username': username}) doc_info = json.dumps({'repo_id': repo_id,
'file_path': file_path,
'username': username})
cache.set("ONLYOFFICE_%s" % doc_key, doc_info, None) cache.set("ONLYOFFICE_%s" % doc_key, doc_info, None)
file_name = os.path.basename(file_path.rstrip('/')) file_name = os.path.basename(file_path.rstrip('/'))
@ -72,7 +82,8 @@ def get_onlyoffice_dict(username, repo_id, file_path,
base_url = get_site_scheme_and_netloc() base_url = get_site_scheme_and_netloc()
onlyoffice_editor_callback_url = reverse('onlyoffice_editor_callback') onlyoffice_editor_callback_url = reverse('onlyoffice_editor_callback')
calllback_url = urllib.parse.urljoin(base_url, onlyoffice_editor_callback_url) callback_url = urllib.parse.urljoin(base_url,
onlyoffice_editor_callback_url)
return_dict = { return_dict = {
'repo_id': repo_id, 'repo_id': repo_id,
@ -83,7 +94,7 @@ def get_onlyoffice_dict(username, repo_id, file_path,
'doc_title': file_name, 'doc_title': file_name,
'doc_url': doc_url, 'doc_url': doc_url,
'document_type': document_type, 'document_type': document_type,
'callback_url': calllback_url, 'callback_url': callback_url,
'can_edit': can_edit, 'can_edit': can_edit,
'can_download': can_download, 'can_download': can_download,
'username': username, 'username': username,
@ -91,4 +102,36 @@ def get_onlyoffice_dict(username, repo_id, file_path,
'enable_watermark': ENABLE_WATERMARK and not can_edit, 'enable_watermark': ENABLE_WATERMARK and not can_edit,
} }
if ONLYOFFICE_JWT_SECRET:
import jwt
config = {
"document": {
"fileType": fileext,
"key": doc_key,
"title": file_name,
"url": doc_url,
"permissions": {
"download": can_download,
"edit": can_edit,
"print": can_download,
"review": True
}
},
"documentType": document_type,
"editorConfig": {
"callbackUrl": callback_url,
"lang": request.LANGUAGE_CODE,
"mode": can_edit,
"customization": {
"forcesave": ONLYOFFICE_FORCE_SAVE,
},
"user": {
"name": email2nickname(username)
}
}
}
return_dict['onlyoffice_jwt_token'] = jwt.encode(config,
ONLYOFFICE_JWT_SECRET)
return return_dict return return_dict

View File

@ -84,6 +84,7 @@ from seahub.api2.endpoints.activities import ActivitiesView
from seahub.api2.endpoints.wiki_pages import WikiPagesDirView, WikiPageContentView from seahub.api2.endpoints.wiki_pages import WikiPagesDirView, WikiPageContentView
from seahub.api2.endpoints.revision_tag import TaggedItemsView, TagNamesView from seahub.api2.endpoints.revision_tag import TaggedItemsView, TagNamesView
from seahub.api2.endpoints.user import User from seahub.api2.endpoints.user import User
from seahub.api2.endpoints.auth_token_by_session import AuthTokenBySession
from seahub.api2.endpoints.repo_tags import RepoTagsView, RepoTagView from seahub.api2.endpoints.repo_tags import RepoTagsView, RepoTagView
from seahub.api2.endpoints.file_tag import RepoFileTagsView, RepoFileTagView from seahub.api2.endpoints.file_tag import RepoFileTagsView, RepoFileTagView
from seahub.api2.endpoints.tag_filter_file import TaggedFilesView from seahub.api2.endpoints.tag_filter_file import TaggedFilesView
@ -278,6 +279,9 @@ urlpatterns = [
## user ## user
url(r'^api/v2.1/user/$', User.as_view(), name="api-v2.1-user"), url(r'^api/v2.1/user/$', User.as_view(), name="api-v2.1-user"),
## obtain auth token by login session
url(r'^api/v2.1/auth-token-by-session/$', AuthTokenBySession.as_view(), name="api-v2.1-auth-token-by-session"),
## user::smart-link ## user::smart-link
url(r'^api/v2.1/smart-link/$', SmartLink.as_view(), name="api-v2.1-smart-link"), url(r'^api/v2.1/smart-link/$', SmartLink.as_view(), name="api-v2.1-smart-link"),
url(r'^api/v2.1/smart-links/(?P<token>[-0-9a-f]{36})/$', SmartLinkToken.as_view(), name="api-v2.1-smart-links-token"), url(r'^api/v2.1/smart-links/(?P<token>[-0-9a-f]{36})/$', SmartLinkToken.as_view(), name="api-v2.1-smart-links-token"),

View File

@ -9,7 +9,7 @@ import logging
import posixpath import posixpath
from django.core.cache import cache from django.core.cache import cache
from django.urls import reverse from django.urls import reverse, resolve
from django.contrib import messages from django.contrib import messages
from django.http import HttpResponse, Http404, \ from django.http import HttpResponse, Http404, \
HttpResponseRedirect HttpResponseRedirect
@ -1120,10 +1120,47 @@ def choose_register(request):
'login_bg_image_path': login_bg_image_path 'login_bg_image_path': login_bg_image_path
}) })
@login_required @login_required
def react_fake_view(request, **kwargs): def react_fake_view(request, **kwargs):
username = request.user.username username = request.user.username
if resolve(request.path).url_name == 'lib_view':
repo_id = kwargs.get('repo_id', '')
path = kwargs.get('path', '')
if repo_id and path and \
not check_folder_permission(request, repo_id, path):
converted_repo_path = seafile_api.convert_repo_path(repo_id, path, username)
if not converted_repo_path:
error_msg = 'Permission denied.'
return render_error(request, error_msg)
repo_path_dict = json.loads(converted_repo_path)
converted_repo_id = repo_path_dict['repo_id']
converted_repo = seafile_api.get_repo(converted_repo_id)
if not converted_repo:
error_msg = 'Library %s not found.' % converted_repo_id
return render_error(request, error_msg)
converted_path = repo_path_dict['path']
if not seafile_api.get_dirent_by_path(converted_repo_id, converted_path):
error_msg = 'Dirent %s not found.' % converted_path
return render_error(request, error_msg)
if not check_folder_permission(request, converted_repo_id, converted_path):
error_msg = 'Permission denied.'
return render_error(request, error_msg)
next_url = reverse('lib_view', args=[converted_repo_id,
converted_repo.repo_name,
converted_path.strip('/')])
return HttpResponseRedirect(next_url)
guide_enabled = UserOptions.objects.is_user_guide_enabled(username) guide_enabled = UserOptions.objects.is_user_guide_enabled(username)
if guide_enabled: if guide_enabled:
create_default_library(request) create_default_library(request)
@ -1165,9 +1202,9 @@ def react_fake_view(request, **kwargs):
'is_email_configured': IS_EMAIL_CONFIGURED, 'is_email_configured': IS_EMAIL_CONFIGURED,
'can_add_public_repo': request.user.permissions.can_add_public_repo(), 'can_add_public_repo': request.user.permissions.can_add_public_repo(),
'folder_perm_enabled': folder_perm_enabled, 'folder_perm_enabled': folder_perm_enabled,
'file_audit_enabled' : FILE_AUDIT_ENABLED, 'file_audit_enabled': FILE_AUDIT_ENABLED,
'custom_nav_items' : json.dumps(CUSTOM_NAV_ITEMS), 'custom_nav_items': json.dumps(CUSTOM_NAV_ITEMS),
'enable_show_contact_email_when_search_user' : settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER, 'enable_show_contact_email_when_search_user': settings.ENABLE_SHOW_CONTACT_EMAIL_WHEN_SEARCH_USER,
'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE, 'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS, 'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS, 'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,

View File

@ -125,11 +125,6 @@ try:
except ImportError: except ImportError:
ONLYOFFICE_EDIT_FILE_EXTENSION = () ONLYOFFICE_EDIT_FILE_EXTENSION = ()
try:
from seahub.onlyoffice.settings import ONLYOFFICE_JWT_SECRET
except ImportError:
ONLYOFFICE_JWT_SECRET = ''
# bisheng office # bisheng office
from seahub.bisheng_office.utils import get_bisheng_dict, \ from seahub.bisheng_office.utils import get_bisheng_dict, \
get_bisheng_editor_url, get_bisheng_preivew_url get_bisheng_editor_url, get_bisheng_preivew_url
@ -790,9 +785,8 @@ def view_lib_file(request, repo_id, path):
(is_locked and locked_by_online_office)): (is_locked and locked_by_online_office)):
can_edit = True can_edit = True
onlyoffice_dict = get_onlyoffice_dict(username, repo_id, path, onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, path,
can_edit=can_edit, can_edit=can_edit, can_download=parse_repo_perm(permission).can_download)
can_download=parse_repo_perm(permission).can_download)
if onlyoffice_dict: if onlyoffice_dict:
if is_pro_version() and can_edit: if is_pro_version() and can_edit:
@ -806,36 +800,6 @@ def view_lib_file(request, repo_id, path):
send_file_access_msg(request, repo, path, 'web') send_file_access_msg(request, repo, path, 'web')
if ONLYOFFICE_JWT_SECRET:
import jwt
config = {
"document": {
"fileType": onlyoffice_dict['file_type'],
"key": onlyoffice_dict['doc_key'],
"title": onlyoffice_dict['doc_title'],
"url": onlyoffice_dict['doc_url'],
"permissions": {
"download": onlyoffice_dict['can_download'],
"edit": onlyoffice_dict['can_edit'],
"print": onlyoffice_dict['can_download'],
"review": True
}
},
"documentType": onlyoffice_dict['document_type'],
"editorConfig": {
"callbackUrl": onlyoffice_dict['callback_url'],
"lang": request.LANGUAGE_CODE,
"mode": onlyoffice_dict['can_edit'],
"customization": {
"forcesave": onlyoffice_dict['onlyoffice_force_save'],
},
"user": {
"name": email2nickname(username)
}
}
};
onlyoffice_dict['onlyoffice_jwt_token'] = jwt.encode(config, ONLYOFFICE_JWT_SECRET)
return render(request, 'view_file_onlyoffice.html', onlyoffice_dict) return render(request, 'view_file_onlyoffice.html', onlyoffice_dict)
else: else:
return_dict['err'] = _('Error when prepare OnlyOffice file preview page.') return_dict['err'] = _('Error when prepare OnlyOffice file preview page.')
@ -944,7 +908,7 @@ def view_history_file_common(request, repo_id, ret_dict):
if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION: if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION:
onlyoffice_dict = get_onlyoffice_dict(username, repo_id, path, onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, path,
file_id=obj_id, can_download=parse_repo_perm(user_perm).can_download) file_id=obj_id, can_download=parse_repo_perm(user_perm).can_download)
if onlyoffice_dict: if onlyoffice_dict:
@ -1230,7 +1194,7 @@ def view_shared_file(request, fileshare):
if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION: if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION:
onlyoffice_dict = get_onlyoffice_dict(username, repo_id, path, onlyoffice_dict = get_onlyoffice_dict(request, username, repo_id, path,
can_edit=can_edit, can_download=can_download) can_edit=can_edit, can_download=can_download)
if onlyoffice_dict: if onlyoffice_dict:
@ -1416,7 +1380,7 @@ def view_file_via_shared_dir(request, fileshare):
if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION: if ENABLE_ONLYOFFICE and fileext in ONLYOFFICE_FILE_EXTENSION:
onlyoffice_dict = get_onlyoffice_dict(username, onlyoffice_dict = get_onlyoffice_dict(request, username,
repo_id, real_path) repo_id, real_path)
if onlyoffice_dict: if onlyoffice_dict: