Revert "Revert "Prevent IDV access from audit courses""

This commit is contained in:
Bianca Severino
2020-12-07 10:18:40 -05:00
committed by GitHub
parent 37e338df0f
commit 0a4dcb4eaa
8 changed files with 140 additions and 68 deletions

View File

@@ -0,0 +1,48 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
import { ERROR_REASONS } from './IdVerificationContext';
function AccessBlocked({ error, intl }) {
const handleMessage = () => {
if (error === ERROR_REASONS.COURSE_ENROLLMENT) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}</p>;
} else if (error === ERROR_REASONS.EXISTING_REQUEST) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.pending'])}</p>;
}
return (
<FormattedMessage
id="id.verification.access.blocked.denied"
defaultMessage="You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
values={{
email: <strong>no-reply@registration.edx.org</strong>,
}}
/>
);
};
return (
<div>
<h3 aria-level="1" tabIndex="-1">
{intl.formatMessage(messages['id.verification.access.blocked.title'])}
</h3>
{handleMessage()}
<div className="action-row">
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</div>
</div>
);
}
AccessBlocked.propTypes = {
intl: intlShape.isRequired,
error: PropTypes.string.isRequired,
};
export default injectIntl(AccessBlocked);

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
function ExistingRequest(props) {
return (
<div>
<h3 aria-level="1" tabIndex="-1">
{props.intl.formatMessage(messages['id.verification.existing.request.title'])}
</h3>
{props.status === 'pending' || props.status === 'approved'
? <p>{props.intl.formatMessage(messages['id.verification.existing.request.pending.text'])}</p>
: <FormattedMessage
id="id.verification.existing.request.denied.text"
defaultMessage="You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
values={{
email: <strong>no-reply@registration.edx.org</strong>,
}}
/>
}
<div className="action-row">
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</div>
</div>
);
}
ExistingRequest.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ExistingRequest);

View File

@@ -61,13 +61,18 @@ const messages = defineMessages({
defaultMessage: 'We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.',
description: 'Answering what edX does with the verification photo.',
},
'id.verification.existing.request.title': {
id: 'id.verification.existing.request.title',
'id.verification.access.blocked.title': {
id: 'id.verification.access.blocked.title',
defaultMessage: 'Identity Verification',
description: 'Title for text that displays when user has already made a request.',
description: 'Title for text that displays when a user is blocked from ID verification.',
},
'id.verification.existing.request.pending.text': {
id: 'id.verification.existing.request.pending.text',
'id.verification.access.blocked.enrollment': {
id: 'id.verification.access.blocked.enrollment',
defaultMessage: 'You are not currently enrolled in a course that requires identity verification.',
description: 'Text that displays when user is trying to verify while not enrolled in a course that requires ID verification.',
},
'id.verification.access.blocked.pending': {
id: 'id.verification.access.blocked.pending',
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).',
description: 'Text that displays when user has a pending or approved request.',
},

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { hasGetUserMediaSupport } from './getUserMediaShim';
import { getExistingIdVerification } from './data/service';
import { getExistingIdVerification, getEnrollments } from './data/service';
import PageLoading from '../account-settings/PageLoading';
import ExistingRequest from './ExistingRequest';
import AccessBlocked from './AccessBlocked';
const IdVerificationContext = React.createContext({});
@@ -16,6 +16,14 @@ const MEDIA_ACCESS = {
GRANTED: 'granted',
};
const ERROR_REASONS = {
COURSE_ENROLLMENT: 'course_enrollment',
EXISTING_REQUEST: 'existing_request',
CANNOT_VERIFY: 'cannot_verify',
};
const VERIFIED_MODES = ['verified', 'professional', 'masters', 'executive_education'];
function IdVerificationContextProvider({ children }) {
const [existingIdVerification, setExistingIdVerification] = useState(null);
const [facePhotoFile, setFacePhotoFile] = useState(null);
@@ -25,6 +33,8 @@ function IdVerificationContextProvider({ children }) {
const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport ?
MEDIA_ACCESS.PENDING :
MEDIA_ACCESS.UNSUPPORTED);
const [canVerify, setCanVerify] = useState(true);
const [error, setError] = useState('');
const { authenticatedUser } = useContext(AppContext);
const contextValue = {
@@ -61,24 +71,49 @@ function IdVerificationContextProvider({ children }) {
},
};
// Call verification status endpoint to check whether we can verify.
useEffect(() => {
// Call verification status endpoint to check whether we can verify.
(async () => {
const existingIdV = await getExistingIdVerification();
setExistingIdVerification(existingIdV);
})();
}, []);
useEffect(() => {
// Check whether the learner is enrolled in a verified course mode.
(async () => {
/* eslint-disable arrow-body-style */
const enrollments = await getEnrollments();
const verifiedEnrollments = enrollments.filter((enrollment) => {
return VERIFIED_MODES.includes(enrollment.mode);
});
if (verifiedEnrollments.length === 0) {
setCanVerify(false);
setError(ERROR_REASONS.COURSE_ENROLLMENT);
}
})();
}, []);
useEffect(() => {
// Check for an existing verification attempt
if (existingIdVerification && !existingIdVerification.canVerify) {
const { status } = existingIdVerification;
setCanVerify(false);
if (status === 'pending' || status === 'approved') {
setError(ERROR_REASONS.EXISTING_REQUEST);
} else {
setError(ERROR_REASONS.CANNOT_VERIFY);
}
}
}, [existingIdVerification]);
// If we are waiting for verification status endpoint, show spinner.
if (!existingIdVerification) {
return <PageLoading srMessage="Loading verification status" />;
}
if (!existingIdVerification.canVerify) {
const { status } = existingIdVerification;
return (
<ExistingRequest status={status} />
);
if (!canVerify) {
return <AccessBlocked error={error} />;
}
return (
@@ -95,4 +130,5 @@ export {
IdVerificationContext,
IdVerificationContextProvider,
MEDIA_ACCESS,
ERROR_REASONS,
};

View File

@@ -80,11 +80,8 @@ function IdVerificationPage(props) {
</>
);
}
IdVerificationPage.propTypes = {
intl: intlShape.isRequired,
};
export default connect(idVerificationSelector, {
})(injectIntl(IdVerificationPage));

View File

@@ -29,6 +29,25 @@ export async function getExistingIdVerification() {
}
}
/**
* Get the learner's enrollments. Used to check whether the learner is enrolled
* in a verified course mode.
*
* Returns an array: [{...data, mode: String}]
*/
export async function getEnrollments() {
const url = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
const requestConfig = {
headers: { Accept: 'application/json' },
};
try {
const { data } = await getAuthenticatedHttpClient().get(url, requestConfig);
return data;
} catch (e) {
return [];
}
}
/**
* Submit ID verifiction to LMS.
*

View File

@@ -5,29 +5,31 @@ import { render, cleanup, act, screen } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import ExistingRequest from '../ExistingRequest';
const IntlExistingRequest = injectIntl(ExistingRequest);
import { ERROR_REASONS } from '../IdVerificationContext';
import AccessBlocked from '../AccessBlocked';
const IntlAccessBlocked = injectIntl(AccessBlocked);
const history = createMemoryHistory();
describe('ExistingRequest', () => {
describe('AccessBlocked', () => {
const defaultProps = {
intl: {},
status: '',
error: '',
};
afterEach(() => {
cleanup();
});
it('renders correctly when status is pending', async () => {
defaultProps.status = 'pending';
it('renders correctly when there is an existing request', async () => {
defaultProps.error = ERROR_REASONS.EXISTING_REQUEST;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));
@@ -37,29 +39,29 @@ describe('ExistingRequest', () => {
expect(text).toBeInTheDocument();
});
it('renders correctly when status is approved', async () => {
defaultProps.status = 'approved';
it('renders correctly when learner is not enrolled in a verified course mode', async () => {
defaultProps.error = ERROR_REASONS.COURSE_ENROLLMENT;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));
const text = screen.getByText(/You have already submitted your verification information./);
const text = screen.getByText(/You are not currently enrolled in a course that requires identity verification./);
expect(text).toBeInTheDocument();
});
it('renders correctly when status is denied', async () => {
defaultProps.status = 'denied';
defaultProps.error = ERROR_REASONS.CANNOT_VERIFY;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));

View File

@@ -3,11 +3,12 @@ import { render, cleanup, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { getExistingIdVerification } from '../data/service';
import { getExistingIdVerification, getEnrollments } from '../data/service';
import { IdVerificationContextProvider } from '../IdVerificationContext';
jest.mock('../data/service', () => ({
getExistingIdVerification: jest.fn(),
getEnrollments: jest.fn(() => []),
}));
describe('IdVerificationContext', () => {
@@ -20,7 +21,7 @@ describe('IdVerificationContext', () => {
cleanup();
});
it('renders correctly and calls getExistingIdVerification', async () => {
it('renders correctly and calls getExistingIdVerification + getEnrollments', async () => {
await act(async () => render((
<AppContext.Provider value={{ authenticatedUser: { userId: 3 } }}>
<IntlProvider locale="en">
@@ -29,5 +30,6 @@ describe('IdVerificationContext', () => {
</AppContext.Provider>
)));
expect(getExistingIdVerification).toHaveBeenCalled();
expect(getEnrollments).toHaveBeenCalled();
});
});