diff --git a/CHANGELOG.md b/CHANGELOG.md index ed2f3801538..0e9054947f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - cvat-ui: added cookie policy drawer for login page () - Added `datumaro_project` export format (https://github.com/opencv/cvat/pull/1352) +- Ability to configure user agreements for the user registration form (https://github.com/opencv/cvat/pull/1464) ### Changed - Downloaded file name in annotations export became more informative (https://github.com/opencv/cvat/pull/1352) diff --git a/Dockerfile.ui b/Dockerfile.ui index 80fe3c7fd10..c0b04e3d104 100644 --- a/Dockerfile.ui +++ b/Dockerfile.ui @@ -4,6 +4,7 @@ ARG http_proxy ARG https_proxy ARG no_proxy ARG socks_proxy +ARG PUBLIC_INSTANCE ENV TERM=xterm \ http_proxy=${http_proxy} \ diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 2147c19c9cb..da83fcac47e 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -69,10 +69,15 @@ return new AnnotationFormats(result); }; + cvat.server.userAgreements.implementation = async () => { + const result = await serverProxy.server.userAgreements(); + return result; + }; + cvat.server.register.implementation = async (username, firstName, lastName, - email, password1, password2) => { + email, password1, password2, userConfirmations) => { await serverProxy.server.register(username, firstName, lastName, email, - password1, password2); + password1, password2, userConfirmations); }; cvat.server.login.implementation = async (username, password) => { diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 777d3fc0187..0e04c5c8fc6 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -119,6 +119,21 @@ function build() { return result; }, /** + * Method returns user agreements that the user must accept + * @method userAgreements + * @async + * @memberof module:API.cvat.server + * @returns {Object[]} + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + */ + async userAgreements() { + const result = await PluginRegistry + .apiWrapper(cvat.server.userAgreements); + return result; + }, + /** + * Method allows to register on a server * @method register * @async @@ -129,13 +144,14 @@ function build() { * @param {string} email A email address for the new account * @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 * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ServerError} */ - async register(username, firstName, lastName, email, password1, password2) { + async register(username, firstName, lastName, email, password1, password2, userConfirmations) { const result = await PluginRegistry .apiWrapper(cvat.server.register, username, firstName, - lastName, email, password1, password2); + lastName, email, password1, password2, userConfirmations); return result; }, /** diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 2c28ea27d98..5b685114e3a 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -154,7 +154,23 @@ return response.data; } - async function register(username, firstName, lastName, email, password1, password2) { + + async function userAgreements() { + const { backendAPI } = config; + let response = null; + try { + response = await Axios.get(`${backendAPI}/restrictions/user-agreements`, { + proxy: config.proxy, + }); + + } catch (errorData) { + throw generateError(errorData); + } + + return response.data; + } + + async function register(username, firstName, lastName, email, password1, password2, confirmations) { let response = null; try { const data = JSON.stringify({ @@ -164,6 +180,7 @@ email, password1, password2, + confirmations, }); response = await Axios.post(`${config.backendAPI}/auth/register`, data, { proxy: config.proxy, @@ -657,6 +674,7 @@ authorized, register, request: serverRequest, + userAgreements, }), writable: false, }, diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 6d610735753..ed6ecd30d72 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -3,6 +3,7 @@ // SPDX-License-Identifier: MIT import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import { UserConfirmation } from 'components/register-page/register-form'; import getCore from 'cvat-core-wrapper'; const cvat = getCore(); @@ -44,13 +45,14 @@ export const registerAsync = ( email: string, password1: string, password2: string, + confirmations: UserConfirmation[], ): ThunkAction => async ( dispatch, ) => { dispatch(authActions.register()); try { - await cvat.server.register(username, firstName, lastName, email, password1, password2); + await cvat.server.register(username, firstName, lastName, email, password1, password2, confirmations); const users = await cvat.users.get({ self: true }); dispatch(authActions.registerSuccess(users[0])); diff --git a/cvat-ui/src/actions/useragreements-actions.ts b/cvat-ui/src/actions/useragreements-actions.ts new file mode 100644 index 00000000000..29645ddbf22 --- /dev/null +++ b/cvat-ui/src/actions/useragreements-actions.ts @@ -0,0 +1,38 @@ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import getCore from 'cvat-core-wrapper'; +import { UserAgreement } from 'reducers/interfaces' + +const core = getCore(); + +export enum UserAgreementsActionTypes { + GET_USER_AGREEMENTS = 'GET_USER_AGREEMENTS', + GET_USER_AGREEMENTS_SUCCESS = 'GET_USER_AGREEMENTS_SUCCESS', + GET_USER_AGREEMENTS_FAILED = 'GET_USER_AGREEMENTS_FAILED', +} + +const userAgreementsActions = { + getUserAgreements: () => createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS), + getUserAgreementsSuccess: (userAgreements: UserAgreement[]) => + createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_SUCCESS, userAgreements), + getUserAgreementsFailed: (error: any) => + createAction(UserAgreementsActionTypes.GET_USER_AGREEMENTS_FAILED, { error }), +}; + +export type UserAgreementsActions = ActionUnion; + +export const getUserAgreementsAsync = (): ThunkAction => async (dispatch): Promise => { + dispatch(userAgreementsActions.getUserAgreements()); + + try { + const userAgreements = await core.server.userAgreements(); + dispatch( + userAgreementsActions.getUserAgreementsSuccess(userAgreements), + ); + } catch (error) { + dispatch(userAgreementsActions.getUserAgreementsFailed(error)); + } +}; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 82b6ca103cb..fdc7f9157fe 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -33,6 +33,7 @@ interface CVATAppProps { loadUsers: () => void; loadAbout: () => void; verifyAuthorized: () => void; + loadUserAgreements: () => void; initPlugins: () => void; resetErrors: () => void; resetMessages: () => void; @@ -51,6 +52,8 @@ interface CVATAppProps { installedAutoAnnotation: boolean; installedTFAnnotation: boolean; installedTFSegmentation: boolean; + userAgreementsFetching: boolean, + userAgreementsInitialized: boolean, notifications: NotificationsState; user: any; } @@ -58,7 +61,7 @@ interface CVATAppProps { class CVATApplication extends React.PureComponent { public componentDidMount(): void { const core = getCore(); - const { verifyAuthorized } = this.props; + const { verifyAuthorized, loadUserAgreements } = this.props; configure({ ignoreRepeatedEventsWhenKeyHeldDown: false }); // Logger configuration @@ -77,6 +80,7 @@ class CVATApplication extends React.PureComponent { callback(); }; + private validateAgrement = (agreement: any, value: any, callback: any): void => { + const { userAgreements } = this.props; + let isValid: boolean = true; + for (const userAgreement of userAgreements) { + if (agreement.field === userAgreement.name + && userAgreement.required && !value) { + isValid = false; + callback(`You must accept the ${userAgreement.displayText} to continue!`); + break; + } + } + if (isValid) { + callback(); + } + }; + private handleSubmit = (e: React.FormEvent): void => { e.preventDefault(); const { form, onSubmit, + userAgreements, } = this.props; form.validateFields((error, values): void => { if (!error) { + values.confirmations = [] + + for (const userAgreement of userAgreements) { + + values.confirmations.push({ + name: userAgreement.name, + value: values[userAgreement.name] + }); + delete values[userAgreement.name]; + } + onSubmit(values); } }); @@ -214,6 +252,38 @@ class RegisterFormComponent extends React.PureComponent { ); } + private renderUserAgreements(): JSX.Element[] { + const { form, userAgreements } = this.props; + const getUserAgreementsElements = () => + { + const agreementsList: JSX.Element[] = []; + for (const userAgreement of userAgreements) { + agreementsList.push( + + {form.getFieldDecorator(userAgreement.name, { + initialValue: false, + valuePropName: 'checked', + rules: [{ + required: true, + message: 'You must accept to continue!', + }, { + validator: this.validateAgrement, + }] + })( + + I read and accept the { userAgreement.displayText } + + )} + + ); + } + return agreementsList; + } + + return getUserAgreementsElements(); + } + public render(): JSX.Element { const { fetching } = this.props; @@ -225,6 +295,7 @@ class RegisterFormComponent extends React.PureComponent { {this.renderEmailField()} {this.renderPasswordField()} {this.renderPasswordConfirmationField()} + {this.renderUserAgreements()}