diff --git a/frontend/src/pages/sys-admin/admin-logs/login-logs.js b/frontend/src/pages/sys-admin/admin-logs/login-logs.js new file mode 100644 index 0000000000..edd7a0d376 --- /dev/null +++ b/frontend/src/pages/sys-admin/admin-logs/login-logs.js @@ -0,0 +1,180 @@ +import React, { Component, Fragment } from 'react'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { gettext, loginUrl, siteRoot } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; +import EmptyTip from '../../../components/empty-tip'; +import moment from 'moment'; +import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; +import LogsNav from './logs-nav'; +import MainPanelTopbar from '../main-panel-topbar'; + + +class Content extends Component { + + constructor(props) { + super(props); + } + + 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 ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( + +

{gettext('No Admin Login Logs.')}

+
+ ); + const table = ( + + + + + + + + + + + {items && + + {items.map((item, index) => { + return (); + })} + + } +
{gettext('Name')}{gettext('IP')}{gettext('Status')}{gettext('Time')}
+ +
+ ); + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + } + + render() { + let { item } = this.props; + return ( + + {item.name} + {item.login_ip} + {item.login_success ? gettext('Success') : gettext('Failed')} + {moment(item.login_time).fromNow()} + + ); + } +} + +class AdminLoginLogs extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + logList: [], + perPage: 100, + currentPage: 1, + hasNextPage: false, + }; + this.initPage = 1; + } + + componentDidMount () { + this.getLogsByPage(this.initPage); + } + + getLogsByPage = (page) => { + let { perPage } = this.state; + seafileAPI.sysAdminListAdminLoginLogs(page, perPage).then((res) => { + this.setState({ + logList: res.data.data, + loading: false, + currentPage: page, + hasNextPage: Utils.hasNextPage(page, perPage, res.data.total_count), + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + resetPerPage = (newPerPage) => { + this.setState({ + perPage: newPerPage, + }, () => this.getLogsByPage(this.initPage)); + } + + render() { + let { logList, currentPage, perPage, hasNextPage } = this.state; + return ( + + +
+
+ +
+ +
+
+
+
+ ); + } +} + +export default AdminLoginLogs; diff --git a/frontend/src/pages/sys-admin/admin-logs/logs-nav.js b/frontend/src/pages/sys-admin/admin-logs/logs-nav.js new file mode 100644 index 0000000000..d113e27a6a --- /dev/null +++ b/frontend/src/pages/sys-admin/admin-logs/logs-nav.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from '@reach/router'; +import { siteRoot, gettext } from '../../../utils/constants'; + +const propTypes = { + currentItem: PropTypes.string.isRequired +}; + +class LogsNav extends React.Component { + + constructor(props) { + super(props); + this.navItems = [ + {name: 'adminOperationLogs', urlPart:'admin-logs/operation', text: gettext('Admin Operation Logs')}, + {name: 'adminLoginLogs', urlPart:'admin-logs/login', text: gettext('Admin Login Logs')}, + ]; + } + + render() { + const { currentItem } = this.props; + return ( +
+ +
+ ); + } +} + +LogsNav.propTypes = propTypes; + +export default LogsNav; diff --git a/frontend/src/pages/sys-admin/admin-logs/operation-logs.js b/frontend/src/pages/sys-admin/admin-logs/operation-logs.js new file mode 100644 index 0000000000..ac30e7ffea --- /dev/null +++ b/frontend/src/pages/sys-admin/admin-logs/operation-logs.js @@ -0,0 +1,278 @@ +import React, { Component, Fragment } from 'react'; +import { seafileAPI } from '../../../utils/seafile-api'; +import { gettext, loginUrl, siteRoot, enableSysAdminViewRepo, isPro } from '../../../utils/constants'; +import { Utils } from '../../../utils/utils'; +import EmptyTip from '../../../components/empty-tip'; +import moment from 'moment'; +import Loading from '../../../components/loading'; +import Paginator from '../../../components/paginator'; +import LogsNav from './logs-nav'; +import MainPanelTopbar from '../main-panel-topbar'; + + +class Content extends Component { + + constructor(props) { + super(props); + } + + 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 ; + } else if (errorMsg) { + return

{errorMsg}

; + } else { + const emptyTip = ( + +

{gettext('No Admin Operation Logs.')}

+
+ ); + const table = ( + + + + + + + + + + + {items && + + {items.map((item, index) => { + return (); + })} + + } +
{gettext('Name')}{gettext('Operation')}{gettext('Detail')}{gettext('Time')}
+ +
+ ); + return items.length ? table : emptyTip; + } + } +} + +class Item extends Component { + + constructor(props) { + super(props); + this.state = { + isOpIconShown: false, + }; + } + + getOperationText = (operationType) => { + switch (operationType) { + case 'repo_create': return gettext('Create Library'); + case 'repo_delete': return gettext('Delete Library'); + case 'repo_transfer': return gettext('Transfer Library'); + case 'group_create': return gettext('Create Group'); + case 'group_transfer': return gettext('Transfer Group'); + case 'group_delete': return gettext('Delete Group'); + case 'user_add': return gettext('Add User'); + case 'user_delete': return gettext('Delete User'); + default: return ''; + } + } + + getOperationDetail = (item) => { + let detail = item.detail; + + let ownerPageUrl = ''; + if (detail.owner) { + ownerPageUrl = siteRoot + 'useradmin/info/' + detail.owner + '/'; + } + let userPageUrl = ''; + if (detail.email) { + userPageUrl = siteRoot + 'useradmin/info/' + detail.email + '/'; + } + let detailText = ''; + let repoPageUrl = ''; + let groupPageUrl = ''; + if (item.operation == 'repo_create' || item.operation == 'repo_delete' || item.operation == 'repo_transfer') { + repoPageUrl = siteRoot + 'sys/libraries/' + detail.id + '/' + detail.name + '/'; + } + if (item.operation == 'group_create' || item.operation == 'group_delete' || item.operation == 'group_transfer') { + groupPageUrl = siteRoot + 'sys/groups/' + detail.id + '/libraries/'; + } + + switch (item.operation) { + case 'repo_create': + detailText = gettext('Created library {library_name} with {owner} as its owner') + .replace('{owner}', '' + detail.owner + ''); + if (isPro && enableSysAdminViewRepo) { + detailText.replace('{library_name}', '' + detail.name + ''); + } else { + detailText.replace('{library_name}', '' + detail.name + ''); + } + return detailText; + + case 'repo_delete': + detailText = gettext('Deleted library {library_name}') + .replace('{library_name}', '' + detail.name + ''); + return detailText; + + case 'repo_transfer': + detailText = gettext('Transferred library {library_name} from {user_from} to {user_to}') + .replace('{user_from}', '' + detail.from + '') + .replace('{user_to}', '' + detail.to+ ''); + if (isPro && enableSysAdminViewRepo) { + detailText.replace('{library_name}', '' + detail.name + ''); + } else { + detailText.replace('{library_name}', '' + detail.name + ''); + } + return detailText; + + case 'group_create': + detailText = gettext('Created group {group_name}') + .replace('{group_name}', '' + detail.name+ ''); + return detailText; + + case 'group_transfer': + detailText = gettext('Transferred group {group_name} from {user_from} to {user_to}') + .replace('{user_from}', '' + detail.from + '') + .replace('{user_to}', '' + detail.to+ '') + .replace('{group_name}', '' + detail.name+ ''); + return detailText; + + case 'group_delete': + detailText = gettext('Deleted group {group_name}') + .replace('{group_name}', '' + detail.name + ''); + return detailText; + + case 'user_add': + detailText = gettext('Added user {user}') + .replace('{user}', '' + detail.email+ ''); + return detailText; + + case 'user_delete': + detailText = gettext('Deleted user {user}') + .replace('{user}', '' + detail.email+ ''); + return detailText; + + default: return ''; + } + } + + render() { + let { item } = this.props; + return ( + + {item.name} + {this.getOperationText(item.operation)} + + + + {moment(item.datetime).fromNow()} + + ); + } +} + +class AdminOperationLogs extends Component { + + constructor(props) { + super(props); + this.state = { + loading: true, + errorMsg: '', + logList: [], + perPage: 100, + currentPage: 1, + hasNextPage: false, + }; + this.initPage = 1; + } + + componentDidMount () { + this.getLogsByPage(this.initPage); + } + + getLogsByPage = (page) => { + let { perPage } = this.state; + seafileAPI.sysAdminListAdminLogs(page, perPage).then((res) => { + this.setState({ + logList: res.data.data, + loading: false, + currentPage: page, + hasNextPage: Utils.hasNextPage(page, perPage, res.data.total_count), + }); + }).catch((error) => { + if (error.response) { + if (error.response.status == 403) { + this.setState({ + loading: false, + errorMsg: gettext('Permission denied') + }); + location.href = `${loginUrl}?next=${encodeURIComponent(location.href)}`; + } else { + this.setState({ + loading: false, + errorMsg: gettext('Error') + }); + } + } else { + this.setState({ + loading: false, + errorMsg: gettext('Please check the network.') + }); + } + }); + } + + resetPerPage = (newPerPage) => { + this.setState({ + perPage: newPerPage, + }, () => this.getLogsByPage(this.initPage)); + } + + render() { + let { logList, currentPage, perPage, hasNextPage } = this.state; + return ( + + +
+
+ +
+ +
+
+
+
+ ); + } +} + +export default AdminOperationLogs; diff --git a/frontend/src/pages/sys-admin/index.js b/frontend/src/pages/sys-admin/index.js index ba19c25faa..da85818b6b 100644 --- a/frontend/src/pages/sys-admin/index.js +++ b/frontend/src/pages/sys-admin/index.js @@ -38,6 +38,9 @@ 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 AdminOperationLogs from './admin-logs/operation-logs'; +import AdminLoginLogs from './admin-logs/login-logs'; + import WebSettings from './web-settings/web-settings'; import Notifications from './notifications/notifications'; import FileScanRecords from './file-scan-records'; @@ -149,6 +152,8 @@ class SysAdmin extends React.Component { + + -

{gettext('No Share Links.')}

+

{gettext('No File Access Logs.')}

); const table = ( diff --git a/frontend/src/pages/sys-admin/logs-page/file-update-logs.js b/frontend/src/pages/sys-admin/logs-page/file-update-logs.js index 1478d90909..7e112b2081 100644 --- a/frontend/src/pages/sys-admin/logs-page/file-update-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/file-update-logs.js @@ -35,7 +35,7 @@ class Content extends Component { } else { const emptyTip = ( -

{gettext('No Share Links.')}

+

{gettext('No File Update Logs.')}

); const table = ( diff --git a/frontend/src/pages/sys-admin/logs-page/login-logs.js b/frontend/src/pages/sys-admin/logs-page/login-logs.js index fc903a7edc..671d76ae22 100644 --- a/frontend/src/pages/sys-admin/logs-page/login-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/login-logs.js @@ -36,7 +36,7 @@ class Content extends Component { } else { const emptyTip = ( -

{gettext('No Share Links.')}

+

{gettext('No Login Logs.')}

); const table = ( diff --git a/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js b/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js index 9c10e08442..602bfe1d9d 100644 --- a/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js +++ b/frontend/src/pages/sys-admin/logs-page/share-permission-logs.js @@ -35,7 +35,7 @@ class Content extends Component { } else { const emptyTip = ( -

{gettext('No Share Links.')}

+

{gettext('No Permission Logs.')}

); const table = ( diff --git a/frontend/src/pages/sys-admin/side-panel.js b/frontend/src/pages/sys-admin/side-panel.js index 90c4e8f236..2c3b619fcd 100644 --- a/frontend/src/pages/sys-admin/side-panel.js +++ b/frontend/src/pages/sys-admin/side-panel.js @@ -213,10 +213,14 @@ class SidePanel extends React.Component { } {isPro && canViewAdminLog &&
  • - - + this.props.tabItemClick('adminLogs')} + > + {gettext('Admin Logs')} - +
  • } {isDefaultAdmin && enableWorkWeixin && diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 15aa19a71d..5499e4c249 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -124,3 +124,4 @@ export const canManageGroup = window.sysadmin ? window.sysadmin.pageOptions.admi export const canViewUserLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_user_log : ''; export const canViewAdminLog = window.sysadmin ? window.sysadmin.pageOptions.admin_permissions.can_view_admin_log : ''; export const enableWorkWeixin = window.sysadmin ? window.sysadmin.pageOptions.enable_work_weixin : ''; +export const enableSysAdminViewRepo = window.sysadmin ? window.sysadmin.pageOptions.enableSysAdminViewRepo : ''; diff --git a/seahub/urls.py b/seahub/urls.py index dc96c86492..5956279700 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -682,6 +682,8 @@ urlpatterns = [ url(r'^sys/logs/file-access/$', sysadmin_react_fake_view, name="sys_logs_file_access"), url(r'^sys/logs/file-update/$', sysadmin_react_fake_view, name="sys_logs_file_update"), url(r'^sys/logs/share-permission/$', sysadmin_react_fake_view, name="sys_logs_share_permission"), + url(r'^sys/admin-logs/operation/$', sysadmin_react_fake_view, name="sys_admin_logs_operation"), + url(r'^sys/admin-logs/login/$', sysadmin_react_fake_view, name="sys_admin_logs_login"), url(r'^sys/organizations/$', sysadmin_react_fake_view, name="sys_organizations"), url(r'^sys/organizations/(?P\d+)/info/$', sysadmin_react_fake_view, name="sys_organization_info"), url(r'^sys/organizations/(?P\d+)/users/$', sysadmin_react_fake_view, name="sys_organization_users"),