fix(client): put token widget behind email check (#63910)

This commit is contained in:
Shaun Hamilton
2025-11-19 21:48:09 +02:00
committed by GitHub
parent 9157a25ef4
commit 0367c2ddb1
5 changed files with 51 additions and 15 deletions
@@ -1314,7 +1314,8 @@
"copied": "Token copied to clipboard",
"copy-error": "Error copying token to clipboard",
"token-usage": "Your Exam Environment authorization token is used to log you into the desktop application.",
"generated": "A new Exam Environment authorization token has been generated for your account."
"generated": "A new Exam Environment authorization token has been generated for your account.",
"non-staff-testing": "Only freeCodeCamp staff are allowed to generate exam tokens on non-production environments at this time."
},
"shortcuts": {
"title": "Keyboard shortcuts",
@@ -6,7 +6,6 @@ import { createSelector } from 'reselect';
import { scroller } from 'react-scroll';
import { Container, Spacer } from '@freecodecamp/ui';
import { useFeatureIsOn } from '@growthbook/growthbook-react';
import store from 'store';
import envData from '../../config/env.json';
@@ -108,8 +107,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
const isSignedInRef = useRef(isSignedIn);
const examTokenFlag = useFeatureIsOn('exam-token-widget');
const handleHashChange = () => {
const id = window.location.hash.replace('#', '');
if (id) {
@@ -205,7 +202,7 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
<Spacer size='m' />
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
<Spacer size='m' />
{examTokenFlag && <ExamToken />}
<ExamToken email={email} />
<Certification
completedChallenges={completedChallenges}
createFlashMessage={createFlashMessage}
+13 -2
View File
@@ -3,8 +3,15 @@ import { Button, Panel, Modal, Spacer } from '@freecodecamp/ui';
import { useTranslation } from 'react-i18next';
import { FullWidthRow } from '../helpers';
import { generateExamToken } from '../../utils/ajax';
import envData from '../../../config/env.json';
function ExamToken(): JSX.Element {
const { deploymentEnv } = envData;
interface ExamTokenProps {
email: string;
}
function ExamToken({ email }: ExamTokenProps) {
const [examToken, setExamToken] = useState<string | null>(null);
const [examTokenError, setExamTokenError] = useState<string | null>(null);
@@ -32,6 +39,9 @@ function ExamToken(): JSX.Element {
setTimeout(() => setRecentlyGenerated(false), 10000);
};
const nonStaffTesting =
deploymentEnv !== 'production' && !email.endsWith('@freecodecamp.org');
return (
<FullWidthRow>
<Modal
@@ -90,9 +100,10 @@ function ExamToken(): JSX.Element {
<p>{t('exam-token.note')}</p>
<strong>{t('exam-token.invalidation-2')}</strong>
<Spacer size='s' />
{nonStaffTesting && <p>{t('exam-token.non-staff-testing')}</p>}
<Button
block={true}
disabled={recentlyGenerated}
disabled={recentlyGenerated || nonStaffTesting}
onClick={() => void getToken()}
>
{t('exam-token.generate-exam-token')}
@@ -4,8 +4,17 @@ import { Button, Spacer } from '@freecodecamp/ui';
import { examEnvironmentAuthorizationTokenApi } from '../../../utils/ajax';
import { Loader } from '../../../components/helpers';
import envData from '../../../../config/env.json';
export function ExamTokenControls(): JSX.Element {
const { deploymentEnv } = envData;
interface ExamTokenControlsProps {
email: string;
}
export function ExamTokenControls({
email
}: ExamTokenControlsProps): JSX.Element {
const { t } = useTranslation();
const [copySuccess, setCopySuccess] = useState<string | null>(null);
@@ -33,6 +42,9 @@ export function ExamTokenControls(): JSX.Element {
);
}
const nonStaffTesting =
deploymentEnv !== 'production' && !email.endsWith('@freecodecamp.org');
return (
<>
<h3>{t('exam-token.exam-token')}</h3>
@@ -60,6 +72,7 @@ export function ExamTokenControls(): JSX.Element {
{t('exam-token.no-token')}
</p>
)}
{nonStaffTesting && <p>{t('exam-token.non-staff-testing')}</p>}
{generateMutation.isLoading || getTokenQuery.isLoading ? (
<Button block={true}>
<Loader />
@@ -67,7 +80,11 @@ export function ExamTokenControls(): JSX.Element {
) : (
<Button
block={true}
disabled={generateMutation.isLoading || getTokenQuery.isLoading}
disabled={
generateMutation.isLoading ||
getTokenQuery.isLoading ||
nonStaffTesting
}
onClick={() => void generateToken()}
>
{t('exam-token.generate-exam-token')}
@@ -19,10 +19,15 @@ import { connect } from 'react-redux';
import LearnLayout from '../../../components/layouts/learn';
import ChallengeTitle from '../components/challenge-title';
import useDetectOS from '../utils/use-detect-os';
import { ChallengeNode, CompletedChallenge } from '../../../redux/prop-types';
import {
ChallengeNode,
CompletedChallenge,
User
} from '../../../redux/prop-types';
import {
completedChallengesSelector,
isSignedInSelector
isSignedInSelector,
userSelector
} from '../../../redux/selectors';
import { examAttempts } from '../../../utils/ajax';
import MissingPrerequisites from '../exam/components/missing-prerequisites';
@@ -43,14 +48,17 @@ const mapStateToProps = createSelector(
completedChallengesSelector,
isChallengeCompletedSelector,
isSignedInSelector,
userSelector,
(
completedChallenges: CompletedChallenge[],
isChallengeCompleted: boolean,
isSignedIn: boolean
isSignedIn: boolean,
user: User | null
) => ({
completedChallenges,
isChallengeCompleted,
isSignedIn
isSignedIn,
user
})
);
@@ -62,6 +70,7 @@ interface ShowExamDownloadProps {
completedChallenges: CompletedChallenge[];
isChallengeCompleted: boolean;
isSignedIn: boolean;
user: User | null;
}
function ShowExamDownload({
@@ -73,7 +82,8 @@ function ShowExamDownload({
},
completedChallenges,
isChallengeCompleted,
isSignedIn
isSignedIn,
user
}: ShowExamDownloadProps): JSX.Element {
const [latestVersion, setLatestVersion] = useState<string | null>(null);
@@ -217,7 +227,7 @@ function ShowExamDownload({
<h2>{t('exam.attempts')}</h2>
<Attempts examChallengeId={id} />
<Spacer size='l' />
<ExamTokenControls />
<ExamTokenControls email={user!.email} />
</>
)}
<p>