未验证 提交 b7371ede 编写于 作者: K Kirill Lakhov 提交者: GitHub

Enabled authentication via email (#5037)

上级 f719f58d
......@@ -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 (<https://github.com/opencv/cvat/pull/4928>)
- Datumaro version (<https://github.com/opencv/cvat/pull/4984>)
- Enabled authentication via email (<https://github.com/opencv/cvat/pull/5037>)
### Deprecated
- TDB
......
{
"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": {
......
// 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) {
......
......@@ -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('&')
......
// 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) {
......
// 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 {
<Form onFinish={onSubmit} className='login-form'>
<Form.Item
hasFeedback
name='username'
name='credential'
rules={[
{
required: true,
message: 'Please specify a username',
message: 'Please specify a email or username',
},
]}
>
<Input
autoComplete='username'
autoComplete='credential'
prefix={<UserOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Username'
placeholder='Email or Username'
/>
</Form.Item>
......
......@@ -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
<LoginForm
fetching={fetching}
onSubmit={(loginData: LoginData): void => {
onLogin(loginData.username, loginData.password);
onLogin(loginData.credential, loginData.password);
}}
/>
<Row justify='start' align='top'>
......
// 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 (
<Form
form={form}
onFinish={(values: Record<string, string | boolean>) => {
const agreements = Object.keys(values)
.filter((key: string):boolean => key.startsWith('agreement:'));
......@@ -155,44 +159,50 @@ function RegisterFormComponent(props: Props): JSX.Element {
</Row>
<Form.Item
hasFeedback
name='username'
name='email'
rules={[
{
required: true,
message: 'Please specify a username',
type: 'email',
message: 'The input is not valid E-mail!',
},
{
validator: validateUsername,
required: true,
message: 'Please specify an email address',
},
]}
>
<Input
prefix={<UserAddOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Username'
autoComplete='email'
prefix={<MailOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Email address'
onChange={(event) => {
const { value } = event.target;
if (!usernameEdited) {
const [username] = value.split('@');
form.setFieldsValue({ username });
}
}}
/>
</Form.Item>
<Form.Item
hasFeedback
name='email'
name='username'
rules={[
{
type: 'email',
message: 'The input is not valid E-mail!',
required: true,
message: 'Please specify a username',
},
{
required: true,
message: 'Please specify an email address',
validator: validateUsername,
},
]}
>
<Input
autoComplete='email'
prefix={<MailOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Email address'
prefix={<UserAddOutlined style={{ color: 'rgba(0, 0, 0, 0.25)' }} />}
placeholder='Username'
onChange={() => setUsernameEdited(true)}
/>
</Form.Item>
<Form.Item
hasFeedback
name='password1'
......
......@@ -4,8 +4,11 @@
# SPDX-License-Identifier: MIT
from dj_rest_auth.registration.serializers import RegisterSerializer
from dj_rest_auth.serializers import PasswordResetSerializer
from dj_rest_auth.serializers import PasswordResetSerializer, LoginSerializer
from rest_framework.exceptions import ValidationError
from rest_framework import serializers
from allauth.account import app_settings
from allauth.account.utils import filter_users_by_email
from django.conf import settings
......@@ -38,3 +41,21 @@ class PasswordResetSerializerEx(PasswordResetSerializer):
return {
'domain_override': domain
}
class LoginSerializerEx(LoginSerializer):
def get_auth_user_using_allauth(self, username, email, password):
# Authentication through email
if settings.ACCOUNT_AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:
return self._validate_email(email, password)
# Authentication through username
if settings.ACCOUNT_AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME:
return self._validate_username(username, password)
# Authentication through either username or email
if email:
users = filter_users_by_email(email)
if not users or len(users) > 1:
raise ValidationError('Unable to login with provided credentials')
return self._validate_username_email(username, email, password)
......@@ -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'
......
#!/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'
......
......@@ -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'
......
......@@ -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'));
......
......@@ -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}`);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册