Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Show login modal when user session expires. #2541

Merged
merged 11 commits into from
Sep 22, 2021
57 changes: 37 additions & 20 deletions src/actions/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,8 @@ export const onGetPolicy = () => async (dispatch, getState) => {

export const withCsrf = (fn) => (dispatch, getState) => {
const csrf = sel.csrf(getState());
const csrfIsNeeded = sel.getCsrfIsNeeded(getState());
if (csrf || csrfIsNeeded) return fn(dispatch, getState, csrf);
if (csrf) return fn(dispatch, getState, csrf);

dispatch(act.CSRF_NEEDED(true));
return dispatch(requestApiInfo()).then(() =>
withCsrf(fn)(dispatch, getState)
);
Expand Down Expand Up @@ -256,24 +254,31 @@ export const onSearchUser = (query, isCMS) => (dispatch) => {
// onLogin handles a user's login. If it is his first login on the app
// after registering, his key will be saved under his email. If so, it
// changes the storage key to his uuid.
export const onLogin = ({ email, password, code }) =>
withCsrf(async (dispatch, _, csrf) => {
dispatch(act.REQUEST_LOGIN({ email }));
try {
const response = await api.login(csrf, email, password, code);
await dispatch(onRequestMe());
dispatch(act.RECEIVE_LOGIN(response));
const { userid, username } = response;
const keyNeedsReplace = await pki.needStorageKeyReplace(email, username);
if (keyNeedsReplace) {
pki.replaceStorageKey(keyNeedsReplace, userid);
export const onLogin =
({ email, password, code }) =>
(dispatch, getState) => {
dispatch(onRequireCSRF());
return withCsrf(async (dispatch, _, csrf) => {
dispatch(act.REQUEST_LOGIN({ email }));
try {
const response = await api.login(csrf, email, password, code);
await dispatch(onRequestMe());
dispatch(act.RECEIVE_LOGIN(response));
const { userid, username } = response;
const keyNeedsReplace = await pki.needStorageKeyReplace(
email,
username
);
if (keyNeedsReplace) {
pki.replaceStorageKey(keyNeedsReplace, userid);
}
return;
} catch (error) {
dispatch(act.RECEIVE_LOGIN(null, error));
throw error;
}
return;
} catch (error) {
dispatch(act.RECEIVE_LOGIN(null, error));
throw error;
}
});
})(dispatch, getState);
};

// handleLogout calls the correct logout handler according to the user
// selected option between a normal logout or a permanent logout.
Expand All @@ -290,6 +295,13 @@ export const handleNormalLogout = () => {
clearProposalPaymentPollingPointer();
};

// handleLocalLogout can be used to clean user session on logout
// without requesting the API
export const handleLocalLogout = () => (dispatch) => {
dispatch(act.RECEIVE_LOGOUT());
handleNormalLogout();
};

// handlePermanentLogout handles the logout procedures while deleting all
// user related information from the browser storage and cache.
export const handlePermanentLogout = (userid) =>
Expand Down Expand Up @@ -317,6 +329,10 @@ export const onLogout = (isCMS, isPermanent) =>
});
});

export const onRequireCSRF = () => (dispatch) => {
dispatch(act.CSRF_NEEDED(true));
};

export const onChangeUsername = (password, newUsername) =>
withCsrf((dispatch, _, csrf) => {
dispatch(act.REQUEST_CHANGE_USERNAME());
Expand Down Expand Up @@ -1056,6 +1072,7 @@ export const onCommentVote = (
})
.catch((error) => {
dispatch(act.RECEIVE_LIKE_COMMENT(null, error));
throw error;
});
});

Expand Down
28 changes: 23 additions & 5 deletions src/actions/tests/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ describe("test api actions (actions/api.js)", () => {
api.onLogin,
[FAKE_USER],
(e) => [
{
error: false,
payload: true,
type: act.CSRF_NEEDED
},
{
type: act.REQUEST_LOGIN,
error: false,
Expand Down Expand Up @@ -657,8 +662,16 @@ describe("test api actions (actions/api.js)", () => {
const path = "/api/comments/v1/vote";
const commentid = 0;
const up_action = 1;
//const down_action = -1;
const params = [FAKE_USER.id, FAKE_PROPOSAL_TOKEN, commentid, up_action];
const sectionId = "main";

const params = [
FAKE_USER.id,
FAKE_PROPOSAL_TOKEN,
commentid,
up_action,
PROPOSAL_STATE_VETTED,
sectionId
];

// this needs a custom assertion for success response as the common one
// doesn't work for this case.
Expand All @@ -676,15 +689,20 @@ describe("test api actions (actions/api.js)", () => {
path,
api.onCommentVote,
params,
(errorcode) => [
(e) => [
{
error: false,
payload: { commentid, token: FAKE_PROPOSAL_TOKEN, vote: up_action },
payload: {
commentid,
token: FAKE_PROPOSAL_TOKEN,
vote: up_action,
sectionId
},
type: act.REQUEST_LIKE_COMMENT
},
{
error: true,
payload: { errorcode },
payload: e,
type: act.RECEIVE_LIKE_COMMENT
}
],
Expand Down
25 changes: 21 additions & 4 deletions src/components/CommentForm/CommentForm.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import { Formik } from "formik";
import FormikPersist from "src/components/FormikPersist";
Expand All @@ -15,8 +15,9 @@ import {
import { Row } from "../layout";
import MarkdownEditor from "src/components/MarkdownEditor";
import validationSchema from "./validation";
import { usePolicy } from "src/hooks";
import useModalContext from "src/hooks/utils/useModalContext";
import ModalLogin from "src/components/ModalLogin";
import { usePolicy } from "src/hooks";
import ModalConfirm from "src/components/ModalConfirm";
import styles from "./CommentForm.module.css";

Expand All @@ -32,10 +33,19 @@ const CommentForm = ({
isAuthorUpdate,
hasAuthorUpdates
}) => {
const [handleOpenModal, handleCloseModal] = useModalContext();

const openLoginModal = useCallback(() => {
handleOpenModal(ModalLogin, {
onLoggedIn: handleCloseModal,
onClose: handleCloseModal,
title: "Your session has expired. Please log in again"
});
}, [handleOpenModal, handleCloseModal]);

const {
policyPi: { namesupportedchars, namelengthmax, namelengthmin }
} = usePolicy();
const [handleOpenModal, handleCloseModal] = useModalContext();
const smallTablet = useMediaQuery("(max-width: 685px)");
async function handleSubmit(
{ comment, title },
Expand Down Expand Up @@ -68,7 +78,13 @@ const CommentForm = ({
}
} catch (e) {
setSubmitting(false);
setFieldError("global", e);
// Hardcode the login modal to show up when user session expires
// ref: https://github.com/decred/politeiagui/pull/2541#issuecomment-909194251
if (e.statusCode === 403) {
openLoginModal();
} else {
setFieldError("global", e);
}
}
}
return (
Expand Down Expand Up @@ -156,6 +172,7 @@ will only be able to reply to your most recent update thread.">
)}
<Button
type="submit"
data-testid="comment-submit-button"
kind={!isValid || disableSubmit ? "disabled" : "primary"}
loading={isSubmitting}>
Add comment
Expand Down
1 change: 1 addition & 0 deletions src/components/ModalLogin.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const ModalLogin = ({ title = "Login", onLoggedIn, onClose, ...props }) => {
title={title}
onClose={onClose}
iconType="info"
data-testid="modal-login"
iconSize="lg"
{...props}
contentStyle={{ width: "100%" }}
Expand Down
32 changes: 20 additions & 12 deletions src/components/ModalProvider.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, createContext } from "react";
import isEmpty from "lodash/fp/isEmpty";

export const modalContext = createContext({ component: () => null, props: {} });

Expand All @@ -10,29 +11,36 @@ const initialState = {
};

const ModalProvider = ({ children }) => {
const [modal, setModal] = useState(initialState);
const handleOpenModal = function handleOpenModal(modal, props = {}) {
setModal({
component: modal,
props: {
show: true,
...props
}
});
const [modalStack] = useState([]);
const [currentModal, setModal] = useState(initialState);
const handleOpenModal = function handleOpenModal(
modal,
props = {},
{ overlay } = {}
) {
const newModal = { component: modal, props: { show: true, ...props } };
if (isEmpty(modalStack) || overlay) {
modalStack.push(newModal);
setModal(newModal);
} else if (!currentModal || !currentModal.props.show) {
setModal(newModal);
}
};

const handleCloseModal = function handleCloseModal() {
setModal(initialState);
modalStack.pop();
const previousModal = modalStack.pop();
setModal(previousModal || initialState);
};

const props = {
onClose: handleCloseModal,
...modal.props
...currentModal.props
};
return (
<modalContext.Provider value={[handleOpenModal, handleCloseModal]}>
{children}
<modal.component {...props} />
<currentModal.component {...props} />
</modalContext.Provider>
);
};
Expand Down
24 changes: 22 additions & 2 deletions src/components/ProposalForm/ProposalForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SelectField from "src/components/Select/SelectField";
import styles from "./ProposalForm.module.css";
import MarkdownEditor from "src/components/MarkdownEditor";
import ModalMDGuide from "src/components/ModalMDGuide";
import ModalLogin from "src/components/ModalLogin";
import ThumbnailGrid from "src/components/Files";
import AttachFileInput from "src/components/AttachFileInput";
import DraftSaver from "./DraftSaver";
Expand Down Expand Up @@ -420,6 +421,13 @@ const ProposalFormWrapper = ({
onClose: handleCloseModal
});
}, [handleCloseModal, handleOpenModal]);
const openLoginModal = useCallback(() => {
handleOpenModal(ModalLogin, {
onLoggedIn: handleCloseModal,
onClose: handleCloseModal,
title: "Your session has expired. Please log in again"
});
}, [handleOpenModal, handleCloseModal]);
const [submitSuccess, setSubmitSuccess] = useState(false);
const { proposalFormValidation, onFetchProposalsBatchWithoutState } =
useProposalForm();
Expand Down Expand Up @@ -482,10 +490,22 @@ const ProposalFormWrapper = ({
resetForm();
} catch (e) {
setSubmitting(false);
setFieldError("global", e);
// Hardcode the login modal to show up when user session expires
// ref: https://github.com/decred/politeiagui/pull/2541#issuecomment-909194251
if (e.statusCode === 403) {
openLoginModal();
} else {
setFieldError("global", e);
}
}
},
[history, onSubmit, onFetchProposalsBatchWithoutState, isPublic]
[
history,
onSubmit,
onFetchProposalsBatchWithoutState,
isPublic,
openLoginModal
]
);

const newInitialValues = initialValues
Expand Down
12 changes: 8 additions & 4 deletions src/containers/User/Login/Form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,14 @@ const LoginForm = ({
} catch (e) {
setSubmitting(false);
if (e.errorcode === TOTP_MISSING_LOGIN_ERROR) {
handleOpenModal(ModalTotpVerify, {
onVerify: (code) => onLogin({ ...credentials, code }),
onClose: handleCloseModal
});
handleOpenModal(
ModalTotpVerify,
{
onVerify: (code) => onLogin({ ...credentials, code }),
onClose: handleCloseModal
},
{ overlay: true }
);
return;
}
setFieldError("global", e);
Expand Down
Loading