feat(client,api): add user tokens and route for submitting coderoad tutorials (#43304)
Co-authored-by: Nicholas Carrigan (he/him) <nhcarrigan@gmail.com> Co-authored-by: Shaun Hamilton <shauhami020@gmail.com> Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@ -325,6 +325,11 @@
|
|||||||
"type": "hasMany",
|
"type": "hasMany",
|
||||||
"model": "article",
|
"model": "article",
|
||||||
"foreignKey": "externalId"
|
"foreignKey": "externalId"
|
||||||
|
},
|
||||||
|
"webhookTokens": {
|
||||||
|
"type": "hasMany",
|
||||||
|
"model": "WebhookToken",
|
||||||
|
"foreignKey": "userId"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"acls": [
|
"acls": [
|
||||||
|
@ -36,7 +36,8 @@ const {
|
|||||||
infosecV7Id,
|
infosecV7Id,
|
||||||
sciCompPyV7Id,
|
sciCompPyV7Id,
|
||||||
dataAnalysisPyV7Id,
|
dataAnalysisPyV7Id,
|
||||||
machineLearningPyV7Id
|
machineLearningPyV7Id,
|
||||||
|
relationalDatabasesV8Id
|
||||||
} = certIds;
|
} = certIds;
|
||||||
|
|
||||||
const log = debug('fcc:certification');
|
const log = debug('fcc:certification');
|
||||||
@ -112,6 +113,10 @@ function createCertTypeIds(allChallenges) {
|
|||||||
[certTypes.machineLearningPyV7]: getCertById(
|
[certTypes.machineLearningPyV7]: getCertById(
|
||||||
machineLearningPyV7Id,
|
machineLearningPyV7Id,
|
||||||
allChallenges
|
allChallenges
|
||||||
|
),
|
||||||
|
[certTypes.relationalDatabasesV8]: getCertById(
|
||||||
|
relationalDatabasesV8Id,
|
||||||
|
allChallenges
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { Observable } from 'rx';
|
|||||||
import isNumeric from 'validator/lib/isNumeric';
|
import isNumeric from 'validator/lib/isNumeric';
|
||||||
import isURL from 'validator/lib/isURL';
|
import isURL from 'validator/lib/isURL';
|
||||||
|
|
||||||
|
import { environment, deploymentEnv } from '../../../../config/env.json';
|
||||||
import { fixCompletedChallengeItem } from '../../common/utils';
|
import { fixCompletedChallengeItem } from '../../common/utils';
|
||||||
import { getChallenges } from '../utils/get-curriculum';
|
import { getChallenges } from '../utils/get-curriculum';
|
||||||
import { ifNoUserSend } from '../utils/middleware';
|
import { ifNoUserSend } from '../utils/middleware';
|
||||||
@ -59,6 +60,10 @@ export default async function bootChallenge(app, done) {
|
|||||||
|
|
||||||
router.get('/challenges/current-challenge', redirectToCurrentChallenge);
|
router.get('/challenges/current-challenge', redirectToCurrentChallenge);
|
||||||
|
|
||||||
|
const coderoadChallengeCompleted = createCoderoadChallengeCompleted(app);
|
||||||
|
|
||||||
|
api.post('/coderoad-challenge-completed', coderoadChallengeCompleted);
|
||||||
|
|
||||||
app.use(api);
|
app.use(api);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
done();
|
done();
|
||||||
@ -78,7 +83,7 @@ export function buildUserUpdate(
|
|||||||
_completedChallenge,
|
_completedChallenge,
|
||||||
timezone
|
timezone
|
||||||
) {
|
) {
|
||||||
const { files } = _completedChallenge;
|
const { files, completedDate = Date.now() } = _completedChallenge;
|
||||||
let completedChallenge = {};
|
let completedChallenge = {};
|
||||||
if (jsProjects.includes(challengeId)) {
|
if (jsProjects.includes(challengeId)) {
|
||||||
completedChallenge = {
|
completedChallenge = {
|
||||||
@ -108,7 +113,7 @@ export function buildUserUpdate(
|
|||||||
} else {
|
} else {
|
||||||
updateData.$push = {
|
updateData.$push = {
|
||||||
...updateData.$push,
|
...updateData.$push,
|
||||||
progressTimestamps: Date.now()
|
progressTimestamps: completedDate
|
||||||
};
|
};
|
||||||
finalChallenge = {
|
finalChallenge = {
|
||||||
...completedChallenge
|
...completedChallenge
|
||||||
@ -328,6 +333,87 @@ function backendChallengeCompleted(req, res, next) {
|
|||||||
.subscribe(() => {}, 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
|
// TODO: extend tests to cover www.freecodecamp.org/language and
|
||||||
// chinese.freecodecamp.org
|
// chinese.freecodecamp.org
|
||||||
export function createRedirectToCurrentChallenge(
|
export function createRedirectToCurrentChallenge(
|
||||||
|
@ -24,6 +24,8 @@ function bootUser(app) {
|
|||||||
const getSessionUser = createReadSessionUser(app);
|
const getSessionUser = createReadSessionUser(app);
|
||||||
const postReportUserProfile = createPostReportUserProfile(app);
|
const postReportUserProfile = createPostReportUserProfile(app);
|
||||||
const postDeleteAccount = createPostDeleteAccount(app);
|
const postDeleteAccount = createPostDeleteAccount(app);
|
||||||
|
const postWebhookToken = createPostWebhookToken(app);
|
||||||
|
const deleteWebhookToken = createDeleteWebhookToken(app);
|
||||||
|
|
||||||
api.get('/account', sendNonUserToHome, getAccount);
|
api.get('/account', sendNonUserToHome, getAccount);
|
||||||
api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial);
|
api.get('/account/unlink/:social', sendNonUserToHome, getUnlinkSocial);
|
||||||
@ -31,6 +33,7 @@ function bootUser(app) {
|
|||||||
|
|
||||||
api.post('/account/delete', ifNoUser401, postDeleteAccount);
|
api.post('/account/delete', ifNoUser401, postDeleteAccount);
|
||||||
api.post('/account/reset-progress', ifNoUser401, postResetProgress);
|
api.post('/account/reset-progress', ifNoUser401, postResetProgress);
|
||||||
|
api.post('/user/webhook-token', postWebhookToken);
|
||||||
api.post(
|
api.post(
|
||||||
'/user/report-user/',
|
'/user/report-user/',
|
||||||
ifNoUser401,
|
ifNoUser401,
|
||||||
@ -38,14 +41,60 @@ function bootUser(app) {
|
|||||||
postReportUserProfile
|
postReportUserProfile
|
||||||
);
|
);
|
||||||
|
|
||||||
|
api.delete('/user/webhook-token', deleteWebhookToken);
|
||||||
|
|
||||||
app.use(api);
|
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) {
|
function createReadSessionUser(app) {
|
||||||
const { Donation } = app.models;
|
const { Donation } = app.models;
|
||||||
|
|
||||||
return function getSessionUser(req, res, next) {
|
return async function getSessionUser(req, res, next) {
|
||||||
const queryUser = req.user;
|
const queryUser = req.user;
|
||||||
|
|
||||||
|
const webhookTokenArr = await queryUser.webhookTokens({
|
||||||
|
userId: queryUser.id
|
||||||
|
});
|
||||||
|
const webhookToken = webhookTokenArr[0]?.id;
|
||||||
|
|
||||||
const source =
|
const source =
|
||||||
queryUser &&
|
queryUser &&
|
||||||
Observable.forkJoin(
|
Observable.forkJoin(
|
||||||
@ -83,7 +132,8 @@ function createReadSessionUser(app) {
|
|||||||
isTwitter: !!user.twitter,
|
isTwitter: !!user.twitter,
|
||||||
isWebsite: !!user.website,
|
isWebsite: !!user.website,
|
||||||
...normaliseUserFields(user),
|
...normaliseUserFields(user),
|
||||||
joinDate: user.id.getTimestamp()
|
joinDate: user.id.getTimestamp(),
|
||||||
|
webhookToken
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sessionMeta,
|
sessionMeta,
|
||||||
@ -192,8 +242,20 @@ function postResetProgress(req, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createPostDeleteAccount(app) {
|
function createPostDeleteAccount(app) {
|
||||||
const { User } = app.models;
|
const { User, WebhookToken } = app.models;
|
||||||
return function postDeleteAccount(req, res, next) {
|
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) {
|
return User.destroyById(req.user.id, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
|
@ -14,7 +14,9 @@ export default function getCsurf() {
|
|||||||
const { path } = req;
|
const { path } = req;
|
||||||
if (
|
if (
|
||||||
// eslint-disable-next-line max-len
|
// 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();
|
next();
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,6 +26,7 @@ const unsubscribeRE = /^\/u\/|^\/unsubscribe\/|^\/ue\//;
|
|||||||
const updateHooksRE = /^\/hooks\/update-paypal$/;
|
const updateHooksRE = /^\/hooks\/update-paypal$/;
|
||||||
// note: this would be replaced by webhooks later
|
// note: this would be replaced by webhooks later
|
||||||
const donateRE = /^\/donate\/charge-stripe$/;
|
const donateRE = /^\/donate\/charge-stripe$/;
|
||||||
|
const submitCoderoadChallengeRE = /^\/coderoad-challenge-completed$/;
|
||||||
|
|
||||||
const _pathsAllowedREs = [
|
const _pathsAllowedREs = [
|
||||||
authRE,
|
authRE,
|
||||||
@ -40,7 +41,8 @@ const _pathsAllowedREs = [
|
|||||||
unsubscribedRE,
|
unsubscribedRE,
|
||||||
unsubscribeRE,
|
unsubscribeRE,
|
||||||
updateHooksRE,
|
updateHooksRE,
|
||||||
donateRE
|
donateRE,
|
||||||
|
submitCoderoadChallengeRE
|
||||||
];
|
];
|
||||||
|
|
||||||
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
|
export function isAllowedPath(path, pathsAllowedREs = _pathsAllowedREs) {
|
||||||
|
@ -58,5 +58,9 @@
|
|||||||
"User": {
|
"User": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": false
|
"public": false
|
||||||
|
},
|
||||||
|
"WebhookToken": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
api-server/src/server/models/webhook-token.json
Normal file
20
api-server/src/server/models/webhook-token.json
Normal file
@ -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": {}
|
||||||
|
}
|
@ -13,5 +13,6 @@
|
|||||||
"sciCompPyV7": "isSciCompPyCertV7",
|
"sciCompPyV7": "isSciCompPyCertV7",
|
||||||
"dataAnalysisPyV7": "isDataAnalysisPyCertV7",
|
"dataAnalysisPyV7": "isDataAnalysisPyCertV7",
|
||||||
"machineLearningPyV7": "isMachineLearningPyCertV7",
|
"machineLearningPyV7": "isMachineLearningPyCertV7",
|
||||||
"fullStack": "isFullStackCert"
|
"fullStack": "isFullStackCert",
|
||||||
|
"relationalDatabasesV8": "isRelationalDatabasesV8"
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,11 @@
|
|||||||
"unlink-success": "你已成功取消 {{website}} 鏈接",
|
"unlink-success": "你已成功取消 {{website}} 鏈接",
|
||||||
"provide-username": "檢查你是否提供用戶名和報告",
|
"provide-username": "檢查你是否提供用戶名和報告",
|
||||||
"report-sent": "已通過 {{email}} 向團隊發送副本報告",
|
"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": {
|
"validation": {
|
||||||
"max-characters": "字符數最多爲 288 個,你還可以輸入 {{charsLeft}} 個字符",
|
"max-characters": "字符數最多爲 288 個,你還可以輸入 {{charsLeft}} 個字符",
|
||||||
@ -594,5 +598,18 @@
|
|||||||
"add-code-one": "用你的複製代碼替換這兩句。",
|
"add-code-one": "用你的複製代碼替換這兩句。",
|
||||||
"add-code-two": "請保留上方的 ``` 行和下方的 ``` 行",
|
"add-code-two": "請保留上方的 ``` 行和下方的 ``` 行",
|
||||||
"add-code-three": "因爲它們允許你的代碼在帖子中被正確格式化。"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,11 @@
|
|||||||
"unlink-success": "你已成功取消 {{website}} 链接",
|
"unlink-success": "你已成功取消 {{website}} 链接",
|
||||||
"provide-username": "检查你是否提供用户名和报告",
|
"provide-username": "检查你是否提供用户名和报告",
|
||||||
"report-sent": "已通过 {{email}} 向团队发送副本报告",
|
"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": {
|
"validation": {
|
||||||
"max-characters": "字符数最多为 288 个,你还可以输入 {{charsLeft}} 个字符",
|
"max-characters": "字符数最多为 288 个,你还可以输入 {{charsLeft}} 个字符",
|
||||||
@ -594,5 +598,18 @@
|
|||||||
"add-code-one": "用你的复制代码替换这两句。",
|
"add-code-one": "用你的复制代码替换这两句。",
|
||||||
"add-code-two": "请保留上方的 ``` 行和下方的 ``` 行",
|
"add-code-two": "请保留上方的 ``` 行和下方的 ``` 行",
|
||||||
"add-code-three": "因为它们允许你的代码在帖子中被正确格式化。"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,11 @@
|
|||||||
"unlink-success": "You've successfully unlinked your {{website}}",
|
"unlink-success": "You've successfully unlinked your {{website}}",
|
||||||
"provide-username": "Check if you have provided a username and a report",
|
"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",
|
"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": {
|
"validation": {
|
||||||
"max-characters": "There is a maximum limit of 288 characters, you have {{charsLeft}} left",
|
"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-one": "Replace these two sentences with your copied code.",
|
||||||
"add-code-two": "Please leave the ``` line above and the ``` line below,",
|
"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."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,11 @@
|
|||||||
"unlink-success": "Has desvinculado correctamente tu {{website}}",
|
"unlink-success": "Has desvinculado correctamente tu {{website}}",
|
||||||
"provide-username": "Comprueba si has proporcionado un nombre de usuario y un reporte",
|
"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",
|
"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": {
|
"validation": {
|
||||||
"max-characters": "Hay un límite máximo de 288 caracteres, te quedan {{charsLeft}}",
|
"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-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-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."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,11 @@
|
|||||||
"unlink-success": "Hai scollegato correttamente il tuo {{website}}",
|
"unlink-success": "Hai scollegato correttamente il tuo {{website}}",
|
||||||
"provide-username": "Spunta la casella se hai fornito un nome utente e un report",
|
"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",
|
"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": {
|
"validation": {
|
||||||
"max-characters": "C'è un limite massimo di 288 caratteri, hai {{charsLeft}} rimanenti",
|
"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-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-two": "Si prega di lasciare la linea ``` sopra e la linea ``` sotto,",
|
||||||
"add-code-three": "perché consentono al codice di disporsi correttamente nel post."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -466,7 +466,11 @@
|
|||||||
"unlink-success": "Você desvinculou seu {{website}} com sucesso",
|
"unlink-success": "Você desvinculou seu {{website}} com sucesso",
|
||||||
"provide-username": "Verifique se você forneceu um nome de usuário e um relatório",
|
"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",
|
"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": {
|
"validation": {
|
||||||
"max-characters": "Há um limite máximo de 288 caracteres, você tem {{charsLeft}} restante(s)",
|
"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-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-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."
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ import Honesty from '../components/settings/honesty';
|
|||||||
import Internet from '../components/settings/internet';
|
import Internet from '../components/settings/internet';
|
||||||
import Portfolio from '../components/settings/portfolio';
|
import Portfolio from '../components/settings/portfolio';
|
||||||
import Privacy from '../components/settings/privacy';
|
import Privacy from '../components/settings/privacy';
|
||||||
|
import WebhookToken from '../components/settings/webhook-token';
|
||||||
import {
|
import {
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
userSelector,
|
userSelector,
|
||||||
@ -25,7 +26,7 @@ import {
|
|||||||
import { User } from '../redux/prop-types';
|
import { User } from '../redux/prop-types';
|
||||||
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
|
import { submitNewAbout, updateUserFlag, verifyCert } from '../redux/settings';
|
||||||
|
|
||||||
const { apiLocation } = envData;
|
const { apiLocation, showUpcomingChanges } = envData;
|
||||||
|
|
||||||
// TODO: update types for actions
|
// TODO: update types for actions
|
||||||
interface ShowSettingsProps {
|
interface ShowSettingsProps {
|
||||||
@ -198,6 +199,8 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
username={username}
|
username={username}
|
||||||
verifyCert={verifyCert}
|
verifyCert={verifyCert}
|
||||||
/>
|
/>
|
||||||
|
{showUpcomingChanges && <Spacer />}
|
||||||
|
{showUpcomingChanges && <WebhookToken />}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<DangerZone />
|
<DangerZone />
|
||||||
</main>
|
</main>
|
||||||
|
66
client/src/components/settings/webhook-delete-modal.tsx
Normal file
66
client/src/components/settings/webhook-delete-modal.tsx
Normal file
@ -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 (
|
||||||
|
<Modal
|
||||||
|
aria-labelledby='modal-title'
|
||||||
|
backdrop={true}
|
||||||
|
bsSize='lg'
|
||||||
|
className='text-center'
|
||||||
|
keyboard={true}
|
||||||
|
onHide={onHide}
|
||||||
|
show={show}
|
||||||
|
>
|
||||||
|
<Modal.Header closeButton={true}>
|
||||||
|
<Modal.Title id='modal-title'>
|
||||||
|
{t('webhook-token.delete-title')}
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p>{t('webhook-token.delete-p2')}</p>
|
||||||
|
<p>{t('webhook-token.delete-p3')}</p>
|
||||||
|
<hr />
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='primary'
|
||||||
|
className='btn-invert'
|
||||||
|
onClick={props.onHide}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('webhook-token.no-thanks')}
|
||||||
|
</Button>
|
||||||
|
<ButtonSpacer />
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
className='btn-danger'
|
||||||
|
onClick={deleteFunction}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('webhook-token.yes-please')}
|
||||||
|
</Button>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WebhookDeleteModal.displayName = 'WebhookDeleteModal';
|
||||||
|
|
||||||
|
export default WebhookDeleteModal;
|
45
client/src/components/settings/webhook-token.css
Normal file
45
client/src/components/settings/webhook-token.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.webhook-panel {
|
||||||
|
border-color: var(--highlight-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-token-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background-color: var(--primary-background);
|
||||||
|
border-color: var(--highlight-background);
|
||||||
|
border-style: solid;
|
||||||
|
color: var(--highlight-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-token-input:focus {
|
||||||
|
border-color: var(--highlight-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
color: var(--highlight-background);
|
||||||
|
border-color: var(--highlight-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover,
|
||||||
|
.btn-info:focus {
|
||||||
|
color: var(--highlight-color);
|
||||||
|
background-color: var(--highlight-background);
|
||||||
|
border-color: var(--highlight-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-token .panel-heading {
|
||||||
|
color: var(--highlight-color);
|
||||||
|
background-color: var(--highlight-background);
|
||||||
|
border-radius: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-token .panel-info {
|
||||||
|
border-color: var(--highlight-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.webhook-token p {
|
||||||
|
color: var(--highlight-color);
|
||||||
|
}
|
154
client/src/components/settings/webhook-token.tsx
Normal file
154
client/src/components/settings/webhook-token.tsx
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
|
import { Button, Panel } from '@freecodecamp/react-bootstrap';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { TFunction, withTranslation } from 'react-i18next';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
import {
|
||||||
|
postWebhookToken,
|
||||||
|
deleteWebhookToken,
|
||||||
|
webhookTokenSelector
|
||||||
|
} from '../../redux';
|
||||||
|
import { ButtonSpacer, FullWidthRow, Spacer } from '../helpers';
|
||||||
|
import WebhookDeleteModal from './webhook-delete-modal';
|
||||||
|
|
||||||
|
import './webhook-token.css';
|
||||||
|
|
||||||
|
type WebhookTokenProps = {
|
||||||
|
deleteWebhookToken: () => void;
|
||||||
|
isSuperBlockPage?: boolean;
|
||||||
|
postWebhookToken: () => void;
|
||||||
|
t: TFunction;
|
||||||
|
webhookToken: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WebhookTokenState = {
|
||||||
|
webhookDeleteModal: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
webhookTokenSelector,
|
||||||
|
(webhookToken: string | null) => ({
|
||||||
|
webhookToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
postWebhookToken,
|
||||||
|
deleteWebhookToken
|
||||||
|
};
|
||||||
|
|
||||||
|
class WebhookToken extends Component<WebhookTokenProps, WebhookTokenState> {
|
||||||
|
static displayName: string;
|
||||||
|
constructor(props: WebhookTokenProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
webhookDeleteModal: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.createToken = this.createToken.bind(this);
|
||||||
|
this.deleteToken = this.deleteToken.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
createToken = () => {
|
||||||
|
this.props.postWebhookToken();
|
||||||
|
};
|
||||||
|
|
||||||
|
deleteToken = () => {
|
||||||
|
this.props.deleteWebhookToken();
|
||||||
|
this.toggleWebhookDeleteModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
toggleWebhookDeleteModal = () => {
|
||||||
|
return this.setState(state => ({
|
||||||
|
...state,
|
||||||
|
webhookDeleteModal: !state.webhookDeleteModal
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { isSuperBlockPage = false, t, webhookToken = null } = this.props;
|
||||||
|
|
||||||
|
return isSuperBlockPage ? (
|
||||||
|
<>
|
||||||
|
{!webhookToken && (
|
||||||
|
<div className='alert alert-info'>
|
||||||
|
<p>{t('webhook-token.create-p1')}</p>
|
||||||
|
<Spacer />
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
onClick={() => this.createToken()}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('webhook-token.create')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className='webhook-token text-center'>
|
||||||
|
<FullWidthRow>
|
||||||
|
<Panel className='webhook-panel'>
|
||||||
|
<Panel.Heading>{t('webhook-token.title')}</Panel.Heading>
|
||||||
|
<Spacer />
|
||||||
|
{!webhookToken ? (
|
||||||
|
<p>{t('webhook-token.create-p2')}</p>
|
||||||
|
) : (
|
||||||
|
<p>{t('webhook-token.delete-p1')}</p>
|
||||||
|
)}
|
||||||
|
<FullWidthRow>
|
||||||
|
<input
|
||||||
|
aria-label='Webhook token'
|
||||||
|
className='webhook-token-input'
|
||||||
|
readOnly={true}
|
||||||
|
type='text'
|
||||||
|
value={webhookToken || ''}
|
||||||
|
/>
|
||||||
|
<ButtonSpacer />
|
||||||
|
{!webhookToken ? (
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
className='btn-info'
|
||||||
|
onClick={() => this.createToken()}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('webhook-token.create')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
block={true}
|
||||||
|
bsSize='lg'
|
||||||
|
bsStyle='danger'
|
||||||
|
className='btn-info'
|
||||||
|
onClick={() => this.toggleWebhookDeleteModal()}
|
||||||
|
type='button'
|
||||||
|
>
|
||||||
|
{t('webhook-token.delete')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Spacer />
|
||||||
|
</FullWidthRow>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<WebhookDeleteModal
|
||||||
|
deleteFunction={() => this.deleteToken()}
|
||||||
|
onHide={() => this.toggleWebhookDeleteModal()}
|
||||||
|
show={this.state.webhookDeleteModal}
|
||||||
|
/>
|
||||||
|
</FullWidthRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WebhookToken.displayName = 'WebhookToken';
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(withTranslation()(WebhookToken));
|
@ -29,7 +29,9 @@ export const actionTypes = createTypes(
|
|||||||
...createAsyncTypes('acceptTerms'),
|
...createAsyncTypes('acceptTerms'),
|
||||||
...createAsyncTypes('showCert'),
|
...createAsyncTypes('showCert'),
|
||||||
...createAsyncTypes('reportUser'),
|
...createAsyncTypes('reportUser'),
|
||||||
...createAsyncTypes('postChargeStripeCard')
|
...createAsyncTypes('postChargeStripeCard'),
|
||||||
|
...createAsyncTypes('postWebhookToken'),
|
||||||
|
...createAsyncTypes('deleteWebhookToken')
|
||||||
],
|
],
|
||||||
ns
|
ns
|
||||||
);
|
);
|
||||||
|
@ -18,6 +18,7 @@ import { actionTypes as settingsTypes } from './settings/action-types';
|
|||||||
import { createShowCertSaga } from './show-cert-saga';
|
import { createShowCertSaga } from './show-cert-saga';
|
||||||
import { createSoundModeSaga } from './sound-mode-saga';
|
import { createSoundModeSaga } from './sound-mode-saga';
|
||||||
import updateCompleteEpic from './update-complete-epic';
|
import updateCompleteEpic from './update-complete-epic';
|
||||||
|
import { createWebhookSaga } from './webhook-saga';
|
||||||
|
|
||||||
export const MainApp = 'app';
|
export const MainApp = 'app';
|
||||||
|
|
||||||
@ -75,7 +76,8 @@ export const sagas = [
|
|||||||
...createFetchUserSaga(actionTypes),
|
...createFetchUserSaga(actionTypes),
|
||||||
...createShowCertSaga(actionTypes),
|
...createShowCertSaga(actionTypes),
|
||||||
...createReportUserSaga(actionTypes),
|
...createReportUserSaga(actionTypes),
|
||||||
...createSoundModeSaga({ ...actionTypes, ...settingsTypes })
|
...createSoundModeSaga({ ...actionTypes, ...settingsTypes }),
|
||||||
|
...createWebhookSaga(actionTypes)
|
||||||
];
|
];
|
||||||
|
|
||||||
export const appMount = createAction(actionTypes.appMount);
|
export const appMount = createAction(actionTypes.appMount);
|
||||||
@ -167,6 +169,15 @@ export const showCert = createAction(actionTypes.showCert);
|
|||||||
export const showCertComplete = createAction(actionTypes.showCertComplete);
|
export const showCertComplete = createAction(actionTypes.showCertComplete);
|
||||||
export const showCertError = createAction(actionTypes.showCertError);
|
export const showCertError = createAction(actionTypes.showCertError);
|
||||||
|
|
||||||
|
export const postWebhookToken = createAction(actionTypes.postWebhookToken);
|
||||||
|
export const postWebhookTokenComplete = createAction(
|
||||||
|
actionTypes.postWebhookTokenComplete
|
||||||
|
);
|
||||||
|
export const deleteWebhookToken = createAction(actionTypes.deleteWebhookToken);
|
||||||
|
export const deleteWebhookTokenComplete = createAction(
|
||||||
|
actionTypes.deleteWebhookTokenComplete
|
||||||
|
);
|
||||||
|
|
||||||
export const updateCurrentChallengeId = createAction(
|
export const updateCurrentChallengeId = createAction(
|
||||||
actionTypes.updateCurrentChallengeId
|
actionTypes.updateCurrentChallengeId
|
||||||
);
|
);
|
||||||
@ -230,6 +241,10 @@ export const shouldRequestDonationSelector = state => {
|
|||||||
return completionCount >= 3;
|
return completionCount >= 3;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const webhookTokenSelector = state => {
|
||||||
|
return userSelector(state).webhookToken;
|
||||||
|
};
|
||||||
|
|
||||||
export const userByNameSelector = username => state => {
|
export const userByNameSelector = username => state => {
|
||||||
const { user } = state[MainApp];
|
const { user } = state[MainApp];
|
||||||
// return initial state empty user empty object instead of empty
|
// return initial state empty user empty object instead of empty
|
||||||
@ -633,6 +648,32 @@ export const reducer = handleActions(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
[actionTypes.postWebhookTokenComplete]: (state, { payload }) => {
|
||||||
|
const { appUsername } = state;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[appUsername]: {
|
||||||
|
...state.user[appUsername],
|
||||||
|
webhookToken: payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[actionTypes.deleteWebhookTokenComplete]: state => {
|
||||||
|
const { appUsername } = state;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
[appUsername]: {
|
||||||
|
...state.user[appUsername],
|
||||||
|
webhookToken: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
[challengeTypes.challengeMounted]: (state, { payload }) => ({
|
[challengeTypes.challengeMounted]: (state, { payload }) => ({
|
||||||
...state,
|
...state,
|
||||||
currentChallengeId: payload
|
currentChallengeId: payload
|
||||||
|
60
client/src/redux/webhook-saga.js
Normal file
60
client/src/redux/webhook-saga.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
import { createFlashMessage } from '../components/Flash/redux';
|
||||||
|
import { postWebhookToken, deleteWebhookToken } from '../utils/ajax';
|
||||||
|
import { postWebhookTokenComplete, deleteWebhookTokenComplete } from '.';
|
||||||
|
|
||||||
|
const message = {
|
||||||
|
created: {
|
||||||
|
type: 'success',
|
||||||
|
message: 'flash.token-created'
|
||||||
|
},
|
||||||
|
createErr: {
|
||||||
|
type: 'danger',
|
||||||
|
message: 'flash.create-token-err'
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
type: 'info',
|
||||||
|
message: 'flash.token-deleted'
|
||||||
|
},
|
||||||
|
deleteErr: {
|
||||||
|
type: 'danger',
|
||||||
|
message: 'flash.delete-token-err'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function* postWebhookTokenSaga() {
|
||||||
|
try {
|
||||||
|
const response = yield call(postWebhookToken);
|
||||||
|
|
||||||
|
if (response?.message) {
|
||||||
|
yield put(createFlashMessage(response));
|
||||||
|
} else {
|
||||||
|
yield put(postWebhookTokenComplete(response));
|
||||||
|
yield put(createFlashMessage(message.created));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put(createFlashMessage(message.createErr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* deleteWebhookTokenSaga() {
|
||||||
|
try {
|
||||||
|
const response = yield call(deleteWebhookToken);
|
||||||
|
|
||||||
|
if (response?.message) {
|
||||||
|
yield put(createFlashMessage(response));
|
||||||
|
} else {
|
||||||
|
yield put(deleteWebhookTokenComplete());
|
||||||
|
yield put(createFlashMessage(message.deleted));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
yield put(createFlashMessage(message.deleteErr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createWebhookSaga(types) {
|
||||||
|
return [
|
||||||
|
takeEvery(types.postWebhookToken, postWebhookTokenSaga),
|
||||||
|
takeEvery(types.deleteWebhookToken, deleteWebhookTokenSaga)
|
||||||
|
];
|
||||||
|
}
|
@ -7,13 +7,22 @@ import Helmet from 'react-helmet';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import type { Dispatch } from 'redux';
|
import type { Dispatch } from 'redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
// Local Utilities
|
// Local Utilities
|
||||||
import LearnLayout from '../../../components/layouts/learn';
|
import LearnLayout from '../../../components/layouts/learn';
|
||||||
|
import { webhookTokenSelector } from '../../../redux';
|
||||||
import { ChallengeNode, ChallengeMeta } from '../../../redux/prop-types';
|
import { ChallengeNode, ChallengeMeta } from '../../../redux/prop-types';
|
||||||
import { updateChallengeMeta, challengeMounted } from '../redux';
|
import { updateChallengeMeta, challengeMounted } from '../redux';
|
||||||
// Redux
|
// Redux
|
||||||
const mapStateToProps = () => ({});
|
|
||||||
|
const mapStateToProps = createSelector(
|
||||||
|
webhookTokenSelector,
|
||||||
|
(webhookToken: string | null) => ({
|
||||||
|
webhookToken
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch: Dispatch) =>
|
const mapDispatchToProps = (dispatch: Dispatch) =>
|
||||||
bindActionCreators(
|
bindActionCreators(
|
||||||
{
|
{
|
||||||
@ -30,6 +39,7 @@ interface ShowCodeAllyProps {
|
|||||||
challengeMeta: ChallengeMeta;
|
challengeMeta: ChallengeMeta;
|
||||||
};
|
};
|
||||||
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
updateChallengeMeta: (arg0: ChallengeMeta) => void;
|
||||||
|
webhookToken: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component
|
// Component
|
||||||
@ -48,10 +58,19 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
title,
|
data: {
|
||||||
fields: { blockName },
|
challengeNode: {
|
||||||
url
|
title,
|
||||||
} = this.props.data.challengeNode;
|
fields: { blockName },
|
||||||
|
url
|
||||||
|
}
|
||||||
|
},
|
||||||
|
webhookToken = null
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const envVariables = webhookToken
|
||||||
|
? `&envVariables=CODEROAD_WEBHOOK_TOKEN=${webhookToken}`
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LearnLayout>
|
<LearnLayout>
|
||||||
@ -60,7 +79,7 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
|
|||||||
className='codeally-frame'
|
className='codeally-frame'
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||||
sandbox='allow-modals allow-forms allow-popups allow-scripts allow-same-origin'
|
sandbox='allow-modals allow-forms allow-popups allow-scripts allow-same-origin'
|
||||||
src={`https://codeally.io/embed/?repoUrl=${url}`}
|
src={`https://codeally.io/embed/?repoUrl=${url}${envVariables}`}
|
||||||
title='Editor'
|
title='Editor'
|
||||||
/>
|
/>
|
||||||
</LearnLayout>
|
</LearnLayout>
|
||||||
|
@ -14,6 +14,7 @@ import DonateModal from '../../components/Donation/DonationModal';
|
|||||||
import Login from '../../components/Header/components/Login';
|
import Login from '../../components/Header/components/Login';
|
||||||
import Map from '../../components/Map';
|
import Map from '../../components/Map';
|
||||||
import { Spacer } from '../../components/helpers';
|
import { Spacer } from '../../components/helpers';
|
||||||
|
import WebhookToken from '../../components/settings/webhook-token';
|
||||||
import {
|
import {
|
||||||
currentChallengeIdSelector,
|
currentChallengeIdSelector,
|
||||||
userFetchStateSelector,
|
userFetchStateSelector,
|
||||||
@ -187,6 +188,9 @@ const SuperBlockIntroductionPage = (props: SuperBlockProp) => {
|
|||||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||||
<Spacer size={2} />
|
<Spacer size={2} />
|
||||||
<SuperBlockIntro superBlock={superBlock} />
|
<SuperBlockIntro superBlock={superBlock} />
|
||||||
|
{superBlock === 'relational-databases' && isSignedIn && (
|
||||||
|
<WebhookToken isSuperBlockPage={true} />
|
||||||
|
)}
|
||||||
<Spacer size={2} />
|
<Spacer size={2} />
|
||||||
<h2 className='text-center big-subheading'>
|
<h2 className='text-center big-subheading'>
|
||||||
{t(`intro:misc-text.courses`)}
|
{t(`intro:misc-text.courses`)}
|
||||||
|
@ -35,8 +35,12 @@ function put<T = void>(path: string, body: unknown): Promise<T> {
|
|||||||
return request('PUT', path, body);
|
return request('PUT', path, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function deleteRequest<T = void>(path: string, body: unknown): Promise<T> {
|
||||||
|
return request('DELETE', path, body);
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(
|
async function request<T>(
|
||||||
method: 'POST' | 'PUT',
|
method: 'POST' | 'PUT' | 'DELETE',
|
||||||
path: string,
|
path: string,
|
||||||
body: unknown
|
body: unknown
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
@ -207,6 +211,10 @@ export function postResetProgress(): Promise<void> {
|
|||||||
return post('/account/reset-progress', {});
|
return post('/account/reset-progress', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postWebhookToken(): Promise<void> {
|
||||||
|
return post('/user/webhook-token', {});
|
||||||
|
}
|
||||||
|
|
||||||
/** PUT **/
|
/** PUT **/
|
||||||
|
|
||||||
interface MyAbout {
|
interface MyAbout {
|
||||||
@ -249,3 +257,8 @@ export function putUserUpdateEmail(email: string): Promise<void> {
|
|||||||
export function putVerifyCert(certSlug: string): Promise<void> {
|
export function putVerifyCert(certSlug: string): Promise<void> {
|
||||||
return put('/certificate/verify', { certSlug });
|
return put('/certificate/verify', { certSlug });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** DELETE **/
|
||||||
|
export function deleteWebhookToken(): Promise<void> {
|
||||||
|
return deleteRequest('/user/webhook-token', {});
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user