mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat(client): validate MS transcript links (#51445)
Co-authored-by: Mrugesh Mohapatra <1884376+raisedadead@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
ddd60e7e2d
commit
f6b970fbee
@@ -450,7 +450,8 @@
|
||||
"link-li-3": "If you do not have a transcript link, click the \"Create link\" button to create one.",
|
||||
"link-li-4": "Click the \"Copy link\" button to copy the transcript URL.",
|
||||
"link-li-5": "Paste the URL into the input below and click \"Link Account\".",
|
||||
"transcript-label": "Your Microsoft Transcript Link"
|
||||
"transcript-label": "Your Microsoft Transcript Link",
|
||||
"invalid-transcript": "Your transcript link is not correct, it should have the following form: <1>https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID</1> - check the UPPERCASE items in your link are correct."
|
||||
}
|
||||
},
|
||||
"donate": {
|
||||
|
||||
@@ -3,16 +3,17 @@ import {
|
||||
Button,
|
||||
FormGroup,
|
||||
ControlLabel,
|
||||
FormControl
|
||||
FormControl,
|
||||
HelpBlock
|
||||
} from '@freecodecamp/react-bootstrap';
|
||||
import { connect } from 'react-redux';
|
||||
import { ConnectedProps, connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { Trans, withTranslation } from 'react-i18next';
|
||||
import { Spacer } from '../../../components/helpers';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Spacer } from '../../../components/helpers';
|
||||
import { isMicrosoftTranscriptLink } from '../../../../../utils/validate';
|
||||
import {
|
||||
linkMsUsername,
|
||||
unlinkMsUsername,
|
||||
@@ -27,7 +28,7 @@ import Login from '../../../components/Header/components/login';
|
||||
|
||||
import './link-ms-user.css';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
const mapState = createSelector(
|
||||
isSignedInSelector,
|
||||
msUsernameSelector,
|
||||
isProcessingSelector,
|
||||
@@ -42,7 +43,7 @@ const mapStateToProps = createSelector(
|
||||
})
|
||||
);
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
const mapDispatch = (dispatch: Dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
linkMsUsername,
|
||||
@@ -52,26 +53,20 @@ const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||
dispatch
|
||||
);
|
||||
|
||||
interface LinkMsUserProps {
|
||||
isSignedIn: boolean;
|
||||
msUsername: string | undefined | null;
|
||||
linkMsUsername: (arg0: { msTranscriptUrl: string }) => void;
|
||||
unlinkMsUsername: () => void;
|
||||
isProcessing: boolean;
|
||||
setIsProcessing: (arg0: boolean) => void;
|
||||
t: TFunction;
|
||||
}
|
||||
const connector = connect(mapState, mapDispatch);
|
||||
|
||||
export function LinkMsUser({
|
||||
type Props = ConnectedProps<typeof connector>;
|
||||
|
||||
function LinkMsUser({
|
||||
isSignedIn,
|
||||
msUsername,
|
||||
linkMsUsername,
|
||||
unlinkMsUsername,
|
||||
isProcessing,
|
||||
setIsProcessing,
|
||||
t
|
||||
}: LinkMsUserProps): JSX.Element {
|
||||
const [msTranscriptUrl, setMsTranscriptUrl] = useState<string>('');
|
||||
setIsProcessing
|
||||
}: Props): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const [msTranscriptUrl, setMsTranscriptUrl] = useState('');
|
||||
|
||||
function handleLinkUsername(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
@@ -84,6 +79,11 @@ export function LinkMsUser({
|
||||
setMsTranscriptUrl(e.target.value);
|
||||
}
|
||||
|
||||
const isValid = isMicrosoftTranscriptLink(msTranscriptUrl);
|
||||
const isPristine = msTranscriptUrl === '';
|
||||
const isDisabled = isProcessing || !isValid;
|
||||
const showWarning = !isPristine && !isValid;
|
||||
|
||||
return !isSignedIn ? (
|
||||
<>
|
||||
<h2 className='link-ms-user-title'>{t('learn.ms.link-header')}</h2>
|
||||
@@ -133,7 +133,7 @@ export function LinkMsUser({
|
||||
|
||||
<Spacer size='medium' />
|
||||
<form onSubmit={handleLinkUsername}>
|
||||
<FormGroup>
|
||||
<FormGroup validationState={isValid ? 'success' : 'error'}>
|
||||
<ControlLabel>
|
||||
<strong>{t('learn.ms.transcript-label')}</strong>
|
||||
</ControlLabel>
|
||||
@@ -144,7 +144,7 @@ export function LinkMsUser({
|
||||
/>
|
||||
</FormGroup>
|
||||
<Button
|
||||
disabled={isProcessing || msTranscriptUrl.length === 0}
|
||||
disabled={isDisabled}
|
||||
block={true}
|
||||
bsStyle='primary'
|
||||
className='btn-invert'
|
||||
@@ -152,6 +152,13 @@ export function LinkMsUser({
|
||||
>
|
||||
{t('buttons.link-account')}
|
||||
</Button>
|
||||
{showWarning && (
|
||||
<HelpBlock>
|
||||
<Trans i18nKey='learn.ms.invalid-transcript'>
|
||||
placeholder <code>placeholder</code> placeholder
|
||||
</Trans>
|
||||
</HelpBlock>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
@@ -161,7 +168,4 @@ export function LinkMsUser({
|
||||
|
||||
LinkMsUser.displayName = 'LinkMsUser';
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withTranslation()(LinkMsUser));
|
||||
export default connector(LinkMsUser);
|
||||
|
||||
+59
-1
@@ -3,7 +3,8 @@ import {
|
||||
usernameTooShort,
|
||||
validationSuccess,
|
||||
usernameIsHttpStatusCode,
|
||||
invalidCharError
|
||||
invalidCharError,
|
||||
isMicrosoftTranscriptLink
|
||||
} from './validate';
|
||||
|
||||
function inRange(num: number, range: number[]) {
|
||||
@@ -56,3 +57,60 @@ describe('isValidUsername', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const baseUrl = 'https://learn.microsoft.com/';
|
||||
|
||||
describe('isMicrosoftTranscriptLink', () => {
|
||||
it('should reject links to domains other than learn.microsoft.com', () => {
|
||||
{
|
||||
expect(isMicrosoftTranscriptLink('https://lean.microsoft.com')).toBe(
|
||||
false
|
||||
);
|
||||
expect(isMicrosoftTranscriptLink('https://learn.microsft.com')).toBe(
|
||||
false
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject links without a username', () => {
|
||||
expect(isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/`)).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject links without a unique id', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(`${baseUrl}/en-us/users/moT01/transcript`)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject links with anything after the unique id', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
`${baseUrl}/en-us/users/moT01/transcript/any-id/more-stuff`
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject the placeholder link', () => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID'
|
||||
)
|
||||
).toBe(false);
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
'https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID/'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['en-us', 'fr-fr', 'lang-country'])(
|
||||
'should accept links with the %s locale',
|
||||
locale => {
|
||||
expect(
|
||||
isMicrosoftTranscriptLink(
|
||||
`https://learn.microsoft.com/${locale}/users/moT01/transcript/any-id`
|
||||
)
|
||||
).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,3 +36,23 @@ export const isValidUsername = (str: string): Validated => {
|
||||
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;
|
||||
return validationSuccess;
|
||||
};
|
||||
|
||||
// link template:
|
||||
// https://learn.microsoft.com/LOCALE/users/USERNAME/transcript/ID
|
||||
export const isMicrosoftTranscriptLink = (value: string): boolean => {
|
||||
let url;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const correctDomain = url.hostname === 'learn.microsoft.com';
|
||||
const correctPath = !!url.pathname.match(
|
||||
/^\/[^/]+\/users\/[^/]+\/transcript\/[^/]+$/
|
||||
);
|
||||
const notPlaceholder = !url.pathname.match(
|
||||
'/LOCALE/users/USERNAME/transcript/ID'
|
||||
);
|
||||
return correctDomain && correctPath && notPlaceholder;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user