diff --git a/src/actions/api.js b/src/actions/api.js index a572583dd..2b2be339c 100644 --- a/src/actions/api.js +++ b/src/actions/api.js @@ -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) ); @@ -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. @@ -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) => @@ -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()); @@ -1056,6 +1072,7 @@ export const onCommentVote = ( }) .catch((error) => { dispatch(act.RECEIVE_LIKE_COMMENT(null, error)); + throw error; }); }); diff --git a/src/actions/tests/api.test.js b/src/actions/tests/api.test.js index b3f6b0df8..db8b6f988 100644 --- a/src/actions/tests/api.test.js +++ b/src/actions/tests/api.test.js @@ -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, @@ -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. @@ -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 } ], diff --git a/src/components/CommentForm/CommentForm.jsx b/src/components/CommentForm/CommentForm.jsx index 67f60daab..8a91f624f 100644 --- a/src/components/CommentForm/CommentForm.jsx +++ b/src/components/CommentForm/CommentForm.jsx @@ -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"; @@ -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"; @@ -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 }, @@ -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 ( @@ -156,6 +172,7 @@ will only be able to reply to your most recent update thread."> )}