feat(client/api): validate ms users (#51372)

Co-authored-by: Muhammed Mustafa <MuhammedElruby@gmail.com>
This commit is contained in:
Tom
2023-08-26 07:57:02 -05:00
committed by GitHub
parent 0f9ba6e9a5
commit 9a1895d2e3
45 changed files with 927 additions and 332 deletions
+5
View File
@@ -423,6 +423,11 @@
"type": "hasMany",
"model": "UserToken",
"foreignKey": "userId"
},
"msUsernames": {
"type": "hasMany",
"model": "MsUsername",
"foreignKey": "userId"
}
},
"acls": [
+85 -23
View File
@@ -17,6 +17,7 @@ import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';
import { jwtSecret } from '../../../../config/secrets';
import { challengeTypes } from '../../../../config/challenge-types';
import {
fixPartiallyCompletedChallengeItem,
@@ -36,8 +37,6 @@ import {
validateGeneratedExamSchema,
validateUserCompletedExamSchema
} from '../utils/exam-schemas';
import { isMicrosoftLearnLink } from '../../../../utils/validate';
import { getApiUrlFromTrophy } from '../utils/ms-learn-utils';
const log = debug('fcc:boot:challenges');
@@ -100,6 +99,14 @@ export default async function bootChallenge(app, done) {
api.post('/coderoad-challenge-completed', coderoadChallengeCompleted);
const msTrophyChallengeCompleted = createMsTrophyChallengeCompleted(app);
api.post(
'/ms-trophy-challenge-completed',
send200toNonUser,
msTrophyChallengeCompleted
);
app.use(api);
app.use(router);
done();
@@ -121,6 +128,10 @@ const savableChallenges = getChallenges()
.filter(challenge => challenge.challengeType === 14)
.map(challenge => challenge.id);
const msTrophyChallenges = getChallenges()
.filter(challenge => challenge.challengeType === challengeTypes.msTrophy)
.map(({ id, msTrophyId }) => ({ id, msTrophyId }));
export function buildUserUpdate(
user,
challengeId,
@@ -446,25 +457,6 @@ async function projectCompleted(req, res, next) {
}
}
const isMSTrophyProject = completedChallenge.challengeType === 18;
let isTrophyMissing = false;
if (isMSTrophyProject) {
if (!isMicrosoftLearnLink(completedChallenge.solution)) {
return res.status(403).json({
type: 'error',
message:
'You have not provided the valid links for us to inspect your work.'
});
}
try {
const mSLearnAPIUrl = getApiUrlFromTrophy(completedChallenge.solution);
isTrophyMissing = mSLearnAPIUrl ? !(await fetch(mSLearnAPIUrl)).ok : true;
} catch {
isTrophyMissing = true;
log(`Error verifying trophy: ${completedChallenge.solution}`);
}
}
try {
// This is an ugly hack to update `user.completedChallenges`
await user.getCompletedChallenges$().toPromise();
@@ -486,8 +478,7 @@ async function projectCompleted(req, res, next) {
return res.json({
alreadyCompleted,
points: alreadyCompleted ? user.points : user.points + 1,
completedDate: completedChallenge.completedDate,
...(isMSTrophyProject && { isTrophyMissing })
completedDate: completedChallenge.completedDate
});
});
}
@@ -697,6 +688,77 @@ function createExamChallengeCompleted(app) {
};
}
function createMsTrophyChallengeCompleted(app) {
const { MsUsername } = app.models;
return async function msTrophyChallengeCompleted(req, res, next) {
const { user, body = {} } = req;
const { id = '' } = body;
try {
const msUser = await MsUsername.findOne({
where: { userId: user.id }
});
if (!msUser || !msUser.msUsername) {
throw new Error('Microsoft username not found.');
}
const { msUsername } = msUser;
const challenge = msTrophyChallenges.find(
challenge => challenge.id === id
);
if (!challenge) {
throw new Error('Challenge not found');
}
const { msTrophyId = '' } = challenge;
const msTrophyApiUrl = `https://learn.microsoft.com/api/gamestatus/achievements/${msTrophyId}?username=${msUsername}&locale=en-us`;
const msApiRes = await fetch(msTrophyApiUrl);
if (!msApiRes.ok) {
throw new Error('Unable to validate trophy');
}
const completedChallenge = pick(body, ['id']);
completedChallenge.solution = msTrophyApiUrl;
completedChallenge.completedDate = Date.now();
try {
await user.getCompletedChallenges$().toPromise();
} catch (e) {
return next(e);
}
const { alreadyCompleted, updateData } = buildUserUpdate(
user,
completedChallenge.id,
completedChallenge
);
user.updateAttributes(updateData, err => {
if (err) {
return next(err);
}
return res.json({
alreadyCompleted,
points: alreadyCompleted ? user.points : user.points + 1,
completedDate: completedChallenge.completedDate
});
});
} catch (e) {
return res.status(500).json({
type: 'error',
message: e.message
});
}
};
}
async function saveChallenge(req, res, next) {
const user = req.user;
const { savedChallenges = [] } = user;
+118 -3
View File
@@ -2,6 +2,7 @@ import debugFactory from 'debug';
import dedent from 'dedent';
import { body } from 'express-validator';
import { pick } from 'lodash';
import fetch from 'node-fetch';
import {
fixCompletedChallengeItem,
@@ -22,6 +23,7 @@ import {
createDeleteUserToken,
encodeUserToken
} from '../middlewares/user-token';
import { createDeleteMsUsername } from '../middlewares/ms-username';
import { deprecatedEndpoint } from '../utils/disabled-endpoints';
const log = debugFactory('fcc:boot:user');
@@ -35,15 +37,24 @@ function bootUser(app) {
const postDeleteAccount = createPostDeleteAccount(app);
const postUserToken = createPostUserToken(app);
const deleteUserToken = createDeleteUserToken(app);
const postMsUsername = createPostMsUsername(app);
const deleteMsUsername = createDeleteMsUsername(app);
api.get('/account', sendNonUserToHome, deprecatedEndpoint);
api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial);
api.get('/user/get-session-user', getSessionUser);
api.post('/account/delete', ifNoUser401, deleteUserToken, postDeleteAccount);
api.post(
'/account/delete',
ifNoUser401,
deleteUserToken,
deleteMsUsername,
postDeleteAccount
);
api.post(
'/account/reset-progress',
ifNoUser401,
deleteUserToken,
deleteMsUsername,
postResetProgress
);
api.post(
@@ -61,6 +72,14 @@ function bootUser(app) {
deleteUserTokenResponse
);
api.post('/user/ms-username', ifNoUser401, postMsUsername);
api.delete(
'/user/ms-username',
ifNoUser401,
deleteMsUsername,
deleteMsUsernameResponse
);
app.use(api);
}
@@ -93,8 +112,89 @@ function deleteUserTokenResponse(req, res) {
return res.send({ userToken: null });
}
function createPostMsUsername(app) {
const { MsUsername } = app.models;
return async function postMsUsername(req, res) {
// example msTranscriptUrl: https://learn.microsoft.com/en-us/users/mot01/transcript/8u6awert43q1plo
// the last part is the transcript ID
// the username is irrelevant, and retrieved from the MS API response
const { msTranscriptUrl } = req.body;
if (!msTranscriptUrl) {
return res
.status(400)
.send('Please include a Microsoft transcript URL in request');
}
const msTranscriptId = msTranscriptUrl.split('/').pop();
const msTranscriptApiUrl = `https://learn.microsoft.com/api/profiles/transcript/share/${msTranscriptId}`;
try {
const msApiRes = await fetch(msTranscriptApiUrl);
if (!msApiRes.ok) {
res.status(500);
throw new Error(
'An error occurred trying to get your Microsoft transcript'
);
}
const { userName } = await msApiRes.json();
if (!userName) {
res.status(500);
throw new Error(
'An error occured trying to link your Microsoft account'
);
}
// Don't create if username is used by another fCC account
const usernameUsed = await MsUsername.findOne({
where: { msUsername: userName }
});
if (usernameUsed) {
throw new Error('That username is already used');
}
await MsUsername.destroyAll({ userId: req.user.id });
const ttl = 900 * 24 * 60 * 60 * 1000;
const newMsUsername = await MsUsername.create({
ttl,
userId: req.user.id,
msUsername: userName
});
if (!newMsUsername?.id) {
res.status(500);
throw new Error(
'An error occured trying to link your Microsoft account'
);
}
return res.json({ msUsername: userName });
} catch (e) {
log(e);
return res.send(e.message);
}
};
}
function deleteMsUsernameResponse(req, res) {
if (!req.msUsernameDeleted) {
return res
.status(500)
.send('An error occurred trying to unlink your Microsoft username');
}
return res.send({ msUsername: null });
}
function createReadSessionUser(app) {
const { UserToken } = app.models;
const { MsUsername, UserToken } = app.models;
return async function getSessionUser(req, res, next) {
const queryUser = req.user;
@@ -113,6 +213,20 @@ function createReadSessionUser(app) {
return next(e);
}
let msUsername;
try {
const userId = queryUser?.id;
const msUser = userId
? await MsUsername.findOne({
where: { userId }
})
: null;
msUsername = msUser ? msUser.msUsername : undefined;
} catch (e) {
return next(e);
}
if (!queryUser || !queryUser.toJSON().username) {
// TODO: This should return an error status
return res.json({ user: {}, result: '' });
@@ -154,7 +268,8 @@ function createReadSessionUser(app) {
isEmailVerified: !!user.emailVerified,
...normaliseUserFields(user),
joinDate: user.id.getTimestamp(),
userToken: encodedUserToken
userToken: encodedUserToken,
msUsername
}
},
result: user.username
@@ -0,0 +1,20 @@
import debugFactory from 'debug';
const log = debugFactory('fcc:boot:user');
export function createDeleteMsUsername(app) {
const { MsUsername } = app.models;
return async function deleteMsUsername(req, res, next) {
try {
await MsUsername.destroyAll({ userId: req.user.id });
req.msUsernameDeleted = true;
} catch (e) {
req.msUsernameDeleted = false;
log(
`An error occurred deleting Microsoft username for user with id ${req.user.id}`
);
}
next();
};
}
+4
View File
@@ -43,6 +43,10 @@
"dataSource": "db",
"public": false
},
"MsUsername": {
"dataSource": "db",
"public": false
},
"RoleMapping": {
"dataSource": "db",
"public": false
@@ -0,0 +1,20 @@
{
"name": "MsUsername",
"description": "Microsoft account usernames",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "userId"
}
},
"acls": [],
"methods": {}
}
@@ -1,27 +0,0 @@
// TODO: port to new API!
const MS_LEARN_DOMAIN = 'learn.microsoft.com';
const mSLearnRegex = /^\/[^/]+\/training\/achievements\/([^/]+)$/;
export const getApiUrlFromTrophy = trophyUrlString => {
if (!trophyUrlString) return null;
let mSLearnUrl;
try {
mSLearnUrl = new URL(trophyUrlString);
} catch {
return null;
}
if (mSLearnUrl.protocol !== 'https:') return null;
if (mSLearnUrl.hostname !== MS_LEARN_DOMAIN) return null;
const match = mSLearnUrl.pathname.match(mSLearnRegex);
if (!match) return null;
const apiUrl = new URL(
`https://${MS_LEARN_DOMAIN}/api/gamestatus/achievements/${match[1]}`
);
apiUrl.searchParams.set('username', mSLearnUrl.searchParams.get('username'));
return apiUrl.href;
};
@@ -1,50 +0,0 @@
const { getApiUrlFromTrophy } = require('./ms-learn-utils');
const validApiUrl =
'https://learn.microsoft.com/api/gamestatus/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01';
const validTrophyUrl =
'https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01&sharingId=E2EF453C1F9208B';
describe('ms-learn-utils', () => {
describe('getApiUrlFromTrophy', () => {
it('should return null if the trophy url is empty', () => {
expect(getApiUrlFromTrophy('')).toBeNull();
});
it('should return null if the protocol is wrong', () => {
expect(getApiUrlFromTrophy('http://learn.microsoft.com')).toBeNull();
});
it('should return null if the domain is wrong', () => {
expect(getApiUrlFromTrophy('https://learn.microsoft.co')).toBeNull();
});
it('should return null if the path is incomplete', () => {
expect(getApiUrlFromTrophy('https://learn.microsoft.com')).toBeNull();
expect(
getApiUrlFromTrophy(
'https://learn.microsoft.com/en-us/trainin/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01'
)
).toBeNull();
});
it('should add the username as a query param', () => {
const username = 'moT01';
const url = new URL(getApiUrlFromTrophy(validTrophyUrl));
expect(url.searchParams.get('username')).toBe(username);
});
it('should only add a single query param', () => {
const url = new URL(getApiUrlFromTrophy(validTrophyUrl));
// URLSearchParams.size is only supported in Node 19+
expect([...url.searchParams.keys()].length).toBe(1);
});
it('should append the trophy path to the api url', () => {
expect(getApiUrlFromTrophy(validTrophyUrl)).toBe(validApiUrl);
});
});
});
+2
View File
@@ -84,6 +84,7 @@ exports.createPages = function createPages({ graphql, actions, reporter }) {
}
hasEditableBoundaries
id
msTrophyId
order
prerequisites {
id
@@ -289,6 +290,7 @@ exports.createSchemaCustomization = ({ actions }) => {
url: String
assignments: [String]
prerequisites: [PrerequisiteChallenge]
msTrophyId: String
}
type FileContents {
fileKey: String
+23 -3
View File
@@ -84,7 +84,10 @@
"exit": "Exit",
"finish-exam": "Finish the exam",
"finish": "Finish",
"submit-exam-results": "Submit my results"
"submit-exam-results": "Submit my results",
"verify-trophy": "Verify Trophy",
"link-account": "Link Account",
"unlink-account": "Unlink Account"
},
"landing": {
"big-heading-1": "Learn to code — for free.",
@@ -433,6 +436,18 @@
"exit": "Are you sure you want to leave the exam? You will lose any progress you have made.",
"exit-yes": "Yes, I want to leave the exam",
"exit-no": "No, I would like to continue the exam"
},
"ms": {
"link-header": "Link your Microsoft account",
"link-signin": "To complete this challenge, you must first link your Microsoft username to your freeCodeCamp account. Sign in to link your Microsoft username.",
"linked": "The Microsoft account with username \"{{ msUsername }}\" is currently linked to your freeCodeCamp account. If this is not your Microsoft username, remove the link.",
"unlinked": "To complete this challenge, you must first link your Microsoft username to your freeCodeCamp account by following these instructions:",
"link-li-1": "Using a browser where you are logged into your Microsoft account, go to <0>https://learn.microsoft.com/users/me/transcript</0>",
"link-li-2": "Find and click the \"Share link\" button.",
"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"
}
},
"donate": {
@@ -686,7 +701,6 @@
"complete-project-first": "You must complete the project first.",
"local-code-save-error": "Oops, your code did not save, your browser's local storage may be full.",
"local-code-saved": "Saved! Your code was saved to your browser's local storage.",
"ms-trophy-missing": "It looks like the trophy link you provided is not valid. Please check the link and try again.",
"timeline-private": "{{username}} has chosen to make their timeline private. They will need to make their timeline public in order for others to be able to view their certification.",
"code-saved": "Your code was saved to the database. It will be here when you return.",
"code-save-error": "An error occurred trying to save your code.",
@@ -694,7 +708,13 @@
"challenge-save-too-big": "Sorry, you cannot save your code. Your code is {{user-size}} bytes. We allow a maximum of {{max-size}} bytes. Please make your code smaller and try again or request assistance on https://forum.freecodecamp.org",
"challenge-submit-too-big": "Sorry, you cannot submit your code. Your code is {{user-size}} bytes. We allow a maximum of {{max-size}} bytes. Please make your code smaller and try again or request assistance on https://forum.freecodecamp.org",
"invalid-update-flag": "You are attempting to access forbidden resources. Please request assistance on https://forum.freecodecamp.org if this is a valid request.",
"generate-exam-error": "An error occurred trying to generate your exam."
"generate-exam-error": "An error occurred trying to generate your exam.",
"ms-trophy-err": "We were unable to verify your trophy from Microsoft's learning platform.",
"ms-trophy-verified": "Your trophy from Microsoft's learning platform was verified.",
"ms-linked": "Your Microsoft username been linked to your freeCodeCamp account.",
"ms-link-err": "An error occurred trying to link your Microsoft username to your freeCodeCamp account.",
"ms-unlinked": "The link to your Microsoft username has been removed.",
"ms-unlink-err": "An error occurred trying to remove the link to your Microsoft username."
},
"validation": {
"max-characters": "There is a maximum limit of 288 characters, you have {{charsLeft}} left",
@@ -18,7 +18,12 @@ export enum FlashMessages {
IncompleteSteps = 'flash.incomplete-steps',
LocalCodeSaved = 'flash.local-code-saved',
LocalCodeSaveError = 'flash.local-code-save-error',
MSTrophyMissing = 'flash.ms-trophy-missing',
MsLinked = 'flash.ms-linked',
MsLinkErr = 'flash.ms-link-err',
MsTrophyErr = 'flash.ms-trophy-err',
MsTrophyVerified = 'flash.ms-trophy-verified',
MsUnlinked = 'flash.ms-unlinked',
MsUnlinkErr = 'flash.ms-unlink-err',
NameNeeded = 'flash.name-needed',
None = '',
NotEligible = 'flash.not-eligible',
@@ -15,15 +15,13 @@ import {
composeValidators,
fCCValidator,
httpValidator,
pathValidator,
microsoftValidator
pathValidator
} from './form-validators';
export type FormOptions = {
ignored?: string[];
isEditorLinkAllowed?: boolean;
isLocalLinkAllowed?: boolean;
isMicrosoftLearnLink?: boolean;
required?: string[];
types?: { [key: string]: string };
placeholders?: { [key: string]: string };
@@ -42,8 +40,7 @@ function FormFields({ formFields, options }: FormFieldsProps): JSX.Element {
required = [],
types = {},
isEditorLinkAllowed = false,
isLocalLinkAllowed = false,
isMicrosoftLearnLink = false
isLocalLinkAllowed = false
} = options;
const nullOrWarning = (
@@ -72,7 +69,6 @@ function FormFields({ formFields, options }: FormFieldsProps): JSX.Element {
if (!isLocalLinkAllowed) {
validators.push(localhostValidator);
}
if (isMicrosoftLearnLink) validators.push(microsoftValidator);
const validationWarning = composeValidators(...validators)(value);
const message: string = (error ||
validationError ||
@@ -1,6 +1,5 @@
import React from 'react';
import { Trans } from 'react-i18next';
import { isMicrosoftLearnLink } from '../../../../utils/validate';
// Matches editor links for: Replit, Glitch, CodeSandbox, GitHub
const editorRegex =
@@ -20,9 +19,6 @@ function isPathRoot(urlString: string): boolean {
type Validator = (value: string) => React.ReactElement | null;
export const microsoftValidator: Validator = value =>
!isMicrosoftLearnLink(value) ? <Trans>validation.ms-learn-link</Trans> : null;
export const editorValidator: Validator = value =>
editorRegex.test(value) ? <Trans>validation.editor-url</Trans> : null;
+2 -11
View File
@@ -8,8 +8,7 @@ import {
editorValidator,
composeValidators,
fCCValidator,
httpValidator,
microsoftValidator
httpValidator
} from './form-validators';
import FormFields, { FormOptions } from './form-fields';
@@ -36,12 +35,7 @@ function validateFormValues(
formValues: FormValues,
options: FormOptions
): ValidatedValues {
const {
isEditorLinkAllowed,
isLocalLinkAllowed,
isMicrosoftLearnLink,
types
} = options;
const { isEditorLinkAllowed, isLocalLinkAllowed, types } = options;
const validatedValues: ValidatedValues = {
values: {},
errors: [],
@@ -59,9 +53,6 @@ function validateFormValues(
if (!isLocalLinkAllowed) {
validators.push(localhostValidator);
}
if (isMicrosoftLearnLink) {
validators.push(microsoftValidator);
}
const nullOrWarning = composeValidators(...validators)(value);
if (nullOrWarning) {
+4
View File
@@ -26,6 +26,10 @@ export const actionTypes = createTypes(
'startExam',
'stopExam',
'clearExamResults',
'linkMsUsername',
'unlinkMsUsername',
'setMsUsername',
'setIsProcessing',
'submitComplete',
'updateComplete',
'updateFailed',
+6
View File
@@ -102,5 +102,11 @@ export const startExam = createAction(actionTypes.startExam);
export const stopExam = createAction(actionTypes.stopExam);
export const clearExamResults = createAction(actionTypes.clearExamResults);
export const linkMsUsername = createAction(actionTypes.linkMsUsername);
export const unlinkMsUsername = createAction(actionTypes.unlinkMsUsername);
export const setMsUsername = createAction(actionTypes.setMsUsername);
export const setIsProcessing = createAction(actionTypes.setIsProcessing);
export const closeSignoutModal = createAction(actionTypes.closeSignoutModal);
export const openSignoutModal = createAction(actionTypes.openSignoutModal);
+23 -1
View File
@@ -22,6 +22,7 @@ import { actionTypes as settingsTypes } from './settings/action-types';
import { createShowCertSaga } from './show-cert-saga';
import updateCompleteEpic from './update-complete-epic';
import { createUserTokenSaga } from './user-token-saga';
import { createMsUsernameSaga } from './ms-username-saga';
const defaultFetchState = {
pending: true,
@@ -50,6 +51,7 @@ const initialState = {
completionCount: 0,
currentChallengeId: store.get(CURRENT_CHALLENGE_KEY),
examInProgress: false,
isProcessing: false,
showCert: {},
showCertFetchState: {
...defaultFetchState
@@ -88,7 +90,8 @@ export const sagas = [
...createShowCertSaga(actionTypes),
...createReportUserSaga(actionTypes),
...createUserTokenSaga(actionTypes),
...createSaveChallengeSaga(actionTypes)
...createSaveChallengeSaga(actionTypes),
...createMsUsernameSaga(actionTypes)
];
function spreadThePayloadOnUser(state, payload) {
@@ -335,6 +338,25 @@ export const reducer = handleActions(
}
};
},
[actionTypes.setMsUsername]: (state, { payload }) => {
const { appUsername } = state;
return {
...state,
user: {
...state.user,
[appUsername]: {
...state.user[appUsername],
msUsername: payload
}
}
};
},
[actionTypes.setIsProcessing]: (state, { payload }) => {
return {
...state,
isProcessing: payload
};
},
[actionTypes.updateUserToken]: (state, { payload }) => {
const { appUsername } = state;
return {
+65
View File
@@ -0,0 +1,65 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import { createFlashMessage } from '../components/Flash/redux';
import { FlashMessages } from '../components/Flash/redux/flash-messages';
import { postMsUsername, deleteMsUsername } from '../utils/ajax';
import { setMsUsername, setIsProcessing } from './actions';
const message = {
linked: {
type: 'success',
message: FlashMessages.MsLinked
},
linkErr: {
type: 'danger',
message: FlashMessages.MsLinkErr
},
unlinked: {
type: 'info',
message: FlashMessages.MsUnlinked
},
unlinkErr: {
type: 'danger',
message: FlashMessages.MsUnlinkErr
}
};
function* linkMsUsernameSaga({ payload: { msTranscriptUrl } }) {
try {
const { data } = yield call(postMsUsername, { msTranscriptUrl });
if (data && Object.prototype.hasOwnProperty.call(data, 'msUsername')) {
yield put(setMsUsername(data.msUsername));
yield put(setIsProcessing(false));
yield put(createFlashMessage(message.linked));
} else {
yield put(setIsProcessing(false));
yield put(createFlashMessage(message.linkErr));
}
} catch {
yield put(setIsProcessing(false));
yield put(createFlashMessage(message.linkErr));
}
}
function* unlinkMsUsernameSaga() {
try {
const { data } = yield call(deleteMsUsername);
if (data && Object.prototype.hasOwnProperty.call(data, 'msUsername')) {
yield put(setMsUsername(data.msUsername));
yield put(createFlashMessage(message.unlinked));
} else {
yield put(createFlashMessage(message.unlinkErr));
}
} catch {
yield put(createFlashMessage(message.unlinkErr));
}
}
export function createMsUsernameSaga(types) {
return [
takeEvery(types.linkMsUsername, linkMsUsernameSaga),
takeEvery(types.unlinkMsUsername, unlinkMsUsernameSaga)
];
}
+1
View File
@@ -123,6 +123,7 @@ export type ChallengeNode = {
owner: string;
type: string;
};
msTrophyId: string;
notes: string;
prerequisites: PrerequisiteChallenge[];
removeComments: boolean;
+8
View File
@@ -87,6 +87,14 @@ export const examInProgressSelector = state => {
export const examResultsSelector = state => userSelector(state).examResults;
export const msUsernameSelector = state => {
return userSelector(state).msUsername;
};
export const isProcessingSelector = state => {
return state[MainApp].isProcessing;
};
export const userByNameSelector = username => state => {
const { user } = state[MainApp];
// return initial state empty user empty object instead of empty
@@ -0,0 +1,12 @@
.link-ms-user-title {
text-align: center;
font-size: inherit;
}
.link-ms-user-ol li {
margin-bottom: 5px;
}
.link-ms-user-ol li:last-child {
margin-bottom: 0;
}
@@ -0,0 +1,167 @@
import React, { useState } from 'react';
import {
Button,
FormGroup,
ControlLabel,
FormControl
} from '@freecodecamp/react-bootstrap';
import { 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 {
linkMsUsername,
unlinkMsUsername,
setIsProcessing
} from '../../../redux/actions';
import {
isSignedInSelector,
msUsernameSelector,
isProcessingSelector
} from '../../../redux/selectors';
import Login from '../../../components/Header/components/login';
import './link-ms-user.css';
const mapStateToProps = createSelector(
isSignedInSelector,
msUsernameSelector,
isProcessingSelector,
(
isSignedIn: boolean,
msUsername: string | undefined | null,
isProcessing: boolean
) => ({
isSignedIn,
msUsername,
isProcessing
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
linkMsUsername,
unlinkMsUsername,
setIsProcessing
},
dispatch
);
interface LinkMsUserProps {
isSignedIn: boolean;
msUsername: string | undefined | null;
linkMsUsername: (arg0: { msTranscriptUrl: string }) => void;
unlinkMsUsername: () => void;
isProcessing: boolean;
setIsProcessing: (arg0: boolean) => void;
t: TFunction;
}
export function LinkMsUser({
isSignedIn,
msUsername,
linkMsUsername,
unlinkMsUsername,
isProcessing,
setIsProcessing,
t
}: LinkMsUserProps): JSX.Element {
const [msTranscriptUrl, setMsTranscriptUrl] = useState<string>('');
function handleLinkUsername(e: React.FormEvent) {
e.preventDefault();
setIsProcessing(true);
linkMsUsername({ msTranscriptUrl });
}
function handleInputChange(e: React.ChangeEvent<HTMLInputElement>) {
e.preventDefault();
setMsTranscriptUrl(e.target.value);
}
return !isSignedIn ? (
<>
<h2 className='link-ms-user-title'>{t('learn.ms.link-header')}</h2>
<Spacer size='small' />
<p>{t('learn.ms.link-signin')}</p>
<Login />
</>
) : (
<>
{msUsername ? (
<>
<p>{t('learn.ms.linked', { msUsername })}</p>
<Button
block={true}
bsStyle='primary'
className='btn-invert'
disabled={isProcessing}
onClick={unlinkMsUsername}
>
{t('buttons.unlink-account')}
</Button>
</>
) : (
<div>
<h2 className='link-ms-user-title'>{t('learn.ms.link-header')}</h2>
<Spacer size='small' />
<p>{t('learn.ms.unlinked')}</p>
<ol className='link-ms-user-ol'>
<li>
<Trans i18nKey='learn.ms.link-li-1'>
<a
href='https://learn.microsoft.com/users/me/transcript'
rel='noreferrer'
target='_blank'
>
placeholder
</a>
</Trans>
</li>
<li>{t('learn.ms.link-li-2')}</li>
<li>{t('learn.ms.link-li-3')}</li>
<li>{t('learn.ms.link-li-4')}</li>
<li>{t('learn.ms.link-li-5')}</li>
</ol>
<Spacer size='medium' />
<form onSubmit={handleLinkUsername}>
<FormGroup>
<ControlLabel>
<strong>{t('learn.ms.transcript-label')}</strong>
</ControlLabel>
<FormControl
type='url'
onChange={handleInputChange}
placeholder='https://learn.microsoft.com/en-us/users/username/transcript/transcriptId'
/>
</FormGroup>
<Button
disabled={isProcessing || msTranscriptUrl.length === 0}
block={true}
bsStyle='primary'
className='btn-invert'
onClick={handleLinkUsername}
>
{t('buttons.link-account')}
</Button>
</form>
</div>
)}
</>
);
}
LinkMsUser.displayName = 'LinkMsUser';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(LinkMsUser));
@@ -0,0 +1,240 @@
import { Col, Row, Button } from '@freecodecamp/react-bootstrap';
import { graphql } from 'gatsby';
import React, { Component } from 'react';
import Helmet from 'react-helmet';
import type { TFunction } from 'i18next';
import { withTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import type { Dispatch } from 'redux';
import { createSelector } from 'reselect';
import { Container } from '@freecodecamp/ui';
import Spacer from '../../../components/helpers/spacer';
import LearnLayout from '../../../components/layouts/learn';
import { ChallengeNode, ChallengeMeta } from '../../../redux/prop-types';
import ChallengeDescription from '../components/challenge-description';
import Hotkeys from '../components/hotkeys';
import ChallengeTitle from '../components/challenge-title';
import CompletionModal from '../components/completion-modal';
import {
challengeMounted,
updateChallengeMeta,
openModal,
updateSolutionFormValues,
submitChallenge
} from '../redux/actions';
import { isChallengeCompletedSelector } from '../redux/selectors';
import { setIsProcessing } from '../../../redux/actions';
import {
isProcessingSelector,
msUsernameSelector
} from '../../../redux/selectors';
import LinkMsUser from './link-ms-user';
// Redux Setup
const mapStateToProps = createSelector(
isChallengeCompletedSelector,
isProcessingSelector,
msUsernameSelector,
(
isChallengeCompleted: boolean,
isProcessing: boolean,
msUsername: string | undefined | null
) => ({
isChallengeCompleted,
isProcessing,
msUsername
})
);
const mapDispatchToProps = (dispatch: Dispatch) =>
bindActionCreators(
{
updateChallengeMeta,
challengeMounted,
updateSolutionFormValues,
openCompletionModal: () => openModal('completion'),
setIsProcessing,
submitChallenge
},
dispatch
);
// Types
interface MsTrophyProps {
challengeMounted: (arg0: string) => void;
data: { challengeNode: ChallengeNode };
isChallengeCompleted: boolean;
isProcessing: boolean;
setIsProcessing: (arg0: boolean) => void;
msUsername: string | undefined | null;
openCompletionModal: () => void;
pageContext: {
challengeMeta: ChallengeMeta;
};
submitChallenge: () => void;
t: TFunction;
updateChallengeMeta: (arg0: ChallengeMeta) => void;
}
// Component
class MsTrophy extends Component<MsTrophyProps> {
static displayName: string;
private _container: HTMLElement | null = null;
constructor(props: MsTrophyProps) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title, challengeType }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
updateChallengeMeta({
...challengeMeta,
title,
challengeType
});
challengeMounted(challengeMeta.id);
this._container?.focus();
}
componentDidUpdate(prevProps: MsTrophyProps): void {
const {
data: {
challengeNode: {
challenge: { title: prevTitle }
}
}
} = prevProps;
const {
challengeMounted,
data: {
challengeNode: {
challenge: { title: currentTitle, challengeType }
}
},
pageContext: { challengeMeta },
updateChallengeMeta
} = this.props;
if (prevTitle !== currentTitle) {
updateChallengeMeta({
...challengeMeta,
title: currentTitle,
challengeType
});
challengeMounted(challengeMeta.id);
}
}
handleSubmit = (): void => {
const { setIsProcessing, submitChallenge } = this.props;
setIsProcessing(true);
submitChallenge();
};
render() {
const {
data: {
challengeNode: {
challenge: {
title,
description,
instructions,
superBlock,
block,
translationPending
}
}
},
isChallengeCompleted,
isProcessing,
msUsername,
pageContext: {
challengeMeta: { nextChallengePath, prevChallengePath }
},
t
} = this.props;
const blockNameTitle = `${t(
`intro:${superBlock}.blocks.${block}.title`
)} - ${title}`;
return (
<Hotkeys
innerRef={(c: HTMLElement | null) => (this._container = c)}
nextChallengePath={nextChallengePath}
prevChallengePath={prevChallengePath}
>
<LearnLayout>
<Helmet
title={`${blockNameTitle} | ${t('learn.learn')} | freeCodeCamp.org`}
/>
<Container>
<Row>
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
<Spacer size='medium' />
<ChallengeTitle
isCompleted={isChallengeCompleted}
translationPending={translationPending}
>
{title}
</ChallengeTitle>
<ChallengeDescription
description={description}
instructions={instructions}
/>
<LinkMsUser />
<hr />
<Button
block={true}
bsStyle='primary'
className='btn-invert'
disabled={!msUsername || isProcessing}
onClick={this.handleSubmit}
>
{t('buttons.verify-trophy')}
</Button>
<br />
<Spacer size='medium' />
</Col>
<CompletionModal />
</Row>
</Container>
</LearnLayout>
</Hotkeys>
);
}
}
MsTrophy.displayName = 'MsTrophy';
export default connect(
mapStateToProps,
mapDispatchToProps
)(withTranslation()(MsTrophy));
export const query = graphql`
query MsTrophyChallenge($slug: String!) {
challengeNode(challenge: { fields: { slug: { eq: $slug } } }) {
challenge {
title
description
instructions
challengeType
superBlock
block
translationPending
}
}
}
`;
@@ -28,7 +28,6 @@ import { isChallengeCompletedSelector } from '../../redux/selectors';
import { getGuideUrl } from '../../utils';
import SolutionForm from '../solution-form';
import ProjectToolPanel from '../tool-panel';
import { challengeTypes } from '../../../../../../config/challenge-types';
// Redux Setup
const mapStateToProps = createSelector(
@@ -192,11 +191,9 @@ class Project extends Component<ProjectProps> {
onSubmit={this.handleSubmit}
updateSolutionForm={updateSolutionFormValues}
/>
{challengeType !== challengeTypes.msTrophyUrl && (
<ProjectToolPanel
guideUrl={getGuideUrl({ forumTopicId, title })}
/>
)}
<ProjectToolPanel
guideUrl={getGuideUrl({ forumTopicId, title })}
/>
<br />
<Spacer size='medium' />
</Col>
@@ -53,7 +53,6 @@ export class SolutionForm extends Component<SolutionFormProps> {
{ name: 'solution', label: t('learn.solution-link') },
{ name: 'githubLink', label: t('learn.github-link') }
];
const msTrophyField = [{ name: 'solution', label: t('learn.ms-link') }];
const buttonCopy = t('learn.i-completed');
@@ -64,8 +63,7 @@ export class SolutionForm extends Component<SolutionFormProps> {
},
required: ['solution'],
isEditorLinkAllowed: false,
isLocalLinkAllowed: false,
isMicrosoftLearnLink: false
isLocalLinkAllowed: false
};
let formFields = solutionField;
@@ -108,14 +106,6 @@ export class SolutionForm extends Component<SolutionFormProps> {
solutionLink = solutionLink + 'https://your-git-repo.url/files';
break;
case challengeTypes.msTrophyUrl:
formFields = msTrophyField;
options.isMicrosoftLearnLink = true;
solutionLink =
solutionLink +
'https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=you';
break;
default:
formFields = solutionField;
solutionLink =
@@ -13,7 +13,8 @@ import {
import { createFlashMessage } from '../../../components/Flash/redux';
import {
standardErrorMessage,
trophyMissingMessage
msTrophyError,
msTrophyVerified
} from '../../../utils/error-messages';
import {
challengeTypes,
@@ -23,6 +24,7 @@ import {
import { actionTypes as submitActionTypes } from '../../../redux/action-types';
import {
allowBlockDonationRequests,
setIsProcessing,
setRenderStartTime,
submitComplete,
updateComplete,
@@ -50,10 +52,13 @@ import {
} from './selectors';
function postChallenge(update, username) {
const {
payload: { challengeType }
} = update;
const saveChallenge = postUpdate$(update).pipe(
retry(3),
switchMap(({ data }) => {
const { savedChallenges, points, isTrophyMissing, examResults } = data;
const { savedChallenges, points, type, examResults } = data;
const payloadWithClientProperties = {
...omit(update.payload, ['files'])
};
@@ -66,7 +71,7 @@ function postChallenge(update, username) {
);
}
const actions = [
let actions = [
submitComplete({
submittedChallenge: {
username,
@@ -79,9 +84,13 @@ function postChallenge(update, username) {
updateComplete(),
submitChallengeComplete()
];
// TODO(Post-MVP): separate endpoint for trophy submission?
if (isTrophyMissing)
actions.push(createFlashMessage(trophyMissingMessage));
if (challengeType === challengeTypes.msTrophy && type === 'error') {
actions = [createFlashMessage(msTrophyError), submitChallengeError()];
} else if (challengeType === challengeTypes.msTrophy) {
actions.push(createFlashMessage(msTrophyVerified));
}
return of(...actions);
}),
catchError(() => of(updateFailed(update), submitChallengeError()))
@@ -176,7 +185,8 @@ const submitters = {
backend: submitBackendChallenge,
'project.frontEnd': submitProject,
'project.backEnd': submitProject,
exam: submitExam
exam: submitExam,
msTrophy: submitMsTrophy
};
function submitExam(type, state) {
@@ -197,6 +207,22 @@ function submitExam(type, state) {
return empty();
}
function submitMsTrophy(type, state) {
if (type === actionTypes.submitChallenge) {
const { id, challengeType } = challengeMetaSelector(state);
const { username } = userSelector(state);
const challengeInfo = { id, challengeType };
const update = {
endpoint: '/ms-trophy-challenge-completed',
payload: challengeInfo
};
return postChallenge(update, username);
}
return empty();
}
export default function completionEpic(action$, state$) {
return action$.pipe(
ofType(actionTypes.submitChallenge),
@@ -238,7 +264,9 @@ export default function completionEpic(action$, state$) {
action.type === submitActionTypes.submitComplete;
return submitter(type, state).pipe(
concat(of(setIsAdvancing(!lastChallengeInBlock))),
concat(
of(setIsAdvancing(!lastChallengeInBlock), setIsProcessing(false))
),
mergeMap(x =>
canAllowDonationRequest(state, x)
? of(x, allowBlockDonationRequests({ superBlock, block }))
+10
View File
@@ -271,6 +271,12 @@ export function postUserToken(): Promise<ResponseWithData<void>> {
return post('/user/user-token', {});
}
export function postMsUsername(body: {
msTranscriptUrl: string;
}): Promise<ResponseWithData<void>> {
return post('/user/ms-username', body);
}
export function postSaveChallenge(body: {
id: string;
files: ChallengeFiles;
@@ -368,3 +374,7 @@ export function putVerifyCert(
export function deleteUserToken(): Promise<ResponseWithData<void>> {
return deleteRequest('/user/user-token', {});
}
export function deleteMsUsername(): Promise<ResponseWithData<void>> {
return deleteRequest('/user/ms-username', {});
}
+7 -2
View File
@@ -5,9 +5,9 @@ export const standardErrorMessage = {
message: FlashMessages.WentWrong
};
export const trophyMissingMessage = {
export const msTrophyError = {
type: 'danger',
message: FlashMessages.MSTrophyMissing
message: FlashMessages.MsTrophyErr
};
export const reallyWeirdErrorMessage = {
@@ -24,3 +24,8 @@ export const certificateMissingErrorMessage = {
type: 'danger',
message: FlashMessages.CertificateMissing
};
export const msTrophyVerified = {
type: 'success',
message: FlashMessages.MsTrophyVerified
};
+6 -1
View File
@@ -63,7 +63,12 @@ const toneUrls = {
[FlashMessages.WrongName]: TRY_AGAIN,
[FlashMessages.WrongUpdating]: TRY_AGAIN,
[FlashMessages.WentWrong]: TRY_AGAIN,
[FlashMessages.MSTrophyMissing]: TRY_AGAIN
[FlashMessages.MsTrophyErr]: TRY_AGAIN,
[FlashMessages.MsTrophyVerified]: CHAL_COMP,
[FlashMessages.MsLinked]: CHAL_COMP,
[FlashMessages.MsLinkErr]: TRY_AGAIN,
[FlashMessages.MsUnlinked]: CHAL_COMP,
[FlashMessages.MsUnlinkErr]: TRY_AGAIN
} as const;
type ToneStates = keyof typeof toneUrls;
@@ -44,6 +44,11 @@ const exam = path.resolve(
'../../src/templates/Challenges/exam/show.tsx'
);
const msTrophy = path.resolve(
__dirname,
'../../src/templates/Challenges/ms-trophy/show.tsx'
);
const views = {
backend,
classic,
@@ -52,7 +57,8 @@ const views = {
video,
codeAlly,
odin,
exam
exam,
msTrophy
// quiz: Quiz
};
+4 -4
View File
@@ -17,7 +17,7 @@ const multifileCertProject = 14;
const theOdinProject = 15;
const colab = 16;
const exam = 17;
const msTrophyUrl = 18;
const msTrophy = 18;
const multipleChoice = 19;
const python = 20;
@@ -41,7 +41,7 @@ export const challengeTypes = {
theOdinProject,
colab,
exam,
msTrophyUrl,
msTrophy,
multipleChoice,
python
};
@@ -92,7 +92,7 @@ export const viewTypes = {
[theOdinProject]: 'odin',
[colab]: 'frontend',
[exam]: 'exam',
[msTrophyUrl]: 'frontend',
[msTrophy]: 'msTrophy',
[multipleChoice]: 'video',
[python]: 'modern'
};
@@ -120,7 +120,7 @@ export const submitTypes = {
[theOdinProject]: 'tests',
[colab]: 'project.backEnd',
[exam]: 'exam',
[msTrophyUrl]: 'project.frontEnd',
[msTrophy]: 'msTrophy',
[multipleChoice]: 'tests',
[python]: 'tests'
};
@@ -20,15 +20,15 @@ Which of the following lines of code is a valid use of the conditional operator?
## --answers--
`int value = amount >= 10? 10: 20;`
`int value = amount >= 10 ? 10 : 20;`
---
`int value = amount >= 10: 10? 20;`
`int value = amount >= 10 : 10 ? 20;`
---
`int value = amount >= 10? 10| 20;`
`int value = amount >= 10 ? 10 | 20;`
## --video-solution--
@@ -3,21 +3,11 @@ id: 647f882207d29547b3bee1c0
title: Trophy - Add Logic to C# Console Applications
challengeType: 18
dashedName: trophy-add-logic-to-c-sharp-console-applications
msTrophyId: learn.wwl.get-started-c-sharp-part-3.trophy
---
# --description--
Now that you've completed all of the "Add Logic to C# Console Applications" modules on Microsoft's learn platform, submit the URL to your trophy below.
Now that you've completed all of the "Add Logic to C# Console Applications" challenges, you should have earned a trophy for it on Microsoft's learning platform.
Follow these instructions to find your trophy URL:
1. Go to <a href="https://learn.microsoft.com/users/me/achievements#badges-section" target="_blank">https://learn.microsoft.com/users/me/achievements#badges-section</a> using a browser you are logged into Microsoft with
1. Find the trophy for "Add Logic to C# Console Applications" and click the "share" icon next to it
1. Click the "Copy URL" button
1. Paste the URL into the input below
The URL should look similar to this:
`https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-3.trophy?username=your-username&sharingId=your-sharing-id`
This trophy is required to qualify to take the certification exam.
Link your Microsoft username to your freeCodeCamp account and click the "Verify Trophy" button below to complete the challenge. This trophy is required to qualify to take the certification exam.
@@ -3,21 +3,11 @@ id: 647f87dc07d29547b3bee1bf
title: Trophy - Create and Run Simple C# Console Applications
challengeType: 18
dashedName: trophy-create-and-run-simple-c-sharp-console-applications
msTrophyId: learn.wwl.get-started-c-sharp-part-2.trophy
---
# --description--
Now that you've completed all of the "Create and Run Simple C# Console Applications" modules on Microsoft's learn platform, submit the URL to your trophy below.
Now that you've completed all of the "Create and Run Simple C# Console Applications" challenges, you should have earned a trophy for it on Microsoft's learning platform.
Follow these instructions to find your trophy URL:
1. Go to <a href="https://learn.microsoft.com/users/me/achievements#badges-section" target="_blank">https://learn.microsoft.com/users/me/achievements#badges-section</a> using a browser you are logged into Microsoft with
1. Find the trophy for "Create and Run Simple C# Console Applications" and click the "share" icon next to it
1. Click the "Copy URL" button
1. Paste the URL into the input below
The URL should look similar to this:
`https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-2.trophy?username=your-username&sharingId=your-sharing-id`
This trophy is required to qualify to take the certification exam.
Link your Microsoft username to your freeCodeCamp account and click the "Verify Trophy" button below to complete the challenge. This trophy is required to qualify to take the certification exam.
@@ -18,9 +18,11 @@ This challenge will be partially completed on Microsoft's learn platform. Follow
Given the method signature,
`void Print(string name, string number = "", bool member = false)`,
```clike
void Print(string name, string number = "", bool member = false)
```
which of the following options correctly uses named and optional arguments?
Which of the following options correctly uses named and optional arguments?
## --answers--
@@ -3,21 +3,11 @@ id: 647f877f07d29547b3bee1be
title: Trophy - Create Methods in C# Console Applications
challengeType: 18
dashedName: trophy-create-methods-in-c-sharp-console-applications
msTrophyId: learn.wwl.get-started-c-sharp-part-5.trophy
---
# --description--
Now that you've completed all of the "Create Methods in C# Console Applications" modules on Microsoft's learn platform, submit the URL to your trophy below.
Now that you've completed all of the "Create Methods in C# Console Applications" challenges, you should have earned a trophy for it on Microsoft's learning platform.
Follow these instructions to find your trophy URL:
1. Go to <a href="https://learn.microsoft.com/users/me/achievements#badges-section" target="_blank">https://learn.microsoft.com/users/me/achievements#badges-section</a> using a browser you are logged into Microsoft with
1. Find the trophy for "Create Methods in C# Console Applications" and click the "share" icon next to it
1. Click the "Copy URL" button
1. Paste the URL into the input below
The URL should look similar to this:
`https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-5.trophy?username=your-username&sharingId=your-sharing-id`
This trophy is required to qualify to take the certification exam.
Link your Microsoft username to your freeCodeCamp account and click the "Verify Trophy" button below to complete the challenge. This trophy is required to qualify to take the certification exam.
@@ -3,21 +3,11 @@ id: 647f86ff07d29547b3bee1bd
title: Trophy - Debug C# Console Applications
challengeType: 18
dashedName: trophy-debug-c-sharp-console-applications
msTrophyId: learn.wwl.get-started-c-sharp-part-6.trophy
---
# --description--
Now that you've completed all of the "Debug C# Console Applications" modules on Microsoft's learn platform, submit the URL to your trophy below.
Now that you've completed all of the "Debug C# Console Applications" challenges, you should have earned a trophy for it on Microsoft's learning platform.
Follow these instructions to find your trophy URL:
1. Go to <a href="https://learn.microsoft.com/users/me/achievements#badges-section" target="_blank">https://learn.microsoft.com/users/me/achievements#badges-section</a> using a browser you are logged into Microsoft with
1. Find the trophy for "Debug C# Console Applications" and click the "share" icon next to it
1. Click the "Copy URL" button
1. Paste the URL into the input below
The URL should look similar to this:
`https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-6.trophy?username=your-username&sharingId=your-sharing-id`
This trophy is required to qualify to take the certification exam.
Link your Microsoft username to your freeCodeCamp account and click the "Verify Trophy" button below to complete the challenge. This trophy is required to qualify to take the certification exam.
@@ -3,21 +3,11 @@ id: 647f867a07d29547b3bee1bc
title: Trophy - Work with Variable Data in C# Console Applications
challengeType: 18
dashedName: trophy-work-with-variable-data-in-c-sharp-console-applications
msTrophyId: learn.wwl.get-started-c-sharp-part-4.trophy
---
# --description--
Now that you've completed all of the "Work with Variable Data in C# Console Applications" modules on Microsoft's learn platform, submit the URL to your trophy below.
Now that you've completed all of the "Work with Variable Data in C# Console Applications" challenges, you should have earned a trophy for it on Microsoft's learning platform.
Follow these instructions to find your trophy URL:
1. Go to <a href="https://learn.microsoft.com/users/me/achievements#badges-section" target="_blank">https://learn.microsoft.com/users/me/achievements#badges-section</a> using a browser you are logged into Microsoft with
1. Find the trophy for "Work with Variable Data in C# Console Applications" and click the "share" icon next to it
1. Click the "Copy URL" button
1. Paste the URL into the input below
The URL should look similar to this:
`https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-4.trophy?username=your-username&sharingId=your-sharing-id`
This trophy is required to qualify to take the certification exam.
Link your Microsoft username to your freeCodeCamp account and click the "Verify Trophy" button below to complete the challenge. This trophy is required to qualify to take the certification exam.
@@ -18,7 +18,10 @@ This challenge will be partially completed on Microsoft's learn platform. Follow
What is wrong with the following code?
`int sophiaSum; Console.WriteLine("Sophia: " + sophiaSum);`
```clike
int sophiaSum;
Console.WriteLine("Sophia: " + sophiaSum);
```
## --answers--
@@ -18,7 +18,9 @@ This challenge will be partially completed on Microsoft's learn platform. Follow
What is the value of the following result?
`int result = 3 + 1 * 5 / 2;`
```clike
int result = 3 + 1 * 5 / 2;
```
## --answers--
@@ -20,15 +20,15 @@ Which of the following lines of code correctly uses string interpolation assumin
## --answers--
'`Console.WriteLine(@"My value: {value}");`'
`Console.WriteLine(@"My value: {value}");`
---
'`Console.WriteLine($"My value: {value}");`'
`Console.WriteLine($"My value: {value}");`
---
'`Console.WriteLine(@"My value: [value]");`'
`Console.WriteLine(@"My value: [value]");`
## --video-solution--
@@ -3,21 +3,11 @@ id: 647f85d407d29547b3bee1bb
title: Trophy - Write Your First Code Using C#
challengeType: 18
dashedName: trophy-write-your-first-code-using-c-sharp
msTrophyId: learn.wwl.get-started-c-sharp-part-1.trophy
---
# --description--
Now that you've completed all of the "Write Your First Code Using C#" modules on Microsoft's learn platform, submit the URL to your trophy below.
Now that you've completed all of the "Write Your First Code Using C#" challenges, you should have earned a trophy for it on Microsoft's learning platform.
Follow these instructions to find your trophy URL:
1. Go to <a href="https://learn.microsoft.com/users/me/achievements#badges-section" target="_blank">https://learn.microsoft.com/users/me/achievements#badges-section</a> using a browser you are logged into Microsoft with
1. Find the trophy for "Write Your First Code Using C#" and click the "share" icon next to it
1. Click the "Copy URL" button
1. Paste the URL into the input below
The URL should look similar to this:
`https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=your-username&sharingId=your-sharing-id`
This trophy is required to qualify to take the certification exam.
Link your Microsoft username to your freeCodeCamp account and click the "Verify Trophy" button below to complete the challenge. This trophy is required to qualify to take the certification exam.
+4
View File
@@ -58,6 +58,10 @@ const schema = Joi.object()
isComingSoon: Joi.bool(),
isLocked: Joi.bool(),
isPrivate: Joi.bool(),
msTrophyId: Joi.when('challengeType', {
is: [challengeTypes.msTrophy],
then: Joi.string().required()
}),
notes: Joi.string().allow(''),
order: Joi.number(),
prerequisites: Joi.when('challengeType', {
+1 -64
View File
@@ -3,8 +3,7 @@ import {
usernameTooShort,
validationSuccess,
usernameIsHttpStatusCode,
invalidCharError,
isMicrosoftLearnLink
invalidCharError
} from './validate';
function inRange(num: number, range: number[]) {
@@ -57,65 +56,3 @@ describe('isValidUsername', () => {
}
});
});
const baseUrl =
'https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-1.trophy';
describe('form-validators', () => {
describe('isMicrosoftLearnLink', () => {
it('should reject links to domains other than learn.microsoft.com', () => {
{
expect(isMicrosoftLearnLink('https://lean.microsoft.com')).toBe(false);
expect(isMicrosoftLearnLink('https://learn.microsft.com')).toBe(false);
}
});
it('should reject links without a sharingId', () => {
expect(isMicrosoftLearnLink(`${baseUrl}?username=moT01`)).toBe(false);
expect(isMicrosoftLearnLink(`${baseUrl}?username=moT01&sharingId=`)).toBe(
false
);
});
it('should reject links without a username', () => {
expect(isMicrosoftLearnLink(`${baseUrl}?sharingId=Whatever`)).toBe(false);
expect(isMicrosoftLearnLink(`${baseUrl}?sharingId=123&username=`)).toBe(
false
);
});
it('should reject links without the /training/achievements/ subpath', () => {
expect(
isMicrosoftLearnLink(
'https://learn.microsoft.com/en-us/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01&sharingId=E2EF453C1F9208B8'
)
).toBe(false);
});
it('should reject links with the wrong trophy subpath', () => {
// missing .trophy
expect(
isMicrosoftLearnLink(
'https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-1?username=moT01&sharingId=E2EF453C1F9208B8'
)
).toBe(false);
// no number
expect(
isMicrosoftLearnLink(
'https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-a.trophy?username=moT01&sharingId=E2EF453C1F9208B8'
)
).toBe(false);
});
it.each(['en-us', 'fr-fr', 'lang-country'])(
'should accept links with the %s locale',
locale => {
expect(
isMicrosoftLearnLink(
`https://learn.microsoft.com/${locale}/training/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01&sharingId=E2EF453C1F9208B8`
)
).toBe(true);
}
);
});
});
-18
View File
@@ -36,21 +36,3 @@ export const isValidUsername = (str: string): Validated => {
if (isHttpStatusCode(str)) return usernameIsHttpStatusCode;
return validationSuccess;
};
// example link: https://learn.microsoft.com/en-us/training/achievements/learn.wwl.get-started-c-sharp-part-1.trophy?username=moT01&sharingId=E2EF453C1F9208B8
export const isMicrosoftLearnLink = (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(
/^\/[^/]+\/training\/achievements\/learn\.wwl\.get-started-c-sharp-part-\d\.trophy$/
);
const hasSharingId = !!url.searchParams.get('sharingId');
const hasUsername = !!url.searchParams.get('username');
return correctDomain && correctPath && hasSharingId && hasUsername;
};