From 2510d4d65996c2e41d260d009b1bcdbb35bdb099 Mon Sep 17 00:00:00 2001 From: Andrey Zhavoronkov Date: Wed, 26 Aug 2020 17:05:15 +0300 Subject: [PATCH] Added ability to configure email verification for registered user (#1929) * Added ability to configure email verification for registered user Removed unused code * updated changelog * fixed comments * fixed swagger * updated docs Co-authored-by: Nikita Manovich --- CHANGELOG.md | 1 + cvat-core/src/api-implementation.js | 6 ++++-- cvat-core/src/api.js | 1 + cvat-core/src/user.js | 11 ++++++++++ cvat-ui/src/actions/auth-actions.ts | 5 ++--- cvat-ui/src/components/cvat-app.tsx | 6 +++--- cvat-ui/src/reducers/interfaces.ts | 1 + cvat-ui/src/reducers/notifications-reducer.ts | 20 ++++++++++++++++++ cvat/apps/authentication/auth_basic.py | 13 ++++++++++-- .../email_confirmation_signup_message.html | 8 +++++++ cvat/apps/authentication/urls.py | 16 +++++++++++--- cvat/apps/authentication/views.py | 11 ++++++++++ cvat/apps/documentation/installation.md | 21 +++++++++++++++++++ cvat/settings/base.py | 6 ++++-- cvat/settings/development.py | 2 ++ cvat/urls.py | 1 - 16 files changed, 113 insertions(+), 16 deletions(-) create mode 100644 cvat/apps/authentication/templates/account/email/email_confirmation_signup_message.html diff --git a/CHANGELOG.md b/CHANGELOG.md index ca2dea56..3a81cc7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Datumaro] Dataset statistics () - Ability to change label color in tasks and predefined labels () - [Datumaro] Multi-dataset merge (https://github.com/opencv/cvat/pull/1695) +- Ability to configure email verification for new users () - Link to django admin page from UI () - Notification message when users use wrong browser () diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 32dd6c5b..5fc7958c 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -81,8 +81,10 @@ cvat.server.register.implementation = async (username, firstName, lastName, email, password1, password2, userConfirmations) => { - await serverProxy.server.register(username, firstName, lastName, email, - password1, password2, userConfirmations); + const user = await serverProxy.server.register(username, firstName, + lastName, email, password1, password2, userConfirmations); + + return new User(user); }; cvat.server.login.implementation = async (username, password) => { diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index ab5ba39b..9d040683 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -148,6 +148,7 @@ function build() { * @param {string} password1 A password for the new account * @param {string} password2 The confirmation password for the new account * @param {Object} userConfirmations An user confirmations of terms of use if needed + * @returns {Object} response data * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ServerError} */ diff --git a/cvat-core/src/user.js b/cvat-core/src/user.js index 62221c67..555ea83d 100644 --- a/cvat-core/src/user.js +++ b/cvat-core/src/user.js @@ -23,6 +23,7 @@ is_staff: null, is_superuser: null, is_active: null, + email_verification_required: null, }; for (const property in data) { @@ -143,6 +144,16 @@ */ get: () => data.is_active, }, + isVerified: { + /** + * @name isVerified + * @type {boolean} + * @memberof module:API.cvat.classes.User + * @readonly + * @instance + */ + get: () => !data.email_verification_required, + }, })); } } diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 3aff6ef3..f12049fa 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -75,11 +75,10 @@ export const registerAsync = ( dispatch(authActions.register()); try { - await cvat.server.register(username, firstName, lastName, email, password1, password2, + const user = await cvat.server.register(username, firstName, lastName, email, password1, password2, confirmations); - const users = await cvat.users.get({ self: true }); - dispatch(authActions.registerSuccess(users[0])); + dispatch(authActions.registerSuccess(user)); } catch (error) { dispatch(authActions.registerFailed(error)); } diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 83a81ede..5c1af2cf 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -129,7 +129,7 @@ class CVATApplication extends React.PureComponent diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index cd971cff..fbc4b5ed 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -252,6 +252,7 @@ export interface NotificationsState { }; auth: { changePasswordDone: string; + registerDone: string; }; }; } diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index a869011f..dfbfdbf5 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -97,6 +97,7 @@ const defaultState: NotificationsState = { }, auth: { changePasswordDone: '', + registerDone: '', }, }, }; @@ -163,6 +164,25 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case AuthActionTypes.REGISTER_SUCCESS: { + if (!action.payload.user.isVerified) { + return { + ...state, + messages: { + ...state.messages, + auth: { + ...state.messages.auth, + registerDone: `To use your account, you need to confirm the email address. \ + We have sent an email with a confirmation link to ${action.payload.user.email}.`, + }, + }, + }; + } + + return { + ...state, + }; + } case AuthActionTypes.CHANGE_PASSWORD_SUCCESS: { return { ...state, diff --git a/cvat/apps/authentication/auth_basic.py b/cvat/apps/authentication/auth_basic.py index 588baf9d..4c4ab1bc 100644 --- a/cvat/apps/authentication/auth_basic.py +++ b/cvat/apps/authentication/auth_basic.py @@ -1,8 +1,12 @@ -# Copyright (C) 2018 Intel Corporation +# Copyright (C) 2018-2020 Intel Corporation # # SPDX-License-Identifier: MIT -from . import AUTH_ROLE + from django.conf import settings +from allauth.account import app_settings as allauth_settings +from allauth.account.models import EmailAddress + +from . import AUTH_ROLE def create_user(sender, instance, created, **kwargs): from django.contrib.auth.models import Group @@ -10,6 +14,11 @@ def create_user(sender, instance, created, **kwargs): if instance.is_superuser and instance.is_staff: db_group = Group.objects.get(name=AUTH_ROLE.ADMIN) instance.groups.add(db_group) + + # create and verify EmailAdress for superuser accounts + if allauth_settings.EMAIL_REQUIRED: + EmailAddress.objects.get_or_create(user=instance, email=instance.email, primary=True, verified=True) + for group_name in settings.DJANGO_AUTH_DEFAULT_GROUPS: db_group = Group.objects.get(name=getattr(AUTH_ROLE, group_name)) instance.groups.add(db_group) diff --git a/cvat/apps/authentication/templates/account/email/email_confirmation_signup_message.html b/cvat/apps/authentication/templates/account/email/email_confirmation_signup_message.html new file mode 100644 index 00000000..8bdc2508 --- /dev/null +++ b/cvat/apps/authentication/templates/account/email/email_confirmation_signup_message.html @@ -0,0 +1,8 @@ +{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! + +

You're receiving this e-mail because user {{ user_display }} has given yours as an e-mail address to connect their account.

+ +

To confirm this is correct, go to {{ activate_url }}

+{% endblocktrans %} +{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}{{ site_domain }}{% endblocktrans %} +{% endautoescape %} diff --git a/cvat/apps/authentication/urls.py b/cvat/apps/authentication/urls.py index a3752ad5..d18ad09a 100644 --- a/cvat/apps/authentication/urls.py +++ b/cvat/apps/authentication/urls.py @@ -2,13 +2,15 @@ # # SPDX-License-Identifier: MIT -from django.urls import path +from django.urls import path, re_path from django.conf import settings from rest_auth.views import ( LoginView, LogoutView, PasswordChangeView, PasswordResetView, PasswordResetConfirmView) -from rest_auth.registration.views import RegisterView -from .views import SigningView +from allauth.account.views import ConfirmEmailView, EmailVerificationSentView +from allauth.account import app_settings as allauth_settings + +from cvat.apps.authentication.views import SigningView, RegisterView urlpatterns = [ path('login', LoginView.as_view(), name='rest_login'), @@ -26,3 +28,11 @@ if settings.DJANGO_AUTH_TYPE == 'BASIC': path('password/change', PasswordChangeView.as_view(), name='rest_password_change'), ] + if allauth_settings.EMAIL_VERIFICATION != \ + allauth_settings.EmailVerificationMethod.NONE: + urlpatterns += [ + re_path(r'^account-confirm-email/(?P[-:\w]+)/$', ConfirmEmailView.as_view(), + name='account_confirm_email'), + path('register/account-email-verification-sent', EmailVerificationSentView.as_view(), + name='account_email_verification_sent'), + ] diff --git a/cvat/apps/authentication/views.py b/cvat/apps/authentication/views.py index f38e3409..6f503199 100644 --- a/cvat/apps/authentication/views.py +++ b/cvat/apps/authentication/views.py @@ -5,6 +5,8 @@ from rest_framework import views from rest_framework.exceptions import ValidationError from rest_framework.response import Response +from rest_auth.registration.views import RegisterView as _RegisterView +from allauth.account import app_settings as allauth_settings from furl import furl from . import signature @@ -43,3 +45,12 @@ class SigningView(views.APIView): url = furl(url).add({signature.QUERY_PARAM: sign}).url return Response(url) + + +class RegisterView(_RegisterView): + def get_response_data(self, user): + data = self.get_serializer(user).data + data['email_verification_required'] = allauth_settings.EMAIL_VERIFICATION == \ + allauth_settings.EmailVerificationMethod.MANDATORY + + return data diff --git a/cvat/apps/documentation/installation.md b/cvat/apps/documentation/installation.md index dc37a025..ce2e9008 100644 --- a/cvat/apps/documentation/installation.md +++ b/cvat/apps/documentation/installation.md @@ -9,6 +9,7 @@ - [Stop all containers](#stop-all-containers) - [Advanced settings](#advanced-settings) - [Share path](#share-path) + - [Email verification](#email-verification) - [Serving over HTTPS](#serving-over-https) - [Prerequisites](#prerequisites) - [Roadmap](#roadmap) @@ -362,6 +363,26 @@ You can change the share device path to your actual share. For user convenience we have defined the environment variable $CVAT_SHARE_URL. This variable contains a text (url for example) which is shown in the client-share browser. +### Email verification + +You can enable email verification for newly registered users. +Specify these options in the [settings file](../../settings/base.py) to configure Django allauth +to enable email verification (ACCOUNT_EMAIL_VERIFICATION = 'mandatory'). +Access is denied until the user's email address is verified. +```python +ACCOUNT_AUTHENTICATION_METHOD = 'username' +ACCOUNT_CONFIRM_EMAIL_ON_GET = True +ACCOUNT_EMAIL_REQUIRED = True +ACCOUNT_EMAIL_VERIFICATION = 'mandatory' + +# Email backend settings for Django +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +``` +Also you need to configure the Django email backend to send emails. +This depends on the email server you are using and is not covered in this tutorial, please see +[Django SMTP backend configuration](https://docs.djangoproject.com/en/3.1/topics/email/#django.core.mail.backends.smtp.EmailBackend) +for details. + ### Serving over HTTPS We will add [letsencrypt.org](https://letsencrypt.org/) issued certificate to secure diff --git a/cvat/settings/base.py b/cvat/settings/base.py index fb0d2913..6a9a7a6a 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -207,15 +207,17 @@ DJANGO_AUTH_TYPE = 'BASIC' DJANGO_AUTH_DEFAULT_GROUPS = [] LOGIN_URL = 'rest_login' LOGIN_REDIRECT_URL = '/' -AUTH_LOGIN_NOTE = '

Have not registered yet? Register here.

' AUTHENTICATION_BACKENDS = [ 'rules.permissions.ObjectPermissionBackend', - 'django.contrib.auth.backends.ModelBackend' + 'django.contrib.auth.backends.ModelBackend', + 'allauth.account.auth_backends.AuthenticationBackend', ] # https://github.com/pennersr/django-allauth ACCOUNT_EMAIL_VERIFICATION = 'none' +# set UI url to redirect after a successful e-mail confirmation +ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/login' OLD_PASSWORD_FIELD_ENABLED = True # Django-RQ diff --git a/cvat/settings/development.py b/cvat/settings/development.py index 40e597ee..0382d41e 100644 --- a/cvat/settings/development.py +++ b/cvat/settings/development.py @@ -37,6 +37,8 @@ UI_URL = '{}://{}'.format(UI_SCHEME, UI_HOST) if UI_PORT and UI_PORT != '80': UI_URL += ':{}'.format(UI_PORT) +# set UI url to redirect to after successful e-mail confirmation +ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '{}/auth/login'.format(UI_URL) CORS_ORIGIN_WHITELIST = [UI_URL] CORS_REPLACE_HTTPS_REFERER = True diff --git a/cvat/urls.py b/cvat/urls.py index 0d652e9b..db7e65d8 100644 --- a/cvat/urls.py +++ b/cvat/urls.py @@ -27,7 +27,6 @@ urlpatterns = [ path('admin/', admin.site.urls), path('', include('cvat.apps.engine.urls')), path('django-rq/', include('django_rq.urls')), - path('auth/', include('cvat.apps.authentication.urls')), path('documentation/', include('cvat.apps.documentation.urls')), ]