mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-31 22:57:47 +00:00
Update transfer repo add audit log (#7480)
* add admin log * update * update * update * add org transfer log * optimize * Update file-transfer-log.js * add db_index * add-operator * update * file --> repo * Update mysql.sql * update * update --------- Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com> Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
parent
83d615d3ed
commit
d91cdad79e
29
frontend/src/models/org-logs-file-transfer.js
Normal file
29
frontend/src/models/org-logs-file-transfer.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { lang } from '../utils/constants';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
dayjs.locale(lang);
|
||||
|
||||
class OrgLogsFileTransferEvent {
|
||||
constructor(object) {
|
||||
this.from_group_id = object.from_group_id;
|
||||
this.from_group_name = object.from_group_name;
|
||||
this.from_type = object.from_type;
|
||||
this.from_user_contact_email = object.from_user_contact_email;
|
||||
this.from_user_email = object.from_user_email;
|
||||
this.from_user_name = object.from_user_name;
|
||||
this.repo_id = object.repo_id;
|
||||
this.repo_name = object.repo_name;
|
||||
this.to_group_id = object.to_group_id;
|
||||
this.to_group_name = object.to_group_name;
|
||||
this.to_type = object.to_type;
|
||||
this.to_user_contact_email = object.to_user_contact_email;
|
||||
this.to_user_email = object.to_user_email;
|
||||
this.to_user_name = object.to_user_name;
|
||||
this.operator_email = object.operator_email;
|
||||
this.operator_name = object.operator_name;
|
||||
this.operator_contact_email = object.operator_contact_email;
|
||||
this.time = dayjs(object.date).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
}
|
||||
|
||||
export default OrgLogsFileTransferEvent;
|
@ -37,6 +37,7 @@ import OrgLogs from './org-logs';
|
||||
import OrgLogsFileAudit from './org-logs-file-audit';
|
||||
import OrgLogsFileUpdate from './org-logs-file-update';
|
||||
import OrgLogsPermAudit from './org-logs-perm-audit';
|
||||
import OrgLogsFileTransfer from './org-logs-file-transfer';
|
||||
import OrgSAMLConfig from './org-saml-config';
|
||||
import OrgSubscription from './org-subscription';
|
||||
|
||||
@ -126,6 +127,7 @@ class Org extends React.Component {
|
||||
<OrgLogsFileAudit path='/' />
|
||||
<OrgLogsFileUpdate path='file-update' />
|
||||
<OrgLogsPermAudit path='perm-audit' />
|
||||
<OrgLogsFileTransfer path='repo-transfer' />
|
||||
</OrgLogs>
|
||||
{enableMultiADFS &&
|
||||
<OrgSAMLConfig path={siteRoot + 'org/samlconfig/'}/>
|
||||
|
@ -215,7 +215,7 @@ class RepoItem extends React.Component {
|
||||
onTransferRepo = (email, reshare) => {
|
||||
let repo = this.props.repo;
|
||||
orgAdminAPI.orgAdminTransferOrgRepo(orgID, repo.repoID, email, reshare).then(res => {
|
||||
const { owner_name, owner_email } = res;
|
||||
const { owner_name, owner_email } = res.data;
|
||||
this.props.transferRepoItem(repo.repoID, owner_name, owner_email);
|
||||
let msg = gettext('Successfully transferred the library.');
|
||||
toaster.success(msg);
|
||||
|
178
frontend/src/pages/org-admin/org-logs-file-transfer.js
Normal file
178
frontend/src/pages/org-admin/org-logs-file-transfer.js
Normal file
@ -0,0 +1,178 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import dayjs from 'dayjs';
|
||||
import { orgAdminAPI } from '../../utils/org-admin-api';
|
||||
import { siteRoot, gettext, lang } from '../../utils/constants';
|
||||
import { Utils } from '../../utils/utils';
|
||||
import toaster from '../../components/toast';
|
||||
import OrgLogsFileTransferEvent from '../../models/org-logs-file-transfer';
|
||||
import '../../css/org-logs.css';
|
||||
import UserLink from './user-link';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
|
||||
dayjs.locale(lang);
|
||||
|
||||
class OrgLogsFileTransfer extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
page: 1,
|
||||
perPage: 25,
|
||||
pageNext: false,
|
||||
eventList: [],
|
||||
isItemFreezed: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let page = this.state.page;
|
||||
let perPage = this.state.perPage;
|
||||
this.initData(page, perPage);
|
||||
}
|
||||
|
||||
initData = (page, perPage) => {
|
||||
orgAdminAPI.orgAdminListFileTransfer(page, perPage).then(res => {
|
||||
let eventList = res.data.log_list.map(item => {
|
||||
return new OrgLogsFileTransferEvent(item);
|
||||
});
|
||||
|
||||
this.setState({
|
||||
eventList: eventList,
|
||||
pageNext: res.data.page_next,
|
||||
page: res.data.page,
|
||||
});
|
||||
}).catch(error => {
|
||||
let errMessage = Utils.getErrorMsg(error);
|
||||
toaster.danger(errMessage);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
onChangePageNum = (e, num) => {
|
||||
e.preventDefault();
|
||||
let page = this.state.page;
|
||||
let perPage = this.state.perPage;
|
||||
if (num == 1) {
|
||||
page = page + 1;
|
||||
} else {
|
||||
page = page - 1;
|
||||
}
|
||||
this.initData(page, perPage);
|
||||
};
|
||||
|
||||
render() {
|
||||
let eventList = this.state.eventList;
|
||||
return (
|
||||
<div className="cur-view-content">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">{gettext('Transfer From')}</th>
|
||||
<th width="20%">{gettext('Transfer To')}</th>
|
||||
<th width="20%">{gettext('Operator')}</th>
|
||||
<th width="25%">{gettext('Library')}</th>
|
||||
<th width="15%">{gettext('Date')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{eventList.map((item, index) => {
|
||||
return (
|
||||
<FileTransferItem
|
||||
key={index}
|
||||
fileEvent={item}
|
||||
isItemFreezed={this.state.isItemFreezed}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="paginator">
|
||||
{this.state.page != 1 && <a href="#" onClick={(e) => this.onChangePageNum(e, -1)}>{gettext('Previous')}</a>}
|
||||
{(this.state.page != 1 && this.state.pageNext) && <span> | </span>}
|
||||
{this.state.pageNext && <a href="#" onClick={(e) => this.onChangePageNum(e, 1)}>{gettext('Next')}</a>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const propTypes = {
|
||||
isItemFreezed: PropTypes.bool.isRequired,
|
||||
fileEvent: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class FileTransferItem 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
getTransferTo = (item) => {
|
||||
switch (item.to_type) {
|
||||
case 'user':
|
||||
return <UserLink email={item.to_user_email} name={item.to_user_name} />;
|
||||
case 'group':
|
||||
return <Link to={`${siteRoot}org/groupadmin/${item.to_group_id}/`}>{item.to_group_name}</Link>;
|
||||
default:
|
||||
return gettext('Deleted');
|
||||
}
|
||||
};
|
||||
|
||||
getTransferFrom = (item) => {
|
||||
switch (item.from_type) {
|
||||
case 'user':
|
||||
return <UserLink email={item.from_user_email} name={item.from_user_name} />;
|
||||
case 'group':
|
||||
return <Link to={`${siteRoot}org/groupadmin/${item.from_group_id}/`}>{item.from_group_name}</Link>;
|
||||
default:
|
||||
return gettext('Deleted');
|
||||
}
|
||||
};
|
||||
|
||||
getOperator = (item) => {
|
||||
return <UserLink email={item.operator_email} name={item.operator_name} />;
|
||||
};
|
||||
|
||||
render() {
|
||||
let { fileEvent } = this.props;
|
||||
return (
|
||||
<tr className={this.state.highlight ? 'tr-highlight' : ''}
|
||||
onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
<td>{this.getTransferFrom(fileEvent)}</td>
|
||||
<td>{this.getTransferTo(fileEvent)}</td>
|
||||
<td>{this.getOperator(fileEvent)}</td>
|
||||
<td>{fileEvent.repo_name ? fileEvent.repo_name : gettext('Deleted')}</td>
|
||||
<td>{dayjs(fileEvent.time).fromNow()}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileTransferItem.propTypes = propTypes;
|
||||
|
||||
export default OrgLogsFileTransfer;
|
@ -40,9 +40,13 @@ class OrgLogs extends Component {
|
||||
const { isExportExcelDialogOpen, logType } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar>
|
||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
|
||||
</MainPanelTopbar>
|
||||
{this.props.currentTab === 'repo-transfer' ?
|
||||
<MainPanelTopbar />
|
||||
:
|
||||
<MainPanelTopbar>
|
||||
<Button className="btn btn-secondary operation-item" onClick={this.toggleExportExcelDialog}>{gettext('Export Excel')}</Button>
|
||||
</MainPanelTopbar>
|
||||
}
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container h-100">
|
||||
<div className="cur-view-path org-user-nav">
|
||||
@ -65,6 +69,12 @@ class OrgLogs extends Component {
|
||||
to={siteRoot + 'org/logadmin/perm-audit/'} title={gettext('Permission')}>{gettext('Permission')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav-item" onClick={() => this.tabItemClick('repo-transfer')}>
|
||||
<Link
|
||||
className={`nav-link ${this.props.currentTab === 'repo-transfer' ? 'active' : ''}`}
|
||||
to={siteRoot + 'org/logadmin/repo-transfer/'} title={gettext('Repo Transfer')}>{gettext('Repo Transfer')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="h-100 o-auto">
|
||||
|
@ -67,6 +67,7 @@ import LoginLogs from './logs-page/login-logs';
|
||||
import FileAccessLogs from './logs-page/file-access-logs';
|
||||
import FileUpdateLogs from './logs-page/file-update-logs';
|
||||
import SharePermissionLogs from './logs-page/share-permission-logs';
|
||||
import FIleTransferLogs from './logs-page/file-transfer-log';
|
||||
|
||||
import WebSettings from './web-settings/web-settings';
|
||||
import Notifications from './notifications/notifications';
|
||||
@ -251,6 +252,7 @@ class SysAdmin extends React.Component {
|
||||
<InstitutionAdmins path={siteRoot + 'sys/institutions/:institutionID/admins'} {...commonProps} />
|
||||
<LoginLogs path={siteRoot + 'sys/logs/login'} {...commonProps} />
|
||||
<FileAccessLogs path={siteRoot + 'sys/logs/file-access'} {...commonProps} />
|
||||
<FIleTransferLogs path={siteRoot + 'sys/logs/repo-transfer'} {...commonProps} />
|
||||
<FileUpdateLogs path={siteRoot + 'sys/logs/file-update'} {...commonProps} />
|
||||
<SharePermissionLogs path={siteRoot + 'sys/logs/share-permission'} {...commonProps} />
|
||||
<AdminOperationLogs path={siteRoot + 'sys/admin-logs/operation'} {...commonProps} />
|
||||
|
230
frontend/src/pages/sys-admin/logs-page/file-transfer-log.js
Normal file
230
frontend/src/pages/sys-admin/logs-page/file-transfer-log.js
Normal file
@ -0,0 +1,230 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@gatsbyjs/reach-router';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { seafileAPI } from '../../../utils/seafile-api';
|
||||
import { gettext, siteRoot } from '../../../utils/constants';
|
||||
import { Utils } from '../../../utils/utils';
|
||||
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';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
class Content extends Component {
|
||||
|
||||
getPreviousPage = () => {
|
||||
this.props.getLogsByPage(this.props.currentPage - 1);
|
||||
};
|
||||
|
||||
getNextPage = () => {
|
||||
this.props.getLogsByPage(this.props.currentPage + 1);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, errorMsg, items, perPage, currentPage, hasNextPage } = this.props;
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
} else if (errorMsg) {
|
||||
return <p className="error text-center">{errorMsg}</p>;
|
||||
} else {
|
||||
const emptyTip = (
|
||||
<EmptyTip text={gettext('No transfer logs')}>
|
||||
</EmptyTip>
|
||||
);
|
||||
const table = (
|
||||
<Fragment>
|
||||
<table className="table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">{gettext('Transfer From')}</th>
|
||||
<th width="20%">{gettext('Transfer To')}</th>
|
||||
<th width="20%">{gettext('Operator')}</th>
|
||||
<th width="25%">{gettext('Library')}</th>
|
||||
<th width="15%">{gettext('Date')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{items &&
|
||||
<tbody>
|
||||
{items.map((item, index) => {
|
||||
return (<Item
|
||||
key={index}
|
||||
item={item}
|
||||
/>);
|
||||
})}
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
<Paginator
|
||||
gotoPreviousPage={this.getPreviousPage}
|
||||
gotoNextPage={this.getNextPage}
|
||||
currentPage={currentPage}
|
||||
hasNextPage={hasNextPage}
|
||||
curPerPage={perPage}
|
||||
resetPerPage={this.props.resetPerPage}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
return items.length ? table : emptyTip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Content.propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
errorMsg: PropTypes.string.isRequired,
|
||||
items: PropTypes.array.isRequired,
|
||||
getLogsByPage: PropTypes.func,
|
||||
resetPerPage: PropTypes.func,
|
||||
currentPage: PropTypes.number,
|
||||
perPage: PropTypes.number,
|
||||
pageInfo: PropTypes.object,
|
||||
hasNextPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
class Item extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpIconShown: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleMouseOver = () => {
|
||||
this.setState({
|
||||
isOpIconShown: true
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseOut = () => {
|
||||
this.setState({
|
||||
isOpIconShown: false
|
||||
});
|
||||
};
|
||||
|
||||
getTransferTo = (item) => {
|
||||
switch (item.to_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>;
|
||||
default:
|
||||
return gettext('Deleted');
|
||||
}
|
||||
};
|
||||
|
||||
getTransferFrom = (item) => {
|
||||
switch (item.from_type) {
|
||||
case 'user':
|
||||
return <UserLink email={item.from_user_email} name={item.from_user_name} />;
|
||||
case 'group':
|
||||
return <Link to={`${siteRoot}sys/groups/${item.from_group_id}/libraries/`}>{item.from_group_name}</Link>;
|
||||
default:
|
||||
return gettext('Deleted');
|
||||
}
|
||||
};
|
||||
|
||||
getOperator = (item) => {
|
||||
return <UserLink email={item.operator_email} name={item.operator_name} />;
|
||||
};
|
||||
|
||||
render() {
|
||||
let { item } = this.props;
|
||||
return (
|
||||
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||
<td>{this.getTransferFrom(item)}</td>
|
||||
<td>{this.getTransferTo(item)}</td>
|
||||
<td>{this.getOperator(item)}</td>
|
||||
<td>{item.repo_name ? item.repo_name : gettext('Deleted')}</td>
|
||||
<td>{dayjs(item.date).fromNow()}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Item.propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
class FIleTransferLogs extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
loading: true,
|
||||
errorMsg: '',
|
||||
logList: [],
|
||||
perPage: 100,
|
||||
currentPage: 1,
|
||||
hasNextPage: false,
|
||||
};
|
||||
this.initPage = 1;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let urlParams = (new URL(window.location)).searchParams;
|
||||
const { currentPage, perPage } = this.state;
|
||||
this.setState({
|
||||
perPage: parseInt(urlParams.get('per_page') || perPage),
|
||||
currentPage: parseInt(urlParams.get('page') || currentPage)
|
||||
}, () => {
|
||||
this.getLogsByPage(this.state.currentPage);
|
||||
});
|
||||
}
|
||||
|
||||
getLogsByPage = (page) => {
|
||||
let { perPage } = this.state;
|
||||
seafileAPI.sysAdminListFileTransferLogs(page, perPage).then((res) => {
|
||||
this.setState({
|
||||
logList: res.data.repo_transfer_log_list,
|
||||
loading: false,
|
||||
currentPage: page,
|
||||
hasNextPage: res.data.has_next_page,
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
loading: false,
|
||||
errorMsg: Utils.getErrorMsg(error, true) // true: show login tip if 403
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
resetPerPage = (newPerPage) => {
|
||||
this.setState({
|
||||
perPage: newPerPage,
|
||||
}, () => this.getLogsByPage(this.initPage));
|
||||
};
|
||||
|
||||
render() {
|
||||
let { logList, currentPage, perPage, hasNextPage } = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
<MainPanelTopbar {...this.props} />
|
||||
<div className="main-panel-center flex-row">
|
||||
<div className="cur-view-container">
|
||||
<LogsNav currentItem="fileTransfer" />
|
||||
<div className="cur-view-content">
|
||||
<Content
|
||||
loading={this.state.loading}
|
||||
errorMsg={this.state.errorMsg}
|
||||
items={logList}
|
||||
currentPage={currentPage}
|
||||
perPage={perPage}
|
||||
hasNextPage={hasNextPage}
|
||||
getLogsByPage={this.getLogsByPage}
|
||||
resetPerPage={this.resetPerPage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FIleTransferLogs;
|
@ -16,6 +16,7 @@ class Nav extends React.Component {
|
||||
{ name: 'fileAccessLogs', urlPart: 'logs/file-access', text: gettext('File Access') },
|
||||
{ name: 'fileUpdateLogs', urlPart: 'logs/file-update', text: gettext('File Update') },
|
||||
{ name: 'sharePermissionLogs', urlPart: 'logs/share-permission', text: gettext('Permission') },
|
||||
{ name: 'fileTransfer', urlPart: 'logs/repo-transfer', text: gettext('Repo Transfer') },
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Utils } from "../../utils/utils";
|
||||
import { Utils } from '../../utils/utils';
|
||||
|
||||
describe('getFileExtension', () => {
|
||||
it('should return the file extension with dot', () => {
|
||||
|
@ -481,6 +481,15 @@ class OrgAdminAPI {
|
||||
}
|
||||
|
||||
// org admin logs
|
||||
orgAdminListFileTransfer(page, perPage) {
|
||||
let url = this.server + '/api/v2.1/org/admin/logs/repo-transfer/';
|
||||
let params = {
|
||||
page: page,
|
||||
per_page: perPage
|
||||
};
|
||||
return this.req.get(url, { params: params });
|
||||
}
|
||||
|
||||
orgAdminListFileAudit(email, repoID, page) {
|
||||
let url = this.server + '/api/v2.1/org/admin/logs/file-access/?page=' + page;
|
||||
if (email) {
|
||||
|
@ -2169,6 +2169,15 @@ class SeafileAPI {
|
||||
return this._sendPostRequest(url, formData);
|
||||
}
|
||||
|
||||
sysAdminListFileTransferLogs(page, perPage) {
|
||||
const url = this.server + '/api/v2.1/admin/logs/repo-transfer-logs/';
|
||||
let params = {
|
||||
page: page,
|
||||
per_page: perPage
|
||||
};
|
||||
return this.req.get(url, { params: params });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let seafileAPI = new SeafileAPI();
|
||||
|
@ -28,6 +28,7 @@ from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||
|
||||
from seahub.api2.endpoints.group_owned_libraries import get_group_id_by_repo_owner
|
||||
from seahub.constants import PERMISSION_READ_WRITE
|
||||
from seahub.base.models import RepoTransfer
|
||||
|
||||
try:
|
||||
from seahub.settings import MULTI_TENANCY
|
||||
@ -423,6 +424,11 @@ class AdminLibrary(APIView):
|
||||
# transfer repo
|
||||
try:
|
||||
transfer_repo(repo_id, new_owner, is_share)
|
||||
RepoTransfer.objects.create(from_user=repo_owner,
|
||||
to=new_owner,
|
||||
repo_id=repo_id,
|
||||
org_id=-1,
|
||||
operator=request.user.username)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
|
@ -20,6 +20,7 @@ from seahub.utils import get_file_audit_events, generate_file_audit_event_type,
|
||||
get_file_update_events, get_perm_audit_events, is_valid_email
|
||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr, utc_datetime_to_isoformat_timestr
|
||||
from seahub.utils.repo import is_valid_repo_id_format
|
||||
from seahub.base.models import RepoTransfer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -420,3 +421,146 @@ class AdminLogsSharePermissionLogs(APIView):
|
||||
}
|
||||
|
||||
return Response(resp)
|
||||
|
||||
|
||||
class AdminLogsFileTransferLogs(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAdminUser, IsProVersion)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request):
|
||||
""" Get all transfer repo logs.
|
||||
|
||||
Permission checking:
|
||||
1. only admin can perform this action.
|
||||
"""
|
||||
|
||||
if not request.user.admin_permissions.can_view_user_log():
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
try:
|
||||
current_page = int(request.GET.get('page', '1'))
|
||||
per_page = int(request.GET.get('per_page', '100'))
|
||||
except ValueError:
|
||||
current_page = 1
|
||||
per_page = 100
|
||||
|
||||
start = per_page * (current_page - 1)
|
||||
limit = per_page
|
||||
|
||||
if start < 0:
|
||||
error_msg = 'start invalid'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
if limit < 0:
|
||||
error_msg = 'limit invalid'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
events = RepoTransfer.objects.all().order_by('-timestamp')[start:start+limit+1]
|
||||
if len(events) > limit:
|
||||
has_next_page = True
|
||||
events = events[:limit]
|
||||
else:
|
||||
has_next_page = False
|
||||
|
||||
# Use dict to reduce memcache fetch cost in large for-loop.
|
||||
nickname_dict = {}
|
||||
contact_email_dict = {}
|
||||
repo_dict = {}
|
||||
group_name_dict = {}
|
||||
|
||||
user_email_set = set()
|
||||
repo_id_set = set()
|
||||
group_id_set = set()
|
||||
|
||||
for event in events:
|
||||
repo_id_set.add(event.repo_id)
|
||||
if is_valid_email(event.from_user):
|
||||
user_email_set.add(event.from_user)
|
||||
if is_valid_email(event.to):
|
||||
user_email_set.add(event.to)
|
||||
if is_valid_email(event.operator):
|
||||
user_email_set.add(event.operator)
|
||||
if '@seafile_group' in event.to:
|
||||
group_id = int(event.to.split('@')[0])
|
||||
group_id_set.add(group_id)
|
||||
if '@seafile_group' in event.from_user:
|
||||
group_id = int(event.from_user.split('@')[0])
|
||||
group_id_set.add(group_id)
|
||||
|
||||
for e in user_email_set:
|
||||
if e not in nickname_dict:
|
||||
nickname_dict[e] = email2nickname(e)
|
||||
if e not in contact_email_dict:
|
||||
contact_email_dict[e] = email2contact_email(e)
|
||||
for e in repo_id_set:
|
||||
if e not in repo_dict:
|
||||
repo_dict[e] = seafile_api.get_repo(e)
|
||||
|
||||
for group_id in group_id_set:
|
||||
if group_id not in group_name_dict:
|
||||
group = ccnet_api.get_group(int(group_id))
|
||||
if group:
|
||||
group_name_dict[group_id] = group.group_name
|
||||
|
||||
events_info = []
|
||||
for ev in events:
|
||||
data = {
|
||||
'from_user_email': '',
|
||||
'from_user_name': '',
|
||||
'from_user_contact_email': '',
|
||||
'from_group_id': '',
|
||||
'from_group_name': '',
|
||||
'to_user_email': '',
|
||||
'to_user_name': '',
|
||||
'to_user_contact_email': '',
|
||||
'to_group_id': '',
|
||||
'to_group_name': '',
|
||||
'operator_email': '',
|
||||
'operator_name': '',
|
||||
'operator_contact_email': '',
|
||||
}
|
||||
from_user_email = ev.from_user
|
||||
data['from_user_email'] = from_user_email
|
||||
data['from_user_name'] = nickname_dict.get(from_user_email, '')
|
||||
data['from_user_contact_email'] = contact_email_dict.get(from_user_email, '')
|
||||
|
||||
operator_email = ev.operator
|
||||
data['operator_email'] = operator_email
|
||||
data['operator_name'] = nickname_dict.get(operator_email, '')
|
||||
data['operator_contact_email'] = contact_email_dict.get(operator_email, '')
|
||||
|
||||
if is_valid_email(from_user_email):
|
||||
data['from_type'] = 'user'
|
||||
if '@seafile_group' in from_user_email:
|
||||
from_group_id = int(from_user_email.split('@')[0])
|
||||
data['from_group_id'] = from_group_id
|
||||
data['from_group_name'] = group_name_dict.get(from_group_id, '')
|
||||
data['from_type'] = 'group'
|
||||
|
||||
repo_id = ev.repo_id
|
||||
data['repo_id'] = repo_id
|
||||
repo = repo_dict.get(repo_id, None)
|
||||
data['repo_name'] = repo.name if repo else ''
|
||||
data['date'] = datetime_to_isoformat_timestr(ev.timestamp)
|
||||
|
||||
if is_valid_email(ev.to):
|
||||
to_user_email = ev.to
|
||||
data['to_user_email'] = to_user_email
|
||||
data['to_user_name'] = nickname_dict.get(to_user_email, '')
|
||||
data['to_user_contact_email'] = contact_email_dict.get(to_user_email, '')
|
||||
data['to_type'] = 'user'
|
||||
if '@seafile_group' in ev.to:
|
||||
to_group_id = int(ev.to.split('@')[0])
|
||||
data['to_group_id'] = to_group_id
|
||||
data['to_group_name'] = group_name_dict.get(to_group_id, '')
|
||||
data['to_type'] = 'group'
|
||||
|
||||
events_info.append(data)
|
||||
|
||||
resp = {
|
||||
'repo_transfer_log_list': events_info,
|
||||
'has_next_page': has_next_page,
|
||||
}
|
||||
|
||||
return Response(resp)
|
||||
|
@ -50,6 +50,7 @@ from seahub.views import check_folder_permission
|
||||
from seahub.settings import ENABLE_STORAGE_CLASSES, STORAGE_CLASS_MAPPING_POLICY, \
|
||||
ENCRYPTED_LIBRARY_VERSION, ENCRYPTED_LIBRARY_PWD_HASH_ALGO, \
|
||||
ENCRYPTED_LIBRARY_PWD_HASH_PARAMS
|
||||
from seahub.base.models import RepoTransfer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -1497,6 +1498,12 @@ class GroupOwnedLibraryTransferView(APIView):
|
||||
# transfer repo
|
||||
try:
|
||||
transfer_repo(repo_id, new_owner, is_share, org_id)
|
||||
org_id = seafile_api.get_org_id_by_repo_id(repo_id)
|
||||
RepoTransfer.objects.create(from_user=repo_owner,
|
||||
to=new_owner,
|
||||
repo_id=repo_id,
|
||||
org_id=org_id,
|
||||
operator=request.user.username)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
|
@ -14,6 +14,7 @@ from seahub.share.models import UploadLinkShare, FileShare, check_share_link_acc
|
||||
from seaserv import seafile_api
|
||||
from seahub.utils.repo import parse_repo_perm
|
||||
from seahub.views.file import send_file_access_msg
|
||||
from seahub.utils import normalize_file_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -146,6 +147,7 @@ class InternalCheckFileOperationAccess(APIView):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
file_path = request.data.get('path', '/')
|
||||
file_path = normalize_file_path(file_path)
|
||||
repo = seafile_api.get_repo(repo_id)
|
||||
if not repo:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, 'Library %s not found.' % repo_id)
|
||||
|
@ -46,7 +46,7 @@ from seahub.avatar.templatetags.avatar_tags import api_avatar_url, avatar
|
||||
from seahub.avatar.templatetags.group_avatar_tags import api_grp_avatar_url, \
|
||||
grp_avatar
|
||||
from seahub.base.accounts import User
|
||||
from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey, FileComment
|
||||
from seahub.base.models import UserStarredFiles, DeviceToken, RepoSecretKey, FileComment, RepoTransfer
|
||||
from seahub.share.models import ExtraSharePermission, ExtraGroupsSharePermission
|
||||
from seahub.share.utils import is_repo_admin, check_group_share_in_permission, normalize_custom_permission_name
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname, \
|
||||
@ -1871,6 +1871,12 @@ class RepoOwner(APIView):
|
||||
# transfer repo
|
||||
try:
|
||||
transfer_repo(repo_id, new_owner, is_share, org_id)
|
||||
org_id = seafile_api.get_org_id_by_repo_id(repo_id)
|
||||
RepoTransfer.objects.create(from_user=repo_owner,
|
||||
to=new_owner,
|
||||
repo_id=repo_id,
|
||||
operator=username,
|
||||
org_id=org_id)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
|
@ -451,3 +451,15 @@ class ClientSSOToken(models.Model):
|
||||
if not self.token:
|
||||
self.token = self.gen_token()
|
||||
return super(ClientSSOToken, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class RepoTransfer(models.Model):
|
||||
repo_id = models.CharField(max_length=36)
|
||||
org_id = models.IntegerField(db_index=True)
|
||||
from_user = models.CharField(max_length=255)
|
||||
to = models.CharField(max_length=255)
|
||||
operator = models.CharField(max_length=255)
|
||||
timestamp = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
|
||||
class Meta:
|
||||
db_table = 'RepoTransfer'
|
||||
|
@ -17,7 +17,8 @@ from seahub.api2.endpoints.utils import get_user_name_dict, \
|
||||
get_user_contact_email_dict, get_repo_dict, get_group_dict
|
||||
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
||||
from seahub.utils import EVENTS_ENABLED, get_file_audit_events, get_file_update_events, get_perm_audit_events
|
||||
from seahub.base.models import RepoTransfer
|
||||
from seahub.utils import EVENTS_ENABLED, get_file_audit_events, get_file_update_events, get_perm_audit_events, is_valid_email
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
|
||||
|
||||
from seahub.organizations.api.permissions import IsOrgAdmin
|
||||
@ -263,3 +264,140 @@ class OrgAdminLogsPermAudit(APIView):
|
||||
'page': page,
|
||||
'page_next': page_next,
|
||||
})
|
||||
|
||||
|
||||
class OrgAdminLogsFileTransfer(APIView):
|
||||
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
permission_classes = (IsProVersion, IsOrgAdmin)
|
||||
|
||||
def get(self, request):
|
||||
"""List organization file transfer in logs
|
||||
"""
|
||||
try:
|
||||
page = int(request.GET.get('page', '1'))
|
||||
per_page = int(request.GET.get('per_page', '25'))
|
||||
except ValueError:
|
||||
page = 1
|
||||
per_page = 25
|
||||
|
||||
start = per_page * (page - 1)
|
||||
limit = per_page
|
||||
|
||||
org_id = request.user.org.org_id
|
||||
events = RepoTransfer.objects.filter(org_id=org_id).all().order_by('-timestamp')[start:start+limit+1]
|
||||
if len(events) > limit:
|
||||
page_next = True
|
||||
events = events[:limit]
|
||||
else:
|
||||
page_next = False
|
||||
|
||||
event_list = []
|
||||
if not events:
|
||||
return Response({
|
||||
'log_list': event_list,
|
||||
'page': page,
|
||||
'page_next': False
|
||||
})
|
||||
|
||||
# Use dict to reduce memcache fetch cost in large for-loop.
|
||||
nickname_dict = {}
|
||||
contact_email_dict = {}
|
||||
repo_dict = {}
|
||||
group_name_dict = {}
|
||||
|
||||
user_email_set = set()
|
||||
repo_id_set = set()
|
||||
group_id_set = set()
|
||||
|
||||
for event in events:
|
||||
repo_id_set.add(event.repo_id)
|
||||
if is_valid_email(event.from_user):
|
||||
user_email_set.add(event.from_user)
|
||||
if is_valid_email(event.to):
|
||||
user_email_set.add(event.to)
|
||||
if is_valid_email(event.operator):
|
||||
user_email_set.add(event.operator)
|
||||
if '@seafile_group' in event.to:
|
||||
group_id = int(event.to.split('@')[0])
|
||||
group_id_set.add(group_id)
|
||||
if '@seafile_group' in event.from_user:
|
||||
group_id = int(event.from_user.split('@')[0])
|
||||
group_id_set.add(group_id)
|
||||
|
||||
for e in user_email_set:
|
||||
if e not in nickname_dict:
|
||||
nickname_dict[e] = email2nickname(e)
|
||||
if e not in contact_email_dict:
|
||||
contact_email_dict[e] = email2contact_email(e)
|
||||
for e in repo_id_set:
|
||||
if e not in repo_dict:
|
||||
repo_dict[e] = seafile_api.get_repo(e)
|
||||
|
||||
for group_id in group_id_set:
|
||||
if group_id not in group_name_dict:
|
||||
group = ccnet_api.get_group(int(group_id))
|
||||
if group:
|
||||
group_name_dict[group_id] = group.group_name
|
||||
|
||||
event_list = []
|
||||
for ev in events:
|
||||
data = {
|
||||
'from_user_email': '',
|
||||
'from_user_name': '',
|
||||
'from_user_contact_email': '',
|
||||
'from_group_id': '',
|
||||
'from_group_name': '',
|
||||
'to_user_email': '',
|
||||
'to_user_name': '',
|
||||
'to_user_contact_email': '',
|
||||
'to_group_id': '',
|
||||
'to_group_name': '',
|
||||
'operator_email': '',
|
||||
'operator_name': '',
|
||||
'operator_contact_email': '',
|
||||
}
|
||||
from_user_email = ev.from_user
|
||||
data['from_user_email'] = from_user_email
|
||||
data['from_user_name'] = nickname_dict.get(from_user_email, '')
|
||||
data['from_user_contact_email'] = contact_email_dict.get(from_user_email, '')
|
||||
|
||||
operator_email = ev.operator
|
||||
data['operator_email'] = operator_email
|
||||
data['operator_name'] = nickname_dict.get(operator_email, '')
|
||||
data['operator_contact_email'] = contact_email_dict.get(operator_email, '')
|
||||
|
||||
if is_valid_email(from_user_email):
|
||||
data['from_type'] = 'user'
|
||||
if '@seafile_group' in from_user_email:
|
||||
from_group_id = int(from_user_email.split('@')[0])
|
||||
data['from_group_id'] = from_group_id
|
||||
data['from_group_name'] = group_name_dict.get(from_group_id, '')
|
||||
data['from_type'] = 'group'
|
||||
|
||||
repo_id = ev.repo_id
|
||||
data['repo_id'] = repo_id
|
||||
repo = repo_dict.get(repo_id, None)
|
||||
data['repo_name'] = repo.name if repo else ''
|
||||
data['date'] = datetime_to_isoformat_timestr(ev.timestamp)
|
||||
|
||||
if is_valid_email(ev.to):
|
||||
to_user_email = ev.to
|
||||
data['to_user_email'] = to_user_email
|
||||
data['to_user_name'] = nickname_dict.get(to_user_email, '')
|
||||
data['to_user_contact_email'] = contact_email_dict.get(to_user_email, '')
|
||||
data['to_type'] = 'user'
|
||||
if '@seafile_group' in ev.to:
|
||||
to_group_id = int(ev.to.split('@')[0])
|
||||
data['to_group_id'] = to_group_id
|
||||
data['to_group_name'] = group_name_dict.get(to_group_id, '')
|
||||
data['to_type'] = 'group'
|
||||
|
||||
event_list.append(data)
|
||||
|
||||
return Response({
|
||||
'log_list': event_list,
|
||||
'page': page,
|
||||
'page_next': page_next,
|
||||
})
|
||||
|
@ -15,6 +15,7 @@ from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.utils import api_error
|
||||
from seahub.base.templatetags.seahub_tags import email2nickname, email2contact_email
|
||||
from seahub.base.models import RepoTransfer
|
||||
from seahub.group.utils import group_id_to_name
|
||||
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
|
||||
from seahub.utils import is_valid_email, transfer_repo
|
||||
@ -166,6 +167,11 @@ class OrgAdminRepo(APIView):
|
||||
# transfer repo
|
||||
try:
|
||||
transfer_repo(repo_id, new_owner, is_share, org_id)
|
||||
RepoTransfer.objects.create(from_user=repo_owner,
|
||||
to=new_owner,
|
||||
repo_id=repo_id,
|
||||
org_id=org_id,
|
||||
operator=request.user.username)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
error_msg = 'Internal Server Error'
|
||||
@ -182,9 +188,15 @@ class OrgAdminRepo(APIView):
|
||||
break
|
||||
|
||||
repo_info = {}
|
||||
|
||||
repo_info['owner_email'] = new_owner
|
||||
repo_info['owner_name'] = email2nickname(new_owner)
|
||||
repo_info['encrypted'] = repo.encrypted
|
||||
if '@seafile_group' in new_owner:
|
||||
group_id = get_group_id_by_repo_owner(new_owner)
|
||||
repo_info['group_name'] = group_id_to_name(group_id)
|
||||
repo_info['owner_name'] = group_id_to_name(group_id)
|
||||
else:
|
||||
repo_info['owner_name'] = email2nickname(new_owner)
|
||||
repo_info['encrypted'] = repo.encrypted
|
||||
repo_info['repo_id'] = repo.repo_id
|
||||
repo_info['repo_name'] = repo.name
|
||||
repo_info['is_department_repo'] = False
|
||||
|
@ -20,7 +20,7 @@ from .api.admin.trash_libraries import OrgAdminTrashLibraries, OrgAdminTrashLibr
|
||||
from .api.admin.info import OrgAdminInfo
|
||||
from .api.admin.links import OrgAdminLinks, OrgAdminLink
|
||||
from .api.admin.web_settings import OrgAdminWebSettings
|
||||
from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit
|
||||
from .api.admin.logs import OrgAdminLogsFileAccess, OrgAdminLogsFileUpdate, OrgAdminLogsPermAudit, OrgAdminLogsFileTransfer
|
||||
from .api.admin.user_repos import OrgAdminUserRepos, OrgAdminUserBesharedRepos
|
||||
|
||||
from .api.admin.devices import OrgAdminDevices, OrgAdminDevicesErrors
|
||||
@ -102,6 +102,7 @@ urlpatterns = [
|
||||
path('admin/logs/file-access/', OrgAdminLogsFileAccess.as_view(), name='api-v2.1-org-admin-logs-file-access'),
|
||||
path('admin/logs/file-update/', OrgAdminLogsFileUpdate.as_view(), name='api-v2.1-org-admin-logs-file-update'),
|
||||
path('admin/logs/repo-permission/', OrgAdminLogsPermAudit.as_view(), name='api-v2.1-org-admin-logs-repo-permission'),
|
||||
path('admin/logs/repo-transfer/', OrgAdminLogsFileTransfer.as_view(), name='api-v2.1-org-admin-logs-repo-transfer'),
|
||||
path('<int:org_id>/admin/departments/', OrgAdminDepartments.as_view(), name='api-v2.1-org-admin-departments'),
|
||||
path('<int:org_id>/admin/logs/export-excel/', OrgLogsExport.as_view(), name='api-v2.1-org-logs-export-excel'),
|
||||
path('admin/log/export-excel/', org_log_export_excel, name='org_log_export_excel'),
|
||||
|
@ -35,6 +35,7 @@ urlpatterns = [
|
||||
path('logadmin/', react_fake_view, name='org_log_file_audit'),
|
||||
path('logadmin/file-update/', react_fake_view, name='org_log_file_update'),
|
||||
path('logadmin/perm-audit/', react_fake_view, name='org_log_perm_audit'),
|
||||
path('logadmin/repo-transfer/', react_fake_view, name='org_log_file_transfer'),
|
||||
|
||||
path('info/', react_fake_view, name='org_info'),
|
||||
path('settings/', react_fake_view, name='org_settings'),
|
||||
|
@ -193,7 +193,7 @@ from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
|
||||
from seahub.api2.endpoints.admin.notifications import AdminNotificationsView
|
||||
from seahub.api2.endpoints.admin.sys_notifications import AdminSysNotificationsView, AdminSysNotificationView
|
||||
from seahub.api2.endpoints.admin.logs import AdminLogsLoginLogs, AdminLogsFileAccessLogs, AdminLogsFileUpdateLogs, \
|
||||
AdminLogsSharePermissionLogs
|
||||
AdminLogsSharePermissionLogs, AdminLogsFileTransferLogs
|
||||
from seahub.api2.endpoints.admin.terms_and_conditions import AdminTermsAndConditions, AdminTermAndCondition
|
||||
from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \
|
||||
AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch, AdminWorkWeixinDepartmentsImport
|
||||
@ -691,6 +691,7 @@ urlpatterns = [
|
||||
re_path(r'^api/v2.1/admin/logs/file-access-logs/$', AdminLogsFileAccessLogs.as_view(), name='api-v2.1-admin-logs-file-access-logs'),
|
||||
re_path(r'^api/v2.1/admin/logs/file-update-logs/$', AdminLogsFileUpdateLogs.as_view(), name='api-v2.1-admin-logs-file-update-logs'),
|
||||
re_path(r'^api/v2.1/admin/logs/share-permission-logs/$', AdminLogsSharePermissionLogs.as_view(), name='api-v2.1-admin-logs-share-permission-logs'),
|
||||
re_path(r'^api/v2.1/admin/logs/repo-transfer-logs/$', AdminLogsFileTransferLogs.as_view(), name='api-v2.1-admin-logs-repo-transfer-logs'),
|
||||
|
||||
## admin::admin logs
|
||||
re_path(r'^api/v2.1/admin/admin-logs/$', AdminOperationLogs.as_view(), name='api-v2.1-admin-admin-operation-logs'),
|
||||
@ -865,6 +866,7 @@ urlpatterns = [
|
||||
path('sys/logs/file-access/', sysadmin_react_fake_view, name="sys_logs_file_access"),
|
||||
path('sys/logs/file-update/', sysadmin_react_fake_view, name="sys_logs_file_update"),
|
||||
path('sys/logs/share-permission/', sysadmin_react_fake_view, name="sys_logs_share_permission"),
|
||||
path('sys/logs/repo-transfer/', sysadmin_react_fake_view, name="sys_logs_file_transfer"),
|
||||
path('sys/admin-logs/operation/', sysadmin_react_fake_view, name="sys_admin_logs_operation"),
|
||||
path('sys/admin-logs/login/', sysadmin_react_fake_view, name="sys_admin_logs_login"),
|
||||
path('sys/organizations/', sysadmin_react_fake_view, name="sys_organizations"),
|
||||
|
@ -1575,3 +1575,16 @@ CREATE TABLE `wiki_wiki2_publish` (
|
||||
UNIQUE KEY `publish_url` (`publish_url`),
|
||||
KEY `ix_wiki2_publish_repo_id` (`repo_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `RepoTransfer` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`repo_id` varchar(36) NOT NULL,
|
||||
`org_id` int(11) NOT NULL,
|
||||
`from_user` varchar(255) NOT NULL,
|
||||
`to` varchar(255) NOT NULL,
|
||||
`timestamp` datetime NOT NULL,
|
||||
`operator` varchar(255) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_file_transfer_org_id` (`org_id`),
|
||||
KEY `idx_file_transfer_timestamp` (`timestamp`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
Loading…
Reference in New Issue
Block a user