diff --git a/frontend/src/components/dialog/confirm-disconnect-weixin.js b/frontend/src/components/dialog/confirm-disconnect-weixin.js new file mode 100644 index 0000000000..c0c3c53adb --- /dev/null +++ b/frontend/src/components/dialog/confirm-disconnect-weixin.js @@ -0,0 +1,45 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; +import { gettext } from '../../utils/constants'; + +const propTypes = { + formActionURL: PropTypes.string.isRequired, + csrfToken: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired +}; + +class ConfirmDisconnectWeixin extends Component { + + constructor(props) { + super(props); + this.form = React.createRef(); + } + + disconnect = () => { + this.form.current.submit(); + }; + + render() { + const { formActionURL, csrfToken, toggle } = this.props; + return ( + + {gettext('Disconnect')} + +

{gettext('Are you sure you want to disconnect?')}

+
+ +
+
+ + + + +
+ ); + } +} + +ConfirmDisconnectWeixin.propTypes = propTypes; + +export default ConfirmDisconnectWeixin; diff --git a/frontend/src/components/user-settings/social-login-weixin.js b/frontend/src/components/user-settings/social-login-weixin.js new file mode 100644 index 0000000000..bb09f0935a --- /dev/null +++ b/frontend/src/components/user-settings/social-login-weixin.js @@ -0,0 +1,59 @@ +import React from 'react'; +import { gettext, siteRoot } from '../../utils/constants'; +import ModalPortal from '../modal-portal'; +import ConfirmDisconnectWeixin from '../dialog/confirm-disconnect-weixin'; + +const { + csrfToken, + langCode, + socialConnectedWeixin, + socialNextPage +} = window.app.pageOptions; + +class SocialLoginWeixin extends React.Component { + + constructor(props) { + super(props); + this.state = { + isConfirmDialogOpen: false + }; + } + + confirmDisconnect = () => { + this.setState({ + isConfirmDialogOpen: true + }); + }; + + toggleDialog = () => { + this.setState({ + isConfirmDialogOpen: !this.state.isConfirmDialogOpen + }); + }; + + render() { + return ( + +
+

{gettext('Social Login')}

+

{langCode == 'zh-cn' ? '微信' : 'Weixin'}

+ {socialConnectedWeixin ? + : + {gettext('Connect')} + } +
+ {this.state.isConfirmDialogOpen && ( + + + + )} +
+ ); + } +} + +export default SocialLoginWeixin; diff --git a/frontend/src/settings.js b/frontend/src/settings.js index 025894e84f..4dd69b4eba 100644 --- a/frontend/src/settings.js +++ b/frontend/src/settings.js @@ -19,6 +19,7 @@ import EmailNotice from './components/user-settings/email-notice'; import TwoFactorAuthentication from './components/user-settings/two-factor-auth'; import SocialLogin from './components/user-settings/social-login'; import SocialLoginDingtalk from './components/user-settings/social-login-dingtalk'; +import SocialLoginWeixin from './components/user-settings/social-login-weixin'; import SocialLoginSAML from './components/user-settings/social-login-saml'; import LinkedDevices from './components/user-settings/linked-devices'; import DeleteAccount from './components/user-settings/delete-account'; @@ -39,6 +40,7 @@ const { twoFactorAuthEnabled, enableWechatWork, enableDingtalk, + enableWeixin, isOrgContext, enableADFS, enableMultiADFS, @@ -59,7 +61,7 @@ class Settings extends React.Component { { show: true, href: '#lang-setting', text: gettext('Language') }, { show: isPro, href: '#email-notice', text: gettext('Email Notification') }, { show: twoFactorAuthEnabled, href: '#two-factor-auth', text: gettext('Two-Factor Authentication') }, - { show: (enableWechatWork || enableDingtalk || enableADFS || (enableMultiADFS || isOrgContext)), href: '#social-auth', text: gettext('Social Login') }, + { show: (enableWechatWork || enableDingtalk || enableWeixin || enableADFS || (enableMultiADFS || isOrgContext)), href: '#social-auth', text: gettext('Social Login') }, { show: true, href: '#linked-devices', text: gettext('Linked Devices') }, { show: enableDeleteAccount, href: '#del-account', text: gettext('Delete Account') }, ]; @@ -180,6 +182,7 @@ class Settings extends React.Component { {twoFactorAuthEnabled && } {enableWechatWork && } {enableDingtalk && } + {enableWeixin && } {(enableADFS || (enableMultiADFS && isOrgContext)) && } {enableDeleteAccount && } diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html index 18999b3697..4fd2dac780 100644 --- a/seahub/profile/templates/profile/set_profile_react.html +++ b/seahub/profile/templates/profile/set_profile_react.html @@ -78,6 +78,12 @@ window.app.pageOptions = { socialNextPage: "{{ social_next_page|escapejs }}", {% endif %} + enableWeixin: {% if enable_weixin %} true {% else %} false {% endif %}, + {% if enable_weixin %} + socialConnectedWeixin: {% if social_connected_weixin %} true {% else %} false {% endif %}, + socialNextPage: "{{ social_next_page|escapejs }}", + {% endif %} + enableADFS: {% if enable_adfs %} true {% else %} false {% endif %}, {% if enable_adfs %} samlConnected: {% if saml_connected %} true {% else %} false {% endif %}, diff --git a/seahub/profile/views.py b/seahub/profile/views.py index aa349931a2..91b01d4a09 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -26,10 +26,11 @@ from seahub.views import get_owned_repo_list from seahub.work_weixin.utils import work_weixin_oauth_check from seahub.settings import ENABLE_DELETE_ACCOUNT, ENABLE_UPDATE_USER_INFO, ENABLE_ADFS_LOGIN, ENABLE_MULTI_ADFS from seahub.dingtalk.settings import ENABLE_DINGTALK +from seahub.weixin.settings import ENABLE_WEIXIN from constance import config try: from seahub.settings import SAML_PROVIDER_IDENTIFIER -except ImportError as e: +except ImportError: SAML_PROVIDER_IDENTIFIER = 'saml' @@ -107,6 +108,14 @@ def edit_profile(request): enable_dingtalk = False social_connected_dingtalk = False + if ENABLE_WEIXIN: + enable_weixin = True + social_connected_weixin = SocialAuthUser.objects.filter( + username=request.user.username, provider='weixin').count() > 0 + else: + enable_weixin = False + social_connected_weixin = False + if ENABLE_ADFS_LOGIN: enable_adfs = True saml_connected = SocialAuthUser.objects.filter( @@ -164,6 +173,8 @@ def edit_profile(request): 'social_connected': social_connected, 'enable_dingtalk': enable_dingtalk, 'social_connected_dingtalk': social_connected_dingtalk, + 'enable_weixin': enable_weixin, + 'social_connected_weixin': social_connected_weixin, 'ENABLE_USER_SET_CONTACT_EMAIL': settings.ENABLE_USER_SET_CONTACT_EMAIL, 'ENABLE_USER_SET_NAME': settings.ENABLE_USER_SET_NAME, 'user_unusable_password': request.user.enc_password == UNUSABLE_PASSWORD, diff --git a/seahub/settings.py b/seahub/settings.py index 64dad3d546..3228edf7d0 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -271,6 +271,7 @@ INSTALLED_APPS = [ 'seahub.file_tags', 'seahub.related_files', 'seahub.work_weixin', + 'seahub.weixin', 'seahub.dingtalk', 'seahub.file_participants', 'seahub.repo_api_tokens', diff --git a/seahub/weixin/templates/weixin/weixin_user_not_found_error.html b/seahub/weixin/templates/weixin/weixin_user_not_found_error.html new file mode 100644 index 0000000000..06e5534fda --- /dev/null +++ b/seahub/weixin/templates/weixin/weixin_user_not_found_error.html @@ -0,0 +1,8 @@ +{% extends 'base.html' %} + +{% block main_content %} +
+

没找到对应的账号,请先用邮箱注册一个团队账号,然后在个人设置页绑定微信后再扫码登录。

+

[注册团队账号]

+
+{% endblock %} diff --git a/seahub/weixin/urls.py b/seahub/weixin/urls.py index bc1bd71855..c92c75b36a 100644 --- a/seahub/weixin/urls.py +++ b/seahub/weixin/urls.py @@ -2,9 +2,13 @@ # encoding: utf-8 from django.urls import path -from seahub.weixin.views import weixin_oauth_login, weixin_oauth_callback +from seahub.weixin.views import weixin_oauth_login, weixin_oauth_callback, \ + weixin_oauth_disconnect, weixin_oauth_connect, weixin_oauth_connect_callback urlpatterns = [ path('oauth-login/', weixin_oauth_login, name='weixin_oauth_login'), path('oauth-callback/', weixin_oauth_callback, name='weixin_oauth_callback'), + path('oauth-connect/', weixin_oauth_connect, name='weixin_oauth_connect'), + path('oauth-connect-callback/', weixin_oauth_connect_callback, name='weixin_oauth_connect_callback'), + path('oauth-disconnect/', weixin_oauth_disconnect, name='weixin_oauth_disconnect'), ] diff --git a/seahub/weixin/views.py b/seahub/weixin/views.py index 18f74a3e9c..6835d0c2db 100644 --- a/seahub/weixin/views.py +++ b/seahub/weixin/views.py @@ -9,6 +9,7 @@ from django.http import HttpResponseRedirect from django.urls import reverse from django.core.files.base import ContentFile from django.utils.translation import gettext as _ +from django.shortcuts import render from seahub.api2.utils import get_api_token @@ -18,13 +19,15 @@ from seahub.avatar.models import Avatar from seahub.profile.models import Profile from seahub.utils import render_error, get_site_scheme_and_netloc from seahub.auth.models import SocialAuthUser +from seahub.utils.auth import VIRTUAL_ID_EMAIL_DOMAIN +from seahub.auth.decorators import login_required from seahub.settings import SITE_ROOT from seahub.weixin.settings import ENABLE_WEIXIN, \ WEIXIN_OAUTH_APP_ID, WEIXIN_OAUTH_APP_SECRET, \ WEIXIN_OAUTH_SCOPE, WEIXIN_OAUTH_RESPONSE_TYPE, WEIXIN_OAUTH_QR_CONNECT_URL, \ WEIXIN_OAUTH_GRANT_TYPE, WEIXIN_OAUTH_ACCESS_TOKEN_URL, \ - WEIXIN_OAUTH_USER_INFO_URL + WEIXIN_OAUTH_USER_INFO_URL, WEIXIN_OAUTH_CREATE_UNKNOWN_USER logger = logging.getLogger(__name__) @@ -34,15 +37,17 @@ logger = logging.getLogger(__name__) def weixin_oauth_login(request): if not ENABLE_WEIXIN: - return render_error(request, _('Error, please contact administrator.')) + return render_error(request, _('Feature is not enabled.')) state = str(uuid.uuid4()) request.session['weixin_oauth_login_state'] = state - request.session['weixin_oauth_login_redirect'] = request.GET.get(auth.REDIRECT_FIELD_NAME, '/') + redirect_to = request.GET.get(auth.REDIRECT_FIELD_NAME, SITE_ROOT) + request.session['weixin_oauth_login_redirect'] = redirect_to + redirect_uri = get_site_scheme_and_netloc() + reverse('weixin_oauth_callback') data = { 'appid': WEIXIN_OAUTH_APP_ID, - 'redirect_uri': get_site_scheme_and_netloc() + reverse('weixin_oauth_callback'), + 'redirect_uri': redirect_uri, 'response_type': WEIXIN_OAUTH_RESPONSE_TYPE, 'scope': WEIXIN_OAUTH_SCOPE, 'state': state, @@ -54,7 +59,7 @@ def weixin_oauth_login(request): def weixin_oauth_callback(request): if not ENABLE_WEIXIN: - return render_error(request, _('Error, please contact administrator.')) + return render_error(request, _('Feature is not enabled.')) state = request.GET.get('state', '') if not state or state != request.session.get('weixin_oauth_login_state', ''): @@ -89,6 +94,9 @@ def weixin_oauth_callback(request): email = None is_new_user = True + if is_new_user and not WEIXIN_OAUTH_CREATE_UNKNOWN_USER: + return render(request, 'weixin/weixin_user_not_found_error.html') + try: user = auth.authenticate(remote_user=email) email = user.username @@ -147,3 +155,88 @@ def weixin_oauth_callback(request): SITE_ROOT)) response.set_cookie('seahub_auth', email + '@' + api_token.key) return response + + +@login_required +def weixin_oauth_connect(request): + + if not ENABLE_WEIXIN: + return render_error(request, _('Feature is not enabled.')) + + state = str(uuid.uuid4()) + request.session['weixin_oauth_connect_state'] = state + redirect_to = request.GET.get(auth.REDIRECT_FIELD_NAME, SITE_ROOT) + request.session['weixin_oauth_connect_redirect'] = redirect_to + + redirect_uri = get_site_scheme_and_netloc() + reverse('weixin_oauth_connect_callback') + data = { + 'appid': WEIXIN_OAUTH_APP_ID, + 'redirect_uri': redirect_uri, + 'response_type': WEIXIN_OAUTH_RESPONSE_TYPE, + 'scope': WEIXIN_OAUTH_SCOPE, + 'state': state, + } + url = WEIXIN_OAUTH_QR_CONNECT_URL + '?' + urllib.parse.urlencode(data) + return HttpResponseRedirect(url) + + +@login_required +def weixin_oauth_connect_callback(request): + + if not ENABLE_WEIXIN: + return render_error(request, _('Feature is not enabled.')) + + state = request.GET.get('state', '') + if not state or state != request.session.get('weixin_oauth_connect_state', ''): + logger.error('invalid state') + return render_error(request, _('Error, please contact administrator.')) + + # get access_token and user openid + parameters = { + 'appid': WEIXIN_OAUTH_APP_ID, + 'secret': WEIXIN_OAUTH_APP_SECRET, + 'code': request.GET.get('code'), + 'grant_type': WEIXIN_OAUTH_GRANT_TYPE, + } + + access_token_url = WEIXIN_OAUTH_ACCESS_TOKEN_URL + '?' + urllib.parse.urlencode(parameters) + access_token_json = requests.get(access_token_url).json() + + openid = access_token_json.get('openid', '') + access_token = access_token_json.get('access_token', '') + if not access_token or not openid: + logger.error('invalid access_token or openid') + logger.error(access_token_url) + logger.error(access_token_json) + return render_error(request, _('Error, please contact administrator.')) + + auth_user = SocialAuthUser.objects.get_by_provider_and_uid('weixin', openid) + if auth_user: + logger.warning('weixin account already exists %s' % openid) + return render_error(request, '出错了,此微信账号已被绑定') + + username = request.user.username + SocialAuthUser.objects.add(username, 'weixin', openid) + + weixin_oauth_connect_redirect = request.session['weixin_oauth_connect_redirect'] + response = HttpResponseRedirect(weixin_oauth_connect_redirect) + return response + + +@login_required +def weixin_oauth_disconnect(request): + + if not ENABLE_WEIXIN: + return render_error(request, _('Feature is not enabled.')) + + username = request.user.username + if username[-(len(VIRTUAL_ID_EMAIL_DOMAIN)):] == VIRTUAL_ID_EMAIL_DOMAIN: + profile = Profile.objects.get_profile_by_user(username) + if not profile: + return render_error(request, '出错了,当前账号不能解绑微信') + + SocialAuthUser.objects.delete_by_username_and_provider(username, 'weixin') + + # redirect user to page + response = HttpResponseRedirect(request.GET.get(auth.REDIRECT_FIELD_NAME, SITE_ROOT)) + return response