diff --git a/CHANGELOG.md b/CHANGELOG.md index 47f84f63..52938473 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - `api/docs`, `api/swagger`, `api/schema` endpoints now allow unauthorized access () - Datumaro version () +- Enabled authentication via email () ### Deprecated - TDB diff --git a/cvat-core/package.json b/cvat-core/package.json index e244b24d..d7941e18 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "7.0.0", + "version": "7.0.1", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/common.ts b/cvat-core/src/common.ts index 41b8957f..10eed7cf 100644 --- a/cvat-core/src/common.ts +++ b/cvat-core/src/common.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -12,6 +13,10 @@ export function isInteger(value): boolean { return typeof value === 'number' && Number.isInteger(value); } +export function isEmail(value): boolean { + return typeof value === 'string' && RegExp(/^[^\s@]+@[^\s@]+\.[^\s@]+$/).test(value); +} + // Called with specific Enum context export function isEnum(value): boolean { for (const key in this) { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index f78bc923..156f1f28 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: MIT +import { isEmail } from './common'; import { StorageLocation, WebhookSourceType } from './enums'; import { Storage } from './storage'; @@ -325,9 +326,9 @@ class ServerProxy { return response.data; } - async function login(username, password) { + async function login(credential, password) { const authenticationData = [ - `${encodeURIComponent('username')}=${encodeURIComponent(username)}`, + `${encodeURIComponent(isEmail(credential) ? 'email' : 'username')}=${encodeURIComponent(credential)}`, `${encodeURIComponent('password')}=${encodeURIComponent(password)}`, ] .join('&') diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index bc908253..143a8be1 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -100,11 +101,11 @@ export const registerAsync = ( } }; -export const loginAsync = (username: string, password: string): ThunkAction => async (dispatch) => { +export const loginAsync = (credential: string, password: string): ThunkAction => async (dispatch) => { dispatch(authActions.login()); try { - await cvat.server.login(username, password); + await cvat.server.login(credential, password); const users = await cvat.users.get({ self: true }); dispatch(authActions.loginSuccess(users[0])); } catch (error) { diff --git a/cvat-ui/src/components/login-page/login-form.tsx b/cvat-ui/src/components/login-page/login-form.tsx index d57fe9c2..fb7e622e 100644 --- a/cvat-ui/src/components/login-page/login-form.tsx +++ b/cvat-ui/src/components/login-page/login-form.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,7 +10,7 @@ import Input from 'antd/lib/input'; import { UserOutlined, LockOutlined } from '@ant-design/icons'; export interface LoginData { - username: string; + credential: string; password: string; } @@ -24,18 +25,18 @@ function LoginFormComponent(props: Props): JSX.Element {
} - placeholder='Username' + placeholder='Email or Username' /> diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 63b63286..9572a1ac 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -16,7 +16,7 @@ import LoginForm, { LoginData } from './login-form'; interface LoginPageComponentProps { fetching: boolean; renderResetPassword: boolean; - onLogin: (username: string, password: string) => void; + onLogin: (credential: string, password: string) => void; } function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element { @@ -40,7 +40,7 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps { - onLogin(loginData.username, loginData.password); + onLogin(loginData.credential, loginData.password); }} /> diff --git a/cvat-ui/src/components/register-page/register-form.tsx b/cvat-ui/src/components/register-page/register-form.tsx index 192dc1cd..b81e3f91 100644 --- a/cvat-ui/src/components/register-page/register-form.tsx +++ b/cvat-ui/src/components/register-page/register-form.tsx @@ -1,8 +1,9 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2022 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { useState } from 'react'; import { UserAddOutlined, MailOutlined, LockOutlined } from '@ant-design/icons'; import Form, { RuleRender, RuleObject } from 'antd/lib/form'; import Button from 'antd/lib/button'; @@ -98,8 +99,11 @@ const validateAgreement: ((userAgreements: UserAgreement[]) => RuleRender) = ( function RegisterFormComponent(props: Props): JSX.Element { const { fetching, userAgreements, onSubmit } = props; + const [form] = Form.useForm(); + const [usernameEdited, setUsernameEdited] = useState(false); return ( ) => { const agreements = Object.keys(values) .filter((key: string):boolean => key.startsWith('agreement:')); @@ -155,44 +159,50 @@ function RegisterFormComponent(props: Props): JSX.Element { } - placeholder='Username' + autoComplete='email' + prefix={} + placeholder='Email address' + onChange={(event) => { + const { value } = event.target; + if (!usernameEdited) { + const [username] = value.split('@'); + form.setFieldsValue({ username }); + } + }} /> - } - placeholder='Email address' + prefix={} + placeholder='Username' + onChange={() => setUsernameEdited(true)} /> - 1: + raise ValidationError('Unable to login with provided credentials') + + return self._validate_username_email(username, email, password) diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 4a18a9e4..6eeae762 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -188,6 +188,7 @@ REST_AUTH_REGISTER_SERIALIZERS = { } REST_AUTH_SERIALIZERS = { + 'LOGIN_SERIALIZER': 'cvat.apps.iam.serializers.LoginSerializerEx', 'PASSWORD_RESET_SERIALIZER': 'cvat.apps.iam.serializers.PasswordResetSerializerEx', } @@ -258,6 +259,7 @@ AUTHENTICATION_BACKENDS = [ # https://github.com/pennersr/django-allauth ACCOUNT_EMAIL_VERIFICATION = 'none' +ACCOUNT_AUTHENTICATION_METHOD = 'username_email' # set UI url to redirect after a successful e-mail confirmation #changed from '/auth/login' to '/auth/email-confirmation' for email confirmation message ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = '/auth/email-confirmation' diff --git a/cvat/settings/email_settings.py b/cvat/settings/email_settings.py index c7fdaa4e..ca84a575 100644 --- a/cvat/settings/email_settings.py +++ b/cvat/settings/email_settings.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # Copyright (C) 2020-2022 Intel Corporation +# Copyright (C) 2022 CVAT.ai Corporation # # SPDX-License-Identifier: MIT @@ -8,7 +9,7 @@ from cvat.settings.production import * # https://github.com/pennersr/django-allauth -ACCOUNT_AUTHENTICATION_METHOD = 'username' +ACCOUNT_AUTHENTICATION_METHOD = 'username_email' ACCOUNT_CONFIRM_EMAIL_ON_GET = True ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' diff --git a/site/content/en/docs/administration/basics/installation.md b/site/content/en/docs/administration/basics/installation.md index 8f4199d0..087dc1c9 100644 --- a/site/content/en/docs/administration/basics/installation.md +++ b/site/content/en/docs/administration/basics/installation.md @@ -472,7 +472,7 @@ 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_AUTHENTICATION_METHOD = 'username_email' ACCOUNT_CONFIRM_EMAIL_ON_GET = True ACCOUNT_EMAIL_REQUIRED = True ACCOUNT_EMAIL_VERIFICATION = 'mandatory' diff --git a/tests/cypress/integration/actions_users/issue_1810_login_logout.js b/tests/cypress/integration/actions_users/issue_1810_login_logout.js index 2761818f..858c626c 100644 --- a/tests/cypress/integration/actions_users/issue_1810_login_logout.js +++ b/tests/cypress/integration/actions_users/issue_1810_login_logout.js @@ -10,8 +10,8 @@ context('When clicking on the Logout button, get the user session closed.', () = const issueId = '1810'; let taskId; - function login(userName, password) { - cy.get('[placeholder="Username"]').clear().type(userName); + function login(credential, password) { + cy.get('[placeholder="Email or Username"]').clear().type(credential); cy.get('[placeholder="Password"]').clear().type(password); cy.get('[type="submit"]').click(); } @@ -73,6 +73,12 @@ context('When clicking on the Logout button, get the user session closed.', () = }); }); + it('Login via email', () => { + cy.logout(); + login(Cypress.env('email'), Cypress.env('password')); + cy.url().should('contain', '/tasks'); + }); + it('Incorrect user and correct password', () => { cy.logout(); login('randomUser123', Cypress.env('password')); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index d326857f..0ecc3eda 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -20,7 +20,7 @@ require('cy-verify-downloads').addCustomCommand(); let selectedValueGlobal = ''; Cypress.Commands.add('login', (username = Cypress.env('user'), password = Cypress.env('password'), page = 'tasks') => { - cy.get('[placeholder="Username"]').type(username); + cy.get('[placeholder="Email or Username"]').type(username); cy.get('[placeholder="Password"]').type(password); cy.get('[type="submit"]').click(); cy.url().should('contain', `/${page}`);