Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

invite user #7470

Open
wants to merge 1 commit into
base: 13.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions frontend/src/components/dialog/group-invite-members-dialog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Button, Modal, ModalBody } from 'reactstrap';
import SeahubModalHeader from '@/components/common/seahub-modal-header';
import copy from 'copy-to-clipboard';
import toaster from '../toast';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';

import '../../css/group-invite-members-dialog.css';

const propTypes = {
groupID: PropTypes.string.isRequired,
toggleGroupInviteDialog: PropTypes.func.isRequired,
};

class GroupInviteMembersDialog extends React.Component {

constructor(props) {
super(props);
this.state = {
inviteList: [],
};
}

componentDidMount() {
this.listInviteLinks();
}

listInviteLinks = () => {
seafileAPI.getGroupInviteLinks(this.props.groupID).then((res) => {
this.setState({ inviteList: res.data.group_invite_link_list });
}).catch(error => {
this.onError(error);
});
};

addInviteLink = () => {
seafileAPI.addGroupInviteLinks(this.props.groupID).then(() => {
this.listInviteLinks();
}).catch(error => {
this.onError(error);
});
};

deleteLink = (token) => {
seafileAPI.deleteGroupInviteLinks(this.props.groupID, token).then(() => {
this.listInviteLinks();
}).catch(error => {
this.onError(error);
});
};

onError = (error) => {
let errMsg = Utils.getErrorMsg(error, true);
if (!error.response || error.response.status !== 403) {
toaster.danger(errMsg);
}
};

copyLink = () => {
const inviteLinkItem = this.state.inviteList[0];
copy(inviteLinkItem.link);
const message = gettext('Invitation link has been copied to clipboard');
toaster.success((message), {
duration: 2
});
};

toggle = () => {
this.props.toggleGroupInviteDialog();
};

render() {
const { inviteList } = this.state;
const link = inviteList[0];
return (
<Modal isOpen={true} toggle={this.toggle} className="group-invite-members">
<SeahubModalHeader toggle={this.toggle}>{gettext('Invite members')}</SeahubModalHeader>
<ModalBody>
{link ?
<>
<div>
<strong>{gettext('Group invitation link')}</strong>
</div>
<div className="invite-link-item">
<div className="form-item text-truncate">{link.link}</div>
<div className="invite-link-copy">
<Button color="primary" onClick={this.copyLink} className="invite-link-copy-btn text-truncate">{gettext('Copy')}</Button>
</div>
<Button color="secondary" onClick={this.deleteLink.bind(this, link.token)} className="delete-link-btn ml-2">
<i className="sf2-icon-delete"></i>
</Button>
</div>
</>
:
<>
<div className="no-link-tip mb-4">
{gettext('No group invitation link yet. Group invitation link let registered users to join the group by clicking a link.')}
</div>
<Button color="primary" onClick={this.addInviteLink} className="my-4">{gettext('Generate')}</Button>
</>
}
</ModalBody>
</Modal>
);
}
}

GroupInviteMembersDialog.propTypes = propTypes;

export default GroupInviteMembersDialog;
40 changes: 40 additions & 0 deletions frontend/src/css/group-invite-members-dialog.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.group-invite-members th,
.group-invite-members td {
vertical-align: middle;
text-align: left;
}

.group-invite-members .no-link-tip {
line-height: 24px;
color: #999;
}

.invite-link-item {
display: flex;
margin: 1rem 0 2.5rem;
}

.invite-link-item .form-item {
width: calc(100% - 120px);
padding-left: 10px;
height: 40px;
line-height: 40px;
border: 1px solid #ccc;
border-right: none;
}

.invite-link-item .invite-link-copy {
width: 72px;
}

.invite-link-item .invite-link-copy-btn {
width: 72px;
height: 40px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}

.invite-link-item .delete-link-btn {
color: #999;
width: 40px;
}
22 changes: 21 additions & 1 deletion frontend/src/pages/groups/group-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import cookie from 'react-cookies';
import classnames from 'classnames';
import { gettext, username, canAddRepo } from '../../utils/constants';
import { gettext, username, canAddRepo, isMultiTenancy } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api';
import { Utils } from '../../utils/utils';
import Loading from '../../components/loading';
Expand All @@ -21,6 +21,7 @@ import DepartmentDetailDialog from '../../components/dialog/department-detail-di
import LeaveGroupDialog from '../../components/dialog/leave-group-dialog';
import SharedRepoListView from '../../components/shared-repo-list-view/shared-repo-list-view';
import SortOptionsDialog from '../../components/dialog/sort-options';
import GroupInviteMembersDialog from '../../components/dialog/group-invite-members-dialog';
import SingleDropdownToolbar from '../../components/toolbar/single-dropdown-toolbar';
import ViewModes from '../../components/view-modes';
import ReposSortMenu from '../../components/sort-menu';
Expand Down Expand Up @@ -65,6 +66,7 @@ class GroupView extends React.Component {
showTransferGroupDialog: false,
showImportMembersDialog: false,
showManageMembersDialog: false,
showInviteMembersDialog: false,
isLeaveGroupDialogOpen: false,
isMembersDialogOpen: false
};
Expand Down Expand Up @@ -287,6 +289,13 @@ class GroupView extends React.Component {
});
};

toggleInviteMembersDialog = () => {
this.setState({
showInviteMembersDialog: !this.state.showInviteMembersDialog,
showGroupDropdown: false,
});
};

importMembersInBatch = (file) => {
toaster.notify(gettext('It may take some time, please wait.'));
seafileAPI.importGroupMembersViaFile(this.state.currentGroup.id, file).then((res) => {
Expand Down Expand Up @@ -365,6 +374,7 @@ class GroupView extends React.Component {

getOpList = () => {
const { currentGroup, isDepartmentGroup, isStaff, isOwner } = this.state;
// const isGroup = this.state.currentGroup.owner !== 'system admin';
const opList = [];
if ((!isDepartmentGroup && canAddRepo) ||
(isDepartmentGroup && isStaff)) {
Expand All @@ -389,6 +399,10 @@ class GroupView extends React.Component {
if (!isOwner && !isDepartmentGroup) {
opList.push({ 'text': gettext('Leave group'), 'onClick': this.toggleLeaveGroupDialog });
}

if (isOwner && this.state.currentGroup.owner !== 'system admin' && !isMultiTenancy) {
opList.push({ 'text': gettext('Invite Members'), 'onClick': this.toggleInviteMembersDialog });
}
}

return opList;
Expand Down Expand Up @@ -589,6 +603,12 @@ class GroupView extends React.Component {
onGroupChanged={this.props.onGroupChanged}
/>
}
{this.state.showInviteMembersDialog &&
<GroupInviteMembersDialog
groupID={this.props.groupID}
onGroupChanged={this.props.onGroupChanged}
toggleGroupInviteDialog={this.toggleInviteMembersDialog}/>
}
</Fragment>
);
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utils/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export const enablePDFThumbnail = window.app.pageOptions.enablePDFThumbnail;
export const enableOnlyoffice = window.app.pageOptions.enableOnlyoffice || false;
export const onlyofficeConverterExtensions = window.app.pageOptions.onlyofficeConverterExtensions || [];

export const isMultiTenancy = window.app.pageOptions.isMultiTenacy;
export const enableFileTags = window.app.pageOptions.enableFileTags || false;

export const enableShowAbout = window.app.pageOptions.enableShowAbout || false;
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/utils/seafile-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,22 @@ class SeafileAPI {
return this.req.delete(url, { data: params });
}

deleteGroupInviteLinks(groupID, token) {
const url = this.server + '/api/v2.1/groups/' + groupID + '/invite-links/' + token + '/';
return this.req.delete(url);
}

addGroupInviteLinks(groupID) {
const url = this.server + '/api/v2.1/groups/' + groupID + '/invite-links/';
let formData = new FormData();
return this._sendPostRequest(url, formData);
}

getGroupInviteLinks(groupID) {
const url = this.server + '/api/v2.1/groups/' + groupID + '/invite-links/';
return this.req.get(url);
}

// ---- share operation

listShareLinks({ repoID, path, page, perPage }) {
Expand Down
99 changes: 97 additions & 2 deletions seahub/api2/endpoints/group_members.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from io import BytesIO
from openpyxl import load_workbook

from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.translation import gettext as _

from rest_framework.authentication import SessionAuthentication
Expand All @@ -19,16 +19,18 @@
from seahub.api2.endpoints.utils import is_org_user
from seahub.api2.throttling import UserRateThrottle
from seahub.api2.authentication import TokenAuthentication
from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.utils import string2list, is_org_context, get_file_type_and_ext
from seahub.group.models import GroupInviteLinkModel
from seahub.utils.ms_excel import write_xls
from seahub.utils.error_msg import file_type_error_msg
from seahub.base.accounts import User
from seahub.group.signals import add_user_to_group
from seahub.group.views import group_invite
from seahub.group.utils import is_group_member, is_group_admin, \
is_group_owner, is_group_admin_or_owner, get_group_member_info
from seahub.profile.models import Profile
from seahub.settings import MULTI_TENANCY

from .utils import api_check_group

Expand Down Expand Up @@ -541,3 +543,96 @@ def get(self, request):
wb.save(response)

return response


class GroupInviteLinks(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)

@api_check_group
def get(self, request, group_id):
"""
Get invitation link
"""
group_id = int(group_id)
email = request.user.username

if MULTI_TENANCY:
error_msg = 'Feature disabled.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

group = ccnet_api.get_group(group_id)
if group.creator_name == "system admin":
error_msg = 'Forbidden to operate department group'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

if not is_group_admin_or_owner(group_id, email):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)

try:
invite_link_query_set = GroupInviteLinkModel.objects.filter(group_id=group_id)
except Exception as e:
logger.error(f'query group invite links failed. {e}')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')

return Response({'group_invite_link_list': [group_invite_link.to_dict() for group_invite_link in
invite_link_query_set]})

@api_check_group
def post(self, request, group_id):
group_id = int(group_id)
email = request.user.username
if MULTI_TENANCY:
error_msg = 'Feature disabled.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

group = ccnet_api.get_group(group_id)
if group.creator_name == "system admin":
error_msg = 'Forbidden to operate department group'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

if not is_group_admin_or_owner(group_id, email):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)

try:
invite_link = GroupInviteLinkModel.objects.create_link(group_id, email)
except Exception as e:
logger.error(f'create group invite links failed. {e}')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')

return Response(invite_link.to_dict())


class GroupInviteLink(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated,)
throttle_classes = (UserRateThrottle,)

@api_check_group
def delete(self, request, group_id, token):
group_id = int(group_id)
email = request.user.username

if MULTI_TENANCY:
error_msg = 'Feature disabled.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

group = ccnet_api.get_group(group_id)
if group.creator_name == "system admin":
error_msg = 'Forbidden to operate department group'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)

if not is_group_admin_or_owner(group_id, email):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)

try:
GroupInviteLinkModel.objects.filter(token=token, group_id=group_id).delete()
except Exception as e:
logger.error(f'delete group invite links failed. {e}')
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal Server Error')

return Response({'success': True})
Loading