Skip to content

Commit

Permalink
Added password reset functionality (#2058)
Browse files Browse the repository at this point in the history
* added reset password functionality

* updated changelog and versions of cvat-core, cvat-ui

* fixed comments

* Update cvat-ui/src/components/reset-password-confirm-page/reset-password-confirm-form.tsx

* Fix CHANGELOG

* fixed comments

Co-authored-by: Nikita Manovich <[email protected]>
  • Loading branch information
Andrey Zhavoronkov and Nikita Manovich authored Sep 1, 2020
1 parent a688442 commit 510191f
Show file tree
Hide file tree
Showing 21 changed files with 699 additions and 20 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.2.0] - Unreleased
### Added
-
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>)

### Changed
- UI models (like DEXTR) were redesigned to be more interactive (<https://github.com/opencv/cvat/pull/2054>)
Expand Down
8 changes: 8 additions & 0 deletions cvat-core/src/api-implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@
await serverProxy.server.changePassword(oldPassword, newPassword1, newPassword2);
};

cvat.server.requestPasswordReset.implementation = async (email) => {
await serverProxy.server.requestPasswordReset(email);
};

cvat.server.resetPassword.implementation = async(newPassword1, newPassword2, uid, token) => {
await serverProxy.server.resetPassword(newPassword1, newPassword2, uid, token);
};

cvat.server.authorized.implementation = async () => {
const result = await serverProxy.server.authorized();
return result;
Expand Down
35 changes: 35 additions & 0 deletions cvat-core/src/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ function build() {
* @method changePassword
* @async
* @memberof module:API.cvat.server
* @param {string} oldPassword Current password for the account
* @param {string} newPassword1 New password for the account
* @param {string} newPassword2 Confirmation password for the account
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
Expand All @@ -207,6 +210,38 @@ function build() {
.apiWrapper(cvat.server.changePassword, oldPassword, newPassword1, newPassword2);
return result;
},
/**
* Method allows to reset user password
* @method requestPasswordReset
* @async
* @memberof module:API.cvat.server
* @param {string} email A email address for the account
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async requestPasswordReset(email) {
const result = await PluginRegistry
.apiWrapper(cvat.server.requestPasswordReset, email);
return result;
},
/**
* Method allows to confirm reset user password
* @method resetPassword
* @async
* @memberof module:API.cvat.server
* @param {string} newPassword1 New password for the account
* @param {string} newPassword2 Confirmation password for the account
* @param {string} uid User id
* @param {string} token Request authentication token
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async resetPassword(newPassword1, newPassword2, uid, token) {
const result = await PluginRegistry
.apiWrapper(cvat.server.resetPassword, newPassword1, newPassword2,
uid, token);
return result;
},
/**
* Method allows to know whether you are authorized on the server
* @method authorized
Expand Down
37 changes: 37 additions & 0 deletions cvat-core/src/server-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,41 @@
}
}

async function requestPasswordReset(email) {
try {
const data = JSON.stringify({
email,
});
await Axios.post(`${config.backendAPI}/auth/password/reset`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}

async function resetPassword(newPassword1, newPassword2, uid, token) {
try {
const data = JSON.stringify({
new_password1: newPassword1,
new_password2: newPassword2,
uid,
token,
});
await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}

async function authorized() {
try {
await module.exports.users.getSelf();
Expand Down Expand Up @@ -787,6 +822,8 @@
login,
logout,
changePassword,
requestPasswordReset,
resetPassword,
authorized,
register,
request: serverRequest,
Expand Down
2 changes: 1 addition & 1 deletion cvat-ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cvat-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.8.4",
"version": "1.9.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
Expand Down
62 changes: 57 additions & 5 deletions cvat-ui/src/actions/auth-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export enum AuthActionTypes {
CHANGE_PASSWORD_SUCCESS = 'CHANGE_PASSWORD_SUCCESS',
CHANGE_PASSWORD_FAILED = 'CHANGE_PASSWORD_FAILED',
SWITCH_CHANGE_PASSWORD_DIALOG = 'SWITCH_CHANGE_PASSWORD_DIALOG',
REQUEST_PASSWORD_RESET = 'REQUEST_PASSWORD_RESET',
REQUEST_PASSWORD_RESET_SUCCESS = 'REQUEST_PASSWORD_RESET_SUCCESS',
REQUEST_PASSWORD_RESET_FAILED = 'REQUEST_PASSWORD_RESET_FAILED',
RESET_PASSWORD = 'RESET_PASSWORD_CONFIRM',
RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS',
RESET_PASSWORD_FAILED = 'RESET_PASSWORD_CONFIRM_FAILED',
LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS',
LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS',
LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED',
Expand All @@ -50,9 +56,22 @@ export const authActions = {
switchChangePasswordDialog: (showChangePasswordDialog: boolean) => (
createAction(AuthActionTypes.SWITCH_CHANGE_PASSWORD_DIALOG, { showChangePasswordDialog })
),
requestPasswordReset: () => createAction(AuthActionTypes.REQUEST_PASSWORD_RESET),
requestPasswordResetSuccess: () => createAction(AuthActionTypes.REQUEST_PASSWORD_RESET_SUCCESS),
requestPasswordResetFailed: (error: any) => (
createAction(AuthActionTypes.REQUEST_PASSWORD_RESET_FAILED, { error })
),
resetPassword: () => createAction(AuthActionTypes.RESET_PASSWORD),
resetPasswordSuccess: () => createAction(AuthActionTypes.RESET_PASSWORD_SUCCESS),
resetPasswordFailed: (error: any) => (
createAction(AuthActionTypes.RESET_PASSWORD_FAILED, { error })
),
loadServerAuthActions: () => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS),
loadServerAuthActionsSuccess: (allowChangePassword: boolean) => (
createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, { allowChangePassword })
loadServerAuthActionsSuccess: (allowChangePassword: boolean, allowResetPassword: boolean) => (
createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, {
allowChangePassword,
allowResetPassword,
})
),
loadServerAuthActionsFailed: (error: any) => (
createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error })
Expand Down Expand Up @@ -135,16 +154,49 @@ export const changePasswordAsync = (oldPassword: string,
}
};

export const requestPasswordResetAsync = (email: string): ThunkAction => async (dispatch) => {
dispatch(authActions.requestPasswordReset());

try {
await cvat.server.requestPasswordReset(email);
dispatch(authActions.requestPasswordResetSuccess());
} catch (error) {
dispatch(authActions.requestPasswordResetFailed(error));
}
};

export const resetPasswordAsync = (
newPassword1: string,
newPassword2: string,
uid: string,
token: string,
): ThunkAction => async (dispatch) => {
dispatch(authActions.resetPassword());

try {
await cvat.server.resetPassword(newPassword1, newPassword2, uid, token);
dispatch(authActions.resetPasswordSuccess());
} catch (error) {
dispatch(authActions.resetPasswordFailed(error));
}
};

export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.loadServerAuthActions());

try {
const promises: Promise<boolean>[] = [
isReachable(`${cvat.config.backendAPI}/auth/password/change`, 'OPTIONS'),
isReachable(`${cvat.config.backendAPI}/auth/password/reset`, 'OPTIONS'),
];
const [allowChangePassword] = await Promise.all(promises);

dispatch(authActions.loadServerAuthActionsSuccess(allowChangePassword));
const [
allowChangePassword,
allowResetPassword] = await Promise.all(promises);

dispatch(authActions.loadServerAuthActionsSuccess(
allowChangePassword,
allowResetPassword,
));
} catch (error) {
dispatch(authActions.loadServerAuthActionsFailed(error));
}
Expand Down
5 changes: 4 additions & 1 deletion cvat-ui/src/components/cvat-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import ModelsPageContainer from 'containers/models-page/models-page';
import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from 'containers/register-page/register-page';
import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page';
import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page';
import Header from 'components/header/header';
import { customWaViewHit } from 'utils/enviroment';
import showPlatformNotification, { stopNotifications, platformInfo } from 'utils/platform-checker';
Expand Down Expand Up @@ -61,7 +63,6 @@ interface CVATAppProps {
userAgreementsInitialized: boolean;
authActionsFetching: boolean;
authActionsInitialized: boolean;
allowChangePassword: boolean;
notifications: NotificationsState;
user: any;
}
Expand Down Expand Up @@ -332,6 +333,8 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Route exact path='/auth/password/reset' component={ResetPasswordPageComponent} />
<Route exact path='/auth/password/reset/confirm' component={ResetPasswordPageConfirmComponent} />
<Redirect to='/auth/login' />
</Switch>
</GlobalErrorBoundary>
Expand Down
12 changes: 12 additions & 0 deletions cvat-ui/src/components/login-page/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import CookieDrawer from './cookie-policy-drawer';

interface LoginPageComponentProps {
fetching: boolean;
renderResetPassword: boolean;
onLogin: (username: string, password: string) => void;
}

Expand All @@ -29,6 +30,7 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps
const {
fetching,
onLogin,
renderResetPassword,
} = props;

return (
Expand All @@ -50,6 +52,16 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps
</Text>
</Col>
</Row>
{ renderResetPassword
&& (
<Row type='flex' justify='start' align='top'>
<Col>
<Text strong>
<Link to='/auth/password/reset'>Forgot your password?</Link>
</Text>
</Col>
</Row>
)}
</Col>
</Row>
<CookieDrawer />
Expand Down
Loading

0 comments on commit 510191f

Please sign in to comment.