diff --git a/frontend/src/components/user-settings/email-notice.js b/frontend/src/components/user-settings/email-notice.js index d8ebf1b1b6..1d3d96a7bc 100644 --- a/frontend/src/components/user-settings/email-notice.js +++ b/frontend/src/components/user-settings/email-notice.js @@ -1,12 +1,15 @@ import React from 'react'; import { gettext } from '../../utils/constants'; -import { seafileAPI } from '../../utils/seafile-api'; +import { userAPI } from '../../utils/user-api'; import { Utils } from '../../utils/utils'; import toaster from '../toast'; const { fileUpdatesEmailInterval, - collaborateEmailInterval + collaborateEmailInterval, + enableLoginEmail, + enablePasswordUpdateEmail, + } = window.app.pageOptions; class EmailNotice extends React.Component { @@ -28,9 +31,21 @@ class EmailNotice extends React.Component { { interval: 3600, text: gettext('Per hour') + ' (' + gettext('If notifications have not been read within one hour, they will be sent to your mailbox.') + ')' } ]; + this.passwordOption = [ + { enabled: 0, text: gettext('Don\'t send emails') }, + { enabled: 1, text: gettext('Send email after changing password') } + ]; + + this.loginOption = [ + { enabled: 0, text: gettext('Don\'t send emails') }, + { enabled: 1, text: gettext('Send an email when a new device or browser logs in for the first time') } + ]; + this.state = { fileUpdatesEmailInterval: fileUpdatesEmailInterval, - collaborateEmailInterval: collaborateEmailInterval + collaborateEmailInterval: collaborateEmailInterval, + enableLoginEmail: enableLoginEmail, + enablePasswordUpdateEmail: enablePasswordUpdateEmail }; } @@ -50,11 +65,27 @@ class EmailNotice extends React.Component { } }; + inputPasswordEmailEnabledChange = (e) => { + if (e.target.checked) { + this.setState({ + enablePasswordUpdateEmail: parseInt(e.target.value) + }); + } + }; + + inputLoginEmailEnabledChange = (e) => { + if (e.target.checked) { + this.setState({ + enableLoginEmail: parseInt(e.target.value) + }); + } + }; + formSubmit = (e) => { e.preventDefault(); - let { fileUpdatesEmailInterval, collaborateEmailInterval } = this.state; - seafileAPI.updateEmailNotificationInterval(fileUpdatesEmailInterval, collaborateEmailInterval).then((res) => { - toaster.success(gettext('Email notification updated')); + let { fileUpdatesEmailInterval, collaborateEmailInterval, enablePasswordUpdateEmail, enableLoginEmail } = this.state; + userAPI.updateEmailNotificationInterval(fileUpdatesEmailInterval, collaborateEmailInterval, enablePasswordUpdateEmail, enableLoginEmail).then((res) => { + toaster.success(gettext('Success')); }).catch((error) => { let errorMsg = Utils.getErrorMsg(error); toaster.danger(errorMsg); @@ -62,7 +93,7 @@ class EmailNotice extends React.Component { }; render() { - const { fileUpdatesEmailInterval, collaborateEmailInterval } = this.state; + const { fileUpdatesEmailInterval, collaborateEmailInterval, enableLoginEmail, enablePasswordUpdateEmail } = this.state; return (

{gettext('Email Notification')}

@@ -88,6 +119,27 @@ class EmailNotice extends React.Component {
); })} + +

{gettext('Notifications of change password')}

+ {this.passwordOption.map((item, index) => { + return ( +
+ + +
+ ); + })} + +

{gettext('Notifications of login')}

+

{gettext('Send a mail as soon as a new device or browser has signed into the account (like google and many other services do).')}

+ {this.loginOption.map((item, index) => { + return ( +
+ + +
+ ); + })} diff --git a/frontend/src/utils/user-api.js b/frontend/src/utils/user-api.js index 5296dd015c..b5e37d216c 100644 --- a/frontend/src/utils/user-api.js +++ b/frontend/src/utils/user-api.js @@ -81,6 +81,17 @@ class UserAPI { return this.req.get(url); } + updateEmailNotificationInterval(fileUpdatesEmailInterval, collaborateEmailInterval, enablePasswordUpdateEmail, enableLoginEmail) { + let url = this.server + '/api2/account/info/'; + let data = { + 'file_updates_email_interval': fileUpdatesEmailInterval, + 'collaborate_email_interval': collaborateEmailInterval, + 'enable_password_update_email': enablePasswordUpdateEmail, + 'enable_login_email': enableLoginEmail + }; + return this.req.put(url, data); + } + } let userAPI = new UserAPI(); diff --git a/seahub/api2/endpoints/user.py b/seahub/api2/endpoints/user.py index 7041aa3b2a..ada8cf6e71 100644 --- a/seahub/api2/endpoints/user.py +++ b/seahub/api2/endpoints/user.py @@ -26,6 +26,8 @@ from seahub.base.templatetags.seahub_tags import email2nickname, \ from seahub.profile.models import Profile, DetailedProfile from seahub.settings import ENABLE_UPDATE_USER_INFO, ENABLE_USER_SET_CONTACT_EMAIL, ENABLE_CONVERT_TO_TEAM_ACCOUNT, \ ENABLE_USER_SET_NAME +from seahub.options.models import UserOptions + import seaserv from seaserv import ccnet_api, seafile_api @@ -251,19 +253,21 @@ class ResetPasswordView(APIView): user.set_password(new_password) user.save() - email_template_name = 'registration/password_change_email.html' - send_to = email2contact_email(request.user.username) - site_name = get_site_name() - c = { - 'email': send_to, - 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') - } - try: - send_html_email(_("Successfully Changed Password on %s") % site_name, - email_template_name, c, None, - [send_to]) - except Exception as e: - logger.error('Failed to send notification to %s' % send_to) + enable_pwd_email = bool(UserOptions.objects.get_password_update_email_enable_status(user.username)) + if enable_pwd_email: + email_template_name = 'registration/password_change_email.html' + send_to = email2contact_email(request.user.username) + site_name = get_site_name() + c = { + 'email': send_to, + 'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + try: + send_html_email(_("Successfully Changed Password on %s") % site_name, + email_template_name, c, None, + [send_to]) + except Exception as e: + logger.error('Failed to send notification to %s' % send_to) if not request.session.is_empty(): # invalidate all active sessions after change password. diff --git a/seahub/api2/utils.py b/seahub/api2/utils.py index 04c18735b6..5dc5ff9683 100644 --- a/seahub/api2/utils.py +++ b/seahub/api2/utils.py @@ -34,6 +34,8 @@ from seahub.utils import get_user_repos, send_html_email, get_site_name from seahub.utils.mail import send_html_email_with_dj_template from django.utils.translation import gettext as _ import seahub.settings as settings +from seahub.options.models import UserOptions + JWT_PRIVATE_KEY = getattr(settings, 'JWT_PRIVATE_KEY', '') @@ -210,8 +212,8 @@ def get_token_v2(request, username, platform, device_id, device_name, raise serializers.ValidationError('invalid device id') else: raise serializers.ValidationError('invalid platform') - - if not TokenV2.objects.filter(user=username, device_id=device_id).first(): + enable_new_device_email = bool(UserOptions.objects.get_login_email_enable_status(username)) + if not TokenV2.objects.filter(user=username, device_id=device_id).first() and enable_new_device_email: email_template_name='registration/new_device_login_email.html' send_to = email2contact_email(username) site_name = get_site_name() diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 3f08fb1b19..29c3cce452 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -383,6 +383,21 @@ class AccountInfo(APIView): return api_error(status.HTTP_400_BAD_REQUEST, 'collaborate_email_interval invalid') + enable_login_email = request.data.get("enable_login_email", None) + if enable_login_email is not None: + try: + enable_login_email = int(enable_login_email) + except ValueError: + return api_error(status.HTTP_400_BAD_REQUEST, + 'enable_login_email invalid') + + enable_password_update_email = request.data.get("enable_password_update_email", None) + if enable_password_update_email is not None: + try: + enable_password_update_email = int(enable_password_update_email) + except ValueError: + return api_error(status.HTTP_400_BAD_REQUEST, + 'enable_password_update_email invalid') # update user info if name is not None: @@ -403,6 +418,14 @@ class AccountInfo(APIView): UserOptions.objects.set_collaborate_email_interval( username, collaborate_email_interval) + if enable_password_update_email is not None: + UserOptions.objects.set_password_update_email_enable_status( + username, enable_password_update_email) + + if enable_login_email is not None: + UserOptions.objects.set_login_email_enable_status( + username, enable_login_email) + return Response(self._get_account_info(request)) diff --git a/seahub/auth/__init__.py b/seahub/auth/__init__.py index da3fcc025d..682fbf5858 100644 --- a/seahub/auth/__init__.py +++ b/seahub/auth/__init__.py @@ -119,7 +119,11 @@ def logout(request): session data. Also remove all passwords used to decrypt repos. """ + already_logged_list = request.session.get('_already_logged', []) request.session.flush() + if request.user.username not in already_logged_list: + already_logged_list.append(request.user.username) + request.session['_already_logged'] = already_logged_list if hasattr(request, 'user'): from seahub.base.accounts import User if isinstance(request.user, User): diff --git a/seahub/auth/views.py b/seahub/auth/views.py index 41f2ec16ba..21006296d5 100644 --- a/seahub/auth/views.py +++ b/seahub/auth/views.py @@ -43,7 +43,6 @@ from seahub.utils.two_factor_auth import two_factor_auth_enabled, handle_two_fac from seahub.utils.user_permissions import get_user_role from seahub.utils.auth import get_login_bg_image_path from seahub.organizations.models import OrgSAMLConfig -from seahub.sysadmin_extra.models import UserLoginLog from constance import config @@ -78,7 +77,9 @@ def log_user_in(request, user, redirect_to): # Okay, security checks complete. Log the user in. auth_login(request, user) - if UserLoginLog.objects.filter(username=user.username, login_success=1).count() == 1: + enable_login_email = bool(UserOptions.objects.get_login_email_enable_status(user.username)) + already_logs = request.session.get('_already_logged', []) + if user.username not in already_logs and enable_login_email: email_template_name = 'registration/browse_login_email.html' send_to = email2contact_email(request.user.username) site_name = get_site_name() diff --git a/seahub/options/models.py b/seahub/options/models.py index 3166b55279..5a38c5f832 100644 --- a/seahub/options/models.py +++ b/seahub/options/models.py @@ -38,9 +38,11 @@ KEY_FILE_UPDATES_EMAIL_INTERVAL = "file_updates_email_interval" KEY_FILE_UPDATES_LAST_EMAILED_TIME = "file_updates_last_emailed_time" KEY_COLLABORATE_EMAIL_INTERVAL = 'collaborate_email_interval' KEY_COLLABORATE_LAST_EMAILED_TIME = 'collaborate_last_emailed_time' +KEY_LOGIN_EMAIL_INTERVAL = 'enable_login_email' +KEY_PASSWD_UPDATE_EMAIL_INTERVAL = 'enable_password_update_email' DEFAULT_COLLABORATE_EMAIL_INTERVAL = 3600 - +DEFAULT_PWD_UPDATE_EMAIL_ENABLED = 1 class CryptoOptionNotSetError(Exception): pass @@ -346,6 +348,34 @@ class UserOptionsManager(models.Manager): def unset_collaborate_last_emailed_time(self, username): return self.unset_user_option(username, KEY_COLLABORATE_LAST_EMAILED_TIME) + def get_login_email_enable_status(self, username): + val = self.get_user_option(username, KEY_LOGIN_EMAIL_INTERVAL) + if not val: + return None + try: + return int(val) + except ValueError: + logger.error('Failed to convert string %s to int' % val) + return None + + def set_login_email_enable_status(self, username, enable): + return self.set_user_option(username, KEY_LOGIN_EMAIL_INTERVAL, + str(enable)) + + def get_password_update_email_enable_status(self, username): + val = self.get_user_option(username, KEY_PASSWD_UPDATE_EMAIL_INTERVAL) + if not val: + return DEFAULT_PWD_UPDATE_EMAIL_ENABLED + try: + return int(val) + except ValueError: + logger.error('Failed to convert string %s to int' % val) + return None + + def set_password_update_email_enable_status(self, username, enable): + return self.set_user_option(username, KEY_PASSWD_UPDATE_EMAIL_INTERVAL, + str(enable)) + class UserOptions(models.Model): email = LowerCaseCharField(max_length=255, db_index=True) diff --git a/seahub/profile/templates/profile/set_profile_react.html b/seahub/profile/templates/profile/set_profile_react.html index 4fd2dac780..d63a1951bd 100644 --- a/seahub/profile/templates/profile/set_profile_react.html +++ b/seahub/profile/templates/profile/set_profile_react.html @@ -58,6 +58,8 @@ window.app.pageOptions = { fileUpdatesEmailInterval: {{ file_updates_email_interval }}, collaborateEmailInterval: {{ collaborate_email_interval }}, + enablePasswordUpdateEmail: {{ enable_password_update_email }}, + enableLoginEmail: {{ enable_login_email }}, twoFactorAuthEnabled: {% if two_factor_auth_enabled %} true {% else %} false {% endif %}, {% if two_factor_auth_enabled %} diff --git a/seahub/profile/views.py b/seahub/profile/views.py index 91b01d4a09..36a93d4076 100644 --- a/seahub/profile/views.py +++ b/seahub/profile/views.py @@ -90,6 +90,9 @@ def edit_profile(request): file_updates_email_interval = file_updates_email_interval if file_updates_email_interval is not None else 0 collaborate_email_interval = UserOptions.objects.get_collaborate_email_interval(username) collaborate_email_interval = collaborate_email_interval if collaborate_email_interval is not None else DEFAULT_COLLABORATE_EMAIL_INTERVAL + enable_login_email = UserOptions.objects.get_login_email_enable_status(username) + enable_login_email = enable_login_email if enable_login_email is not None else 0 + enable_password_update_email = UserOptions.objects.get_password_update_email_enable_status(username) if work_weixin_oauth_check(): enable_wechat_work = True @@ -168,6 +171,8 @@ def edit_profile(request): 'ENABLE_UPDATE_USER_INFO': ENABLE_UPDATE_USER_INFO, 'file_updates_email_interval': file_updates_email_interval, 'collaborate_email_interval': collaborate_email_interval, + 'enable_password_update_email': enable_password_update_email, + 'enable_login_email': enable_login_email, 'social_next_page': reverse('edit_profile'), 'enable_wechat_work': enable_wechat_work, 'social_connected': social_connected,