diff --git a/api-server/src/common/models/user.json b/api-server/src/common/models/user.json
index ea2ecec9c9..bc7187ab73 100644
--- a/api-server/src/common/models/user.json
+++ b/api-server/src/common/models/user.json
@@ -325,6 +325,11 @@
"type": "hasMany",
"model": "article",
"foreignKey": "externalId"
+ },
+ "webhookTokens": {
+ "type": "hasMany",
+ "model": "WebhookToken",
+ "foreignKey": "userId"
}
},
"acls": [
diff --git a/api-server/src/server/boot/certificate.js b/api-server/src/server/boot/certificate.js
index 42f3a626c5..f412a26912 100644
--- a/api-server/src/server/boot/certificate.js
+++ b/api-server/src/server/boot/certificate.js
@@ -36,7 +36,8 @@ const {
infosecV7Id,
sciCompPyV7Id,
dataAnalysisPyV7Id,
- machineLearningPyV7Id
+ machineLearningPyV7Id,
+ relationalDatabasesV8Id
} = certIds;
const log = debug('fcc:certification');
@@ -112,6 +113,10 @@ function createCertTypeIds(allChallenges) {
[certTypes.machineLearningPyV7]: getCertById(
machineLearningPyV7Id,
allChallenges
+ ),
+ [certTypes.relationalDatabasesV8]: getCertById(
+ relationalDatabasesV8Id,
+ allChallenges
)
};
}
diff --git a/api-server/src/server/boot/challenge.js b/api-server/src/server/boot/challenge.js
index 5cfea2df55..52641abacf 100644
--- a/api-server/src/server/boot/challenge.js
+++ b/api-server/src/server/boot/challenge.js
@@ -12,6 +12,7 @@ import { Observable } from 'rx';
import isNumeric from 'validator/lib/isNumeric';
import isURL from 'validator/lib/isURL';
+import { environment, deploymentEnv } from '../../../../config/env.json';
import { fixCompletedChallengeItem } from '../../common/utils';
import { getChallenges } from '../utils/get-curriculum';
import { ifNoUserSend } from '../utils/middleware';
@@ -59,6 +60,10 @@ export default async function bootChallenge(app, done) {
router.get('/challenges/current-challenge', redirectToCurrentChallenge);
+ const coderoadChallengeCompleted = createCoderoadChallengeCompleted(app);
+
+ api.post('/coderoad-challenge-completed', coderoadChallengeCompleted);
+
app.use(api);
app.use(router);
done();
@@ -78,7 +83,7 @@ export function buildUserUpdate(
_completedChallenge,
timezone
) {
- const { files } = _completedChallenge;
+ const { files, completedDate = Date.now() } = _completedChallenge;
let completedChallenge = {};
if (jsProjects.includes(challengeId)) {
completedChallenge = {
@@ -108,7 +113,7 @@ export function buildUserUpdate(
} else {
updateData.$push = {
...updateData.$push,
- progressTimestamps: Date.now()
+ progressTimestamps: completedDate
};
finalChallenge = {
...completedChallenge
@@ -328,6 +333,87 @@ function backendChallengeCompleted(req, res, next) {
.subscribe(() => {}, next);
}
+function createCoderoadChallengeCompleted(app) {
+ /* Example request coming from CodeRoad:
+ * req.body: { tutorialId: 'freeCodeCamp/learn-bash-by-building-a-boilerplate:v1.0.0' }
+ * req.headers: { coderoad-user-token: '8kFIlZiwMioY6hqqt...' }
+ */
+
+ const { WebhookToken, User } = app.models;
+
+ return async function coderoadChallengeCompleted(req, res) {
+ const { 'coderoad-user-token': userWebhookToken } = req.headers;
+ const { tutorialId } = req.body;
+
+ if (!tutorialId) return res.send(`'tutorialId' not found in request body`);
+
+ if (!userWebhookToken)
+ return res.send(`'coderoad-user-token' not found in request headers`);
+
+ const tutorialRepoPath = tutorialId?.split(':')[0];
+ const tutorialSplit = tutorialRepoPath?.split('/');
+ const tutorialOrg = tutorialSplit?.[0];
+ const tutorialRepoName = tutorialSplit?.[1];
+
+ // this allows any GH account to host the repo in development or staging
+ // .org submissions should always be from repos hosted on the fCC GH org
+ if (deploymentEnv !== 'staging' && environment !== 'development') {
+ if (tutorialOrg !== 'freeCodeCamp')
+ return res.send('Tutorial not hosted on freeCodeCamp GitHub account');
+ }
+
+ const codeRoadChallenges = getChallenges().filter(
+ challenge => challenge.challengeType === 12
+ );
+
+ // validate tutorial name is in codeRoadChallenges object
+ const tutorialInfo = codeRoadChallenges.find(tutorial =>
+ tutorial.url?.includes(tutorialRepoName)
+ );
+
+ if (!tutorialInfo) return res.send('Tutorial name is not valid');
+
+ const tutorialMongoId = tutorialInfo?.id;
+
+ try {
+ // check if webhook token is in database
+ const tokenInfo = await WebhookToken.findOne({
+ where: { id: userWebhookToken }
+ });
+
+ if (!tokenInfo) return res.send('User webhook token not found');
+
+ const { userId } = tokenInfo;
+
+ // check if user exists for webhook token
+ const user = await User.findOne({
+ where: { id: userId }
+ });
+
+ if (!user) return res.send('User for webhook token not found');
+
+ // submit challenge
+ const completedDate = Date.now();
+
+ const userUpdateInfo = buildUserUpdate(user, tutorialMongoId, {
+ id: tutorialMongoId,
+ completedDate
+ });
+
+ const updatedUser = await user.updateAttributes(
+ userUpdateInfo?.updateData
+ );
+
+ if (!updatedUser)
+ return res.send('An error occurred trying to submit the challenge');
+ } catch (e) {
+ return res.send('An error occurred trying to submit the challenge');
+ }
+
+ return res.send('Successfully submitted challenge');
+ };
+}
+
// TODO: extend tests to cover www.freecodecamp.org/language and
// chinese.freecodecamp.org
export function createRedirectToCurrentChallenge(
diff --git a/api-server/src/server/boot/user.js b/api-server/src/server/boot/user.js
index d0ed41a8dc..14ac343f7d 100644
--- a/api-server/src/server/boot/user.js
+++ b/api-server/src/server/boot/user.js
@@ -24,6 +24,8 @@ function bootUser(app) {
const getSessionUser = createReadSessionUser(app);
const postReportUserProfile = createPostReportUserProfile(app);
const postDeleteAccount = createPostDeleteAccount(app);
+ const postWebhookToken = createPostWebhookToken(app);
+ const deleteWebhookToken = createDeleteWebhookToken(app);
api.get('/account', sendNonUserToHome, getAccount);
api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial);
@@ -31,6 +33,7 @@ function bootUser(app) {
api.post('/account/delete', ifNoUser401, postDeleteAccount);
api.post('/account/reset-progress', ifNoUser401, postResetProgress);
+ api.post('/user/webhook-token', postWebhookToken);
api.post(
'/user/report-user/',
ifNoUser401,
@@ -38,14 +41,60 @@ function bootUser(app) {
postReportUserProfile
);
+ api.delete('/user/webhook-token', deleteWebhookToken);
+
app.use(api);
}
+function createPostWebhookToken(app) {
+ const { WebhookToken } = app.models;
+
+ return async function postWebhookToken(req, res) {
+ const ttl = 900 * 24 * 60 * 60 * 1000;
+ let newToken;
+
+ try {
+ await WebhookToken.destroyAll({ userId: req.user.id });
+ newToken = await WebhookToken.create({ ttl, userId: req.user.id });
+ } catch (e) {
+ return res.status(500).json({
+ type: 'danger',
+ message: 'flash.create-token-err'
+ });
+ }
+
+ return res.json(newToken?.id);
+ };
+}
+
+function createDeleteWebhookToken(app) {
+ const { WebhookToken } = app.models;
+
+ return async function deleteWebhookToken(req, res) {
+ try {
+ await WebhookToken.destroyAll({ userId: req.user.id });
+ } catch (e) {
+ return res.status(500).json({
+ type: 'danger',
+ message: 'flash.delete-token-err'
+ });
+ }
+
+ return res.json(null);
+ };
+}
+
function createReadSessionUser(app) {
const { Donation } = app.models;
- return function getSessionUser(req, res, next) {
+ return async function getSessionUser(req, res, next) {
const queryUser = req.user;
+
+ const webhookTokenArr = await queryUser.webhookTokens({
+ userId: queryUser.id
+ });
+ const webhookToken = webhookTokenArr[0]?.id;
+
const source =
queryUser &&
Observable.forkJoin(
@@ -83,7 +132,8 @@ function createReadSessionUser(app) {
isTwitter: !!user.twitter,
isWebsite: !!user.website,
...normaliseUserFields(user),
- joinDate: user.id.getTimestamp()
+ joinDate: user.id.getTimestamp(),
+ webhookToken
}
},
sessionMeta,
@@ -192,8 +242,20 @@ function postResetProgress(req, res, next) {
}
function createPostDeleteAccount(app) {
- const { User } = app.models;
- return function postDeleteAccount(req, res, next) {
+ const { User, WebhookToken } = app.models;
+ return async function postDeleteAccount(req, res, next) {
+ const {
+ user: { id: userId }
+ } = req;
+
+ try {
+ await WebhookToken.destroyAll({ userId });
+ } catch (err) {
+ log(
+ `An error occurred deleting webhook tokens for user with id ${userId} when they tried to delete their account`
+ );
+ }
+
return User.destroyById(req.user.id, function (err) {
if (err) {
return next(err);
diff --git a/api-server/src/server/middlewares/csurf.js b/api-server/src/server/middlewares/csurf.js
index 50b0cdafd3..2d7a70f248 100644
--- a/api-server/src/server/middlewares/csurf.js
+++ b/api-server/src/server/middlewares/csurf.js
@@ -14,7 +14,9 @@ export default function getCsurf() {
const { path } = req;
if (
// eslint-disable-next-line max-len
- /^\/hooks\/update-paypal$|^\/donate\/charge-stripe$/.test(path)
+ /^\/hooks\/update-paypal$|^\/donate\/charge-stripe$|^\/coderoad-challenge-completed$/.test(
+ path
+ )
) {
next();
} else {
diff --git a/api-server/src/server/middlewares/request-authorization.js b/api-server/src/server/middlewares/request-authorization.js
index d1dc353810..aabc6c194e 100644
--- a/api-server/src/server/middlewares/request-authorization.js
+++ b/api-server/src/server/middlewares/request-authorization.js
@@ -26,6 +26,7 @@ const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
const updateHooksRE = /^\/hooks\/update-paypal$/;
// note: this would be replaced by webhooks later
const donateRE = /^\/donate\/charge-stripe$/;
+const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/;
const _pathsAllowedREs = [
authRE,
@@ -40,7 +41,8 @@ const _pathsAllowedREs = [
unsubscribedRE,
unsubscribeRE,
updateHooksRE,
- donateRE
+ donateRE,
+ submitCoderoadChallengeRE
];
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
diff --git a/api-server/src/server/model-config.json b/api-server/src/server/model-config.json
index d441fa0db8..6da9b766d9 100644
--- a/api-server/src/server/model-config.json
+++ b/api-server/src/server/model-config.json
@@ -58,5 +58,9 @@
"User": {
"dataSource": "db",
"public": false
+ },
+ "WebhookToken": {
+ "dataSource": "db",
+ "public": false
}
}
diff --git a/api-server/src/server/models/webhook-token.json b/api-server/src/server/models/webhook-token.json
new file mode 100644
index 0000000000..4743866abd
--- /dev/null
+++ b/api-server/src/server/models/webhook-token.json
@@ -0,0 +1,20 @@
+{
+ "name": "WebhookToken",
+ "description": "Tokens for submitting curricula through CodeRoad",
+ "base": "AccessToken",
+ "idInjection": true,
+ "options": {
+ "validateUpsert": true
+ },
+ "properties": {},
+ "validations": [],
+ "relations": {
+ "user": {
+ "type": "belongsTo",
+ "model": "user",
+ "foreignKey": "userId"
+ }
+ },
+ "acls": [],
+ "methods": {}
+}
diff --git a/api-server/src/server/utils/certTypes.json b/api-server/src/server/utils/certTypes.json
index 5d441b9a3f..9d928781e8 100644
--- a/api-server/src/server/utils/certTypes.json
+++ b/api-server/src/server/utils/certTypes.json
@@ -13,5 +13,6 @@
"sciCompPyV7": "isSciCompPyCertV7",
"dataAnalysisPyV7": "isDataAnalysisPyCertV7",
"machineLearningPyV7": "isMachineLearningPyCertV7",
- "fullStack": "isFullStackCert"
+ "fullStack": "isFullStackCert",
+ "relationalDatabasesV8": "isRelationalDatabasesV8"
}
diff --git a/client/i18n/locales/chinese-traditional/translations.json b/client/i18n/locales/chinese-traditional/translations.json
index 6f1f6a807e..8347457ce9 100644
--- a/client/i18n/locales/chinese-traditional/translations.json
+++ b/client/i18n/locales/chinese-traditional/translations.json
@@ -466,7 +466,11 @@
"unlink-success": "你已成功取消 {{website}} 鏈接",
"provide-username": "檢查你是否提供用戶名和報告",
"report-sent": "已通過 {{email}} 向團隊發送副本報告",
- "certificate-missing": "你嘗試查看的認證不存在"
+ "certificate-missing": "你嘗試查看的認證不存在",
+ "create-token-err": "An error occurred trying to create a token",
+ "delete-token-err": "An error occurred trying to delete your token",
+ "token-created": "You have successfully created a new token.",
+ "token-deleted": "Your token has been deleted."
},
"validation": {
"max-characters": "字符數最多爲 288 個,你還可以輸入 {{charsLeft}} 個字符",
@@ -594,5 +598,18 @@
"add-code-one": "用你的複製代碼替換這兩句。",
"add-code-two": "請保留上方的 ``` 行和下方的 ``` 行",
"add-code-three": "因爲它們允許你的代碼在帖子中被正確格式化。"
+ },
+ "webhook-token": {
+ "title": "Webhook Token",
+ "create": "Create a new token",
+ "create-p1": "It looks like you don't have a webhook token. Create one to save your progress on this section",
+ "create-p2": "Create a webhook token to save your progress on the curriculum sections that use a virtual machine.",
+ "delete": "Delete my token",
+ "delete-title": "Delete My Webhook Token",
+ "delete-p1": "Your webhook token below is used to save your progress on the curriculum sections that use a virtual machine.",
+ "delete-p2": "If you suspect your token has been compromised, you can delete it to make it unusable. Progress on previously submitted lessons will not be lost.",
+ "delete-p3": "You will need to create a new token to save future progress on the curriculum sections that use a virtual machine.",
+ "no-thanks": "No thanks, I would like to keep my token",
+ "yes-please": "Yes please, I would like to delete my token"
}
}
diff --git a/client/i18n/locales/chinese/translations.json b/client/i18n/locales/chinese/translations.json
index d034726332..41eced360b 100644
--- a/client/i18n/locales/chinese/translations.json
+++ b/client/i18n/locales/chinese/translations.json
@@ -466,7 +466,11 @@
"unlink-success": "你已成功取消 {{website}} 链接",
"provide-username": "检查你是否提供用户名和报告",
"report-sent": "已通过 {{email}} 向团队发送副本报告",
- "certificate-missing": "你尝试查看的认证不存在"
+ "certificate-missing": "你尝试查看的认证不存在",
+ "create-token-err": "An error occurred trying to create a token",
+ "delete-token-err": "An error occurred trying to delete your token",
+ "token-created": "You have successfully created a new token.",
+ "token-deleted": "Your token has been deleted."
},
"validation": {
"max-characters": "字符数最多为 288 个,你还可以输入 {{charsLeft}} 个字符",
@@ -594,5 +598,18 @@
"add-code-one": "用你的复制代码替换这两句。",
"add-code-two": "请保留上方的 ``` 行和下方的 ``` 行",
"add-code-three": "因为它们允许你的代码在帖子中被正确格式化。"
+ },
+ "webhook-token": {
+ "title": "Webhook Token",
+ "create": "Create a new token",
+ "create-p1": "It looks like you don't have a webhook token. Create one to save your progress on this section",
+ "create-p2": "Create a webhook token to save your progress on the curriculum sections that use a virtual machine.",
+ "delete": "Delete my token",
+ "delete-title": "Delete My Webhook Token",
+ "delete-p1": "Your webhook token below is used to save your progress on the curriculum sections that use a virtual machine.",
+ "delete-p2": "If you suspect your token has been compromised, you can delete it to make it unusable. Progress on previously submitted lessons will not be lost.",
+ "delete-p3": "You will need to create a new token to save future progress on the curriculum sections that use a virtual machine.",
+ "no-thanks": "No thanks, I would like to keep my token",
+ "yes-please": "Yes please, I would like to delete my token"
}
}
diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json
index c52abaedb2..4c8a9f0206 100644
--- a/client/i18n/locales/english/translations.json
+++ b/client/i18n/locales/english/translations.json
@@ -466,7 +466,11 @@
"unlink-success": "You've successfully unlinked your {{website}}",
"provide-username": "Check if you have provided a username and a report",
"report-sent": "A report was sent to the team with {{email}} in copy",
- "certificate-missing": "The certification you tried to view does not exist"
+ "certificate-missing": "The certification you tried to view does not exist",
+ "create-token-err": "An error occurred trying to create a token",
+ "delete-token-err": "An error occurred trying to delete your token",
+ "token-created": "You have successfully created a new token.",
+ "token-deleted": "Your token has been deleted."
},
"validation": {
"max-characters": "There is a maximum limit of 288 characters, you have {{charsLeft}} left",
@@ -594,5 +598,18 @@
"add-code-one": "Replace these two sentences with your copied code.",
"add-code-two": "Please leave the ``` line above and the ``` line below,",
"add-code-three": "because they allow your code to properly format in the post."
+ },
+ "webhook-token": {
+ "title": "Webhook Token",
+ "create": "Create a new token",
+ "create-p1": "It looks like you don't have a webhook token. Create one to save your progress on this section",
+ "create-p2": "Create a webhook token to save your progress on the curriculum sections that use a virtual machine.",
+ "delete": "Delete my token",
+ "delete-title": "Delete My Webhook Token",
+ "delete-p1": "Your webhook token below is used to save your progress on the curriculum sections that use a virtual machine.",
+ "delete-p2": "If you suspect your token has been compromised, you can delete it to make it unusable. Progress on previously submitted lessons will not be lost.",
+ "delete-p3": "You will need to create a new token to save future progress on the curriculum sections that use a virtual machine.",
+ "no-thanks": "No thanks, I would like to keep my token",
+ "yes-please": "Yes please, I would like to delete my token"
}
}
diff --git a/client/i18n/locales/espanol/translations.json b/client/i18n/locales/espanol/translations.json
index 09bcd11826..a753b1f1b8 100644
--- a/client/i18n/locales/espanol/translations.json
+++ b/client/i18n/locales/espanol/translations.json
@@ -466,7 +466,11 @@
"unlink-success": "Has desvinculado correctamente tu {{website}}",
"provide-username": "Comprueba si has proporcionado un nombre de usuario y un reporte",
"report-sent": "Se envió un informe al equipo con {{email}} en copia",
- "certificate-missing": "La certificación que intentaste ver no existe"
+ "certificate-missing": "La certificación que intentaste ver no existe",
+ "create-token-err": "An error occurred trying to create a token",
+ "delete-token-err": "An error occurred trying to delete your token",
+ "token-created": "You have successfully created a new token.",
+ "token-deleted": "Your token has been deleted."
},
"validation": {
"max-characters": "Hay un límite máximo de 288 caracteres, te quedan {{charsLeft}}",
@@ -594,5 +598,18 @@
"add-code-one": "Reemplaza estas dos oraciones con tu código copiado.",
"add-code-two": "Por favor, deja la línea ``` arriba y la línea ``` abajo,",
"add-code-three": "porque permiten que tu código formatee correctamente en el post."
+ },
+ "webhook-token": {
+ "title": "Webhook Token",
+ "create": "Create a new token",
+ "create-p1": "It looks like you don't have a webhook token. Create one to save your progress on this section",
+ "create-p2": "Create a webhook token to save your progress on the curriculum sections that use a virtual machine.",
+ "delete": "Delete my token",
+ "delete-title": "Delete My Webhook Token",
+ "delete-p1": "Your webhook token below is used to save your progress on the curriculum sections that use a virtual machine.",
+ "delete-p2": "If you suspect your token has been compromised, you can delete it to make it unusable. Progress on previously submitted lessons will not be lost.",
+ "delete-p3": "You will need to create a new token to save future progress on the curriculum sections that use a virtual machine.",
+ "no-thanks": "No thanks, I would like to keep my token",
+ "yes-please": "Yes please, I would like to delete my token"
}
}
diff --git a/client/i18n/locales/italian/translations.json b/client/i18n/locales/italian/translations.json
index cabcad3ed4..7379fc1c62 100644
--- a/client/i18n/locales/italian/translations.json
+++ b/client/i18n/locales/italian/translations.json
@@ -466,7 +466,11 @@
"unlink-success": "Hai scollegato correttamente il tuo {{website}}",
"provide-username": "Spunta la casella se hai fornito un nome utente e un report",
"report-sent": "Un report è stato inviato al team con {{email}} in copia",
- "certificate-missing": "La certificazione che hai cercato di visualizzare non esiste"
+ "certificate-missing": "La certificazione che hai cercato di visualizzare non esiste",
+ "create-token-err": "An error occurred trying to create a token",
+ "delete-token-err": "An error occurred trying to delete your token",
+ "token-created": "You have successfully created a new token.",
+ "token-deleted": "Your token has been deleted."
},
"validation": {
"max-characters": "C'è un limite massimo di 288 caratteri, hai {{charsLeft}} rimanenti",
@@ -594,5 +598,18 @@
"add-code-one": "Sostituisci queste due frasi con il codice copiato.",
"add-code-two": "Si prega di lasciare la linea ``` sopra e la linea ``` sotto,",
"add-code-three": "perché consentono al codice di disporsi correttamente nel post."
+ },
+ "webhook-token": {
+ "title": "Webhook Token",
+ "create": "Create a new token",
+ "create-p1": "It looks like you don't have a webhook token. Create one to save your progress on this section",
+ "create-p2": "Create a webhook token to save your progress on the curriculum sections that use a virtual machine.",
+ "delete": "Delete my token",
+ "delete-title": "Delete My Webhook Token",
+ "delete-p1": "Your webhook token below is used to save your progress on the curriculum sections that use a virtual machine.",
+ "delete-p2": "If you suspect your token has been compromised, you can delete it to make it unusable. Progress on previously submitted lessons will not be lost.",
+ "delete-p3": "You will need to create a new token to save future progress on the curriculum sections that use a virtual machine.",
+ "no-thanks": "No thanks, I would like to keep my token",
+ "yes-please": "Yes please, I would like to delete my token"
}
}
diff --git a/client/i18n/locales/portuguese/translations.json b/client/i18n/locales/portuguese/translations.json
index f26352fead..89584c4766 100644
--- a/client/i18n/locales/portuguese/translations.json
+++ b/client/i18n/locales/portuguese/translations.json
@@ -466,7 +466,11 @@
"unlink-success": "Você desvinculou seu {{website}} com sucesso",
"provide-username": "Verifique se você forneceu um nome de usuário e um relatório",
"report-sent": "Um relatório foi enviado para a equipe com {{email}} em cópia",
- "certificate-missing": "A certificação que você tentou visualizar não existe"
+ "certificate-missing": "A certificação que você tentou visualizar não existe",
+ "create-token-err": "An error occurred trying to create a token",
+ "delete-token-err": "An error occurred trying to delete your token",
+ "token-created": "You have successfully created a new token.",
+ "token-deleted": "Your token has been deleted."
},
"validation": {
"max-characters": "Há um limite máximo de 288 caracteres, você tem {{charsLeft}} restante(s)",
@@ -594,5 +598,18 @@
"add-code-one": "Substitua essas duas frases pelo código que você copiou.",
"add-code-two": "Deixe as linhas com ``` acima e ``` abaixo de seu código,",
"add-code-three": "pois elas permitirão a formatação adequada de seu código na publicação."
+ },
+ "webhook-token": {
+ "title": "Webhook Token",
+ "create": "Create a new token",
+ "create-p1": "It looks like you don't have a webhook token. Create one to save your progress on this section",
+ "create-p2": "Create a webhook token to save your progress on the curriculum sections that use a virtual machine.",
+ "delete": "Delete my token",
+ "delete-title": "Delete My Webhook Token",
+ "delete-p1": "Your webhook token below is used to save your progress on the curriculum sections that use a virtual machine.",
+ "delete-p2": "If you suspect your token has been compromised, you can delete it to make it unusable. Progress on previously submitted lessons will not be lost.",
+ "delete-p3": "You will need to create a new token to save future progress on the curriculum sections that use a virtual machine.",
+ "no-thanks": "No thanks, I would like to keep my token",
+ "yes-please": "Yes please, I would like to delete my token"
}
}
diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx
index d411f67fab..335c30cbec 100644
--- a/client/src/client-only-routes/show-settings.tsx
+++ b/client/src/client-only-routes/show-settings.tsx
@@ -16,6 +16,7 @@ import Honesty from '../components/settings/honesty';
import Internet from '../components/settings/internet';
import Portfolio from '../components/settings/portfolio';
import Privacy from '../components/settings/privacy';
+import WebhookToken from '../components/settings/webhook-token';
import {
signInLoadingSelector,
userSelector,
@@ -25,7 +26,7 @@ import {
import { User } from '../redux/prop-types';
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
-const { apiLocation } = envData;
+const { apiLocation, showUpcomingChanges } = envData;
// TODO: update types for actions
interface ShowSettingsProps {
@@ -198,6 +199,8 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
username={username}
verifyCert={verifyCert}
/>
+ {showUpcomingChanges && }
+ {showUpcomingChanges && }
diff --git a/client/src/components/settings/webhook-delete-modal.tsx b/client/src/components/settings/webhook-delete-modal.tsx
new file mode 100644
index 0000000000..ec02cd457a
--- /dev/null
+++ b/client/src/components/settings/webhook-delete-modal.tsx
@@ -0,0 +1,66 @@
+import { Modal, Button } from '@freecodecamp/react-bootstrap';
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { ButtonSpacer } from '../helpers';
+
+type WebhookDeleteModalProps = {
+ onHide: () => void;
+ deleteFunction: () => void;
+ show: boolean;
+};
+
+function WebhookDeleteModal(props: WebhookDeleteModalProps): JSX.Element {
+ const { t } = useTranslation();
+ const { show, onHide, deleteFunction } = props;
+
+ return (
+
+
+
+ {t('webhook-token.delete-title')}
+
+
+
+