diff --git a/__tests__/app/containers/Authentication/__snapshots__/Login.test.tsx.snap b/__tests__/app/containers/Authentication/__snapshots__/Login.test.tsx.snap index efd2a5a7..c4db6d25 100644 --- a/__tests__/app/containers/Authentication/__snapshots__/Login.test.tsx.snap +++ b/__tests__/app/containers/Authentication/__snapshots__/Login.test.tsx.snap @@ -3,6 +3,7 @@ exports[`Login Container Should render 1`] = ` action(Type.CHECK_USERNAME_EXISTS, { username }); + export const changeUserPassword = ( + newPassword: string, + passwordResetToken: string, + userId: number, + ) => + action(Type.CHANGE_USER_PASSWORD, { + newPassword, + passwordResetToken, + userId, + }); + export const toggleUserProfileModal = (isUserProfileModalOpen: boolean) => action(Type.TOGGLE_USER_PROFILE_MODAL, { isUserProfileModalOpen }); @@ -82,4 +95,6 @@ export namespace UserActions { export const setIsLoginLoading = (isLoginLoading: boolean) => action(Type.SET_IS_LOGIN_LOADING, { isLoginLoading }); + + export const forgotPassword = (email: string) => action(Type.FORGOT_PASSWORD, { email }); } diff --git a/src/app/apiFetch/Code.ts b/src/app/apiFetch/Code.ts index 07ba5f06..58d0961c 100644 --- a/src/app/apiFetch/Code.ts +++ b/src/app/apiFetch/Code.ts @@ -131,7 +131,6 @@ export const getLastSaveTime = () => { return textResponseWrapper(response); }) .then((data) => { - console.log(data); return data; }) .catch((error) => { diff --git a/src/app/apiFetch/User.ts b/src/app/apiFetch/User.ts index 8b5dcc8a..c503eb21 100644 --- a/src/app/apiFetch/User.ts +++ b/src/app/apiFetch/User.ts @@ -1,5 +1,10 @@ /* tslint:disable:no-console*/ -import { HeadReqType, headResponseWrapper, jsonResponseWrapper } from 'app/apiFetch/utils'; +import { + HeadReqType, + headResponseWrapper, + jsonResponseWrapper, + textResponseWrapper, +} from 'app/apiFetch/utils'; import * as UserInterfaces from 'app/types/User'; import { API_BASE_URL } from '../../config/config'; @@ -131,6 +136,43 @@ export const userEditPassword = (body: UserInterfaces.EditUserPassword) => { }); }; +export const changeUserPassword = (body: UserInterfaces.ChangeUserPassword) => { + return fetch(`${API_BASE_URL}user/password`, { + body: JSON.stringify(body), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + .then((response) => { + return textResponseWrapper(response); + }) + .then((data) => { + return data; + }) + .catch((error) => { + console.error(error); + }); +}; +export const userForgotPassword = (email: string) => { + return fetch(`${API_BASE_URL}user/forgot-password`, { + body: email, + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + .then((response) => { + return response.text(); + }) + .then((data) => { + return data; + }) + .catch((error) => { + console.error(error); + }); +}; + export const userGetDetails = () => { return fetch(`${API_BASE_URL}user`, { credentials: 'include', diff --git a/src/app/apiFetch/utils/index.ts b/src/app/apiFetch/utils/index.ts index cd609447..fb8ec5a7 100644 --- a/src/app/apiFetch/utils/index.ts +++ b/src/app/apiFetch/utils/index.ts @@ -90,8 +90,11 @@ export function textResponseWrapper(response: any) { case 302: case 409: case 401: + case 404: + case 403: case 500: case 403: + case 501: error = 'Oops! Something went wrong.'; type = resType.ERROR; } diff --git a/src/app/components/Authentication/ChangePassword.tsx b/src/app/components/Authentication/ChangePassword.tsx new file mode 100644 index 00000000..6ad682ca --- /dev/null +++ b/src/app/components/Authentication/ChangePassword.tsx @@ -0,0 +1,146 @@ +import * as authStyles from 'app/styles/Authentication.module.css'; +import * as registerStyles from 'app/styles/Register.module.css'; +import { changePasswordProps, ChangePasswordState } from 'app/types/Authentication/ChangePassword'; +import classnames from 'classnames'; +import * as React from 'react'; + +export class ChangePassword extends React.Component { + private credentialsFormRef = React.createRef(); + private passwordResetToken = ''; + private userId = 0; + constructor(props: changePasswordProps) { + super(props); + this.state = { + password: '', + passwordError: '', + repeatPassword: '', + }; + } + + public componentDidMount() { + // get string from url + const search = this.props.location.search; + const urlParams = search.split('&'); + this.passwordResetToken = urlParams[0].split('=')[1]; + this.userId = parseInt(urlParams[1].split('=')[1], 0); + } + + public render() { + return ( +
+
+

Reset Password

+

Enter your new password

+
+
+
+
+ +
New Password
+
+ + this.setState({ + password: e.target.value, + }) + } + required + /> +
+ Password should have minimum 5 characters. +
+
+
Confirm Password
+
+ + this.setState({ + repeatPassword: e.target.value, + }) + } + required + /> +
+ +
+
+ {this.state.passwordError} + {'\n'} + {this.props.errorMessage} +
+
+
+ +
+ +
+ +
+
+ ); + } + + private submitPassword = (e: React.MouseEvent) => { + e.preventDefault(); + if (this.state.password === this.state.repeatPassword) { + if (this.credentialsFormRef.current) { + this.credentialsFormRef.current.classList.add('was-validated'); + if (this.credentialsFormRef.current.checkValidity()) { + this.setState({ + ...this.state, + passwordError: '', + }); + this.props.changePassword(this.state.password, this.passwordResetToken, this.userId); + } + } + } else { + this.setState({ + ...this.state, + passwordError: 'Password and confirm passwords have different values', + }); + } + }; +} diff --git a/src/app/components/Authentication/ForgotPassword.tsx b/src/app/components/Authentication/ForgotPassword.tsx new file mode 100644 index 00000000..246ef40e --- /dev/null +++ b/src/app/components/Authentication/ForgotPassword.tsx @@ -0,0 +1,131 @@ +import { Routes } from 'app/routes'; +import * as styles from 'app/styles/Authentication.module.css'; +import * as registerStyles from 'app/styles/Register.module.css'; +import { AuthType } from 'app/types/Authentication'; +import * as LoginInterfaces from 'app/types/Authentication/Login'; +import classnames from 'classnames'; +import * as React from 'react'; +import { Col, Row } from 'react-bootstrap'; + +// tslint:disable-next-line: variable-name +const ForgotPassword = (props: LoginInterfaces.ForgotPasswordProps) => { + const forgotPasswordRef = React.createRef(); + + const handleForgotPassword = (event: React.FormEvent) => { + const form = forgotPasswordRef.current; + + event.preventDefault(); + if (form) { + if (form.checkValidity()) { + props.forgotPassword(props.username); + } + form.classList.add('was-validated'); + } + }; + + return ( +
+
+
+

Forgot your password?

+
+ + +
+ +
+
+
+
+
Your Email:
+
+ props.setUsername(e.target.value)} + /> +
+ {' '} + Please enter a valid Email.{' '} +
+
+
+
+
+
+ {props.errorMessage} +
+
+
+
+ +
+
+
+
+
+
+ +
+ + +
props.closeForgotPassword()} + > + Back{' '} +
+ +
+ + + + + +
+
+ ); +}; + +export default ForgotPassword; diff --git a/src/app/components/Authentication/Login.tsx b/src/app/components/Authentication/Login.tsx index e7bbc7da..ceb623be 100644 --- a/src/app/components/Authentication/Login.tsx +++ b/src/app/components/Authentication/Login.tsx @@ -1,6 +1,7 @@ import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { API_BASE_URL } from 'app/../config/config'; +import ForgotPassword from 'app/components/Authentication/ForgotPassword'; import PopUpMenu from 'app/components/PopUpMenu'; import { Routes } from 'app/routes'; import * as styles from 'app/styles/Authentication.module.css'; @@ -16,7 +17,6 @@ export enum OAUTH_ROUTES { GOOGLE = 'login/google', GITHUB = 'login/github', } - export class Login extends React.Component { private loginRef = React.createRef(); @@ -24,6 +24,7 @@ export class Login extends React.Component; } - - return ( -
-
-

Welcome!

-

Log in to access your dashboard and profile

-
- You can use your{' '} - - Pragyan - {' '} - account credentials to login. -
-
-
- { - window.location.href = `${API_BASE_URL}${OAUTH_ROUTES.GOOGLE}`; - }} - className={classnames( - styles['google-btn'], - 'border justify-content-center my-3', - styles.oauth_btn, - styles.no_margin, - )} - > -
- -
-

Log in with Google

-
- { - window.location.href = `${API_BASE_URL}${OAUTH_ROUTES.GITHUB}`; - }} - className={classnames( - 'justify-content-center', - styles['github-btn'], - styles.oauth_btn, - styles.no_margin, - )} - > -
- + if (errorMessage === ' ' && isForgotPasswordOpen) { + this.setState({ + isForgotPasswordOpen: false, + }); + updateErrorMessage(''); + } + if (!isForgotPasswordOpen) { + return ( +
+
+

Welcome!

+

Log in to access your dashboard and profile

+
+ You can use your{' '} + + Pragyan + {' '} + account credentials to login.
-

Log in with Github

- - -
-
- or +
+
+ { + window.location.href = `${API_BASE_URL}${OAUTH_ROUTES.GOOGLE}`; + }} + className={classnames( + styles['google-btn'], + 'border justify-content-center my-3', + styles.oauth_btn, + styles.no_margin, + )} + > +
+
-
- -
- -
-
Log in with Google

+ + { + window.location.href = `${API_BASE_URL}${OAUTH_ROUTES.GITHUB}`; + }} + className={classnames( + 'justify-content-center', + styles['github-btn'], + styles.oauth_btn, + styles.no_margin, + )} > -
-
-
Email
-
- - this.setState({ - username: e.target.value, - }) - } - /> -
- {' '} - Please enter a valid Email.{' '} +
+ +
+

Log in with Github

+ + +
+
+ or +
+
+
+
+ +
+ +
+
+
Email
+
+ + this.setState({ + username: e.target.value, + }) + } + /> +
+ {' '} + Please enter a valid Email.{' '} +
-
-
-
-
Password
-
- - this.setState({ - password: e.target.value, - }) - } - required - /> -
- Please enter the correct password. +
+
+
Password
+
+ + this.setState({ + password: e.target.value, + }) + } + required + /> +
+ Please enter the correct password. +
-
-
-
- {errorMessage} +
+
+ {errorMessage} +
-
-
-
- +
+
+ +
-
- -
- - - - - - + + + +
this.setState({ isForgotPasswordOpen: true })} + > + Forgot Your Password?{' '} +
+ +
+ + + + + + +
+ ); + } + return ( +
+ + this.setState({ + username: newUsername, + }) + } + closeForgotPassword={() => + this.setState({ + isForgotPasswordOpen: false, + }) + } + />
); diff --git a/src/app/components/Authentication/Register.tsx b/src/app/components/Authentication/Register.tsx index 2a1f3186..6dd0ad0e 100644 --- a/src/app/components/Authentication/Register.tsx +++ b/src/app/components/Authentication/Register.tsx @@ -70,6 +70,7 @@ export class Register extends React.Component { + return { + errorMessage: rootState.user.errorMessage, + }; +}; + +const changePasswordContainer = connect(mapStateToProps, { + changePassword: UserActions.changeUserPassword, +})(ChangePassword); + +export default changePasswordContainer; diff --git a/src/app/containers/Authentication/Login.ts b/src/app/containers/Authentication/Login.ts index 7f3eee77..7d900daf 100644 --- a/src/app/containers/Authentication/Login.ts +++ b/src/app/containers/Authentication/Login.ts @@ -15,6 +15,7 @@ const mapStateToProps = (rootState: RootState) => { const loginContainer = connect( mapStateToProps, { + forgotPassword: UserActions.forgotPassword, login: UserActions.login, updateErrorMessage: UserActions.updateErrorMessage, }, diff --git a/src/app/index.tsx b/src/app/index.tsx index d70d20c7..49d531db 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,4 +1,5 @@ import ActivateUser from 'app/containers/Authentication/ActivateUser'; +import ChangePassword from 'app/containers/Authentication/ChangePassword'; import Login from 'app/containers/Authentication/Login'; import Register from 'app/containers/Authentication/Register'; import Dashboard from 'app/containers/Dashboard'; @@ -25,6 +26,7 @@ export const App = hot(module)(() => ( + diff --git a/src/app/routes.ts b/src/app/routes.ts index 7b0c8cb8..174ec139 100644 --- a/src/app/routes.ts +++ b/src/app/routes.ts @@ -7,4 +7,5 @@ export enum Routes { GITHUB_OAUTH = '/login/github', GOOGLE_OAUTH = '/login/google', USER_ACTIVATION = '/user-activate', + CHANGE_PASSWORD = '/reset-password', } diff --git a/src/app/sagas/User.ts b/src/app/sagas/User.ts index c3435890..78995d05 100644 --- a/src/app/sagas/User.ts +++ b/src/app/sagas/User.ts @@ -209,6 +209,19 @@ export function* editUserPassword(action: ActionType) { + try { + const res = yield call(UserFetch.changeUserPassword, action.payload); + yield put(UserActions.updateErrorMessage(res.error ? res.body.message : '')); + + if (res.type !== resType.ERROR) { + window.location.assign('/login'); + } + } catch (err) { + console.error(err); + } +} + export function* checkEmailExists(action: ActionType) { try { const res = yield call(UserFetch.checkEmailExists, action.payload.email); @@ -231,6 +244,29 @@ export function* checkUsernameExists(action: ActionType) { + try { + const res = yield call(UserFetch.userForgotPassword, action.payload.email); + + if (res === 'Password Reset URL sent to the registered email!') { + yield put( + NotificationActions.success(`Password reset URL have been sent to ${action.payload.email}`), + ); + } + + // Call returns error if username already exists, else empty + const message = + res === 'Invalid email' + ? 'Email is not registered' + : res === 'Password Reset URL sent to the registered email!' + ? ' ' + : res; + yield put(UserActions.updateErrorMessage(message)); + } catch (err) { + console.error(err); + } +} + export function* resetAppState(action: ActionType) { try { yield put(CodeActions.resetCodeState()); @@ -253,11 +289,13 @@ export function* userSagas() { takeEvery(UserActions.Type.REGISTER, register), takeEvery(UserActions.Type.EDIT_USER_PROFILE, editUserProfile), takeEvery(UserActions.Type.EDIT_USER_PASSWORD, editUserPassword), + takeEvery(UserActions.Type.CHANGE_USER_PASSWORD, changeUserPassword), takeEvery(UserActions.Type.LOGIN, login), takeEvery(UserActions.Type.LOGOUT, logout), takeEvery(UserActions.Type.CHECK_EMAIL_EXISTS, checkEmailExists), takeEvery(UserActions.Type.CHECK_USERNAME_EXISTS, checkUsernameExists), takeEvery(UserActions.Type.GET_USER_DETAILS, getUserDetails), takeEvery(UserActions.Type.RESET_APP_STATE, resetAppState), + takeEvery(UserActions.Type.FORGOT_PASSWORD, forgotPassword), ]); } diff --git a/src/app/styles/Authentication.module.css b/src/app/styles/Authentication.module.css index 37c00f98..6627a5fa 100755 --- a/src/app/styles/Authentication.module.css +++ b/src/app/styles/Authentication.module.css @@ -568,3 +568,11 @@ background: #fff; padding: 0 10px; } + +.forgot-your-password { + cursor: pointer; +} + +.forgot-your-password:hover { + color: #4630eb !important; +} diff --git a/src/app/types/Authentication/ChangePassword.ts b/src/app/types/Authentication/ChangePassword.ts new file mode 100644 index 00000000..15888467 --- /dev/null +++ b/src/app/types/Authentication/ChangePassword.ts @@ -0,0 +1,16 @@ +import { RouteComponentProps } from 'react-router-dom'; + +export interface ChangePasswordState { + password: string; + repeatPassword: string; + passwordError: string; +} +export interface StateProps { + errorMessage: string; +} + +export interface DispatchProps { + changePassword: (password: string, passwordResetToken: string, userId: number) => void; +} + +export type changePasswordProps = StateProps & DispatchProps & RouteComponentProps; diff --git a/src/app/types/Authentication/Login.ts b/src/app/types/Authentication/Login.ts index 2cbf9e8e..9c020162 100644 --- a/src/app/types/Authentication/Login.ts +++ b/src/app/types/Authentication/Login.ts @@ -1,6 +1,7 @@ import { AuthType } from 'app/types/Authentication'; export interface State { + isForgotPasswordOpen: boolean; username: string; password: string; } @@ -15,8 +16,19 @@ export interface StateProps { } export interface DispatchProps { + forgotPassword: (email: string) => void; login: (username: string, password: string) => void; updateErrorMessage: (errorMessage: string) => void; } +export interface ForgotPasswordProps { + updateErrorMessage: (errorMessage: string) => void; + handleSelectPanel: (authType: AuthType) => void; + closeForgotPassword: () => void; + errorMessage: string; + username: string; + setUsername: (username: string) => void; + forgotPassword: (email: string) => void; +} + export type Props = ElementOwnProps & StateProps & DispatchProps; diff --git a/src/app/types/User.ts b/src/app/types/User.ts index 99e8ab1a..ebb2c782 100644 --- a/src/app/types/User.ts +++ b/src/app/types/User.ts @@ -30,6 +30,12 @@ export interface EditUserPassword { oldPassword?: string; } +export interface ChangeUserPassword { + newPassword: string; + passwordResetToken: string; + userId: number; +} + export interface Login { email: string; password: string; @@ -39,11 +45,15 @@ export interface ActivateUser { authToken: string; userId: number; } +export interface ForgotPassword { + email: string; +} const actions = { ActivateUser: UserActions.activateUser, editUserPassword: UserActions.editUserPassword, editUserProfile: UserActions.editUserProfile, + forgotPassword: UserActions.forgotPassword, getUserDetails: UserActions.getUserDetails, login: UserActions.login, logout: UserActions.logout,